@yume-chan/scrcpy-decoder-webcodecs 2.5.0 → 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.
Files changed (117) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/esm/video/codec/av1.d.ts +4 -6
  4. package/esm/video/codec/av1.d.ts.map +1 -1
  5. package/esm/video/codec/av1.js +28 -70
  6. package/esm/video/codec/av1.js.map +1 -1
  7. package/esm/video/codec/h264.d.ts +4 -6
  8. package/esm/video/codec/h264.d.ts.map +1 -1
  9. package/esm/video/codec/h264.js +13 -25
  10. package/esm/video/codec/h264.js.map +1 -1
  11. package/esm/video/codec/h265.d.ts +4 -6
  12. package/esm/video/codec/h265.d.ts.map +1 -1
  13. package/esm/video/codec/h265.js +15 -31
  14. package/esm/video/codec/h265.js.map +1 -1
  15. package/esm/video/codec/h26x.d.ts +9 -6
  16. package/esm/video/codec/h26x.d.ts.map +1 -1
  17. package/esm/video/codec/h26x.js +25 -31
  18. package/esm/video/codec/h26x.js.map +1 -1
  19. package/esm/video/codec/index.d.ts +0 -1
  20. package/esm/video/codec/index.d.ts.map +1 -1
  21. package/esm/video/codec/index.js +0 -1
  22. package/esm/video/codec/index.js.map +1 -1
  23. package/esm/video/codec/type.d.ts +23 -8
  24. package/esm/video/codec/type.d.ts.map +1 -1
  25. package/esm/video/decoder.d.ts +62 -15
  26. package/esm/video/decoder.d.ts.map +1 -1
  27. package/esm/video/decoder.js +170 -134
  28. package/esm/video/decoder.js.map +1 -1
  29. package/esm/video/index.d.ts +1 -0
  30. package/esm/video/index.d.ts.map +1 -1
  31. package/esm/video/index.js +1 -0
  32. package/esm/video/index.js.map +1 -1
  33. package/esm/video/render/auto-canvas.d.ts +26 -0
  34. package/esm/video/render/auto-canvas.d.ts.map +1 -0
  35. package/esm/video/render/auto-canvas.js +80 -0
  36. package/esm/video/render/auto-canvas.js.map +1 -0
  37. package/esm/video/render/bitmap.d.ts +2 -2
  38. package/esm/video/render/bitmap.d.ts.map +1 -1
  39. package/esm/video/render/bitmap.js +18 -8
  40. package/esm/video/render/bitmap.js.map +1 -1
  41. package/esm/video/render/canvas.d.ts +38 -4
  42. package/esm/video/render/canvas.d.ts.map +1 -1
  43. package/esm/video/render/canvas.js +97 -10
  44. package/esm/video/render/canvas.js.map +1 -1
  45. package/esm/video/render/flow-control.d.ts +32 -0
  46. package/esm/video/render/flow-control.d.ts.map +1 -0
  47. package/esm/video/render/flow-control.js +139 -0
  48. package/esm/video/render/flow-control.js.map +1 -0
  49. package/esm/video/render/index.d.ts +2 -0
  50. package/esm/video/render/index.d.ts.map +1 -1
  51. package/esm/video/render/index.js +2 -0
  52. package/esm/video/render/index.js.map +1 -1
  53. package/esm/video/render/insertable-stream.d.ts +15 -3
  54. package/esm/video/render/insertable-stream.d.ts.map +1 -1
  55. package/esm/video/render/insertable-stream.js +29 -11
  56. package/esm/video/render/insertable-stream.js.map +1 -1
  57. package/esm/video/render/redraw.d.ts +31 -0
  58. package/esm/video/render/redraw.d.ts.map +1 -0
  59. package/esm/video/render/redraw.js +75 -0
  60. package/esm/video/render/redraw.js.map +1 -0
  61. package/esm/video/render/type.d.ts +8 -2
  62. package/esm/video/render/type.d.ts.map +1 -1
  63. package/esm/video/render/webgl.d.ts +38 -5
  64. package/esm/video/render/webgl.d.ts.map +1 -1
  65. package/esm/video/render/webgl.js +228 -59
  66. package/esm/video/render/webgl.js.map +1 -1
  67. package/esm/video/utils/index.d.ts +4 -0
  68. package/esm/video/utils/index.d.ts.map +1 -0
  69. package/esm/video/utils/index.js +4 -0
  70. package/esm/video/utils/index.js.map +1 -0
  71. package/esm/video/utils/snapshot.d.ts +2 -0
  72. package/esm/video/utils/snapshot.d.ts.map +1 -0
  73. package/esm/video/utils/snapshot.js +18 -0
  74. package/esm/video/utils/snapshot.js.map +1 -0
  75. package/esm/video/utils/timestamp.d.ts +19 -0
  76. package/esm/video/utils/timestamp.d.ts.map +1 -0
  77. package/esm/video/utils/timestamp.js +87 -0
  78. package/esm/video/utils/timestamp.js.map +1 -0
  79. package/esm/video/utils/video-decoder-stream.d.ts +30 -0
  80. package/esm/video/utils/video-decoder-stream.d.ts.map +1 -0
  81. package/esm/video/utils/video-decoder-stream.js +284 -0
  82. package/esm/video/utils/video-decoder-stream.js.map +1 -0
  83. package/package.json +12 -11
  84. package/src/video/codec/av1.ts +40 -102
  85. package/src/video/codec/h264.ts +17 -41
  86. package/src/video/codec/h265.ts +18 -49
  87. package/src/video/codec/h26x.ts +41 -36
  88. package/src/video/codec/index.ts +0 -1
  89. package/src/video/codec/type.ts +32 -12
  90. package/src/video/decoder.ts +203 -165
  91. package/src/video/index.ts +1 -0
  92. package/src/video/render/auto-canvas.ts +105 -0
  93. package/src/video/render/bitmap.ts +23 -9
  94. package/src/video/render/canvas.ts +157 -12
  95. package/src/video/render/flow-control.ts +182 -0
  96. package/src/video/render/index.ts +2 -0
  97. package/src/video/render/insertable-stream.ts +50 -10
  98. package/src/video/render/redraw.ts +88 -0
  99. package/src/video/render/type.ts +10 -2
  100. package/src/video/render/webgl.ts +316 -80
  101. package/src/video/utils/index.ts +3 -0
  102. package/src/video/utils/snapshot.ts +22 -0
  103. package/src/video/utils/timestamp.ts +108 -0
  104. package/src/video/utils/video-decoder-stream.ts +406 -0
  105. package/tsconfig.build.json +0 -14
  106. package/CHANGELOG.md +0 -140
  107. package/esm/video/codec/utils.d.ts +0 -4
  108. package/esm/video/codec/utils.d.ts.map +0 -1
  109. package/esm/video/codec/utils.js +0 -10
  110. package/esm/video/codec/utils.js.map +0 -1
  111. package/esm/video/snapshot.d.ts +0 -6
  112. package/esm/video/snapshot.d.ts.map +0 -1
  113. package/esm/video/snapshot.js +0 -41
  114. package/esm/video/snapshot.js.map +0 -1
  115. package/src/video/codec/utils.ts +0 -11
  116. package/src/video/snapshot.ts +0 -41
  117. package/tsconfig.build.tsbuildinfo +0 -1
