@stream-io/video-filters-web 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +27 -24
- package/dist/index.cjs.js +1099 -6
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.es.js +1096 -7
- package/dist/index.es.js.map +1 -1
- package/dist/src/FallbackGenerator.d.ts +17 -0
- package/dist/src/FallbackProcessor.d.ts +38 -0
- package/dist/src/VirtualBackground.d.ts +42 -0
- package/dist/src/WebGLRenderer.d.ts +80 -0
- package/dist/src/compatibility.d.ts +5 -0
- package/dist/src/{createRenderer.d.ts → legacy/createRenderer.d.ts} +1 -2
- package/dist/src/{webgl2 → legacy/webgl2}/backgroundBlurStage.d.ts +1 -1
- package/dist/src/{webgl2 → legacy/webgl2}/webgl2Pipeline.d.ts +1 -1
- package/dist/src/mediapipe.d.ts +4 -0
- package/dist/src/types.d.ts +45 -0
- package/index.ts +6 -3
- package/mediapipe/models/selfie_segmenter.tflite +0 -0
- package/mediapipe/wasm/vision_wasm_internal.js +20 -0
- package/mediapipe/wasm/vision_wasm_internal.wasm +0 -0
- package/mediapipe/wasm/vision_wasm_nosimd_internal.js +20 -0
- package/mediapipe/wasm/vision_wasm_nosimd_internal.wasm +0 -0
- package/package.json +5 -2
- package/src/FallbackGenerator.ts +90 -0
- package/src/FallbackProcessor.ts +132 -0
- package/src/VirtualBackground.ts +276 -0
- package/src/WebGLRenderer.ts +1187 -0
- package/src/compatibility.ts +22 -0
- package/src/{createRenderer.ts → legacy/createRenderer.ts} +1 -2
- package/src/{tflite.ts → legacy/tflite.ts} +1 -1
- package/src/{webgl2 → legacy/webgl2}/backgroundBlurStage.ts +1 -1
- package/src/{webgl2 → legacy/webgl2}/webgl2Pipeline.ts +1 -1
- package/src/mediapipe.ts +25 -0
- package/src/types.ts +62 -0
- /package/dist/src/{helpers → legacy/helpers}/webglHelper.d.ts +0 -0
- /package/dist/src/{segmentation.d.ts → legacy/segmentation.d.ts} +0 -0
- /package/dist/src/{tflite.d.ts → legacy/tflite.d.ts} +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/backgroundImageStage.d.ts +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/jointBilateralFilterStage.d.ts +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/resizingStage.d.ts +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/softmaxStage.d.ts +0 -0
- /package/src/{helpers → legacy/helpers}/webglHelper.ts +0 -0
- /package/src/{segmentation.ts → legacy/segmentation.ts} +0 -0
- /package/src/{tflite-simd.js → legacy/tflite-simd.js} +0 -0
- /package/src/{webgl2 → legacy/webgl2}/backgroundImageStage.ts +0 -0
- /package/src/{webgl2 → legacy/webgl2}/jointBilateralFilterStage.ts +0 -0
- /package/src/{webgl2 → legacy/webgl2}/resizingStage.ts +0 -0
- /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.
|
|
1564
|
+
const version = "0.5.0";
|
|
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 =
|
|
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)
|
|
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
|