@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 +10 -0
- package/dist/index.cjs.js +245 -244
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +245 -245
- package/dist/index.es.js.map +1 -1
- package/index.ts +1 -0
- package/package.json +1 -1
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.
|
|
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;
|