@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.
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 +29 -95
  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 +11 -21
  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 +13 -27
  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 -69
  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 +41 -131
  85. package/src/video/codec/h264.ts +15 -37
  86. package/src/video/codec/h265.ts +16 -45
  87. package/src/video/codec/h26x.ts +45 -96
  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
@@ -1,22 +1,25 @@
1
- import { StickyEventEmitter } from "@yume-chan/event";
2
- import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
3
- import { ScrcpyVideoCodecId } from "@yume-chan/scrcpy";
1
+ import { pipe } from "@yume-chan/event";
2
+ import { ScrcpyVideoCodecId, ScrcpyVideoSizeImpl } from "@yume-chan/scrcpy";
4
3
  import type {
5
4
  ScrcpyVideoDecoder,
6
5
  ScrcpyVideoDecoderCapability,
7
- } from "@yume-chan/scrcpy-decoder-tinyh264";
8
- import type { WritableStreamDefaultController } from "@yume-chan/stream-extra";
9
- import { WritableStream } from "@yume-chan/stream-extra";
10
-
11
- import { Av1Codec, H264Decoder, H265Decoder } from "./codec/index.js";
12
- import type { CodecDecoder, CodecDecoderOptions } from "./codec/type.js";
13
- import { Pool } from "./pool.js";
6
+ } from "@yume-chan/scrcpy-decoder-shared";
7
+ import { ScrcpyVideoDecoderPauseController } from "@yume-chan/scrcpy-decoder-shared";
8
+ import { InspectStream } from "@yume-chan/stream-extra";
9
+
10
+ import {
11
+ Av1TransformStream,
12
+ H264TransformStream,
13
+ H265TransformStream,
14
+ } from "./codec/index.js";
15
+ import type { CodecTransformStream } from "./codec/type.js";
14
16
  import type { VideoFrameRenderer } from "./render/index.js";
15
- import { VideoFrameCapturer } from "./snapshot.js";
16
-
17
- const VideoFrameCapturerPool =
18
- /* #__PURE__ */
19
- new Pool(() => new VideoFrameCapturer(), 4);
17
+ import {
18
+ AutoCanvasRenderer,
19
+ BitmapVideoFrameRenderer,
20
+ RendererController,
21
+ } from "./render/index.js";
22
+ import { TimestampTransforms, VideoDecoderStream } from "./utils/index.js";
20
23
 
21
24
  export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
22
25
  static get isSupported() {
@@ -30,6 +33,8 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
30
33
  av1: {},
31
34
  };
32
35
 
36
+ // #region parameters
37
+
33
38
  #codec: ScrcpyVideoCodecId;
