@stream-io/video-filters-web 0.8.6 → 0.8.7

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/esm/index.js +10 -0
  3. package/dist/esm/index.js.map +1 -0
  4. package/dist/esm/src/BaseVideoProcessor.js +107 -0
  5. package/dist/esm/src/BaseVideoProcessor.js.map +1 -0
  6. package/dist/esm/src/FallbackGenerator.js +58 -0
  7. package/dist/esm/src/FallbackGenerator.js.map +1 -0
  8. package/dist/esm/src/FallbackProcessor.js +89 -0
  9. package/dist/esm/src/FallbackProcessor.js.map +1 -0
  10. package/dist/esm/src/FullScreenBlur.js +41 -0
  11. package/dist/esm/src/FullScreenBlur.js.map +1 -0
  12. package/dist/esm/src/FullScreenBlurRenderer.js +276 -0
  13. package/dist/esm/src/FullScreenBlurRenderer.js.map +1 -0
  14. package/dist/esm/src/VirtualBackground.js +143 -0
  15. package/dist/esm/src/VirtualBackground.js.map +1 -0
  16. package/dist/esm/src/WebGLRenderer.js +723 -0
  17. package/dist/esm/src/WebGLRenderer.js.map +1 -0
  18. package/dist/esm/src/compatibility.js +48 -0
  19. package/dist/esm/src/compatibility.js.map +1 -0
  20. package/dist/esm/src/legacy/createRenderer.js +33 -0
  21. package/dist/esm/src/legacy/createRenderer.js.map +1 -0
  22. package/dist/esm/src/legacy/helpers/webglHelper.js +94 -0
  23. package/dist/esm/src/legacy/helpers/webglHelper.js.map +1 -0
  24. package/dist/esm/src/legacy/segmentation.js +14 -0
  25. package/dist/esm/src/legacy/segmentation.js.map +1 -0
  26. package/dist/esm/src/legacy/tflite-simd.js +728 -0
  27. package/dist/esm/src/legacy/tflite-simd.js.map +1 -0
  28. package/dist/esm/src/legacy/tflite.js +31 -0
  29. package/dist/esm/src/legacy/tflite.js.map +1 -0
  30. package/dist/esm/src/legacy/webgl2/backgroundBlurStage.js +202 -0
  31. package/dist/esm/src/legacy/webgl2/backgroundBlurStage.js.map +1 -0
  32. package/dist/esm/src/legacy/webgl2/backgroundImageStage.js +157 -0
  33. package/dist/esm/src/legacy/webgl2/backgroundImageStage.js.map +1 -0
  34. package/dist/esm/src/legacy/webgl2/jointBilateralFilterStage.js +113 -0
  35. package/dist/esm/src/legacy/webgl2/jointBilateralFilterStage.js.map +1 -0
  36. package/dist/esm/src/legacy/webgl2/resizingStage.js +57 -0
  37. package/dist/esm/src/legacy/webgl2/resizingStage.js.map +1 -0
  38. package/dist/esm/src/legacy/webgl2/softmaxStage.js +51 -0
  39. package/dist/esm/src/legacy/webgl2/softmaxStage.js.map +1 -0
  40. package/dist/esm/src/legacy/webgl2/webgl2Pipeline.js +105 -0
  41. package/dist/esm/src/legacy/webgl2/webgl2Pipeline.js.map +1 -0
  42. package/dist/esm/src/mediapipe.js +16 -0
  43. package/dist/esm/src/mediapipe.js.map +1 -0
  44. package/dist/esm/src/types.js +11 -0
  45. package/dist/esm/src/types.js.map +1 -0
  46. package/dist/esm/src/version.js +5 -0
  47. package/dist/esm/src/version.js.map +1 -0
  48. package/dist/index.cjs.js +1 -1
  49. package/package.json +3 -3
  50. package/dist/index.es.js +0 -3022
  51. package/dist/index.es.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [0.8.7](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-filters-web-0.8.6...@stream-io/video-filters-web-0.8.7) (2026-08-28)
