@yume-chan/scrcpy-decoder-webcodecs 1.0.0 → 2.0.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 (42) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +1 -1
  3. package/esm/video/decoder.d.ts +6 -4
  4. package/esm/video/decoder.d.ts.map +1 -1
  5. package/esm/video/decoder.js +17 -74
  6. package/esm/video/decoder.js.map +1 -1
  7. package/esm/video/pool.d.ts +7 -0
  8. package/esm/video/pool.d.ts.map +1 -0
  9. package/esm/video/pool.js +30 -0
  10. package/esm/video/pool.js.map +1 -0
  11. package/esm/video/render/bitmap.d.ts +2 -2
  12. package/esm/video/render/bitmap.d.ts.map +1 -1
  13. package/esm/video/render/bitmap.js +2 -2
  14. package/esm/video/render/bitmap.js.map +1 -1
  15. package/esm/video/render/canvas.d.ts +2 -2
  16. package/esm/video/render/canvas.d.ts.map +1 -1
  17. package/esm/video/render/canvas.js +1 -1
  18. package/esm/video/render/canvas.js.map +1 -1
  19. package/esm/video/render/insertable-stream.d.ts +2 -2
  20. package/esm/video/render/insertable-stream.d.ts.map +1 -1
  21. package/esm/video/render/insertable-stream.js +1 -1
  22. package/esm/video/render/insertable-stream.js.map +1 -1
  23. package/esm/video/render/type.d.ts +1 -1
  24. package/esm/video/render/type.d.ts.map +1 -1
  25. package/esm/video/render/webgl.d.ts +2 -2
  26. package/esm/video/render/webgl.d.ts.map +1 -1
  27. package/esm/video/render/webgl.js +4 -4
  28. package/esm/video/render/webgl.js.map +1 -1
  29. package/esm/video/snapshot.d.ts +6 -0
  30. package/esm/video/snapshot.d.ts.map +1 -0
  31. package/esm/video/snapshot.js +41 -0
  32. package/esm/video/snapshot.js.map +1 -0
  33. package/package.json +8 -8
  34. package/src/video/decoder.ts +24 -88
  35. package/src/video/pool.ts +37 -0
  36. package/src/video/render/bitmap.ts +2 -2
  37. package/src/video/render/canvas.ts +2 -4
  38. package/src/video/render/insertable-stream.ts +2 -4
  39. package/src/video/render/type.ts +1 -1
  40. package/src/video/render/webgl.ts +4 -4
  41. package/src/video/snapshot.ts +41 -0
  42. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from "@yume-chan/event";
1
+ import { StickyEventEmitter } from "@yume-chan/event";
2
2
  import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
3
3
  import { ScrcpyVideoCodecId } from "@yume-chan/scrcpy";
4
4
  import type {
@@ -10,87 +10,9 @@ 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 { WebCodecsVideoDecoderRenderer } 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
- }
13
+ import { Pool } from "./pool.js";
14
+ import type { VideoFrameRenderer } from "./render/index.js";
15
+ import { VideoFrameCapturer } from "./snapshot.js";
94
16
 
95
17
  const VideoFrameCapturerPool =
96
18
  /* #__PURE__ */
@@ -123,7 +45,7 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
123
45
  #error: Error | undefined;
124
46
  #controller!: WritableStreamDefaultController;
125
47
 
126
- #renderer: WebCodecsVideoDecoderRenderer;
48
+ #renderer: VideoFrameRenderer;
127
49
  get renderer() {
128
50
  return this.#renderer;
129
51
  }
@@ -139,11 +61,21 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
139
61
  return this.#framesSkipped;
140
62
  }
141
63
 
142
- #sizeChanged = new EventEmitter<{ width: number; height: number }>();
64
+ #sizeChanged = new StickyEventEmitter<{ width: number; height: number }>();
143
65
  get sizeChanged() {
144
66
  return this.#sizeChanged.event;
145
67
  }
146
68
 
69
+ #width: number = 0;
70
+ get width() {
71
+ return this.#width;
72
+ }
73
+
74
+ #height: number = 0;
75
+ get height() {
76
+ return this.#height;
77
+ }
78
+
147
79
  #decoder: VideoDecoder;
