@yume-chan/scrcpy-decoder-webcodecs 0.0.24 → 1.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 (43) hide show
  1. package/CHANGELOG.md +31 -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 +29 -0
  37. package/src/video/render/index.ts +4 -0
  38. package/src/video/render/insertable-stream.ts +62 -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
@@ -0,0 +1,22 @@
1
+ import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
2
+ export class CanvasWebCodecsDecoderRenderer {
3
+ #canvas;
4
+ get canvas() {
5
+ return this.#canvas;
6
+ }
7
+ constructor(canvas) {
8
+ if (canvas) {
9
+ this.#canvas = canvas;
10
+ }
11
+ else {
12
+ this.#canvas = createCanvas();
13
+ }
14
+ }
15
+ setSize(width, height) {
16
+ if (this.#canvas.width !== width || this.#canvas.height !== height) {
17
+ this.#canvas.width = width;
18
+ this.#canvas.height = height;
19
+ }
20
+ }
21
+ }
22
+ //# sourceMappingURL=canvas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/video/render/canvas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAIlE,MAAM,OAAgB,8BAA8B;IAGhD,OAAO,CAAsC;IAC7C,IAAI,MAAM;QACN,OAAO,IAAI,CAAC,OAAO,CAAC;IACxB,CAAC;IAED,YAAY,MAA4C;QACpD,IAAI,MAAM,EAAE,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QAC1B,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,OAAO,GAAG,YAAY,EAAE,CAAC;QAClC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,KAAa,EAAE,MAAc;QACjC,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,KAAK,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YACjE,IAAI,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;YAC3B,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;QACjC,CAAC;IACL,CAAC;CAGJ"}
@@ -1,4 +1,6 @@
1
1
  export * from "./bitmap.js";
2
+ export * from "./canvas.js";
3
+ export * from "./insertable-stream.js";
2
4
  export * from "./type.js";
3
5
  export * from "./webgl.js";
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/video/render/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/video/render/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
@@ -1,4 +1,7 @@
1
+ // cspell: ignore insertable
1
2
  export * from "./bitmap.js";
3
+ export * from "./canvas.js";
4
+ export * from "./insertable-stream.js";
2
5
  export * from "./type.js";
3
6
  export * from "./webgl.js";
4
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/video/render/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/video/render/index.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAE5B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { WebCodecsVideoDecoderRenderer } from "./type.js";
2
+ export declare class InsertableStreamWebCodecsDecoderRenderer implements WebCodecsVideoDecoderRenderer {
3
+ #private;
4
+ static get isSupported(): boolean;
5
+ get element(): HTMLVideoElement;
6
+ constructor(element?: HTMLVideoElement);
7
+ setSize(width: number, height: number): void;
8
+ draw(frame: VideoFrame): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=insertable-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insertable-stream.d.ts","sourceRoot":"","sources":["../../../src/video/render/insertable-stream.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAQ/D,qBAAa,wCACT,YAAW,6BAA6B;;IAExC,MAAM,KAAK,WAAW,YAErB;IAGD,IAAI,OAAO,qBAEV;gBAMW,OAAO,CAAC,EAAE,gBAAgB;IAyBtC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAOtC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CAG/C"}
@@ -0,0 +1,45 @@
1
+ // cspell: ignore insertable
2
+ export class InsertableStreamWebCodecsDecoderRenderer {
3
+ static get isSupported() {
4
+ return typeof MediaStreamTrackGenerator !== "undefined";
5
+ }
6
+ #element;
7
+ get element() {
8
+ return this.#element;
9
+ }
10
+ #generator;
11
+ #writer;
12
+ #stream;
13
+ constructor(element) {
14
+ if (element) {
15
+ this.#element = element;
16
+ }
17
+ else if (typeof document !== "undefined") {
18
+ this.#element = document.createElement("video");
19
+ }
20
+ else {
21
+ throw new Error("no video element input found nor any video element can be created");
22
+ }
23
+ this.#element.muted = true;
24
+ this.#element.autoplay = true;
25
+ this.#element.disablePictureInPicture = true;
26
+ this.#element.disableRemotePlayback = true;
27
+ // The spec replaced `MediaStreamTrackGenerator` with `VideoTrackGenerator`.
28
+ // But Chrome has not implemented it yet.
29
+ // https://issues.chromium.org/issues/40058895
30
+ this.#generator = new MediaStreamTrackGenerator({ kind: "video" });
31
+ this.#writer = this.#generator.writable.getWriter();
32
+ this.#stream = new MediaStream([this.#generator]);
33
+ this.#element.srcObject = this.#stream;
34
+ }
35
+ setSize(width, height) {
36
+ if (this.#element.width !== width || this.#element.height !== height) {
37
+ this.#element.width = width;
38
+ this.#element.height = height;
39
+ }
40
+ }
41
+ async draw(frame) {
42
+ await this.#writer.write(frame);
43
+ }
44
+ }
45
+ //# sourceMappingURL=insertable-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insertable-stream.js","sourceRoot":"","sources":["../../../src/video/render/insertable-stream.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAU5B,MAAM,OAAO,wCAAwC;IAGjD,MAAM,KAAK,WAAW;QAClB,OAAO,OAAO,yBAAyB,KAAK,WAAW,CAAC;IAC5D,CAAC;IAED,QAAQ,CAAmB;IAC3B,IAAI,OAAO;QACP,OAAO,IAAI,CAAC,QAAQ,CAAC;IACzB,CAAC;IAED,UAAU,CAA4B;IACtC,OAAO,CAA0C;IACjD,OAAO,CAAc;IAErB,YAAY,OAA0B;QAClC,IAAI,OAAO,EAAE,CAAC;YACV,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QAC5B,CAAC;aAAM,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;YACzC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACpD,CAAC;aAAM,CAAC;YACJ,MAAM,IAAI,KAAK,CACX,mEAAmE,CACtE,CAAC;QACN,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAAC;QAC9B,IAAI,CAAC,QAAQ,CAAC,uBAAuB,GAAG,IAAI,CAAC;QAC7C,IAAI,CAAC,QAAQ,CAAC,qBAAqB,GAAG,IAAI,CAAC;QAE3C,4EAA4E;QAC5E,yCAAyC;QACzC,8CAA8C;QAC9C,IAAI,CAAC,UAAU,GAAG,IAAI,yBAAyB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;QAEpD,IAAI,CAAC,OAAO,GAAG,IAAI,WAAW,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC;IAC3C,CAAC;IAED,OAAO,CAAC,KAAa,EAAE,MAAc;QACjC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YACnE,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC;QAClC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAiB;QACxB,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;CACJ"}
@@ -1,4 +1,6 @@
1
- export interface FrameRenderer {
2
- draw(frame: VideoFrame): void;
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+ export interface WebCodecsVideoDecoderRenderer {
3
+ setSize(width: number, height: number): void;
4
+ draw(frame: VideoFrame): MaybePromiseLike<void>;
3
5
  }
4
6
  //# sourceMappingURL=type.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/video/render/type.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC1B,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CACjC"}
1
+ {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/video/render/type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,MAAM,WAAW,6BAA6B;IAC1C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;CACnD"}
@@ -1,8 +1,9 @@
1
- import type { FrameRenderer } from "./type.js";
2
- export declare class WebGLFrameRenderer implements FrameRenderer {
1
+ import { CanvasWebCodecsDecoderRenderer } from "./canvas.js";
2
+ export declare class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRenderer {
3
3
  #private;
4
4
  static vertexShaderSource: string;
5
5
  static fragmentShaderSource: string;
6
+ static get isSupported(): boolean;
6
7
  /**
7
8
  * Create a new WebGL frame renderer.
8
9
  * @param canvas The canvas to render frames to.
@@ -10,7 +11,7 @@ export declare class WebGLFrameRenderer implements FrameRenderer {
10
11
  * Whether to allow capturing the canvas content using APIs like `readPixels` and `toDataURL`.
11
12
  * Enable this option may reduce performance.
12
13
  */
13
- constructor(canvas: HTMLCanvasElement, enableCapture: boolean);
14
- draw(frame: VideoFrame): void;
14
+ constructor(canvas?: HTMLCanvasElement | OffscreenCanvas, enableCapture?: boolean);
15
+ draw(frame: VideoFrame): Promise<void>;
15
16
  }
16
17
  //# sourceMappingURL=webgl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webgl.d.ts","sourceRoot":"","sources":["../../../src/video/render/webgl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,qBAAa,kBAAmB,YAAW,aAAa;;IACpD,MAAM,CAAC,kBAAkB,SAW3B;IAEE,MAAM,CAAC,oBAAoB,SAQ7B;IAIE;;;;;;OAMG;gBACS,MAAM,EAAE,iBAAiB,EAAE,aAAa,EAAE,OAAO;IAiE7D,IAAI,CAAC,KAAK,EAAE,UAAU;CAezB"}
1
+ {"version":3,"file":"webgl.d.ts","sourceRoot":"","sources":["../../../src/video/render/webgl.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAwB7D,qBAAa,6BAA8B,SAAQ,8BAA8B;;IAC7E,MAAM,CAAC,kBAAkB,SAW3B;IAEE,MAAM,CAAC,oBAAoB,SAQ7B;IAEE,MAAM,KAAK,WAAW,YAGrB;IAID;;;;;;OAMG;gBAEC,MAAM,CAAC,EAAE,iBAAiB,GAAG,eAAe,EAC5C,aAAa,CAAC,EAAE,OAAO;IA6D3B,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBzC"}
@@ -1,4 +1,20 @@
1
- export class WebGLFrameRenderer {
1
+ import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
2
+ import { CanvasWebCodecsDecoderRenderer } from "./canvas.js";
3
+ const Resolved = Promise.resolve();
4
+ function createContext(canvas, enableCapture) {
5
+ const attributes = {
6
+ // Low-power GPU should be enough for video rendering.
7
+ powerPreference: "low-power",
8
+ alpha: false,
9
+ // Disallow software rendering.
10
+ // Other rendering methods are faster than software-based WebGL.
11
+ failIfMajorPerformanceCaveat: true,
12
+ preserveDrawingBuffer: !!enableCapture,
13
+ };
14
+ return (canvas.getContext("webgl2", attributes) ||
15
+ canvas.getContext("webgl", attributes));
16
+ }
17
+ export class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRenderer {
2
18
  static vertexShaderSource = `
3
19
  attribute vec2 xy;
4
20
 
@@ -20,6 +36,10 @@ export class WebGLFrameRenderer {
20
36
  gl_FragColor = texture2D(texture, uv);
21
37
  }
22
38
  `;
39
+ static get isSupported() {
40
+ const canvas = createCanvas();
41
+ return !!createContext(canvas);
42
+ }
23
43
  #context;
24
44
  /**
25
45
  * Create a new WebGL frame renderer.
@@ -29,28 +49,20 @@ export class WebGLFrameRenderer {
29
49
  * Enable this option may reduce performance.
30
50
  */
31
51
  constructor(canvas, enableCapture) {
32
- const gl = canvas.getContext("webgl2", {
33
- alpha: false,
34
- failIfMajorPerformanceCaveat: true,
35
- preserveDrawingBuffer: enableCapture,
36
- }) ||
37
- canvas.getContext("webgl", {
38
- alpha: false,
39
- failIfMajorPerformanceCaveat: true,
40
- preserveDrawingBuffer: enableCapture,
41
- });
52
+ super(canvas);
53
+ const gl = createContext(this.canvas, enableCapture);
42
54
  if (!gl) {
43
55
  throw new Error("WebGL not supported");
44
56
  }
45
57
  this.#context = gl;
46
58
  const vertexShader = gl.createShader(gl.VERTEX_SHADER);
47
- gl.shaderSource(vertexShader, WebGLFrameRenderer.vertexShaderSource);
59
+ gl.shaderSource(vertexShader, WebGLWebCodecsDecoderRenderer.vertexShaderSource);
48
60
  gl.compileShader(vertexShader);
49
61
  if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
50
62
  throw new Error(gl.getShaderInfoLog(vertexShader));
51
63
  }
52
64
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
53
- gl.shaderSource(fragmentShader, WebGLFrameRenderer.fragmentShaderSource);
65
+ gl.shaderSource(fragmentShader, WebGLWebCodecsDecoderRenderer.fragmentShaderSource);
54
66
  gl.compileShader(fragmentShader);
55
67
  if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
56
68
  throw new Error(gl.getShaderInfoLog(fragmentShader));
@@ -81,9 +93,9 @@ export class WebGLFrameRenderer {
81
93
  draw(frame) {
82
94
  const gl = this.#context;
83
95
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
84
- frame.close();
85
96
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
86
97
  gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
98
+ return Resolved;
87
99
  }
88
100
  }
89
101
  //# sourceMappingURL=webgl.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"webgl.js","sourceRoot":"","sources":["../../../src/video/render/webgl.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webgl.js","sourceRoot":"","sources":["../../../src/video/render/webgl.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yume-chan/scrcpy-decoder-webcodecs",
3
- "version": "0.0.24",
3
+ "version": "1.0.0",
4
4
  "description": "Raw H.264 stream decoder and renderer using WebCodecs API (requires modern browser).",
5
5
  "keywords": [
6
6
  "adb",
@@ -26,26 +26,23 @@
26
26
  "type": "module",
27
27
  "main": "esm/index.js",
28
28
  "types": "esm/index.d.ts",
29
+ "sideEffects": false,
29
30
  "dependencies": {
30
- "@yume-chan/event": "^0.0.24",
31
- "@yume-chan/no-data-view": "^0.0.24",
32
- "@yume-chan/scrcpy": "^0.0.24",
33
- "@yume-chan/scrcpy-decoder-tinyh264": "^0.0.24",
34
- "@yume-chan/stream-extra": "^0.0.24"
31
+ "@yume-chan/async": "^4.0.2",
32
+ "@yume-chan/no-data-view": "^1.0.0",
33
+ "@yume-chan/stream-extra": "^1.0.0",
34
+ "@yume-chan/scrcpy": "^1.0.0",
35
+ "@yume-chan/scrcpy-decoder-tinyh264": "^1.0.0",
36
+ "@yume-chan/event": "^1.0.0"
35
37
  },
36
38
  "devDependencies": {
37
- "@jest/globals": "^30.0.0-alpha.4",
39
+ "prettier": "^3.4.1",
40
+ "typescript": "^5.7.2",
38
41
  "@yume-chan/eslint-config": "^1.0.0",
39
- "@yume-chan/tsconfig": "^1.0.0",
40
- "cross-env": "^7.0.3",
41
- "jest": "^30.0.0-alpha.4",
42
- "prettier": "^3.3.2",
43
- "ts-jest": "^29.1.4",
44
- "typescript": "^5.4.5"
42
+ "@yume-chan/tsconfig": "^1.0.0"
45
43
  },
46
44
  "scripts": {
47
45
  "build": "tsc -b tsconfig.build.json",
48
- "build:watch": "tsc -b tsconfig.build.json",
49
46
  "lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4"
50
47
  }
51
48
  }
@@ -41,6 +41,9 @@ export class H265Decoder extends H26xDecoder {
41
41
  ].join(".");
42
42
  this.#decoder.configure({
43
43
  codec,
44
+ // Microsoft Edge requires explicit size to work
45
+ codedWidth: croppedWidth,
46
+ codedHeight: croppedHeight,
44
47
  optimizeForLatency: true,
45
48
  });
46
49
  }
@@ -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 { 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
+ }
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: WebCodecsVideoDecoderRenderer;
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: WebCodecsVideoDecoderRenderer;
178
306
  }
179
307
  }