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

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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +27 -24
  3. package/dist/index.cjs.js +1099 -6
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.d.ts +6 -3
  6. package/dist/index.es.js +1096 -7
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/FallbackGenerator.d.ts +17 -0
  9. package/dist/src/FallbackProcessor.d.ts +38 -0
  10. package/dist/src/VirtualBackground.d.ts +42 -0
  11. package/dist/src/WebGLRenderer.d.ts +80 -0
  12. package/dist/src/compatibility.d.ts +5 -0
  13. package/dist/src/{createRenderer.d.ts → legacy/createRenderer.d.ts} +1 -2
  14. package/dist/src/{webgl2 → legacy/webgl2}/backgroundBlurStage.d.ts +1 -1
  15. package/dist/src/{webgl2 → legacy/webgl2}/webgl2Pipeline.d.ts +1 -1
  16. package/dist/src/mediapipe.d.ts +4 -0
  17. package/dist/src/types.d.ts +45 -0
  18. package/index.ts +6 -3
  19. package/mediapipe/models/selfie_segmenter.tflite +0 -0
  20. package/mediapipe/wasm/vision_wasm_internal.js +20 -0
  21. package/mediapipe/wasm/vision_wasm_internal.wasm +0 -0
  22. package/mediapipe/wasm/vision_wasm_nosimd_internal.js +20 -0
  23. package/mediapipe/wasm/vision_wasm_nosimd_internal.wasm +0 -0
  24. package/package.json +4 -1
  25. package/src/FallbackGenerator.ts +90 -0
  26. package/src/FallbackProcessor.ts +132 -0
  27. package/src/VirtualBackground.ts +276 -0
  28. package/src/WebGLRenderer.ts +1187 -0
  29. package/src/compatibility.ts +22 -0
  30. package/src/{createRenderer.ts → legacy/createRenderer.ts} +1 -2
  31. package/src/{tflite.ts → legacy/tflite.ts} +1 -1
  32. package/src/{webgl2 → legacy/webgl2}/backgroundBlurStage.ts +1 -1
  33. package/src/{webgl2 → legacy/webgl2}/webgl2Pipeline.ts +1 -1
  34. package/src/mediapipe.ts +25 -0
  35. package/src/types.ts +62 -0
  36. /package/dist/src/{helpers → legacy/helpers}/webglHelper.d.ts +0 -0
  37. /package/dist/src/{segmentation.d.ts → legacy/segmentation.d.ts} +0 -0
  38. /package/dist/src/{tflite.d.ts → legacy/tflite.d.ts} +0 -0
  39. /package/dist/src/{webgl2 → legacy/webgl2}/backgroundImageStage.d.ts +0 -0
  40. /package/dist/src/{webgl2 → legacy/webgl2}/jointBilateralFilterStage.d.ts +0 -0
  41. /package/dist/src/{webgl2 → legacy/webgl2}/resizingStage.d.ts +0 -0
  42. /package/dist/src/{webgl2 → legacy/webgl2}/softmaxStage.d.ts +0 -0
  43. /package/src/{helpers → legacy/helpers}/webglHelper.ts +0 -0
  44. /package/src/{segmentation.ts → legacy/segmentation.ts} +0 -0
  45. /package/src/{tflite-simd.js → legacy/tflite-simd.js} +0 -0
  46. /package/src/{webgl2 → legacy/webgl2}/backgroundImageStage.ts +0 -0
  47. /package/src/{webgl2 → legacy/webgl2}/jointBilateralFilterStage.ts +0 -0
  48. /package/src/{webgl2 → legacy/webgl2}/resizingStage.ts +0 -0
  49. /package/src/{webgl2 → legacy/webgl2}/softmaxStage.ts +0 -0
package/dist/index.cjs.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var wasmFeatureDetect = require('wasm-feature-detect');
4
4
  var workerTimer = require('@stream-io/worker-timer');
5
+ var tasksVision = require('@mediapipe/tasks-vision');
5
6
 
6
7
  /**
7
8
  * Checks if the current platform is a mobile device.
@@ -29,6 +30,23 @@ const isPlatformSupported = async ({ forceMobileSupport = false, forceSafariSupp
29
30
  !!window.WebGL2RenderingContext && // WebGL2 is required for the video filters
30
31
  !!document.createElement('canvas').getContext('webgl2') &&
31
32
  (await wasmFeatureDetect.simd()); // SIMD is required for the wasm module
33
+ /**
34
+ * Runs a check to see if the current platform supports
35
+ * the necessary APIs required for the MediaPipe-based video filters.
36
+ */
37
+ const isMediaPipePlatformSupported = async ({ forceMobileSupport = false, forceSafariSupport = false, } = {}) => typeof document !== 'undefined' &&
38
+ typeof window !== 'undefined' &&
39
+ typeof navigator !== 'undefined' &&
40
+ // we don't support mobile devices yet due to performance issues
41
+ (forceMobileSupport || !isMobile()) &&
42
+ // Safari has issues with timer throttling, causing low FPS when the tab goes to the background
43
+ (forceSafariSupport || !isSafari()) &&
44
+ typeof WebAssembly !== 'undefined' &&
45
+ typeof OffscreenCanvas !== 'undefined' && // OffscreenCanvas is required for efficient rendering
46
+ !!window.WebGL2RenderingContext && // WebGL2 is required for the video filters
47
+ !!new OffscreenCanvas(1, 1).getContext('webgl2') &&
48
+ typeof VideoFrame !== 'undefined' && // VideoFrame API is required for frame processing
49
+ typeof createImageBitmap !== 'undefined'; // createImageBitmap is required for background image processing
32
50
 
