diff --git a/packages/caspar-graphics/src/client/Sidebar.jsx b/packages/caspar-graphics/src/client/Sidebar.jsx index 02eb92b..3f76379 100644 --- a/packages/caspar-graphics/src/client/Sidebar.jsx +++ b/packages/caspar-graphics/src/client/Sidebar.jsx @@ -172,6 +172,15 @@ const Template = ({ > Update + diff --git a/packages/caspar-graphics/src/client/TemplatePreview.jsx b/packages/caspar-graphics/src/client/TemplatePreview.jsx index 114faff..d7b7413 100644 --- a/packages/caspar-graphics/src/client/TemplatePreview.jsx +++ b/packages/caspar-graphics/src/client/TemplatePreview.jsx @@ -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` @@ -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 } @@ -74,6 +80,7 @@ export const TemplatePreview = ({ src = `/templates/${name}/index.html`, show, data, + nextExecutionId, }) => { const [templateWindow, setTemplateWindow] = useState() const [didShow, setDidShow] = useState(false) @@ -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 diff --git a/packages/caspar-graphics/src/client/app.jsx b/packages/caspar-graphics/src/client/app.jsx index f0f6b92..e9164e9 100644 --- a/packages/caspar-graphics/src/client/app.jsx +++ b/packages/caspar-graphics/src/client/app.jsx @@ -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) @@ -171,12 +176,14 @@ export function App({ name, templates: initialTemplates }) { containerSize={screenSize} onKeyDown={onKeyDown} {...template} + nextExecutionId={template.nextExecutionId} /> {serverState === 'connected' && ( )} @@ -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 } } 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 }) @@ -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] } @@ -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 + ) + return { ...state, templates } + } default: return state } @@ -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), diff --git a/packages/caspar-graphics/src/node/server.js b/packages/caspar-graphics/src/node/server.js index d36949f..19454af 100644 --- a/packages/caspar-graphics/src/node/server.js +++ b/packages/caspar-graphics/src/node/server.js @@ -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) @@ -210,6 +218,7 @@ export async function createServer({ name, mode, host = 'localhost' }) { update, play, stop, + next, }[type] if (fn) { diff --git a/packages/graphics-kit/src/TemplateProvider.jsx b/packages/graphics-kit/src/TemplateProvider.jsx index 5691f72..cdbfe54 100644 --- a/packages/graphics-kit/src/TemplateProvider.jsx +++ b/packages/graphics-kit/src/TemplateProvider.jsx @@ -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}`) @@ -75,14 +76,11 @@ 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) @@ -90,12 +88,18 @@ export const TemplateProvider = ({ } } + 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 } }, []) @@ -133,6 +137,7 @@ export const TemplateProvider = ({ safeToRemove, delayPlay, size: windowSize, + nextCount, }} > {state !== States.removed ? ( diff --git a/packages/graphics-kit/src/use-caspar.js b/packages/graphics-kit/src/use-caspar.js index 5c43c6f..ab67fe1 100644 --- a/packages/graphics-kit/src/use-caspar.js +++ b/packages/graphics-kit/src/use-caspar.js @@ -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( @@ -27,6 +28,7 @@ export const useCaspar = (opts) => { size, aspectRatio: size.width / size.height, data, + nextCount, state, safeToRemove, isPlaying: state === States.playing,