@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,27 +1,172 @@
1
- import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
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";
2
5
 
6
+ import { canvasToBlob } from "../utils/index.js";
7
+
8
+ import { RedrawController } from "./redraw.js";
3
9
  import type { VideoFrameRenderer } from "./type.js";
4
10
 
5
- export abstract class CanvasVideoFrameRenderer implements VideoFrameRenderer {
11
+ export abstract class CanvasVideoFrameRenderer<
12
+ TOptions extends CanvasVideoFrameRenderer.Options =
13
+ CanvasVideoFrameRenderer.Options,
14
+ > implements VideoFrameRenderer {
15
+ abstract get type(): ScrcpyVideoDecoder.RendererType;
16
+
6
17
  #canvas: HTMLCanvasElement | OffscreenCanvas;
7
18
  get canvas() {
8
19
  return this.#canvas;
9
20
  }
10
21
 
11
- constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
12
- if (canvas) {
13
- this.#canvas = canvas;
14
- } else {
15
- this.#canvas = createCanvas();
22
+ #options: TOptions | undefined;
23
+ #canvasSize: CanvasVideoFrameRenderer.Options["canvasSize"];
24
+ get options(): Readonly<TOptions> | undefined {
25
+ return this.#options;
26
+ }
27
+
28
+ #resizeObserver: ResizeObserver | undefined;
29
+ #displayWidth = Infinity;
30
+ #displayHeight = Infinity;
31
+
32
+ #draw: (frame: VideoFrame) => MaybePromiseLike<undefined>;
33
+ #controller = new RedrawController((frame) => {
34
+ if (this.#canvasSize !== "external") {
35
+ this.#updateSize(frame);
36
+ }
37
+
38
+ return this.#draw(frame);
39
+ });
40
+ get lastFrame() {
41
+ return this.#controller.lastFrame;
42
+ }
43
+
44
+ #writable = new WritableStream<VideoFrame>({
45
+ write: (frame) => this.#controller.draw(frame),
46
+ });
47
+ get writable() {
48
+ return this.#writable;
49
+ }
50
+
51
+ constructor(
52
+ draw: (frame: VideoFrame) => MaybePromiseLike<undefined>,
53
+ options?: TOptions,
54
+ ) {
55
+ this.#draw = draw;
56
+ this.#canvas = options?.canvas ?? createCanvas();
57
+ this.#options = options;
58
+ this.#canvasSize = options?.canvasSize ?? "video";
59
+
60
+ if (this.#canvasSize === "display") {
61
+ if (
62
+ typeof HTMLCanvasElement === "undefined" ||
63
+ !(this.#canvas instanceof HTMLCanvasElement)
64
+ ) {
65
+ throw new Error(
66
+ "`canvasSize: display` is only supported for HTMLCanvasElement",
67
+ );
68
+ }
69
+
70
+ this.#resizeObserver = new ResizeObserver((entries) => {
71
+ const entry = entries[0];
72
+ if (!entry) {
73
+ return;
74
+ }
75
+
76
+ const devicePixelSize = entry.devicePixelContentBoxSize?.[0];
77
+ if (devicePixelSize) {
78
+ this.#setDisplaySize(
79
+ devicePixelSize.inlineSize,
80
+ devicePixelSize.blockSize,
81
+ );
82
+ return;
83
+ }
84
+
85
+ const cssSize = entry.contentBoxSize[0];
86
+ if (cssSize) {
87
+ this.#setDisplaySize(
88
+ Math.round(cssSize.inlineSize * devicePixelRatio),
89
+ Math.round(cssSize.blockSize * devicePixelRatio),
90
+ );
91
+ }
92
+ });
93
+ this.#resizeObserver.observe(this.#canvas);
16
94
  }
17
95
  }
18
96
 
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;
97
+ #setDisplaySize(width: number, height: number) {
98
+ if (this.#displayWidth === width && this.#displayHeight === height) {
99
+ return;
23
100
  }
101
+
102
+ this.#displayWidth = width;
103
+ this.#displayHeight = height;
104
+
105
+ void this.#controller.redraw();
24
106
  }
25
107
 