33
51
  /**
34
52
  * Use it along with boyswan.glsl-literal VSCode extension
@@ -1543,7 +1561,7 @@ const createTFLiteSIMDModule = (__Module) => {
1543
1561
  return __Module.ready;
1544
1562
  };
1545
1563
 
1546
- const version = "0.4.0";
1564
+ const version = "0.5.1";
1547
1565
  const packageName = "@stream-io/video-filters-web";
1548
1566
 
1549
1567
  // @ts-expect-error - module is not declared
@@ -1560,19 +1578,1094 @@ const loadTFLite = async (options = {}) => {
1560
1578
  tfLite._loadModel(model.byteLength);
1561
1579
  return tfLite;
1562
1580
  };
1563
- let lastModelFilePath = '';
1564
- let modelFileCache;
1581
+ let lastModelFilePath$1 = '';
1582
+ let modelFileCache$1;
1565
1583
  const fetchModel = async (modelFilePath) => {
1566
- const model = modelFilePath === lastModelFilePath && modelFileCache
1567
- ? modelFileCache
1584
+ const model = modelFilePath === lastModelFilePath$1 && modelFileCache$1
1585
+ ? modelFileCache$1
1568
1586
  : await fetch(modelFilePath).then((r) => r.arrayBuffer());
1569
1587
  // Cache the model file for future use.
1588
+ modelFileCache$1 = model;
1589
+ lastModelFilePath$1 = modelFilePath;
1590
+ return model;
1591
+ };
1592
+
1593
+ let lastModelFilePath = '';
1594
+ let modelFileCache;
1595
+ const loadMediaPipe = async (options = {}) => {
1596
+ const { basePath = `https://unpkg.com/${packageName}@${version}/mediapipe`, modelPath = `${basePath}/models/selfie_segmenter.tflite`, } = options;
1597
+ const model = modelPath === lastModelFilePath && modelFileCache
1598
+ ? modelFileCache
1599
+ : await fetch(modelPath).then((r) => r.arrayBuffer());
1570
1600
  modelFileCache = model;
1571
- lastModelFilePath = modelFilePath;
1601
+ lastModelFilePath = modelPath;
1572
1602
  return model;
1573
1603
  };
1574
1604
 
1605
+ const BACKGROUND_BLUR_MAP = {
1606
+ low: {
1607
+ bgBlur: 15,
1608
+ bgBlurRadius: 5,
1609
+ },
1610
+ medium: {
1611
+ bgBlur: 20,
1612
+ bgBlurRadius: 7,
1613
+ },
1614
+ high: {
1615
+ bgBlur: 25,
1616
+ bgBlurRadius: 10,
1617
+ },
1618
+ };
1619
+
1620
+ class WebGLRenderer {
1621
+ constructor(canvas) {
1622
+ this.running = false;
1623
+ this.currentStateIndex = 0;
1624
+ this.backgroundRenderInfo = null;
1625
+ this.activeBackgroundSourceIdentifier = null;
1626
+ this.canvas = canvas;
1627
+ const gl = this.canvas.getContext('webgl2', {
1628
+ alpha: false,
1629
+ antialias: false,
1630
+ desynchronized: true,
1631
+ });
1632
+ if (!gl)
1633
+ throw new Error('WebGL2 not supported');
1634
+ this.gl = gl;
1635
+ const stateUpdateVertexShaderSource = `attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord; }`;
1636
+ const stateUpdateFragmentShaderSource = `
1637
+ precision mediump float;
1638
+ varying vec2 v_texCoord;
1639
+ uniform sampler2D u_categoryTexture;
1640
+ uniform sampler2D u_confidenceTexture;
1641
+ uniform sampler2D u_prevStateTexture;
1642
+ uniform float u_smoothingFactor;
1643
+ uniform float u_smoothstepMin;
1644
+ uniform float u_smoothstepMax;
1645
+ uniform int u_selfieModel;
1646
+
1647
+ void main() {
1648
+ vec2 prevCoord = vec2(v_texCoord.x, 1.0 - v_texCoord.y);
1649
+ float categoryValue = texture2D(u_categoryTexture, v_texCoord).r;
1650
+ float confidenceValue = texture2D(u_confidenceTexture, v_texCoord).r;
1651
+
1652
+ if (u_selfieModel == 1) {
1653
+ categoryValue = 1.0 - categoryValue;
1654
+ confidenceValue = 1.0 - confidenceValue;
1655
+ }
1656
+
1657
+ if (categoryValue > 0.0) {
1658
+ categoryValue = 1.0;
1659
+ confidenceValue = 1.0 - confidenceValue;
1660
+ }
1661
+
1662
+ float nonLinearConfidence = smoothstep(u_smoothstepMin, u_smoothstepMax, confidenceValue);
1663
+ float prevCategoryValue = texture2D(u_prevStateTexture, prevCoord).r;
1664
+ float alpha = u_smoothingFactor * nonLinearConfidence;
1665
+ float newCategoryValue = alpha * categoryValue + (1.0 - alpha) * prevCategoryValue;
1666
+
1667
+ gl_FragColor = vec4(newCategoryValue, 0.0, 0.0, 0.0);
1668
+ }
1669
+ `;
1670
+ this.stateUpdateProgram = this.createAndLinkProgram(stateUpdateVertexShaderSource, stateUpdateFragmentShaderSource);
1671
+ this.stateUpdateLocations = {
1672
+ position: gl.getAttribLocation(this.stateUpdateProgram, 'a_position'),
1673
+ texCoord: gl.getAttribLocation(this.stateUpdateProgram, 'a_texCoord'),
1674
+ categoryTexture: gl.getUniformLocation(this.stateUpdateProgram, 'u_categoryTexture'),
1675
+ confidenceTexture: gl.getUniformLocation(this.stateUpdateProgram, 'u_confidenceTexture'),
1676
+ prevStateTexture: gl.getUniformLocation(this.stateUpdateProgram, 'u_prevStateTexture'),
1677
+ smoothingFactor: gl.getUniformLocation(this.stateUpdateProgram, 'u_smoothingFactor'),
1678
+ smoothstepMin: gl.getUniformLocation(this.stateUpdateProgram, 'u_smoothstepMin'),
1679
+ smoothstepMax: gl.getUniformLocation(this.stateUpdateProgram, 'u_smoothstepMax'),
1680
+ selfieModel: gl.getUniformLocation(this.stateUpdateProgram, 'u_selfieModel'),
1681
+ };
1682
+ const maskRefineVertexShaderSource = stateUpdateVertexShaderSource;
1683
+ const maskRefineFragmentShaderSource = `
1684
+ precision mediump float;
1685
+ varying vec2 v_texCoord;
1686
+
1687
+ uniform sampler2D u_maskTexture;
1688
+ uniform sampler2D u_frameTexture;
1689
+ uniform vec2 u_texelSize;
1690
+ uniform float u_sigmaSpatial;
1691
+ uniform float u_sigmaRange;
1692
+
1693
+ void main() {
1694
+ vec2 flippedCoord = v_texCoord;
1695
+ vec3 centerPixelColor = texture2D(u_frameTexture, v_texCoord).rgb;
1696
+ float totalWeight = 0.0;
1697
+ float weightedMaskSum = 0.0;
1698
+
1699
+ for (int offsetX = -2; offsetX <= 2; offsetX++) {
1700
+ for (int offsetY = -2; offsetY <= 2; offsetY++) {
1701
+ vec2 shift = vec2(float(offsetX), float(offsetY)) * u_texelSize;
1702
+ vec2 frameCoord = v_texCoord + shift;
1703
+ vec2 maskCoord = flippedCoord + shift;
1704
+
1705
+ vec3 neighborPixelColor = texture2D(u_frameTexture, frameCoord).rgb;
1706
+ float neighborMaskValue = texture2D(u_maskTexture, maskCoord).r;
1707
+
1708
+ float spatialWeight = exp(-dot(shift, shift) / (2.0 * u_sigmaSpatial * u_sigmaSpatial));
1709
+ vec3 colorDifference = neighborPixelColor - centerPixelColor;
1710
+ float rangeWeight = exp(-(dot(colorDifference, colorDifference)) / (2.0 * u_sigmaRange * u_sigmaRange));
1711
+
1712
+ float combinedWeight = spatialWeight * rangeWeight;
1713
+ weightedMaskSum += neighborMaskValue * combinedWeight;
1714
+ totalWeight += combinedWeight;
1715
+ }
1716
+ }
1717
+
1718
+ float refinedMaskValue = weightedMaskSum / max(totalWeight, 1e-6);
1719
+ gl_FragColor = vec4(refinedMaskValue, refinedMaskValue, refinedMaskValue, 1.0);
1720
+ }
1721
+ `;
1722
+ this.maskRefineProgram = this.createAndLinkProgram(maskRefineVertexShaderSource, maskRefineFragmentShaderSource);
1723
+ this.maskRefineLocations = {
1724
+ position: gl.getAttribLocation(this.maskRefineProgram, 'a_position'),
1725
+ texCoord: gl.getAttribLocation(this.maskRefineProgram, 'a_texCoord'),
1726
+ maskTexture: gl.getUniformLocation(this.maskRefineProgram, 'u_maskTexture'),
1727
+ frameTexture: gl.getUniformLocation(this.maskRefineProgram, 'u_frameTexture'),
1728
+ texelSize: gl.getUniformLocation(this.maskRefineProgram, 'u_texelSize'),
1729
+ sigmaSpatial: gl.getUniformLocation(this.maskRefineProgram, 'u_sigmaSpatial'),
1730
+ sigmaRange: gl.getUniformLocation(this.maskRefineProgram, 'u_sigmaRange'),
1731
+ };
1732
+ const blurVertexShaderSource = stateUpdateVertexShaderSource;
1733
+ const blurFragmentShaderSource = `
1734
+ precision highp float;
1735
+ varying vec2 v_texCoord;
1736
+
1737
+ uniform sampler2D u_image;
1738
+ uniform sampler2D u_personMask;
1739
+ uniform vec2 u_texelSize;
1740
+ uniform float u_sigma;
1741
+ uniform float u_radiusScale;
1742
+ uniform vec2 u_direction;
1743
+
1744
+ const int KERNEL_RADIUS = 10;
1745
+
1746
+ float gauss(float x, float s) {
1747
+ return exp(-(x * x) / (2.0 * s * s));
1748
+ }
1749
+
1750
+ void main() {
1751
+ vec2 maskCoord = u_direction.y > 0.5 ? vec2(v_texCoord.x, 1.0 - v_texCoord.y) : v_texCoord;
1752
+ float mCenter = texture2D(u_personMask, maskCoord).r;
1753
+ float wCenter = gauss(0.0, u_sigma);
1754
+ vec4 accum = texture2D(u_image, v_texCoord) * wCenter * (1.0 - mCenter);
1755
+ float weightSum = wCenter * (1.0 - mCenter);
1756
+
1757
+ for (int i = 1; i <= KERNEL_RADIUS; i++) {
1758
+ float f = float(i);
1759
+ float offset = f * u_radiusScale;
1760
+ float w = gauss(offset, u_sigma);
1761
+ vec2 texOffset = u_direction * offset * u_texelSize;
1762
+
1763
+ vec2 uvPlus = v_texCoord + texOffset;
1764
+ vec2 maskCoordPlus = u_direction.y > 0.5 ? vec2(uvPlus.x, 1.0 - uvPlus.y) : uvPlus;
1765
+ float mPlus = texture2D(u_personMask, maskCoordPlus).r;
1766
+ accum += texture2D(u_image, uvPlus) * w * (1.0 - mPlus);
1767
+ weightSum += w * (1.0 - mPlus);
1768
+
1769
+ vec2 uvMinus = v_texCoord - texOffset;
1770
+ vec2 maskCoordMinus = u_direction.y > 0.5 ? vec2(uvMinus.x, 1.0 - uvMinus.y) : uvMinus;
1771
+ float mMinus = texture2D(u_personMask, maskCoordMinus).r;
1772
+ accum += texture2D(u_image, uvMinus) * w * (1.0 - mMinus);
1773
+ weightSum += w * (1.0 - mMinus);
1774
+ }
1775
+
1776
+ vec4 blurred = accum / max(weightSum, 1e-6);
1777
+ gl_FragColor = blurred;
1778
+ }
1779
+ `;
1780
+ this.blurProgram = this.createAndLinkProgram(blurVertexShaderSource, blurFragmentShaderSource);
1781
+ this.blurLocations = {
1782
+ position: gl.getAttribLocation(this.blurProgram, 'a_position'),
1783
+ texCoord: gl.getAttribLocation(this.blurProgram, 'a_texCoord'),
1784
+ image: gl.getUniformLocation(this.blurProgram, 'u_image'),
1785
+ personMask: gl.getUniformLocation(this.blurProgram, 'u_personMask'),
1786
+ texelSize: gl.getUniformLocation(this.blurProgram, 'u_texelSize'),
1787
+ sigma: gl.getUniformLocation(this.blurProgram, 'u_sigma'),
1788
+ radiusScale: gl.getUniformLocation(this.blurProgram, 'u_radiusScale'),
1789
+ direction: gl.getUniformLocation(this.blurProgram, 'u_direction'),
1790
+ };
1791
+ const blendVertexShaderSource = stateUpdateVertexShaderSource;
1792
+ const blendFragmentShaderSource = `
1793
+ precision mediump float;
1794
+ varying vec2 v_texCoord;
1795
+
1796
+ uniform sampler2D u_frameTexture;
1797
+ uniform sampler2D u_currentStateTexture;
1798
+ uniform sampler2D u_backgroundTexture;
1799
+ uniform vec2 u_bgImageDimensions;
1800
+ uniform vec2 u_canvasDimensions;
1801
+ uniform float u_borderSmooth;
1802
+ uniform float u_bgBlur;
1803
+ uniform float u_bgBlurRadius;
1804
+ uniform int u_enabled;
1805
+
1806
+ vec4 getMixedFragColor(vec2 bgTexCoord, vec2 categoryCoord, vec2 offset) {
1807
+ vec4 backgroundColor = texture2D(u_backgroundTexture, bgTexCoord + offset);
1808
+ vec4 frameColor = texture2D(u_frameTexture, v_texCoord + offset);
1809
+ float categoryValue = texture2D(u_currentStateTexture, categoryCoord + offset).r;
1810
+ return mix(backgroundColor, frameColor, categoryValue);
1811
+ }
1812
+
1813
+ void main() {
1814
+ if (u_enabled == 0) {
1815
+ gl_FragColor = texture2D(u_frameTexture, v_texCoord);
1816
+ return;
1817
+ }
1818
+
1819
+ vec2 categoryCoord = v_texCoord;
1820
+ float categoryValue = texture2D(u_currentStateTexture, categoryCoord).r;
1821
+
1822
+ float canvasAspect = u_canvasDimensions.x / u_canvasDimensions.y;
1823
+ float bgAspect = u_bgImageDimensions.x / u_bgImageDimensions.y;
1824
+
1825
+ vec2 bgTexCoord = v_texCoord;
1826
+ float scaleX = 1.0;
1827
+ float scaleY = 1.0;
1828
+ float offsetX = 0.0;
1829
+ float offsetY = 0.0;
1830
+
1831
+ if (canvasAspect < bgAspect) {
1832
+ scaleY = 1.0;
1833
+ scaleX = bgAspect / canvasAspect;
1834
+ offsetX = (1.0 - scaleX) / 2.0;
1835
+ } else {
1836
+ scaleX = 1.0;
1837
+ scaleY = canvasAspect / bgAspect;
1838
+ offsetY = (1.0 - scaleY) / 2.0;
1839
+ }
1840
+
1841
+ bgTexCoord = vec2((v_texCoord.x - offsetX) / scaleX, (v_texCoord.y - offsetY) / scaleY);
1842
+ gl_FragColor = getMixedFragColor(bgTexCoord, categoryCoord, vec2(0.0, 0.0));
1843
+ }`;
1844
+ this.blendProgram = this.createAndLinkProgram(blendVertexShaderSource, blendFragmentShaderSource);
1845
+ this.blendLocations = {
1846
+ position: gl.getAttribLocation(this.blendProgram, 'a_position'),
1847
+ texCoord: gl.getAttribLocation(this.blendProgram, 'a_texCoord'),
1848
+ frameTexture: gl.getUniformLocation(this.blendProgram, 'u_frameTexture'),
1849
+ currentStateTexture: gl.getUniformLocation(this.blendProgram, 'u_currentStateTexture'),
1850
+ backgroundTexture: gl.getUniformLocation(this.blendProgram, 'u_backgroundTexture'),
1851
+ bgImageDimensions: gl.getUniformLocation(this.blendProgram, 'u_bgImageDimensions'),
1852
+ canvasDimensions: gl.getUniformLocation(this.blendProgram, 'u_canvasDimensions'),
1853
+ borderSmooth: gl.getUniformLocation(this.blendProgram, 'u_borderSmooth'),
1854
+ bgBlur: gl.getUniformLocation(this.blendProgram, 'u_bgBlur'),
1855
+ bgBlurRadius: gl.getUniformLocation(this.blendProgram, 'u_bgBlurRadius'),
1856
+ enabled: gl.getUniformLocation(this.blendProgram, 'u_enabled'),
1857
+ };
1858
+ this.positionBuffer = gl.createBuffer();
1859
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
1860
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
1861
+ this.texCoordBuffer = gl.createBuffer();
1862
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
1863
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0]), gl.STATIC_DRAW);
1864
+ this.storedStateTextures = Array.from({ length: 2 }, () => {
1865
+ const tex = gl.createTexture();
1866
+ gl.bindTexture(gl.TEXTURE_2D, tex);
1867
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
1868
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1869
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1870
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1871
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1872
+ return tex;
1873
+ });
1874
+ gl.bindTexture(gl.TEXTURE_2D, null);
1875
+ this.fbo = gl.createFramebuffer();
1876
+ this.refineFbo = gl.createFramebuffer();
1877
+ const refinedTex = gl.createTexture();
1878
+ this.frameTexture = gl.createTexture();
1879
+ if (!refinedTex)
1880
+ throw new Error('Failed to create refined mask texture');
1881
+ gl.bindTexture(gl.TEXTURE_2D, refinedTex);
1882
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
1883
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1884
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1885
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1886
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1887
+ gl.bindTexture(gl.TEXTURE_2D, null);
1888
+ this.refinedMaskTexture = refinedTex;
1889
+ const mkColorTex = () => {
1890
+ const t = gl.createTexture();
1891
+ if (!t)
1892
+ throw new Error('Failed to create blur texture');
1893
+ gl.bindTexture(gl.TEXTURE_2D, t);
1894
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
1895
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1896
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1897
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1898
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1899
+ gl.bindTexture(gl.TEXTURE_2D, null);
1900
+ return t;
1901
+ };
1902
+ this.blurTexture1 = mkColorTex();
1903
+ this.blurTexture2 = mkColorTex();
1904
+ const mkFbo = (tex) => {
1905
+ const fb = gl.createFramebuffer();
1906
+ if (!fb || !tex)
1907
+ throw new Error('Failed to create blur FBO');
1908
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
1909
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
1910
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1911
+ return fb;
1912
+ };
1913
+ this.blurFbo1 = mkFbo(this.blurTexture1);
1914
+ this.blurFbo2 = mkFbo(this.blurTexture2);
1915
+ this.running = true;
1916
+ }
1917
+ createAndLinkProgram(vsSource, fsSource) {
1918
+ const vs = this.createShader(this.gl.VERTEX_SHADER, vsSource);
1919
+ const fs = this.createShader(this.gl.FRAGMENT_SHADER, fsSource);
1920
+ const prog = this.gl.createProgram();
1921
+ if (!prog)
1922
+ throw new Error('Failed to create program');
1923
+ this.gl.attachShader(prog, vs);
1924
+ this.gl.attachShader(prog, fs);
1925
+ this.gl.linkProgram(prog);
1926
+ if (!this.gl.getProgramParameter(prog, this.gl.LINK_STATUS)) {
1927
+ console.error('Program link error:', this.gl.getProgramInfoLog(prog));
1928
+ this.gl.deleteProgram(prog);
1929
+ throw new Error('Link fail');
1930
+ }
1931
+ this.gl.detachShader(prog, vs);
1932
+ this.gl.detachShader(prog, fs);
1933
+ this.gl.deleteShader(vs);
1934
+ this.gl.deleteShader(fs);
1935
+ return prog;
1936
+ }
1937
+ createShader(type, source) {
1938
+ const shader = this.gl.createShader(type);
1939
+ if (!shader)
1940
+ throw new Error(`Failed to create shader type: ${type}`);
1941
+ this.gl.shaderSource(shader, source);
1942
+ this.gl.compileShader(shader);
1943
+ if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
1944
+ console.error('Shader compile error:', this.gl.getShaderInfoLog(shader));
1945
+ this.gl.deleteShader(shader);
1946
+ throw new Error('Failed to compile shader');
1947
+ }
1948
+ return shader;
1949
+ }
1950
+ createColorTexture(r, g, b, a) {
1951
+ const texture = this.gl.createTexture();
1952
+ if (!texture)
1953
+ throw new Error('Failed to create texture for color');
1954
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
1955
+ const pixel = new Uint8Array([r, g, b, a]);
1956
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixel);
1957
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
1958
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
1959
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
1960
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
1961
+ this.gl.bindTexture(this.gl.TEXTURE_2D, null);
1962
+ return { texture, color: [r, g, b, a] };
1963
+ }
1964
+ updateBackgroundIfNeeded(newSource) {
1965
+ const gl = this.gl;
1966
+ let newIdentifier;
1967
+ if (!newSource) {
1968
+ const [r, g, b, a] = WebGLRenderer.DEFAULT_BG_COLOR;
1969
+ newIdentifier = `color(${r},${g},${b},${a})`;
1970
+ }
1971
+ else {
1972
+ newIdentifier = newSource.url;
1973
+ }
1974
+ if (newIdentifier === this.activeBackgroundSourceIdentifier &&
1975
+ this.backgroundRenderInfo) {
1976
+ return;
1977
+ }
1978
+ if (this.backgroundRenderInfo) {
1979
+ gl.deleteTexture(this.backgroundRenderInfo.texture);
1980
+ this.backgroundRenderInfo = null;
1981
+ }
1982
+ this.activeBackgroundSourceIdentifier = newIdentifier;
1983
+ if (!newSource) {
1984
+ const [r, g, b, a] = WebGLRenderer.DEFAULT_BG_COLOR;
1985
+ const colorTexData = this.createColorTexture(r, g, b, a);
1986
+ this.backgroundRenderInfo = {
1987
+ type: 'color',
1988
+ texture: colorTexData.texture,
1989
+ color: colorTexData.color,
1990
+ };
1991
+ this.activeBackgroundSourceIdentifier = `color(${r},${g},${b},${a})`;
1992
+ }
1993
+ else {
1994
+ if (newSource.type === 'image') {
1995
+ const { media, url } = newSource;
1996
+ const texture = this.gl.createTexture();
1997
+ if (!texture) {
1998
+ throw new Error('Failed to create texture object for image.');
1999
+ }
2000
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
2001
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, media);
2002
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
2003
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
2004
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
2005
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
2006
+ this.gl.bindTexture(this.gl.TEXTURE_2D, null);
2007
+ this.backgroundRenderInfo = {
2008
+ type: 'image',
2009
+ texture,
2010
+ width: media.width,
2011
+ height: media.height,
2012
+ url,
2013
+ };
2014
+ }
2015
+ else if (newSource.type === 'video') {
2016
+ const { media, url } = newSource;
2017
+ const canvas = new OffscreenCanvas(1, 1);
2018
+ const ctx = canvas.getContext('2d');
2019
+ const writer = new WritableStream({
2020
+ write(videoFrame) {
2021
+ canvas.width = videoFrame.codedWidth;
2022
+ canvas.height = videoFrame.codedHeight;
2023
+ ctx?.drawImage(videoFrame, 0, 0);
2024
+ videoFrame.close();
2025
+ },
2026
+ close() {
2027
+ console.log('[virtual-background] video background close');
2028
+ },
2029
+ });
2030
+ media.pipeTo(writer).catch((err) => {
2031
+ console.error('media.pipeTo(writer) error', err);
2032
+ });
2033
+ const texture = this.gl.createTexture();
2034
+ if (!texture)
2035
+ throw new Error('Failed to create texture for video');
2036
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
2037
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
2038
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
2039
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
2040
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
2041
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
2042
+ this.gl.bindTexture(this.gl.TEXTURE_2D, null);
2043
+ this.backgroundRenderInfo = {
2044
+ type: 'video',
2045
+ texture,
2046
+ url,
2047
+ media,
2048
+ canvas,
2049
+ };
2050
+ }
2051
+ }
2052
+ if (!this.backgroundRenderInfo) {
2053
+ console.error('Critical: backgroundRenderInfo is null after processing new source. Setting default color.');
2054
+ const [r, g, b, a] = WebGLRenderer.DEFAULT_BG_COLOR;
2055
+ const colorTexData = this.createColorTexture(r, g, b, a);
2056
+ this.backgroundRenderInfo = {
2057
+ type: 'color',
2058
+ texture: colorTexData.texture,
2059
+ color: colorTexData.color,
2060
+ };
2061
+ this.activeBackgroundSourceIdentifier = `color(${r},${g},${b},${a})`;
2062
+ }
2063
+ }
2064
+ render(videoFrame, options, categoryTexture, confidenceTexture) {
2065
+ if (!this.running)
2066
+ return;
2067
+ const { gl, fbo, frameTexture, storedStateTextures, stateUpdateProgram, stateUpdateLocations, refineFbo, refinedMaskTexture, maskRefineProgram, maskRefineLocations, blendProgram, blendLocations, blurFbo1, blurFbo2, blurTexture1, blurTexture2, } = this;
2068
+ const { displayWidth: width, displayHeight: height } = videoFrame;
2069
+ if (this.canvas.width !== width || this.canvas.height !== height) {
2070
+ this.canvas.width = width;
2071
+ this.canvas.height = height;
2072
+ }
2073
+ if (!categoryTexture || !confidenceTexture) {
2074
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
2075
+ gl.useProgram(blendProgram);
2076
+ const frame = gl.createTexture();
2077
+ gl.activeTexture(gl.TEXTURE0);
2078
+ gl.bindTexture(gl.TEXTURE_2D, frame);
2079
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoFrame);
2080
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2081
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2082
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
2083
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
2084
+ gl.uniform1i(blendLocations.frameTexture, 0);
2085
+ gl.uniform1i(blendLocations.enabled, 0);
2086
+ gl.enableVertexAttribArray(blendLocations.position);
2087
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
2088
+ gl.vertexAttribPointer(blendLocations.position, 2, gl.FLOAT, false, 0, 0);
2089
+ gl.enableVertexAttribArray(blendLocations.texCoord);
2090
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
2091
+ gl.vertexAttribPointer(blendLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
2092
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2093
+ gl.deleteTexture(frame);
2094
+ gl.activeTexture(gl.TEXTURE0);
2095
+ gl.bindTexture(gl.TEXTURE_2D, null);
2096
+ return;
2097
+ }
2098
+ const readStateIndex = this.currentStateIndex;
2099
+ const writeStateIndex = (this.currentStateIndex + 1) % 2;
2100
+ const prevStateTexture = storedStateTextures[readStateIndex];
2101
+ const newStateTexture = storedStateTextures[writeStateIndex];
2102
+ this.updateBackgroundIfNeeded(options.backgroundSource);
2103
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
2104
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, newStateTexture, 0);
2105
+ gl.bindTexture(gl.TEXTURE_2D, newStateTexture);
2106
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
2107
+ gl.viewport(0, 0, width, height);
2108
+ gl.useProgram(stateUpdateProgram);
2109
+ gl.activeTexture(gl.TEXTURE0);
2110
+ gl.bindTexture(gl.TEXTURE_2D, categoryTexture);
2111
+ gl.uniform1i(stateUpdateLocations.categoryTexture, 0);
2112
+ gl.activeTexture(gl.TEXTURE1);
2113
+ gl.bindTexture(gl.TEXTURE_2D, confidenceTexture);
2114
+ gl.uniform1i(stateUpdateLocations.confidenceTexture, 1);
2115
+ gl.activeTexture(gl.TEXTURE2);
2116
+ gl.bindTexture(gl.TEXTURE_2D, prevStateTexture);
2117
+ gl.uniform1i(stateUpdateLocations.prevStateTexture, 2);
2118
+ gl.uniform1f(stateUpdateLocations.smoothingFactor, 0.8);
2119
+ gl.uniform1f(stateUpdateLocations.smoothstepMin, 0.6);
2120
+ gl.uniform1f(stateUpdateLocations.smoothstepMax, 0.9);
2121
+ gl.uniform1i(stateUpdateLocations.selfieModel, options.isSelfieMode ? 1 : 0);
2122
+ gl.enableVertexAttribArray(stateUpdateLocations.position);
2123
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
2124
+ gl.vertexAttribPointer(stateUpdateLocations.position, 2, gl.FLOAT, false, 0, 0);
2125
+ gl.enableVertexAttribArray(stateUpdateLocations.texCoord);
2126
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
2127
+ gl.vertexAttribPointer(stateUpdateLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
2128
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2129
+ gl.bindFramebuffer(gl.FRAMEBUFFER, refineFbo);
2130
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, refinedMaskTexture, 0);
2131
+ gl.bindTexture(gl.TEXTURE_2D, refinedMaskTexture);
2132
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
2133
+ gl.viewport(0, 0, width, height);
2134
+ gl.useProgram(maskRefineProgram);
2135
+ gl.enableVertexAttribArray(maskRefineLocations.position);
2136
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
2137
+ gl.vertexAttribPointer(maskRefineLocations.position, 2, gl.FLOAT, false, 0, 0);
2138
+ gl.enableVertexAttribArray(maskRefineLocations.texCoord);
2139
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
2140
+ gl.vertexAttribPointer(maskRefineLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
2141
+ gl.activeTexture(gl.TEXTURE0);
2142
+ gl.bindTexture(gl.TEXTURE_2D, newStateTexture);
2143
+ gl.uniform1i(maskRefineLocations.maskTexture, 0);
2144
+ gl.activeTexture(gl.TEXTURE1);
2145
+ gl.bindTexture(gl.TEXTURE_2D, frameTexture);
2146
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoFrame);
2147
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2148
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2149
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
2150
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
2151
+ gl.uniform1i(maskRefineLocations.frameTexture, 1);
2152
+ gl.uniform2f(maskRefineLocations.texelSize, 1.0 / width, 1.0 / height);
2153
+ gl.uniform1f(maskRefineLocations.sigmaSpatial, 2.0);
2154
+ gl.uniform1f(maskRefineLocations.sigmaRange, 0.1);
2155
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2156
+ gl.disableVertexAttribArray(maskRefineLocations.position);
2157
+ gl.disableVertexAttribArray(maskRefineLocations.texCoord);
2158
+ let backgroundTexToUse;
2159
+ let bgWToSend = width;
2160
+ let bgHToSend = height;
2161
+ if (options.bgBlur > 0 && options.bgBlurRadius > 0) {
2162
+ const downscale = 0.5;
2163
+ const blurW = Math.floor(width * downscale);
2164
+ const blurH = Math.floor(height * downscale);
2165
+ gl.bindTexture(gl.TEXTURE_2D, blurTexture1);
2166
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
2167
+ gl.bindTexture(gl.TEXTURE_2D, blurTexture2);
2168
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
2169
+ const KERNEL_RADIUS = 10.0;
2170
+ const radiusScale = Math.max(0.0, options.bgBlurRadius) / KERNEL_RADIUS;
2171
+ gl.useProgram(this.blurProgram);
2172
+ gl.enableVertexAttribArray(this.blurLocations.position);
2173
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
2174
+ gl.vertexAttribPointer(this.blurLocations.position, 2, gl.FLOAT, false, 0, 0);
2175
+ gl.enableVertexAttribArray(this.blurLocations.texCoord);
2176
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
2177
+ gl.vertexAttribPointer(this.blurLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
2178
+ gl.activeTexture(gl.TEXTURE1);
2179
+ gl.bindTexture(gl.TEXTURE_2D, refinedMaskTexture);
2180
+ gl.uniform1i(this.blurLocations.personMask, 1);
2181
+ gl.uniform1f(this.blurLocations.sigma, options.bgBlur * 0.7);
2182
+ gl.uniform1f(this.blurLocations.radiusScale, radiusScale);
2183
+ const blurPasses = [
2184
+ {
2185
+ direction: [1.0, 0.0],
2186
+ input: frameTexture,
2187
+ output: blurFbo1,
2188
+ texelSize: [1.0 / width, 1.0 / height],
2189
+ },
2190
+ {
2191
+ direction: [0.0, 1.0],
2192
+ input: blurTexture1,
2193
+ output: blurFbo2,
2194
+ texelSize: [1.0 / blurW, 1.0 / blurH],
2195
+ },
2196
+ {
2197
+ direction: [1.0, 0.0],
2198
+ input: blurTexture2,
2199
+ output: blurFbo1,
2200
+ texelSize: [1.0 / blurW, 1.0 / blurH],
2201
+ },
2202
+ {
2203
+ direction: [0.0, 1.0],
2204
+ input: blurTexture1,
2205
+ output: blurFbo2,
2206
+ texelSize: [1.0 / blurW, 1.0 / blurH],
2207
+ },
2208
+ ];
2209
+ for (const pass of blurPasses) {
2210
+ gl.bindFramebuffer(gl.FRAMEBUFFER, pass.output);
2211
+ gl.viewport(0, 0, blurW, blurH);
2212
+ gl.activeTexture(gl.TEXTURE0);
2213
+ gl.bindTexture(gl.TEXTURE_2D, pass.input);
2214
+ gl.uniform1i(this.blurLocations.image, 0);
2215
+ gl.uniform2f(this.blurLocations.texelSize, pass.texelSize[0], pass.texelSize[1]);
2216
+ gl.uniform2f(this.blurLocations.direction, pass.direction[0], pass.direction[1]);
2217
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2218
+ }
2219
+ backgroundTexToUse = blurTexture2;
2220
+ bgWToSend = blurW;
2221
+ bgHToSend = blurH;
2222
+ }
2223
+ else if (options.backgroundSource && this.backgroundRenderInfo) {
2224
+ backgroundTexToUse = this.backgroundRenderInfo.texture;
2225
+ if (this.backgroundRenderInfo.type === 'video') {
2226
+ const { canvas } = this.backgroundRenderInfo;
2227
+ bgWToSend = canvas.width || width;
2228
+ bgHToSend = canvas.height || height;
2229
+ }
2230
+ else if (this.backgroundRenderInfo.type === 'image') {
2231
+ bgWToSend = this.backgroundRenderInfo.width;
2232
+ bgHToSend = this.backgroundRenderInfo.height;
2233
+ }
2234
+ else {
2235
+ bgWToSend = width;
2236
+ bgHToSend = height;
2237
+ }
2238
+ }
2239
+ else {
2240
+ backgroundTexToUse = this.backgroundRenderInfo?.texture ?? null;
2241
+ }
2242
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2243
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
2244
+ gl.useProgram(blendProgram);
2245
+ gl.activeTexture(gl.TEXTURE0);
2246
+ gl.bindTexture(gl.TEXTURE_2D, frameTexture);
2247
+ gl.uniform1i(blendLocations.frameTexture, 0);
2248
+ gl.uniform1f(blendLocations.borderSmooth, 0);
2249
+ gl.uniform1f(blendLocations.bgBlur, options.bgBlur);
2250
+ gl.uniform1f(blendLocations.bgBlurRadius, options.bgBlurRadius);
2251
+ gl.uniform1i(blendLocations.enabled, 1);
2252
+ gl.activeTexture(gl.TEXTURE1);
2253
+ gl.bindTexture(gl.TEXTURE_2D, refinedMaskTexture);
2254
+ gl.uniform1i(blendLocations.currentStateTexture, 1);
2255
+ if (backgroundTexToUse) {
2256
+ gl.activeTexture(gl.TEXTURE2);
2257
+ gl.bindTexture(gl.TEXTURE_2D, backgroundTexToUse);
2258
+ gl.uniform1i(blendLocations.backgroundTexture, 2);
2259
+ gl.uniform2f(blendLocations.bgImageDimensions, bgWToSend, bgHToSend);
2260
+ gl.uniform2f(blendLocations.canvasDimensions, width, height);
2261
+ }
2262
+ else {
2263
+ gl.uniform2f(blendLocations.bgImageDimensions, width, height);
2264
+ gl.uniform2f(blendLocations.canvasDimensions, width, height);
2265
+ }
2266
+ gl.enableVertexAttribArray(blendLocations.position);
2267
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
2268
+ gl.vertexAttribPointer(blendLocations.position, 2, gl.FLOAT, false, 0, 0);
2269
+ gl.enableVertexAttribArray(blendLocations.texCoord);
2270
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
2271
+ gl.vertexAttribPointer(blendLocations.texCoord, 2, gl.FLOAT, false, 0, 0);
2272
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2273
+ for (let i = 0; i < 3; ++i) {
2274
+ gl.activeTexture(gl.TEXTURE0 + i);
2275
+ gl.bindTexture(gl.TEXTURE_2D, null);
2276
+ }
2277
+ this.currentStateIndex = writeStateIndex;
2278
+ }
2279
+ close() {
2280
+ if (!this.running)
2281
+ return;
2282
+ this.running = false;
2283
+ const { gl, fbo, refineFbo, refinedMaskTexture, blurFbo1, blurFbo2 } = this;
2284
+ gl.clearColor(0, 0, 0, 0);
2285
+ gl.clear(gl.COLOR_BUFFER_BIT);
2286
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2287
+ if (fbo)
2288
+ gl.deleteFramebuffer(fbo);
2289
+ if (refineFbo)
2290
+ gl.deleteFramebuffer(refineFbo);
2291
+ if (blurFbo1)
2292
+ gl.deleteFramebuffer(blurFbo1);
2293
+ if (blurFbo2)
2294
+ gl.deleteFramebuffer(blurFbo2);
2295
+ gl.deleteProgram(this.stateUpdateProgram);
2296
+ gl.deleteProgram(this.maskRefineProgram);
2297
+ gl.deleteProgram(this.blurProgram);
2298
+ gl.deleteProgram(this.blendProgram);
2299
+ if (this.positionBuffer)
2300
+ gl.deleteBuffer(this.positionBuffer);
2301
+ if (this.texCoordBuffer)
2302
+ gl.deleteBuffer(this.texCoordBuffer);
2303
+ if (refinedMaskTexture)
2304
+ gl.deleteTexture(refinedMaskTexture);
2305
+ if (this.blurTexture1)
2306
+ gl.deleteTexture(this.blurTexture1);
2307
+ if (this.blurTexture2)
2308
+ gl.deleteTexture(this.blurTexture2);
2309
+ this.storedStateTextures.forEach((t) => t && gl.deleteTexture(t));
2310
+ this.storedStateTextures.splice(0, this.storedStateTextures.length);
2311
+ if (this.backgroundRenderInfo?.texture) {
2312
+ gl.deleteTexture(this.backgroundRenderInfo.texture);
2313
+ this.backgroundRenderInfo = null;
2314
+ }
2315
+ this.activeBackgroundSourceIdentifier = null;
2316
+ }
2317
+ }
2318
+ WebGLRenderer.DEFAULT_BG_COLOR = [33, 150, 243, 255];
2319
+
2320
+ /**
2321
+ * Fallback video processor for browsers that do not support MediaStreamTrackGenerator.
2322
+ *
2323
+ * Produces a video MediaStreamTrack sourced from a canvas and exposes
2324
+ * a WritableStream<VideoFrame> on track.writable for writing frames.
2325
+ */
2326
+ class FallbackGenerator {
2327
+ constructor({ kind, signalTarget }) {
2328
+ if (kind !== 'video') {
2329
+ throw new Error('Only video tracks are supported');
2330
+ }
2331
+ const canvas = document.createElement('canvas');
2332
+ const ctx = canvas.getContext('2d', { desynchronized: true });
2333
+ if (!ctx) {
2334
+ throw new Error('Failed to get 2D context from canvas');
2335
+ }
2336
+ const mediaStream = canvas.captureStream();
2337
+ const track = mediaStream.getVideoTracks()[0];
2338
+ const height = signalTarget?.getSettings().height;
2339
+ const width = signalTarget?.getSettings().width;
2340
+ if (height && width) {
2341
+ canvas.height = height;
2342
+ canvas.width = width;
2343
+ }
2344
+ if (!track) {
2345
+ throw new Error('Failed to create canvas track');
2346
+ }
2347
+ if (signalTarget) {
2348
+ signalTarget.addEventListener('ended', () => {
2349
+ track.stop();
2350
+ });
2351
+ }
2352
+ track.writable = new WritableStream({
2353
+ write: (frame) => {
2354
+ if (canvas.width !== frame.displayWidth ||
2355
+ canvas.height !== frame.displayHeight) {
2356
+ canvas.width = frame.displayWidth;
2357
+ canvas.height = frame.displayHeight;
2358
+ }
2359
+ ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
2360
+ frame.close();
2361
+ },
2362
+ abort: () => {
2363
+ track.stop();
2364
+ },
2365
+ close: () => {
2366
+ track.stop();
2367
+ },
2368
+ });
2369
+ return track;
2370
+ }
2371
+ }
2372
+ const TrackGenerator = typeof MediaStreamTrackGenerator !== 'undefined'
2373
+ ? MediaStreamTrackGenerator
2374
+ : FallbackGenerator;
2375
+
2376
+ /**
2377
+ * Fallback implementation for browsers without MediaStreamTrackGenerator.
2378
+ *
2379
+ * Produces a video MediaStreamTrack sourced from a canvas and exposes a
2380
+ * WritableStream<VideoFrame> on track.writable. Written frames are drawn
2381
+ * into the canvas and update the underlying track automatically.
2382
+ */
2383
+ class FallbackProcessor {
2384
+ constructor({ track }) {
2385
+ this.close = () => {
2386
+ this.video.pause();
2387
+ this.video.srcObject = null;
2388
+ this.video.src = '';
2389
+ this.workerTimer.destroy();
2390
+ };
2391
+ if (!track)
2392
+ throw new Error('MediaStreamTrack is required');
2393
+ if (track.kind !== 'video') {
2394
+ throw new Error('MediaStreamTrack must be video');
2395
+ }
2396
+ let running = true;
2397
+ this.video = document.createElement('video');
2398
+ this.video.muted = true;
2399
+ this.video.playsInline = true;
2400
+ this.video.srcObject = new MediaStream([track]);
2401
+ const canvas = new OffscreenCanvas(1, 1);
2402
+ const ctx = canvas.getContext('2d');
2403
+ if (!ctx)
2404
+ throw new Error('Failed to get 2D context from OffscreenCanvas');
2405
+ let timestamp = 0;
2406
+ const frameRate = track.getSettings().frameRate || 30;
2407
+ let frameDuration = 1000 / frameRate;
2408
+ let lastVideoTime = -1;
2409
+ this.workerTimer = new workerTimer.WorkerTimer({ useWorker: true });
2410
+ this.readable = new ReadableStream({
2411
+ start: async () => {
2412
+ await Promise.all([
2413
+ this.video.play(),
2414
+ new Promise((r) => this.video.addEventListener('loadeddata', r, { once: true })),
2415
+ ]);
2416
+ frameDuration = 1000 / (track.getSettings().frameRate || 30);
2417
+ timestamp = performance.now();
2418
+ },
2419
+ pull: async (controller) => {
2420
+ if (!running) {
2421
+ controller.close();
2422
+ this.close();
2423
+ return;
2424
+ }
2425
+ const delta = performance.now() - timestamp;
2426
+ if (delta <= frameDuration) {
2427
+ await new Promise((r) => this.workerTimer.setTimeout(r, frameDuration - delta));
2428
+ }
2429
+ timestamp = performance.now();
2430
+ const currentTime = this.video.currentTime;
2431
+ const hasNewFrame = currentTime !== lastVideoTime;
2432
+ if (!hasNewFrame) {
2433
+ await new Promise((r) => this.workerTimer.setTimeout(r, frameDuration));
2434
+ return;
2435
+ }
2436
+ lastVideoTime = currentTime;
2437
+ if (canvas.width !== this.video.videoWidth ||
2438
+ canvas.height !== this.video.videoHeight) {
2439
+ canvas.width = this.video.videoWidth;
2440
+ canvas.height = this.video.videoHeight;
2441
+ }
2442
+ ctx.drawImage(this.video, 0, 0);
2443
+ try {
2444
+ const frame = new VideoFrame(canvas, { timestamp });
2445
+ controller.enqueue(frame);
2446
+ }
2447
+ catch (err) {
2448
+ running = false;
2449
+ controller.error(err);
2450
+ this.close();
2451
+ }
2452
+ },
2453
+ cancel: () => {
2454
+ running = false;
2455
+ this.close();
2456
+ },
2457
+ });
2458
+ }
2459
+ }
2460
+ const TrackProcessor = typeof MediaStreamTrackProcessor !== 'undefined'
2461
+ ? MediaStreamTrackProcessor
2462
+ : FallbackProcessor;
2463
+
2464
+ /**
2465
+ * Wraps a video MediaStreamTrack in a real-time processing pipeline.
2466
+ * Incoming frames are processed through a transformer and re-emitted
2467
+ * on a new MediaStreamVideoTrack for downstream consumption.
2468
+ */
2469
+ class VirtualBackground {
2470
+ constructor(track, options = {}, hooks = {}) {
2471
+ this.track = track;
2472
+ this.options = options;
2473
+ this.hooks = hooks;
2474
+ this.segmenter = null;
2475
+ this.isSegmenterReady = false;
2476
+ this.segmenterDelayTotal = 0;
2477
+ this.frames = 0;
2478
+ this.lastStatsTime = 0;
2479
+ this.processor = new TrackProcessor({ track });
2480
+ this.generator = new TrackGenerator({
2481
+ kind: 'video',
2482
+ signalTarget: track,
2483
+ });
2484
+ this.abortController = new AbortController();
2485
+ }
2486
+ async start() {
2487
+ const { onError } = this.hooks;
2488
+ const { readable } = this.processor;
2489
+ const { writable } = this.generator;
2490
+ const displayWidth = this.track.getSettings().width ?? 1280;
2491
+ const displayHeight = this.track.getSettings().height ?? 720;
2492
+ this.canvas = new OffscreenCanvas(displayWidth, displayHeight);
2493
+ this.webGlRenderer = new WebGLRenderer(this.canvas);
2494
+ await this.initializeSegmenter();
2495
+ const opts = await this.initializeSegmenterOptions();
2496
+ const transformStream = new TransformStream({
2497
+ transform: async (frame, controller) => {
2498
+ try {
2499
+ if (this.abortController.signal.aborted) {
2500
+ return frame.close();
2501
+ }
2502
+ const processed = await this.transform(frame, opts);
2503
+ controller.enqueue(processed);
2504
+ }
2505
+ catch (e) {
2506
+ console.error('[virtual-background] error processing frame:', e);
2507
+ this.hooks.onError?.(e);
2508
+ if (!this.abortController.signal.aborted) {
2509
+ controller.enqueue(frame);
2510
+ }
2511
+ }
2512
+ finally {
2513
+ frame.close();
2514
+ }
2515
+ },
2516
+ flush: () => {
2517
+ if (this.segmenter) {
2518
+ this.segmenter.close();
2519
+ this.segmenter = null;
2520
+ }
2521
+ this.isSegmenterReady = false;
2522
+ },
2523
+ });
2524
+ const signal = this.abortController.signal;
2525
+ readable
2526
+ .pipeThrough(transformStream, { signal })
2527
+ .pipeTo(writable, { signal })
2528
+ .catch((e) => {
2529
+ if (e.name !== 'AbortError') {
2530
+ console.error('[virtual-background] Error processing track:', e);
2531
+ onError?.(e);
2532
+ }
2533
+ });
2534
+ return this.generator;
2535
+ }
2536
+ /**
2537
+ * Loads and initializes the MediaPipe `ImageSegmenter`.
2538
+ */
2539
+ async initializeSegmenter() {
2540
+ try {
2541
+ const basePath = this.options?.basePath ||
2542
+ `https://unpkg.com/${packageName}@${version}/mediapipe`;
2543
+ const defaultModelPath = `${basePath}/models/selfie_segmenter.tflite`;
2544
+ const model = this.options?.modelPath || defaultModelPath;
2545
+ const wasmPath = `${basePath}/wasm`;
2546
+ const fileset = await tasksVision.FilesetResolver.forVisionTasks(wasmPath);
2547
+ this.segmenter = await tasksVision.ImageSegmenter.createFromOptions(fileset, {
2548
+ baseOptions: {
2549
+ modelAssetPath: model,
2550
+ delegate: 'GPU',
2551
+ },
2552
+ runningMode: 'VIDEO',
2553
+ outputCategoryMask: true,
2554
+ outputConfidenceMasks: true,
2555
+ canvas: this.canvas,
2556
+ });
2557
+ this.isSegmenterReady = true;
2558
+ }
2559
+ catch (error) {
2560
+ console.error('[virtual-background] Failed to initialize MediaPipe segmenter:', error);
2561
+ this.isSegmenterReady = false;
2562
+ }
2563
+ }
2564
+ /**
2565
+ * Processes a single video frame.
2566
+ *
2567
+ * Performs segmentation via MediaPipe and then composites the frame
2568
+ * through the WebGL renderer to apply background effects.
2569
+ *
2570
+ * @param frame - The incoming frame from the processor.
2571
+ * @param opts - The segmentation options to use.
2572
+ *
2573
+ * @returns A new `VideoFrame` containing the processed image.
2574
+ */
2575
+ async transform(frame, opts) {
2576
+ if (this.isSegmenterReady && this.segmenter) {
2577
+ try {
2578
+ const start = performance.now();
2579
+ await new Promise((resolve) => {
2580
+ this.segmenter.segmentForVideo(frame, frame.timestamp, (result) => {
2581
+ const categoryMask = result.categoryMask.getAsWebGLTexture();
2582
+ const confidenceMask = result.confidenceMasks[0].getAsWebGLTexture();
2583
+ this.webGlRenderer.render(frame, opts, categoryMask, confidenceMask);
2584
+ const now = performance.now();
2585
+ this.segmenterDelayTotal += now - start;
2586
+ this.frames++;
2587
+ if (this.lastStatsTime === 0) {
2588
+ this.lastStatsTime = now;
2589
+ }
2590
+ if (now - this.lastStatsTime > 1000) {
2591
+ const delay = Math.round((this.segmenterDelayTotal / this.frames) * 100) /
2592
+ 100;
2593
+ const fps = Math.round((1000 * this.frames) / (now - this.lastStatsTime));
2594
+ this.hooks.onStats?.({ delay, fps, timestamp: now });
2595
+ this.lastStatsTime = now;
2596
+ this.segmenterDelayTotal = 0;
2597
+ this.frames = 0;
2598
+ }
2599
+ resolve();
2600
+ });
2601
+ });
2602
+ }
2603
+ catch (error) {
2604
+ console.error('[virtual-background] Error during segmentation:', error);
2605
+ }
2606
+ }
2607
+ return new VideoFrame(this.canvas, { timestamp: frame.timestamp });
2608
+ }
2609
+ async loadBackground(url) {
2610
+ if (!url) {
2611
+ return;
2612
+ }
2613
+ const response = await fetch(url);
2614
+ if (!response.ok) {
2615
+ console.error(`[virtual-background] Failed to fetch background source ${url} (status: ${response.status})`);
2616
+ return;
2617
+ }
2618
+ const blob = await response.blob();
2619
+ const imageBitmap = await createImageBitmap(blob);
2620
+ return { type: 'image', media: imageBitmap, url };
2621
+ }
2622
+ async initializeSegmenterOptions() {
2623
+ const isSelfieMode = this.options.modelPath
2624
+ ? this.options.modelPath?.includes('selfie_segmenter')
2625
+ : true;
2626
+ if (this.options.backgroundFilter === 'image') {
2627
+ return {
2628
+ backgroundSource: await this.loadBackground(this.options.backgroundImage),
2629
+ bgBlur: 0,
2630
+ bgBlurRadius: 0,
2631
+ isSelfieMode,
2632
+ };
2633
+ }
2634
+ const blurLevel = this.options.backgroundBlurLevel;
2635
+ if (typeof blurLevel === 'string') {
2636
+ return {
2637
+ ...BACKGROUND_BLUR_MAP[blurLevel],
2638
+ backgroundSource: undefined,
2639
+ isSelfieMode,
2640
+ };
2641
+ }
2642
+ const numeric = blurLevel ?? 5;
2643
+ const bgBlur = Math.min(numeric * 3, 30);
2644
+ const bgBlurRadius = Math.min(numeric, 10);
2645
+ return {
2646
+ backgroundSource: undefined,
2647
+ bgBlur,
2648
+ bgBlurRadius,
2649
+ isSelfieMode,
2650
+ };
2651
+ }
2652
+ stop() {
2653
+ this.abortController.abort();
2654
+ this.webGlRenderer.close();
2655
+ this.generator.stop();
2656
+ if (this.segmenter) {
2657
+ this.segmenter.close();
2658
+ this.segmenter = null;
2659
+ }
2660
+ this.isSegmenterReady = false;
2661
+ }
2662
+ }
2663
+
2664
+ exports.BACKGROUND_BLUR_MAP = BACKGROUND_BLUR_MAP;
2665
+ exports.VirtualBackground = VirtualBackground;
1575
2666
  exports.createRenderer = createRenderer;
2667
+ exports.isMediaPipePlatformSupported = isMediaPipePlatformSupported;
1576
2668
  exports.isPlatformSupported = isPlatformSupported;
2669
+ exports.loadMediaPipe = loadMediaPipe;
1577
2670
  exports.loadTFLite = loadTFLite;
1578
2671
  //# sourceMappingURL=index.cjs.js.map