Skip to main content
Version: Latest

Manipulating Ingest Data

In Sofie we receive the rundown from an NRCS in the form of the IngestRundown, IngestSegment and IngestPart types. (Source Code) These are passed into the getRundown or getSegment blueprints methods to transform them into a Rundown that Sofie can display and play.

At times it can be useful to manipulate this data before it gets passed into these methods. This wants to be done before getSegment in order to limit the scope of the re-generation needed. We could have made it so that getSegment is able to view the whole IngestRundown, but that would mean that any change to the IngestRundown would require re-generating every segment. This would be costly and could have side effects.

A new method processIngestData was added to transform the NRCSIngestRundown into a SofieIngestRundown. The types of the two are the same, so implementing the processIngestData method is optional, with the default being to pass through the NRCS rundown unchanged. (There is an exception here for MOS, which is explained below).

The basic implementation of this method which simply propogates nrcs changes is:

function processIngestData(
context: IProcessIngestDataContext,
mutableIngestRundown: MutableIngestRundown<any, any, any>,
nrcsIngestRundown: IngestRundown,
previousNrcsIngestRundown: IngestRundown | undefined,
changes: NrcsIngestChangeDetails | UserOperationChange
) {
if (changes.source === 'ingest') {
blueprintContext.defaultApplyIngestChanges(mutableIngestRundown, nrcsIngestRundown, changes)
}
}

In this method, the key part is the mutableIngestRundown which is the IngestRundown that will get used for getRundown and getSegment later. It is a class with various mutator methods which allows Sofie to cheaply check what has changed and know what needs to be regenerated. (We did consider performing deep diffs, but were concerned about the cost of diffing these very large rundown objects).
This object internally contains an IngestRundown.

The nrcsIngestRundown parameter is the full IngestRundown as seen by the NRCS. The previousNrcsIngestRundown parameter is the nrcsIngestRundown from the previous call. This is to allow you to perform any comparisons between the data that may be useful.

The changes object is a structure that defines what the NRCS provided changes for. The changes have already been applied onto the nrcsIngestRundown, this provides a description of what/where the changes were applied to.

Finally, the blueprintContext.defaultApplyIngestChanges call is what performs the 'magic'. Inside of this it is interpreting the changes object, and calling the appropriate methods on mutableIngestRundown. It is expected that this logic should be able to handle most use cases, but there may be some where they need something custom, so it is completely possible to reimplement inside blueprints.

So far this has ignored that the changes object can be of type UserOperationChange; this is explained below.

Modifying NRCS Ingest Data

MOS does not have Segments, to handle this Sofie creates a Segment and Part for each MOS Story, expecting them to be grouped later if needed.

In the past Sofie has had a hardcoded grouping logic, based on how NRK define this as a prefix in the Part names. Obviously this doesn't work for everyone, so this needed to be made more customisable. (This is still the default behaviour when processIngestData is not implemented)

To perform the NRK grouping behaviour the following implementation can be used:

function processIngestData(
context: IProcessIngestDataContext,
mutableIngestRundown: MutableIngestRundown<any, any, any>,
nrcsIngestRundown: IngestRundown,
previousNrcsIngestRundown: IngestRundown | undefined,
changes: NrcsIngestChangeDetails | UserOperationChange
) {
if (changes.source === 'ingest') {
// Group parts by interpreting the slug to be in the form `SEGMENTNAME;PARTNAME`
const groupedResult = context.groupMosPartsInRundownAndChangesWithSeparator(
nrcsIngestRundown,
previousNrcsIngestRundown,
ingestRundownChanges.changes,
';' // Backwards compatibility
)

context.defaultApplyIngestChanges(
mutableIngestRundown,
groupedResult.nrcsIngestRundown,
groupedResult.ingestChanges
)
}
}

There is also a helper method for doing your own logic:

function processIngestData(
context: IProcessIngestDataContext,
mutableIngestRundown: MutableIngestRundown<any, any, any>,
nrcsIngestRundown: IngestRundown,
previousNrcsIngestRundown: IngestRundown | undefined,
changes: NrcsIngestChangeDetails | UserOperationChange
) {
if (changes.source === 'ingest') {
// Group parts by some custom logic
const groupedResult = context.groupPartsInRundownAndChanges(
nrcsIngestRundown,
previousNrcsIngestRundown,
ingestRundownChanges.changes,
(segments) => {
// TODO - perform the grouping here
return segmentsAfterMyChanges
}
)

context.defaultApplyIngestChanges(
mutableIngestRundown,
groupedResult.nrcsIngestRundown,
groupedResult.ingestChanges
)
}
}

Both of these return a modified nrcsIngestRundown with the changes applied, and a new changes object which is similarly updated to match the new layout.

You can of course do any portions of this yourself if you desire.

User Edits

In some cases, it can be beneficial to allow the user to perform some editing of the Rundown from within the Sofie UI. AdLibs and AdLib Actions can allow for some of this to be done in the current and next Part, but this is limited and doesn't persist when re-running the Part.

The idea here is that the UI will be given some descriptors on operations it can perform, which will then make calls to processIngestData so that they can be applied to the IngestRundown. Doing it at this level allows things to persist and for decisions to be made by blueprints over how to merge the changes when an update for a Part is received from the NRCS.

This page doesn't go into how to define the editor for the UI, just how to handle the operations.

There are a few Sofie defined definitions of operations, but it is also expected that custom operations will be defined. You can check the Typescript types for the builtin operations that you might want to handle.

For example, it could be possible for Segments to be locked, so that any NRCS changes for them are ignored.

function processIngestData(
context: IProcessIngestDataContext,
mutableIngestRundown: MutableIngestRundown<any, any, any>,
nrcsIngestRundown: IngestRundown,
previousNrcsIngestRundown: IngestRundown | undefined,
changes: NrcsIngestChangeDetails | UserOperationChange
) {
if (changes.source === 'ingest') {
for (const segment of mutableIngestRundown.segments) {
delete ingestRundownChanges.changes.segmentChanges[segment.externalId]
// TODO - does this need to revert nrcsIngestRundown too?
}

blueprintContext.defaultApplyIngestChanges(mutableIngestRundown, nrcsIngestRundown, changes)
} else if (changes.source === 'user') {
if (changes.operation.id === 'lock-segment') {
mutableIngestRundown.getSegment(changes.operationTarget.segmentExternalId)?.setUserEditState('locked', true)
}
}
}