Skip to content

Commit

Permalink
Implement hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
wkozyra95 committed Nov 20, 2024
1 parent be1e8d5 commit 8d0305d
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 14 deletions.
11 changes: 7 additions & 4 deletions ts/@live-compositor/core/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@ class Output {
this.shouldUpdateWhenReady = true;
};

const hasAudio = 'audio' in registerRequest && !!registerRequest.audio;
if (hasAudio) {
const supportsAudio = 'audio' in registerRequest && !!registerRequest.audio;
if (supportsAudio) {
this.initialAudioConfig = registerRequest.audio!.initial ?? { inputs: [] };
}

const onUpdate = () => this.throttledUpdate();
this.outputCtx = new _liveCompositorInternals.OutputContext(onUpdate, hasAudio);
this.outputCtx = new _liveCompositorInternals.OutputContext(onUpdate, {
supportsAudio,
offlineMode: false,
});

if (registerRequest.video) {
const rootElement = createElement(OutputRootComponent, {
Expand All @@ -61,7 +64,7 @@ class Output {
}

public scene(): { video?: Api.Video; audio?: Api.Audio } {
const audio = this.outputCtx.getAudioConfig() ?? this.initialAudioConfig;
const audio = this.outputCtx.audioContext.getAudioConfig() ?? this.initialAudioConfig;
return {
video: this.videoRenderer && { root: this.videoRenderer.scene() },
audio: audio && intoAudioInputsConfiguration(audio),
Expand Down
24 changes: 24 additions & 0 deletions ts/live-compositor/src/components/Delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type React from 'react';

import { useAfterTimestamp } from '../hooks.js';
import { LiveCompositorContext } from '../context/index.js';
import { useContext, useEffect, useState } from 'react';

export type DelayProps = {
durationMs: number;
children?: React.ReactNode;
};

function Delay(props: DelayProps) {
const ctx = useContext(LiveCompositorContext);
const [start, setStart] = useState<number>(Infinity);
useEffect(() => {
// TODO: fix for online processing
setStart(ctx.outputCtx.offlineContext?.timestamp() ?? 0);
}, []);

const isAfter = useAfterTimestamp(start + props.durationMs);
return isAfter ? props.children : null;
}

export default Delay;
21 changes: 21 additions & 0 deletions ts/live-compositor/src/components/Show.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type React from 'react';

import { useAfterTimestamp } from '../hooks.js';

export type ShowProps = {
timestamp: { start: number; end: number };
children?: React.ReactNode;
};

function Show(props: ShowProps) {
const afterStart = useAfterTimestamp(props.timestamp.start ?? 0);
const afterEnd = useAfterTimestamp(props.timestamp.end ?? Infinity);

if (afterStart && !afterEnd) {
return props.children;
} else {
return null;
}
}

export default Show;
2 changes: 1 addition & 1 deletion ts/live-compositor/src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createContext } from 'react';
import { InstanceContextStore } from './instanceContextStore.js';
import { OutputContext } from './outputContext.js';

type CompositorOutputContext = {
export type CompositorOutputContext = {
// global store for the entire LiveCompositor instance
instanceStore: InstanceContextStore;
// state specific to the current output
Expand Down
58 changes: 58 additions & 0 deletions ts/live-compositor/src/context/outputContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,28 @@ export type AudioInputConfig = {
volumeComponents: ContextAudioOptions[];
};

export type OutputContextOptions = {
supportsAudio: boolean;
offlineMode: boolean;
};

export interface BlockingTask {
done(): void;
}

export class OutputContext {
public readonly audioContext: AudioContext;
public readonly offlineContext?: OfflineContext;

constructor(onChange: () => void, opts: OutputContextOptions) {
this.audioContext = new AudioContext(onChange, opts.supportsAudio);
if (opts.offlineMode) {
this.offlineContext = new OfflineContext(onChange);
}
}
}

class AudioContext {
private audioMixerConfig?: AudioConfig;
private onChange: () => void;

Expand Down Expand Up @@ -69,3 +90,40 @@ export class OutputContext {
}
}
}

type TimestampObject = { timestamp: number };

class OfflineContext {
private timestamps: TimestampObject[];
private tasks: BlockingTask[];
private onChange: () => void;
private currentTimestamp: number = 0;

constructor(onChange: () => void) {
this.onChange = onChange;
this.tasks = [];
this.timestamps = [];
}

public timestamp(): number {
return this.currentTimestamp;
}

public newBlockingTask(): BlockingTask {
const task: BlockingTask = {} as any;
task.done = () => {
this.tasks = this.tasks.filter(t => t !== task);
this.onChange();
};
this.tasks.push();
return task;
}

public addTimestamp(timestamp: TimestampObject) {
this.timestamps.push(timestamp);
}

public removeTimestamp(timestamp: TimestampObject) {
this.timestamps = this.timestamps.filter(t => timestamp !== t);
}
}
65 changes: 56 additions & 9 deletions ts/live-compositor/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useContext, useEffect, useSyncExternalStore } from 'react';
import { useContext, useEffect, useState, useSyncExternalStore } from 'react';

import type * as Api from './api.js';
import type { CompositorOutputContext } from './context/index.js';
import { LiveCompositorContext } from './context/index.js';
import type { InputStreamInfo } from './context/instanceContextStore.js';
import type { BlockingTask } from './context/outputContext.js';

export function useInputStreams(): Record<Api.InputId, InputStreamInfo> {
const ctx = useContext(LiveCompositorContext);
Expand All @@ -26,22 +28,67 @@ export function useAudioInput(inputId: Api.InputId, audioOptions: AudioOptions)

useEffect(() => {
const options = { ...audioOptions };
ctx.outputCtx.addInputAudioComponent(inputId, options);
ctx.outputCtx.audioContext.addInputAudioComponent(inputId, options);
return () => {
ctx.outputCtx.removeInputAudioComponent(inputId, options);
ctx.outputCtx.audioContext.removeInputAudioComponent(inputId, options);
};
}, [audioOptions]);
}

/**
* Hook that allows you to trigger updates after specific timestamp. Primary useful for
* offline processing.
*
* For online processing it always returns false, this can change in the future.
*/
export function useAfterTimestamp(): Record<Api.InputId, InputStreamInfo> {
export function useAfterTimestamp(timestamp: number): boolean {
const ctx = useContext(LiveCompositorContext);
const instanceCtx = useSyncExternalStore(
ctx.instanceStore.subscribe,
ctx.instanceStore.getSnapshot
);
return instanceCtx.inputs;

useEffect(() => {
if (timestamp === Infinity) {
return;
}
const tsObject = { timestamp };
ctx.outputCtx.offlineContext?.addTimestamp(tsObject);
return () => {
ctx.outputCtx.offlineContext?.removeTimestamp(tsObject);
};
});

if (ctx.outputCtx.offlineContext) {
return ctx.outputCtx.offlineContext.timestamp() >= timestamp;
} else {
return false;
}
}

/**
* Create task that will stop rendering when compositor runs in offline mode.
*
* `task.done()` needs to be called when async action is finished, otherwise rendering will block indefinitely.
*/
export function newBlockingTask(ctx: CompositorOutputContext): BlockingTask {
if (ctx.outputCtx.offlineContext) {
return ctx.outputCtx.offlineContext.newBlockingTask();
} else {
return { done: () => null };
}
}

export function useBlockingTask<T>(fn: () => Promise<T>): T | undefined {
const ctx = useContext(LiveCompositorContext);
const [result, setResult] = useState<T>();
useEffect(() => {
const task = newBlockingTask(ctx);
void (async () => {
try {
setResult(await fn());
} finally {
task.done();
}
})();
return () => task.done();
}, []);

return result;
}

0 comments on commit 8d0305d

Please sign in to comment.