Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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: 1 addition & 1 deletion meteor/__mocks__/defaultCollectionObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioI
rehearsal: false,
currentPartInfo: null,
nextPartInfo: null,
previousPartInfo: null,
previousPartsInfo: [],
timing: {
type: 'none' as any,
},
Expand Down
2 changes: 1 addition & 1 deletion meteor/server/__tests__/cronjobs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ describe('cronjobs', () => {
externalId: '',
modified: Date.now(),
name: 'Rundown',
previousPartInfo: null,
previousPartsInfo: [],
rundownIdsInOrder: [],
studioId,
timing: {
Expand Down
2 changes: 1 addition & 1 deletion meteor/server/api/__tests__/externalMessageQueue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Test external message queue static methods', () => {
manuallySelected: false,
consumesQueuedSegmentId: false,
},
previousPartInfo: null,
previousPartsInfo: [],
activationId: protectString('active'),
timing: {
type: PlaylistTimingType.None,
Expand Down
2 changes: 1 addition & 1 deletion meteor/server/api/__tests__/peripheralDevice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
modified: 0,
currentPartInfo: null,
nextPartInfo: null,
previousPartInfo: null,
previousPartsInfo: [],
activationId: protectString('active'),
timing: {
type: PlaylistTimingType.None,
Expand Down Expand Up @@ -495,7 +495,7 @@
).rejects.toThrowMeteor(418, `Error thrown, as requested`)
})

/*

Check warning on line 498 in meteor/server/api/__tests__/peripheralDevice.test.ts

View workflow job for this annotation

GitHub Actions / Typecheck and Lint Core

Do not comment out tests
test('timelineTriggerTime', () => {
if (DEBUG) setLogLevel(LogLevel.DEBUG)
let timelineTriggerTimeResult: PeripheralDeviceAPI.TimelineTriggerTimeResult = [
Expand Down Expand Up @@ -563,7 +563,7 @@
})

// Note: this test fails, due to a backwards-compatibility hack in #c579c8f0
// test('initialize with bad arguments', () => {

Check warning on line 566 in meteor/server/api/__tests__/peripheralDevice.test.ts

View workflow job for this annotation

GitHub Actions / Typecheck and Lint Core

Do not comment out tests
// let options: PeripheralDeviceInitOptions = {
// category: PeripheralDeviceCategory.INGEST,
// type: PeripheralDeviceType.MOS,
Expand All @@ -584,7 +584,7 @@
// }
// })

// test('setStatus with bad arguments', () => {

Check warning on line 587 in meteor/server/api/__tests__/peripheralDevice.test.ts

View workflow job for this annotation

GitHub Actions / Typecheck and Lint Core

Do not comment out tests
// try {
// Meteor.call(PeripheralDeviceAPIMethods.setStatus, 'wibbly', device.token, { statusCode: 0 })
// fail('expected to throw')
Expand Down
25 changes: 13 additions & 12 deletions meteor/server/api/deviceTriggers/TagsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class TagsService {
return false
}

const previousPartInstanceId = rundownPlaylist?.previousPartInfo?.partInstanceId
const previousPartInstanceIds = (rundownPlaylist?.previousPartsInfo ?? []).map((info) => info.partInstanceId)
const currentPartInstanceId = rundownPlaylist?.currentPartInfo?.partInstanceId
const nextPartInstanceId = rundownPlaylist?.nextPartInfo?.partInstanceId

Expand All @@ -66,13 +66,13 @@ export class TagsService {

const resolvedSourceLayers = applyAndValidateOverrides(showStyleBase.sourceLayersWithOverrides).obj

const inPreviousPartInstance = previousPartInstanceId
? this.processAndPrunePieceInstanceTimings(
cache.PartInstances.findOne(previousPartInstanceId)?.timings,
cache.PieceInstances.find({ partInstanceId: previousPartInstanceId }).fetch(),
resolvedSourceLayers
)
: []
const inPreviousPartInstances = previousPartInstanceIds.flatMap((previousPartInstanceId) =>
this.processAndPrunePieceInstanceTimings(
cache.PartInstances.findOne(previousPartInstanceId)?.timings,
cache.PieceInstances.find({ partInstanceId: previousPartInstanceId }).fetch(),
resolvedSourceLayers
)
)
const inCurrentPartInstance = currentPartInstanceId
? this.processAndPrunePieceInstanceTimings(
cache.PartInstances.findOne(currentPartInstanceId)?.timings,
Expand All @@ -88,8 +88,9 @@ export class TagsService {
)
: []

const activePieceInstances = [...inPreviousPartInstance, ...inCurrentPartInstance].filter((pieceInstance) =>
this.isPieceInstanceActive(pieceInstance, previousPartInstanceId, currentPartInstanceId)
const previousPartInstanceIdSet = new Set(previousPartInstanceIds)
const activePieceInstances = [...inPreviousPartInstances, ...inCurrentPartInstance].filter((pieceInstance) =>
this.isPieceInstanceActive(pieceInstance, previousPartInstanceIdSet, currentPartInstanceId)
)

const activePieceInstancesTags = new Set<string>()
Expand Down Expand Up @@ -144,14 +145,14 @@ export class TagsService {

private isPieceInstanceActive(
pieceInstance: PieceInstanceWithTimings,
previousPartInstanceId: PartInstanceId | undefined,
previousPartInstanceIds: Set<PartInstanceId>,
currentPartInstanceId: PartInstanceId | undefined
) {
return (
pieceInstance.reportedStoppedPlayback == null &&
pieceInstance.piece.virtual !== true &&
pieceInstance.disabled !== true &&
(pieceInstance.partInstanceId === previousPartInstanceId || // a piece from previous part instance may be active during transition
(previousPartInstanceIds.has(pieceInstance.partInstanceId) || // a piece from a previous part instance may be active during transition/overlap
pieceInstance.partInstanceId === currentPartInstanceId) &&
(pieceInstance.reportedStartedPlayback != null || // has been reported to have started by the Playout Gateway
pieceInstance.plannedStartedPlayback != null || // a time to start playing has been set by Core
Expand Down
167 changes: 167 additions & 0 deletions meteor/server/api/deviceTriggers/__tests__/TagsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ const tag2 = 'tag2'
const tag3 = 'tag3'

const tag4 = 'tag4'
const tag5 = 'tag5'
const tag6 = 'tag6'

const partInstanceId3 = protectString<PartInstanceId>('partInstance3')
const partInstanceId4 = protectString<PartInstanceId>('partInstance4')
const pieceInstanceId4 = protectString<PieceInstanceId>('pieceInstance4')
const pieceInstanceId5 = protectString<PieceInstanceId>('pieceInstance5')

function createAndPopulateMockCache(): ContentCache {
const newCache: ContentCache = {
Expand Down Expand Up @@ -233,4 +240,164 @@ describe('TagsService', () => {

expect(result).toEqual(true)
})

test('piece in previousPartsInfo[0] (most-recent previous) is treated as on-air', () => {
// partInstanceId3 = previous (index 0), partInstanceId0 = current
const testee = createTestee()
const cache: ContentCache = {
RundownPlaylists: new ReactiveCacheCollection('rundownPlaylists'),
ShowStyleBases: new ReactiveCacheCollection('showStyleBases'),
PieceInstances: new ReactiveCacheCollection('pieceInstances'),
PartInstances: new ReactiveCacheCollection('partInstances'),
}
cache.RundownPlaylists.insert({
_id: playlistId,
activationId,
previousPartsInfo: [{ partInstanceId: partInstanceId3 }],
currentPartInfo: { partInstanceId: partInstanceId0 },
nextPartInfo: { partInstanceId: partInstanceId1 },
} as DBRundownPlaylist)
cache.ShowStyleBases.insert({
_id: showStyleBaseId,
sourceLayersWithOverrides: wrapDefaultObject(
normalizeArray(
[
literal<ISourceLayer>({
_id: sourceLayerId0,
_rank: 0,
name: 'Camera',
type: SourceLayerType.CAMERA,
}),
],
'_id'
)
),
} as DBShowStyleBase)
// Piece in the previous part — started playback, not yet stopped
cache.PieceInstances.insert({
_id: pieceInstanceId4,
piece: {
tags: [tag5],
sourceLayerId: sourceLayerId0,
enable: { start: 0 },
lifespan: PieceLifespan.WithinPart,
},
partInstanceId: partInstanceId3,
plannedStartedPlayback: 1000,
} as PieceInstance)
// Piece in the current part
cache.PieceInstances.insert({
_id: pieceInstanceId0,
piece: {
tags: [tag0],
sourceLayerId: sourceLayerId0,
enable: { start: 0 },
lifespan: PieceLifespan.WithinPart,
},
partInstanceId: partInstanceId0,
} as PieceInstance)
cache.PartInstances.insert({ _id: partInstanceId3 } as DBPartInstance)
cache.PartInstances.insert({ _id: partInstanceId0 } as DBPartInstance)
cache.PartInstances.insert({ _id: partInstanceId1 } as DBPartInstance)

testee.updatePieceInstances(cache, showStyleBaseId)

// tag5 is from previous part → on-air; tag0 is from current → on-air; neither is next
expect(testee.getTallyStateFromTags({ currentPieceTags: [tag5] } as IWrappedAdLib)).toEqual({
isActive: true,
isNext: false,
})
expect(testee.getTallyStateFromTags({ currentPieceTags: [tag0] } as IWrappedAdLib)).toEqual({
isActive: true,
isNext: false,
})
})

test('pieces in all entries of previousPartsInfo are treated as on-air', () => {
// partInstanceId4 = older previous (index 1), partInstanceId3 = recent previous (index 0), partInstanceId0 = current
const testee = createTestee()
const cache: ContentCache = {
RundownPlaylists: new ReactiveCacheCollection('rundownPlaylists'),
ShowStyleBases: new ReactiveCacheCollection('showStyleBases'),
PieceInstances: new ReactiveCacheCollection('pieceInstances'),
PartInstances: new ReactiveCacheCollection('partInstances'),
}
cache.RundownPlaylists.insert({
_id: playlistId,
activationId,
// most-recent-first: index 0 = partInstanceId3, index 1 = partInstanceId4
previousPartsInfo: [{ partInstanceId: partInstanceId3 }, { partInstanceId: partInstanceId4 }],
currentPartInfo: { partInstanceId: partInstanceId0 },
} as DBRundownPlaylist)
cache.ShowStyleBases.insert({
_id: showStyleBaseId,
sourceLayersWithOverrides: wrapDefaultObject(
normalizeArray(
[
literal<ISourceLayer>({
_id: sourceLayerId0,
_rank: 0,
name: 'Camera',
type: SourceLayerType.CAMERA,
}),
],
'_id'
)
),
} as DBShowStyleBase)
// Piece in the most-recent previous part (index 0)
cache.PieceInstances.insert({
_id: pieceInstanceId4,
piece: {
tags: [tag5],
sourceLayerId: sourceLayerId0,
enable: { start: 0 },
lifespan: PieceLifespan.WithinPart,
},
partInstanceId: partInstanceId3,
plannedStartedPlayback: 1000,
} as PieceInstance)
// Piece in the older previous part (index 1) — still has started playback, not stopped
cache.PieceInstances.insert({
_id: pieceInstanceId5,
piece: {
tags: [tag6],
sourceLayerId: sourceLayerId0,
enable: { start: 0 },
lifespan: PieceLifespan.WithinPart,
},
partInstanceId: partInstanceId4,
plannedStartedPlayback: 500,
} as PieceInstance)
// Piece in the current part
cache.PieceInstances.insert({
_id: pieceInstanceId0,
piece: {
tags: [tag0],
sourceLayerId: sourceLayerId0,
enable: { start: 0 },
lifespan: PieceLifespan.WithinPart,
},
partInstanceId: partInstanceId0,
} as PieceInstance)
cache.PartInstances.insert({ _id: partInstanceId4 } as DBPartInstance)
cache.PartInstances.insert({ _id: partInstanceId3 } as DBPartInstance)
cache.PartInstances.insert({ _id: partInstanceId0 } as DBPartInstance)

testee.updatePieceInstances(cache, showStyleBaseId)

// All three tags should be on-air
expect(testee.getTallyStateFromTags({ currentPieceTags: [tag5] } as IWrappedAdLib)).toEqual({
isActive: true,
isNext: false,
})
expect(testee.getTallyStateFromTags({ currentPieceTags: [tag6] } as IWrappedAdLib)).toEqual({
isActive: true,
isNext: false,
})
expect(testee.getTallyStateFromTags({ currentPieceTags: [tag0] } as IWrappedAdLib)).toEqual({
isActive: true,
isNext: false,
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type RundownPlaylistFields =
| 'activationId'
| 'currentPartInfo'
| 'nextPartInfo'
| 'previousPartInfo'
| 'previousPartsInfo'
export const rundownPlaylistFieldSpecifier = literal<
MongoFieldSpecifierOnesStrict<Pick<DBRundownPlaylist, RundownPlaylistFields>>
>({
Expand All @@ -23,7 +23,7 @@ export const rundownPlaylistFieldSpecifier = literal<
activationId: 1,
currentPartInfo: 1,
nextPartInfo: 1,
previousPartInfo: 1,
previousPartsInfo: 1,
})

export type PieceInstanceFields =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,13 @@ export interface DBRundownPlaylist {
nextPartInfo: SelectedPartInstance | null
/** The time offset of the next line */
nextTimeOffset?: number | null
/** the id of the Previous Part */
previousPartInfo: SelectedPartInstance | null
/**
* Previously played PartInstances, ordered most-recent-first (index 0 = the one taken from most recently).
* There may be more than one entry when keepalive/postroll/preroll cause PartInstances to overlap:
* e.g. if Part A is still audible due to postroll when Part C is taken, both A and B are retained here
* until their timeline contribution has fully ended.
*/
previousPartsInfo: SelectedPartInstance[]

/**
* The id of the Queued Segment. If set, the Next point will jump to that segment when reaching the end of the currently playing segment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioI
rehearsal: false,
currentPartInfo: null,
nextPartInfo: null,
previousPartInfo: null,
previousPartsInfo: [],

timing: {
type: PlaylistTimingType.None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli
showStyleBlueprintConfig: ProcessedShowStyleConfig,
playlist: ReadonlyDeep<DBRundownPlaylist>,
rundown: ReadonlyDeep<DBRundown>,
previousPartInstance: ReadonlyDeep<DBPartInstance> | undefined,
previousPartInstances: ReadonlyDeep<DBPartInstance>[],
currentPartInstance: ReadonlyDeep<DBPartInstance> | undefined,
nextPartInstance: ReadonlyDeep<DBPartInstance> | undefined,
pieceInstances: ReadonlyDeep<ResolvedPieceInstance[]>
) {
super(
{
name: playlist.name,
identifier: `playlistId=${playlist._id},previousPartInstance=${previousPartInstance?._id},currentPartInstance=${currentPartInstance?._id},nextPartInstance=${nextPartInstance?._id}`,
identifier: `playlistId=${playlist._id},previousPartInstance=${previousPartInstances[0]?._id},currentPartInstance=${currentPartInstance?._id},nextPartInstance=${nextPartInstance?._id}`,
},
studio,
studioBlueprintConfig,
Expand All @@ -62,11 +62,12 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli

this.currentPartInstance = currentPartInstance && convertPartInstanceToBlueprints(currentPartInstance)
this.nextPartInstance = nextPartInstance && convertPartInstanceToBlueprints(nextPartInstance)
this.previousPartInstance = previousPartInstance && convertPartInstanceToBlueprints(previousPartInstance)
this.previousPartInstance =
previousPartInstances[0] && convertPartInstanceToBlueprints(previousPartInstances[0])

this.quickLoopInfo = createBlueprintQuickLoopInfo(playlist)

const partInstances = _.compact([previousPartInstance, currentPartInstance, nextPartInstance])
const partInstances = _.compact([...previousPartInstances, currentPartInstance, nextPartInstance])

for (const pieceInstance of pieceInstances) {
this.#pieceInstanceCache.set(pieceInstance.instance._id, pieceInstance.instance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ describe('Test blueprint api context', () => {
if (previousPartInstance !== undefined) {
await jobContext.mockCollections.RundownPlaylists.update(playlistId, {
$set: {
previousPartInfo: convertInfo(previousPartInstance),
previousPartsInfo: previousPartInstance ? [convertInfo(previousPartInstance)!] : [],
},
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('Test external message queue static methods', () => {
manuallySelected: false,
consumesQueuedSegmentId: false,
},
previousPartInfo: null,
previousPartsInfo: [],
activationId: protectString('active'),
timing: {
type: PlaylistTimingType.None,
Expand Down Expand Up @@ -200,7 +200,7 @@ describe('Test sending messages to mocked endpoints', () => {
manuallySelected: false,
consumesQueuedSegmentId: false,
},
previousPartInfo: null,
previousPartsInfo: [],
activationId: protectString('active'),
timing: {
type: PlaylistTimingType.None,
Expand Down
4 changes: 2 additions & 2 deletions packages/job-worker/src/ingest/__tests__/ingest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1853,7 +1853,7 @@ describe('Test ingest actions for rundowns and segments', () => {
)) as DBRundownPlaylist
expect(playlist).toBeTruthy()
expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId1)
expect(playlist.previousPartInfo?.partInstanceId).toBe(partInstanceId0)
expect(playlist.previousPartsInfo?.[0]?.partInstanceId).toBe(partInstanceId0)

const currentPartInstance = (await getSelectedPartInstances(context, playlist))
.currentPartInstance as DBPartInstance
Expand Down Expand Up @@ -1902,7 +1902,7 @@ describe('Test ingest actions for rundowns and segments', () => {
)) as DBRundownPlaylist
expect(playlist).toBeTruthy()
expect(playlist.currentPartInfo?.partInstanceId).toBe(partInstanceId1)
expect(playlist.previousPartInfo?.partInstanceId).toBe(partInstanceId0)
expect(playlist.previousPartsInfo?.[0]?.partInstanceId).toBe(partInstanceId0)

const currentPartInstance = (await getSelectedPartInstances(context, playlist))
.currentPartInstance as DBPartInstance
Expand Down
Loading
Loading