34
39
  get codec() {
35
40
  return this.#codec;
@@ -39,214 +44,247 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
39
44
  get renderer() {
40
45
  return this.#renderer;
41
46
  }
47
+ get rendererType() {
48
+ return this.#renderer.type;
49
+ }
50
+ get onRendererTypeChange() {
51
+ return this.#renderer.onTypeChanged;
52
+ }
42
53
 
43
- #options: CodecDecoderOptions;
54
+ // #endregion parameters
44
55
 
45
- #codecDecoder: CodecDecoder;
56
+ // #region pause controller
46
57
 
47
- #writable: WritableStream<ScrcpyMediaStreamPacket>;
58
+ #pause = new ScrcpyVideoDecoderPauseController();
59
+ get paused() {
60
+ return this.#pause.paused;
61
+ }
48
62
  get writable() {
49
- return this.#writable;
63
+ return this.#pause.writable;
50
64
  }
51
65
 
52
- #error: Error | undefined;
53
- #controller!: WritableStreamDefaultController;
66
+ // #endregion pause controller
54
67
 
55
- #framesDraw = 0;
56
- #framesPresented = 0;
57
- get framesRendered() {
58
- return this.#framesPresented;
59
- }
68
+ // #region size
60
69
 
61
- #framesSkipped = 0;
62
- get framesSkipped() {
63
- return this.#framesSkipped;
70
+ #size = new ScrcpyVideoSizeImpl();
71
+ get width() {
72
+ return this.#size.width;
73
+ }
74
+ get height() {
75
+ return this.#size.height;
64
76
  }
65
-
66
- #sizeChanged = new StickyEventEmitter<{ width: number; height: number }>();
67
77
  get sizeChanged() {
68
- return this.#sizeChanged.event;
78
+ return this.#size.sizeChanged;
69
79
  }
70
80
 
71
- #width: number = 0;
72
- get width() {
73
- return this.#width;
81
+ // #endregion size
82
+
83
+ // #region raw decoder
84
+
85
+ #rawDecoder = new VideoDecoderStream();
86
+ get type() {
87
+ return this.#rawDecoder.hardwareAcceleration !== "prefer-software"
88
+ ? "hardware"
89
+ : "software";
90
+ }
91
+ #onTypeChange = pipe(
92
+ this.#rawDecoder.onHardwareAccelerationChange,
93
+ (value) => (value !== "prefer-software" ? "hardware" : "software"),
94
+ );
95
+ get onTypeChange() {
96
+ return this.#onTypeChange;
97
+ }
98
+ /**
99
+ * Gets the number of frames waiting to be decoded.
100
+ */
101
+ get decodeQueueSize() {
102
+ return this.#rawDecoder.decodeQueueSize;
103
+ }
104
+ /**
105
+ * Gets an event when a frame is dequeued (either decoded or discarded).
106
+ */
107
+ get onDequeue() {
108
+ return this.#rawDecoder.onDequeue;
109
+ }
110
+ /**
111
+ * Gets the number of times the decoder has been reset to catch up new keyframes.
112
+ */
113
+ get decoderResetCount() {
114
+ return this.#rawDecoder.decoderResetCount;
115
+ }
116
+ /**
117
+ * Gets the number of frames decoded by the decoder.
118
+ */
119
+ get framesDecoded() {
120
+ return this.#rawDecoder.framesDecoded;
121
+ }
122
+ /**
123
+ * Gets the number of frames skipped by the decoder.
124
+ */
125
+ get framesSkippedDecoding() {
126
+ return this.#rawDecoder.framesSkippedDecoding;
74
127
  }
75
128
 
76
- #height: number = 0;
77
- get height() {
78
- return this.#height;
129
+ // #endregion raw decoder
130
+
131
+ #timestampTransforms = new TimestampTransforms();
132
+ /**
133
+ * Gets the total time spent processing and decoding frames in milliseconds.
134
+ */
135
+ get totalDecodeTime() {
136
+ return this.#timestampTransforms.totalDecodeTime;
79
137
  }
80
138
 
81
- #decoder: VideoDecoder;
139
+ // #region renderer
82
140
 
83
- #drawing = false;
84
- #nextFrame: VideoFrame | undefined;
85
- #captureFrame: VideoFrame | undefined;
141
+ #renderController = new RendererController();
142
+ /**
143
+ * Gets the number of frames that have been drawn on the renderer.
144
+ */
145
+ get framesRendered() {
146
+ return this.#renderController.framesRendered;
147
+ }
148
+ /**
149
+ * Gets the number of frames that's visible to the user.
150
+ *
151
+ * Multiple frames might be rendered during one vertical sync interval,
152
+ * but only the last of them is represented to the user.
153
+ * This costs some performance but reduces latency by 1 frame.
154
+ *
155
+ * Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
156
+ * https://issues.chromium.org/issues/41483010
157
+ */
158
+ get framesDisplayed() {
159
+ return this.#renderController.framesDisplayed;
160
+ }
161
+ /**
162
+ * Gets the number of frames that wasn't drawn on the renderer
163
+ * because the renderer can't keep up
164
+ */
165
+ get framesSkippedRendering() {
166
+ return this.#renderController.framesSkippedRendering;
167
+ }
86
168
 
87
- #animationFrameId = 0;
169
+ // #endregion renderer
88
170
 
89
171
  /**
90
172
  * Create a new WebCodecs video decoder.
91
173
  */
92
174
  constructor({
93
175
  codec,
94
- renderer,
95
- ...options
176
+ renderer = new AutoCanvasRenderer(),
177
+ hardwareAcceleration = "no-preference",
178
+ optimizeForLatency = true,
96
179
  }: WebCodecsVideoDecoder.Options) {
97
180
  this.#codec = codec;
98
181
  this.#renderer = renderer;
99
- this.#options = options;
100
-
101
- this.#decoder = new VideoDecoder({
102
- output: (frame) => {
103
- this.#captureFrame?.close();
104
- // PERF: `VideoFrame#clone` is cheap
105
- this.#captureFrame = frame.clone();
106
-
107
- if (this.#drawing) {
108
- if (this.#nextFrame) {
109
- this.#nextFrame.close();
110
- this.#framesSkipped += 1;
111
- }
112
- this.#nextFrame = frame;
113
- return;
114
- }
115
-
116
- void this.#draw(frame);
117
- },
118
- error: (error) => {
119
- this.#setError(error);
120
- },
121
- });
122
182
 
183
+ let codecTransform: CodecTransformStream;
123
184
  switch (this.#codec) {
124
185
  case ScrcpyVideoCodecId.H264:
125
- this.#codecDecoder = new H264Decoder(
126
- this.#decoder,
127
- this.#updateSize,
128
- this.#options,
129
- );
186
+ codecTransform = new H264TransformStream();
130
187
  break;
131
188
  case ScrcpyVideoCodecId.H265:
132
- this.#codecDecoder = new H265Decoder(
133
- this.#decoder,
134
- this.#updateSize,
135
- this.#options,
136
- );
189
+ codecTransform = new H265TransformStream();
137
190
  break;
138
191
  case ScrcpyVideoCodecId.AV1:
139
- this.#codecDecoder = new Av1Codec(
140
- this.#decoder,
141
- this.#updateSize,
142
- this.#options,
143
- );
192
+ codecTransform = new Av1TransformStream();
144
193
  break;
