Skip to content

Commit

Permalink
feat: add segment timing countdownType
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Oct 3, 2024
1 parent f4e1108 commit f38cd4e
Show file tree
Hide file tree
Showing 32 changed files with 297 additions and 169 deletions.
3 changes: 0 additions & 3 deletions packages/blueprints-integration/src/documents/part.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ export interface IBlueprintMutatablePart<TPrivateData = unknown, TPublicData = u
/** Expected duration of the line, in milliseconds */
expectedDuration?: number

/** Budget duration of this part, in milliseconds */
budgetDuration?: number

/** Whether this segment line supports being used in HOLD */
holdMode?: PartHoldMode

Expand Down
13 changes: 13 additions & 0 deletions packages/blueprints-integration/src/documents/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,25 @@ export enum SegmentDisplayMode {
List = 'list',
}

export enum CountdownType {
/** Should count down till the end of the current part */
PART_EXPECTED_DURATION = 'part_expected_duration',
/** Should count down till the end of the segment's budget */
SEGMENT_BUDGET_DURATION = 'segment_budget_duration',
}

export interface SegmentTimingInfo {
/** A unix timestamp of when the segment is expected to begin. Affects rundown timing. */
expectedStart?: number

/** A unix timestamp of when the segment is expected to end. Affects rundown timing. */
expectedEnd?: number

/** Budget duration of this segment, in milliseconds */
budgetDuration?: number

/** Defines the behavior of countdowns during this segment. Default: `CountdownType.PART_EXPECTED_DURATION` */
countdownType?: CountdownType
}

/** The Segment generated from Blueprint */
Expand Down
2 changes: 0 additions & 2 deletions packages/job-worker/src/blueprints/context/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ export const IBlueprintMutatablePartSampleKeys = allKeysOfObject<IBlueprintMutat
disableNextInTransition: true,
outTransition: true,
expectedDuration: true,
budgetDuration: true,
holdMode: true,
shouldNotifyCurrentPlayingPart: true,
classes: true,
Expand Down Expand Up @@ -251,7 +250,6 @@ export function convertPartToBlueprints(part: ReadonlyDeep<DBPart>): IBlueprintP
disableNextInTransition: part.disableNextInTransition,
outTransition: clone(part.outTransition),
expectedDuration: part.expectedDuration,
budgetDuration: part.budgetDuration,
holdMode: part.holdMode,
shouldNotifyCurrentPlayingPart: part.shouldNotifyCurrentPlayingPart,
classes: clone<string[] | undefined>(part.classes),
Expand Down
7 changes: 7 additions & 0 deletions packages/live-status-gateway/api/schemas/activePlaylist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ $defs:
projectedEndTime:
description: Unix timestamp of when the segment is projected to end (milliseconds). The time this segment started, offset by its budget duration, if the segment has a defined budget duration. Otherwise, the time the current part started, offset by the difference between expected durations of all parts in this segment and the as-played durations of the parts that already stopped.
type: number
countdownType:
description: 'Countdown type within the segment. Default: `part_expected_duration`'
type: string
enum:
- part_expected_duration
- segment_budget_duration
required: [expectedDurationMs, projectedEndTime]
required: [id, timing]
additionalProperties: false
Expand All @@ -152,6 +158,7 @@ $defs:
expectedDurationMs: 15000
budgetDurationMs: 20000
projectedEndTime: 1600000075000
countdownType: segment_budget_duration
piece:
type: object
properties:
Expand Down
7 changes: 7 additions & 0 deletions packages/live-status-gateway/api/schemas/segments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ $defs:
budgetDurationMs:
description: Budget duration of the segment (milliseconds)
type: number
countdownType:
description: 'Countdown type within the segment. Default: `part_expected_duration`'
type: string
enum:
- part_expected_duration
- segment_budget_duration
required: [expectedDurationMs]
publicData:
description: Optional arbitrary data
Expand All @@ -60,5 +66,6 @@ $defs:
timing:
expectedDurationMs: 15000
budgetDurationMs: 20000
countdownType: segment_budget_duration
publicData:
containsLiveSource: true
18 changes: 9 additions & 9 deletions packages/live-status-gateway/src/collections/segmentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ export class SegmentHandler
if (!collection) throw new Error(`collection '${this._collectionName}' not found!`)
const allSegments = collection.find(undefined)
await this._segmentsHandler.setSegments(allSegments)
if (this._currentSegmentId) {
this._collectionData = collection.findOne(this._currentSegmentId)
await this.updateAndNotify()
}

