@yume-chan/scrcpy-decoder-webcodecs 2.5.3 → 3.0.0-beta.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/LICENSE +1 -1
- package/README.md +1 -1
- package/esm/video/codec/av1.d.ts +4 -6
- package/esm/video/codec/av1.d.ts.map +1 -1
- package/esm/video/codec/av1.js +29 -95
- package/esm/video/codec/av1.js.map +1 -1
- package/esm/video/codec/h264.d.ts +4 -6
- package/esm/video/codec/h264.d.ts.map +1 -1
- package/esm/video/codec/h264.js +11 -21
- package/esm/video/codec/h264.js.map +1 -1
- package/esm/video/codec/h265.d.ts +4 -6
- package/esm/video/codec/h265.d.ts.map +1 -1
- package/esm/video/codec/h265.js +13 -27
- package/esm/video/codec/h265.js.map +1 -1
- package/esm/video/codec/h26x.d.ts +9 -6
- package/esm/video/codec/h26x.d.ts.map +1 -1
- package/esm/video/codec/h26x.js +25 -69
- package/esm/video/codec/h26x.js.map +1 -1
- package/esm/video/codec/index.d.ts +0 -1
- package/esm/video/codec/index.d.ts.map +1 -1
- package/esm/video/codec/index.js +0 -1
- package/esm/video/codec/index.js.map +1 -1
- package/esm/video/codec/type.d.ts +23 -8
- package/esm/video/codec/type.d.ts.map +1 -1
- package/esm/video/decoder.d.ts +62 -15
- package/esm/video/decoder.d.ts.map +1 -1
- package/esm/video/decoder.js +170 -134
- package/esm/video/decoder.js.map +1 -1
- package/esm/video/index.d.ts +1 -0
- package/esm/video/index.d.ts.map +1 -1
- package/esm/video/index.js +1 -0
- package/esm/video/index.js.map +1 -1
- package/esm/video/render/auto-canvas.d.ts +26 -0
- package/esm/video/render/auto-canvas.d.ts.map +1 -0
- package/esm/video/render/auto-canvas.js +80 -0
- package/esm/video/render/auto-canvas.js.map +1 -0
- package/esm/video/render/bitmap.d.ts +2 -2
- package/esm/video/render/bitmap.d.ts.map +1 -1
- package/esm/video/render/bitmap.js +18 -8
- package/esm/video/render/bitmap.js.map +1 -1
- package/esm/video/render/canvas.d.ts +38 -4
- package/esm/video/render/canvas.d.ts.map +1 -1
- package/esm/video/render/canvas.js +97 -10
- package/esm/video/render/canvas.js.map +1 -1
- package/esm/video/render/flow-control.d.ts +32 -0
- package/esm/video/render/flow-control.d.ts.map +1 -0
- package/esm/video/render/flow-control.js +139 -0
- package/esm/video/render/flow-control.js.map +1 -0
- package/esm/video/render/index.d.ts +2 -0
- package/esm/video/render/index.d.ts.map +1 -1
- package/esm/video/render/index.js +2 -0
- package/esm/video/render/index.js.map +1 -1
- package/esm/video/render/insertable-stream.d.ts +15 -3
- package/esm/video/render/insertable-stream.d.ts.map +1 -1
- package/esm/video/render/insertable-stream.js +29 -11
- package/esm/video/render/insertable-stream.js.map +1 -1
- package/esm/video/render/redraw.d.ts +31 -0
- package/esm/video/render/redraw.d.ts.map +1 -0
- package/esm/video/render/redraw.js +75 -0
- package/esm/video/render/redraw.js.map +1 -0
- package/esm/video/render/type.d.ts +8 -2
- package/esm/video/render/type.d.ts.map +1 -1
- package/esm/video/render/webgl.d.ts +38 -5
- package/esm/video/render/webgl.d.ts.map +1 -1
- package/esm/video/render/webgl.js +228 -59
- package/esm/video/render/webgl.js.map +1 -1
- package/esm/video/utils/index.d.ts +4 -0
- package/esm/video/utils/index.d.ts.map +1 -0
- package/esm/video/utils/index.js +4 -0
- package/esm/video/utils/index.js.map +1 -0
- package/esm/video/utils/snapshot.d.ts +2 -0
- package/esm/video/utils/snapshot.d.ts.map +1 -0
- package/esm/video/utils/snapshot.js +18 -0
- package/esm/video/utils/snapshot.js.map +1 -0
- package/esm/video/utils/timestamp.d.ts +19 -0
- package/esm/video/utils/timestamp.d.ts.map +1 -0
- package/esm/video/utils/timestamp.js +87 -0
- package/esm/video/utils/timestamp.js.map +1 -0
- package/esm/video/utils/video-decoder-stream.d.ts +30 -0
- package/esm/video/utils/video-decoder-stream.d.ts.map +1 -0
- package/esm/video/utils/video-decoder-stream.js +284 -0
- package/esm/video/utils/video-decoder-stream.js.map +1 -0
- package/package.json +12 -11
- package/src/video/codec/av1.ts +41 -131
- package/src/video/codec/h264.ts +15 -37
- package/src/video/codec/h265.ts +16 -45
- package/src/video/codec/h26x.ts +45 -96
- package/src/video/codec/index.ts +0 -1
- package/src/video/codec/type.ts +32 -12
- package/src/video/decoder.ts +203 -165
- package/src/video/index.ts +1 -0
- package/src/video/render/auto-canvas.ts +105 -0
- package/src/video/render/bitmap.ts +23 -9
- package/src/video/render/canvas.ts +157 -12
- package/src/video/render/flow-control.ts +182 -0
- package/src/video/render/index.ts +2 -0
- package/src/video/render/insertable-stream.ts +50 -10
- package/src/video/render/redraw.ts +88 -0
- package/src/video/render/type.ts +10 -2
- package/src/video/render/webgl.ts +316 -80
- package/src/video/utils/index.ts +3 -0
- package/src/video/utils/snapshot.ts +22 -0
- package/src/video/utils/timestamp.ts +108 -0
- package/src/video/utils/video-decoder-stream.ts +406 -0
- package/tsconfig.build.json +0 -14
- package/CHANGELOG.md +0 -140
- package/esm/video/codec/utils.d.ts +0 -4
- package/esm/video/codec/utils.d.ts.map +0 -1
- package/esm/video/codec/utils.js +0 -10
- package/esm/video/codec/utils.js.map +0 -1
- package/esm/video/snapshot.d.ts +0 -6
- package/esm/video/snapshot.d.ts.map +0 -1
- package/esm/video/snapshot.js +0 -41
- package/esm/video/snapshot.js.map +0 -1
- package/src/video/codec/utils.ts +0 -11
- package/src/video/snapshot.ts +0 -41
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -1,59 +1,189 @@
|
|
|
1
|
-
|
|
1
|
+
// cspell: ignore highp
|
|
2
|
+
// cspell: ignore mediump
|
|
3
|
+
|
|
4
|
+
import { StickyEventEmitter } from "@yume-chan/event";
|
|
5
|
+
import {
|
|
6
|
+
glCreateContext,
|
|
7
|
+
glIsSupported,
|
|
8
|
+
glLoseContext,
|
|
9
|
+
} from "@yume-chan/scrcpy-decoder-shared";
|
|
2
10
|
|
|
3
11
|
import { CanvasVideoFrameRenderer } from "./canvas.js";
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
canvas.getContext("webgl2", attributes) ||
|
|
23
|
-
canvas.getContext("webgl", attributes)
|
|
13
|
+
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
|
|
14
|
+
const shader = gl.createShader(type)!;
|
|
15
|
+
gl.shaderSource(shader, source);
|
|
16
|
+
gl.compileShader(shader);
|
|
17
|
+
return shader;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createProgram(
|
|
21
|
+
gl: WebGLRenderingContext,
|
|
22
|
+
vertexShaderSource: string,
|
|
23
|
+
fragmentShaderSource: string,
|
|
24
|
+
) {
|
|
25
|
+
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
|
|
26
|
+
const fragmentShader = createShader(
|
|
27
|
+
gl,
|
|
28
|
+
gl.FRAGMENT_SHADER,
|
|
29
|
+
fragmentShaderSource,
|
|
24
30
|
);
|
|
31
|
+
|
|
32
|
+
const program = gl.createProgram();
|
|
33
|
+
gl.attachShader(program, vertexShader);
|
|
34
|
+
gl.attachShader(program, fragmentShader);
|
|
35
|
+
gl.linkProgram(program);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
39
|
+
return program;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Don't check shader compile status unless linking fails
|
|
43
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#dont_check_shader_compile_status_unless_linking_fails
|
|
44
|
+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
45
|
+
throw new Error(gl.getShaderInfoLog(vertexShader)!);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
49
|
+
throw new Error(gl.getShaderInfoLog(fragmentShader)!);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(gl.getProgramInfoLog(program)!);
|
|
53
|
+
} finally {
|
|
54
|
+
// Delete objects eagerly
|
|
55
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#delete_objects_eagerly
|
|
56
|
+
gl.deleteShader(vertexShader);
|
|
57
|
+
gl.deleteShader(fragmentShader);
|
|
58
|
+
}
|
|
25
59
|
}
|
|
26
60
|
|
|
27
|
-
export class WebGLVideoFrameRenderer extends CanvasVideoFrameRenderer {
|
|
28
|
-
static
|
|
61
|
+
export class WebGLVideoFrameRenderer extends CanvasVideoFrameRenderer<WebGLVideoFrameRenderer.Options> {
|
|
62
|
+
static VertexShader = `
|
|
29
63
|
attribute vec2 xy;
|
|
30
64
|
|
|
31
65
|
varying highp vec2 uv;
|
|
32
66
|
|
|
33
67
|
void main(void) {
|
|
34
68
|
gl_Position = vec4(xy, 0.0, 1.0);
|
|
69
|
+
|
|
35
70
|
// Map vertex coordinates (-1 to +1) to UV coordinates (0 to 1).
|
|
71
|
+
uv = xy * 0.5 + 0.5;
|
|
36
72
|
// UV coordinates are Y-flipped relative to vertex coordinates.
|
|
37
|
-
uv =
|
|
73
|
+
uv.y = 1.0 - uv.y;
|
|
38
74
|
}
|
|
39
75
|
`;
|
|
40
76
|
|
|
41
|
-
static
|
|
42
|
-
|
|
77
|
+
static FragmentShader = `
|
|
78
|
+
precision mediump float;
|
|
43
79
|
|
|
44
|
-
uniform sampler2D
|
|
80
|
+
uniform sampler2D source;
|
|
81
|
+
uniform vec2 texelSize;
|
|
82
|
+
uniform float zoom;
|
|
45
83
|
|
|
46
|
-
|
|
47
|
-
|
|
84
|
+
varying vec2 uv;
|
|
85
|
+
|
|
86
|
+
vec4 tent4(vec2 uv) {
|
|
87
|
+
vec2 dx = vec2(texelSize.x, 0.0);
|
|
88
|
+
vec2 dy = vec2(0.0, texelSize.y);
|
|
89
|
+
|
|
90
|
+
vec4 c0 = texture2D(source, uv);
|
|
91
|
+
vec4 c1 = texture2D(source, uv + dx);
|
|
92
|
+
vec4 c2 = texture2D(source, uv + dy);
|
|
93
|
+
vec4 c3 = texture2D(source, uv + dx + dy);
|
|
94
|
+
|
|
95
|
+
return 0.25 * (c0 + c1 + c2 + c3);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
float mnWeight(float x) {
|
|
99
|
+
x = abs(x);
|
|
100
|
+
float x2 = x * x;
|
|
101
|
+
float x3 = x2 * x;
|
|
102
|
+
|
|
103
|
+
if (x < 1.0) {
|
|
104
|
+
return (1.0/6.0) * ((12.0 - 9.0 * (1.0/3.0) - 6.0 * (1.0/3.0)) * x3 +
|
|
105
|
+
(-18.0 + 12.0 * (1.0/3.0) + 6.0 * (1.0/3.0)) * x2 +
|
|
106
|
+
(6.0 - 2.0 * (1.0/3.0)));
|
|
107
|
+
} else if (x < 2.0) {
|
|
108
|
+
return (1.0/6.0) * ((- (1.0/3.0) - 6.0 * (1.0/3.0)) * x3 +
|
|
109
|
+
((6.0 * (1.0/3.0) + 30.0 * (1.0/3.0)) * x2 +
|
|
110
|
+
(-12.0 * (1.0/3.0) - 48.0 * (1.0/3.0)) * x +
|
|
111
|
+
(8.0 * (1.0/3.0) + 24.0 * (1.0/3.0))));
|
|
112
|
+
}
|
|
113
|
+
return 0.0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
vec4 bicubicMN(vec2 uv, vec2 texelSize) {
|
|
117
|
+
vec2 texCoord = uv / texelSize;
|
|
118
|
+
vec2 base = floor(texCoord - 0.5);
|
|
119
|
+
vec2 f = texCoord - base - 0.5;
|
|
120
|
+
|
|
121
|
+
vec4 sum = vec4(0.0);
|
|
122
|
+
float total = 0.0;
|
|
123
|
+
|
|
124
|
+
for (int j = -1; j <= 2; j++) {
|
|
125
|
+
float wy = mnWeight(float(j) - f.y);
|
|
126
|
+
for (int i = -1; i <= 2; i++) {
|
|
127
|
+
float wx = mnWeight(float(i) - f.x);
|
|
128
|
+
float w = wx * wy;
|
|
129
|
+
|
|
130
|
+
vec2 coord = (base + vec2(float(i), float(j)) + 0.5) * texelSize;
|
|
131
|
+
sum += texture2D(source, coord) * w;
|
|
132
|
+
total += w;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return sum / total;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
void main() {
|
|
139
|
+
if (zoom > 0.95) {
|
|
140
|
+
gl_FragColor = texture2D(source, uv);
|
|
141
|
+
}
|
|
142
|
+
else if (zoom > 0.5) {
|
|
143
|
+
gl_FragColor = bicubicMN(uv, texelSize);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
gl_FragColor = tent4(uv);
|
|
147
|
+
}
|
|
48
148
|
}
|
|
49
149
|
`;
|
|
50
150
|
|
|
151
|
+
/**
|
|
152
|
+
* A single oversized triangle that covers the entire canvas.
|
|
153
|
+
*/
|
|
154
|
+
static Vertices = new Float32Array([-1.0, -1.0, 3.0, -1.0, -1.0, 3.0]);
|
|
155
|
+
|
|
51
156
|
static get isSupported() {
|
|
52
|
-
|
|
53
|
-
|
|
157
|
+
return glIsSupported({
|
|
158
|
+
// Disallow software rendering.
|
|
159
|
+
// `ImageBitmapRenderingContext` is faster than software-based WebGL.
|
|
160
|
+
failIfMajorPerformanceCaveat: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
override get type() {
|
|
165
|
+
return "hardware" as const;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#contextLostTimer: number | undefined;
|
|
169
|
+
#onContextLost = new StickyEventEmitter<void>();
|
|
170
|
+
/**
|
|
171
|
+
* Fired when the WebGL context is lost and cannot be restored.
|
|
172
|
+
*
|
|
173
|
+
* The current renderer will be automatically disposed.
|
|
174
|
+
*/
|
|
175
|
+
get onContextLost() {
|
|
176
|
+
return this.#onContextLost.event;
|
|
54
177
|
}
|
|
55
178
|
|
|
56
179
|
#context: WebGLRenderingContext;
|
|
180
|
+
#program!: WebGLProgram;
|
|
181
|
+
#vertexBuffer!: WebGLBuffer;
|
|
182
|
+
#texture!: WebGLTexture;
|
|
183
|
+
#texelSizeLocation!: WebGLUniformLocation;
|
|
184
|
+
#zoomLocation!: WebGLUniformLocation;
|
|
185
|
+
|
|
186
|
+
#abortController = new AbortController();
|
|
57
187
|
|
|
58
188
|
/**
|
|
59
189
|
* Create a new WebGL frame renderer.
|
|
@@ -62,83 +192,189 @@ export class WebGLVideoFrameRenderer extends CanvasVideoFrameRenderer {
|
|
|
62
192
|
* Whether to allow capturing the canvas content using APIs like `readPixels` and `toDataURL`.
|
|
63
193
|
* Enable this option may reduce performance.
|
|
64
194
|
*/
|
|
65
|
-
constructor(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
195
|
+
constructor(options?: WebGLVideoFrameRenderer.Options) {
|
|
196
|
+
super((frame): undefined => {
|
|
197
|
+
const gl = this.#context;
|
|
198
|
+
if (gl.isContextLost()) {
|
|
199
|
+
// The base class will close the frame.
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
gl.texImage2D(
|
|
204
|
+
gl.TEXTURE_2D,
|
|
205
|
+
0,
|
|
206
|
+
gl.RGBA,
|
|
207
|
+
gl.RGBA,
|
|
208
|
+
gl.UNSIGNED_BYTE,
|
|
209
|
+
frame,
|
|
210
|
+
);
|
|
70
211
|
|
|
71
|
-
|
|
212
|
+
gl.uniform2f(
|
|
213
|
+
this.#texelSizeLocation,
|
|
214
|
+
1.0 / frame.codedWidth,
|
|
215
|
+
1.0 / frame.codedHeight,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
gl.uniform1f(
|
|
219
|
+
this.#zoomLocation,
|
|
220
|
+
this.canvas.width / frame.codedWidth,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
224
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
225
|
+
|
|
226
|
+
gl.flush();
|
|
227
|
+
}, options);
|
|
228
|
+
|
|
229
|
+
const gl = glCreateContext(this.canvas, {
|
|
230
|
+
// Low-power GPU should be enough for video rendering.
|
|
231
|
+
// Note that `OffscreenCanvas` created in Web Workers can only use `low-power` anyway.
|
|
232
|
+
powerPreference: "low-power",
|
|
233
|
+
// Avoid alpha:false, which can be expensive
|
|
234
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#avoid_alphafalse_which_can_be_expensive
|
|
235
|
+
alpha: true,
|
|
236
|
+
// Disallow software rendering.
|
|
237
|
+
// `ImageBitmapRenderingContext` is faster than software-based WebGL.
|
|
238
|
+
failIfMajorPerformanceCaveat: true,
|
|
239
|
+
preserveDrawingBuffer: !!this.options?.enableCapture,
|
|
240
|
+
// Enable desynchronized mode when not capturing to reduce latency.
|
|
241
|
+
desynchronized: !this.options?.enableCapture,
|
|
242
|
+
antialias: false,
|
|
243
|
+
depth: false,
|
|
244
|
+
premultipliedAlpha: true,
|
|
245
|
+
stencil: false,
|
|
246
|
+
});
|
|
72
247
|
if (!gl) {
|
|
73
|
-
throw new Error("WebGL not supported");
|
|
248
|
+
throw new Error("WebGL not supported, check `isSupported` first");
|
|
74
249
|
}
|
|
75
250
|
this.#context = gl;
|
|
76
251
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
252
|
+
this.#initialize();
|
|
253
|
+
|
|
254
|
+
this.canvas.addEventListener(
|
|
255
|
+
"webglcontextlost",
|
|
256
|
+
(e) => {
|
|
257
|
+
if (options?.contextRestoreTimeout === 0) {
|
|
258
|
+
this.#onContextLost.fire();
|
|
259
|
+
this.dispose();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Notify WebGL we want to handle context restoration
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
|
|
266
|
+
this.#contextLostTimer = globalThis.setTimeout(() => {
|
|
267
|
+
this.#onContextLost.fire();
|
|
268
|
+
this.dispose();
|
|
269
|
+
}, options?.contextRestoreTimeout ?? 3000);
|
|
270
|
+
},
|
|
271
|
+
{ signal: this.#abortController.signal },
|
|
81
272
|
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
273
|
+
this.canvas.addEventListener(
|
|
274
|
+
"webglcontextrestored",
|
|
275
|
+
() => {
|
|
276
|
+
if (this.#onContextLost.hasValue) {
|
|
277
|
+
// Don't restore context if `onContextLost` has been fired,
|
|
278
|
+
// which means the caller will handle the context loss.
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
86
281
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
282
|
+
globalThis.clearTimeout(this.#contextLostTimer);
|
|
283
|
+
this.#contextLostTimer = undefined;
|
|
284
|
+
|
|
285
|
+
this.#initialize();
|
|
286
|
+
void this.redraw();
|
|
287
|
+
},
|
|
288
|
+
{ signal: this.#abortController.signal },
|
|
91
289
|
);
|
|
92
|
-
|
|
93
|
-
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
94
|
-
throw new Error(gl.getShaderInfoLog(fragmentShader)!);
|
|
95
|
-
}
|
|
290
|
+
}
|
|
96
291
|
|
|
97
|
-
|
|
98
|
-
gl
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
292
|
+
#initialize() {
|
|
293
|
+
const gl = this.#context;
|
|
294
|
+
|
|
295
|
+
this.#program = createProgram(
|
|
296
|
+
gl,
|
|
297
|
+
WebGLVideoFrameRenderer.VertexShader,
|
|
298
|
+
WebGLVideoFrameRenderer.FragmentShader,
|
|
299
|
+
);
|
|
300
|
+
gl.useProgram(this.#program);
|
|
105
301
|
|
|
106
302
|
// Vertex coordinates, clockwise from bottom-left.
|
|
107
|
-
|
|
108
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
303
|
+
this.#vertexBuffer = gl.createBuffer();
|
|
304
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.#vertexBuffer);
|
|
109
305
|
gl.bufferData(
|
|
110
306
|
gl.ARRAY_BUFFER,
|
|
111
|
-
|
|
307
|
+
WebGLVideoFrameRenderer.Vertices,
|
|
112
308
|
gl.STATIC_DRAW,
|
|
113
309
|
);
|
|
114
310
|
|
|
115
|
-
const xyLocation = gl.getAttribLocation(
|
|
311
|
+
const xyLocation = gl.getAttribLocation(this.#program, "xy");
|
|
116
312
|
gl.vertexAttribPointer(xyLocation, 2, gl.FLOAT, false, 0, 0);
|
|
117
313
|
gl.enableVertexAttribArray(xyLocation);
|
|
118
314
|
|
|
315
|
+
this.#texelSizeLocation = gl.getUniformLocation(
|
|
316
|
+
this.#program,
|
|
317
|
+
"texelSize",
|
|
318
|
+
)!;
|
|
319
|
+
this.#zoomLocation = gl.getUniformLocation(this.#program, "zoom")!;
|
|
320
|
+
|
|
119
321
|
// Create one texture to upload frames to.
|
|
120
|
-
|
|
121
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
322
|
+
this.#texture = gl.createTexture();
|
|
323
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#texture);
|
|
122
324
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
123
325
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
124
326
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
125
327
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
126
328
|
}
|
|
127
329
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
330
|
+
override async snapshot(
|
|
331
|
+
options?: ImageEncodeOptions,
|
|
332
|
+
): Promise<Blob | undefined> {
|
|
333
|
+
if (!this.options?.enableCapture) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
return super.snapshot(options);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
override dispose(): undefined {
|
|
340
|
+
globalThis.clearTimeout(this.#contextLostTimer);
|
|
341
|
+
this.#onContextLost.dispose();
|
|
342
|
+
|
|
343
|
+
this.#abortController.abort();
|
|
344
|
+
|
|
345
|
+
this.#context.deleteBuffer(this.#vertexBuffer);
|
|
346
|
+
this.#context.deleteTexture(this.#texture);
|
|
347
|
+
this.#context.deleteProgram(this.#program);
|
|
348
|
+
|
|
349
|
+
if (!this.#context.isContextLost()) {
|
|
350
|
+
// Lose contexts eagerly
|
|
351
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#lose_contexts_eagerly
|
|
352
|
+
glLoseContext(this.#context);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
super.dispose();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
138
358
|
|
|
139
|
-
|
|
140
|
-
|
|
359
|
+
export namespace WebGLVideoFrameRenderer {
|
|
360
|
+
export interface Options extends CanvasVideoFrameRenderer.Options {
|
|
361
|
+
/**
|
|
362
|
+
* Whether to allow capturing the canvas content using APIs
|
|
363
|
+
* like `readPixels` and `toDataURL`.
|
|
364
|
+
*
|
|
365
|
+
* Enabling this option may reduce performance.
|
|
366
|
+
*/
|
|
367
|
+
enableCapture?: boolean;
|
|
141
368
|
|
|
142
|
-
|
|
369
|
+
/**
|
|
370
|
+
* The timeout in milliseconds to wait for context restoration
|
|
371
|
+
* before firing `onContextLost`.
|
|
372
|
+
*
|
|
373
|
+
* When set to 0, also disables automatic context restoration,
|
|
374
|
+
* `onContextLost` will be fired immediately when the context is lost.
|
|
375
|
+
*
|
|
376
|
+
* Default is 3000.
|
|
377
|
+
*/
|
|
378
|
+
contextRestoreTimeout?: number;
|
|
143
379
|
}
|
|
144
380
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function canvasToBlob(
|
|
2
|
+
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
3
|
+
options?: ImageEncodeOptions,
|
|
4
|
+
) {
|
|
5
|
+
if (canvas instanceof OffscreenCanvas) {
|
|
6
|
+
return canvas.convertToBlob(options);
|
|
7
|
+
} else {
|
|
8
|
+
return new Promise<Blob>((resolve, reject) => {
|
|
9
|
+
canvas.toBlob(
|
|
10
|
+
(blob) => {
|
|
11
|
+
if (!blob) {
|
|
12
|
+
reject(new Error("Failed to convert canvas to blob"));
|
|
13
|
+
} else {
|
|
14
|
+
resolve(blob);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
options?.type,
|
|
18
|
+
options?.quality,
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ScrcpyVideoDecoderPauseController } from "@yume-chan/scrcpy-decoder-shared";
|
|
2
|
+
import { TransformStream } from "@yume-chan/stream-extra";
|
|
3
|
+
|
|
4
|
+
import type { CodecTransformStream } from "../codec/type.js";
|
|
5
|
+
|
|
6
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gets the next presentable double-precision floating point value greater than x
|
|
10
|
+
* @param x A number. The caller must ensure that x is not NaN or Infinity
|
|
11
|
+
* @returns The next presentable double-precision floating point value
|
|
12
|
+
*/
|
|
13
|
+
function nextUp(x: number) {
|
|
14
|
+
if (x === 0) return Number.MIN_VALUE;
|
|
15
|
+
|
|
16
|
+
// Write the number as a float64
|
|
17
|
+
view.setFloat64(0, x, false);
|
|
18
|
+
|
|
19
|
+
let bits = view.getBigUint64(0, false);
|
|
20
|
+
|
|
21
|
+
// If x > 0, increment bits; if x < 0, decrement bits
|
|
22
|
+
bits += x > 0 ? 1n : -1n;
|
|
23
|
+
|
|
24
|
+
view.setBigUint64(0, bits, false);
|
|
25
|
+
return view.getFloat64(0, false);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let prevValue = 0;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets current time.
|
|
32
|
+
*
|
|
33
|
+
* The returned value is guaranteed to be monotonically increasing.
|
|
34
|
+
*/
|
|
35
|
+
export function increasingNow() {
|
|
36
|
+
let now = performance.now();
|
|
37
|
+
if (now <= prevValue) {
|
|
38
|
+
now = nextUp(prevValue);
|
|
39
|
+
}
|
|
40
|
+
prevValue = now;
|
|
41
|
+
return now;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class TimestampTransforms {
|
|
45
|
+
/**
|
|
46
|
+
* Timestamp of the last frame to be skipped by pause controller.
|
|
47
|
+
*/
|
|
48
|
+
#skipFramesUntil = -1;
|
|
49
|
+
|
|
50
|
+
#addTimestamp = new TransformStream<
|
|
51
|
+
ScrcpyVideoDecoderPauseController.Output,
|
|
52
|
+
CodecTransformStream.Input
|
|
53
|
+
>({
|
|
54
|
+
transform: (packet, controller) => {
|
|
55
|
+
if (packet.type === "configuration") {
|
|
56
|
+
controller.enqueue(packet);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Use `timestamp` to convey `skipRendering` to later step
|
|
61
|
+
// and track total decoding time
|
|
62
|
+
const timestamp = increasingNow();
|
|
63
|
+
|
|
64
|
+
if (packet.skipRendering) {
|
|
65
|
+
this.#skipFramesUntil = timestamp;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
controller.enqueue({
|
|
69
|
+
...packet,
|
|
70
|
+
timestamp,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
get addTimestamp() {
|
|
75
|
+
return this.#addTimestamp;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// This is not in `VideoDecoderStream` because
|
|
79
|
+
// this time includes all pre-processing time,
|
|
80
|
+
// and requires `EncodedVideoChunk.timestamp` to contain
|
|
81
|
+
// local time of when the frame is received,
|
|
82
|
+
// which is set by this class.
|
|
83
|
+
#totalDecodeTime = 0;
|
|
84
|
+
/**
|
|
85
|
+
* Gets the total time spent processing and decoding frames in milliseconds.
|
|
86
|
+
*/
|
|
87
|
+
get totalDecodeTime() {
|
|
88
|
+
return this.#totalDecodeTime;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#consumeTimestamp = new TransformStream<VideoFrame, VideoFrame>({
|
|
92
|
+
transform: (frame, controller) => {
|
|
93
|
+
// `frame.timestamp` is the same `EncodedVideoChunk.timestamp` set above
|
|
94
|
+
this.#totalDecodeTime += performance.now() - frame.timestamp;
|
|
95
|
+
|
|
96
|
+
// Don't count these frames as skipped rendering
|
|
97
|
+
if (frame.timestamp <= this.#skipFramesUntil) {
|
|
98
|
+
frame.close();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
controller.enqueue(frame);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
get consumeTimestamp() {
|
|
106
|
+
return this.#consumeTimestamp;
|
|
107
|
+
}
|
|
108
|
+
}
|