145
194
  default:
146
- throw new Error(`Unsupported codec: ${this.#codec as number}`);
195
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
196
+ throw new Error(`Unsupported codec: ${this.#codec}`);
147
197
  }
148
198
 
149
- this.#writable = new WritableStream<ScrcpyMediaStreamPacket>({
150
- start: (controller) => {
151
- if (this.#error) {
152
- controller.error(this.#error);
153
- } else {
154
- this.#controller = controller;
155
- }
156
- },
157
- write: (packet) => {
158
- this.#codecDecoder.decode(packet);
159
- },
160
- });
161
-
162
- this.#handleAnimationFrame();
163
- }
164
-
165
- #setError(error: Error) {
166
- if (this.#controller) {
167
- try {
168
- this.#controller.error(error);
169
- } catch {
170
- // ignore
171
- }
172
- } else {
173
- this.#error = error;
174
- }
199
+ void this.#pause.readable
200
+ // Add timestamp
201
+ .pipeThrough(this.#timestampTransforms.addTimestamp)
202
+ // Convert Scrcpy packets to `VideoDecoder` config/chunk
203
+ .pipeThrough(codecTransform)
204
+ // Insert extra `VideoDecoder` config and intercept size changes
205
+ .pipeThrough(
206
+ new InspectStream((chunk): undefined => {
207
+ if ("codec" in chunk) {
208
+ chunk.hardwareAcceleration = hardwareAcceleration;
209
+ chunk.optimizeForLatency = optimizeForLatency;
210
+
211
+ this.#size.setSize(chunk.codedWidth, chunk.codedHeight);
212
+ }
213
+ }),
214
+ )
215
+ // Decode `VideoDecoder` config/chunk to `VideoFrame`s
216
+ .pipeThrough(this.#rawDecoder)
217
+ // Track decoding time and filter skipped frames
218
+ .pipeThrough(this.#timestampTransforms.consumeTimestamp)
219
+ // Skip frames if renderer can't keep up
220
+ .pipeThrough(this.#renderController)
221
+ // Render
222
+ .pipeTo(renderer.writable)
223
+ // Errors will be handled by source stream
224
+ .catch(() => {});
175
225
  }
176
226
 
177
- async #draw(frame: VideoFrame) {
178
- try {
179
- this.#drawing = true;
180
- // PERF: Draw every frame to minimize latency at cost of performance.
181
- // When multiple frames are drawn in one vertical sync interval,
182
- // only the last one is visible to users.
183
- // But this ensures users can always see the most up-to-date screen.
184
- // This is also the behavior of official Scrcpy client.
185
- // https://github.com/Genymobile/scrcpy/issues/3679
186
- this.#updateSize(frame.displayWidth, frame.displayHeight);
187
- await this.#renderer.draw(frame);
188
- this.#framesDraw += 1;
189
- frame.close();
190
-
191
- if (this.#nextFrame) {
192
- const frame = this.#nextFrame;
193
- this.#nextFrame = undefined;
194
- await this.#draw(frame);
195
- }
196
-
197
- this.#drawing = false;
198
- } catch (error) {
199
- this.#setError(error as Error);
200
- }
227
+ pause(): void {
228
+ this.#pause.pause();
201
229
  }
202
230
 
203
- #updateSize = (width: number, height: number) => {
204
- this.#renderer.setSize(width, height);
205
- this.#width = width;
206
- this.#height = height;
207
- this.#sizeChanged.fire({ width, height });
208
- };
231
+ resume(): undefined {
232
+ this.#pause.resume();
233
+ }
209
234
 
210
- #handleAnimationFrame = () => {
211
- if (this.#framesDraw > 0) {
212
- this.#framesPresented += 1;
213
- this.#framesSkipped += this.#framesDraw - 1;
214
- this.#framesDraw = 0;
215
- }
216
- this.#animationFrameId = requestAnimationFrame(
217
- this.#handleAnimationFrame,
218
- );
219
- };
235
+ trackDocumentVisibility(document: Document): () => undefined {
236
+ return this.#pause.trackDocumentVisibility(document);
237
+ }
220
238
 
