|
1 | 1 | import { |
2 | 2 | DefaultUserOperationEditProperties, |
| 3 | + DefaultUserOperationRetimePiece, |
3 | 4 | DefaultUserOperations, |
4 | 5 | DefaultUserOperationsTypes, |
5 | 6 | IngestChangeType, |
@@ -106,11 +107,142 @@ async function applyUserOperation( |
106 | 107 | case DefaultUserOperationsTypes.UPDATE_PROPS: |
107 | 108 | processUpdateProps(context, mutableIngestRundown, changes.operationTarget, changes) |
108 | 109 | break |
| 110 | + // Handle drag and drop retime operations: |
| 111 | + case DefaultUserOperationsTypes.RETIME_PIECE: |
| 112 | + processRetimePiece(context, mutableIngestRundown, changes.operationTarget, changes) |
| 113 | + break |
109 | 114 | default: |
110 | 115 | context.logWarning(`Unknown operation: ${changes.operation.id}`) |
111 | 116 | } |
112 | 117 | } |
113 | 118 |
|
| 119 | +/** |
| 120 | + * Process piece retiming operations from the UI. |
| 121 | + * |
| 122 | + * This function handles drag-and-drop retime operations from the Sofie UI. |
| 123 | + * |
| 124 | + * It updates the piece timing in the ingest data structure based on the new inPoint provided and |
| 125 | + * locks the segment/part to prevent NRCS updates. |
| 126 | + * |
| 127 | + * @param context - The ingest data processing context |
| 128 | + * @param mutableIngestRundown - The mutable rundown being processed |
| 129 | + * @param operationTarget - Target containing partExternalId and pieceExternalId |
| 130 | + * @param changes - The user operation change containing timing information |
| 131 | + */ |
| 132 | +function processRetimePiece( |
| 133 | + context: IProcessIngestDataContext, |
| 134 | + mutableIngestRundown: BlueprintMutableIngestRundown, |
| 135 | + operationTarget: UserOperationTarget, |
| 136 | + changes: UserOperationChange<BlueprintsUserOperations | DefaultUserOperations> |
| 137 | +) { |
| 138 | + // Extract piece timing information from the operation |
| 139 | + const operation = changes.operation as DefaultUserOperationRetimePiece |
| 140 | + |
| 141 | + context.logDebug('Processing piece retime operation: ' + JSON.stringify(changes, null, 2)) |
| 142 | + |
| 143 | + // Ensure we have the required identifiers |
| 144 | + if (!operationTarget.partExternalId || !operationTarget.pieceExternalId) { |
| 145 | + context.logError('Retime piece operation missing part or piece external ID') |
| 146 | + return |
| 147 | + } |
| 148 | + |
| 149 | + // Find the part containing the piece |
| 150 | + const partAndSegment = mutableIngestRundown.findPartAndSegment(operationTarget.partExternalId) |
| 151 | + const { part, segment } = partAndSegment || {} |
| 152 | + |
| 153 | + if (!part?.payload) { |
| 154 | + context.logError(`Part not found for retime operation: ${operationTarget.partExternalId}`) |
| 155 | + return |
| 156 | + } |
| 157 | + |
| 158 | + if (!segment) { |
| 159 | + context.logError(`Segment not found for retime operation: ${operationTarget.segmentExternalId}`) |
| 160 | + return |
| 161 | + } |
| 162 | + |
| 163 | + // Parse the part payload to access pieces |
| 164 | + const partPayload: any = part.payload |
| 165 | + if (!partPayload.pieces || !Array.isArray(partPayload.pieces)) { |
| 166 | + context.logError(`Part has no pieces array: ${operationTarget.partExternalId}`) |
| 167 | + return |
| 168 | + } |
| 169 | + |
| 170 | + context.logDebug('Original partPayload: ' + JSON.stringify(partPayload, null, 2)) |
| 171 | + |
| 172 | + // Find the specific piece to retime |
| 173 | + const pieceIndex = partPayload.pieces.findIndex((piece: any) => piece.id === operationTarget.pieceExternalId) |
| 174 | + if (pieceIndex === -1) { |
| 175 | + context.logError(`Piece not found for retime: ${operationTarget.pieceExternalId}`) |
| 176 | + return |
| 177 | + } |
| 178 | + |
| 179 | + const piece = partPayload.pieces[pieceIndex] |
| 180 | + const originalTime = piece.objectTime || 0 |
| 181 | + |
| 182 | + // Extract new timing from operation payload |
| 183 | + const payload = operation.payload || {} |
| 184 | + const newTime = payload.inPoint / 1000 // Convert milliseconds to seconds for comparison |
| 185 | + |
| 186 | + // Example payload structure for retime operations: |
| 187 | + // "payload": { |
| 188 | + // "segmentExternalId": "d26d22e2-4f4e-4d31-a0ca-de6f37ff9b3f", |
| 189 | + // "partExternalId": "42077925-8d15-4a5d-abeb-a445ccee2984", |
| 190 | + // "inPoint": 1061.4136732329084 |
| 191 | + // } |
| 192 | + |
| 193 | + // Handle different retime operation types |
| 194 | + if (payload.inPoint === undefined) { |
| 195 | + context.logError('Retime piece operation missing inPoint in payload') |
| 196 | + return |
| 197 | + } |
| 198 | + |
| 199 | + // Check if there are any unknown values in the payload that need to be handled apart from inPoint, segmentExternalId, partExternalId |
| 200 | + const knownKeys = ['inPoint', 'segmentExternalId', 'partExternalId'] |
| 201 | + const unknownKeys = Object.keys(payload).filter((key) => !knownKeys.includes(key)) |
| 202 | + if (unknownKeys.length > 0) { |
| 203 | + context.logWarning(`Retime piece operation has unknown keys in payload: ${unknownKeys.join(', ')}`) |
| 204 | + } |
| 205 | + |
| 206 | + // Check if there are actually changes to apply |
| 207 | + const timeDifference = Math.abs(newTime - originalTime) |
| 208 | + if (timeDifference < 0.005) { |
| 209 | + // Less than 5ms difference - consider it unchanged |
| 210 | + context.logDebug( |
| 211 | + `No significant timing changes needed for piece ${operationTarget.pieceExternalId} (${timeDifference}s difference)` |
| 212 | + ) |
| 213 | + return |
| 214 | + } |
| 215 | + |
| 216 | + // Apply the retime changes to the ingest data structure |
| 217 | + // Note: Ingest pieces use objectTime, not enable.start |
| 218 | + const updatedPiece = { |
| 219 | + ...piece, |
| 220 | + objectTime: newTime, |
| 221 | + } |
| 222 | + |
| 223 | + // Lock segment to prevent NRCS updates: |
| 224 | + segment.setUserEditState(BlueprintUserOperationTypes.LOCK_SEGMENT_NRCS_UPDATES, true) |
| 225 | + |
| 226 | + // Mark both segment and part as user-edited |
| 227 | + segment.setUserEditState(BlueprintUserOperationTypes.USER_EDITED, true) |
| 228 | + part.setUserEditState(BlueprintUserOperationTypes.USER_EDITED, true) |
| 229 | + |
| 230 | + // Update the piece in the part payload |
| 231 | + partPayload.pieces[pieceIndex] = updatedPiece |
| 232 | + |
| 233 | + // Mark the part as modified using replacePayload |
| 234 | + part.replacePayload(partPayload) |
| 235 | + |
| 236 | + // Store the retime operation as a user edit state |
| 237 | + // Each retime gets a unique key to ensure state changes trigger persistence |
| 238 | + // This is a horrible hack. |
| 239 | + // segment.setUserEditState(pieceRetimeKey, true) |
| 240 | + const pieceRetimeKey = `${operation.id}_${operationTarget.pieceExternalId}_${Date.now()}` |
| 241 | + part.setUserEditState(pieceRetimeKey, true) |
| 242 | + |
| 243 | + context.logDebug(`Marked segment and part as user-edited and created unique retime state: ${pieceRetimeKey}`) |
| 244 | +} |
| 245 | + |
114 | 246 | function processUpdateProps( |
115 | 247 | context: IProcessIngestDataContext, |
116 | 248 | mutableIngestRundown: BlueprintMutableIngestRundown, |
@@ -218,6 +350,7 @@ function updatePieceProps( |
218 | 350 |
|
219 | 351 | const lifespan = operation.payload.globalProperties['lifespan'] |
220 | 352 | context.logDebug('Update piece ' + operationTarget.pieceExternalId + ': ' + lifespan) |
| 353 | + // TODO: Do we actually update anything here? We just seem to log. |
221 | 354 | } |
222 | 355 |
|
223 | 356 | function changeSource( |
|
0 commit comments