@yume-chan/scrcpy-decoder-webcodecs 2.5.3 → 3.0.0-beta.1

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