221
- async snapshot() {
222
- const frame = this.#captureFrame;
239
+ async snapshot(options?: ImageEncodeOptions) {
240
+ const frame = this.#renderController.captureFrame;
223
241
  if (!frame) {
224
242
  return undefined;
225
243
  }
226
244
 
227
- const capturer = await VideoFrameCapturerPool.borrow();
228
- const result = await capturer.capture(frame);
229
- VideoFrameCapturerPool.return(capturer);
230
- return result;
231
- }
245
+ // First check if the renderer can provide a snapshot natively
246
+ const blob = await this.#renderer.snapshot?.(options);
247
+ if (blob) {
248
+ return blob;
249
+ }
232
250
 
233
- dispose() {
234
- cancelAnimationFrame(this.#animationFrameId);
235
- if (this.#decoder.state !== "closed") {
236
- this.#decoder.close();
251
+ // Create a BitmapVideoFrameRenderer to draw the frame
252
+ const renderer = new BitmapVideoFrameRenderer();
253
+ try {
254
+ const writer = renderer.writable.getWriter();
255
+ // Draw the frame
256
+ // `renderer` will close the passed in `frame`, so make a clone
257
+ await writer.write(frame.clone());
258
+ await writer.close();
259
+ // BitmapVideoFrameRenderer.snapshot will definitely return a value
260
+ return await renderer.snapshot(options);
261
+ } finally {
262
+ renderer.dispose();
237
263
  }
238
- this.#nextFrame?.close();
239
- this.#captureFrame?.close();
264
+ }
265
+
266
+ dispose(): undefined {
267
+ // Most cleanup happens automatically when `writable` ends
268
+ // (in each stream's `close` callback).
269
+ // This method cleanup things that still available after `writable` ends
270
+
271
+ this.#pause.dispose();
272
+ this.#size.dispose();
273
+ this.#renderController.dispose();
274
+ this.#renderer.dispose?.();
240
275
  }
241
276
  }
242
277
 
243
278
  export namespace WebCodecsVideoDecoder {
244
- export interface Options extends CodecDecoderOptions {
279
+ export interface Options extends Pick<
280
+ VideoDecoderConfig,
281
+ "hardwareAcceleration" | "optimizeForLatency"
282
+ > {
245
283
  /**
246
284
  * The video codec to decode
247
285
  */
248
286
  codec: ScrcpyVideoCodecId;
249
287
 
250
- renderer: VideoFrameRenderer;
288
+ renderer?: VideoFrameRenderer | undefined;
251
289
  }
252
290
  }
@@ -1,3 +1,4 @@
1
1
  export * from "./codec/index.js";
2
2
  export * from "./decoder.js";
3
3
  export * from "./render/index.js";
4
+ export * from "./utils/index.js";
@@ -0,0 +1,105 @@
1
+ import { StickyEventEmitter } from "@yume-chan/event";
2
+ import type { ScrcpyVideoDecoder } from "@yume-chan/scrcpy-decoder-shared";
3
+ import { createCanvas } from "@yume-chan/scrcpy-decoder-shared";
4
+ import { WritableStream } from "@yume-chan/stream-extra";
5
+
6
+ import { BitmapVideoFrameRenderer } from "./bitmap.js";
7
+ import type { CanvasVideoFrameRenderer } from "./canvas.js";
8
+ import type { VideoFrameRenderer } from "./type.js";
9
+ import { WebGLVideoFrameRenderer } from "./webgl.js";
10
+
11
+ export class AutoCanvasRenderer implements VideoFrameRenderer {
12
+ #inner!: CanvasVideoFrameRenderer;
13
+ #innerWriter!: WritableStreamDefaultWriter<VideoFrame>;
14
+
15
+ get type() {
16
+ return this.#inner.type;
17
+ }
18
+ #onTypeChanged = new StickyEventEmitter<ScrcpyVideoDecoder.RendererType>();
19
+ get onTypeChanged() {
20
+ return this.#onTypeChanged.event;
21
+ }
22
+
23
+ #canvas!: HTMLCanvasElement | OffscreenCanvas;
24
+ /**
25
+ * Gets the canvas used by the renderer.
26
+ *
27
+ * The `canvas` property might change when the renderer switches between WebGL and 2D rendering.
28
+ * Specify a `createCanvas` option, or listen to the `onTypeChanged` event to handle the canvas change.
29
+ */
30
+ get canvas() {
31
+ return this.#canvas;
32
+ }
33
+
34
+ #lastFrame: VideoFrame | undefined;
35
+ #writableController!: WritableStreamDefaultController;
36
+ #writable = new WritableStream<VideoFrame>({
37
+ start: (controller) => {
38
+ this.#writableController = controller;
39
+ },
40
+ write: async (frame) => {
41
+ this.#lastFrame?.close();
42
+ this.#lastFrame = frame.clone();
43
+
44
+ await this.#innerWriter.write(frame);
45
+ },
46
+ });
47
+ get writable() {
48
+ return this.#writable;
49
+ }
50
+
51
+ constructor(options?: AutoCanvasRenderer.Options) {
52
+ this.#createInner(options, WebGLVideoFrameRenderer.isSupported);
53
+ }
54
+
55
+ #createInner(
56
+ options: AutoCanvasRenderer.Options | undefined,
57
+ webgl: boolean,
58
+ ) {
59
+ this.#canvas = options?.createCanvas?.() ?? createCanvas();
60
+ if (webgl) {
61
+ const webgl = new WebGLVideoFrameRenderer({
62
+ ...options,
63
+ canvas: this.#canvas,
64
+ });
65
+ webgl.onContextLost(() => {
66
+ this.#createInner(options, false);
67
+ if (this.#lastFrame) {
68
+ this.#innerWriter
69
+ .write(this.#lastFrame.clone())
70
+ .catch((e) => this.#writableController.error(e));
71
+ }
72
+ });
73
+ this.#inner = webgl;
74
+ } else {
75
+ this.#inner = new BitmapVideoFrameRenderer({
76
+ ...options,
77
+ canvas: this.#canvas,
78
+ });
79
+ }
80
+ this.#innerWriter = this.#inner.writable.getWriter();
81
+ this.#onTypeChanged.fire(this.#inner.type);
82
+ }
83
+
84
+ snapshot(options?: ImageEncodeOptions): Promise<Blob | undefined> {
85
+ return this.#inner.snapshot(options);
86
+ }
87
+
88
+ dispose() {
89
+ this.#onTypeChanged.dispose();
90
+
91
+ this.#lastFrame?.close();
92
+ this.#lastFrame = undefined;
93
+
94
+ return this.#inner.dispose();
95
+ }
96
+ }
97
+
98
+ export namespace AutoCanvasRenderer {
99
+ export interface Options extends Omit<
100
+ WebGLVideoFrameRenderer.Options,
101
+ "canvas"
102
+ > {
103
+ createCanvas?: () => HTMLCanvasElement | OffscreenCanvas;
104
+ }
105
+ }
@@ -1,18 +1,32 @@
1
1
  import { CanvasVideoFrameRenderer } from "./canvas.js";