@@ -0,0 +1,88 @@
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+ import { TaskQueue } from "@yume-chan/stream-extra";
3
+
4
+ /**
5
+ * Manages drawing and redrawing of video frames.
6
+ */
7
+ export class RedrawController {
8
+ #draw: (frame: VideoFrame) => MaybePromiseLike<undefined>;
9
+
10
+ #queue = new TaskQueue();
11
+ #pendingRedraw: AbortController | undefined;
12
+
13
+ #lastFrame: VideoFrame | undefined;
14
+ get lastFrame() {
15
+ return this.#lastFrame;
16
+ }
17
+
18
+ constructor(draw: (frame: VideoFrame) => MaybePromiseLike<undefined>) {
19
+ this.#draw = draw;
20
+ }
21
+
22
+ /**
23
+ * Draws a new frame.
24
+ *
25
+ * If a redraw is in progress, it waits for the redraw to finish before drawing the new frame.
26
+ *
27
+ * If a redraw is in progress and another one is in queue,
28
+ * it cancels the queued redraw and draws the new frame instead.
29
+ *
30
+ * @param frame A `VideoFrame` to draw. The frame will be closed after drawing.
31
+ */
32
+ draw(frame: VideoFrame) {
33
+ this.#pendingRedraw?.abort();
34
+ this.#pendingRedraw = undefined;
35
+
36
+ this.#lastFrame?.close();
37
+ this.#lastFrame = frame.clone();
38
+
39
+ return this.#queue.enqueue(async (): Promise<undefined> => {
40
+ try {
41
+ await this.#draw(frame);
42
+ } finally {
43
+ frame.close();
44
+ }
45
+ }, true);
46
+ }
47
+
48
+ /**
49
+ * Redraws the last drawn frame.
50
+ *
51
+ * If a draw or redraw is in progress, it waits for them to finish before redrawing.
52
+ *
53
+ * If a redraw is in progress and another one is in queue,
54
+ * it cancels the queued redraw and redraws the latest frame instead.
55
+ */
56
+ async redraw(): Promise<undefined> {
57
+ if (!this.#lastFrame || this.#pendingRedraw) {
58
+ return;
59
+ }
60
+
61
+ const abortController = new AbortController();
62
+ this.#pendingRedraw = abortController;
63
+
64
+ return await this.#queue.enqueue(async (): Promise<undefined> => {
65
+ if (abortController.signal.aborted) {
66
+ return;
67
+ }
68
+
69
+ this.#pendingRedraw = undefined;
70
+
71
+ const frame = this.#lastFrame!.clone();
72
+ try {
73
+ await this.#draw(frame);
74
+ } finally {
75
+ frame.close();
76
+ }
77
+ }, true);
78
+ }
79
+
80
+ dispose() {
81
+ this.#pendingRedraw?.abort();
82
+
83
+ this.#lastFrame?.close();
84
+ this.#lastFrame = undefined;
85
+
86
+ this.#queue.dispose();
87
+ }
88
+ }
@@ -1,7 +1,15 @@
1
1
  import type { MaybePromiseLike } from "@yume-chan/async";
