Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement QuickLoop (SOFIE-2878) #1112

Merged
merged 48 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0140ac5
wip(SOFIE-69): looping section
ianshade Dec 11, 2023
199b8b1
wip(SOFIE-69): appearance of the looping section
ianshade Dec 14, 2023
225b03c
wip(SOFIE-69): improve finding next part
ianshade Dec 19, 2023
6ac90d5
wip(SOFIE-69): improve loop markers in timeline view
ianshade Dec 19, 2023
079293a
wip(SOFIE-69): style line parts
ianshade Dec 20, 2023
971c7d3
wip(SOFIE-69): more styling
ianshade Dec 21, 2023
adf027a
wip(SOFIE-69): appearance tweaks
ianshade Dec 22, 2023
a1194ee
wip(SOFIE-69): fix dots when loop start and end order is reversed
ianshade Dec 22, 2023
49a6487
wip(SOFIE-69): legacy looping to new looping
ianshade Jan 5, 2024
e936479
wip(SOFIE-69): alow extending loop by dynamically inserting parts
ianshade Jan 5, 2024
262b7a8
wip(SOFIE-69): fix extending a single-part loop with dynamically inse…
ianshade Jan 8, 2024
68f790e
wip(SOFIE-69): reset pieceInstances alongside partInstances when leav…
ianshade Jan 9, 2024
dfb8d27
wip(SOFIE-69): reset part overrides when clearing the loop
ianshade Jan 9, 2024
216287d
Merge remote-tracking branch 'origin/release51' into feat/quickLoop
ianshade Jan 11, 2024
41d021c
Merge remote-tracking branch 'origin/release51' into feat/quickLoop
ianshade Jan 12, 2024
c03f3ba
wip(SOFIE-69): hide quickloop context menu when locked
ianshade Jan 15, 2024
a904c59
wip(SOFIE-69): rename css class
ianshade Jan 15, 2024
4d084de
wip(SOFIE-69): metaData is now privateData
ianshade Jan 15, 2024
fb46080
wip(SOFIE-69): rename css class
ianshade Jan 15, 2024
562ea2b
wip(SOFIE-69): disable dots when looping entire playlist
ianshade Jan 15, 2024
134938d
wip(SOFIE-69): invalid parts when forcing autonext and make quickloop…
ianshade Jan 15, 2024
f0fe93c
wip(SOFIE-69): deduplicate partInstances when looping a single part
ianshade Jan 15, 2024
7227fdb
wip(SOFIE-69): make meteor tests pass; remove unused property
ianshade Jan 19, 2024
55b7546
wip(SOFIE-69): make package tests pass
ianshade Jan 19, 2024
7ff22b2
Merge remote-tracking branch 'origin/release51' into feat/quickLoop
ianshade Jan 19, 2024
84082c0
wip(SOFIE-69): move findPartInstancesInQuickLoop and add tests
ianshade Jan 30, 2024
e9bdf70
wip(SOFIE-69): refactor; add tests for selectNextPart
ianshade Jan 31, 2024
528694d
wip(SOFIE-69): add fallbackPartDuration; refactor quickloop
ianshade Jan 31, 2024
81fb057
wip(SOFIE-69): changing next part when moving markers
ianshade Feb 1, 2024
06eb35b
Merge remote-tracking branch 'origin/release51' into feat/quickLoop
ianshade Feb 1, 2024
1af9cfd
wip(SOFIE-69): fix changing next when moving markers
ianshade Feb 1, 2024
951a79c
wip(SOFIE-69): solve wrong marker order
ianshade Feb 1, 2024
9b53aa2
refactor(SOFIE-69): code improvements from PR comments
ianshade Feb 26, 2024
5fbea60
fix(SOFIE-69): wrong defaults in calls to selectNextPart
ianshade Feb 26, 2024
31ddb92
refactor(SOFIE-69): overridenProperties type, based on PR review comm…
ianshade Feb 26, 2024
f6cc8d0
refactor(SOFIE-69): extract quickLoop methods from PlayoutModelImpl
ianshade Feb 26, 2024
4b41445
chore: minor fixes after PR discussion
nytamin Apr 12, 2024
36dc3ce
refactor: move UIParts to the client-side of meteor
ianshade Apr 22, 2024
a793397
perf: track invalidated segments and parts in UIParts publication
ianshade May 7, 2024
398ecd1
wip: turn partInstances into a custom uiPartInstances publication
ianshade Jun 27, 2024
f6c1640
refactor: apply quickloop overrides during playout
ianshade Aug 19, 2024
0d1b890
Merge remote-tracking branch 'origin/release51' into feat/quickLoop
ianshade Sep 2, 2024
f94db28
fix: removing parts/segments with quickloop markers
mint-dewit Sep 18, 2024
e8826c1
fix: no quickloop autotake on parts that were running for a long time
mint-dewit Sep 19, 2024
6950fea
fix: quickloop rundown reset
mint-dewit Sep 19, 2024
a19ab10
Merge remote-tracking branch 'nrk/release52' into feat/quickLoop
mint-dewit Sep 24, 2024
6afbb53
chore: lint
mint-dewit Sep 24, 2024
1d87ef1
chore: remove file that got duplicated in merge
mint-dewit Sep 24, 2024
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
6 changes: 5 additions & 1 deletion meteor/__mocks__/defaultCollectionObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import {
ShowStyleVariantId,
StudioId,
} from '@sofie-automation/corelib/dist/dataModel/Ids'
import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants'
import {
DEFAULT_FALLBACK_PART_DURATION,
DEFAULT_MINIMUM_TAKE_SPAN,
} from '@sofie-automation/shared-lib/dist/core/constants'