6
+
7
+ ### Performance Improvements
8
+
9
+ - emit per-module ESM builds for the React packages ([#2378](https://github.com/GetStream/stream-video-js/issues/2378)) ([9c937f4](https://github.com/GetStream/stream-video-js/commit/9c937f4b5845aa0e8108a218b1c84b7acb249d77))
10
+
5
11
  ## [0.8.6](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-filters-web-0.8.5...@stream-io/video-filters-web-0.8.6) (2026-08-07)
6
12
 
7
13
  ## [0.8.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-filters-web-0.8.4...@stream-io/video-filters-web-0.8.5) (2026-07-30)
@@ -0,0 +1,10 @@
1
+ export { isMediaPipePlatformSupported, isPlatformSupported } from './src/compatibility.js';
2
+ export { createRenderer } from './src/legacy/createRenderer.js';
3
+ export { SegmentationLevel } from './src/legacy/segmentation.js';
4
+ export { loadTFLite } from './src/legacy/tflite.js';
5
+ export { loadMediaPipe } from './src/mediapipe.js';
6
+ export { BACKGROUND_BLUR_MAP } from './src/types.js';
7
+ export { BaseVideoProcessor } from './src/BaseVideoProcessor.js';
8
+ export { VirtualBackground } from './src/VirtualBackground.js';
9
+ export { FullScreenBlur } from './src/FullScreenBlur.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,107 @@
1
+ import { TrackGenerator } from './FallbackGenerator.js';
2
+ import { TrackProcessor } from './FallbackProcessor.js';
3
+
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
+ class BaseVideoProcessor {
17
+ track;
18
+ processor;
19
+ generator;
20
+ hooks;
21
+ abortController = new AbortController();
22
+ canvas;
23
+ frames = 0;
24
+ delayTotal = 0;
25
+ lastStatsTime = 0;
26
+ /**
27
+ * Constructs a new instance.
28
+ */
29
+ constructor(track, hooks = {}) {
30
+ this.track = track;
31
+ this.processor = new TrackProcessor({ track });
32
+ this.generator = new TrackGenerator({
33
+ kind: 'video',
34
+ signalTarget: track,
35
+ });
36
+ this.hooks = hooks;
37
+ }
38
+ async start() {
39
+ const { readable } = this.processor;
40
+ const { writable } = this.generator;
41
+ const { width = 1280, height = 720 } = this.track.getSettings();
42
+ this.canvas = new OffscreenCanvas(width, height);
43
+ await this.initialize();
44
+ const transformStream = new TransformStream({
45
+ transform: async (frame, controller) => {
46
+ try {
47
+ if (this.abortController.signal.aborted)
48
+ return frame.close();
49
+ if (this.canvas.width !== frame.displayWidth ||
50
+ this.canvas.height !== frame.displayHeight) {
51
+ this.canvas.width = frame.displayWidth;
52
+ this.canvas.height = frame.displayHeight;
53
+ }
54
+ const processed = await this.transform(frame);
55
+ controller.enqueue(processed);
56
+ }
57
+ catch (e) {
58
+ this.hooks.onError?.(e);
59
+ }
60
+ finally {
61
+ frame.close();
62
+ }
63
+ },
64
+ flush: () => this.onFlush(),
65
+ });
66
+ readable
67
+ .pipeThrough(transformStream, { signal: this.abortController.signal })
68
+ .pipeTo(writable, { signal: this.abortController.signal })
69
+ .catch((e) => {
70
+ if (e.name !== 'AbortError' && e.name !== 'InvalidStateError') {
71
+ console.error(`[${this.processorName}] Error processing track:`, e);
72
+ this.hooks.onError?.(e);
73
+ }
74
+ });
75
+ return this.generator;
76
+ }
77
+ stop() {
78
+ this.abortController.abort();
79
+ this.generator.stop();
80
+ this.onStop();
81
+ }
82
+ updateStats(delay) {
83
+ this.frames++;
84
+ this.delayTotal += delay;
85
+ const now = performance.now();
86
+ if (this.lastStatsTime === 0) {
87
+ this.lastStatsTime = now;
88
+ return;
89
+ }
90
+ if (now - this.lastStatsTime >= 1000) {
91
+ const avgDelay = Math.round((this.delayTotal / this.frames) * 100) / 100;
92
+ const fps = Math.round((1000 * this.frames) / (now - this.lastStatsTime));
93
+ this.hooks.onStats?.({ delay: avgDelay, fps, timestamp: now });
94
+ this.frames = 0;
95
+ this.delayTotal = 0;
96
+ this.lastStatsTime = now;
97
+ }
98
+ }
99
+ onFlush() { }
100
+ onStop() { }
101
+ get processorName() {
102
+ return 'base-processor';
103
+ }
104
+ }
105
+
106
+ export { BaseVideoProcessor };
107
+ //# sourceMappingURL=BaseVideoProcessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BaseVideoProcessor.js","sources":["../../../../../src/BaseVideoProcessor.ts"],"sourcesContent":["import { VideoTrackProcessorHooks } from './types';\nimport { TrackGenerator, MediaStreamTrackGenerator } from './FallbackGenerator';\nimport { MediaStreamTrackProcessor, TrackProcessor } from './FallbackProcessor';\n\n/**\n * Base class for real-time video filters.\n *\n * It sets up the full pipeline that reads frames from the input track,\n * processes them, and outputs a new track with your effect applied. Subclasses\n * only need to implement `initialize` (run once before processing starts) and\n * `transform` (called for every frame).\n *\n * Everything else—canvas setup, performance tracking, error handling, and\n * clean shutdown is handled for you. Calling `start()` returns a processed\n * `MediaStreamTrack` ready to use.\n */\nexport abstract class BaseVideoProcessor {\n protected readonly processor: MediaStreamTrackProcessor<VideoFrame>;\n protected readonly generator: MediaStreamTrackGenerator<VideoFrame>;\n\n protected readonly hooks: VideoTrackProcessorHooks;\n\n protected readonly abortController = new AbortController();\n protected canvas!: OffscreenCanvas;\n\n private frames = 0;\n private delayTotal = 0;\n private lastStatsTime = 0;\n\n /**\n * Constructs a new instance.\n */\n protected constructor(\n protected readonly track: MediaStreamVideoTrack,\n hooks: VideoTrackProcessorHooks = {},\n ) {\n this.processor = new TrackProcessor({ track });\n this.generator = new TrackGenerator({\n kind: 'video',\n signalTarget: track,\n });\n this.hooks = hooks;\n }\n\n public async start(): Promise<MediaStreamTrack> {\n const { readable } = this.processor;\n const { writable } = this.generator;\n\n const { width = 1280, height = 720 } = this.track.getSettings();\n this.canvas = new OffscreenCanvas(width, height);\n\n await this.initialize();\n\n const transformStream = new TransformStream<VideoFrame, VideoFrame>({\n transform: async (frame, controller) => {\n try {\n if (this.abortController.signal.aborted) return frame.close();\n\n if (\n this.canvas.width !== frame.displayWidth ||\n this.canvas.height !== frame.displayHeight\n ) {\n this.canvas.width = frame.displayWidth;\n this.canvas.height = frame.displayHeight;\n }\n\n const processed = await this.transform(frame);\n\n controller.enqueue(processed);\n } catch (e) {\n this.hooks.onError?.(e);\n } finally {\n frame.close();\n }\n },\n flush: () => this.onFlush(),\n });\n\n readable\n .pipeThrough(transformStream, { signal: this.abortController.signal })\n .pipeTo(writable, { signal: this.abortController.signal })\n .catch((e) => {\n if (e.name !== 'AbortError' && e.name !== 'InvalidStateError') {\n console.error(`[${this.processorName}] Error processing track:`, e);\n this.hooks.onError?.(e);\n }\n });\n\n return this.generator;\n }\n\n public stop(): void {\n this.abortController.abort();\n this.generator.stop();\n this.onStop();\n }\n\n protected updateStats(delay: number): void {\n this.frames++;\n this.delayTotal += delay;\n\n const now = performance.now();\n if (this.lastStatsTime === 0) {\n this.lastStatsTime = now;\n return;\n }\n\n if (now - this.lastStatsTime >= 1000) {\n const avgDelay = Math.round((this.delayTotal / this.frames) * 100) / 100;\n const fps = Math.round((1000 * this.frames) / (now - this.lastStatsTime));\n\n this.hooks.onStats?.({ delay: avgDelay, fps, timestamp: now });\n\n this.frames = 0;\n this.delayTotal = 0;\n this.lastStatsTime = now;\n }\n }\n\n protected abstract initialize(): Promise<void>;\n protected abstract transform(frame: VideoFrame): Promise<VideoFrame>;\n\n protected onFlush(): void {}\n protected onStop(): void {}\n\n protected get processorName(): string {\n return 'base-processor';\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Fallback video processor for browsers that do not support MediaStreamTrackGenerator.
3
+ *
4
+ * Produces a video MediaStreamTrack sourced from a canvas and exposes
5
+ * a WritableStream<VideoFrame> on track.writable for writing frames.
6
+ */
7
+ class FallbackGenerator {
8
+ constructor({ kind, signalTarget }) {
9
+ if (kind !== 'video') {
10
+ throw new Error('Only video tracks are supported');
11
+ }
12
+ const canvas = document.createElement('canvas');
13
+ const ctx = canvas.getContext('2d', { desynchronized: true });
14
+ if (!ctx) {
15
+ throw new Error('Failed to get 2D context from canvas');
16
+ }
17
+ const mediaStream = canvas.captureStream();
18
+ const track = mediaStream.getVideoTracks()[0];
19
+ const height = signalTarget?.getSettings().height;
20
+ const width = signalTarget?.getSettings().width;
21
+ if (height && width) {
22
+ canvas.height = height;
23
+ canvas.width = width;
24
+ }
25
+ if (!track) {
26
+ throw new Error('Failed to create canvas track');
27
+ }
28
+ if (signalTarget) {
29
+ signalTarget.addEventListener('ended', () => {
30
+ track.stop();
31
+ });
32
+ }
33
+ track.writable = new WritableStream({
34
+ write: (frame) => {
35
+ if (canvas.width !== frame.displayWidth ||
36
+ canvas.height !== frame.displayHeight) {
37
+ canvas.width = frame.displayWidth;
38
+ canvas.height = frame.displayHeight;
39
+ }
40
+ ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
41
+ frame.close();
42
+ },
43
+ abort: () => {
44
+ track.stop();
45
+ },
46
+ close: () => {
47
+ track.stop();
48
+ },
49
+ });
50
+ return track;
51
+ }
52
+ }
53
+ const TrackGenerator = typeof MediaStreamTrackGenerator !== 'undefined'
54
+ ? MediaStreamTrackGenerator
55
+ : FallbackGenerator;
56
+
57
+ export { TrackGenerator };
58
+ //# sourceMappingURL=FallbackGenerator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FallbackGenerator.js","sources":["../../../../../src/FallbackGenerator.ts"],"sourcesContent":["/**\n * Type representing a video track generator that can be either the native\n * MediaStreamTrackGenerator or the fallback implementation.\n */\nexport interface MediaStreamTrackGenerator<T> extends MediaStreamTrack {\n writable: WritableStream<T>;\n}\n\n/**\n * Configuration options for creating a track generator.\n */\nexport interface TrackGeneratorOptions {\n readonly kind: 'video';\n readonly signalTarget?: MediaStreamTrack;\n}\n\n/**\n * Fallback video processor for browsers that do not support MediaStreamTrackGenerator.\n *\n * Produces a video MediaStreamTrack sourced from a canvas and exposes\n * a WritableStream<VideoFrame> on track.writable for writing frames.\n */\nclass FallbackGenerator {\n constructor({ kind, signalTarget }: TrackGeneratorOptions) {\n if (kind !== 'video') {\n throw new Error('Only video tracks are supported');\n }\n\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d', { desynchronized: true });\n if (!ctx) {\n throw new Error('Failed to get 2D context from canvas');\n }\n\n const mediaStream = canvas.captureStream();\n const track =\n mediaStream.getVideoTracks()[0] as MediaStreamTrackGenerator<VideoFrame>;\n\n const height = signalTarget?.getSettings().height;\n const width = signalTarget?.getSettings().width;\n if (height && width) {\n canvas.height = height;\n canvas.width = width;\n }\n\n if (!track) {\n throw new Error('Failed to create canvas track');\n }\n\n if (signalTarget) {\n signalTarget.addEventListener('ended', () => {\n track.stop();\n });\n }\n\n track.writable = new WritableStream({\n write: (frame: VideoFrame) => {\n if (\n canvas.width !== frame.displayWidth ||\n canvas.height !== frame.displayHeight\n ) {\n canvas.width = frame.displayWidth;\n canvas.height = frame.displayHeight;\n }\n\n ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);\n frame.close();\n },\n abort: () => {\n track.stop();\n },\n close: () => {\n track.stop();\n },\n });\n\n return track as MediaStreamTrackGenerator<VideoFrame>;\n }\n}\n\ntype VideoTrackGeneratorConstructor = new (\n options: TrackGeneratorOptions,\n) => MediaStreamTrackGenerator<VideoFrame>;\n\nconst TrackGenerator: VideoTrackGeneratorConstructor =\n typeof MediaStreamTrackGenerator !== 'undefined'\n ? (MediaStreamTrackGenerator as unknown as VideoTrackGeneratorConstructor)\n : (FallbackGenerator as unknown as VideoTrackGeneratorConstructor);\n\nexport { TrackGenerator };\n"],"names":[],"mappings":"AAgBA;;;;;AAKG;AACH,MAAM,iBAAiB,CAAA;AACrB,IAAA,WAAA,CAAY,EAAE,IAAI,EAAE,YAAY,EAAyB,EAAA;AACvD,QAAA,IAAI,IAAI,KAAK,OAAO,EAAE;AACpB,YAAA,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC;QACpD;QAEA,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC;AAC/C,QAAA,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;QAC7D,IAAI,CAAC,GAAG,EAAE;AACR,YAAA,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC;QACzD;AAEA,QAAA,MAAM,WAAW,GAAG,MAAM,CAAC,aAAa,EAAE;QAC1C,MAAM,KAAK,GACT,WAAW,CAAC,cAAc,EAAE,CAAC,CAAC,CAA0C;QAE1E,MAAM,MAAM,GAAG,YAAY,EAAE,WAAW,EAAE,CAAC,MAAM;QACjD,MAAM,KAAK,GAAG,YAAY,EAAE,WAAW,EAAE,CAAC,KAAK;AAC/C,QAAA,IAAI,MAAM,IAAI,KAAK,EAAE;AACnB,YAAA,MAAM,CAAC,MAAM,GAAG,MAAM;AACtB,YAAA,MAAM,CAAC,KAAK,GAAG,KAAK;QACtB;QAEA,IAAI,CAAC,KAAK,EAAE;AACV,YAAA,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC;QAClD;QAEA,IAAI,YAAY,EAAE;AAChB,YAAA,YAAY,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAK;gBAC1C,KAAK,CAAC,IAAI,EAAE;AACd,YAAA,CAAC,CAAC;QACJ;AAEA,QAAA,KAAK,CAAC,QAAQ,GAAG,IAAI,cAAc,CAAC;AAClC,YAAA,KAAK,EAAE,CAAC,KAAiB,KAAI;AAC3B,gBAAA,IACE,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,YAAY;AACnC,oBAAA,MAAM,CAAC,MAAM,KAAK,KAAK,CAAC,aAAa,EACrC;AACA,oBAAA,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,YAAY;AACjC,oBAAA,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,aAAa;gBACrC;AAEA,gBAAA,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC;gBACvD,KAAK,CAAC,KAAK,EAAE;YACf,CAAC;YACD,KAAK,EAAE,MAAK;gBACV,KAAK,CAAC,IAAI,EAAE;YACd,CAAC;YACD,KAAK,EAAE,MAAK;gBACV,KAAK,CAAC,IAAI,EAAE;YACd,CAAC;AACF,SAAA,CAAC;AAEF,QAAA,OAAO,KAA8C;IACvD;AACD;AAMD,MAAM,cAAc,GAClB,OAAO,yBAAyB,KAAK;AACnC,MAAG;MACA;;;;"}
@@ -0,0 +1,89 @@
1
+ import { WorkerTimer } from '@stream-io/worker-timer';
2
+
3
+ /**
4
+ * Fallback implementation for browsers without MediaStreamTrackGenerator.
5
+ *
6
+ * Produces a video MediaStreamTrack sourced from a canvas and exposes a
7
+ * WritableStream<VideoFrame> on track.writable. Written frames are drawn
8
+ * into the canvas and update the underlying track automatically.
9
+ */
10
+ class FallbackProcessor {
11
+ readable;
12
+ workerTimer;
13
+ video;
14
+ constructor({ track }) {
15
+ if (!track)
16
+ throw new Error('MediaStreamTrack is required');
17
+ if (track.kind !== 'video') {
18
+ throw new Error('MediaStreamTrack must be video');
19
+ }
20
+ let running = true;
21
+ this.video = document.createElement('video');
22
+ this.video.muted = true;
23
+ this.video.playsInline = true;
24
+ this.video.srcObject = new MediaStream([track]);
25
+ const canvas = new OffscreenCanvas(1, 1);
26
+ const ctx = canvas.getContext('2d');
27
+ if (!ctx)
28
+ throw new Error('Failed to get 2D context from OffscreenCanvas');
29
+ let timestamp = 0;
30
+ const frameRate = track.getSettings().frameRate || 30;
31
+ let frameDuration = 1000 / frameRate;
32
+ this.workerTimer = new WorkerTimer({ useWorker: true });
33
+ this.readable = new ReadableStream({
34
+ start: async () => {
35
+ await Promise.all([
36
+ this.video.play(),
37
+ new Promise((r) => this.video.addEventListener('loadeddata', r, { once: true })),
38
+ ]);
39
+ frameDuration = 1000 / (track.getSettings().frameRate || 30);
40
+ timestamp = performance.now();
41
+ },
42
+ pull: async (controller) => {
43
+ if (!running) {
44
+ controller.close();
45
+ this.close();
46
+ return;
47
+ }
48
+ const delta = performance.now() - timestamp;
49
+ if (delta <= frameDuration) {
50
+ await new Promise((r) => this.workerTimer.setTimeout(r, frameDuration - delta));
51
+ }
52
+ timestamp = performance.now();
53
+ if (canvas.width !== this.video.videoWidth ||
54
+ canvas.height !== this.video.videoHeight) {
55
+ canvas.width = this.video.videoWidth;
56
+ canvas.height = this.video.videoHeight;
57
+ }
58
+ ctx.drawImage(this.video, 0, 0);
59
+ try {
60
+ const frame = new VideoFrame(canvas, {
61
+ timestamp: Math.round(this.video.currentTime * 1000000),
62
+ });
63
+ controller.enqueue(frame);
64
+ }
65
+ catch (err) {
66
+ running = false;
67
+ controller.error(err);
68
+ this.close();
69
+ }
70
+ },
71
+ cancel: () => {
72
+ running = false;
73
+ this.close();
74
+ },
75
+ });
76
+ }
77
+ close = () => {
78
+ this.video.pause();
79
+ this.video.srcObject = null;
80
+ this.video.src = '';
81
+ this.workerTimer.destroy();
82
+ };
83
+ }
84
+ const TrackProcessor = typeof MediaStreamTrackProcessor !== 'undefined'
85
+ ? MediaStreamTrackProcessor
86
+ : FallbackProcessor;
87
+
88
+ export { TrackProcessor };
89
+ //# sourceMappingURL=FallbackProcessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FallbackProcessor.js","sources":["../../../../../src/FallbackProcessor.ts"],"sourcesContent":["import { WorkerTimer } from '@stream-io/worker-timer';\n\n/**\n * Representing a video track processor that can be either the native\n * MediaStreamTrackProcessor or the fallback implementation.\n */\nexport interface MediaStreamTrackProcessor<T> {\n readonly readable: ReadableStream<T>;\n}\n\n/**\n * Configuration options for creating a track processor.\n */\nexport interface TrackProcessorOptions {\n readonly track: MediaStreamTrack;\n}\n\n/**\n * Fallback implementation for browsers without MediaStreamTrackGenerator.\n *\n * Produces a video MediaStreamTrack sourced from a canvas and exposes a\n * WritableStream<VideoFrame> on track.writable. Written frames are drawn\n * into the canvas and update the underlying track automatically.\n */\nclass FallbackProcessor implements MediaStreamTrackProcessor<VideoFrame> {\n readonly readable: ReadableStream<VideoFrame>;\n\n readonly workerTimer: WorkerTimer;\n readonly video: HTMLVideoElement;\n\n constructor({ track }: TrackProcessorOptions) {\n if (!track) throw new Error('MediaStreamTrack is required');\n if (track.kind !== 'video') {\n throw new Error('MediaStreamTrack must be video');\n }\n let running = true;\n\n this.video = document.createElement('video');\n\n this.video.muted = true;\n this.video.playsInline = true;\n this.video.srcObject = new MediaStream([track]);\n\n const canvas = new OffscreenCanvas(1, 1);\n const ctx = canvas.getContext('2d');\n\n if (!ctx) throw new Error('Failed to get 2D context from OffscreenCanvas');\n\n let timestamp = 0;\n const frameRate = track.getSettings().frameRate || 30;\n let frameDuration = 1000 / frameRate;\n\n this.workerTimer = new WorkerTimer({ useWorker: true });\n this.readable = new ReadableStream({\n start: async () => {\n await Promise.all([\n this.video.play(),\n new Promise((r) =>\n this.video.addEventListener('loadeddata', r, { once: true }),\n ),\n ]);\n frameDuration = 1000 / (track.getSettings().frameRate || 30);\n timestamp = performance.now();\n },\n pull: async (controller) => {\n if (!running) {\n controller.close();\n this.close();\n return;\n }\n const delta = performance.now() - timestamp;\n if (delta <= frameDuration) {\n await new Promise((r: (value?: unknown) => void) =>\n this.workerTimer.setTimeout(r, frameDuration - delta),\n );\n }\n timestamp = performance.now();\n\n if (\n canvas.width !== this.video.videoWidth ||\n canvas.height !== this.video.videoHeight\n ) {\n canvas.width = this.video.videoWidth;\n canvas.height = this.video.videoHeight;\n }\n\n ctx.drawImage(this.video, 0, 0);\n\n try {\n const frame = new VideoFrame(canvas, {\n timestamp: Math.round(this.video.currentTime * 1000000),\n });\n\n controller.enqueue(frame);\n } catch (err) {\n running = false;\n controller.error(err);\n this.close();\n }\n },\n cancel: () => {\n running = false;\n this.close();\n },\n });\n }\n\n public close = () => {\n this.video.pause();\n this.video.srcObject = null;\n this.video.src = '';\n\n this.workerTimer.destroy();\n };\n}\n\nconst TrackProcessor =\n typeof MediaStreamTrackProcessor !== 'undefined'\n ? MediaStreamTrackProcessor\n : FallbackProcessor;\n\nexport { TrackProcessor };\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { BaseVideoProcessor } from './BaseVideoProcessor.js';
2
+ import { FullScreenBlurRenderer } from './FullScreenBlurRenderer.js';
3
+
4
+ /**
5
+ * A video filter that applies a full-screen blur to each frame.
6
+ *
7
+ * It uses a WebGL renderer to blur the incoming camera track and outputs
8
+ * a new track with the effect applied. Setup and frame handling are managed
9
+ * by the base processor.
10
+ */
11
+ class FullScreenBlur extends BaseVideoProcessor {
12
+ blurRenderer;
13
+ blurRadius;
14
+ /**
15
+ * Creates a new full-screen blur processor for the given video track.
16
+ *
17
+ * @param track - The input camera track to blur.
18
+ * @param options - Optional settings such as the blur radius.
19
+ * @param hooks - Optional callbacks for stats and error reporting.
20
+ */
21
+ constructor(track, options = {}, hooks = {}) {
22
+ super(track, hooks);
23
+ this.blurRadius = options.blurRadius ?? 6;
24
+ }
25
+ async initialize() {
26
+ this.blurRenderer = new FullScreenBlurRenderer(this.canvas);
27
+ }
28
+ async transform(frame) {
29
+ this.blurRenderer.render(frame, this.blurRadius);
30
+ return new VideoFrame(this.canvas, { timestamp: frame.timestamp });
31
+ }
32
+ onStop() {
33
+ this.blurRenderer?.close();
34
+ }
35
+ get processorName() {
36
+ return 'fullscreen-blur';
37
+ }
38
+ }
39
+
40
+ export { FullScreenBlur };
41
+ //# sourceMappingURL=FullScreenBlur.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FullScreenBlur.js","sources":["../../../../../src/FullScreenBlur.ts"],"sourcesContent":["import { VideoTrackProcessorHooks } from './types';\nimport { BaseVideoProcessor } from './BaseVideoProcessor';\nimport { FullScreenBlurRenderer } from './FullScreenBlurRenderer';\n\nexport interface FullScreenBlurOptions {\n blurRadius?: number;\n}\n\n/**\n * A video filter that applies a full-screen blur to each frame.\n *\n * It uses a WebGL renderer to blur the incoming camera track and outputs\n * a new track with the effect applied. Setup and frame handling are managed\n * by the base processor.\n */\nexport class FullScreenBlur extends BaseVideoProcessor {\n private blurRenderer!: FullScreenBlurRenderer;\n private readonly blurRadius: number;\n\n /**\n * Creates a new full-screen blur processor for the given video track.\n *\n * @param track - The input camera track to blur.\n * @param options - Optional settings such as the blur radius.\n * @param hooks - Optional callbacks for stats and error reporting.\n */\n constructor(\n track: MediaStreamVideoTrack,\n options: FullScreenBlurOptions = {},\n hooks: VideoTrackProcessorHooks = {},\n ) {\n super(track, hooks);\n this.blurRadius = options.blurRadius ?? 6;\n }\n\n protected async initialize(): Promise<void> {\n this.blurRenderer = new FullScreenBlurRenderer(this.canvas);\n }\n\n protected async transform(frame: VideoFrame): Promise<VideoFrame> {\n this.blurRenderer.render(frame, this.blurRadius);\n return new VideoFrame(this.canvas, { timestamp: frame.timestamp });\n }\n\n protected onStop(): void {\n this.blurRenderer?.close();\n }\n\n protected get processorName(): string {\n return 'fullscreen-blur';\n }\n}\n"],"names":[],"mappings":";;;AAQA;;;;;;AAMG;AACG,MAAO,cAAe,SAAQ,kBAAkB,CAAA;AAC5C,IAAA,YAAY;AACH,IAAA,UAAU;AAE3B;;;;;;AAMG;AACH,IAAA,WAAA,CACE,KAA4B,EAC5B,OAAA,GAAiC,EAAE,EACnC,QAAkC,EAAE,EAAA;AAEpC,QAAA,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC;IAC3C;AAEU,IAAA,MAAM,UAAU,GAAA;QACxB,IAAI,CAAC,YAAY,GAAG,IAAI,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC;IAC7D;IAEU,MAAM,SAAS,CAAC,KAAiB,EAAA;QACzC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC;AAChD,QAAA,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;IACpE;IAEU,MAAM,GAAA;AACd,QAAA,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE;IAC5B;AAEA,IAAA,IAAc,aAAa,GAAA;AACzB,QAAA,OAAO,iBAAiB;IAC1B;AACD;;;;"}
@@ -0,0 +1,276 @@
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
+ class FullScreenBlurRenderer {
8
+ canvas;
9
+ gl;
10
+ blurProgramHandle;
11
+ blurLocations;
12
+ passthroughProgramHandle;
13
+ passthroughLocations;
14
+ positionBuffer;
15
+ texCoordBuffer;
16
+ pingTexture;
17
+ pongTexture;
18
+ pingFbo;
19
+ pongFbo;
20
+ inputTexture = null;
21
+ isRunning = false;
22
+ targetWidth = 0;
23
+ targetHeight = 0;
24
+ weightCache = new Map();
25
+ constructor(canvas) {
26
+ this.canvas = canvas;
27
+ const gl = canvas.getContext('webgl2', {
28
+ alpha: false,
29
+ antialias: false,
30
+ desynchronized: true,
31
+ });
32
+ if (!gl)
33
+ throw new Error('WebGL2 not supported');
34
+ this.gl = gl;
35
+ const vertexShaderSource = `#version 300 es
36
+ precision highp float;
37
+ in vec2 a_position;
38
+ in vec2 a_texCoord;
39
+ out vec2 v_texCoord;
40
+ void main() {
41
+ v_texCoord = a_texCoord;
42
+ gl_Position = vec4(a_position, 0.0, 1.0);
43
+ }
44
+ `;
45
+ const fragmentShaderSource = `#version 300 es
46
+ precision highp float;
47
+ in vec2 v_texCoord;
48
+ out vec4 outColor;
49
+ uniform sampler2D u_image;
50
+ uniform vec2 u_texelSize;
51
+ uniform vec2 u_direction;
52
+ uniform float u_weights[25];
53
+ void main() {
54
+ vec4 color = vec4(0.0);
55
+ for (int i = -12; i <= 12; i++) {
56
+ float w = u_weights[i + 12];
57
+ if (w == 0.0) continue;
58
+ vec2 offset = float(i) * u_direction * u_texelSize;
59
+ color += w * texture(u_image, v_texCoord + offset);
60
+ }
61
+ outColor = color;
62
+ }
63
+ `;
64
+ this.blurProgramHandle = this.createAndLinkProgram(vertexShaderSource, fragmentShaderSource);
65
+ const passthroughFragmentShaderSource = `#version 300 es
66
+ precision highp float;
67
+ in vec2 v_texCoord;
68
+ out vec4 outColor;
69
+ uniform sampler2D u_image;
70
+ void main() {
71
+ outColor = texture(u_image, v_texCoord);
72
+ }
73
+ `;
74
+ this.passthroughProgramHandle = this.createAndLinkProgram(vertexShaderSource, passthroughFragmentShaderSource);
75
+ const blurProgram = this.blurProgramHandle;
76
+ const passthroughProgram = this.passthroughProgramHandle;
77
+ this.blurLocations = {
78
+ positionLocation: gl.getAttribLocation(blurProgram, 'a_position'),
79
+ texCoordLocation: gl.getAttribLocation(blurProgram, 'a_texCoord'),
80
+ imageLocation: gl.getUniformLocation(blurProgram, 'u_image'),
81
+ texelSizeLocation: gl.getUniformLocation(blurProgram, 'u_texelSize'),
82
+ directionLocation: gl.getUniformLocation(blurProgram, 'u_direction'),
83
+ weightsLocation: gl.getUniformLocation(blurProgram, 'u_weights'),
84
+ };
85
+ this.passthroughLocations = {
86
+ positionLocation: gl.getAttribLocation(passthroughProgram, 'a_position'),
87
+ texCoordLocation: gl.getAttribLocation(passthroughProgram, 'a_texCoord'),
88
+ imageLocation: gl.getUniformLocation(passthroughProgram, 'u_image'),
89
+ };
90
+ this.positionBuffer = gl.createBuffer();
91
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
92
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
93
+ this.texCoordBuffer = gl.createBuffer();
94
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
95
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0]), gl.STATIC_DRAW);
96
+ const createTexture2D = () => {
97
+ const tex = gl.createTexture();
98
+ if (!tex)
99
+ throw new Error('Failed to create texture');
100
+ gl.bindTexture(gl.TEXTURE_2D, tex);
101
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
102
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
103
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
104
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
105
+ gl.bindTexture(gl.TEXTURE_2D, null);
106
+ return tex;
107
+ };
108
+ const createFramebufferForTexture = (tex) => {
109
+ const fb = gl.createFramebuffer();
110
+ if (!fb)
111
+ throw new Error('Failed to create framebuffer');
112
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
113
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
114
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
115
+ return fb;
116
+ };
117
+ this.pingTexture = createTexture2D();
118
+ this.pongTexture = createTexture2D();
119
+ this.pingFbo = createFramebufferForTexture(this.pingTexture);
120
+ this.pongFbo = createFramebufferForTexture(this.pongTexture);
121
+ this.inputTexture = createTexture2D();
122
+ gl.useProgram(blurProgram);
123
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
124
+ gl.enableVertexAttribArray(this.blurLocations.positionLocation);
125
+ gl.vertexAttribPointer(this.blurLocations.positionLocation, 2, gl.FLOAT, false, 0, 0);
126
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
127
+ gl.enableVertexAttribArray(this.blurLocations.texCoordLocation);
128
+ gl.vertexAttribPointer(this.blurLocations.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
129
+ if (this.blurLocations.imageLocation) {
130
+ gl.uniform1i(this.blurLocations.imageLocation, 0);
131
+ }
132
+ this.isRunning = true;
133
+ }
134
+ createAndLinkProgram(vsSource, fsSource) {
135
+ const gl = this.gl;
136
+ const vs = this.createShader(gl.VERTEX_SHADER, vsSource);
137
+ const fs = this.createShader(gl.FRAGMENT_SHADER, fsSource);
138
+ const prog = gl.createProgram();
139
+ if (!prog)
140
+ throw new Error('Failed to create program');
141
+ gl.attachShader(prog, vs);
142
+ gl.attachShader(prog, fs);
143
+ gl.linkProgram(prog);
144
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
145
+ throw new Error('Shader link failed: ' + gl.getProgramInfoLog(prog));
146
+ }
147
+ gl.deleteShader(vs);
148
+ gl.deleteShader(fs);
149
+ return prog;
150
+ }
151
+ createShader(type, source) {
152
+ const gl = this.gl;
153
+ const shader = gl.createShader(type);
154
+ if (!shader)
155
+ throw new Error('Failed to create shader');
156
+ gl.shaderSource(shader, source);
157
+ gl.compileShader(shader);
158
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
159
+ throw new Error('Shader compile failed: ' + gl.getShaderInfoLog(shader));
160
+ }
161
+ return shader;
162
+ }
163
+ getGaussianWeights(radius) {
164
+ const r = Math.max(0, Math.min(radius | 0, 12));
165
+ const cached = this.weightCache.get(r);
166
+ if (cached)
167
+ return cached;
168
+ const weights = new Float32Array(25);
169
+ if (r === 0) {
170
+ weights[12] = 1.0;
171
+ this.weightCache.set(r, weights);
172
+ return weights;
173
+ }
174
+ const sigma = r * 0.6;
175
+ let sum = 0;
176
+ for (let i = -r; i <= r; i++) {
177
+ const w = Math.exp(-(i * i) / (2 * sigma * sigma));
178
+ weights[i + 12] = w;
179
+ sum += w;
180
+ }
181
+ for (let i = -r; i <= r; i++) {
182
+ weights[i + 12] /= sum;
183
+ }
184
+ this.weightCache.set(r, weights);
185
+ return weights;
186
+ }
187
+ render(frame, radius) {
188
+ if (!this.isRunning)
189
+ return;
190
+ const gl = this.gl;
191
+ const width = frame.displayWidth;
192
+ const height = frame.displayHeight;
193
+ if (!width || !height)
194
+ return;
195
+ if (this.canvas.width !== width || this.canvas.height !== height) {
196
+ this.canvas.width = width;
197
+ this.canvas.height = height;
198
+ }
199
+ const scale = 0.15;
200
+ const scaledWidth = Math.max(1, Math.floor(width * scale));
201
+ const scaledHeight = Math.max(1, Math.floor(height * scale));
202
+ if (scaledWidth !== this.targetWidth ||
203
+ scaledHeight !== this.targetHeight) {
204
+ this.targetWidth = scaledWidth;
205
+ this.targetHeight = scaledHeight;
206
+ gl.bindTexture(gl.TEXTURE_2D, this.pingTexture);
207
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, scaledWidth, scaledHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
208
+ gl.bindTexture(gl.TEXTURE_2D, this.pongTexture);
209
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, scaledWidth, scaledHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
210
+ gl.bindTexture(gl.TEXTURE_2D, null);
211
+ }
212
+ gl.activeTexture(gl.TEXTURE0);
213
+ gl.bindTexture(gl.TEXTURE_2D, this.inputTexture);
214
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
215
+ gl.useProgram(this.blurProgramHandle);
216
+ if (this.blurLocations.texelSizeLocation) {
217
+ gl.uniform2f(this.blurLocations.texelSizeLocation, 1.0 / scaledWidth, 1.0 / scaledHeight);
218
+ }
219
+ const weights = this.getGaussianWeights(radius);
220
+ if (this.blurLocations.weightsLocation) {
221
+ gl.uniform1fv(this.blurLocations.weightsLocation, weights);
222
+ }
223
+ gl.viewport(0, 0, scaledWidth, scaledHeight);
224
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.pingFbo);
225
+ gl.bindTexture(gl.TEXTURE_2D, this.inputTexture);
226
+ if (this.blurLocations.directionLocation) {
227
+ gl.uniform2f(this.blurLocations.directionLocation, 1.0, 0.0);
228
+ }
229
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
230
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.pongFbo);
231
+ gl.bindTexture(gl.TEXTURE_2D, this.pingTexture);
232
+ if (this.blurLocations.directionLocation) {
233
+ gl.uniform2f(this.blurLocations.directionLocation, 0.0, 1.0);
234
+ }
235
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
236
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
237
+ gl.viewport(0, 0, width, height);
238
+ gl.useProgram(this.passthroughProgramHandle);
239
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
240
+ gl.enableVertexAttribArray(this.passthroughLocations.positionLocation);
241
+ gl.vertexAttribPointer(this.passthroughLocations.positionLocation, 2, gl.FLOAT, false, 0, 0);
242
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
243
+ gl.enableVertexAttribArray(this.passthroughLocations.texCoordLocation);
244
+ gl.vertexAttribPointer(this.passthroughLocations.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
245
+ gl.bindTexture(gl.TEXTURE_2D, this.pongTexture);
246
+ if (this.passthroughLocations.imageLocation) {
247
+ gl.uniform1i(this.passthroughLocations.imageLocation, 0);
248
+ }
249
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
250
+ }
251
+ close() {
252
+ if (!this.isRunning)
253
+ return;
254
+ this.isRunning = false;
255
+ const gl = this.gl;
256
+ if (this.pingFbo)
257
+ gl.deleteFramebuffer(this.pingFbo);
258
+ if (this.pongFbo)
259
+ gl.deleteFramebuffer(this.pongFbo);
260
+ if (this.pingTexture)
261
+ gl.deleteTexture(this.pingTexture);
262
+ if (this.pongTexture)
263
+ gl.deleteTexture(this.pongTexture);
264
+ if (this.inputTexture)
265
+ gl.deleteTexture(this.inputTexture);
266
+ if (this.positionBuffer)
267
+ gl.deleteBuffer(this.positionBuffer);
268
+ if (this.texCoordBuffer)
269
+ gl.deleteBuffer(this.texCoordBuffer);
270
+ gl.deleteProgram(this.blurProgramHandle);
271
+ gl.deleteProgram(this.passthroughProgramHandle);
272
+ }
273
+ }
274
+
275
+ export { FullScreenBlurRenderer };
276
+ //# sourceMappingURL=FullScreenBlurRenderer.js.map