@pexip/media-processor 19.1.0 → 20.0.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 +30 -0
- package/dist/common/backends/webgl/blur.js +7 -5
- package/dist/common/backends/webgl/renderer.d.ts +1 -1
- package/dist/common/backends/webgl/renderer.js +27 -34
- package/dist/common/backends/webgl/smoothingMask.d.ts +1 -1
- package/dist/common/backends/webgl/webglUtils.d.ts +1 -1
- package/dist/common/backends/webgl/webglUtils.js +9 -10
- package/dist/common/backends/webgpu/blender.d.ts +8 -0
- package/dist/common/backends/webgpu/blender.js +78 -0
- package/dist/common/backends/webgpu/constants.d.ts +1 -0
- package/dist/common/backends/webgpu/constants.js +1 -0
- package/dist/common/backends/webgpu/dualFilterBlur.d.ts +5 -0
- package/dist/common/backends/webgpu/dualFilterBlur.js +201 -0
- package/dist/common/backends/webgpu/renderer.d.ts +2 -0
- package/dist/common/backends/webgpu/renderer.js +127 -0
- package/dist/common/backends/webgpu/smoothingMask.d.ts +5 -0
- package/dist/common/backends/webgpu/smoothingMask.js +66 -0
- package/dist/common/backends/webgpu/texture.d.ts +5 -0
- package/dist/common/backends/webgpu/texture.js +35 -0
- package/dist/common/backends/webgpu/textureToTexture.d.ts +4 -0
- package/dist/common/backends/webgpu/textureToTexture.js +35 -0
- package/dist/common/backends/webgpu/types.d.ts +5 -0
- package/dist/common/backends/webgpu/types.js +1 -0
- package/dist/common/backends/webgpu/webgpuUtils.d.ts +7 -0
- package/dist/common/backends/webgpu/webgpuUtils.js +14 -0
- package/dist/common/canvasRenderUtils.d.ts +1 -1
- package/dist/common/canvasRenderUtils.js +8 -10
- package/dist/common/constants.d.ts +5 -0
- package/dist/common/constants.js +4 -0
- package/dist/common/index.d.ts +0 -1
- package/dist/common/index.js +0 -1
- package/dist/common/m3.d.ts +14 -8
- package/dist/common/m3.js +0 -1
- package/dist/common/tsconfig.tsbuildinfo +1 -1
- package/dist/common/typeGuards.d.ts +2 -0
- package/dist/common/typeGuards.js +7 -0
- package/dist/common/types/render.d.ts +7 -4
- package/dist/common/types/segmentation.d.ts +5 -1
- package/dist/common/utils.d.ts +4 -1
- package/dist/common/utils.js +36 -1
- package/dist/main/audio.d.ts +1 -1
- package/dist/main/audio.js +7 -5
- package/dist/main/math.js +1 -1
- package/dist/main/process.d.ts +3 -3
- package/dist/main/process.js +2 -2
- package/dist/main/processor.d.ts +0 -1
- package/dist/main/transformer.d.ts +1 -1
- package/dist/main/tsconfig.tsbuildinfo +1 -1
- package/dist/main/typeGuards.d.ts +1 -1
- package/dist/main/types.d.ts +2 -1
- package/dist/main/utils.js +6 -6
- package/dist/main/video/canvasTransform.d.ts +1 -1
- package/dist/main/video/canvasTransform.js +66 -12
- package/dist/main/video/segmenters/mediapipe.d.ts +1 -11
- package/dist/main/video/segmenters/mediapipe.js +37 -22
- package/dist/main/video/transformer.d.ts +1 -1
- package/dist/main/video/typeGuards.d.ts +2 -1
- package/dist/main/video/types.d.ts +2 -12
- package/dist/main/video/utils.d.ts +2 -2
- package/dist/main/video/utils.js +2 -2
- package/dist/main/video/video.d.ts +3 -2
- package/dist/main/video/video.js +5 -3
- package/dist/main/video/videoStreamTrackProcessor.d.ts +1 -1
- package/dist/main/video/videoStreamTrackProcessor.js +2 -2
- package/dist/workers/mediaWorker.js +16 -16
- package/dist/workers/tsconfig.tsbuildinfo +1 -1
- package/dist/worklets/tsconfig.tsbuildinfo +1 -1
- package/dist/worklets/types.d.ts +0 -2
- package/package.json +44 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 20.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 2bfd9e1: Adds esm entry points.
|
|
8
|
+
|
|
9
|
+
### Minor Changes
|
|
10
|
+
|
|
11
|
+
- 4baa8a4: Implements dual filter blur wglsl shader.
|
|
12
|
+
- 3555d9d: Adds `build:sourcemap` script.
|
|
13
|
+
- 0416d6c: Introduces WebGPU option for video processing.
|
|
14
|
+
- ed53b05: Upgrade Typescript
|
|
15
|
+
- 5855473: Enables dynamic updates of processing width and height.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- d3909ed: Enable recommended linter rules
|
|
20
|
+
- e76a15e: Migrate to biome linter
|
|
21
|
+
- Updated dependencies [2bfd9e1]
|
|
22
|
+
- Updated dependencies [4fe77de]
|
|
23
|
+
- Updated dependencies [2bbff64]
|
|
24
|
+
- Updated dependencies [4ea0492]
|
|
25
|
+
- Updated dependencies [3555d9d]
|
|
26
|
+
- Updated dependencies [1601985]
|
|
27
|
+
- Updated dependencies [84956b8]
|
|
28
|
+
- Updated dependencies [ed53b05]
|
|
29
|
+
- Updated dependencies [d681fd3]
|
|
30
|
+
- Updated dependencies [e76a15e]
|
|
31
|
+
- @pexip/utils@17.0.0
|
|
32
|
+
|
|
3
33
|
## 19.1.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
|
@@ -64,8 +64,8 @@ export const createBlurRenderer = (processor, pass) => {
|
|
|
64
64
|
const texture = createAndSetupTexture(gl);
|
|
65
65
|
assert(texture);
|
|
66
66
|
// Avoid fractional value
|
|
67
|
-
const width = Math.trunc(processor.width /
|
|
68
|
-
const height = Math.trunc(processor.height /
|
|
67
|
+
const width = Math.trunc(processor.width / 2 ** idx);
|
|
68
|
+
const height = Math.trunc(processor.height / 2 ** idx);
|
|
69
69
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
70
70
|
const frameBuffer = gl.createFramebuffer();
|
|
71
71
|
assert(frameBuffer);
|
|
@@ -99,7 +99,9 @@ export const createBlurRenderer = (processor, pass) => {
|
|
|
99
99
|
// Adjust pass number accordingly
|
|
100
100
|
const currentPass = pass + 1;
|
|
101
101
|
if (currentPass > blurBuffers.length) {
|
|
102
|
-
generateBlurBuffers(currentPass - blurBuffers.length, blurBuffers.length)
|
|
102
|
+
for (const buffer of generateBlurBuffers(currentPass - blurBuffers.length, blurBuffers.length)) {
|
|
103
|
+
blurBuffers.push(buffer);
|
|
104
|
+
}
|
|
103
105
|
}
|
|
104
106
|
while (currentPass < blurBuffers.length) {
|
|
105
107
|
const buffer = blurBuffers.pop();
|
|
@@ -158,10 +160,10 @@ export const createBlurRenderer = (processor, pass) => {
|
|
|
158
160
|
};
|
|
159
161
|
},
|
|
160
162
|
release: () => {
|
|
161
|
-
|
|
163
|
+
for (const { frameBuffer, texture } of blurBuffers) {
|
|
162
164
|
gl.deleteTexture(texture);
|
|
163
165
|
gl.deleteFramebuffer(frameBuffer);
|
|
164
|
-
}
|
|
166
|
+
}
|
|
165
167
|
gl.deleteBuffer(positionBuffer);
|
|
166
168
|
gl.deleteBuffer(texCoordBuffer);
|
|
167
169
|
gl.deleteVertexArray(vao);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Renderer, RenderEventHandlers } from '../../types/render';
|
|
2
|
-
export declare const createRenderer: (
|
|
2
|
+
export declare const createRenderer: (renderEventHandlers: RenderEventHandlers, canvas?: OffscreenCanvas) => Renderer;
|
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import { assert } from '@pexip/utils';
|
|
2
2
|
import { PROCESSING_WIDTH, PROCESSING_HEIGHT, EDGE_BLUR_AMOUNT, BACKGROUND_BLUR_AMOUNT, RENDERING_EVENTS, } from '../../constants';
|
|
3
|
-
import { toTextureInfo } from './webglUtils';
|
|
4
3
|
import { createTextureRenderer } from './texture';
|
|
5
4
|
import { createBlurRenderer } from './blur';
|
|
6
5
|
import { createBlendRenderer } from './blender';
|
|
7
6
|
import { createCanvasRenderer } from './canvas';
|
|
8
7
|
import { createTextureToTextureRenderer } from './textureToTexture';
|
|
9
8
|
import { createSmoothingMaskRenderer } from './smoothingMask';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return new Proxy(props, {
|
|
13
|
-
get(target, p, receiver) {
|
|
14
|
-
const prop = target[p];
|
|
15
|
-
if (!prop) {
|
|
16
|
-
target[p] = stateBuilder[p]?.(props);
|
|
17
|
-
}
|
|
18
|
-
return Reflect.get(target, p, receiver);
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
};
|
|
22
|
-
export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PROCESSING_HEIGHT), renderEventHandlers) => {
|
|
9
|
+
import { createLazyProps, handleWebGLContextLoss } from '../../utils';
|
|
10
|
+
export const createRenderer = (renderEventHandlers, canvas = new OffscreenCanvas(PROCESSING_WIDTH, PROCESSING_HEIGHT)) => {
|
|
23
11
|
const rendererCreator = {
|
|
24
12
|
videoFrameRenderer: () => createTextureRenderer(canvas),
|
|
13
|
+
maskRenderer: () => createTextureRenderer(canvas),
|
|
25
14
|
canvasRenderer: () => createCanvasRenderer(canvas),
|
|
26
15
|
backgroundImageRenderer: () => createTextureRenderer(canvas),
|
|
27
16
|
blendRenderer: () => createBlendRenderer(canvas),
|
|
@@ -29,17 +18,26 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
|
|
|
29
18
|
maskBlurRenderer: () => createBlurRenderer(canvas, EDGE_BLUR_AMOUNT),
|
|
30
19
|
textureRenderer: () => createTextureToTextureRenderer(canvas),
|
|
31
20
|
smoothingMaskRenderer: () => createSmoothingMaskRenderer(canvas),
|
|
32
|
-
release: (states) => () =>
|
|
33
|
-
states
|
|
21
|
+
release: (states) => () => {
|
|
22
|
+
for (const key in states) {
|
|
23
|
+
if (key !== 'release') {
|
|
24
|
+
states[key]?.release();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
34
28
|
};
|
|
35
29
|
let lazyProps = createLazyProps(rendererCreator);
|
|
36
30
|
const props = {
|
|
37
31
|
canvas,
|
|
38
32
|
gl: null,
|
|
33
|
+
canvasContextEventsCleanup: () => undefined,
|
|
39
34
|
// TODO: refactor to avoid repeated codes
|
|
40
35
|
get videoFrameRenderer() {
|
|
41
36
|
return lazyProps.videoFrameRenderer;
|
|
42
37
|
},
|
|
38
|
+
get maskRenderer() {
|
|
39
|
+
return lazyProps.maskRenderer;
|
|
40
|
+
},
|
|
43
41
|
get canvasRenderer() {
|
|
44
42
|
return lazyProps.canvasRenderer;
|
|
45
43
|
},
|
|
@@ -63,21 +61,12 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
|
|
|
63
61
|
return lazyProps.smoothingMaskRenderer;
|
|
64
62
|
},
|
|
65
63
|
};
|
|
66
|
-
const handleContextLost = (event) => {
|
|
67
|
-
event.preventDefault();
|
|
68
|
-
cleanup();
|
|
69
|
-
renderEventHandlers.contextLost(event.statusMessage);
|
|
70
|
-
};
|
|
71
|
-
const handleContextRestored = (event) => {
|
|
72
|
-
renderEventHandlers.contextRestored(event.statusMessage);
|
|
73
|
-
};
|
|
74
|
-
const handleContextCreationError = (event) => {
|
|
75
|
-
renderEventHandlers.contextCreationError(event.statusMessage);
|
|
76
|
-
};
|
|
77
64
|
const init = () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
props.canvasContextEventsCleanup = handleWebGLContextLoss(canvas, {
|
|
66
|
+
contextLost: renderEventHandlers.contextLost,
|
|
67
|
+
contextRestored: renderEventHandlers.contextRestored,
|
|
68
|
+
contextCreationError: renderEventHandlers.contextCreationError,
|
|
69
|
+
}, cleanup);
|
|
81
70
|
props.gl = canvas.getContext('webgl2', {
|
|
82
71
|
premultipliedAlpha: false,
|
|
83
72
|
powerPreference: 'high-performance',
|
|
@@ -86,8 +75,14 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
|
|
|
86
75
|
};
|
|
87
76
|
const render = (mask, frame, options) => {
|
|
88
77
|
assert(!props.gl?.isContextLost(), RENDERING_EVENTS.ContextLost);
|
|
78
|
+
const maskTexture = props.maskRenderer.render({
|
|
79
|
+
frame: mask,
|
|
80
|
+
width: mask.width,
|
|
81
|
+
height: mask.height,
|
|
82
|
+
timestamp: frame.timestamp,
|
|
83
|
+
});
|
|
89
84
|
const smoothedMask = props.smoothingMaskRenderer.render({
|
|
90
|
-
mask:
|
|
85
|
+
mask: maskTexture,
|
|
91
86
|
prevMask: props.prevMask,
|
|
92
87
|
}, options);
|
|
93
88
|
const frameTexture = props.videoFrameRenderer.render(frame);
|
|
@@ -113,14 +108,12 @@ export const createRenderer = (canvas = new OffscreenCanvas(PROCESSING_WIDTH, PR
|
|
|
113
108
|
props.prevMask = props.textureRenderer.render(smoothedMask);
|
|
114
109
|
};
|
|
115
110
|
const cleanup = () => {
|
|
111
|
+
props.canvasContextEventsCleanup();
|
|
116
112
|
lazyProps.release();
|
|
117
113
|
lazyProps = createLazyProps(rendererCreator);
|
|
118
114
|
props.prevMask = undefined;
|
|
119
115
|
};
|
|
120
116
|
const release = () => {
|
|
121
|
-
canvas.removeEventListener('webglcontextlost', handleContextLost);
|
|
122
|
-
canvas.removeEventListener('webglcontextrestored', handleContextRestored);
|
|
123
|
-
canvas.removeEventListener('webglcontextcreationerror', handleContextCreationError);
|
|
124
117
|
cleanup();
|
|
125
118
|
};
|
|
126
119
|
const isContextLost = () => {
|
|
@@ -5,7 +5,7 @@ import type { TextureInfo } from './types';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const createSmoothingMaskRenderer: (processor: OffscreenCanvas) => {
|
|
7
7
|
render: ({ mask, prevMask, }: {
|
|
8
|
-
prevMask?: TextureInfo
|
|
8
|
+
prevMask?: TextureInfo;
|
|
9
9
|
mask: TextureInfo;
|
|
10
10
|
}, options: RendererOptions) => TextureInfo;
|
|
11
11
|
release: () => void;
|
|
@@ -61,7 +61,7 @@ export declare const createProgram: (gl: WebGLRenderingContext, shaders: [WebGLS
|
|
|
61
61
|
* A parallel array to opt_attribs letting you assign locations.
|
|
62
62
|
*/
|
|
63
63
|
export declare const createProgramFromSources: (gl: WebGLRenderingContext, [vs, fs]: [GLShaderSource, GLShaderSource], attribs?: string[], locations?: number[]) => [WebGLProgram, Release];
|
|
64
|
-
export declare const createAndSetupTexture: (gl: WebGL2RenderingContext) => WebGLTexture
|
|
64
|
+
export declare const createAndSetupTexture: (gl: WebGL2RenderingContext) => WebGLTexture;
|
|
65
65
|
type Location<S extends string> = S extends `u_${string}` ? ReturnType<WebGL2RenderingContext['getUniformLocation']> : S extends `a_${string}` ? ReturnType<WebGL2RenderingContext['getAttribLocation']> : never;
|
|
66
66
|
export declare const getLocations: <T extends Record<string, string>>(gl: WebGL2RenderingContext, program: WebGLProgram, locations: T) => { [Key in keyof T]: Location<T[Key]>; };
|
|
67
67
|
export declare const createGLProgram: <A extends Record<string, string>, U extends Record<string, string>>(processCanvas: OffscreenCanvas, { attribs, uniforms, vertexSrc, fragmentSrc, }: {
|
|
@@ -37,7 +37,7 @@ function addLineNumbersWithError(src, log = '', lineOffset = 0) {
|
|
|
37
37
|
if (!m[1] || m.index === undefined) {
|
|
38
38
|
return [0, log];
|
|
39
39
|
}
|
|
40
|
-
const lineNo = parseInt(m[1], 10);
|
|
40
|
+
const lineNo = Number.parseInt(m[1], 10);
|
|
41
41
|
const next = matches[ndx + 1];
|
|
42
42
|
const end = next ? next.index : log.length;
|
|
43
43
|
const msg = log.substring(m.index, end);
|
|
@@ -93,10 +93,10 @@ export const createProgram = (gl, shaders, attribs, locations) => {
|
|
|
93
93
|
if (!program) {
|
|
94
94
|
throw new Error('Unable to create WebGL program');
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
for (const shader of shaders) {
|
|
97
97
|
gl.attachShader(program, shader);
|
|
98
|
-
}
|
|
99
|
-
attribs?.forEach(
|
|
98
|
+
}
|
|
99
|
+
attribs?.forEach((attrib, ndx) => {
|
|
100
100
|
const locationIdx = locations?.[ndx] ?? ndx;
|
|
101
101
|
gl.bindAttribLocation(program, locationIdx, attrib);
|
|
102
102
|
});
|
|
@@ -118,10 +118,10 @@ export const createProgram = (gl, shaders, attribs, locations) => {
|
|
|
118
118
|
return [
|
|
119
119
|
program,
|
|
120
120
|
() => {
|
|
121
|
-
|
|
121
|
+
for (const shader of shaders) {
|
|
122
122
|
gl.detachShader(program, shader);
|
|
123
123
|
gl.deleteShader(shader);
|
|
124
|
-
}
|
|
124
|
+
}
|
|
125
125
|
gl.deleteProgram(program);
|
|
126
126
|
},
|
|
127
127
|
];
|
|
@@ -156,12 +156,12 @@ export const getLocations = (gl, program, locations) => Object.keys(locations).r
|
|
|
156
156
|
if (!location) {
|
|
157
157
|
return acc;
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
...acc,
|
|
159
|
+
Object.assign(acc, {
|
|
161
160
|
[key]: location.startsWith('u_')
|
|
162
161
|
? gl.getUniformLocation(program, location)
|
|
163
162
|
: gl.getAttribLocation(program, location),
|
|
164
|
-
};
|
|
163
|
+
});
|
|
164
|
+
return acc;
|
|
165
165
|
}, {});
|
|
166
166
|
export const createGLProgram = (processCanvas, { attribs, uniforms, vertexSrc, fragmentSrc, }) => {
|
|
167
167
|
const gl = processCanvas.getContext('webgl2', {
|
|
@@ -192,4 +192,3 @@ export const toTextureInfo = (mask) => ({
|
|
|
192
192
|
width: mask.width,
|
|
193
193
|
height: mask.height,
|
|
194
194
|
});
|
|
195
|
-
/* eslint-enable max-params --- this is utils */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Blend foreground and background according to the mask
|
|
4
|
+
*/
|
|
5
|
+
export declare const createBlendRenderer: (processor: OffscreenCanvas, device: GPUDevice, presentationFormat: GPUTextureFormat, isContextLost: () => boolean) => {
|
|
6
|
+
render: (frameTexture: GPUTexture, bgTexture: GPUTexture, maskTexture: GPUTexture) => void;
|
|
7
|
+
release: () => void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { assert } from '@pexip/utils';
|
|
2
|
+
import { RENDERING_EVENTS } from '../../constants';
|
|
3
|
+
import blenderShader from './shaders/blenderShader.wgsl';
|
|
4
|
+
import { RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE } from './constants';
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* Blend foreground and background according to the mask
|
|
8
|
+
*/
|
|
9
|
+
export const createBlendRenderer = (processor, device, presentationFormat, isContextLost) => {
|
|
10
|
+
const context = processor.getContext('webgpu');
|
|
11
|
+
if (!context) {
|
|
12
|
+
throw Error('WebGPU not supported on the canvas.');
|
|
13
|
+
}
|
|
14
|
+
context.configure({
|
|
15
|
+
device,
|
|
16
|
+
format: presentationFormat,
|
|
17
|
+
alphaMode: 'premultiplied',
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
render: (frameTexture, bgTexture, maskTexture) => {
|
|
21
|
+
assert(!isContextLost(), RENDERING_EVENTS.ContextLost);
|
|
22
|
+
const pipeline = device.createRenderPipeline({
|
|
23
|
+
label: 'blender pipeline',
|
|
24
|
+
layout: 'auto',
|
|
25
|
+
vertex: {
|
|
26
|
+
module: device.createShaderModule({
|
|
27
|
+
label: 'blender vertex',
|
|
28
|
+
code: blenderShader,
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
fragment: {
|
|
32
|
+
module: device.createShaderModule({
|
|
33
|
+
label: 'blender fragment',
|
|
34
|
+
code: blenderShader,
|
|
35
|
+
}),
|
|
36
|
+
targets: [{ format: presentationFormat }],
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const encoder = device.createCommandEncoder({
|
|
40
|
+
label: 'blender encoder',
|
|
41
|
+
});
|
|
42
|
+
const pass = encoder.beginRenderPass({
|
|
43
|
+
label: 'blender renderPass',
|
|
44
|
+
colorAttachments: [
|
|
45
|
+
{
|
|
46
|
+
view: context.getCurrentTexture().createView(),
|
|
47
|
+
clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
|
|
48
|
+
loadOp: 'clear',
|
|
49
|
+
storeOp: 'store',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
pass.setPipeline(pipeline);
|
|
54
|
+
const sampler = device.createSampler({
|
|
55
|
+
addressModeU: 'repeat',
|
|
56
|
+
addressModeV: 'repeat',
|
|
57
|
+
magFilter: 'nearest',
|
|
58
|
+
});
|
|
59
|
+
const bindGroup = device.createBindGroup({
|
|
60
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
61
|
+
entries: [
|
|
62
|
+
{ binding: 0, resource: sampler },
|
|
63
|
+
{ binding: 1, resource: bgTexture.createView() },
|
|
64
|
+
{ binding: 2, resource: frameTexture.createView() },
|
|
65
|
+
{ binding: 3, resource: maskTexture.createView() },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
pass.setBindGroup(0, bindGroup);
|
|
69
|
+
pass.draw(6); // call our vertex shader 6 times
|
|
70
|
+
pass.end();
|
|
71
|
+
const commandBuffer = encoder.finish();
|
|
72
|
+
device.queue.submit([commandBuffer]);
|
|
73
|
+
},
|
|
74
|
+
release: () => {
|
|
75
|
+
// Nothing
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE: number[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE = [0.0, 0.0, 0.0, 0];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VideoFrameLike } from '../../types/media';
|
|
2
|
+
export declare const createBlurRenderer: (processor: OffscreenCanvas, device: GPUDevice, presentationFormat: GPUTextureFormat, isContextLost: () => boolean) => {
|
|
3
|
+
render: (frame: VideoFrameLike, pass: number) => GPUTexture;
|
|
4
|
+
release: () => void;
|
|
5
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { assert } from '@pexip/utils';
|
|
2
|
+
import { RENDERING_EVENTS } from '../../constants';
|
|
3
|
+
import blurExample from './shaders/dualBlurShader.wgsl';
|
|
4
|
+
import { clamping } from '../../utils';
|
|
5
|
+
import { RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE } from './constants';
|
|
6
|
+
const clampBlurAmount = clamping(1, 9);
|
|
7
|
+
export const createBlurRenderer = (processor, device, presentationFormat, isContextLost) => {
|
|
8
|
+
const context = processor.getContext('webgpu');
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw Error('WebGPU not supported on the canvas.');
|
|
11
|
+
}
|
|
12
|
+
context.configure({
|
|
13
|
+
device,
|
|
14
|
+
format: presentationFormat,
|
|
15
|
+
alphaMode: 'premultiplied',
|
|
16
|
+
});
|
|
17
|
+
// Helper to create a texture of given size
|
|
18
|
+
const createTexture = (width, height) => device.createTexture({
|
|
19
|
+
format: presentationFormat,
|
|
20
|
+
size: [width, height],
|
|
21
|
+
usage: GPUTextureUsage.TEXTURE_BINDING |
|
|
22
|
+
GPUTextureUsage.COPY_DST |
|
|
23
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
24
|
+
});
|
|
25
|
+
// Helper to create a uniform buffer to flip downsampling vs upsampling
|
|
26
|
+
const createFlipBuffer = (value) => {
|
|
27
|
+
const buffer = device.createBuffer({
|
|
28
|
+
size: 4,
|
|
29
|
+
mappedAtCreation: true,
|
|
30
|
+
usage: GPUBufferUsage.UNIFORM,
|
|
31
|
+
});
|
|
32
|
+
new Uint32Array(buffer.getMappedRange())[0] = value;
|
|
33
|
+
buffer.unmap();
|
|
34
|
+
return buffer;
|
|
35
|
+
};
|
|
36
|
+
const sampler = device.createSampler({
|
|
37
|
+
magFilter: 'linear',
|
|
38
|
+
minFilter: 'linear',
|
|
39
|
+
});
|
|
40
|
+
const shaderModule = device.createShaderModule({ code: blurExample });
|
|
41
|
+
const pipeline = device.createRenderPipeline({
|
|
42
|
+
label: 'dual blur render pipeline',
|
|
43
|
+
layout: 'auto',
|
|
44
|
+
vertex: {
|
|
45
|
+
module: shaderModule,
|
|
46
|
+
entryPoint: 'vert_main',
|
|
47
|
+
buffers: [],
|
|
48
|
+
},
|
|
49
|
+
fragment: {
|
|
50
|
+
module: shaderModule,
|
|
51
|
+
entryPoint: 'frag_main',
|
|
52
|
+
targets: [{ format: presentationFormat }],
|
|
53
|
+
},
|
|
54
|
+
primitive: { topology: 'triangle-list' },
|
|
55
|
+
});
|
|
56
|
+
// --- State for persistent textures and buffers ---
|
|
57
|
+
let blurTextures = [];
|
|
58
|
+
let lastWidth = -1;
|
|
59
|
+
let lastHeight = -1;
|
|
60
|
+
// Helper to (re)generate textures if needed
|
|
61
|
+
function ensureBlurTextures(pass, width, height) {
|
|
62
|
+
const length = clampBlurAmount(pass + 1);
|
|
63
|
+
let needsUpdate = false;
|
|
64
|
+
if (blurTextures.length !== length ||
|
|
65
|
+
lastWidth !== width ||
|
|
66
|
+
lastHeight !== height) {
|
|
67
|
+
// Destroy old textures
|
|
68
|
+
for (const t of blurTextures) {
|
|
69
|
+
t.texture.destroy();
|
|
70
|
+
}
|
|
71
|
+
blurTextures = [];
|
|
72
|
+
for (let i = 0; i < length; i++) {
|
|
73
|
+
const w = Math.max(Math.trunc(width / 2 ** i), 1);
|
|
74
|
+
const h = Math.max(Math.trunc(height / 2 ** i), 1);
|
|
75
|
+
blurTextures.push({
|
|
76
|
+
texture: createTexture(w, h),
|
|
77
|
+
width: w,
|
|
78
|
+
height: h,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
lastWidth = width;
|
|
82
|
+
lastHeight = height;
|
|
83
|
+
needsUpdate = true;
|
|
84
|
+
}
|
|
85
|
+
return needsUpdate;
|
|
86
|
+
}
|
|
87
|
+
const flipDownBuffer = createFlipBuffer(0); // create once
|
|
88
|
+
const flipUpBuffer = createFlipBuffer(1); // create once
|
|
89
|
+
return {
|
|
90
|
+
render: (frame, pass) => {
|
|
91
|
+
assert(!isContextLost(), RENDERING_EVENTS.ContextLost);
|
|
92
|
+
// Ensure textures and buffers are up-to-date
|
|
93
|
+
ensureBlurTextures(pass, processor.width, processor.height);
|
|
94
|
+
// Upload frame to the largest texture (level 0)
|
|
95
|
+
const largestTexture = blurTextures[0];
|
|
96
|
+
assert(largestTexture);
|
|
97
|
+
device.queue.copyExternalImageToTexture({ source: frame.frame, flipY: false }, { texture: largestTexture.texture }, { width: largestTexture.width, height: largestTexture.height });
|
|
98
|
+
// Downsampling passes
|
|
99
|
+
for (let i = 1; i < blurTextures.length; i++) {
|
|
100
|
+
const src = blurTextures[i - 1];
|
|
101
|
+
assert(src);
|
|
102
|
+
const dst = blurTextures[i];
|
|
103
|
+
assert(dst);
|
|
104
|
+
const bindGroup = device.createBindGroup({
|
|
105
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
106
|
+
entries: [
|
|
107
|
+
{ binding: 0, resource: sampler },
|
|
108
|
+
{ binding: 1, resource: src.texture.createView() },
|
|
109
|
+
{ binding: 2, resource: { buffer: flipDownBuffer } },
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
const encoder = device.createCommandEncoder();
|
|
113
|
+
const passEncoder = encoder.beginRenderPass({
|
|
114
|
+
colorAttachments: [
|
|
115
|
+
{
|
|
116
|
+
view: dst.texture.createView(),
|
|
117
|
+
loadOp: 'clear',
|
|
118
|
+
storeOp: 'store',
|
|
119
|
+
clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
passEncoder.setPipeline(pipeline);
|
|
124
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
125
|
+
passEncoder.draw(6);
|
|
126
|
+
passEncoder.end();
|
|
127
|
+
device.queue.submit([encoder.finish()]);
|
|
128
|
+
}
|
|
129
|
+
// Upsampling passes
|
|
130
|
+
for (let i = blurTextures.length - 2; i >= 0; i--) {
|
|
131
|
+
const src = blurTextures[i + 1];
|
|
132
|
+
assert(src);
|
|
133
|
+
const dst = blurTextures[i];
|
|
134
|
+
assert(dst);
|
|
135
|
+
const bindGroup = device.createBindGroup({
|
|
136
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
137
|
+
entries: [
|
|
138
|
+
{ binding: 0, resource: sampler },
|
|
139
|
+
{ binding: 1, resource: src.texture.createView() },
|
|
140
|
+
{ binding: 2, resource: { buffer: flipUpBuffer } },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
const encoder = device.createCommandEncoder();
|
|
144
|
+
const passEncoder = encoder.beginRenderPass({
|
|
145
|
+
colorAttachments: [
|
|
146
|
+
{
|
|
147
|
+
view: dst.texture.createView(),
|
|
148
|
+
loadOp: 'clear',
|
|
149
|
+
storeOp: 'store',
|
|
150
|
+
clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
passEncoder.setPipeline(pipeline);
|
|
155
|
+
passEncoder.setBindGroup(0, bindGroup);
|
|
156
|
+
passEncoder.draw(6);
|
|
157
|
+
passEncoder.end();
|
|
158
|
+
device.queue.submit([encoder.finish()]);
|
|
159
|
+
}
|
|
160
|
+
// Final pass: draw to canvas
|
|
161
|
+
const finalTexture = blurTextures[0];
|
|
162
|
+
assert(finalTexture);
|
|
163
|
+
const finalBindGroup = device.createBindGroup({
|
|
164
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
165
|
+
entries: [
|
|
166
|
+
{ binding: 0, resource: sampler },
|
|
167
|
+
{
|
|
168
|
+
binding: 1,
|
|
169
|
+
resource: finalTexture.texture.createView(),
|
|
170
|
+
},
|
|
171
|
+
{ binding: 2, resource: { buffer: flipUpBuffer } },
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
const encoder = device.createCommandEncoder();
|
|
175
|
+
const passEncoder = encoder.beginRenderPass({
|
|
176
|
+
colorAttachments: [
|
|
177
|
+
{
|
|
178
|
+
view: context.getCurrentTexture().createView(),
|
|
179
|
+
loadOp: 'clear',
|
|
180
|
+
storeOp: 'store',
|
|
181
|
+
clearValue: RENDER_PASS_COLOR_ATTACHMENT_CLEAR_VALUE,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
passEncoder.setPipeline(pipeline);
|
|
186
|
+
passEncoder.setBindGroup(0, finalBindGroup);
|
|
187
|
+
passEncoder.draw(6);
|
|
188
|
+
passEncoder.end();
|
|
189
|
+
device.queue.submit([encoder.finish()]);
|
|
190
|
+
return finalTexture.texture;
|
|
191
|
+
},
|
|
192
|
+
release: () => {
|
|
193
|
+
for (const t of blurTextures) {
|
|
194
|
+
t.texture.destroy();
|
|
195
|
+
}
|
|
196
|
+
blurTextures = [];
|
|
197
|
+
flipDownBuffer.destroy();
|
|
198
|
+
flipUpBuffer.destroy();
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
};
|