private async updateAndNotify() {
const collection = this._core.getCollection(this._collectionName)
const newData = this._currentSegmentId ? collection.findOne(this._currentSegmentId) : undefined
if (this._collectionData !== newData) {
this._collectionData = newData
await this.notify(this._collectionData)
}
}

async update(source: string, data: SelectedPartInstances | DBRundownPlaylist | undefined): Promise<void> {
const previousSegmentId = this._currentSegmentId
const previousRundownIds = this._rundownIds

switch (source) {
Expand Down Expand Up @@ -91,11 +96,6 @@ export class SegmentHandler
const allSegments = collection.find(undefined)
await this._segmentsHandler.setSegments(allSegments)
}
if (previousSegmentId !== this._currentSegmentId) {
if (this._currentSegmentId) {
this._collectionData = collection.findOne(this._currentSegmentId)
await this.notify(this._collectionData)
}
}
await this.updateAndNotify()
}
}
1 change: 1 addition & 0 deletions packages/live-status-gateway/src/liveStatusServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export class LiveStatusServer {
await partInstancesHandler.subscribe(activePlaylistTopic)
await partsHandler.subscribe(activePlaylistTopic)
await pieceInstancesHandler.subscribe(activePlaylistTopic)
await segmentHandler.subscribe(activePlaylistTopic)

await playlistHandler.subscribe(activePiecesTopic)
await showStyleBaseHandler.subscribe(activePiecesTopic)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { literal } from '@sofie-automation/corelib/dist/lib'
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
import { PartsHandler } from '../../collections/partsHandler'
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
import { SegmentHandler } from '../../collections/segmentHandler'
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
import { CountdownType } from '@sofie-automation/blueprints-integration'

function makeEmptyTestPartInstances(): SelectedPartInstances {
return {
Expand Down Expand Up @@ -73,6 +76,8 @@ describe('ActivePlaylistTopic', () => {

const testShowStyleBase = makeTestShowStyleBase()
await topic.update(ShowStyleBaseHandler.name, testShowStyleBase as ShowStyleBaseExt)

const segment1id = protectString('SEGMENT_1')
const part1: Partial<DBPart> = {
_id: protectString('PART_1'),
title: 'Test Part',
Expand All @@ -81,13 +86,15 @@ describe('ActivePlaylistTopic', () => {
expectedDuration: 10000,
publicData: { b: 'c' },
}
const currentPartInstance = {
_id: currentPartInstanceId,
part: part1,
timings: { plannedStartedPlayback: 1600000060000 },
segmentId: segment1id,
}
const testPartInstances: PartialDeep<SelectedPartInstances> = {
current: {
_id: currentPartInstanceId,
part: part1,
timings: { plannedStartedPlayback: 1600000060000 },
},
firstInSegmentPlayout: {},
current: currentPartInstance,
firstInSegmentPlayout: currentPartInstance,
inCurrentSegment: [
literal<PartialDeep<DBPartInstance>>({
_id: protectString(currentPartInstanceId),
Expand All @@ -100,6 +107,11 @@ describe('ActivePlaylistTopic', () => {

await topic.update(PartsHandler.name, [part1] as DBPart[])

await topic.update(SegmentHandler.name, {
_id: segment1id,
segmentTiming: { budgetDuration: 12300, countdownType: CountdownType.SEGMENT_BUDGET_DURATION },
} as DBSegment)

topic.addSubscriber(mockSubscriber)

const expectedStatus: ActivePlaylistStatus = {
Expand All @@ -120,7 +132,9 @@ describe('ActivePlaylistTopic', () => {
id: 'SEGMENT_1',
timing: {
expectedDurationMs: 10000,
projectedEndTime: 1600000070000,
budgetDurationMs: 12300,
projectedEndTime: 1600000072300,
countdownType: 'segment_budget_duration',
},
},
rundownIds: unprotectStringArray(playlist.rundownIdsInOrder),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ const RUNDOWN_1_ID = 'RUNDOWN_1'
const RUNDOWN_2_ID = 'RUNDOWN_2'
const THROTTLE_PERIOD_MS = 205

function makeTestSegment(id: string, rank: number, rundownId: string): DBSegment {
function makeTestSegment(id: string, rank: number, rundownId: string, segmentProps?: Partial<DBSegment>): DBSegment {
return {
_id: protectString(id),
externalId: `NCS_SEGMENT_${id}`,
name: `Segment ${id}`,
_rank: rank,
rundownId: protectString(rundownId),
externalModified: 1695799420147,
...segmentProps,
}
}

Expand All @@ -27,7 +28,7 @@ function makeTestPart(
rank: number,
rundownId: string,
segmentId: string,
partProps: Partial<DBPart>
partProps?: Partial<DBPart>
): DBPart {
return {
_id: protectString(id),
Expand Down Expand Up @@ -260,33 +261,19 @@ describe('SegmentsTopic', () => {
const segment_2_2_id = '2_2'
await topic.update(SegmentsHandler.name, [
makeTestSegment('2_1', 1, RUNDOWN_2_ID),
makeTestSegment(segment_2_2_id, 2, RUNDOWN_2_ID),
makeTestSegment(segment_1_2_id, 2, RUNDOWN_1_ID),
makeTestSegment(segment_1_1_id, 1, RUNDOWN_1_ID),
makeTestSegment(segment_2_2_id, 2, RUNDOWN_2_ID, { segmentTiming: { budgetDuration: 51000 } }),
makeTestSegment(segment_1_2_id, 2, RUNDOWN_1_ID, { segmentTiming: { budgetDuration: 15000 } }),
makeTestSegment(segment_1_1_id, 1, RUNDOWN_1_ID, { segmentTiming: { budgetDuration: 5000 } }),
])
mockSubscriber.send.mockClear()
await topic.update(PartsHandler.name, [
makeTestPart('1_2_1', 1, RUNDOWN_1_ID, segment_1_2_id, {
budgetDuration: 10000,
}),
makeTestPart('2_2_1', 1, RUNDOWN_1_ID, segment_2_2_id, {
budgetDuration: 40000,
}),
makeTestPart('1_2_2', 2, RUNDOWN_1_ID, segment_1_2_id, {
budgetDuration: 5000,
}),
makeTestPart('1_1_2', 2, RUNDOWN_1_ID, segment_1_1_id, {
budgetDuration: 1000,
}),
makeTestPart('1_1_1', 1, RUNDOWN_1_ID, segment_1_1_id, {
budgetDuration: 3000,
}),
makeTestPart('2_2_2', 2, RUNDOWN_1_ID, segment_2_2_id, {
budgetDuration: 11000,
}),
makeTestPart('1_1_2', 2, RUNDOWN_1_ID, segment_1_1_id, {
budgetDuration: 1000,
}),
makeTestPart('1_2_1', 1, RUNDOWN_1_ID, segment_1_2_id),
makeTestPart('2_2_1', 1, RUNDOWN_1_ID, segment_2_2_id),
makeTestPart('1_2_2', 2, RUNDOWN_1_ID, segment_1_2_id),
makeTestPart('1_1_2', 2, RUNDOWN_1_ID, segment_1_1_id),
makeTestPart('1_1_1', 1, RUNDOWN_1_ID, segment_1_1_id),
makeTestPart('2_2_2', 2, RUNDOWN_1_ID, segment_2_2_id),
makeTestPart('1_1_2', 2, RUNDOWN_1_ID, segment_1_1_id),
])
jest.advanceTimersByTime(THROTTLE_PERIOD_MS)

Expand Down
17 changes: 15 additions & 2 deletions packages/live-status-gateway/src/topics/activePlaylistTopic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import _ = require('underscore')
import { PartTiming, calculateCurrentPartTiming } from './helpers/partTiming'
import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from '../collections/pieceInstancesHandler'
import { PieceStatus, toPieceStatus } from './helpers/pieceStatus'
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
import { SegmentHandler } from '../collections/segmentHandler'

const THROTTLE_PERIOD_MS = 100

Expand Down Expand Up @@ -55,7 +57,8 @@ export class ActivePlaylistTopic
CollectionObserver<ShowStyleBaseExt>,
CollectionObserver<SelectedPartInstances>,
CollectionObserver<DBPart[]>,
CollectionObserver<SelectedPieceInstances>
CollectionObserver<SelectedPieceInstances>,
CollectionObserver<DBSegment>
{
public observerName = ActivePlaylistTopic.name
private _activePlaylist: DBRundownPlaylist | undefined
Expand All @@ -67,6 +70,7 @@ export class ActivePlaylistTopic
private _pieceInstancesInCurrentPartInstance: PieceInstanceMin[] | undefined
private _pieceInstancesInNextPartInstance: PieceInstanceMin[] | undefined
private _showStyleBaseExt: ShowStyleBaseExt | undefined
private _currentSegment: DBSegment | undefined
private throttledSendStatusToAll: () => void

constructor(logger: Logger) {
Expand Down Expand Up @@ -116,10 +120,11 @@ export class ActivePlaylistTopic
})
: null,
currentSegment:
this._currentPartInstance && currentPart
this._currentPartInstance && currentPart && this._currentSegment
? literal<CurrentSegmentStatus>({
id: unprotectString(currentPart.segmentId),
timing: calculateCurrentSegmentTiming(
this._currentSegment,
this._currentPartInstance,
this._firstInstanceInSegmentPlayout,
this._partInstancesInCurrentSegment,
Expand Down Expand Up @@ -159,6 +164,7 @@ export class ActivePlaylistTopic
private isDataInconsistent() {
return (
this._currentPartInstance?._id !== this._activePlaylist?.currentPartInfo?.partInstanceId ||
this._currentPartInstance?.segmentId !== this._currentSegment?._id ||
this._nextPartInstance?._id !== this._activePlaylist?.nextPartInfo?.partInstanceId ||
(this._pieceInstancesInCurrentPartInstance?.[0] &&
this._pieceInstancesInCurrentPartInstance?.[0].partInstanceId !== this._currentPartInstance?._id) ||
Expand All @@ -175,6 +181,7 @@ export class ActivePlaylistTopic
| SelectedPartInstances
| DBPart[]
| SelectedPieceInstances
| DBSegment
| undefined
): Promise<void> {
let hasAnythingChanged = false
Expand Down Expand Up @@ -230,6 +237,12 @@ export class ActivePlaylistTopic
this._pieceInstancesInNextPartInstance = pieceInstances.nextPartInstance
break
}
case SegmentHandler.name: {
this._currentSegment = data as DBSegment
this.logUpdateReceived('segment', source)
hasAnythingChanged = true
break
}
default:
throw new Error(`${this._name} received unsupported update from ${source}}`)
}
Expand Down
14 changes: 8 additions & 6 deletions packages/live-status-gateway/src/topics/helpers/segmentTiming.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'

export interface SegmentTiming {
budgetDurationMs?: number
expectedDurationMs: number
countdownType?: 'part_expected_duration' | 'segment_budget_duration'
}

export interface CurrentSegmentTiming extends SegmentTiming {
projectedEndTime: number
}

export function calculateCurrentSegmentTiming(
segment: DBSegment,
currentPartInstance: DBPartInstance,
firstInstanceInSegmentPlayout: DBPartInstance | undefined,
segmentPartInstances: DBPartInstance[],
segmentParts: DBPart[]
): CurrentSegmentTiming {
const segmentTiming = calculateSegmentTiming(segmentParts)
const segmentTiming = calculateSegmentTiming(segment, segmentParts)
const playedDurations = segmentPartInstances.reduce((sum, partInstance) => {
return (partInstance.timings?.duration ?? 0) + sum
}, 0)
Expand All @@ -29,22 +32,21 @@ export function calculateCurrentSegmentTiming(
const projectedBudgetEndTime =
(firstInstanceInSegmentPlayout?.timings?.reportedStartedPlayback ??
firstInstanceInSegmentPlayout?.timings?.plannedStartedPlayback ??
0) + (segmentTiming.budgetDurationMs ?? 0)
Date.now()) + (segmentTiming.budgetDurationMs ?? 0)
return {
...segmentTiming,
projectedEndTime: segmentTiming.budgetDurationMs != null ? projectedBudgetEndTime : projectedEndTime,
}
}

export function calculateSegmentTiming(segmentParts: DBPart[]): SegmentTiming {
export function calculateSegmentTiming(segment: DBSegment, segmentParts: DBPart[]): SegmentTiming {
return {
budgetDurationMs: segmentParts.reduce<number | undefined>((sum, part): number | undefined => {
return part.budgetDuration != null && !part.untimed ? (sum ?? 0) + part.budgetDuration : sum
}, undefined),
budgetDurationMs: segment.segmentTiming?.budgetDuration,
expectedDurationMs: segmentParts.reduce<number>((sum, part): number => {
return part.expectedDurationWithTransition != null && !part.untimed
? sum + part.expectedDurationWithTransition
: sum
}, 0),
countdownType: segment.segmentTiming?.countdownType,
}
}
Loading

0 comments on commit f38cd4e

Please sign in to comment.