Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 21 additions & 0 deletions assets/sofie-rundown-editor-piece-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,26 @@
}
],
"id": "head"
},
{
"name": "Coming Up",
"shortName": "CUP",
"colour": "#9B59B6",
"includeTypeInName": true,
"payload": [
{
"id": "count",
"label": "Number of upcoming parts to show",
"type": "number",
"includeInName": true
},
{
"id": "style",
"label": "Display style (list/grid)",
"type": "string",
"includeInName": false
}
],
"id": "coming-up"
}
]
88 changes: 88 additions & 0 deletions assets/template/gfx/coming-up.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coming Up</title>
<style>
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: Arial, sans-serif;
font-size: 300%;
}

.coming-up {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.coming-up h2 {
color: white;
margin-top: 0;
}

#parts-list {
color: white;
list-style: none;
padding: 0;
}
</style>
</head>

<body>
<div class="coming-up">
<h2>Coming Up</h2>
<ul id="parts-list">
<!-- Parts will be injected here -->
<li>Place holder part</li>
</ul>
</div>
<script>
// CasparCG will call update() with the data
function update(data) {
var partsList = document.getElementById('parts-list');
partsList.innerHTML = '';

try {
if (typeof data === 'string') {
data = JSON.parse(data);
}
if (data && data.parts) {
data.parts.forEach(function (part) {
var li = document.createElement('li');
li.className = 'part';
li.textContent = part.number + '. ' + part.title;
partsList.appendChild(li);
});
} else {
var pre = document.createElement('pre');
pre.className = 'part';
pre.textContent = 'No upcoming parts available.\n' + JSON.stringify(data, null, 2);
partsList.appendChild(pre);
}
}
catch (e) {
var pre = document.createElement('pre');
pre.className = 'part';
pre.textContent = 'Error parsing data:\n' + e.message + '\n' + jsonData;
partsList.appendChild(pre);
}
}

// Optional: play/stop animations
function play() { }
function stop() { }
</script>
</body>

</html>
2 changes: 1 addition & 1 deletion packages/blueprints/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"typescript": "~5.8.3"
},
"dependencies": {
"@sofie-automation/blueprints-integration": "1.53.0-nightly-release53-20250924-143640-007a9da.0",
"@sofie-automation/blueprints-integration": "1.53.0-nightly-release53-20251210-154915-f7afd72.0",
"dereference-json-schema": "^0.2.1",
"object-path": "^0.11.8",
"type-fest": "^4.41.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum ActionId {
LastRemote = 'lastRemote',
LastDVE = 'lastDVE',
GFXStep = 'GFXStep',
ComingUp = 'comingUp',
}
109 changes: 109 additions & 0 deletions packages/blueprints/src/base/showstyle/executeActions/comingUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
IActionExecutionContext,
IBlueprintActionManifest,
ExtendedIngestRundown,
TSR,
PieceLifespan,
} from '@sofie-automation/blueprints-integration'
import { literal, t } from '../../../common/util.js'
import { SourceLayer, getOutputLayerForSourceLayer } from '../applyconfig/layers.js'
import { ActionId } from './actionDefinitions.js'
import { CasparCGLayers } from '../../studio/layers.js'
import { TimelineBlueprintExt } from '../../studio/customTypes.js'

/**
* Defines an AdLib Action that creates a "Coming Up" graphic showing the next 5 parts.
* Demonstrates the getUpcomingParts() method from the blueprint API.
*
* @param ingestRundown - {ExtendedIngestRundown}
* @returns IBlueprintActionManifest
*/
export const comingUpAdlibAction = (ingestRundown: ExtendedIngestRundown): IBlueprintActionManifest =>
literal<IBlueprintActionManifest>({
actionId: ActionId.ComingUp,
userData: {},
userDataManifest: {},
display: {
label: t('Coming Up'),
sourceLayerId: SourceLayer.GFX,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.GFX),
tags: ['coming-up'],
},
externalId: ingestRundown.externalId,
})

