Skip to content

Commit

Permalink
[web-wasm] Add minimal audio support
Browse files Browse the repository at this point in the history
  • Loading branch information
wkozyra95 committed Feb 1, 2025
1 parent 497b0bc commit a784d5b
Show file tree
Hide file tree
Showing 23 changed files with 548 additions and 113 deletions.
8 changes: 6 additions & 2 deletions ts/@live-compositor/core/src/api/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { _liveCompositorInternals } from 'live-compositor';
export type RegisterInputRequest =
| Api.RegisterInput
| { type: 'camera' }
| { type: 'screen_capture' };
| { type: 'screen_capture' }
| { type: 'stream'; stream: any };

export type InputRef = _liveCompositorInternals.InputRef;
export const inputRefIntoRawId = _liveCompositorInternals.inputRefIntoRawId;
Expand All @@ -25,7 +26,8 @@ export type RegisterInput =
| ({ type: 'mp4' } & RegisterMp4Input)
| ({ type: 'whip' } & RegisterWhipInput)
| { type: 'camera' }
| { type: 'screen_capture' };
| { type: 'screen_capture' }
| { type: 'stream'; stream: any };

/**
* Converts object passed by user (or modified by platform specific interface) into
Expand All @@ -42,6 +44,8 @@ export function intoRegisterInput(input: RegisterInput): RegisterInputRequest {
return { type: 'camera' };
} else if (input.type === 'screen_capture') {
return { type: 'screen_capture' };
} else if (input.type === 'stream') {
return { type: 'stream', stream: input.stream };
} else {
throw new Error(`Unknown input type ${(input as any).type}`);
}
Expand Down
4 changes: 3 additions & 1 deletion ts/@live-compositor/core/src/api/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type RegisterWasmWhipOutput = {
resolution: Api.Resolution;
maxBitrate?: number;
};
// audio: boolean;
audio?: boolean;
};

export type RegisterWasmCanvasOutput = {
Expand All @@ -40,13 +40,15 @@ export type RegisterWasmCanvasOutput = {
resolution: Api.Resolution;
canvas: any; // HTMLCanvasElement
};
audio?: boolean;
};

export type RegisterWasmStreamOutput = {
type: 'web-wasm-stream';
video?: {
resolution: Api.Resolution;
};
audio?: boolean;
};

export type RegisterWasmSpecificOutput =
Expand Down
14 changes: 6 additions & 8 deletions ts/@live-compositor/web-wasm/src/compositor/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ export type RegisterOutput =
video: {
resolution: Api.Resolution;
};
// audio: boolean;
audio?: boolean;
}
| {
type: 'canvas';
video: {
canvas: HTMLCanvasElement;
resolution: Api.Resolution;
};
// audio: boolean;
audio?: boolean;
}
| {
type: 'whip';
Expand All @@ -34,19 +34,16 @@ export type RegisterOutput =
resolution: Api.Resolution;
maxBitrate?: number;
};
// audio: boolean;
audio?: boolean;
};

export function intoRegisterOutputRequest(request: RegisterOutput): Output.RegisterOutput {
if (request.type === 'stream') {
return { ...request, type: 'web-wasm-stream' };
} else if (request.type === 'canvas') {
return {
...request,
type: 'web-wasm-canvas',
video: {
canvas: request.video.canvas as HTMLCanvasElement,
resolution: request.video.resolution,
},
};
} else if (request.type === 'whip') {
return { ...request, type: 'web-wasm-whip' };
Expand All @@ -57,4 +54,5 @@ export function intoRegisterOutputRequest(request: RegisterOutput): Output.Regis
export type RegisterInput =
| { type: 'mp4'; url: string }
| { type: 'camera' }
| { type: 'screen_capture' };
| { type: 'screen_capture' }
| { type: 'stream'; stream: MediaStream };
14 changes: 12 additions & 2 deletions ts/@live-compositor/web-wasm/src/compositor/compositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
intoRegisterOutputRequest,
} from './api';
import WasmInstance from '../mainContext/instance';
import type { RegisterOutputResponse } from '../mainContext/output';

export type LiveCompositorOptions = {
framerate?: Framerate;
Expand Down Expand Up @@ -61,9 +62,18 @@ export default class LiveCompositor {
outputId: string,
root: ReactElement,
request: RegisterOutput
): Promise<void> {
): Promise<{ stream?: MediaStream }> {
assert(this.coreCompositor);
await this.coreCompositor.registerOutput(outputId, root, intoRegisterOutputRequest(request));
const response = (await this.coreCompositor.registerOutput(
outputId,
root,
intoRegisterOutputRequest(request)
)) as RegisterOutputResponse | undefined;
if (response?.type === 'web-wasm-stream' || response?.type === 'web-wasm-whip') {
return { stream: response.stream };
} else {
return {};
}
}

public async unregisterOutput(outputId: string): Promise<void> {
Expand Down
76 changes: 76 additions & 0 deletions ts/@live-compositor/web-wasm/src/mainContext/AudioMixer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Api } from 'live-compositor';
import { assert } from '../utils';

type AudioInput = {
source: MediaStreamAudioSourceNode;
gain: GainNode;
};

export class AudioMixer<OutputNode extends AudioNode = AudioNode> {
private ctx: AudioContext;
private inputs: Record<string, AudioInput> = {};
protected outputNode: OutputNode;

constructor(ctx: AudioContext, outputNode: OutputNode) {
this.ctx = ctx;
this.outputNode = outputNode;
}

public addInput(inputId: string, track: MediaStreamTrack) {
const stream = new MediaStream();
stream.addTrack(track);
const source = this.ctx.createMediaStreamSource(stream);
const gain = this.ctx.createGain();
source.connect(gain);
gain.connect(this.outputNode ?? this.ctx.destination);
this.inputs[inputId] = {
source,
gain,
};
}

public removeInput(inputId: string) {
this.inputs[inputId]?.source.disconnect();
this.inputs[inputId]?.gain.disconnect();
delete this.inputs[inputId];
}

public update(inputConfig: Api.InputAudio[]) {
for (const [inputId, input] of Object.entries(this.inputs)) {
const config = inputConfig.find(input => input.input_id === inputId);
input.gain.gain.value = config?.volume || 0;
}
}

public async close() {
await this.ctx.close();
for (const inputId of Object.keys(this.inputs)) {
this.removeInput(inputId);
}
}
}

export class MediaStreamAudioMixer extends AudioMixer<MediaStreamAudioDestinationNode> {
constructor() {
const ctx = new AudioContext();
const outputNode = ctx.createMediaStreamDestination();
const silence = ctx.createConstantSource();
silence.offset.value = 0;
silence.connect(outputNode);
silence.start();
super(ctx, outputNode);
}

public outputMediaStreamTrack(): MediaStreamTrack {
const audioTrack = this.outputNode.stream.getAudioTracks()[0];
assert(audioTrack);
return audioTrack;
}
}

export class PlaybackAudioMixer extends AudioMixer<AudioDestinationNode> {
constructor() {
const ctx = new AudioContext();
super(ctx, ctx.destination);
}
}
11 changes: 11 additions & 0 deletions ts/@live-compositor/web-wasm/src/mainContext/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ import type { WorkerMessage } from '../workerApi';
import { assert } from '../utils';
import { handleRegisterCameraInput } from './input/camera';
import { handleRegisterScreenCaptureInput } from './input/screenCapture';
import { handleRegisterStreamInput } from './input/stream';

export interface Input {
get audioTrack(): MediaStreamTrack | undefined;
terminate(): Promise<void>;
}

/**
* Can be used if entire code for the input runs in worker.
*/
class NoopInput implements Input {
public async terminate(): Promise<void> {}

public get audioTrack(): MediaStreamTrack | undefined {
return undefined;
}
}

