Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/blueprints/src/base/showstyle/helpers/graphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers
import { getClipPlayerInput } from './clips.js'
import { createVisionMixerObjects } from './visionMixer.js'
import { TimelineBlueprintExt } from '../../studio/customTypes.js'
import { createPieceUserEditOperations } from './userEditOperations.js'

export interface GraphicsResult {
pieces: IBlueprintPiece[]
Expand Down Expand Up @@ -85,6 +86,7 @@ function parseGraphic(config: StudioConfig, object: GraphicObject | SteppedGraph
lifespan,
sourceLayerId: sourceLayer,
outputLayerId: getOutputLayerForSourceLayer(sourceLayer),
userEditOperations: createPieceUserEditOperations(),
content: {
timelineObjects: getGraphicTlObject(config, object, false),

Expand Down
2 changes: 2 additions & 0 deletions packages/blueprints/src/base/showstyle/helpers/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@sofie-automation/blueprints-integration'
import { literal } from '../../../common/util.js'
import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers.js'
import { createPieceUserEditOperations } from './userEditOperations.js'

function getFirstWords(input: string): string {
const firstWordsMatch = (input + '').match(/^([\S]*[^\n]){1,3}/)
Expand Down Expand Up @@ -40,6 +41,7 @@ export function createScriptPiece(script: string | undefined, extId: string): IB
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.Script),
pieceType: IBlueprintPieceType.InTransition,
lifespan: PieceLifespan.WithinPart,
userEditOperations: createPieceUserEditOperations(),
privateData: {
source: 'script',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
DefaultUserOperationsTypes,
UserEditingType,
UserEditingDefinition,
} from '@sofie-automation/blueprints-integration'

/**
* Creates standard user edit operations for pieces that support retiming
* @returns Array of user edit operations including retime functionality
*/
export function createPieceUserEditOperations(): UserEditingDefinition[] {
return [
{
type: UserEditingType.SOFIE,
id: DefaultUserOperationsTypes.RETIME_PIECE,
},
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getSourceInfoFromRaw } from '../helpers/sources.js'
import { createVisionMixerObjects } from '../helpers/visionMixer.js'
import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers.js'
import { parseConfig } from '../helpers/config.js'
import { createPieceUserEditOperations } from '../helpers/userEditOperations.js'

export function generateCameraPart(context: PartContext, part: PartProps<CameraProps>): BlueprintResultPart {
const config = parseConfig(context).studio
Expand All @@ -29,6 +30,7 @@ export function generateCameraPart(context: PartContext, part: PartProps<CameraP
lifespan: PieceLifespan.WithinPart,
sourceLayerId: SourceLayer.Camera,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.Camera),
userEditOperations: createPieceUserEditOperations(),
content: {
timelineObjects: [...createVisionMixerObjects(config, sourceInfo.input), audioTlObj],
},
Expand Down Expand Up @@ -75,6 +77,7 @@ function addGuest(config: StudioConfig, count: number): IBlueprintPiece {
lifespan: PieceLifespan.OutOnSegmentEnd,
sourceLayerId: SourceLayer.StudioGuests,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.StudioGuests),
userEditOperations: createPieceUserEditOperations(),
content: {
timelineObjects: [audioTlObj],
},
Expand Down
3 changes: 3 additions & 0 deletions packages/blueprints/src/base/showstyle/part-adapters/dve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers
import { TimelineBlueprintExt } from '../../studio/customTypes.js'
import { VmixInputConfig } from '../../../$schemas/generated/main-studio-config.js'
import { parseConfig } from '../helpers/config.js'
import { createPieceUserEditOperations } from '../helpers/userEditOperations.js'

const SUPER_SOURCE_LATENCY = 80
const SUPER_SOURCE_INPUT = 6000
Expand Down Expand Up @@ -134,6 +135,7 @@ export function generateDVEPart(context: PartContext, part: PartProps<DVEProps>)
lifespan: PieceLifespan.WithinPart,
sourceLayerId: SourceLayer.DVE,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.DVE),
userEditOperations: createPieceUserEditOperations(),
prerollDuration: SUPER_SOURCE_LATENCY,
content: {
...dveLayoutToContent(config, { boxes }, part.payload.inputs),
Expand Down Expand Up @@ -206,6 +208,7 @@ export function generateDVEPart(context: PartContext, part: PartProps<DVEProps>)
lifespan: PieceLifespan.OutOnSegmentEnd,
sourceLayerId: SourceLayer.DVE_RETAIN,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.DVE_RETAIN),
userEditOperations: createPieceUserEditOperations(),
prerollDuration: SUPER_SOURCE_LATENCY,
content: {
...dveLayoutToContent(config, { boxes }, part.payload.inputs),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createVisionMixerObjects } from '../helpers/visionMixer.js'
import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers.js'
import { ObjectType } from '../../../common/definitions/objects.js'
import { parseConfig } from '../helpers/config.js'
import { createPieceUserEditOperations } from '../helpers/userEditOperations.js'

export function generateRemotePart(context: PartContext, part: PartProps<RemoteProps>): BlueprintResultPart {
const config = parseConfig(context).studio
Expand All @@ -27,6 +28,7 @@ export function generateRemotePart(context: PartContext, part: PartProps<RemoteP
lifespan: PieceLifespan.WithinPart,
sourceLayerId: SourceLayer.Remote,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.Remote),
userEditOperations: createPieceUserEditOperations(),
content: {
timelineObjects: [...createVisionMixerObjects(config, sourceInfo.input), audioTlObj],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createVisionMixerObjects } from '../helpers/visionMixer.js'
import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers.js'
import { TimelineBlueprintExt } from '../../studio/customTypes.js'
import { parseConfig } from '../helpers/config.js'
import { createPieceUserEditOperations } from '../helpers/userEditOperations.js'

export function generateOpenerPart(context: PartContext, part: PartProps<TitlesProps>): BlueprintResultPart {
const config = parseConfig(context).studio
Expand All @@ -28,6 +29,7 @@ export function generateOpenerPart(context: PartContext, part: PartProps<TitlesP
name: `Titles`,
lifespan: PieceLifespan.WithinPart,
sourceLayerId: SourceLayer.Titles,
userEditOperations: createPieceUserEditOperations(),
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.Titles),

content: {
Expand Down
2 changes: 2 additions & 0 deletions packages/blueprints/src/base/showstyle/part-adapters/vo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createVisionMixerObjects } from '../helpers/visionMixer.js'
import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers.js'
import { TimelineBlueprintExt } from '../../studio/customTypes.js'
import { parseConfig } from '../helpers/config.js'
import { createPieceUserEditOperations } from '../helpers/userEditOperations.js'

export function generateVOPart(context: PartContext, part: PartProps<VOProps>): BlueprintResultPart {
const config = parseConfig(context).studio
Expand All @@ -30,6 +31,7 @@ export function generateVOPart(context: PartContext, part: PartProps<VOProps>):
lifespan: PieceLifespan.WithinPart,
sourceLayerId: SourceLayer.VO,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.VO),
userEditOperations: createPieceUserEditOperations(),

content: {
fileName: part.payload.clipProps.fileName,
Expand Down
2 changes: 2 additions & 0 deletions packages/blueprints/src/base/showstyle/part-adapters/vt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createVisionMixerObjects } from '../helpers/visionMixer.js'
import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers.js'
import { TimelineBlueprintExt } from '../../studio/customTypes.js'
import { parseConfig } from '../helpers/config.js'
import { createPieceUserEditOperations } from '../helpers/userEditOperations.js'

export function generateVTPart(context: PartContext, part: PartProps<VTProps>): BlueprintResultPart {
const config = parseConfig(context).studio
Expand All @@ -34,6 +35,7 @@ export function generateVTPart(context: PartContext, part: PartProps<VTProps>):
lifespan: PieceLifespan.WithinPart,
sourceLayerId: SourceLayer.VT,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.VT),
userEditOperations: createPieceUserEditOperations(),
abSessions: [
{
sessionName: part.payload.externalId,
Expand Down
5 changes: 3 additions & 2 deletions packages/blueprints/src/base/studio/applyConfig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function applyConfig(
allowHold: false,
allowPieceDirectPlay: false,
enableBuckets: false,
enableUserEdits: true,
enableEvaluationForm: true,
},
}
Expand Down Expand Up @@ -75,8 +76,8 @@ function generatePlayoutDevices(config: BlueprintConfig): BlueprintResultApplySt
options: literal<TSR.DeviceOptionsCasparCG>({
type: TSR.DeviceType.CASPARCG,
options: {
host: config.studio.casparcg.host,
port: config.studio.casparcg.port || 5250,
host: config.studio.casparcg?.host || '',
port: config.studio.casparcg?.port || 5250,
},
}),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
DefaultUserOperationEditProperties,
DefaultUserOperationRetimePiece,
DefaultUserOperations,
DefaultUserOperationsTypes,
IngestChangeType,
Expand Down Expand Up @@ -106,11 +107,142 @@ async function applyUserOperation(
case DefaultUserOperationsTypes.UPDATE_PROPS:
processUpdateProps(context, mutableIngestRundown, changes.operationTarget, changes)
break
// Handle drag and drop retime operations:
case DefaultUserOperationsTypes.RETIME_PIECE:
processRetimePiece(context, mutableIngestRundown, changes.operationTarget, changes)
break
default:
context.logWarning(`Unknown operation: ${changes.operation.id}`)
}
}

/**
* Process piece retiming operations from the UI.
*
* This function handles drag-and-drop retime operations from the Sofie UI.
*
* It updates the piece timing in the ingest data structure based on the new inPoint provided and
* locks the segment/part to prevent NRCS updates.
*
* @param context - The ingest data processing context
* @param mutableIngestRundown - The mutable rundown being processed
* @param operationTarget - Target containing partExternalId and pieceExternalId
* @param changes - The user operation change containing timing information
*/
function processRetimePiece(
context: IProcessIngestDataContext,
mutableIngestRundown: BlueprintMutableIngestRundown,
operationTarget: UserOperationTarget,
changes: UserOperationChange<BlueprintsUserOperations | DefaultUserOperations>
) {
// Extract piece timing information from the operation
const operation = changes.operation as DefaultUserOperationRetimePiece

context.logDebug('Processing piece retime operation: ' + JSON.stringify(changes, null, 2))

// Ensure we have the required identifiers
if (!operationTarget.partExternalId || !operationTarget.pieceExternalId) {
context.logError('Retime piece operation missing part or piece external ID')
return
}

// Find the part containing the piece
const partAndSegment = mutableIngestRundown.findPartAndSegment(operationTarget.partExternalId)
const { part, segment } = partAndSegment || {}

if (!part?.payload) {
context.logError(`Part not found for retime operation: ${operationTarget.partExternalId}`)
return
}

if (!segment) {
context.logError(`Segment not found for retime operation: ${operationTarget.segmentExternalId}`)
return
}

// Parse the part payload to access pieces
const partPayload: any = part.payload
if (!partPayload.pieces || !Array.isArray(partPayload.pieces)) {
context.logError(`Part has no pieces array: ${operationTarget.partExternalId}`)
return
}

context.logDebug('Original partPayload: ' + JSON.stringify(partPayload, null, 2))

// Find the specific piece to retime
const pieceIndex = partPayload.pieces.findIndex((piece: any) => piece.id === operationTarget.pieceExternalId)
if (pieceIndex === -1) {
context.logError(`Piece not found for retime: ${operationTarget.pieceExternalId}`)
return
}

const piece = partPayload.pieces[pieceIndex]
const originalTime = piece.objectTime || 0

// Extract new timing from operation payload
const payload = operation.payload || {}
const newTime = payload.inPoint / 1000 // Convert milliseconds to seconds for comparison

// Example payload structure for retime operations:
// "payload": {
// "segmentExternalId": "d26d22e2-4f4e-4d31-a0ca-de6f37ff9b3f",
// "partExternalId": "42077925-8d15-4a5d-abeb-a445ccee2984",
// "inPoint": 1061.4136732329084
// }

// Handle different retime operation types
if (payload.inPoint === undefined) {
context.logError('Retime piece operation missing inPoint in payload')
return
}

// Check if there are any unknown values in the payload that need to be handled apart from inPoint, segmentExternalId, partExternalId
const knownKeys = ['inPoint', 'segmentExternalId', 'partExternalId']
const unknownKeys = Object.keys(payload).filter((key) => !knownKeys.includes(key))
if (unknownKeys.length > 0) {
context.logWarning(`Retime piece operation has unknown keys in payload: ${unknownKeys.join(', ')}`)
}

// Check if there are actually changes to apply
const timeDifference = Math.abs(newTime - originalTime)
if (timeDifference < 0.005) {
// Less than 5ms difference - consider it unchanged
context.logDebug(
`No significant timing changes needed for piece ${operationTarget.pieceExternalId} (${timeDifference}s difference)`
)
return
}

// Apply the retime changes to the ingest data structure
// Note: Ingest pieces use objectTime, not enable.start
const updatedPiece = {
...piece,
objectTime: newTime,
}

// Lock segment to prevent NRCS updates:
segment.setUserEditState(BlueprintUserOperationTypes.LOCK_SEGMENT_NRCS_UPDATES, true)

// Mark both segment and part as user-edited
segment.setUserEditState(BlueprintUserOperationTypes.USER_EDITED, true)
part.setUserEditState(BlueprintUserOperationTypes.USER_EDITED, true)

// Update the piece in the part payload
partPayload.pieces[pieceIndex] = updatedPiece

// Mark the part as modified using replacePayload
part.replacePayload(partPayload)

// Store the retime operation as a user edit state
// Each retime gets a unique key to ensure state changes trigger persistence
// This is a horrible hack.
// segment.setUserEditState(pieceRetimeKey, true)
const pieceRetimeKey = `${operation.id}_${operationTarget.pieceExternalId}_${Date.now()}`
part.setUserEditState(pieceRetimeKey, true)

context.logDebug(`Marked segment and part as user-edited and created unique retime state: ${pieceRetimeKey}`)
}

function processUpdateProps(
context: IProcessIngestDataContext,
mutableIngestRundown: BlueprintMutableIngestRundown,
Expand Down Expand Up @@ -218,6 +350,7 @@ function updatePieceProps(

const lifespan = operation.payload.globalProperties['lifespan']
context.logDebug('Update piece ' + operationTarget.pieceExternalId + ': ' + lifespan)
// TODO: Do we actually update anything here? We just seem to log.
}

function changeSource(
Expand Down