Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a18ae78
Merge branch 'bbc-release53' into upstream release53
olzzon Jul 3, 2025
350b556
update: to latest context-menu
olzzon Jul 3, 2025
99ec63c
Merge remote-tracking branch 'upstream/release53' into bbc-release53
PeterC89 Jul 6, 2025
f10f18c
Merge remote-tracking branch 'origin/release53' into bbc-release53
PeterC89 Jul 16, 2025
a509895
fix: constant re-rendering and possible lost evenlisteners in hover
olzzon Jul 30, 2025
facbdc0
fix: hover only update ref when changed
olzzon Jul 30, 2025
fc855b7
Merge branch 'release53' into bbc-release53
PeterC89 Aug 20, 2025
204ca61
fix: trigger postMessage when changed while already showing iframePre…
olzzon Aug 27, 2025
6b778ca
Merge remote-tracking branch 'origin/release53' into bbc-release53
PeterC89 Aug 27, 2025
9eff487
wip: additional layer info on HoverPreviews
olzzon Sep 3, 2025
7eb12a5
wip: move additionalPreviewContent inside popPpPreview content
olzzon Sep 4, 2025
a497576
wip: LayerInfoPreview handle in,out,duration as string and numbers
olzzon Sep 4, 2025
0fd2a0d
wip: Hover Preview styling
olzzon Sep 4, 2025
3f26c87
wip: additionalPreviewContent css fonts
olzzon Sep 4, 2025
092fd52
wip: mini shelf align thumbnail
olzzon Sep 5, 2025
f526397
chore: Updated hover-pop typography, plus letter case on duration str…
hummelstrand Sep 8, 2025
6fece09
fix: direction rtl would move any dots from beginning of string to en…
olzzon Sep 10, 2025
83f5032
Merge branch 'upstream/feat/additional-layinfo-on-hover' into bbc-rel…
olzzon Sep 10, 2025
0088941
Merge branch 'release53' into bbc-release53
PeterC89 Oct 1, 2025
a72ef05
chore: refactor SourceLayerItem component
Nov 13, 2024
e01e580
feat: retime piece user action
Nov 15, 2024
831c538
feat: edit mode for drag operations
Dec 18, 2024
4843d3a
Return more information when bad blueprint upload happens
rjmunro Sep 19, 2025
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
9 changes: 6 additions & 3 deletions .github/workflows/node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -502,16 +502,19 @@ jobs:
- node-version: 22.x
package-name: job-worker
send-coverage: true
# No tests for the gateways yet
# No tests for some gateways yet
# - node-version: 22.x
# package-name: playout-gateway
# - node-version: 22.x
# package-name: mos-gateway
# send-coverage: true
- node-version: 22.x
package-name: mos-gateway
send-coverage: true
- node-version: 22.x
package-name: live-status-gateway
send-coverage: true
- node-version: 22.x
package-name: webui
send-coverage: true
# manual meteor-lib as it only needs a couple of versions
- node-version: 22.x
package-name: meteor-lib
Expand Down
1 change: 1 addition & 0 deletions meteor/__mocks__/helpers/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ export async function setupMockShowStyleBlueprint(
rundown,
globalAdLibPieces: [],
globalActions: [],
globalPieces: [],
baseline: { timelineObjects: [] },
}
},
Expand Down
9 changes: 7 additions & 2 deletions meteor/server/api/blueprints/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions'
import { assertConnectionHasOneOfPermissions, RequestCredentials } from '../../security/auth'
import { blueprintsPerformDevelopmentMode } from './development'
import { inspect } from 'util'

const PERMISSIONS_FOR_MANAGE_BLUEPRINTS: Array<keyof UserPermissions> = ['configure']