148
80
 
149
81
  #drawing = false;
@@ -218,7 +150,7 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
218
150
  },
219
151
  });
220
152
 
221
- this.#onVerticalSync();
153
+ this.#handleAnimationFrame();
222
154
  }
223
155
 
224
156
  #setError(error: Error) {
@@ -261,16 +193,20 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
261
193
 
262
194
  #updateSize = (width: number, height: number) => {
263
195
  this.#renderer.setSize(width, height);
196
+ this.#width = width;
197
+ this.#height = height;
264
198
  this.#sizeChanged.fire({ width, height });
265
199
  };
266
200
 
267
- #onVerticalSync = () => {
201
+ #handleAnimationFrame = () => {
268
202
  if (this.#framesDraw > 0) {
269
203
  this.#framesPresented += 1;
270
204
  this.#framesSkipped += this.#framesDraw - 1;
271
205
  this.#framesDraw = 0;
272
206
  }
273
- this.#animationFrameId = requestAnimationFrame(this.#onVerticalSync);
207
+ this.#animationFrameId = requestAnimationFrame(
208
+ this.#handleAnimationFrame,
209
+ );
274
210
  };
275
211
 
276
212
  async snapshot() {
@@ -302,6 +238,6 @@ export namespace WebCodecsVideoDecoder {
302
238
  */
303
239
  codec: ScrcpyVideoCodecId;
304
240
 
305
- renderer: WebCodecsVideoDecoderRenderer;
241
+ renderer: VideoFrameRenderer;
306
242
  }
307
243
  }
@@ -0,0 +1,37 @@
1
+ export class Pool<T> {
2
+ #controller!: ReadableStreamDefaultController<T>;
3
+ #readable = new ReadableStream<T>(
4
+ {
5
+ start: (controller) => {
6
+ this.#controller = controller;
7
+ },
8
+ pull: (controller) => {
9
+ controller.enqueue(this.#initializer());
10
+ },
11
+ },
12
+ { highWaterMark: 0 },
13
+ );
14
+ #reader = this.#readable.getReader();
15
+
16
+ #initializer: () => T;
17
+
18
+ #size = 0;
19
+ #capacity: number;
20
+
21
+ constructor(initializer: () => T, capacity: number) {
22
+ this.#initializer = initializer;
23
+ this.#capacity = capacity;
24
+ }
25
+
26
+ async borrow() {
27
+ const result = await this.#reader.read();
28
+ return result.value!;
29
+ }
30
+
31
+ return(value: T) {
32
+ if (this.#size < this.#capacity) {
33
+ this.#controller.enqueue(value);
34
+ this.#size += 1;
35
+ }
36
+ }
37
+ }
@@ -1,6 +1,6 @@
1
- import { CanvasWebCodecsDecoderRenderer } from "./canvas.js";
1
+ import { CanvasVideoFrameRenderer } from "./canvas.js";
2
2
 
