Skip to content

Commit 0b431ae

Browse files
lukasIOdavidzhao
andauthored
Add track processor API (livekit#711)
* model insertable streams api * kitten processor, virtual background api/processor * init support for stream processors * processor stability updates * simplify processor handling * add notes * fix types * Update src/room/track/LocalTrack.ts Co-authored-by: David Zhao <dz@livekit.io> * remove console log * Create lovely-trains-visit.md * change changeset to minor --------- Co-authored-by: David Zhao <dz@livekit.io>
1 parent 74a4b58 commit 0b431ae

File tree

6 files changed

+134
-8
lines changed

6 files changed

+134
-8
lines changed

.DS_Store

6 KB
Binary file not shown.

.changeset/lovely-trains-visit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'livekit-client': minor
3+
---
4+
5+
Add track processor API

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export * from './room/track/options';
4040
export * from './room/track/types';
4141
export type { DataPublishOptions, SimulationScenario } from './room/types';
4242
export * from './version';
43+
export * from './room/track/processor/types';
4344
export {
4445
setLogLevel,
4546
setLogExtension,

src/room/track/LocalTrack.ts

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TrackEvent } from '../events';
66
import { Mutex, compareVersions, isMobile, sleep } from '../utils';
77
import { Track, attachToElement, detachTrack } from './Track';
88
import type { VideoCodec } from './options';
9+
import type { TrackProcessor } from './processor/types';
910

1011
const defaultDimensionsTimeout = 1000;
1112

@@ -26,6 +27,12 @@ export default abstract class LocalTrack extends Track {
2627

2728
protected pauseUpstreamLock: Mutex;
2829

30+
protected processorElement?: HTMLMediaElement;
31+
32+
protected processor?: TrackProcessor<typeof this.kind>;
33+
34+
protected isSettingUpProcessor: boolean = false;
35+
2936
/**
3037
*
3138
* @param mediaTrack
@@ -77,6 +84,10 @@ export default abstract class LocalTrack extends Track {
7784
return this.providedByUser;
7885
}
7986

87+
get mediaStreamTrack() {
88+
return this.processor?.processedTrack ?? this._mediaStreamTrack;
89+
}
90+
8091
async waitForDimensions(timeout = defaultDimensionsTimeout): Promise<Track.Dimensions> {
8192
if (this.kind === Track.Kind.Audio) {
8293
throw new Error('cannot get dimensions for audio tracks');
@@ -153,6 +164,9 @@ export default abstract class LocalTrack extends Track {
153164

154165
this.mediaStream = new MediaStream([track]);
155166
this.providedByUser = userProvidedTrack;
167+
if (this.processor) {
168+
await this.stopProcessor();
169+
}
156170
return this;
157171
}
158172

@@ -175,7 +189,7 @@ export default abstract class LocalTrack extends Track {
175189

176190
// detach
177191
this.attachedElements.forEach((el) => {
178-
detachTrack(this._mediaStreamTrack, el);
192+
detachTrack(this.mediaStreamTrack, el);
179193
});
180194
this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
181195
// on Safari, the old audio track must be stopped before attempting to acquire
@@ -198,12 +212,16 @@ export default abstract class LocalTrack extends Track {
198212

199213
await this.resumeUpstream();
200214

201-
this.attachedElements.forEach((el) => {
202-
attachToElement(newTrack, el);
203-
});
204-
205215
this.mediaStream = mediaStream;
206216
this.constraints = constraints;
217+
if (this.processor) {
218+
const processor = this.processor;
219+
await this.setProcessor(processor);
220+
} else {
221+
this.attachedElements.forEach((el) => {
222+
attachToElement(this._mediaStreamTrack, el);
223+
});
224+
}
207225
this.emit(TrackEvent.Restarted, this);
208226
return this;
209227
}
@@ -248,6 +266,12 @@ export default abstract class LocalTrack extends Track {
248266
this.emit(TrackEvent.Ended, this);
249267
};
250268

269+
stop() {
270+
super.stop();
271+
this.processor?.destroy();
272+
this.processor = undefined;
273+
}
274+
251275
/**
252276
* pauses publishing to the server without disabling the local MediaStreamTrack
253277
* this is used to display a user's own video locally while pausing publishing to
@@ -297,5 +321,81 @@ export default abstract class LocalTrack extends Track {
297321
}
298322
}
299323

324+
/**
325+
* Sets a processor on this track.
326+
* See https://github.com/livekit/track-processors-js for example usage
327+
*
328+
* @experimental
329+
*
330+
* @param processor
331+
* @param showProcessedStreamLocally
332+
* @returns
333+
*/
334+
async setProcessor(
335+
processor: TrackProcessor<typeof this.kind>,
336+
showProcessedStreamLocally = true,
337+
) {
338+
if (this.isSettingUpProcessor) {
339+
log.warn('already trying to set up a processor');
340+
return;
341+
}
342+
log.debug('setting up processor');
343+
this.isSettingUpProcessor = true;
344+
if (this.processor) {
345+
await this.stopProcessor();
346+
}
347+
if (this.kind === 'unknown') {
348+
throw TypeError('cannot set processor on track of unknown kind');
349+
}
350+
this.processorElement = this.processorElement ?? document.createElement(this.kind);
351+
this.processorElement.muted = true;
352+
353+
attachToElement(this._mediaStreamTrack, this.processorElement);
354+
this.processorElement.play().catch((e) => log.error(e));
355+
356+
const processorOptions = {
357+
kind: this.kind,
358+
track: this._mediaStreamTrack,
359+
element: this.processorElement,
360+
};
361+
362+
await processor.init(processorOptions);
363+
this.processor = processor;
364+
if (this.processor.processedTrack) {
365+
for (const el of this.attachedElements) {
366+
if (el !== this.processorElement && showProcessedStreamLocally) {
367+
detachTrack(this._mediaStreamTrack, el);
368+
attachToElement(this.processor.processedTrack, el);
369+
}
370+
}
371+
await this.sender?.replaceTrack(this.processor.processedTrack);
372+
}
373+
this.isSettingUpProcessor = false;
374+
}
375+
376+
getProcessor() {
377+
return this.processor;
378+
}
379+
380+
/**
381+
* Stops the track processor
382+
* See https://github.com/livekit/track-processors-js for example usage
383+
*
384+
* @experimental
385+
* @returns
386+
*/
387+
async stopProcessor() {
388+
if (!this.processor) return;
389+
390+
log.debug('stopping processor');
391+
this.processor.processedTrack?.stop();
392+
await this.processor.destroy();
393+
this.processor = undefined;
394+
this.processorElement?.remove();
395+
this.processorElement = undefined;
396+
397+
await this.restart();
398+
}
399+
300400
protected abstract monitorSender(): void;
301401
}

src/room/track/Track.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
118118
// even if we believe it's already attached to the element, it's possible
119119
// the element's srcObject was set to something else out of band.
120120
// we'll want to re-attach it in that case
121-
attachToElement(this._mediaStreamTrack, element);
121+
attachToElement(this.mediaStreamTrack, element);
122122

123123
// handle auto playback failures
124124
const allMediaStreamTracks = (element.srcObject as MediaStream).getTracks();
@@ -167,7 +167,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
167167
try {
168168
// detach from a single element
169169
if (element) {
170-
detachTrack(this._mediaStreamTrack, element);
170+
detachTrack(this.mediaStreamTrack, element);
171171
const idx = this.attachedElements.indexOf(element);
172172
if (idx >= 0) {
173173
this.attachedElements.splice(idx, 1);
@@ -179,7 +179,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
179179

180180
const detached: HTMLMediaElement[] = [];
181181
this.attachedElements.forEach((elm) => {
182-
detachTrack(this._mediaStreamTrack, elm);
182+
detachTrack(this.mediaStreamTrack, elm);
183183
detached.push(elm);
184184
this.recycleElement(elm);
185185
this.emit(TrackEvent.ElementDetached, elm);

src/room/track/processor/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Track } from '../Track';
2+
3+
/**
4+
* @experimental
5+
*/
6+
export type ProcessorOptions<T extends Track.Kind> = {
7+
kind: T;
8+
track: MediaStreamTrack;
9+
element?: HTMLMediaElement;
10+
};
11+
12+
/**
13+
* @experimental
14+
*/
15+
export interface TrackProcessor<T extends Track.Kind> {
16+
name: string;
17+
init: (opts: ProcessorOptions<T>) => void;
18+
destroy: () => Promise<void>;
19+
processedTrack?: MediaStreamTrack;
20+
}

0 commit comments

Comments
 (0)