export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioId): DBRundownPlaylist {
return {
Expand Down Expand Up @@ -106,6 +109,7 @@ export function defaultStudio(_id: StudioId): DBStudio {
frameRate: 25,
mediaPreviewsUrl: '',
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
fallbackPartDuration: DEFAULT_FALLBACK_PART_DURATION,
},
_rundownVersionHash: '',
routeSets: {},
Expand Down
45 changes: 44 additions & 1 deletion meteor/server/api/rest/v1/typeConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@
import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant'
import { Blueprints, ShowStyleBases, Studios } from '../../../collections'
import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants'
import {
DEFAULT_MINIMUM_TAKE_SPAN,
DEFAULT_FALLBACK_PART_DURATION,
} from '@sofie-automation/shared-lib/dist/core/constants'

Check warning on line 39 in meteor/server/api/rest/v1/typeConversion.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/api/rest/v1/typeConversion.ts#L36-L39

Added lines #L36 - L39 were not covered by tests
import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets'
import { ForceQuickLoopAutoNext } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'

Check warning on line 41 in meteor/server/api/rest/v1/typeConversion.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/api/rest/v1/typeConversion.ts#L41

Added line #L41 was not covered by tests

/*
This file contains functions that convert between the internal Sofie-Core types and types exposed to the external API.
Expand Down Expand Up @@ -314,6 +318,9 @@
allowRundownResetOnAir: apiStudioSettings.allowRundownResetOnAir,
preserveOrphanedSegmentPositionInRundown: apiStudioSettings.preserveOrphanedSegmentPositionInRundown,
minimumTakeSpan: apiStudioSettings.minimumTakeSpan ?? DEFAULT_MINIMUM_TAKE_SPAN,
enableQuickLoop: apiStudioSettings.enableQuickLoop,
forceQuickLoopAutoNext: forceQuickLoopAutoNextFrom(apiStudioSettings.forceQuickLoopAutoNext),
fallbackPartDuration: apiStudioSettings.fallbackPartDuration ?? DEFAULT_FALLBACK_PART_DURATION,

Check warning on line 323 in meteor/server/api/rest/v1/typeConversion.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/api/rest/v1/typeConversion.ts#L321-L323

Added lines #L321 - L323 were not covered by tests
}
}

Expand All @@ -330,6 +337,42 @@
allowRundownResetOnAir: settings.allowRundownResetOnAir,
preserveOrphanedSegmentPositionInRundown: settings.preserveOrphanedSegmentPositionInRundown,
minimumTakeSpan: settings.minimumTakeSpan,
enableQuickLoop: settings.enableQuickLoop,
forceQuickLoopAutoNext: APIForceQuickLoopAutoNextFrom(settings.forceQuickLoopAutoNext),
fallbackPartDuration: settings.fallbackPartDuration,
}
}

export function forceQuickLoopAutoNextFrom(
forceQuickLoopAutoNext: APIStudioSettings['forceQuickLoopAutoNext']
): ForceQuickLoopAutoNext | undefined {
if (!forceQuickLoopAutoNext) return undefined
switch (forceQuickLoopAutoNext) {
case 'disabled':
return ForceQuickLoopAutoNext.DISABLED
case 'enabled_forcing_min_duration':
return ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION
case 'enabled_when_valid_duration':
return ForceQuickLoopAutoNext.ENABLED_WHEN_VALID_DURATION
default:
assertNever(forceQuickLoopAutoNext)
return undefined
}
}

export function APIForceQuickLoopAutoNextFrom(
forceQuickLoopAutoNext: ForceQuickLoopAutoNext | undefined
): APIStudioSettings['forceQuickLoopAutoNext'] {
if (!forceQuickLoopAutoNext) return undefined
switch (forceQuickLoopAutoNext) {
case ForceQuickLoopAutoNext.DISABLED:
return 'disabled'
case ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION:
return 'enabled_forcing_min_duration'
case ForceQuickLoopAutoNext.ENABLED_WHEN_VALID_DURATION:
return 'enabled_when_valid_duration'
default:
assertNever(forceQuickLoopAutoNext)

Check warning on line 375 in meteor/server/api/rest/v1/typeConversion.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/api/rest/v1/typeConversion.ts#L340-L375

Added lines #L340 - L375 were not covered by tests
}
}

Expand Down
47 changes: 47 additions & 0 deletions meteor/server/api/userActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
import { IngestDataCache, Parts, Pieces, Rundowns } from '../collections'
import { IngestCacheType } from '@sofie-automation/corelib/dist/dataModel/IngestDataCache'
import { verifyHashedToken } from './singleUseTokens'
import { QuickLoopMarker } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
import { runIngestOperation } from './ingest/lib'
import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest'

Expand Down Expand Up @@ -1222,6 +1223,52 @@ class ServerUserActionAPI
)
}

async setQuickLoopStart(
userEvent: string,
eventTime: number,
playlistId: RundownPlaylistId,
marker: QuickLoopMarker | null
): Promise<ClientAPI.ClientResponse<void>> {
return ServerClientAPI.runUserActionInLogForPlaylistOnWorker(
this,
userEvent,
eventTime,
playlistId,
() => {
check(playlistId, String)
},
StudioJobs.SetQuickLoopMarker,
{
playlistId,
marker,
type: 'start',
}
)
}

async setQuickLoopEnd(
userEvent: string,
eventTime: number,
playlistId: RundownPlaylistId,
marker: QuickLoopMarker | null
): Promise<ClientAPI.ClientResponse<void>> {
return ServerClientAPI.runUserActionInLogForPlaylistOnWorker(
this,
userEvent,
eventTime,
playlistId,
() => {
check(playlistId, String)
},
StudioJobs.SetQuickLoopMarker,
{
playlistId,
marker,
type: 'end',
}
)
}

async createAdlibTestingRundownForShowStyleVariant(
userEvent: string,
eventTime: number,
Expand Down
3 changes: 3 additions & 0 deletions meteor/server/lib/rest/v1/studios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,8 @@
multiGatewayNowSafeLatency?: number
allowRundownResetOnAir?: boolean
preserveOrphanedSegmentPositionInRundown?: boolean
enableQuickLoop?: boolean
forceQuickLoopAutoNext?: 'disabled' | 'enabled_when_valid_duration' | 'enabled_forcing_min_duration'

Check warning on line 186 in meteor/server/lib/rest/v1/studios.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/lib/rest/v1/studios.ts#L185-L186

Added lines #L185 - L186 were not covered by tests
minimumTakeSpan?: number
fallbackPartDuration?: number

Check warning on line 188 in meteor/server/lib/rest/v1/studios.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/lib/rest/v1/studios.ts#L188

Added line #L188 was not covered by tests
}
2 changes: 2 additions & 0 deletions meteor/server/publications/_publications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import './pieceContentStatusUI/bucket/publication'
import './pieceContentStatusUI/rundown/publication'
import './organization'
import './partsUI/publication'
import './partInstancesUI/publication'

Check warning on line 13 in meteor/server/publications/_publications.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/publications/_publications.ts#L12-L13

Added lines #L12 - L13 were not covered by tests
import './peripheralDevice'
import './peripheralDeviceForDevice'
import './rundown'
Expand Down
148 changes: 148 additions & 0 deletions meteor/server/publications/lib/quickLoop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
import {
DBRundownPlaylist,
ForceQuickLoopAutoNext,
QuickLoopMarker,
QuickLoopMarkerType,
} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/corelib/dist/playout/playlist'
import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
import { DEFAULT_FALLBACK_PART_DURATION } from '@sofie-automation/shared-lib/dist/core/constants'
import { getCurrentTime } from '../../lib/lib'
import { generateTranslation } from '@sofie-automation/meteor-lib/dist/lib'
import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep'
import { ReactiveCacheCollection } from './ReactiveCacheCollection'

export function findPartPosition(
part: DBPart,
segmentRanks: Record<string, number>,
rundownRanks: Record<string, number>
): MarkerPosition {
return {
rundownRank: rundownRanks[part.rundownId as unknown as string] ?? 0,
segmentRank: segmentRanks[part.segmentId as unknown as string] ?? 0,
partRank: part._rank,
}
}

export function stringsToIndexLookup(strings: string[]): Record<string, number> {
return strings.reduce((result, str, index) => {
result[str] = index
return result
}, {} as Record<string, number>)
}

export function extractRanks(docs: { _id: ProtectedString<any>; _rank: number }[]): Record<string, number> {
return docs.reduce((result, doc) => {
result[doc._id as unknown as string] = doc._rank
return result
}, {} as Record<string, number>)
}

export function modifyPartForQuickLoop(
part: DBPart,
segmentRanks: Record<string, number>,
rundownRanks: Record<string, number>,
playlist: Pick<DBRundownPlaylist, 'quickLoop'>,
studio: Pick<DBStudio, 'settings'>,
quickLoopStartPosition: MarkerPosition | undefined,
quickLoopEndPosition: MarkerPosition | undefined,
canSetAutoNext = () => true
): void {
const partPosition = findPartPosition(part, segmentRanks, rundownRanks)
const isLoopDefined = quickLoopStartPosition && quickLoopEndPosition
const isLoopingOverriden =
isLoopDefined &&
playlist.quickLoop?.forceAutoNext !== ForceQuickLoopAutoNext.DISABLED &&
compareMarkerPositions(quickLoopStartPosition, partPosition) >= 0 &&
compareMarkerPositions(partPosition, quickLoopEndPosition) >= 0

const fallbackPartDuration = studio.settings.fallbackPartDuration ?? DEFAULT_FALLBACK_PART_DURATION

if (isLoopingOverriden && (part.expectedDuration ?? 0) < fallbackPartDuration) {
if (playlist.quickLoop?.forceAutoNext === ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION) {
part.expectedDuration = fallbackPartDuration
part.expectedDurationWithTransition = fallbackPartDuration
} else if (playlist.quickLoop?.forceAutoNext === ForceQuickLoopAutoNext.ENABLED_WHEN_VALID_DURATION) {
part.invalid = true
part.invalidReason = {
message: generateTranslation('Part duration is 0.'),
}
}
}
if (!canSetAutoNext()) return
part.autoNext = part.autoNext || (isLoopingOverriden && (part.expectedDuration ?? 0) > 0)
}

export function modifyPartInstanceForQuickLoop(
partInstance: Omit<DBPartInstance, 'part.privateData'>,
segmentRanks: Record<string, number>,
rundownRanks: Record<string, number>,
playlist: Pick<DBRundownPlaylist, 'quickLoop'>,
studio: Pick<DBStudio, 'settings'>,
quickLoopStartPosition: MarkerPosition | undefined,
quickLoopEndPosition: MarkerPosition | undefined
): void {
// note that the logic for when a part does not do autonext in quickloop should reflect the logic in the QuickLoopService in job worker
const canAutoNext = () => {
const start = partInstance.timings?.plannedStartedPlayback
if (start !== undefined && partInstance.part.expectedDuration) {
// date.now - start = playback duration, duration + offset gives position in part
const playbackDuration = getCurrentTime() - start

// If there is an auto next planned soon or was in the past
if (partInstance.part.expectedDuration - playbackDuration < 0) {
return false
}
}

return true
}

modifyPartForQuickLoop(
partInstance.part,
segmentRanks,
rundownRanks,
playlist,
studio,
quickLoopStartPosition,
quickLoopEndPosition,
canAutoNext // do not adjust the part instance if we have passed the time where we can still enable auto next
)
}

export function findMarkerPosition(
marker: QuickLoopMarker,
fallback: number,
segmentCache: ReadonlyObjectDeep<ReactiveCacheCollection<Pick<DBSegment, '_id' | '_rank' | 'rundownId'>>>,
partCache:
| { parts: ReadonlyObjectDeep<ReactiveCacheCollection<Pick<DBPart, '_id' | '_rank' | 'segmentId'>>> }
| { partInstances: ReadonlyObjectDeep<ReactiveCacheCollection<DBPartInstance>> },
rundownRanks: Record<string, number>
): MarkerPosition {
const part =
marker.type === QuickLoopMarkerType.PART
? 'parts' in partCache
? partCache.parts.findOne(marker.id)
: partCache.partInstances.findOne({ 'part._id': marker.id })?.part
: undefined
const partRank = part?._rank ?? fallback

const segmentId = marker.type === QuickLoopMarkerType.SEGMENT ? marker.id : part?.segmentId
const segment = segmentId && segmentCache.findOne(segmentId)
const segmentRank = segment?._rank ?? fallback

const rundownId = marker.type === QuickLoopMarkerType.RUNDOWN ? marker.id : segment?.rundownId
let rundownRank = rundownId ? rundownRanks[unprotectString(rundownId)] : fallback

if (marker.type === QuickLoopMarkerType.PLAYLIST) rundownRank = fallback

return {
rundownRank: rundownRank,
segmentRank: segmentRank,
partRank: partRank,
}
}

Check warning on line 148 in meteor/server/publications/lib/quickLoop.ts

View check run for this annotation

Codecov / codecov/patch

meteor/server/publications/lib/quickLoop.ts#L2-L148

Added lines #L2 - L148 were not covered by tests
Loading
Loading