Expand Down Expand Up @@ -174,8 +175,12 @@ async function innerUploadBlueprint(
let blueprintManifest: SomeBlueprintManifest | undefined
try {
blueprintManifest = evalBlueprint(newBlueprint)
} catch (_e) {
throw new Meteor.Error(400, `Blueprint ${blueprintId} failed to parse`)
} catch (error) {
console.log('Parsing error:', error)
throw new Meteor.Error(
400,
`Blueprint ${blueprintId} failed to parse; error: ${(error as Error).message}.\n${inspect(error, { depth: 5 })}`
)
}

if (!_.isObject(blueprintManifest))
Expand Down
5 changes: 3 additions & 2 deletions meteor/server/api/deviceTriggers/TagsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceIns
import { PieceInstanceFields, ContentCache } from './reactiveContentCacheForPieceInstances'
import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
import {
createPartCurrentTimes,
PieceInstanceWithTimings,
processAndPrunePieceInstanceTimings,
} from '@sofie-automation/corelib/dist/playout/processAndPrune'
import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
import { IWrappedAdLib } from '@sofie-automation/meteor-lib/dist/triggers/actionFilterChainCompilers'
import { areSetsEqual, doSetsIntersect } from '@sofie-automation/corelib/dist/lib'
import { getCurrentTime } from '../../lib/lib'

export class TagsService {
protected onAirPiecesTags: Set<string> = new Set()
Expand Down Expand Up @@ -130,12 +132,11 @@ export class TagsService {
): PieceInstanceWithTimings[] {
// Approximate when 'now' is in the PartInstance, so that any adlibbed Pieces will be timed roughly correctly
const partStarted = partInstanceTimings?.plannedStartedPlayback
const nowInPart = partStarted === undefined ? 0 : Date.now() - partStarted

return processAndPrunePieceInstanceTimings(
sourceLayers,
pieceInstances as PieceInstance[],
nowInPart,
createPartCurrentTimes(getCurrentTime(), partStarted),
false,
false
)
Expand Down
1 change: 1 addition & 0 deletions meteor/server/api/ingest/packageInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function onUpdatedPackageInfo(packageId: ExpectedPackageId, _doc: P
case ExpectedPackageDBType.ADLIB_ACTION:
case ExpectedPackageDBType.BASELINE_ADLIB_PIECE:
case ExpectedPackageDBType.BASELINE_ADLIB_ACTION:
case ExpectedPackageDBType.BASELINE_PIECE:
case ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS:
onUpdatedPackageInfoForRundownDebounce(pkg)
break
Expand Down
2 changes: 2 additions & 0 deletions meteor/server/api/rest/v1/typeConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ export function studioSettingsFrom(apiStudioSettings: APIStudioSettings): Comple
enableBuckets: apiStudioSettings.enableBuckets ?? true, // Backwards compatible
enableEvaluationForm: apiStudioSettings.enableEvaluationForm ?? true, // Backwards compatible
mockPieceContentStatus: apiStudioSettings.mockPieceContentStatus,
rundownGlobalPiecesPrepareTime: apiStudioSettings.rundownGlobalPiecesPrepareTime,
}
}

Expand Down Expand Up @@ -423,6 +424,7 @@ export function APIStudioSettingsFrom(settings: IStudioSettings): Complete<APISt
enableBuckets: settings.enableBuckets,
enableEvaluationForm: settings.enableEvaluationForm,
mockPieceContentStatus: settings.mockPieceContentStatus,
rundownGlobalPiecesPrepareTime: settings.rundownGlobalPiecesPrepareTime,
}
}