2
+ import type { Event } from "@yume-chan/event";
3
+ import type { ScrcpyVideoDecoder } from "@yume-chan/scrcpy-decoder-shared";
4
+ import type { WritableStream } from "@yume-chan/stream-extra";
2
5
 
3
6
  export interface VideoFrameRenderer {
4
- setSize(width: number, height: number): void;
7
+ readonly type: ScrcpyVideoDecoder.RendererType;
8
+ readonly onTypeChanged?: Event<ScrcpyVideoDecoder.RendererType>;
5
9
 
6
- draw(frame: VideoFrame): MaybePromiseLike<void>;
10
+ readonly writable: WritableStream<VideoFrame>;
11
+
12
+ snapshot?(options?: ImageEncodeOptions): Promise<Blob | undefined>;
13
+
14
+ dispose?(): MaybePromiseLike<undefined>;
7
15
  }
@@ -1,59 +1,189 @@
1
- import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
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
- const Resolved = Promise.resolve();
6
-
7
- function createContext(
8
- canvas: HTMLCanvasElement | OffscreenCanvas,
9
- enableCapture?: boolean,
10
- ): WebGLRenderingContext | null {
11
- const attributes: WebGLContextAttributes = {
12
- // Low-power GPU should be enough for video rendering.
13
- powerPreference: "low-power",
14
- alpha: false,
15
- // Disallow software rendering.
16
- // Other rendering methods are faster than software-based WebGL.
17
- failIfMajorPerformanceCaveat: true,
18
- preserveDrawingBuffer: !!enableCapture,
19
- };
20
-
21
- return (
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 vertexShaderSource = `
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 = vec2((1.0 + xy.x) / 2.0, (1.0 - xy.y) / 2.0);
73
+ uv.y = 1.0 - uv.y;
38
74
  }
39
75
  `;
40
76
 
41
- static fragmentShaderSource = `
42
- varying highp vec2 uv;
77
+ static FragmentShader = `
78
+ precision mediump float;
43
79
 
44
- uniform sampler2D texture;
80
+ uniform sampler2D source;
81
+ uniform vec2 texelSize;
82
+ uniform float zoom;
45
83
 
46
- void main(void) {
47
- gl_FragColor = texture2D(texture, uv);
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
- const canvas = createCanvas();
53
- return !!createContext(canvas);
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
- canvas?: HTMLCanvasElement | OffscreenCanvas,
67
- enableCapture?: boolean,
68
- ) {
69
- super(canvas);
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
- const gl = createContext(this.canvas, enableCapture);
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
- const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
78
- gl.shaderSource(
79
- vertexShader,
80
- WebGLVideoFrameRenderer.vertexShaderSource,
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
- gl.compileShader(vertexShader);
83
- if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
84
- throw new Error(gl.getShaderInfoLog(vertexShader)!);
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
- const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
88
- gl.shaderSource(
89
- fragmentShader,
90
- WebGLVideoFrameRenderer.fragmentShaderSource,
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
- gl.compileShader(fragmentShader);
93
- if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
94
- throw new Error(gl.getShaderInfoLog(fragmentShader)!);
95
- }
290
+ }
96
291
 
97
- const shaderProgram = gl.createProgram();
98
- gl.attachShader(shaderProgram, vertexShader);
99
- gl.attachShader(shaderProgram, fragmentShader);
100
- gl.linkProgram(shaderProgram);
101
- if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
102
- throw new Error(gl.getProgramInfoLog(shaderProgram)!);
103
- }
104
- gl.useProgram(shaderProgram);
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
- const vertexBuffer = gl.createBuffer();
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
- new Float32Array([-1.0, -1.0, -1.0, +1.0, +1.0, +1.0, +1.0, -1.0]),
307
+ WebGLVideoFrameRenderer.Vertices,
112
308
  gl.STATIC_DRAW,
113
309
  );
114
310
 
115
- const xyLocation = gl.getAttribLocation(shaderProgram, "xy");
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
- const texture = gl.createTexture();
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
- draw(frame: VideoFrame): Promise<void> {
129
- const gl = this.#context;
130
- gl.texImage2D(
131
- gl.TEXTURE_2D,
132
- 0,
133
- gl.RGBA,
134
- gl.RGBA,
135
- gl.UNSIGNED_BYTE,
136
- frame,
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
- gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
140
- gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
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
- return Resolved;
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,3 @@
1
+ export * from "./snapshot.js";
2
+ export * from "./timestamp.js";
3
+ export * from "./video-decoder-stream.js";
@@ -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
+ }