Skip to content

Instantly share code, notes, and snippets.

@bryanjswift
Last active November 9, 2019 01:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bryanjswift/9637ed3042c3ff8ca109b49df60470b5 to your computer and use it in GitHub Desktop.
Save bryanjswift/9637ed3042c3ff8ca109b49df60470b5 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
/**
* For a given `state` from `CompositeMachine` should the video indicated by
* `state.context.videoSrc` be in a playback state or a paused state.
* @param state node from the `CompositeMachine`.
* @returns `false` (indicating a playing state) or `true` (indicating a paused
* state). `null` or `undefined` state nodes are `true`.
*/
function isVideoPaused(state) {
if (state === null || state === undefined) {
return true;
}
switch (state.value) {
case 'starting':
case 'startRecording':
case 'recording':
return false;
default:
return true;
}
}
/**
* State machine tracking the parallel state of the components that load
* asynchronously. Triggers a "done" state when each of the components have
* indicated readiness. This is sort of like `Promise.all` for the readiness of
* these components.
*/
const CompositeComponentsLoadingMachine = Machine({
id: 'composite-components-loading',
type: 'parallel',
states: {
pixi: {
type: 'compound',
initial: 'pending',
states: {
pending: {
on: {
PIXI_READY: {
target: 'success',
},
},
},
success: {
type: 'final',
},
},
},
video: {
type: 'compound',
initial: 'pending',
states: {
pending: {
on: {
VIDEO_READY: {
target: 'success',
},
},
},
success: {
type: 'final',
},
},
},
},
});
/**
* State machine tracking the state of the components that load asynchronously.
* Enters a "mounted" state when each of the components have indicated
* readiness and then transitions to a "done" state when `PIXI_INIT` is
* completed.
*/
const CompositeComponentsMachine = Machine({
id: 'composite-components',
initial: 'ssr',
states: {
ssr: {
invoke: {
id: 'components-loader',
src: CompositeComponentsLoadingMachine,
onDone: 'mounted',
},
on: {
PIXI_READY: {
actions: [send('PIXI_READY', { to: 'components-loader' })],
},
VIDEO_READY: {
actions: [send('VIDEO_READY', { to: 'components-loader' })],
},
},
},
mounted: {
on: {
PIXI_INIT: {
target: 'ready',
},
},
},
ready: {
type: 'final',
},
},
});
/**
* Track the state of compositing through the lifetime of page setup and
* recorded composite.
*/
const CompositeMachine = Machine(
{
id: 'composite',
initial: 'ssr',
context: {
audioStream: undefined, // MediaStream
canvasDisplay: undefined, // CanvasDisplay - Svelte Component Instance
error: undefined,
framesData: undefined, // Record from getFramesData
recorder: undefined, // VideoRecorderService
sourceVideo: undefined, // Video|Canvas Element - Used for compositing
videoData: undefined, // Blob
videoSrc: undefined, // string
},
states: {
ssr: {
on: {
MOUNT: {
target: 'mounted',
actions: ['setSources'],
},
},
},
mounted: {
on: {
NO_MEDIA: {
target: 'missingMedia',
},
GET_FRAMES: {
target: 'getFrames',
cond: 'hasFramesId',
},
GET_MEDIA: {
target: 'getMedia',
cond: 'hasVideoId',
},
},
},
getFrames: {
invoke: {
id: 'getFrames',
src: (context, event) => getFramesData(event.framesId),
onDone: {
target: 'pending',
actions: ['setFramesContext'],
},
onError: {
target: 'missingMedia',
actions: ['setError'],
},
},
},
getMedia: {
invoke: {
id: 'getMedia',
src: (context, event) => getVideoBlob(event.videoId),
onDone: {
target: 'pending',
actions: ['setVideoContext'],
},
onError: {
target: 'missingMedia',
actions: ['setError'],
},
},
},
missingMedia: {
on: {
GET_FRAMES: {
target: 'getFrames',
cond: 'hasFramesId',
},
GET_MEDIA: {
target: 'getMedia',
cond: 'hasVideoId',
},
},
},
pending: {
on: {
COMPONENTS_READY: {
target: 'preloading',
actions: ['setRecorder'],
},
},
},
preloading: {
on: {
ANIMATION_READY: 'ready',
},
},
ready: {
on: {
GET_MEDIA: 'getMedia',
PLAY: 'starting',
},
},
starting: {
on: {
RECORD: {
actions: ['setRecorder'],
target: 'startRecording',
},
},
},
startRecording: {
invoke: {
id: 'startRecording',
src: (context, event) => {
const { recorder } = context;
return recorder.startRecording();
},
onDone: {
target: 'recording',
},
onError: {
target: 'recordError',
actions: ['setError'],
},
},
},
recordError: {
type: 'final',
},
recording: {
on: {
STOP: {
target: 'awaitingFrames',
actions: ['stopRecording'],
},
},
},
awaitingFrames: {
on: {
FRAMES: {
target: 'framesSetup',
actions: ['setFramesContext'],
},
},
},
framesSetup: {
entry: ['disposeRecorder'],
on: {
FRAMES_WAIT: {
target: 'framesRecordWaiting',
actions: ['setRecorder'],
},
},
},
framesRecordWaiting: {
on: {
FRAMES_PLAY: 'framesRecordStarting',
},
},
framesRecordStarting: {
on: {
FRAMES_RECORD: 'framesRecord',
},
},
framesRecord: {
// This state is only reachable if the source data is a video
invoke: {
id: 'startRecording',
src: (context, event) => {
const { recorder } = context;
const { canvasDisplay, sourceVideo } = event;
const videoStream = sourceVideo.mozCaptureStream
? sourceVideo.mozCaptureStream()
: sourceVideo.captureStream();
const canvasStream = canvasDisplay.canvas.mozCaptureStream
? canvasDisplay.canvas.mozCaptureStream()
: canvasDisplay.canvas.captureStream();
canvasDisplay.play();
sourceVideo.play();
return recorder.startRecording(canvasStream, videoStream);
},
onDone: {
target: 'framesRecording',
},
onError: {
target: 'recordError',
actions: ['setError'],
},
},
},
framesRecording: {
on: {
FRAMES_STOP: {
target: 'ready',
actions: ['stopRecording'],
},
},
},
},
},
{
actions: {
disposeRecorder(context) {
if (context.recorder) {
context.recorder.dispose();
}
},
log(context, event) {
console.log('actions:log', context, event);
},
setError: assign({ error: (context, event) => event.data }),
setFramesContext: assign({ framesData: (context, event) => event.data }),
setRecorder: assign({
recorder: (context, event) => event.recorder || context.recorder,
}),
setSources: assign({
audioStream: (context, event) => event.audioStream,
canvasDisplay: (context, event) => event.canvasDisplay,
sourceVideo: (context, event) => event.sourceVideo,
}),
setVideoContext: assign({
videoData: (context, event) => event.data,
videoSrc: (context, event) => URL.createObjectURL(event.data),
}),
stopRecording(context, event) {
context.recorder.stopRecording();
},
},
guards: {
hasFramesId(context, event) {
return typeof event.framesId === 'string';
},
hasRecorder(context, event) {
return typeof context.recorder !== 'undefined';
},
hasVideoId(context, event) {
return typeof event.videoId === 'string';
},
},
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment