@yume-chan/scrcpy-decoder-webcodecs 0.0.24 → 1.1.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 (43) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +49 -29
  3. package/esm/video/codec/h265.d.ts.map +1 -1
  4. package/esm/video/codec/h265.js +3 -0
  5. package/esm/video/codec/h265.js.map +1 -1
  6. package/esm/video/decoder.d.ts +14 -7
  7. package/esm/video/decoder.d.ts.map +1 -1
  8. package/esm/video/decoder.js +166 -67
  9. package/esm/video/decoder.js.map +1 -1
  10. package/esm/video/render/bitmap.d.ts +4 -4
  11. package/esm/video/render/bitmap.d.ts.map +1 -1
  12. package/esm/video/render/bitmap.js +9 -11
  13. package/esm/video/render/bitmap.js.map +1 -1
  14. package/esm/video/render/canvas.d.ts +9 -0
  15. package/esm/video/render/canvas.d.ts.map +1 -0
  16. package/esm/video/render/canvas.js +22 -0
  17. package/esm/video/render/canvas.js.map +1 -0
  18. package/esm/video/render/index.d.ts +2 -0
  19. package/esm/video/render/index.d.ts.map +1 -1
  20. package/esm/video/render/index.js +3 -0
  21. package/esm/video/render/index.js.map +1 -1
  22. package/esm/video/render/insertable-stream.d.ts +10 -0
  23. package/esm/video/render/insertable-stream.d.ts.map +1 -0
  24. package/esm/video/render/insertable-stream.js +45 -0
  25. package/esm/video/render/insertable-stream.js.map +1 -0
  26. package/esm/video/render/type.d.ts +4 -2
  27. package/esm/video/render/type.d.ts.map +1 -1
  28. package/esm/video/render/webgl.d.ts +5 -4
  29. package/esm/video/render/webgl.d.ts.map +1 -1
  30. package/esm/video/render/webgl.js +26 -14
  31. package/esm/video/render/webgl.js.map +1 -1
  32. package/package.json +11 -14
  33. package/src/video/codec/h265.ts +3 -0
  34. package/src/video/decoder.ts +197 -69
  35. package/src/video/render/bitmap.ts +11 -17
  36. package/src/video/render/canvas.ts +27 -0
  37. package/src/video/render/index.ts +4 -0
  38. package/src/video/render/insertable-stream.ts +60 -0
  39. package/src/video/render/type.ts +6 -2
  40. package/src/video/render/webgl.ts +47 -19
  41. package/tsconfig.build.json +4 -1
  42. package/tsconfig.build.tsbuildinfo +1 -1
  43. package/CHANGELOG.json +0 -89
@@ -10,11 +10,94 @@ import { WritableStream } from "@yume-chan/stream-extra";
10
10
 
11
11
  import { Av1Codec, H264Decoder, H265Decoder } from "./codec/index.js";
12
12
  import type { CodecDecoder } from "./codec/type.js";
13
- import type { FrameRenderer } from "./render/index.js";
14
- import { BitmapFrameRenderer, WebGLFrameRenderer } from "./render/index.js";
13
+ import type { VideoFrameRenderer } from "./render/index.js";
14
+
15
+ class Pool<T> {
16
+ #controller!: ReadableStreamDefaultController<T>;
17
+ #readable = new ReadableStream<T>(
18
+ {
19
+ start: (controller) => {
20
+ this.#controller = controller;
21
+ },
22
+ pull: (controller) => {
23
+ controller.enqueue(this.#initializer());
24
+ },
25
+ },
26
+ { highWaterMark: 0 },
27
+ );
28
+ #reader = this.#readable.getReader();
29
+
30
+ #initializer: () => T;
31
+
32
+ #size = 0;
33
+ #capacity: number;
34
+
35
+ constructor(initializer: () => T, capacity: number) {
36
+ this.#initializer = initializer;
37
+ this.#capacity = capacity;
38
+ }
39
+
40
+ async borrow() {
41
+ const result = await this.#reader.read();
42
+ return result.value!;
43
+ }
44
+
45
+ return(value: T) {
46
+ if (this.#size < this.#capacity) {
47
+ this.#controller.enqueue(value);
48
+ this.#size += 1;
49
+ }
50
+ }
51
+ }
52
+
53
+ class VideoFrameCapturer {
54
+ #canvas: OffscreenCanvas | HTMLCanvasElement;
55
+ #context: ImageBitmapRenderingContext;
56
+
57
+ constructor() {
58
+ if (typeof OffscreenCanvas !== "undefined") {
59
+ this.#canvas = new OffscreenCanvas(1, 1);
60
+ } else {
61
+ this.#canvas = document.createElement("canvas");
62
+ this.#canvas.width = 1;
63
+ this.#canvas.height = 1;
64
+ }
65
+ this.#context = this.#canvas.getContext("bitmaprenderer", {
66
+ alpha: false,
67
+ })!;
68
+ }
69
+
70
+ async capture(frame: VideoFrame): Promise<Blob> {
71
+ this.#canvas.width = frame.displayWidth;
72
+ this.#canvas.height = frame.displayHeight;
73
+
74
+ const bitmap = await createImageBitmap(frame);
75
+ this.#context.transferFromImageBitmap(bitmap);
76
+
77
+ if (this.#canvas instanceof OffscreenCanvas) {
78
+ return await this.#canvas.convertToBlob({
79
+ type: "image/png",
80
+ });
81
+ } else {
82
+ return new Promise((resolve, reject) => {
83
+ (this.#canvas as HTMLCanvasElement).toBlob((blob) => {
84
+ if (!blob) {
85
+ reject(new Error("Failed to convert canvas to blob"));
86
+ } else {
87
+ resolve(blob);
88
+ }
89
+ }, "image/png");
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ const VideoFrameCapturerPool =
96
+ /* #__PURE__ */
97
+ new Pool(() => new VideoFrameCapturer(), 4);
15
98
 
16
99
  export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
17
- static isSupported() {
100
+ static get isSupported() {
18
101
  return typeof globalThis.VideoDecoder !== "undefined";
19
102
  }
20
103
 
@@ -22,6 +105,7 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
22
105
  {
23
106
  h264: {},
24
107
  h265: {},
108
+ av1: {},
25
109
  };
26
110
 
27
111
  #codec: ScrcpyVideoCodecId;
@@ -36,19 +120,23 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
36
120
  return this.#writable;
37
121
  }
38
122
 
39
- #canvas: HTMLCanvasElement;
123
+ #error: Error | undefined;
124
+ #controller!: WritableStreamDefaultController;
125
+
126
+ #renderer: VideoFrameRenderer;
40
127
  get renderer() {
41
- return this.#canvas;
128
+ return this.#renderer;
42
129
  }
43
130
 
44
- #frameRendered = 0;
131
+ #framesDraw = 0;
132
+ #framesPresented = 0;
45
133
  get framesRendered() {
46
- return this.#frameRendered;
134
+ return this.#framesPresented;
47
135
  }
48
136
 
49
- #frameSkipped = 0;
137
+ #framesSkipped = 0;
50
138
  get framesSkipped() {
51
- return this.#frameSkipped;
139
+ return this.#framesSkipped;
52
140
  }
53
141
 
54
142
  #sizeChanged = new EventEmitter<{ width: number; height: number }>();
@@ -57,61 +145,40 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
57
145
  }
58
146
 
59
147
  #decoder: VideoDecoder;
60
- #renderer: FrameRenderer;
61
148
 
62
- #currentFrameRendered = false;
149
+ #drawing = false;
150
+ #nextFrame: VideoFrame | undefined;
151
+ #captureFrame: VideoFrame | undefined;
152
+
63
153
  #animationFrameId = 0;
64
154
 
65
155
  /**
66
156
  * Create a new WebCodecs video decoder.
67
- * @param codec The video codec to decode
68
- * @param enableCapture
69
- * Whether to allow capturing the canvas content using APIs like `readPixels` and `toDataURL`.
70
- * Enable this option may reduce performance.
71
157
  */
72
- constructor(codec: ScrcpyVideoCodecId, enableCapture: boolean) {
158
+ constructor({ codec, renderer }: WebCodecsVideoDecoder.Options) {
73
159
  this.#codec = codec;
74
160
 
75
- this.#canvas = document.createElement("canvas");
76
-
77
- try {
78
- this.#renderer = new WebGLFrameRenderer(
79
- this.#canvas,
80
- enableCapture,
81
- );
82
- } catch {
83
- this.#renderer = new BitmapFrameRenderer(this.#canvas);
84
- }
161
+ this.#renderer = renderer;
85
162
 
86
163
  this.#decoder = new VideoDecoder({
87
164
  output: (frame) => {
88
- if (this.#currentFrameRendered) {
89
- this.#frameRendered += 1;
90
- } else {
91
- this.#frameSkipped += 1;
92
- }
93
- this.#currentFrameRendered = false;
94
-
95
- // PERF: Draw every frame to minimize latency at cost of performance.
96
- // When multiple frames are drawn in one vertical sync interval,
97
- // only the last one is visible to users.
98
- // But this ensures users can always see the most up-to-date screen.
99
- // This is also the behavior of official Scrcpy client.
100
- // https://github.com/Genymobile/scrcpy/issues/3679
101
- this.#updateSize(frame.displayWidth, frame.displayHeight);
102
- this.#renderer.draw(frame);
103
- },
104
- error(e) {
105
- if (controller) {
106
- try {
107
- controller.error(e);
108
- } catch {
109
- // ignore
110
- // `controller` may already in error state
165
+ this.#captureFrame?.close();
166
+ // PERF: `VideoFrame#clone` is cheap
167
+ this.#captureFrame = frame.clone();
168
+
169
+ if (this.#drawing) {
170
+ if (this.#nextFrame) {
171
+ this.#nextFrame.close();
172
+ this.#framesSkipped += 1;
111
173
  }
112
- } else {
113
- error = e;
174
+ this.#nextFrame = frame;
175
+ return;
114
176
  }
177
+
178
+ void this.#draw(frame);
179
+ },
180
+ error: (error) => {
181
+ this.#setError(error);
115
182
  },
116
183
  });
117
184
 
@@ -134,16 +201,16 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
134
201
  this.#updateSize,
135
202
  );
136
203
  break;
204
+ default:
205
+ throw new Error(`Unsupported codec: ${this.#codec as number}`);
137
206
  }
138
207
 
139
- let error: Error | undefined;
140
- let controller: WritableStreamDefaultController | undefined;
141
208
  this.#writable = new WritableStream<ScrcpyMediaStreamPacket>({
142
- start: (_controller) => {
143
- if (error) {
144
- _controller.error(error);
209
+ start: (controller) => {
210
+ if (this.#error) {
211
+ controller.error(this.#error);
145
212
  } else {
146
- controller = _controller;
213
+ this.#controller = controller;
147
214
  }
148
215
  },
149
216
  write: (packet) => {
@@ -151,29 +218,90 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
151
218
  },
152
219
  });
153
220
 
154
- this.#onFramePresented();
221
+ this.#onVerticalSync();
155
222
  }
156
223
 
157
- #updateSize = (width: number, height: number) => {
158
- if (width !== this.#canvas.width || height !== this.#canvas.height) {
159
- this.#canvas.width = width;
160
- this.#canvas.height = height;
161
- this.#sizeChanged.fire({
162
- width: width,
163
- height: height,
164
- });
224
+ #setError(error: Error) {
225
+ if (this.#controller) {
226
+ try {
227
+ this.#controller.error(error);
228
+ } catch {
229
+ // ignore
230
+ }
231
+ } else {
232
+ this.#error = error;
233
+ }
234
+ }
235
+
236
+ async #draw(frame: VideoFrame) {
237
+ try {
238
+ this.#drawing = true;
239
+ // PERF: Draw every frame to minimize latency at cost of performance.
240
+ // When multiple frames are drawn in one vertical sync interval,
241
+ // only the last one is visible to users.
242
+ // But this ensures users can always see the most up-to-date screen.
243
+ // This is also the behavior of official Scrcpy client.
244
+ // https://github.com/Genymobile/scrcpy/issues/3679
245
+ this.#updateSize(frame.displayWidth, frame.displayHeight);
246
+ await this.#renderer.draw(frame);
247
+ this.#framesDraw += 1;
248
+ frame.close();
249
+
250
+ if (this.#nextFrame) {
251
+ const frame = this.#nextFrame;
252
+ this.#nextFrame = undefined;
253
+ await this.#draw(frame);
254
+ }
255
+
256
+ this.#drawing = false;
257
+ } catch (error) {
258
+ this.#setError(error as Error);
165
259
  }
260
+ }
261
+
262
+ #updateSize = (width: number, height: number) => {
263
+ this.#renderer.setSize(width, height);
264
+ this.#sizeChanged.fire({ width, height });
166
265
  };
167
266
 
168
- #onFramePresented = () => {
169
- this.#currentFrameRendered = true;
170
- this.#animationFrameId = requestAnimationFrame(this.#onFramePresented);
267
+ #onVerticalSync = () => {
268
+ if (this.#framesDraw > 0) {
269
+ this.#framesPresented += 1;
270
+ this.#framesSkipped += this.#framesDraw - 1;
271
+ this.#framesDraw = 0;
272
+ }
273
+ this.#animationFrameId = requestAnimationFrame(this.#onVerticalSync);
171
274
  };
172
275
 
276
+ async snapshot() {
277
+ const frame = this.#captureFrame;
278
+ if (!frame) {
279
+ return undefined;
280
+ }
281
+
282
+ const capturer = await VideoFrameCapturerPool.borrow();
283
+ const result = await capturer.capture(frame);
284
+ VideoFrameCapturerPool.return(capturer);
285
+ return result;
286
+ }
287
+
173
288
  dispose() {
174
289
  cancelAnimationFrame(this.#animationFrameId);
175
290
  if (this.#decoder.state !== "closed") {
176
291
  this.#decoder.close();
177
292
  }
293
+ this.#nextFrame?.close();
294
+ this.#captureFrame?.close();
295
+ }
296
+ }
297
+
298
+ export namespace WebCodecsVideoDecoder {
299
+ export interface Options {
300
+ /**
301
+ * The video codec to decode
302
+ */
303
+ codec: ScrcpyVideoCodecId;
304
+
305
+ renderer: VideoFrameRenderer;
178
306
  }
179
307
  }
@@ -1,24 +1,18 @@
1
- import type { FrameRenderer } from "./type.js";
1
+ import { CanvasVideoFrameRenderer } from "./canvas.js";
2
2
 
3
- export class BitmapFrameRenderer implements FrameRenderer {
3
+ export class BitmapVideoFrameRenderer extends CanvasVideoFrameRenderer {
4
4
  #context: ImageBitmapRenderingContext;
5
5
 
6
- constructor(canvas: HTMLCanvasElement) {
7
- this.#context = canvas.getContext("bitmaprenderer", { alpha: false })!;
6
+ constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
7
+ super(canvas);
8
+
9
+ this.#context = this.canvas.getContext("bitmaprenderer", {
10
+ alpha: false,
11
+ })!;
8
12
  }
9
13
 
10
- draw(frame: VideoFrame): void {
11
- createImageBitmap(frame)
12
- .then((bitmap) => {
13
- this.#context.transferFromImageBitmap(bitmap);
14
- frame.close();
15
- })
16
- .catch((e) => {
17
- console.warn(
18
- "[@yume-chan/scrcpy-decoder-webcodecs]",
19
- "VideoDecoder error",
20
- e,
21
- );
22
- });
14
+ async draw(frame: VideoFrame): Promise<void> {
15
+ const bitmap = await createImageBitmap(frame);
16
+ this.#context.transferFromImageBitmap(bitmap);
23
17
  }
24
18
  }
@@ -0,0 +1,27 @@
1
+ import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
2
+
3
+ import type { VideoFrameRenderer } from "./type.js";
4
+
5
+ export abstract class CanvasVideoFrameRenderer implements VideoFrameRenderer {
6
+ #canvas: HTMLCanvasElement | OffscreenCanvas;
7
+ get canvas() {
8
+ return this.#canvas;
9
+ }
10
+
11
+ constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
12
+ if (canvas) {
13
+ this.#canvas = canvas;
14
+ } else {
15
+ this.#canvas = createCanvas();
16
+ }
17
+ }
18
+
19
+ setSize(width: number, height: number): void {
20
+ if (this.#canvas.width !== width || this.#canvas.height !== height) {
21
+ this.#canvas.width = width;
22
+ this.#canvas.height = height;
23
+ }
24
+ }
25
+
26
+ abstract draw(frame: VideoFrame): Promise<void>;
27
+ }
@@ -1,3 +1,7 @@
1
+ // cspell: ignore insertable
2
+
1
3
  export * from "./bitmap.js";
4
+ export * from "./canvas.js";
5
+ export * from "./insertable-stream.js";
2
6
  export * from "./type.js";
3
7
  export * from "./webgl.js";
@@ -0,0 +1,60 @@
1
+ // cspell: ignore insertable
2
+
3
+ import type { VideoFrameRenderer } from "./type.js";
4
+
5
+ declare class MediaStreamTrackGenerator extends MediaStreamTrack {
6
+ constructor(options: { kind: "audio" | "video" });
7
+
8
+ writable: WritableStream<VideoFrame>;
9
+ }
10
+
11
+ export class InsertableStreamVideoFrameRenderer implements VideoFrameRenderer {
12
+ static get isSupported() {
13
+ return typeof MediaStreamTrackGenerator !== "undefined";
14
+ }
15
+
16
+ #element: HTMLVideoElement;
17
+ get element() {
18
+ return this.#element;
19
+ }
20
+
21
+ #generator: MediaStreamTrackGenerator;
22
+ #writer: WritableStreamDefaultWriter<VideoFrame>;
23
+ #stream: MediaStream;
24
+
25
+ constructor(element?: HTMLVideoElement) {
26
+ if (element) {
27
+ this.#element = element;
28
+ } else if (typeof document !== "undefined") {
29
+ this.#element = document.createElement("video");
30
+ } else {
31
+ throw new Error(
32
+ "no video element input found nor any video element can be created",
33
+ );
34
+ }
35
+ this.#element.muted = true;
36
+ this.#element.autoplay = true;
37
+ this.#element.disablePictureInPicture = true;
38
+ this.#element.disableRemotePlayback = true;
39
+
40
+ // The spec replaced `MediaStreamTrackGenerator` with `VideoTrackGenerator`.
41
+ // But Chrome has not implemented it yet.
42
+ // https://issues.chromium.org/issues/40058895
43
+ this.#generator = new MediaStreamTrackGenerator({ kind: "video" });
44
+ this.#writer = this.#generator.writable.getWriter();
45
+
46
+ this.#stream = new MediaStream([this.#generator]);
47
+ this.#element.srcObject = this.#stream;
48
+ }
49
+
50
+ setSize(width: number, height: number): void {
51
+ if (this.#element.width !== width || this.#element.height !== height) {
52
+ this.#element.width = width;
53
+ this.#element.height = height;
54
+ }
55
+ }
56
+
57
+ async draw(frame: VideoFrame): Promise<void> {
58
+ await this.#writer.write(frame);
59
+ }
60
+ }
@@ -1,3 +1,7 @@
1
- export interface FrameRenderer {
2
- draw(frame: VideoFrame): void;
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+
3
+ export interface VideoFrameRenderer {
4
+ setSize(width: number, height: number): void;
5
+
6
+ draw(frame: VideoFrame): MaybePromiseLike<void>;
3
7
  }
@@ -1,6 +1,30 @@
1
- import type { FrameRenderer } from "./type.js";
1
+ import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
2
+
3
+ import { CanvasVideoFrameRenderer } from "./canvas.js";
4
+
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)
24
+ );
25
+ }
2
26
 
3
- export class WebGLFrameRenderer implements FrameRenderer {
27
+ export class WebGLVideoFrameRenderer extends CanvasVideoFrameRenderer {
4
28
  static vertexShaderSource = `
5
29
  attribute vec2 xy;
6
30
 
@@ -24,6 +48,11 @@ export class WebGLFrameRenderer implements FrameRenderer {
24
48
  }
25
49
  `;
26
50
 
51
+ static get isSupported() {
52
+ const canvas = createCanvas();
53
+ return !!createContext(canvas);
54
+ }
55
+
27
56
  #context: WebGLRenderingContext;
28
57
 
29
58
  /**
@@ -33,25 +62,23 @@ export class WebGLFrameRenderer implements FrameRenderer {
33
62
  * Whether to allow capturing the canvas content using APIs like `readPixels` and `toDataURL`.
34
63
  * Enable this option may reduce performance.
35
64
  */
36
- constructor(canvas: HTMLCanvasElement, enableCapture: boolean) {
37
- const gl =
38
- canvas.getContext("webgl2", {
39
- alpha: false,
40
- failIfMajorPerformanceCaveat: true,
41
- preserveDrawingBuffer: enableCapture,
42
- }) ||
43
- canvas.getContext("webgl", {
44
- alpha: false,
45
- failIfMajorPerformanceCaveat: true,
46
- preserveDrawingBuffer: enableCapture,
47
- });
65
+ constructor(
66
+ canvas?: HTMLCanvasElement | OffscreenCanvas,
67
+ enableCapture?: boolean,
68
+ ) {
69
+ super(canvas);
70
+
71
+ const gl = createContext(this.canvas, enableCapture);
48
72
  if (!gl) {
49
73
  throw new Error("WebGL not supported");
50
74
  }
51
75
  this.#context = gl;
52
76
 
53
77
  const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
54
- gl.shaderSource(vertexShader, WebGLFrameRenderer.vertexShaderSource);
78
+ gl.shaderSource(
79
+ vertexShader,
80
+ WebGLVideoFrameRenderer.vertexShaderSource,
81
+ );
55
82
  gl.compileShader(vertexShader);
56
83
  if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
57
84
  throw new Error(gl.getShaderInfoLog(vertexShader)!);
@@ -60,14 +87,14 @@ export class WebGLFrameRenderer implements FrameRenderer {
60
87
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
61
88
  gl.shaderSource(
62
89
  fragmentShader,
63
- WebGLFrameRenderer.fragmentShaderSource,
90
+ WebGLVideoFrameRenderer.fragmentShaderSource,
64
91
  );
65
92
  gl.compileShader(fragmentShader);
66
93
  if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
67
94
  throw new Error(gl.getShaderInfoLog(fragmentShader)!);
68
95
  }
69
96
 
70
- const shaderProgram = gl.createProgram()!;
97
+ const shaderProgram = gl.createProgram();
71
98
  gl.attachShader(shaderProgram, vertexShader);
72
99
  gl.attachShader(shaderProgram, fragmentShader);
73
100
  gl.linkProgram(shaderProgram);
@@ -98,7 +125,7 @@ export class WebGLFrameRenderer implements FrameRenderer {
98
125
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
99
126
  }
100
127
 
101
- draw(frame: VideoFrame) {
128
+ draw(frame: VideoFrame): Promise<void> {
102
129
  const gl = this.#context;
103
130
  gl.texImage2D(
104
131
  gl.TEXTURE_2D,
@@ -108,9 +135,10 @@ export class WebGLFrameRenderer implements FrameRenderer {
108
135
  gl.UNSIGNED_BYTE,
109
136
  frame,
110
137
  );
111
- frame.close();
112
138
 
113
139
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
114
140
  gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
141
+
142
+ return Resolved;
115
143
  }
116
144
  }
@@ -9,11 +9,14 @@
9
9
  },
10
10
  "references": [
11
11
  {
12
- "path": "../scrcpy/tsconfig.build.json"
12
+ "path": "../no-data-view/tsconfig.build.json"
13
13
  },
14
14
  {
15
15
  "path": "../scrcpy-decoder-tinyh264/tsconfig.build.json"
16
16
  },
17
+ {
18
+ "path": "../scrcpy/tsconfig.build.json"
19
+ },
17
20
  {
18
21
  "path": "../stream-extra/tsconfig.build.json"
19
22
  }