26
- abstract draw(frame: VideoFrame): Promise<void>;
108
+ #updateSize(frame: VideoFrame) {
109
+ let { codedWidth: width, codedHeight: height } = frame;
110
+ if (this.#canvasSize === "display") {
111
+ width = Math.min(width, this.#displayWidth);
112
+ height = Math.min(height, this.#displayHeight);
113
+ }
114
+
115
+ if (this.#canvas.width === width && this.#canvas.height === height) {
116
+ return false;
117
+ }
118
+
119
+ this.#canvas.width = width;
120
+ this.#canvas.height = height;
121
+ return true;
122
+ }
123
+
124
+ /**
125
+ * Redraws the last drawn frame.
126
+ *
127
+ * If a draw or redraw is in progress, it waits for them to finish before redrawing.
128
+ *
129
+ * If a redraw is in progress and another one is in queue,
130
+ * it cancels the queued redraw and redraws the latest frame instead.
131
+ */
132
+ redraw() {
133
+ return this.#controller.redraw();
134
+ }
135
+
136
+ async snapshot(options?: ImageEncodeOptions): Promise<Blob | undefined> {
137
+ if (this.#canvasSize !== "video") {
138
+ return undefined;
139
+ }
140
+ return canvasToBlob(this.#canvas, options);
141
+ }
142
+
143
+ dispose(): undefined {
144
+ if (this.#canvasSize !== "external") {
145
+ this.#canvas.width = 0;
146
+ this.#canvas.height = 0;
147
+ }
148
+
149
+ this.#resizeObserver?.disconnect();
150
+ this.#controller.dispose();
151
+ }
152
+ }
153
+
154
+ export namespace CanvasVideoFrameRenderer {
155
+ export interface Options {
156
+ canvas?: HTMLCanvasElement | OffscreenCanvas;
157
+
158
+ /**
159
+ * Whether to update the canvas size (rendering resolution) automatically.
160
+ *
161
+ * * `"video"` (default): update the canvas size to match the video resolution
162
+ * * `"display"` (only for `HTMLCanvasElement`):
163
+ * update the canvas size to match the display size.
164
+ * The display size can be set using `canvas.style.width/height`,
165
+ * and must be in correct aspect ratio.
166
+ * * `"external"`: use the canvas size as it is.
167
+ * The size must be manually set using `canvas.width/height`,
168
+ * and must be in correct aspect ratio.
169
+ */
170
+ canvasSize?: "video" | "display" | "external";
171
+ }
27
172
  }
@@ -0,0 +1,182 @@
1
+ import type { ScrcpyVideoRendererPerformanceCounterInterface } from "@yume-chan/scrcpy-decoder-shared";
2
+ import { ScrcpyVideoRendererPerformanceCounter } from "@yume-chan/scrcpy-decoder-shared";
3
+ import type {
4
+ PushReadableStreamController,
5
+ ReadableStream,
6
+ TransformStream,
7
+ WritableStreamDefaultController,
8
+ } from "@yume-chan/stream-extra";
9
+ import {
10
+ PushReadableStream,
11
+ tryClose,
12
+ WritableStream,
13
+ } from "@yume-chan/stream-extra";
14
+
15
+ export class RendererController
16
+ implements
17
+ TransformStream<VideoFrame, VideoFrame>,
18
+ ScrcpyVideoRendererPerformanceCounterInterface
19
+ {
20
+ #readable: ReadableStream<VideoFrame>;
21
+ #readableController!: PushReadableStreamController<VideoFrame>;
22
+ get readable() {
23
+ return this.#readable;
24
+ }
25
+
26
+ #writable: WritableStream<VideoFrame>;
27
+ #writableController!: WritableStreamDefaultController;
28
+ get writable() {
29
+ return this.#writable;
30
+ }
31
+
32
+ #captureFrame: VideoFrame | undefined;
33
+ get captureFrame() {
34
+ return this.#captureFrame;
35
+ }
36
+
37
+ #nextFrame: VideoFrame | undefined;
38
+
39
+ #drawTask: Promise<undefined> | undefined;
40
+
41
+ #counter = new ScrcpyVideoRendererPerformanceCounter();
42
+ /**
43
+ * Gets the number of frames that have been drawn on the renderer.
44
+ */
45
+ get framesRendered() {
46
+ return this.#counter.framesRendered;
47
+ }
48
+ /**
49
+ * Gets the number of frames that's visible to the user.
50
+ *
51
+ * Multiple frames might be rendered during one vertical sync interval,
52
+ * but only the last of them is represented to the user.
53
+ * This costs some performance but reduces latency by 1 frame.
54
+ *
55
+ * Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
56
+ * https://issues.chromium.org/issues/41483010
57
+ */
58
+ get framesDisplayed() {
59
+ return this.#counter.framesDisplayed;
60
+ }
61
+ /**
62
+ * Gets the number of frames that wasn't drawn on the renderer
63
+ * because the renderer can't keep up
64
+ */
65
+ get framesSkippedRendering() {
66
+ return this.#counter.framesSkippedRendering;
67
+ }
68
+
69
+ constructor() {
70
+ this.#readable = new PushReadableStream((controller) => {
71
+ this.#readableController = controller;
72
+ });
73
+
74
+ this.#writable = new WritableStream({
75
+ start: (controller) => {
76
+ this.#writableController = controller;
77
+
78
+ // Propagate `readable` error back to `writable`
79
+ const signal = this.#readableController.abortSignal;
80
+ signal.addEventListener("abort", () =>
81
+ controller.error(signal.reason),
82
+ );
83
+ },
84
+ write: (frame) => {
85
+ this.#captureFrame?.close();
86
+ // `#captureFrame` and `#nextFrame` must not be the same object
87
+ // because they need to be closed at different times
88
+ this.#captureFrame = frame.clone();
89
+
90
+ // Frame A is drawing, frame B (`#nextFrame`) is waiting,
91
+ // then frame C (`frame`) arrives.
92
+ // Skip frame B and queue frame C
93
+ if (this.#nextFrame) {
94
+ this.#nextFrame.close();
95
+ this.#counter.increaseFramesSkippedRendering();
96
+ }
97
+ this.#nextFrame = frame;
98
+
99
+ // Don't `await` because this writable needs to
100
+ // accept incoming frames as fast as produced.
101
+ // The `#draw` method then draws the frames
102
+ // as fast as the renderer can keep up
103
+ void this.#tryStartDrawing();
104
+ },
105
+ close: async () => {
106
+ // Normally `WritableStream` only calls `close` after `write` finishes,
107
+ // but because our `write` doesn't wait for `#draw`,
108
+ // we might still need to send `#nextFrame` to `readable`.
109
+ // So wait for `#drawTask` before closing `readable`
110
+ await this.#drawTask;
111
+ // `#nextFrame` must be `undefined` at this point
112
+
113
+ // Propagate `writable` close to `readable`
114
+ this.#readableController.close();
115
+
116
+ this.#counter.dispose();
117
+
118
+ // Don't close `#captureFrame` to allow using `snapshot` on the last frame
119
+ },
120
+ abort: async (reason) => {
121
+ // See `close` above
122
+ await this.#drawTask;
123
+
124
+ // Propagate `writable` error to `readable`
125
+ this.#readableController.error(reason);
126
+
127
+ this.#counter.dispose();
128
+ },
129
+ });
130
+ }
131
+
132
+ async #draw(): Promise<undefined> {
133
+ // PERF: Draw every frame to minimize latency at cost of performance.
134
+ // When multiple frames are drawn in one vertical sync interval,
135
+ // only the last one is visible to users.
136
+ // But this ensures users can always see the most up-to-date screen.
137
+ // This is also the behavior of official Scrcpy client.
138
+ // https://github.com/Genymobile/scrcpy/issues/3679
139
+
140
+ let frame: VideoFrame | undefined;
141
+ while ((frame = this.#nextFrame)) {
142
+ this.#nextFrame = undefined;
143
+ if (await this.#readableController.enqueue(frame)) {
144
+ // The consumer is responsible for closing `frame`
145
+ this.#counter.increaseFramesRendered();
146
+ } else {
147
+ // `enqueue` returning `false` means the consumer has called `readable.cancel()`,
148
+ // before consuming the `frame`. So close it here.
149
+ frame.close();
150
+
151
+ // Continue looping to close all `#nextFrame`s
152
+ // In the mean time the `writable` is erroring out its source
153
+ // so no more frames will arrive
154
+ }
155
+ }
156
+
157
+ this.#drawTask = undefined;
158
+ }
159
+
160
+ #tryStartDrawing() {
161
+ if (!this.#drawTask) {
162
+ this.#drawTask = this.#draw();
163
+ }
164
+ }
165
+
166
+ dispose() {
167
+ this.#captureFrame?.close();
168
+ this.#captureFrame = undefined;
169
+
170
+ this.#nextFrame?.close();
171
+ this.#nextFrame = undefined;
172
+
173
+ this.#counter.dispose();
174
+
175
+ tryClose(this.#readableController);
176
+
177
+ // Throw an error similar to native TransformStream
178
+ this.#writableController.error(
179
+ new TypeError("The transform stream has been terminated"),
180
+ );
181
+ }
182
+ }
@@ -2,6 +2,8 @@
2
2
 
