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
9 changes: 9 additions & 0 deletions packages/caspar-graphics/src/client/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ const Template = ({
>
Update
</button>
<button
className={styles.button}
onClick={() => {
dispatch({ type: 'caspar-next', template: name })
}}
disabled={!enabled || !show}
>
Next
</button>
<Menu>
<MenuTrigger className={styles.button}>
<MdArrowDropDown />
Expand Down
15 changes: 14 additions & 1 deletion packages/caspar-graphics/src/client/TemplatePreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const States = {
stop: 3,
}

export const ServerTemplate = ({ socket, name, layer, show, data }) => {
export const ServerTemplate = ({ socket, name, layer, show, data, nextExecutionId }) => {
const [state, setState] = useState(States.load)
const [prevUpdate, setPrevUpdate] = useState()
const source = `/templates/${name}/index.html`
Expand Down Expand Up @@ -61,6 +61,12 @@ export const ServerTemplate = ({ socket, name, layer, show, data }) => {
}
}, [socket, show, state])

useEffect(() => {
if (socket && state === States.play && nextExecutionId) {
socket.send(JSON.stringify({ type: 'next', layer }))
}
}, [socket, state, layer, nextExecutionId])

return null
}

Expand All @@ -74,6 +80,7 @@ export const TemplatePreview = ({
src = `/templates/${name}/index.html`,
show,
data,
nextExecutionId,
}) => {
const [templateWindow, setTemplateWindow] = useState()
const [didShow, setDidShow] = useState(false)
Expand Down Expand Up @@ -117,6 +124,12 @@ export const TemplatePreview = ({
}
}, [templateWindow, onKeyDown])

useEffect(() => {
if (templateWindow?.next && nextExecutionId) {
templateWindow.next()
}
}, [templateWindow, nextExecutionId, name])

let width = containerSize.width
let height = containerSize.height

Expand Down
44 changes: 29 additions & 15 deletions packages/caspar-graphics/src/client/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ export function App({ name, templates: initialTemplates }) {
}

switch (evt.key) {
case 'n': {
// Trigger next for all enabled and playing templates
dispatch({ type: 'caspar-next-all' })
break
}
case 'a':
setPersistedState((persisted) => {
const ratios = Object.values(ASPECT_RATIOS)
Expand Down Expand Up @@ -171,12 +176,14 @@ export function App({ name, templates: initialTemplates }) {
containerSize={screenSize}
onKeyDown={onKeyDown}
{...template}
nextExecutionId={template.nextExecutionId}
/>
{serverState === 'connected' && (
<ServerTemplate
key={template.name}
socket={socket}
{...template}
nextExecutionId={template.nextExecutionId}
/>
)}
</Fragment>
Expand All @@ -197,35 +204,30 @@ function reducer(state, action) {
}
}

if (!action.template) {
if (!action.template && action.type !== 'caspar-next-all') {
console.warn('The action you just dispatched is missing template:', action)
return state
}

// Find template index for single-template actions
const index = state.templates.findIndex(
(template) => template.name === action.template,
)

if (index === -1) {
return state
}

const template = state.templates[index]

const updateTemplate = (data) => {
const updateTemplate = (data, idx = index) => {
const templates = [...state.templates]
templates[index] = { ...template, ...data }
templates[idx] = { ...state.templates[idx], ...data }
return { ...state, templates }
}
Comment on lines +217 to 221
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateTemplate function can be called with an invalid index when no template is found. The removed check for index === -1 means that updateTemplate will attempt to access state.templates[-1] which returns undefined, causing the spread operation to fail. Add a guard check before the switch statement to return early if index === -1 for single-template actions.

Copilot uses AI. Check for mistakes.

switch (action.type) {
case 'toggle-enabled':
return updateTemplate({
enabled: !template.enabled,
show: template.enabled ? false : template.show,
enabled: !state.templates[index].enabled,
show: state.templates[index].enabled ? false : state.templates[index].show,
})
case 'toggle-open':
return updateTemplate({ open: !template.open })
return updateTemplate({ open: !state.templates[index].open })
case 'show':
// TODO: wait for `removed` to arrive before allowing show a second time.
return updateTemplate({ show: true })
Expand All @@ -234,13 +236,13 @@ function reducer(state, action) {
case 'removed':
return updateTemplate({
state: States.loading,
removed: (template.removed ?? 0) + 1,
removed: (state.templates[index].removed ?? 0) + 1,
})
case 'preset-change':
const payload = { preset: action.preset }

if (action.update) {
payload.data = template.presets.find(
payload.data = state.templates[index].presets.find(
([key]) => key === action.preset,
)?.[1]
}
Expand All @@ -253,12 +255,23 @@ function reducer(state, action) {
case 'toggle-image':
return updateTemplate({
image:
template.image?.url === action.url
state.templates[index].image?.url === action.url
? null
: { url: action.url, opacity: 0.5 },
})
case 'select-tab':
return updateTemplate({ tab: action.tab })
case 'caspar-next': {
// Bump nextExecutionId to a new value (timestamp)
return updateTemplate({ nextExecutionId: Date.now() })
}
case 'caspar-next-all': {
// Trigger next for all enabled and playing templates
const templates = state.templates.map(t =>
t.enabled && t.show ? { ...t, nextExecutionId: Date.now() } : t
Comment on lines +270 to +271
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple templates triggered simultaneously with caspar-next-all will receive the same Date.now() timestamp. If the execution is fast enough, these identical timestamps may not trigger the useEffect dependencies properly for all templates. Consider using a more robust approach like incrementing unique IDs (e.g., Date.now() + index) to ensure each template gets a unique value.

Suggested change
const templates = state.templates.map(t =>
t.enabled && t.show ? { ...t, nextExecutionId: Date.now() } : t
const templates = state.templates.map((t, idx) =>
t.enabled && t.show ? { ...t, nextExecutionId: Date.now() + idx } : t

Copilot uses AI. Check for mistakes.
)
return { ...state, templates }
}
default:
return state
}
Expand Down Expand Up @@ -315,6 +328,7 @@ function getInitialState({ projectName, templates }) {
tab: templateSnapshot?.tab,
state: States.loading,
layer: layer ?? index,
nextExecutionId: null,
}
})
.sort((a, b) => a.layer - b.layer),
Expand Down
9 changes: 9 additions & 0 deletions packages/caspar-graphics/src/node/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ export async function createServer({ name, mode, host = 'localhost' }) {
connection.send(`CG`, `${channel}-${layer}`, `STOP`, 1)
}

function next(data) {
console.log('next', data)
if (connection) {
const { channel = connection.channel, layer } = data
connection.send(`CG`, `${channel}-${layer}`, `NEXT`)
}
}

client.on('message', (message) => {
const { type, ...data } = JSON.parse(message)

Expand All @@ -210,6 +218,7 @@ export async function createServer({ name, mode, host = 'localhost' }) {
update,
play,
stop,
next,
}[type]

if (fn) {
Expand Down
11 changes: 8 additions & 3 deletions packages/graphics-kit/src/TemplateProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const TemplateProvider = ({
const [resume, setResume] = useState()
const [requestPlay, setRequestPlay] = useState(false)
const [windowSize, setWindowSize] = useState(intitialWindowSize)
const [nextCount, setNextCount] = useState(0)

const logger = (message) => {
console.log(`${name || ''}${message}`)
Expand Down Expand Up @@ -75,27 +76,30 @@ export const TemplateProvider = ({

window.update = (payload) => {
const data = parse(payload)

if (data) {
logger(
`.update(${data ? JSON.stringify(data || {}, null, 2) : 'null'})`,
)

setData(data)

if (!didPlay) {
const delay = delayPlay('__initialData')
setResume(() => delay)
}
}
}

window.next = () => {
setNextCount((c) => c + 1)
logger('.next()')
}

return () => {
delete window.load
delete window.play
delete window.pause
delete window.stop
delete window.update
delete window.next
}
}, [])

Expand Down Expand Up @@ -133,6 +137,7 @@ export const TemplateProvider = ({
safeToRemove,
delayPlay,
size: windowSize,
nextCount,
}}
>
{state !== States.removed ? (
Expand Down
4 changes: 3 additions & 1 deletion packages/graphics-kit/src/use-caspar.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { States } from './constants'
import { useTimeout } from './use-timeout'

export const useCaspar = (opts) => {
const { state, safeToRemove, size, ...context } = useContext(TemplateContext)
const { state, safeToRemove, size, nextCount, ...context } =
useContext(TemplateContext)
const data = useCasparData(opts)

useTimeout(
Expand All @@ -27,6 +28,7 @@ export const useCaspar = (opts) => {
size,
aspectRatio: size.width / size.height,
data,
nextCount,
state,
safeToRemove,
isPlaying: state === States.playing,
Expand Down
Loading