export type RegisterInputResult = {
Expand Down Expand Up @@ -41,6 +50,8 @@ export async function handleRegisterInputRequest(
return await handleRegisterCameraInput(inputId);
} else if (body.type === 'screen_capture') {
return await handleRegisterScreenCaptureInput(inputId);
} else if (body.type === 'stream') {
return await handleRegisterStreamInput(inputId, body.stream);
} else {
throw new Error(`Unknown input type ${body.type}`);
}
Expand Down
18 changes: 7 additions & 11 deletions ts/@live-compositor/web-wasm/src/mainContext/input/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ export class CameraInput implements Input {
this.mediaStream = mediaStream;
}

public get audioTrack(): MediaStreamTrack | undefined {
return this.mediaStream.getAudioTracks()[0];
}

public async terminate(): Promise<void> {
this.mediaStream.getTracks().forEach(track => track.stop());
}
}

export async function handleRegisterCameraInput(inputId: string): Promise<RegisterInputResult> {
const mediaStream = await navigator.mediaDevices.getUserMedia({
audio: false,
audio: {
noiseSuppression: { ideal: true },
},
video: true,
});
const videoTrack = mediaStream.getVideoTracks()[0];
const audioTrack = mediaStream.getAudioTracks()[0];
const transferable = [];

// @ts-ignore
Expand All @@ -29,14 +34,6 @@ export async function handleRegisterCameraInput(inputId: string): Promise<Regist
transferable.push(videoTrackProcessor.readable);
}

