@stream-io/video-filters-web 0.8.2 → 0.8.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [0.8.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-filters-web-0.8.2...@stream-io/video-filters-web-0.8.3) (2026-07-17)
6
+
7
+ ### Dependency Updates
8
+
9
+ - `@stream-io/typescript-config` updated to version `0.1.0`
10
+
11
+ ### Bug Fixes
12
+
13
+ - **video-filters-web:** export BaseVideoProcessor ([#2337](https://github.com/GetStream/stream-video-js/issues/2337)) ([c51b2d6](https://github.com/GetStream/stream-video-js/commit/c51b2d605bc828e3561a909bd929241308dcf4dd))
14
+
5
15
  ## [0.8.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-filters-web-0.8.1...@stream-io/video-filters-web-0.8.2) (2026-07-09)
6
16
 
7
17
  ### Dependency Updates
package/dist/index.cjs.js CHANGED
@@ -1561,7 +1561,7 @@ const createTFLiteSIMDModule = (__Module) => {
1561
1561
  return __Module.ready;
1562
1562
  };
1563
1563
 
1564
- const version = "0.8.2";
1564
+ const version = "0.8.3";
1565
1565
  const packageName = "@stream-io/video-filters-web";
1566
1566
 
1567
1567
  // @ts-expect-error - module is not declared
@@ -1611,6 +1611,249 @@ const BACKGROUND_BLUR_MAP = {
1611
1611
  high: 7,
1612
1612
  };
1613
1613
 
1614
+ /**
1615
+ * Fallback video processor for browsers that do not support MediaStreamTrackGenerator.
1616
+ *
1617
+ * Produces a video MediaStreamTrack sourced from a canvas and exposes
1618
+ * a WritableStream<VideoFrame> on track.writable for writing frames.
1619
+ */
1620
+ class FallbackGenerator {
1621
+ constructor({ kind, signalTarget }) {
1622
+ if (kind !== 'video') {
1623
+ throw new Error('Only video tracks are supported');
1624
+ }
1625
+ const canvas = document.createElement('canvas');
1626
+ const ctx = canvas.getContext('2d', { desynchronized: true });
1627
+ if (!ctx) {
1628
+ throw new Error('Failed to get 2D context from canvas');
1629
+ }
1630
+ const mediaStream = canvas.captureStream();
1631
+ const track = mediaStream.getVideoTracks()[0];
1632
+ const height = signalTarget?.getSettings().height;
1633
+ const width = signalTarget?.getSettings().width;
1634
+ if (height && width) {
1635
+ canvas.height = height;
1636
+ canvas.width = width;
1637
+ }
1638
+ if (!track) {
1639
+ throw new Error('Failed to create canvas track');
1640
+ }
1641
+ if (signalTarget) {
1642
+ signalTarget.addEventListener('ended', () => {
1643
+ track.stop();
1644
+ });
1645
+ }
1646
+ track.writable = new WritableStream({
1647
+ write: (frame) => {
1648
+ if (canvas.width !== frame.displayWidth ||
1649
+ canvas.height !== frame.displayHeight) {
1650
+ canvas.width = frame.displayWidth;
1651
+ canvas.height = frame.displayHeight;
1652
+ }
1653
+ ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
1654
+ frame.close();
1655
+ },
1656
+ abort: () => {
1657
+ track.stop();
1658
+ },
1659
+ close: () => {
1660
+ track.stop();
1661
+ },
1662
+ });
1663
+ return track;
1664
+ }
1665
+ }
1666
+ const TrackGenerator = typeof MediaStreamTrackGenerator !== 'undefined'
1667
+ ? MediaStreamTrackGenerator
1668
+ : FallbackGenerator;
1669
+
1670
+ /**
1671
+ * Fallback implementation for browsers without MediaStreamTrackGenerator.
1672
+ *
1673
+ * Produces a video MediaStreamTrack sourced from a canvas and exposes a
1674
+ * WritableStream<VideoFrame> on track.writable. Written frames are drawn
1675
+ * into the canvas and update the underlying track automatically.
1676
+ */
1677
+ class FallbackProcessor {
1678
+ readable;
1679
+ workerTimer;
1680
+ video;
1681
+ constructor({ track }) {
1682
+ if (!track)
1683
+ throw new Error('MediaStreamTrack is required');
1684
+ if (track.kind !== 'video') {
1685
+ throw new Error('MediaStreamTrack must be video');
1686
+ }
1687
+ let running = true;
1688
+ this.video = document.createElement('video');
1689
+ this.video.muted = true;
1690
+ this.video.playsInline = true;
1691
+ this.video.srcObject = new MediaStream([track]);
1692
+ const canvas = new OffscreenCanvas(1, 1);
1693
+ const ctx = canvas.getContext('2d');
1694
+ if (!ctx)
1695
+ throw new Error('Failed to get 2D context from OffscreenCanvas');
1696
+ let timestamp = 0;
1697
+ const frameRate = track.getSettings().frameRate || 30;
1698
+ let frameDuration = 1000 / frameRate;
1699
+ this.workerTimer = new workerTimer.WorkerTimer({ useWorker: true });
1700
+ this.readable = new ReadableStream({
1701
+ start: async () => {
1702
+ await Promise.all([
1703
+ this.video.play(),
1704
+ new Promise((r) => this.video.addEventListener('loadeddata', r, { once: true })),
1705
+ ]);
1706
+ frameDuration = 1000 / (track.getSettings().frameRate || 30);
1707
+ timestamp = performance.now();
1708
+ },
1709
+ pull: async (controller) => {
1710
+ if (!running) {
1711
+ controller.close();
1712
+ this.close();
1713
+ return;
1714
+ }
1715
+ const delta = performance.now() - timestamp;
1716
+ if (delta <= frameDuration) {
1717
+ await new Promise((r) => this.workerTimer.setTimeout(r, frameDuration - delta));
1718
+ }
1719
+ timestamp = performance.now();
1720
+ if (canvas.width !== this.video.videoWidth ||
1721
+ canvas.height !== this.video.videoHeight) {
1722
+ canvas.width = this.video.videoWidth;
1723
+ canvas.height = this.video.videoHeight;
1724
+ }
1725
+ ctx.drawImage(this.video, 0, 0);
1726
+ try {
1727
+ const frame = new VideoFrame(canvas, {
1728
+ timestamp: Math.round(this.video.currentTime * 1000000),
1729
+ });
1730
+ controller.enqueue(frame);
1731
+ }
1732
+ catch (err) {
1733
+ running = false;
1734
+ controller.error(err);
1735
+ this.close();
1736
+ }
1737
+ },
1738
+ cancel: () => {
1739
+ running = false;
1740
+ this.close();
1741
+ },
1742
+ });
1743
+ }
1744
+ close = () => {
1745
+ this.video.pause();
1746
+ this.video.srcObject = null;
1747
+ this.video.src = '';
1748
+ this.workerTimer.destroy();
1749
+ };
1750
+ }
1751
+ const TrackProcessor = typeof MediaStreamTrackProcessor !== 'undefined'
1752
+ ? MediaStreamTrackProcessor
1753
+ : FallbackProcessor;
1754
+
1755
+ /**
1756
+ * Base class for real-time video filters.
1757
+ *
1758
+ * It sets up the full pipeline that reads frames from the input track,
1759
+ * processes them, and outputs a new track with your effect applied. Subclasses
1760
+ * only need to implement `initialize` (run once before processing starts) and
1761
+ * `transform` (called for every frame).
1762
+ *
1763
+ * Everything else—canvas setup, performance tracking, error handling, and
1764
+ * clean shutdown is handled for you. Calling `start()` returns a processed
1765
+ * `MediaStreamTrack` ready to use.
1766
+ */
1767
+ class BaseVideoProcessor {
1768
+ track;
1769
+ processor;
1770
+ generator;
1771
+ hooks;
1772
+ abortController = new AbortController();
1773
+ canvas;
1774
+ frames = 0;
1775
+ delayTotal = 0;
1776
+ lastStatsTime = 0;
1777
+ /**
1778
+ * Constructs a new instance.
1779
+ */
1780
+ constructor(track, hooks = {}) {
1781
+ this.track = track;
1782
+ this.processor = new TrackProcessor({ track });
1783
+ this.generator = new TrackGenerator({
1784
+ kind: 'video',
1785
+ signalTarget: track,
1786
+ });
1787
+ this.hooks = hooks;
1788
+ }
1789
+ async start() {
1790
+ const { readable } = this.processor;
1791
+ const { writable } = this.generator;
1792
+ const { width = 1280, height = 720 } = this.track.getSettings();
1793
+ this.canvas = new OffscreenCanvas(width, height);
1794
+ await this.initialize();
1795
+ const transformStream = new TransformStream({
1796
+ transform: async (frame, controller) => {
1797
+ try {
1798
+ if (this.abortController.signal.aborted)
1799
+ return frame.close();
1800
+ if (this.canvas.width !== frame.displayWidth ||
1801
+ this.canvas.height !== frame.displayHeight) {
1802
+ this.canvas.width = frame.displayWidth;
1803
+ this.canvas.height = frame.displayHeight;
1804
+ }
1805
+ const processed = await this.transform(frame);
1806
+ controller.enqueue(processed);
1807
+ }
1808
+ catch (e) {
1809
+ this.hooks.onError?.(e);
1810
+ }
1811
+ finally {
1812
+ frame.close();
1813
+ }
1814
+ },
1815
+ flush: () => this.onFlush(),
1816
+ });
1817
+ readable
1818
+ .pipeThrough(transformStream, { signal: this.abortController.signal })
1819
+ .pipeTo(writable, { signal: this.abortController.signal })
1820
+ .catch((e) => {
1821
+ if (e.name !== 'AbortError' && e.name !== 'InvalidStateError') {
1822
+ console.error(`[${this.processorName}] Error processing track:`, e);
1823
+ this.hooks.onError?.(e);
1824
+ }
1825
+ });
1826
+ return this.generator;
1827
+ }
1828
+ stop() {
1829
+ this.abortController.abort();
1830
+ this.generator.stop();
1831
+ this.onStop();
1832
+ }
1833
+ updateStats(delay) {
1834
+ this.frames++;
1835
+ this.delayTotal += delay;
1836
+ const now = performance.now();
1837
+ if (this.lastStatsTime === 0) {
1838
+ this.lastStatsTime = now;
1839
+ return;
1840
+ }
1841
+ if (now - this.lastStatsTime >= 1000) {
1842
+ const avgDelay = Math.round((this.delayTotal / this.frames) * 100) / 100;
1843
+ const fps = Math.round((1000 * this.frames) / (now - this.lastStatsTime));
1844
+ this.hooks.onStats?.({ delay: avgDelay, fps, timestamp: now });
1845
+ this.frames = 0;
1846
+ this.delayTotal = 0;
1847
+ this.lastStatsTime = now;
1848
+ }
1849
+ }
1850
+ onFlush() { }
1851
+ onStop() { }
1852
+ get processorName() {
1853
+ return 'base-processor';
1854
+ }
1855
+ }
1856
+
1614
1857
  class WebGLRenderer {
1615
1858
  canvas;
1616
1859
  gl;
@@ -2332,249 +2575,6 @@ class WebGLRenderer {
2332
2575
  }
2333
2576
  }
2334
2577
 
2335
- /**
2336
- * Fallback video processor for browsers that do not support MediaStreamTrackGenerator.
2337
- *
2338
- * Produces a video MediaStreamTrack sourced from a canvas and exposes
2339
- * a WritableStream<VideoFrame> on track.writable for writing frames.
2340
- */
2341
- class FallbackGenerator {
2342
- constructor({ kind, signalTarget }) {
2343
- if (kind !== 'video') {
2344
- throw new Error('Only video tracks are supported');
2345
- }
2346
- const canvas = document.createElement('canvas');
2347
- const ctx = canvas.getContext('2d', { desynchronized: true });
2348
- if (!ctx) {
2349
- throw new Error('Failed to get 2D context from canvas');
2350
- }
2351
- const mediaStream = canvas.captureStream();
2352
- const track = mediaStream.getVideoTracks()[0];
2353
- const height = signalTarget?.getSettings().height;
2354
- const width = signalTarget?.getSettings().width;
2355
- if (height && width) {
2356
- canvas.height = height;
2357
- canvas.width = width;
2358
- }
2359
- if (!track) {
2360
- throw new Error('Failed to create canvas track');
2361
- }
2362
- if (signalTarget) {
2363
- signalTarget.addEventListener('ended', () => {
2364
- track.stop();
2365
- });
2366
- }
2367
- track.writable = new WritableStream({
2368
- write: (frame) => {
2369
- if (canvas.width !== frame.displayWidth ||
2370
- canvas.height !== frame.displayHeight) {
2371
- canvas.width = frame.displayWidth;
2372
- canvas.height = frame.displayHeight;
2373
- }
2374
- ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
2375
- frame.close();
2376
- },
2377
- abort: () => {
2378
- track.stop();
2379
- },
2380
- close: () => {
2381
- track.stop();
2382
- },
2383
- });
2384
- return track;
2385
- }
2386
- }
2387
- const TrackGenerator = typeof MediaStreamTrackGenerator !== 'undefined'
2388
- ? MediaStreamTrackGenerator
2389
- : FallbackGenerator;
2390
-
2391
- /**
2392
- * Fallback implementation for browsers without MediaStreamTrackGenerator.
2393
- *
2394
- * Produces a video MediaStreamTrack sourced from a canvas and exposes a
2395
- * WritableStream<VideoFrame> on track.writable. Written frames are drawn
2396
- * into the canvas and update the underlying track automatically.
2397
- */
2398
- class FallbackProcessor {
2399
- readable;
2400
- workerTimer;
2401
- video;
2402
- constructor({ track }) {
2403
- if (!track)
2404
- throw new Error('MediaStreamTrack is required');
2405
- if (track.kind !== 'video') {
2406
- throw new Error('MediaStreamTrack must be video');
2407
- }
2408
- let running = true;
2409
- this.video = document.createElement('video');
2410
- this.video.muted = true;
2411
- this.video.playsInline = true;
2412
- this.video.srcObject = new MediaStream([track]);
2413
- const canvas = new OffscreenCanvas(1, 1);
2414
- const ctx = canvas.getContext('2d');
2415
- if (!ctx)
2416
- throw new Error('Failed to get 2D context from OffscreenCanvas');
2417
- let timestamp = 0;
2418
- const frameRate = track.getSettings().frameRate || 30;
2419
- let frameDuration = 1000 / frameRate;
2420
- this.workerTimer = new workerTimer.WorkerTimer({ useWorker: true });
2421
- this.readable = new ReadableStream({
2422
- start: async () => {
2423
- await Promise.all([
2424
- this.video.play(),
2425
- new Promise((r) => this.video.addEventListener('loadeddata', r, { once: true })),
2426
- ]);
2427
- frameDuration = 1000 / (track.getSettings().frameRate || 30);
2428
- timestamp = performance.now();
2429
- },
2430
- pull: async (controller) => {
2431
- if (!running) {
2432
- controller.close();
2433
- this.close();
2434
- return;
2435
- }
2436
- const delta = performance.now() - timestamp;
2437
- if (delta <= frameDuration) {
2438
- await new Promise((r) => this.workerTimer.setTimeout(r, frameDuration - delta));
2439
- }
2440
- timestamp = performance.now();
2441
- if (canvas.width !== this.video.videoWidth ||
2442
- canvas.height !== this.video.videoHeight) {
2443
- canvas.width = this.video.videoWidth;
2444
- canvas.height = this.video.videoHeight;
2445
- }
2446
- ctx.drawImage(this.video, 0, 0);
2447
- try {
2448
- const frame = new VideoFrame(canvas, {
2449
- timestamp: Math.round(this.video.currentTime * 1000000),
2450
- });
2451
- controller.enqueue(frame);
2452
- }
2453
- catch (err) {
2454
- running = false;
2455
- controller.error(err);
2456
- this.close();
2457
- }
2458
- },
2459
- cancel: () => {
2460
- running = false;
2461
- this.close();
2462
- },
2463
- });
2464
- }
2465
- close = () => {
2466
- this.video.pause();
2467
- this.video.srcObject = null;
2468
- this.video.src = '';
2469
- this.workerTimer.destroy();
2470
- };
2471
- }
2472
- const TrackProcessor = typeof MediaStreamTrackProcessor !== 'undefined'
2473
- ? MediaStreamTrackProcessor
2474
- : FallbackProcessor;
2475
-
2476
- /**
2477
- * Base class for real-time video filters.
2478
- *
2479
- * It sets up the full pipeline that reads frames from the input track,
2480
- * processes them, and outputs a new track with your effect applied. Subclasses
2481
- * only need to implement `initialize` (run once before processing starts) and
2482
- * `transform` (called for every frame).
2483
- *
2484
- * Everything else—canvas setup, performance tracking, error handling, and
2485
- * clean shutdown is handled for you. Calling `start()` returns a processed
2486
- * `MediaStreamTrack` ready to use.
2487
- */
2488
- class BaseVideoProcessor {
2489
- track;
2490
- processor;
2491
- generator;
2492
- hooks;
2493
- abortController = new AbortController();
2494
- canvas;
2495
- frames = 0;
2496
- delayTotal = 0;
2497
- lastStatsTime = 0;
2498
- /**
2499
- * Constructs a new instance.
2500
- */
2501
- constructor(track, hooks = {}) {
2502
- this.track = track;
2503
- this.processor = new TrackProcessor({ track });
2504
- this.generator = new TrackGenerator({
2505
- kind: 'video',
2506
- signalTarget: track,
2507
- });
2508
- this.hooks = hooks;
2509
- }
2510
- async start() {
2511
- const { readable } = this.processor;
2512
- const { writable } = this.generator;
2513
- const { width = 1280, height = 720 } = this.track.getSettings();
2514
- this.canvas = new OffscreenCanvas(width, height);
2515
- await this.initialize();
2516
- const transformStream = new TransformStream({
2517
- transform: async (frame, controller) => {
2518
- try {
2519
- if (this.abortController.signal.aborted)
2520
- return frame.close();
2521
- if (this.canvas.width !== frame.displayWidth ||
2522
- this.canvas.height !== frame.displayHeight) {
2523
- this.canvas.width = frame.displayWidth;
2524
- this.canvas.height = frame.displayHeight;
2525
- }
2526
- const processed = await this.transform(frame);
2527
- controller.enqueue(processed);
2528
- }
2529
- catch (e) {
2530
- this.hooks.onError?.(e);
2531
- }
2532
- finally {
2533
- frame.close();
2534
- }
2535
- },
2536
- flush: () => this.onFlush(),
2537
- });
2538
- readable
2539
- .pipeThrough(transformStream, { signal: this.abortController.signal })
2540
- .pipeTo(writable, { signal: this.abortController.signal })
2541
- .catch((e) => {
2542
- if (e.name !== 'AbortError' && e.name !== 'InvalidStateError') {
2543
- console.error(`[${this.processorName}] Error processing track:`, e);
2544
- this.hooks.onError?.(e);
2545
- }
2546
- });
2547
- return this.generator;
2548
- }
2549
- stop() {
2550
- this.abortController.abort();
2551
- this.generator.stop();
2552
- this.onStop();
2553
- }
2554
- updateStats(delay) {
2555
- this.frames++;
2556
- this.delayTotal += delay;
2557
- const now = performance.now();
2558
- if (this.lastStatsTime === 0) {
2559
- this.lastStatsTime = now;
2560
- return;
2561
- }
2562
- if (now - this.lastStatsTime >= 1000) {
2563
- const avgDelay = Math.round((this.delayTotal / this.frames) * 100) / 100;
2564
- const fps = Math.round((1000 * this.frames) / (now - this.lastStatsTime));
2565
- this.hooks.onStats?.({ delay: avgDelay, fps, timestamp: now });
2566
- this.frames = 0;
2567
- this.delayTotal = 0;
2568
- this.lastStatsTime = now;
2569
- }
2570
- }
2571
- onFlush() { }
2572
- onStop() { }
2573
- get processorName() {
2574
- return 'base-processor';
2575
- }
2576
- }
2577
-
2578
2578
  /**
2579
2579
  * Wraps a video MediaStreamTrack in a real-time processing pipeline.
2580
2580
  * Incoming frames are processed through a transformer and re-emitted
@@ -3021,6 +3021,7 @@ class FullScreenBlur extends BaseVideoProcessor {
3021
3021
  }
3022
3022
 
3023
3023
  exports.BACKGROUND_BLUR_MAP = BACKGROUND_BLUR_MAP;
3024
+ exports.BaseVideoProcessor = BaseVideoProcessor;
3024
3025
  exports.FullScreenBlur = FullScreenBlur;
3025
3026
  exports.VirtualBackground = VirtualBackground;
3026
3027
  exports.createRenderer = createRenderer;