Expand Down
3 changes: 2 additions & 1 deletion meteor/server/collections/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ export interface AsyncOnlyReadOnlyMongoCollection<DBInterface extends { _id: Pro
observeChanges(
selector: MongoQuery<DBInterface> | DBInterface['_id'],
callbacks: PromisifyCallbacks<ObserveChangesCallbacks<DBInterface>>,
options?: Omit<FindOptions<DBInterface>, 'fields'>
findOptions?: Omit<FindOptions<DBInterface>, 'fields'>,
callbackOptions?: { nonMutatingCallbacks?: boolean | undefined }
): Promise<Meteor.LiveQueryHandle>

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ export class WrappedAsyncMongoCollection<DBInterface extends { _id: ProtectedStr
async observeChanges(
selector: MongoQuery<DBInterface> | DBInterface['_id'],
callbacks: PromisifyCallbacks<ObserveChangesCallbacks<DBInterface>>,
options?: FindOptions<DBInterface>
findOptions?: FindOptions<DBInterface>,
callbackOptions?: { nonMutatingCallbacks?: boolean | undefined }
): Promise<Meteor.LiveQueryHandle> {
const span = profiler.startSpan(`MongoCollection.${this.name}.observeChanges`)
if (span) {
Expand All @@ -152,8 +153,8 @@ export class WrappedAsyncMongoCollection<DBInterface extends { _id: ProtectedStr
}
try {
const res = await this._collection
.find((selector ?? {}) as any, options as any)
.observeChangesAsync(callbacks)
.find((selector ?? {}) as any, findOptions as any)
.observeChangesAsync(callbacks, callbackOptions)
if (span) span.end()
return res
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions meteor/server/lib/rest/v1/studios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,5 @@ export interface APIStudioSettings {
enableBuckets?: boolean
enableEvaluationForm?: boolean
mockPieceContentStatus?: boolean
rundownGlobalPiecesPrepareTime?: number
}
2 changes: 1 addition & 1 deletion meteor/server/migration/1_50_0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ export const addSteps = addMigrationSteps('1.50.0', [
PartId
>()
for (const piece of pieces) {
partIdLookup.set(piece._id, piece.startPartId)
if (piece.startPartId) partIdLookup.set(piece._id, piece.startPartId)
}
for (const adlib of adlibPieces) {
if (adlib.partId) partIdLookup.set(adlib._id, adlib.partId)
Expand Down
1 change: 1 addition & 0 deletions meteor/server/publications/_publications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import './lib/lib'

import './buckets'
import './blueprintUpgradeStatus/publication'
import './ingestStatus/publication'
import './packageManager/expectedPackages/publication'
import './packageManager/packageContainers'
import './packageManager/playoutContext'
Expand Down
191 changes: 191 additions & 0 deletions meteor/server/publications/ingestStatus/createIngestRundownStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import type { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache'
import {
IngestRundownStatus,
IngestPartPlaybackStatus,
IngestRundownActiveStatus,
IngestPartStatus,
IngestPartNotifyItemReady,
} from '@sofie-automation/shared-lib/dist/ingest/rundownStatus'
import type { ReadonlyDeep } from 'type-fest'
import _ from 'underscore'
import type { ContentCache, PartCompact, PartInstanceCompact, PlaylistCompact } from './reactiveContentCache'
import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection'
import { unprotectString } from '@sofie-automation/corelib/dist/protectedString'

export function createIngestRundownStatus(
cache: ReadonlyDeep<ContentCache>,
rundownId: RundownId
): IngestRundownStatus | null {
const rundown = cache.Rundowns.findOne(rundownId)
if (!rundown) return null

const newDoc: IngestRundownStatus = {
_id: rundownId,
externalId: rundown.externalId,

active: IngestRundownActiveStatus.INACTIVE,

segments: [],
}

const playlist = cache.Playlists.findOne({
_id: rundown.playlistId,
activationId: { $exists: true },
})

if (playlist) {
newDoc.active = playlist.rehearsal ? IngestRundownActiveStatus.REHEARSAL : IngestRundownActiveStatus.ACTIVE
}

const nrcsSegments = cache.NrcsIngestData.find({ rundownId, type: NrcsIngestCacheType.SEGMENT }).fetch()
for (const nrcsSegment of nrcsSegments) {
const nrcsParts = cache.NrcsIngestData.find({
rundownId,
segmentId: nrcsSegment.segmentId,
type: NrcsIngestCacheType.PART,
}).fetch()

newDoc.segments.push({
externalId: nrcsSegment.data.externalId,
parts: _.compact(
nrcsParts.map((nrcsPart) => {
if (!nrcsPart.partId || !nrcsPart.segmentId) return null

const parts = cache.Parts.find({
rundownId: rundownId,
$or: [
{
externalId: nrcsPart.data.externalId,
ingestNotifyPartExternalId: { $exists: false },
},
{
ingestNotifyPartExternalId: nrcsPart.data.externalId,
},
],
}).fetch()
const partInstances = findPartInstancesForIngestPart(
playlist,
rundownId,
cache.PartInstances,
nrcsPart.data.externalId
)

return createIngestPartStatus(playlist, partInstances, parts, nrcsPart.data.externalId)
})
),
})
}

return newDoc
}

function findPartInstancesForIngestPart(
playlist: PlaylistCompact | undefined,
rundownId: RundownId,
partInstancesCache: ReadonlyDeep<ReactiveCacheCollection<PartInstanceCompact>>,
partExternalId: string
) {
const result: Record<string, PartInstanceCompact> = {}
if (!playlist) return result

const candidatePartInstances = partInstancesCache
.find({
rundownId: rundownId,
$or: [
{
'part.externalId': partExternalId,
'part.ingestNotifyPartExternalId': { $exists: false },
},
{
'part.ingestNotifyPartExternalId': partExternalId,
},
],
})
.fetch()

for (const partInstance of candidatePartInstances) {
if (partInstance.rundownId !== rundownId) continue
// Ignore the next partinstance
if (partInstance._id === playlist.nextPartInfo?.partInstanceId) continue

const partId = unprotectString(partInstance.part._id)

// The current part instance is the most important
if (partInstance._id === playlist.currentPartInfo?.partInstanceId) {
result[partId] = partInstance
continue
}

// Take the part with the highest takeCount
const existingEntry = result[partId]
if (!existingEntry || existingEntry.takeCount < partInstance.takeCount) {
result[partId] = partInstance
}
}

return result
}

function createIngestPartStatus(
playlist: PlaylistCompact | undefined,
partInstances: Record<string, PartInstanceCompact>,
parts: PartCompact[],
ingestPartExternalId: string
): IngestPartStatus {
// Determine the playback status from the PartInstance
let playbackStatus = IngestPartPlaybackStatus.UNKNOWN

let isReady: boolean | null = null // Start off as null, the first value will make this true or false

const itemsReady: IngestPartNotifyItemReady[] = []

const updateStatusWithPart = (part: PartCompact) => {
// If the part affects the ready status, update it
if (typeof part.ingestNotifyPartReady === 'boolean') {
isReady = (isReady ?? true) && part.ingestNotifyPartReady
}

// Include the items
if (part.ingestNotifyItemsReady) {
itemsReady.push(...part.ingestNotifyItemsReady)
}
}

// Loop through the partInstances, starting off the state
if (playlist) {
for (const partInstance of Object.values<PartInstanceCompact>(partInstances)) {
if (!partInstance) continue

if (partInstance.part.shouldNotifyCurrentPlayingPart) {
const isCurrentPartInstance = playlist.currentPartInfo?.partInstanceId === partInstance._id

if (isCurrentPartInstance) {
// If the current, it is playing
playbackStatus = IngestPartPlaybackStatus.PLAY
} else if (playbackStatus === IngestPartPlaybackStatus.UNKNOWN) {
// If not the current, but has been played, it is stopped
playbackStatus = IngestPartPlaybackStatus.STOP
}
}

updateStatusWithPart(partInstance.part)
}
}

for (const part of parts) {
// Check if the part has already been handled by a partInstance
if (partInstances[unprotectString(part._id)]) continue

updateStatusWithPart(part)
}

return {
externalId: ingestPartExternalId,

isReady: isReady,
itemsReady: itemsReady,

playbackStatus,
}
}
Loading
Loading