// @ts-ignore
let audioTrackProcessor: MediaStreamTrackProcessor | undefined;
if (audioTrack) {
// @ts-ignore
audioTrackProcessor = new MediaStreamTrackProcessor({ track: audioTrack });
transferable.push(audioTrackProcessor.readable);
}

return {
input: new CameraInput(mediaStream),
workerMessage: [
Expand All @@ -46,7 +43,6 @@ export async function handleRegisterCameraInput(inputId: string): Promise<Regist
input: {
type: 'stream',
videoStream: videoTrackProcessor.readable,
audioStream: videoTrackProcessor.readable,
},
},
transferable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export class ScreenCaptureInput implements Input {
this.mediaStream = mediaStream;
}

public get audioTrack(): MediaStreamTrack | undefined {
return this.mediaStream.getAudioTracks()[0];
}

public async terminate(): Promise<void> {
this.mediaStream.getTracks().forEach(track => track.stop());
}
Expand All @@ -16,14 +20,13 @@ export async function handleRegisterScreenCaptureInput(
inputId: string
): Promise<RegisterInputResult> {
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
audio: true,
video: {
width: { max: 2048 },
height: { max: 2048 },
},
});
const videoTrack = mediaStream.getVideoTracks()[0];
const audioTrack = mediaStream.getAudioTracks()[0];
const transferable = [];

// @ts-ignore
Expand All @@ -34,14 +37,6 @@ export async function handleRegisterScreenCaptureInput(
transferable.push(videoTrackProcessor.readable);
}

// @ts-ignore
let audioTrackProcessor: MediaStreamTrackProcessor | undefined;
if (audioTrack) {
// @ts-ignore
audioTrackProcessor = new MediaStreamTrackProcessor({ track: audioTrack });
transferable.push(audioTrackProcessor.readable);
}

return {
input: new ScreenCaptureInput(mediaStream),
workerMessage: [
Expand All @@ -51,7 +46,6 @@ export async function handleRegisterScreenCaptureInput(
input: {
type: 'stream',
videoStream: videoTrackProcessor.readable,
audioStream: videoTrackProcessor.readable,
},
},
transferable,
Expand Down
48 changes: 48 additions & 0 deletions ts/@live-compositor/web-wasm/src/mainContext/input/stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Input, RegisterInputResult } from '../input';

export class StreamInput implements Input {
private mediaStream: MediaStream;

constructor(mediaStream: MediaStream) {
this.mediaStream = mediaStream;
}

public get audioTrack(): MediaStreamTrack | undefined {
return this.mediaStream.getAudioTracks()[0];
}

public async terminate(): Promise<void> {
this.mediaStream.getTracks().forEach(track => track.stop());
}
}

export async function handleRegisterStreamInput(
inputId: string,
stream: MediaStream
): Promise<RegisterInputResult> {
const videoTrack = stream.getVideoTracks()[0];
const transferable = [];

// @ts-ignore
let videoTrackProcessor: MediaStreamTrackProcessor | undefined;
if (videoTrack) {
// @ts-ignore
videoTrackProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
transferable.push(videoTrackProcessor.readable);
}

return {
input: new StreamInput(stream),
workerMessage: [
{
type: 'registerInput',
inputId,
input: {
type: 'stream',
videoStream: videoTrackProcessor.readable,
},
},
transferable,
],
};
}
Loading

0 comments on commit a784d5b

Please sign in to comment.