/**
* Executes the "Coming Up" action by fetching the upcoming parts and creating a graphic
* piece with their titles.
*
* This demonstrates the getUpcomingParts() method which is available in action contexts,
* onTake, and onSetAsNext contexts.
*
* @param context - {IActionExecutionContext}
*/
export async function executeComingUp(context: IActionExecutionContext): Promise<void> {
try {
// Fetch the next 5 parts using the new getUpcomingParts method
const upcomingParts = await context.getUpcomingParts(5)

if (upcomingParts.length === 0) {
context.notifyUserWarning('No upcoming parts found')
return
}

// Build the data for the graphic template
const partTitles = upcomingParts.map((part, index) => ({
number: index + 1,
title: part.title,
}))

// Create a graphic piece showing the upcoming parts
await context.insertPiece('current', {
externalId: `coming-up-${Date.now()}`,
name: `Coming Up: ${partTitles[0]?.title || 'Next Parts'}`,
sourceLayerId: SourceLayer.GFX,
outputLayerId: getOutputLayerForSourceLayer(SourceLayer.GFX),
lifespan: PieceLifespan.WithinPart,
content: {
timelineObjects: [
literal<TimelineBlueprintExt<TSR.TimelineContentCCGTemplate>>({
id: '',
enable: {
start: 0,
duration: 10000, // Show for 10 seconds by default
},
layer: CasparCGLayers.CasparCGClipPlayer1,
priority: 10, // Higher priority for adlib
content: {
deviceType: TSR.DeviceType.CASPARCG,
type: TSR.TimelineContentTypeCasparCg.TEMPLATE,
templateType: 'html',
name: 'gfx/coming-up', // This would be your actual template name
data: {
// Pass the upcoming parts data to the template
parts: partTitles,
count: partTitles.length,
},
useStopCommand: true,
},
}),
],
// Store the data in a way that can be previewed
templateData: {
parts: partTitles,
count: partTitles.length,
},
},
enable: {
start: 'now' as const,
},
})

context.logInfo(
`Coming Up graphic created with ${upcomingParts.length} parts: ${upcomingParts.map((p) => p.title).join(', ')}`
)
} catch (error) {
context.notifyUserError('Failed to create Coming Up graphic')
context.logError(`Error in executeComingUp: ${error}`)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { ActionId } from './actionDefinitions.js'
import { SourceLayer } from '../applyconfig/layers.js'
import { ExampleGFXStepActionOptions, executeGraphicNextStep } from './steppedGraphicExample.js'
import { executeComingUp } from './comingUp.js'

export async function executeAction(
context: IActionExecutionContext,
Expand Down Expand Up @@ -40,6 +41,8 @@ export async function executeAction(
await executeLastOnSourceLayer(context, SourceLayer.DVE)
} else if (actionId === ActionId.GFXStep) {
await executeGraphicNextStep(context, triggerMode, actionOptions as ExampleGFXStepActionOptions)
} else if (actionId === ActionId.ComingUp) {
await executeComingUp(context)
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/blueprints/src/base/showstyle/helpers/graphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function getGraphicSourceLayer(object: GraphicObjectBase): SourceLayer {
return SourceLayer.Ticker
} else if (object.clipName.match(/strap/i)) {
return SourceLayer.Strap
} else if (object.clipName.match(/fullscreen/i)) {
} else if (object.clipName.match(/fullscreen|coming-up/i)) {
return SourceLayer.GFX
} else {
return SourceLayer.LowerThird
Expand All @@ -35,7 +35,7 @@ function getGraphicTlLayer(object: GraphicObjectBase): CasparCGLayers {
return CasparCGLayers.CasparCGGraphicsTicker
} else if (object.clipName.match(/strap/i)) {
return CasparCGLayers.CasparCGGraphicsStrap
} else if (object.clipName.match(/fullscreen/i)) {
} else if (object.clipName.match(/fullscreen|coming-up/i)) {
return CasparCGLayers.CasparCGClipPlayer1
} else {
return CasparCGLayers.CasparCGGraphicsLowerThird
Expand Down
2 changes: 2 additions & 0 deletions packages/blueprints/src/base/showstyle/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getAbResolverConfiguration } from './getAbResolverConfiguration.js'
import { getRundown } from './rundown/index.js'
import { validateConfig } from './validateConfig.js'
import { applyConfig } from './applyconfig/index.js'
import { onSetAsNext } from './onSetAsNext.js'
import * as ConfigSchema from '../../$schemas/main-showstyle-config.json'
import { dereferenceSync } from 'dereference-json-schema'

Expand All @@ -40,6 +41,7 @@ export const baseManifest: Omit<ShowStyleBlueprintManifest<ShowStyleConfig>, 'bl

validateConfig,
applyConfig,
onSetAsNext,
onRundownActivate: async (_context: IRundownActivationContext) => {
// Noop
},
Expand Down
86 changes: 86 additions & 0 deletions packages/blueprints/src/base/showstyle/onSetAsNext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { IOnSetAsNextContext } from '@sofie-automation/blueprints-integration'

/**
* Called when a part is set as Next.
* This is used to dynamically update "Coming Up" pieces with actual upcoming part data.
*/
export async function onSetAsNext(context: IOnSetAsNextContext): Promise<void> {
try {
// Get pieces in the next part
const nextPart = await context.getPartInstance('next')
if (!nextPart) {
context.logDebug('onSetAsNext: No next part')
return
}

const pieces = await context.getPieceInstances('next')
context.logDebug(`onSetAsNext: Found ${pieces.length} pieces in next part`)

// Find any "Coming Up" pieces (identified by template name or source layer)
const comingUpPieces = pieces.filter((p) => {
const timelineObjs = p.piece.content.timelineObjects as any[]
const hasComingUpTemplate = timelineObjs?.some((obj) => obj.content?.name?.includes('coming-up'))
const isComingUpName = p.piece.name?.toLowerCase().includes('coming up')

context.logDebug(`Piece ${p._id}: name="${p.piece.name}", hasComingUpTemplate=${hasComingUpTemplate}`)

return hasComingUpTemplate || isComingUpName
})

context.logInfo(`onSetAsNext: Found ${comingUpPieces.length} Coming Up pieces`)
if (comingUpPieces.length === 0) return

// Fetch upcoming parts
const upcomingParts = await context.getUpcomingParts(5)

if (upcomingParts.length === 0) {
context.logInfo('No upcoming parts found for Coming Up piece')
return
}

// Build the data
const partTitles = upcomingParts.map((part, index) => ({
number: index + 1,
title: part.title,
}))

// Update each Coming Up piece with the actual data
for (const pieceInstance of comingUpPieces) {
const piece = pieceInstance.piece
const timelineObjs = piece.content.timelineObjects as any[]

// Find and recreate the Coming Up timeline object with new data
const newTimelineObjs = timelineObjs?.map((obj) => {
if (obj.content?.name?.includes('coming-up')) {
// Create a new timeline object with updated data
return {
...obj,
content: {
...obj.content,
data: {
...obj.content.data,
parts: partTitles,
count: partTitles.length,
},
},
}
}
return obj
})

if (newTimelineObjs) {
await context.updatePieceInstance(pieceInstance._id, {
content: {
timelineObjects: newTimelineObjs,
},
})
}
}

context.logInfo(
`Updated Coming Up piece with ${upcomingParts.length} parts: ${upcomingParts.map((p) => p.title).join(', ')}`
)
} catch (error) {
context.logError(`Error in onSetAsNext: ${error}`)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { literal, t } from '../../../common/util.js'
import { getOutputLayerForSourceLayer, SourceLayer } from '../applyconfig/layers.js'
import { ActionId } from '../executeActions/actionDefinitions.js'
import { exampleGraphicNextStepAdlibAction } from '../executeActions/steppedGraphicExample.js'
import { comingUpAdlibAction } from '../executeActions/comingUp.js'

export function getGlobalActions(
_context: IShowStyleUserContext,
Expand Down Expand Up @@ -36,5 +37,6 @@ export function getGlobalActions(
externalId: ingestRundown.externalId,
}),
exampleGraphicNextStepAdlibAction(ingestRundown),
comingUpAdlibAction(ingestRundown),
]
}
Loading
Loading