@stream-io/video-filters-web 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ import { VideoTrackProcessorHooks } from './types';
2
+ import { MediaStreamTrackGenerator } from './FallbackGenerator';
3
+ import { MediaStreamTrackProcessor } from './FallbackProcessor';
4
+ /**
5
+ * Base class for real-time video filters.
6
+ *
7
+ * It sets up the full pipeline that reads frames from the input track,
8
+ * processes them, and outputs a new track with your effect applied. Subclasses
9
+ * only need to implement `initialize` (run once before processing starts) and
10
+ * `transform` (called for every frame).
11
+ *
12
+ * Everything else—canvas setup, performance tracking, error handling, and
13
+ * clean shutdown is handled for you. Calling `start()` returns a processed
14
+ * `MediaStreamTrack` ready to use.
15
+ */
16
+ export declare abstract class BaseVideoProcessor {
17
+ protected readonly track: MediaStreamVideoTrack;
18
+ protected readonly processor: MediaStreamTrackProcessor<VideoFrame>;
19
+ protected readonly generator: MediaStreamTrackGenerator<VideoFrame>;
20
+ protected readonly hooks: VideoTrackProcessorHooks;
21
+ protected readonly abortController: AbortController;
22
+ protected canvas: OffscreenCanvas;
23
+ private frames;
24
+ private delayTotal;
25
+ private lastStatsTime;
26
+ /**
27
+ * Constructs a new instance.
28
+ */
29
+ protected constructor(track: MediaStreamVideoTrack, hooks?: VideoTrackProcessorHooks);
30
+ start(): Promise<MediaStreamTrack>;
31
+ stop(): void;
32
+ private updateStats;
33
+ protected abstract initialize(): Promise<void>;
34
+ protected abstract transform(frame: VideoFrame): Promise<VideoFrame>;
35
+ protected onFlush(): void;
36
+ protected onStop(): void;
37
+ protected get processorName(): string;
38
+ }
@@ -0,0 +1,28 @@
1
+ import { VideoTrackProcessorHooks } from './types';
2
+ import { BaseVideoProcessor } from './BaseVideoProcessor';
3
+ export interface FullScreenBlurOptions {
4
+ blurRadius?: number;
5
+ }
6
+ /**
7
+ * A video filter that applies a full-screen blur to each frame.
8
+ *
9
+ * It uses a WebGL renderer to blur the incoming camera track and outputs
10
+ * a new track with the effect applied. Setup and frame handling are managed
11
+ * by the base processor.
12
+ */
13
+ export declare class FullScreenBlur extends BaseVideoProcessor {
14
+ private blurRenderer;
15
+ private readonly blurRadius;
16
+ /**
17
+ * Creates a new full-screen blur processor for the given video track.
18
+ *
19
+ * @param track - The input camera track to blur.
20
+ * @param options - Optional settings such as the blur radius.
21
+ * @param hooks - Optional callbacks for stats and error reporting.
22
+ */
23
+ constructor(track: MediaStreamVideoTrack, options?: FullScreenBlurOptions, hooks?: VideoTrackProcessorHooks);
24
+ protected initialize(): Promise<void>;
25
+ protected transform(frame: VideoFrame): Promise<VideoFrame>;
26
+ protected onStop(): void;
27
+ protected get processorName(): string;
28
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Simple WebGL renderer for full-screen Gaussian blur.
3
+ * Uses a two-pass separable Gaussian blur (horizontal then vertical).
4
+ * Optimized for moderation use cases by blurring at reduced resolution (15% scale)
5
+ * and upscaling back to full resolution for output.
6
+ */
7
+ export declare class FullScreenBlurRenderer {
8
+ readonly canvas: OffscreenCanvas;
9
+ readonly gl: WebGL2RenderingContext;
10
+ readonly blurProgramHandle: WebGLProgram;
11
+ readonly blurLocations: {
12
+ positionLocation: number;
13
+ texCoordLocation: number;
14
+ imageLocation: WebGLUniformLocation | null;
15
+ texelSizeLocation: WebGLUniformLocation | null;
16
+ directionLocation: WebGLUniformLocation | null;
17
+ weightsLocation: WebGLUniformLocation | null;
18
+ };
19
+ readonly passthroughProgramHandle: WebGLProgram;
20
+ readonly passthroughLocations: {
21
+ positionLocation: number;
22
+ texCoordLocation: number;
23
+ imageLocation: WebGLUniformLocation | null;
24
+ };
25
+ readonly positionBuffer: WebGLBuffer | null;
26
+ readonly texCoordBuffer: WebGLBuffer | null;
27
+ readonly pingTexture: WebGLTexture | null;
28
+ readonly pongTexture: WebGLTexture | null;
29
+ readonly pingFbo: WebGLFramebuffer | null;
30
+ readonly pongFbo: WebGLFramebuffer | null;
31
+ private inputTexture;
32
+ private isRunning;
33
+ private targetWidth;
34
+ private targetHeight;
35
+ private weightCache;
36
+ constructor(canvas: OffscreenCanvas);
37
+ private createAndLinkProgram;
38
+ private createShader;
39
+ private getGaussianWeights;
40
+ render(frame: VideoFrame, radius: number): void;
41
+ close(): void;
42
+ }
@@ -1,42 +1,29 @@
1
1
  import { BackgroundOptions, VideoTrackProcessorHooks } from './types';
2
+ import { BaseVideoProcessor } from './BaseVideoProcessor';
2
3
  /**
3
4
  * Wraps a video MediaStreamTrack in a real-time processing pipeline.
4
5
  * Incoming frames are processed through a transformer and re-emitted
5
6
  * on a new MediaStreamVideoTrack for downstream consumption.
6
7
  */
7
- export declare class VirtualBackground {
8
- private readonly track;
8
+ export declare class VirtualBackground extends BaseVideoProcessor {
9
9
  private readonly options;
10
- private readonly hooks;
11
- private readonly processor;
12
- private readonly generator;
13
- private canvas;
14
10
  private segmenter;
15
11
  private isSegmenterReady;
16
12
  private webGlRenderer;
17
- private abortController;
18
- private segmenterDelayTotal;
19
- private frames;
20
- private lastStatsTime;
13
+ private opts;
14
+ private latestCategoryMask;
15
+ private latestConfidenceMask;
16
+ private lastFrameTime;
17
+ private count;
21
18
  constructor(track: MediaStreamVideoTrack, options?: BackgroundOptions, hooks?: VideoTrackProcessorHooks);
22
- start(): Promise<MediaStreamTrack>;
23
- /**
24
- * Loads and initializes the MediaPipe `ImageSegmenter`.
25
- */
19
+ protected initialize(): Promise<void>;
26
20
  private initializeSegmenter;
27
- /**
28
- * Processes a single video frame.
29
- *
30
- * Performs segmentation via MediaPipe and then composites the frame
31
- * through the WebGL renderer to apply background effects.
32
- *
33
- * @param frame - The incoming frame from the processor.
34
- * @param opts - The segmentation options to use.
35
- *
36
- * @returns A new `VideoFrame` containing the processed image.
37
- */
38
- private transform;
39
- private loadBackground;
21
+ protected transform(frame: VideoFrame): Promise<VideoFrame>;
22
+ private runSegmentation;
40
23
  private initializeSegmenterOptions;
41
- stop(): void;
24
+ private loadBackground;
25
+ protected onFlush(): void;
26
+ protected onStop(): void;
27
+ private destroySegmenter;
28
+ protected get processorName(): string;
42
29
  }
package/index.ts CHANGED
@@ -5,3 +5,4 @@ export * from './src/legacy/tflite';
5
5
  export * from './src/mediapipe';
6
6
  export * from './src/types';
7
7
  export * from './src/VirtualBackground';
8
+ export * from './src/FullScreenBlur';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-filters-web",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/index.d.ts",
@@ -0,0 +1,132 @@
1
+ import { VideoTrackProcessorHooks } from './types';
2
+ import { TrackGenerator, MediaStreamTrackGenerator } from './FallbackGenerator';
3
+ import { MediaStreamTrackProcessor, TrackProcessor } from './FallbackProcessor';
4
+
5
+ /**
6
+ * Base class for real-time video filters.
7
+ *
8
+ * It sets up the full pipeline that reads frames from the input track,
9
+ * processes them, and outputs a new track with your effect applied. Subclasses
10
+ * only need to implement `initialize` (run once before processing starts) and
11
+ * `transform` (called for every frame).
12
+ *
13
+ * Everything else—canvas setup, performance tracking, error handling, and
14
+ * clean shutdown is handled for you. Calling `start()` returns a processed
15
+ * `MediaStreamTrack` ready to use.
16
+ */
17
+ export abstract class BaseVideoProcessor {
18
+ protected readonly processor: MediaStreamTrackProcessor<VideoFrame>;
19
+ protected readonly generator: MediaStreamTrackGenerator<VideoFrame>;
20
+
21
+ protected readonly hooks: VideoTrackProcessorHooks;
22
+
23
+ protected readonly abortController = new AbortController();
24
+ protected canvas!: OffscreenCanvas;
25
+
26
+ private frames = 0;
27
+ private delayTotal = 0;
28
+ private lastStatsTime = 0;
29
+
30
+ /**
31
+ * Constructs a new instance.
32
+ */
33
+ protected constructor(
34
+ protected readonly track: MediaStreamVideoTrack,
35
+ hooks: VideoTrackProcessorHooks = {},
36
+ ) {
37
+ this.processor = new TrackProcessor({ track });
38
+ this.generator = new TrackGenerator({
39
+ kind: 'video',
40
+ signalTarget: track,
41
+ });
42
+ this.hooks = hooks;
43
+ }
44
+
45
+ public async start(): Promise<MediaStreamTrack> {
46
+ const { readable } = this.processor;
47
+ const { writable } = this.generator;
48
+
49
+ const { width = 1280, height = 720 } = this.track.getSettings();
50
+ this.canvas = new OffscreenCanvas(width, height);
51
+
52
+ await this.initialize();
53
+
54
+ const transformStream = new TransformStream<VideoFrame, VideoFrame>({
55
+ transform: async (frame, controller) => {
56
+ try {
57
+ if (this.abortController.signal.aborted) return frame.close();
58
+
59
+ if (
60
+ this.canvas.width !== frame.displayWidth ||
61
+ this.canvas.height !== frame.displayHeight
62
+ ) {
63
+ this.canvas.width = frame.displayWidth;
64
+ this.canvas.height = frame.displayHeight;
65
+ }
66
+
67
+ const start = performance.now();
68
+ const processed = await this.transform(frame);
69
+ const delay = performance.now() - start;
70
+
71
+ this.updateStats(delay);
72
+ controller.enqueue(processed);
73
+ } catch (e) {
74
+ this.hooks.onError?.(e);
75
+ } finally {
76
+ frame.close();
77
+ }
78
+ },
79
+ flush: () => this.onFlush(),
80
+ });
81
+
82
+ readable
83
+ .pipeThrough(transformStream, { signal: this.abortController.signal })
84
+ .pipeTo(writable, { signal: this.abortController.signal })
85
+ .catch((e) => {
86
+ if (e.name !== 'AbortError' && e.name !== 'InvalidStateError') {
87
+ console.error(`[${this.processorName}] Error processing track:`, e);
88
+ this.hooks.onError?.(e);
89
+ }
90
+ });
91
+
92
+ return this.generator;
93
+ }
94
+
95
+ public stop(): void {
96
+ this.abortController.abort();
97
+ this.generator.stop();
98
+ this.onStop();
99
+ }
100
+
101
+ private updateStats(delay: number): void {
102
+ this.frames++;
103
+ this.delayTotal += delay;
104
+
105
+ const now = performance.now();
106
+ if (this.lastStatsTime === 0) {
107
+ this.lastStatsTime = now;
108
+ return;
109
+ }
110
+
111
+ if (now - this.lastStatsTime >= 1000) {
112
+ const avgDelay = Math.round((this.delayTotal / this.frames) * 100) / 100;
113
+ const fps = Math.round((1000 * this.frames) / (now - this.lastStatsTime));
114
+
115
+ this.hooks.onStats?.({ delay: avgDelay, fps, timestamp: now });
116
+
117
+ this.frames = 0;
118
+ this.delayTotal = 0;
119
+ this.lastStatsTime = now;
120
+ }
121
+ }
122
+
123
+ protected abstract initialize(): Promise<void>;
124
+ protected abstract transform(frame: VideoFrame): Promise<VideoFrame>;
125
+
126
+ protected onFlush(): void {}
127
+ protected onStop(): void {}
128
+
129
+ protected get processorName(): string {
130
+ return 'base-processor';
131
+ }
132
+ }
@@ -49,7 +49,6 @@ class FallbackProcessor implements MediaStreamTrackProcessor<VideoFrame> {
49
49
  let timestamp = 0;
50
50
  const frameRate = track.getSettings().frameRate || 30;
51
51
  let frameDuration = 1000 / frameRate;
52
- let lastVideoTime = -1;
53
52
 
54
53
  this.workerTimer = new WorkerTimer({ useWorker: true });
55
54
  this.readable = new ReadableStream({
@@ -77,18 +76,6 @@ class FallbackProcessor implements MediaStreamTrackProcessor<VideoFrame> {
77
76
  }
78
77
  timestamp = performance.now();
79
78
 
80
- const currentTime = this.video.currentTime;
81
- const hasNewFrame = currentTime !== lastVideoTime;
82
-
83
- if (!hasNewFrame) {
84
- await new Promise((r: (value?: unknown) => void) =>
85
- this.workerTimer.setTimeout(r, frameDuration),
86
- );
87
- return;
88
- }
89
-
90
- lastVideoTime = currentTime;
91
-
92
79
  if (
93
80
  canvas.width !== this.video.videoWidth ||
94
81
  canvas.height !== this.video.videoHeight
@@ -100,7 +87,10 @@ class FallbackProcessor implements MediaStreamTrackProcessor<VideoFrame> {
100
87
  ctx.drawImage(this.video, 0, 0);
101
88
 
102
89
  try {
103
- const frame = new VideoFrame(canvas, { timestamp });
90
+ const frame = new VideoFrame(canvas, {
91
+ timestamp: Math.round(this.video.currentTime * 1000000),
92
+ });
93
+
104
94
  controller.enqueue(frame);
105
95
  } catch (err) {
106
96
  running = false;
@@ -0,0 +1,52 @@
1
+ import { VideoTrackProcessorHooks } from './types';
2
+ import { BaseVideoProcessor } from './BaseVideoProcessor';
3
+ import { FullScreenBlurRenderer } from './FullScreenBlurRenderer';
4
+
5
+ export interface FullScreenBlurOptions {
6
+ blurRadius?: number;
7
+ }
8
+
9
+ /**
10
+ * A video filter that applies a full-screen blur to each frame.
11
+ *
12
+ * It uses a WebGL renderer to blur the incoming camera track and outputs
13
+ * a new track with the effect applied. Setup and frame handling are managed
14
+ * by the base processor.
15
+ */
16
+ export class FullScreenBlur extends BaseVideoProcessor {
17
+ private blurRenderer!: FullScreenBlurRenderer;
18
+ private readonly blurRadius: number;
19
+
20
+ /**
21
+ * Creates a new full-screen blur processor for the given video track.
22
+ *
23
+ * @param track - The input camera track to blur.
24
+ * @param options - Optional settings such as the blur radius.
25
+ * @param hooks - Optional callbacks for stats and error reporting.
26
+ */
27
+ constructor(
28
+ track: MediaStreamVideoTrack,
29
+ options: FullScreenBlurOptions = {},
30
+ hooks: VideoTrackProcessorHooks = {},
31
+ ) {
32
+ super(track, hooks);
33
+ this.blurRadius = options.blurRadius ?? 6;
34
+ }
35
+
36
+ protected async initialize(): Promise<void> {
37
+ this.blurRenderer = new FullScreenBlurRenderer(this.canvas);
38
+ }
39
+
40
+ protected async transform(frame: VideoFrame): Promise<VideoFrame> {
41
+ this.blurRenderer.render(frame, this.blurRadius);
42
+ return new VideoFrame(this.canvas, { timestamp: frame.timestamp });
43
+ }
44
+
45
+ protected onStop(): void {
46
+ this.blurRenderer?.close();
47
+ }
48
+
49
+ protected get processorName(): string {
50
+ return 'fullscreen-blur';
51
+ }
52
+ }