Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
wkozyra95 committed Nov 22, 2024
1 parent 166b275 commit 2577b7b
Show file tree
Hide file tree
Showing 17 changed files with 338 additions and 250 deletions.
6 changes: 3 additions & 3 deletions build_tools/nix/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
rust-analyzer

clang-tools
llvmPackages_18.bintools
llvmPackages.bintools
];
in
{
Expand All @@ -77,7 +77,7 @@
# Fixes "ffplay" used in examples on Linux (not needed on NixOS)
env.LIBGL_DRIVERS_PATH = "${pkgs.mesa.drivers}/lib/dri";

env.LIBCLANG_PATH = "${pkgs.llvmPackages_16.libclang.lib}/lib";
env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
env.LD_LIBRARY_PATH = lib.makeLibraryPath (libcefDependencies ++ [ pkgs.mesa.drivers pkgs.libGL pkgs.blackmagic-desktop-video ]);

inputsFrom = [ packageWithoutChromium ];
Expand All @@ -89,7 +89,7 @@
nixos = pkgs.mkShell {
packages = devDependencies ++ [ pkgs.blackmagic-desktop-video];

env.LIBCLANG_PATH = "${pkgs.llvmPackages_16.libclang.lib}/lib";
env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
env.LD_LIBRARY_PATH = lib.makeLibraryPath (libcefDependencies ++ [ pkgs.blackmagic-desktop-video ]);

inputsFrom = [ packageWithoutChromium ];
Expand Down
7 changes: 4 additions & 3 deletions build_tools/nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
, ffmpeg_7-headless
, openssl
, pkg-config
, llvmPackages_16
, llvmPackages
, libGL
, cmake
, libopus
Expand All @@ -20,6 +20,7 @@ let
libopus
libGL
vulkan-loader
stdenv.cc.cc
] ++ lib.optionals stdenv.isDarwin [
darwin.apple_sdk.frameworks.Metal
darwin.apple_sdk.frameworks.Foundation
Expand All @@ -43,9 +44,9 @@ rustPlatform.buildRustPackage {
doCheck = false;

inherit buildInputs;
nativeBuildInputs = [ pkg-config llvmPackages_16.clang cmake makeWrapper ];
nativeBuildInputs = [ pkg-config llvmPackages.clang cmake makeWrapper ];

env.LIBCLANG_PATH = "${llvmPackages_16.libclang.lib}/lib";
env.LIBCLANG_PATH = "${llvmPackages.libclang.lib}/lib";

postFixup =
''
Expand Down
7 changes: 5 additions & 2 deletions ts/@live-compositor/core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ export class ApiClient {
});
}

public async unregisterOutput(outptuId: string): Promise<object> {
public async unregisterOutput(
outptuId: string,
body: { schedule_time_ms?: number }
): Promise<object> {
return this.serverManager.sendRequest({
method: 'POST',
route: `/api/output/${encodeURIComponent(outptuId)}/unregister`,
body: {},
body,
});
}

Expand Down
5 changes: 3 additions & 2 deletions ts/@live-compositor/core/src/compositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class LiveCompositor {

public async init(): Promise<void> {
this.manager.registerEventListener((event: unknown) => onCompositorEvent(this.store, event));
await this.manager.setupInstance();
await this.manager.setupInstance({ aheadOfTimeProcessing: false });
}

public async registerOutput(outputId: string, request: RegisterOutput): Promise<object> {
Expand All @@ -40,7 +40,8 @@ export class LiveCompositor {
public async unregisterOutput(outputId: string): Promise<object> {
this.outputs[outputId].close();
delete this.outputs[outputId];
return this.api.unregisterOutput(outputId);
// TODO: wait for event
return this.api.unregisterOutput(outputId, {});
}

public async registerInput(inputId: string, request: RegisterInput): Promise<object> {
Expand Down
2 changes: 1 addition & 1 deletion ts/@live-compositor/core/src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function onCompositorEvent(store: InstanceContextStore, rawEvent: unknown
}
}

function parseEvent(event: any): CompositorEvent | null {
export function parseEvent(event: any): CompositorEvent | null {
if (!event.type) {
console.error(`Malformed event: ${event}`);
return null;
Expand Down
61 changes: 41 additions & 20 deletions ts/@live-compositor/core/src/offline_compositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,22 @@ import type { RegisterOutput } from './api/output.js';
import { intoRegisterOutput } from './api/output.js';
import type { RegisterInput } from './api/input.js';
import { intoRegisterInput } from './api/input.js';
import { onCompositorEvent } from './event.js';
import { intoRegisterImage, intoRegisterWebRenderer } from './api/renderer.js';
import OfflineOutput from './offline_output.js';
import { parseEvent } from './event.js';
import { CompositorEventType } from '../../../live-compositor/cjs/index.js';

/**
* Offline rendering only supports one output, so we can just pick any value to use
* as an output ID.
*/
const OFFLINE_OUTPUT_ID = 'offline_output';

export type OfflineRenderOptions = {
output: RegisterOutput;
durationMs: number;
};

export class OfflineCompositor {
private manager: CompositorManager;
private api: ApiClient;
private store: _liveCompositorInternals.InstanceContextStore;
private outputs: Record<string, OfflineOutput> = {};
private render_started: boolean = false;
private renderStarted: boolean = false;

public constructor(manager: CompositorManager) {
this.manager = manager;
Expand All @@ -35,26 +30,43 @@ export class OfflineCompositor {
}

public async init(): Promise<void> {
this.manager.registerEventListener((event: unknown) => onCompositorEvent(this.store, event));
await this.manager.setupInstance({ aheadOfTimeProcessing: true });
}

public async render(request: RegisterOutput): Promise<object> {
this.render_started = true;
const output = new OfflineOutput(OFFLINE_OUTPUT_ID, request, this.api, this.store);
public async render(request: RegisterOutput, durationMs: number) {
this.checkNotStarted();
this.renderStarted = true;

const output = new OfflineOutput(OFFLINE_OUTPUT_ID, request, this.api, this.store);
const apiRequest = intoRegisterOutput(request, output.scene());
const result = await this.api.registerOutput(OFFLINE_OUTPUT_ID, apiRequest);
this.outputs[OFFLINE_OUTPUT_ID] = output;
await output.ready();
// loop next_timestamp()
// - update props/store
// - check if finished rendering
// -
return result;
await this.api.registerOutput(OFFLINE_OUTPUT_ID, apiRequest);
output.markReady();

await output.wait();
// at this point all scene update requests should already be delivered

await this.api.unregisterOutput(OFFLINE_OUTPUT_ID, { schedule_time_ms: durationMs });

const renderPromise = new Promise<void>((res, _rej) => {
this.manager.registerEventListener(rawEvent => {
const event = parseEvent(rawEvent);
if (
event &&
event.type === CompositorEventType.OUTPUT_DONE &&
event.outputId === OFFLINE_OUTPUT_ID
) {
res();
}
});
});

await this.api.start();

await renderPromise;
}

public async registerInput(inputId: string, request: RegisterInput): Promise<object> {
this.checkNotStarted();
return this.store.runBlocking(async updateStore => {
const result = await this.api.registerInput(inputId, intoRegisterInput(request));
updateStore({ type: 'add_input', input: { inputId } });
Expand All @@ -66,17 +78,26 @@ export class OfflineCompositor {
shaderId: string,
request: Renderers.RegisterShader
): Promise<object> {
this.checkNotStarted();
return this.api.registerShader(shaderId, request);
}

public async registerImage(imageId: string, request: Renderers.RegisterImage): Promise<object> {
this.checkNotStarted();
return this.api.registerImage(imageId, intoRegisterImage(request));
}

public async registerWebRenderer(
instanceId: string,
request: Renderers.RegisterWebRenderer
): Promise<object> {
this.checkNotStarted();
return this.api.registerWebRenderer(instanceId, intoRegisterWebRenderer(request));
}

private checkNotStarted() {
if (this.renderStarted) {
throw new Error('Render was already started');
}
}
}
61 changes: 45 additions & 16 deletions ts/@live-compositor/core/src/offline_output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ import type { ApiClient, Api } from './api.js';
import Renderer from './renderer.js';
import type { RegisterOutput } from './api/output.js';
import { intoAudioInputsConfiguration } from './api/output.js';
import { throttle } from './utils.js';

type OutputContext = _liveCompositorInternals.OutputContext;
type OfflineContext = _liveCompositorInternals.OfflineContext;
type InstanceContextStore = _liveCompositorInternals.InstanceContextStore;

class OfflineOutput {
api: ApiClient;
outputId: string;
outputCtx: OutputContext;
outputShutdownStateStore: OutputShutdownStateStore;
donePromise: Promise<void>;
donePromiseRes: () => void;
donePromiseRej: (err: Error) => void;

shouldUpdateWhenReady: boolean = false;
throttledUpdate: () => void;
scheduleUpdate: () => void;
videoRenderer?: Renderer;
initialAudioConfig?: Outputs.AudioInputsConfiguration;

Expand All @@ -31,17 +33,21 @@ class OfflineOutput {
this.api = api;
this.outputId = outputId;
this.outputShutdownStateStore = new OutputShutdownStateStore();
this.shouldUpdateWhenReady = false;
this.throttledUpdate = () => {
this.shouldUpdateWhenReady = true;
};
this.scheduleUpdate = () => {};

this.donePromiseRes = () => {};
this.donePromiseRej = () => {};
this.donePromise = new Promise((res, rej) => {
this.donePromiseRes = res;
this.donePromiseRej = rej;
});

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

const onUpdate = () => this.throttledUpdate();
const onUpdate = () => this.scheduleUpdate();
this.outputCtx = new _liveCompositorInternals.OutputContext(onUpdate, {
supportsAudio,
offlineMode: true,
Expand All @@ -63,28 +69,51 @@ class OfflineOutput {
}
}

public scene(): { video?: Api.Video; audio?: Api.Audio } {
public scene(): { video?: Api.Video; audio?: Api.Audio; schedule_time_ms: number } {
const audio = this.outputCtx.audioContext.getAudioConfig() ?? this.initialAudioConfig;
return {
video: this.videoRenderer && { root: this.videoRenderer.scene() },
audio: audio && intoAudioInputsConfiguration(audio),
schedule_time_ms: this.offlineContext().timestampMs(),
};
}

public close(): void {
this.throttledUpdate = () => {};
this.scheduleUpdate = () => {};
// close will switch a scene to just a <View />, so we need replace `throttledUpdate`
// callback before it is called
this.outputShutdownStateStore.close();
}

public async ready() {
this.throttledUpdate = throttle(async () => {
await this.api.updateScene(this.outputId, this.scene());
}, 30);
if (this.shouldUpdateWhenReady) {
this.throttledUpdate();
public markReady() {
this.scheduleUpdate = async () => {
await this.onUpdate();
};
}

public wait(): Promise<void> {
return this.donePromise;
}

private async onUpdate() {
if (this.offlineContext().isBlocked()) {
return;
}
const scene = this.scene();
if (scene.schedule_time_ms === Infinity) {
this.donePromiseRes();
return;
}

await this.api.updateScene(this.outputId, scene);
}

private offlineContext(): OfflineContext {
const ctx = this.outputCtx.offlineContext;
if (!ctx) {
throw new Error('OfflineContext missing in OfflineCompositor.');
}
return ctx;
}
}

Expand Down
2 changes: 1 addition & 1 deletion ts/@live-compositor/core/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Output {
outputId: string,
registerRequest: RegisterOutput,
api: ApiClient,
store: InstanceContextStore
store: InstanceContextStore,
) {
this.api = api;
this.outputId = outputId;
Expand Down
2 changes: 2 additions & 0 deletions ts/@live-compositor/core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ class Renderer {
// When resetAfterCommit is called `this.root.current` is not updated yet, so we need to rely
// on `pendingChildren`. I'm not sure it is always populated, so there is a fallback to
// `root.current`.

console.log(this.root);
const rootComponent = this.root.pendingChildren[0] ?? rootHostComponent(this.root.current);
return rootComponent.scene();
}
Expand Down
Loading

0 comments on commit 2577b7b

Please sign in to comment.