3
- export class BitmapWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRenderer {
3
+ export class BitmapVideoFrameRenderer extends CanvasVideoFrameRenderer {
4
4
  #context: ImageBitmapRenderingContext;
5
5
 
6
6
  constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
@@ -1,10 +1,8 @@
1
1
  import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
2
2
 
3
- import type { WebCodecsVideoDecoderRenderer } from "./type.js";
3
+ import type { VideoFrameRenderer } from "./type.js";
4
4
 
5
- export abstract class CanvasWebCodecsDecoderRenderer
6
- implements WebCodecsVideoDecoderRenderer
7
- {
5
+ export abstract class CanvasVideoFrameRenderer implements VideoFrameRenderer {
8
6
  #canvas: HTMLCanvasElement | OffscreenCanvas;
9
7
  get canvas() {
10
8
  return this.#canvas;
@@ -1,6 +1,6 @@
1
1
  // cspell: ignore insertable
2
2
 
3
- import type { WebCodecsVideoDecoderRenderer } from "./type.js";
3
+ import type { VideoFrameRenderer } from "./type.js";
4
4
 
5
5
  declare class MediaStreamTrackGenerator extends MediaStreamTrack {
6
6
  constructor(options: { kind: "audio" | "video" });
@@ -8,9 +8,7 @@ declare class MediaStreamTrackGenerator extends MediaStreamTrack {
8
8
  writable: WritableStream<VideoFrame>;
9
9
  }
10
10
 
11
- export class InsertableStreamWebCodecsDecoderRenderer
12
- implements WebCodecsVideoDecoderRenderer
13
- {
11
+ export class InsertableStreamVideoFrameRenderer implements VideoFrameRenderer {
14
12
  static get isSupported() {
15
13
  return typeof MediaStreamTrackGenerator !== "undefined";
16
14
  }
@@ -1,6 +1,6 @@
1
1
  import type { MaybePromiseLike } from "@yume-chan/async";
2
2
 
3
- export interface WebCodecsVideoDecoderRenderer {
3
+ export interface VideoFrameRenderer {
4
4
  setSize(width: number, height: number): void;
5
5
 
6
6
  draw(frame: VideoFrame): MaybePromiseLike<void>;
@@ -1,6 +1,6 @@
1
1
  import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
2
2
 
3
- import { CanvasWebCodecsDecoderRenderer } from "./canvas.js";
3
+ import { CanvasVideoFrameRenderer } from "./canvas.js";
4
4
 
5
5
  const Resolved = Promise.resolve();
6
6
 
@@ -24,7 +24,7 @@ function createContext(
24
24
  );
25
25
  }
26
26
 
27
- export class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRenderer {
27
+ export class WebGLVideoFrameRenderer extends CanvasVideoFrameRenderer {
28
28
  static vertexShaderSource = `
29
29
  attribute vec2 xy;
30
30
 
@@ -77,7 +77,7 @@ export class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRendere
77
77
  const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
78
78
  gl.shaderSource(
79
79
  vertexShader,
80
- WebGLWebCodecsDecoderRenderer.vertexShaderSource,
80
+ WebGLVideoFrameRenderer.vertexShaderSource,
81
81
  );
82
82
  gl.compileShader(vertexShader);
83
83
  if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
@@ -87,7 +87,7 @@ export class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRendere
87
87
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
88
88
  gl.shaderSource(
89
89
  fragmentShader,
90
- WebGLWebCodecsDecoderRenderer.fragmentShaderSource,
90
+ WebGLVideoFrameRenderer.fragmentShaderSource,
91
91
  );
92
92
  gl.compileShader(fragmentShader);
93
93
  if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
@@ -0,0 +1,41 @@
1
+ export class VideoFrameCapturer {
2
+ #canvas: OffscreenCanvas | HTMLCanvasElement;
3
+ #context: ImageBitmapRenderingContext;
4
+
5
+ constructor() {
6
+ if (typeof OffscreenCanvas !== "undefined") {
7
+ this.#canvas = new OffscreenCanvas(1, 1);
8
+ } else {
9
+ this.#canvas = document.createElement("canvas");
10
+ this.#canvas.width = 1;
11
+ this.#canvas.height = 1;
12
+ }
13
+ this.#context = this.#canvas.getContext("bitmaprenderer", {
14
+ alpha: false,
15
+ })!;
16
+ }
17
+
18
+ async capture(frame: VideoFrame): Promise<Blob> {
19
+ this.#canvas.width = frame.displayWidth;
20
+ this.#canvas.height = frame.displayHeight;
21
+
22
+ const bitmap = await createImageBitmap(frame);
23
+ this.#context.transferFromImageBitmap(bitmap);
24
+
25
+ if (this.#canvas instanceof OffscreenCanvas) {
26
+ return await this.#canvas.convertToBlob({
27
+ type: "image/png",
28
+ });
29
+ } else {
30
+ return new Promise((resolve, reject) => {
31
+ (this.#canvas as HTMLCanvasElement).toBlob((blob) => {
32
+ if (!blob) {
33
+ reject(new Error("Failed to convert canvas to blob"));
34
+ } else {
35
+ resolve(blob);
36
+ }
37
+ }, "image/png");
38
+ });
39
+ }
40
+ }
41
+ }