3
3
  export * from "./bitmap.js";
4
4
  export * from "./canvas.js";
5
+ export * from "./auto-canvas.js";
6
+ export * from "./flow-control.js";
5
7
  export * from "./insertable-stream.js";
6
8
  export * from "./type.js";
7
9
  export * from "./webgl.js";
@@ -1,5 +1,7 @@
1
1
  // cspell: ignore insertable
2
2
 
3
+ import type { WritableStream } from "@yume-chan/stream-extra";
4
+
3
5
  import type { VideoFrameRenderer } from "./type.js";
4
6
 
5
7
  declare class MediaStreamTrackGenerator extends MediaStreamTrack {
@@ -13,16 +15,36 @@ export class InsertableStreamVideoFrameRenderer implements VideoFrameRenderer {
13
15
  return typeof MediaStreamTrackGenerator !== "undefined";
14
16
  }
15
17
 
18
+ get type() {
19
+ return "hardware" as const;
20
+ }
21
+
16
22
  #element: HTMLVideoElement;
17
23
  get element() {
18
24
  return this.#element;
19
25
  }
20
26
 
27
+ #options: InsertableStreamVideoFrameRenderer.Options | undefined;
28
+ get options() {
29
+ return this.#options;
30
+ }
31
+
21
32
  #generator: MediaStreamTrackGenerator;
22
- #writer: WritableStreamDefaultWriter<VideoFrame>;
33
+ get writable() {
34
+ return this.#generator.writable;
35
+ }
36
+
23
37
  #stream: MediaStream;
38
+ get stream() {
39
+ return this.#stream;
40
+ }
41
+
42
+ #abortController = new AbortController();
24
43
 
25
- constructor(element?: HTMLVideoElement) {
44
+ constructor(
45
+ element?: HTMLVideoElement,
46
+ options?: InsertableStreamVideoFrameRenderer.Options,
47
+ ) {
26
48
  if (element) {
27
49
  this.#element = element;
28
50
  } else if (typeof document !== "undefined") {
@@ -34,27 +56,45 @@ export class InsertableStreamVideoFrameRenderer implements VideoFrameRenderer {
34
56
  }
35
57
  this.#element.muted = true;
36
58
  this.#element.autoplay = true;
59
+ this.#element.playsInline = true;
37
60
  this.#element.disablePictureInPicture = true;
38
61
  this.#element.disableRemotePlayback = true;
39
62
 
63
+ this.#options = options;
64
+ if (options?.updateSize) {
65
+ this.#element.addEventListener(
66
+ "resize",
67
+ () => {
68
+ this.#element.width = this.#element.videoWidth;
69
+ this.#element.height = this.#element.videoHeight;
70
+ },
71
+ { signal: this.#abortController.signal },
72
+ );
73
+ }
74
+
40
75
  // The spec replaced `MediaStreamTrackGenerator` with `VideoTrackGenerator`.
41
76
  // But Chrome has not implemented it yet.
42
77
  // https://issues.chromium.org/issues/40058895
43
78
  this.#generator = new MediaStreamTrackGenerator({ kind: "video" });
44
- this.#writer = this.#generator.writable.getWriter();
79
+ this.#generator.contentHint = "motion";
45
80
 
46
81
  this.#stream = new MediaStream([this.#generator]);
47
82
  this.#element.srcObject = this.#stream;
48
83
  }
49
84
 
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
- }
85
+ dispose(): undefined {
86
+ this.#abortController.abort();
87
+ this.#generator.stop();
88
+ this.#stream.removeTrack(this.#generator);
89
+ this.#element.srcObject = null;
55
90
  }
91
+ }
56
92
 
57
- async draw(frame: VideoFrame): Promise<void> {
58
- await this.#writer.write(frame);
93
+ export namespace InsertableStreamVideoFrameRenderer {
94
+ export interface Options {
95
+ /**
96
+ * Whether to update the size of the video element when the size of the video frame changes.
97
+ */
98
+ updateSize?: boolean;
59
99
  }
60
100
  }
@@ -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
  }