2
2
 
3
3
  export class BitmapVideoFrameRenderer extends CanvasVideoFrameRenderer {
4
+ override get type() {
5
+ return "software" as const;
6
+ }
7
+
4
8
  #context: ImageBitmapRenderingContext;
5
9
 
6
- constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
7
- super(canvas);
10
+ constructor(options?: CanvasVideoFrameRenderer.Options) {
11
+ super(async (frame) => {
12
+ const bitmap = await createImageBitmap(frame);
13
+ this.#context.transferFromImageBitmap(bitmap);
14
+ bitmap.close();
15
+ }, options);
8
16
 
9
- this.#context = this.canvas.getContext("bitmaprenderer", {
10
- alpha: false,
11
- })!;
12
- }
17
+ const context = (this.canvas as HTMLCanvasElement).getContext(
18
+ "bitmaprenderer",
19
+ {
20
+ // Avoid alpha:false, which can be expensive
21
+ // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#avoid_alphafalse_which_can_be_expensive
22
+ alpha: true,
23
+ },
24
+ );
25
+ if (!context) {
26
+ // This can happen if the canvas already has a context of a different type
27
+ throw new Error("Failed to create rendering context");
28
+ }
13
29
 
14
- async draw(frame: VideoFrame): Promise<void> {
15
- const bitmap = await createImageBitmap(frame);
16
- this.#context.transferFromImageBitmap(bitmap);
30
+ this.#context = context;
17
31
  }
18
32
  }