@yume-chan/scrcpy-decoder-webcodecs 0.0.23 → 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 (100) hide show
  1. package/CHANGELOG.md +40 -2
  2. package/LICENSE +1 -1
  3. package/README.md +49 -29
  4. package/esm/index.d.ts +1 -20
  5. package/esm/index.d.ts.map +1 -1
  6. package/esm/index.js +1 -181
  7. package/esm/index.js.map +1 -1
  8. package/esm/video/codec/av1.d.ts +8 -0
  9. package/esm/video/codec/av1.d.ts.map +1 -0
  10. package/esm/video/codec/av1.js +71 -0
  11. package/esm/video/codec/av1.js.map +1 -0
  12. package/esm/video/codec/h264.d.ts +7 -0
  13. package/esm/video/codec/h264.d.ts.map +1 -0
  14. package/esm/video/codec/h264.js +27 -0
  15. package/esm/video/codec/h264.js.map +1 -0
  16. package/esm/video/codec/h265.d.ts +7 -0
  17. package/esm/video/codec/h265.d.ts.map +1 -0
  18. package/esm/video/codec/h265.js +33 -0
  19. package/esm/video/codec/h265.js.map +1 -0
  20. package/esm/video/codec/h26x.d.ts +9 -0
  21. package/esm/video/codec/h26x.d.ts.map +1 -0
  22. package/esm/video/codec/h26x.js +35 -0
  23. package/esm/video/codec/h26x.js.map +1 -0
  24. package/esm/video/codec/index.d.ts +7 -0
  25. package/esm/video/codec/index.d.ts.map +1 -0
  26. package/esm/video/codec/index.js +7 -0
  27. package/esm/video/codec/index.js.map +1 -0
  28. package/esm/video/codec/type.d.ts +8 -0
  29. package/esm/video/codec/type.d.ts.map +1 -0
  30. package/esm/video/codec/type.js +2 -0
  31. package/esm/video/codec/type.js.map +1 -0
  32. package/esm/video/codec/utils.d.ts +4 -0
  33. package/esm/video/codec/utils.d.ts.map +1 -0
  34. package/esm/video/codec/utils.js +10 -0
  35. package/esm/video/codec/utils.js.map +1 -0
  36. package/esm/video/decoder.d.ts +35 -0
  37. package/esm/video/decoder.d.ts.map +1 -0
  38. package/esm/video/decoder.js +240 -0
  39. package/esm/video/decoder.js.map +1 -0
  40. package/esm/video/index.d.ts +4 -0
  41. package/esm/video/index.d.ts.map +1 -0
  42. package/esm/video/index.js +4 -0
  43. package/esm/video/index.js.map +1 -0
  44. package/esm/video/render/bitmap.d.ts +7 -0
  45. package/esm/video/render/bitmap.d.ts.map +1 -0
  46. package/esm/video/render/bitmap.js +15 -0
  47. package/esm/video/render/bitmap.js.map +1 -0
  48. package/esm/video/render/canvas.d.ts +9 -0
  49. package/esm/video/render/canvas.d.ts.map +1 -0
  50. package/esm/video/render/canvas.js +22 -0
  51. package/esm/video/render/canvas.js.map +1 -0
  52. package/esm/video/render/index.d.ts +6 -0
  53. package/esm/video/render/index.d.ts.map +1 -0
  54. package/esm/video/render/index.js +7 -0
  55. package/esm/video/render/index.js.map +1 -0
  56. package/esm/video/render/insertable-stream.d.ts +10 -0
  57. package/esm/video/render/insertable-stream.d.ts.map +1 -0
  58. package/esm/video/render/insertable-stream.js +45 -0
  59. package/esm/video/render/insertable-stream.js.map +1 -0
  60. package/esm/video/render/type.d.ts +6 -0
  61. package/esm/video/render/type.d.ts.map +1 -0
  62. package/esm/video/render/type.js +2 -0
  63. package/esm/video/render/type.js.map +1 -0
  64. package/esm/video/render/webgl.d.ts +17 -0
  65. package/esm/video/render/webgl.d.ts.map +1 -0
  66. package/esm/{webgl.js → video/render/webgl.js} +47 -26
  67. package/esm/video/render/webgl.js.map +1 -0
  68. package/package.json +12 -17
  69. package/src/index.ts +1 -247
  70. package/src/video/codec/av1.ts +101 -0
  71. package/src/video/codec/h264.ts +42 -0
  72. package/src/video/codec/h265.ts +50 -0
  73. package/src/video/codec/h26x.ts +45 -0
  74. package/src/video/codec/index.ts +6 -0
  75. package/src/video/codec/type.ts +12 -0
  76. package/src/video/codec/utils.ts +11 -0
  77. package/src/video/decoder.ts +307 -0
  78. package/src/video/index.ts +3 -0
  79. package/src/video/render/bitmap.ts +18 -0
  80. package/src/video/render/canvas.ts +29 -0
  81. package/src/video/render/index.ts +7 -0
  82. package/src/video/render/insertable-stream.ts +62 -0
  83. package/src/video/render/type.ts +7 -0
  84. package/src/{webgl.ts → video/render/webgl.ts} +67 -30
  85. package/tsconfig.build.json +4 -1
  86. package/tsconfig.build.tsbuildinfo +1 -1
  87. package/CHANGELOG.json +0 -74
  88. package/esm/bitmap.d.ts +0 -7
  89. package/esm/bitmap.d.ts.map +0 -1
  90. package/esm/bitmap.js +0 -17
  91. package/esm/bitmap.js.map +0 -1
  92. package/esm/renderer.d.ts +0 -4
  93. package/esm/renderer.d.ts.map +0 -1
  94. package/esm/renderer.js +0 -2
  95. package/esm/renderer.js.map +0 -1
  96. package/esm/webgl.d.ts +0 -9
  97. package/esm/webgl.d.ts.map +0 -1
  98. package/esm/webgl.js.map +0 -1
  99. package/src/bitmap.ts +0 -24
  100. package/src/renderer.ts +0 -3
@@ -1,47 +1,68 @@
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
- attribute vec2 xy;
19
+ attribute vec2 xy;
4
20
 
5
- varying highp vec2 uv;
21
+ varying highp vec2 uv;
6
22
 
7
- void main(void) {
8
- gl_Position = vec4(xy, 0.0, 1.0);
9
- // Map vertex coordinates (-1 to +1) to UV coordinates (0 to 1).
10
- // UV coordinates are Y-flipped relative to vertex coordinates.
11
- uv = vec2((1.0 + xy.x) / 2.0, (1.0 - xy.y) / 2.0);
12
- }
23
+ void main(void) {
24
+ gl_Position = vec4(xy, 0.0, 1.0);
25
+ // Map vertex coordinates (-1 to +1) to UV coordinates (0 to 1).
26
+ // UV coordinates are Y-flipped relative to vertex coordinates.
27
+ uv = vec2((1.0 + xy.x) / 2.0, (1.0 - xy.y) / 2.0);
28
+ }
13
29
  `;
14
30
  static fragmentShaderSource = `
15
- varying highp vec2 uv;
31
+ varying highp vec2 uv;
16
32
 
17
- uniform sampler2D texture;
33
+ uniform sampler2D texture;
18
34
 
19
- void main(void) {
20
- gl_FragColor = texture2D(texture, uv);
21
- }
35
+ void main(void) {
36
+ gl_FragColor = texture2D(texture, uv);
37
+ }
22
38
  `;
39
+ static get isSupported() {
40
+ const canvas = createCanvas();
41
+ return !!createContext(canvas);
42
+ }
23
43
  #context;
24
- constructor(canvas) {
25
- const gl = canvas.getContext("webgl2", {
26
- alpha: false,
27
- failIfMajorPerformanceCaveat: true,
28
- }) ||
29
- canvas.getContext("webgl", {
30
- alpha: false,
31
- failIfMajorPerformanceCaveat: true,
32
- });
44
+ /**
45
+ * Create a new WebGL frame renderer.
46
+ * @param canvas The canvas to render frames to.
47
+ * @param enableCapture
48
+ * Whether to allow capturing the canvas content using APIs like `readPixels` and `toDataURL`.
49
+ * Enable this option may reduce performance.
50
+ */
51
+ constructor(canvas, enableCapture) {
52
+ super(canvas);
53
+ const gl = createContext(this.canvas, enableCapture);
33
54
  if (!gl) {
34
55
  throw new Error("WebGL not supported");
35
56
  }
36
57
  this.#context = gl;
37
58
  const vertexShader = gl.createShader(gl.VERTEX_SHADER);
38
- gl.shaderSource(vertexShader, WebGLFrameRenderer.vertexShaderSource);
59
+ gl.shaderSource(vertexShader, WebGLWebCodecsDecoderRenderer.vertexShaderSource);
39
60
  gl.compileShader(vertexShader);
40
61
  if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
41
62
  throw new Error(gl.getShaderInfoLog(vertexShader));
42
63
  }
43
64
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
44
- gl.shaderSource(fragmentShader, WebGLFrameRenderer.fragmentShaderSource);
65
+ gl.shaderSource(fragmentShader, WebGLWebCodecsDecoderRenderer.fragmentShaderSource);
45
66
  gl.compileShader(fragmentShader);
46
67
  if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
47
68
  throw new Error(gl.getShaderInfoLog(fragmentShader));
@@ -72,9 +93,9 @@ void main(void) {
72
93
  draw(frame) {
73
94
  const gl = this.#context;
74
95
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
75
- frame.close();
76
96
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
77
97
  gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
98
+ return Resolved;
78
99
  }
79
100
  }
80
101
  //# sourceMappingURL=webgl.js.map
@@ -0,0 +1 @@
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.23",
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,28 +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.23",
31
- "@yume-chan/scrcpy": "^0.0.23",
32
- "@yume-chan/scrcpy-decoder-tinyh264": "^0.0.23",
33
- "@yume-chan/stream-extra": "^0.0.23",
34
- "tslib": "^2.6.2"
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.3",
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
- "eslint": "^8.57.0",
42
- "jest": "^30.0.0-alpha.3",
43
- "prettier": "^3.2.5",
44
- "ts-jest": "^29.1.2",
45
- "typescript": "^5.4.2"
42
+ "@yume-chan/tsconfig": "^1.0.0"
46
43
  },
47
44
  "scripts": {
48
45
  "build": "tsc -b tsconfig.build.json",
49
- "build:watch": "tsc -b tsconfig.build.json",
50
- "//test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
51
- "lint": "eslint src/**/*.ts --fix && prettier src/**/*.ts --write --tab-width 4"
46
+ "lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4"
52
47
  }
53
48
  }
package/src/index.ts CHANGED
@@ -1,247 +1 @@
1
- import { EventEmitter } from "@yume-chan/event";
2
- import type {
3
- ScrcpyMediaStreamDataPacket,
4
- ScrcpyMediaStreamPacket,
5
- } from "@yume-chan/scrcpy";
6
- import {
7
- ScrcpyVideoCodecId,
8
- h264ParseConfiguration,
9
- h265ParseConfiguration,
10
- } from "@yume-chan/scrcpy";
11
- import type {
12
- ScrcpyVideoDecoder,
13
- ScrcpyVideoDecoderCapability,
14
- } from "@yume-chan/scrcpy-decoder-tinyh264";
15
- import { WritableStream } from "@yume-chan/stream-extra";
16
-
17
- import { BitmapFrameRenderer } from "./bitmap.js";
18
- import type { FrameRenderer } from "./renderer.js";
19
- import { WebGLFrameRenderer } from "./webgl.js";
20
-
21
- function toHex(value: number) {
22
- return value.toString(16).padStart(2, "0").toUpperCase();
23
- }
24
-
25
- function toUint32Le(data: Uint8Array, offset: number) {
26
- return (
27
- data[offset]! |
28
- (data[offset + 1]! << 8) |
29
- (data[offset + 2]! << 16) |
30
- (data[offset + 3]! << 24)
31
- );
32
- }
33
-
34
- export class WebCodecsDecoder implements ScrcpyVideoDecoder {
35
- static isSupported() {
36
- return typeof globalThis.VideoDecoder !== "undefined";
37
- }
38
-
39
- static readonly capabilities: Record<string, ScrcpyVideoDecoderCapability> =
40
- {
41
- h264: {},
42
- h265: {},
43
- };
44
-
45
- #codec: ScrcpyVideoCodecId;
46
- get codec() {
47
- return this.#codec;
48
- }
49
-
50
- #writable: WritableStream<ScrcpyMediaStreamPacket>;
51
- get writable() {
52
- return this.#writable;
53
- }
54
-
55
- #canvas: HTMLCanvasElement;
56
- get renderer() {
57
- return this.#canvas;
58
- }
59
-
60
- #frameRendered = 0;
61
- get frameRendered() {
62
- return this.#frameRendered;
63
- }
64
-
65
- #frameSkipped = 0;
66
- get frameSkipped() {
67
- return this.#frameSkipped;
68
- }
69
-
70
- #sizeChanged = new EventEmitter<{ width: number; height: number }>();
71
- get sizeChanged() {
72
- return this.#sizeChanged.event;
73
- }
74
-
75
- #decoder: VideoDecoder;
76
- #config: Uint8Array | undefined;
77
- #renderer: FrameRenderer;
78
-
79
- #currentFrameRendered = false;
80
- #animationFrameId = 0;
81
-
82
- constructor(codec: ScrcpyVideoCodecId) {
83
- this.#codec = codec;
84
-
85
- this.#canvas = document.createElement("canvas");
86
-
87
- try {
88
- this.#renderer = new WebGLFrameRenderer(this.#canvas);
89
- } catch {
90
- this.#renderer = new BitmapFrameRenderer(this.#canvas);
91
- }
92
-
93
- this.#decoder = new VideoDecoder({
94
- output: (frame) => {
95
- if (this.#currentFrameRendered) {
96
- this.#frameRendered += 1;
97
- } else {
98
- this.#frameSkipped += 1;
99
- }
100
- this.#currentFrameRendered = false;
101
-
102
- // PERF: H.264 renderer may draw multiple frames in one vertical sync interval to minimize latency.
103
- // When multiple frames are drawn in one vertical sync interval,
104
- // only the last one is visible to users.
105
- // But this ensures users can always see the most up-to-date screen.
106
- // This is also the behavior of official Scrcpy client.
107
- // https://github.com/Genymobile/scrcpy/issues/3679
108
- this.#renderer.draw(frame);
109
- },
110
- error(e) {
111
- console.warn(
112
- "[@yume-chan/scrcpy-decoder-webcodecs]",
113
- "VideoDecoder error",
114
- e,
115
- );
116
- },
117
- });
118
-
119
- this.#writable = new WritableStream<ScrcpyMediaStreamPacket>({
120
- write: (packet) => {
121
- switch (packet.type) {
122
- case "configuration":
123
- this.#configure(packet.data);
124
- break;
125
- case "data":
126
- this.#decode(packet);
127
- break;
128
- }
129
- },
130
- });
131
-
132
- this.#onFramePresented();
133
- }
134
-
135
- #onFramePresented = () => {
136
- this.#currentFrameRendered = true;
137
- this.#animationFrameId = requestAnimationFrame(this.#onFramePresented);
138
- };
139
-
140
- #configure(data: Uint8Array) {
141
- switch (this.#codec) {
142
- case ScrcpyVideoCodecId.H264: {
143
- const {
144
- profileIndex,
145
- constraintSet,
146
- levelIndex,
147
- croppedWidth,
148
- croppedHeight,
149
- } = h264ParseConfiguration(data);
150
-
151
- this.#canvas.width = croppedWidth;
152
- this.#canvas.height = croppedHeight;
153
- this.#sizeChanged.fire({
154
- width: croppedWidth,
155
- height: croppedHeight,
156
- });
157
-
158
- // https://www.rfc-editor.org/rfc/rfc6381#section-3.3
159
- // ISO Base Media File Format Name Space
160
- const codec = `avc1.${[profileIndex, constraintSet, levelIndex]
161
- .map(toHex)
162
- .join("")}`;
163
- this.#decoder.configure({
164
- codec: codec,
165
- optimizeForLatency: true,
166
- });
167
- break;
168
- }
169
- case ScrcpyVideoCodecId.H265: {
170
- const {
171
- generalProfileSpace,
172
- generalProfileIndex,
173
- generalProfileCompatibilitySet,
174
- generalTierFlag,
175
- generalLevelIndex,
176
- generalConstraintSet,
177
- croppedWidth,
178
- croppedHeight,
179
- } = h265ParseConfiguration(data);
180
-
181
- this.#canvas.width = croppedWidth;
182
- this.#canvas.height = croppedHeight;
183
- this.#sizeChanged.fire({
184
- width: croppedWidth,
185
- height: croppedHeight,
186
- });
187
-
188
- const codec = [
189
- "hev1",
190
- ["", "A", "B", "C"][generalProfileSpace]! +
191
- generalProfileIndex.toString(),
192
- toUint32Le(generalProfileCompatibilitySet, 0).toString(16),
193
- (generalTierFlag ? "H" : "L") +
194
- generalLevelIndex.toString(),
195
- toUint32Le(generalConstraintSet, 0)
196
- .toString(16)
197
- .toUpperCase(),
198
- toUint32Le(generalConstraintSet, 4)
199
- .toString(16)
200
- .toUpperCase(),
201
- ].join(".");
202
- this.#decoder.configure({
203
- codec,
204
- optimizeForLatency: true,
205
- });
206
- break;
207
- }
208
- }
209
- this.#config = data;
210
- }
211
-
212
- #decode(packet: ScrcpyMediaStreamDataPacket) {
213
- if (this.#decoder.state !== "configured") {
214
- return;
215
- }
216
-
217
- // WebCodecs requires configuration data to be with the first frame.
218
- // https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type
219
- let data: Uint8Array;
220
- if (this.#config !== undefined) {
221
- data = new Uint8Array(
222
- this.#config.byteLength + packet.data.byteLength,
223
- );
224
- data.set(this.#config, 0);
225
- data.set(packet.data, this.#config.byteLength);
226
- this.#config = undefined;
227
- } else {
228
- data = packet.data;
229
- }
230
-
231
- this.#decoder.decode(
232
- new EncodedVideoChunk({
233
- // Treat `undefined` as `key`, otherwise won't decode.
234
- type: packet.keyframe === false ? "delta" : "key",
235
- timestamp: 0,
236
- data,
237
- }),
238
- );
239
- }
240
-
241
- dispose() {
242
- cancelAnimationFrame(this.#animationFrameId);
243
- if (this.#decoder.state !== "closed") {
244
- this.#decoder.close();
245
- }
246
- }
247
- }
1
+ export * from "./video/index.js";
@@ -0,0 +1,101 @@
1
+ import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
2
+ import { Av1 } from "@yume-chan/scrcpy";
3
+
4
+ import type { CodecDecoder } from "./type.js";
5
+ import { decimalTwoDigits } from "./utils.js";
6
+
7
+ export class Av1Codec implements CodecDecoder {
8
+ #decoder: VideoDecoder;
9
+ #updateSize: (width: number, height: number) => void;
10
+
11
+ constructor(
12
+ decoder: VideoDecoder,
13
+ updateSize: (width: number, height: number) => void,
14
+ ) {
15
+ this.#decoder = decoder;
16
+ this.#updateSize = updateSize;
17
+ }
18
+
19
+ #configure(data: Uint8Array) {
20
+ const parser = new Av1(data);
21
+ const sequenceHeader = parser.searchSequenceHeaderObu();
22
+
23
+ if (!sequenceHeader) {
24
+ return;
25
+ }
26
+
27
+ const {
28
+ seq_profile: seqProfile,
29
+ seq_level_idx: [seqLevelIdx = 0],
30
+ max_frame_width_minus_1,
31
+ max_frame_height_minus_1,
32
+ color_config: {
33
+ BitDepth,
34
+ mono_chrome: monoChrome,
35
+ subsampling_x: subsamplingX,
36
+ subsampling_y: subsamplingY,
37
+ chroma_sample_position: chromaSamplePosition,
38
+ color_description_present_flag,
39
+ },
40
+ } = sequenceHeader;
41
+
42
+ let colorPrimaries: Av1.ColorPrimaries;
43
+ let transferCharacteristics: Av1.TransferCharacteristics;
44
+ let matrixCoefficients: Av1.MatrixCoefficients;
45
+ let colorRange: boolean;
46
+ if (color_description_present_flag) {
47
+ ({
48
+ color_primaries: colorPrimaries,
49
+ transfer_characteristics: transferCharacteristics,
50
+ matrix_coefficients: matrixCoefficients,
51
+ color_range: colorRange,
52
+ } = sequenceHeader.color_config);
53
+ } else {
54
+ colorPrimaries = Av1.ColorPrimaries.Bt709;
55
+ transferCharacteristics = Av1.TransferCharacteristics.Bt709;
56
+ matrixCoefficients = Av1.MatrixCoefficients.Bt709;
57
+ colorRange = false;
58
+ }
59
+
60
+ const width = max_frame_width_minus_1 + 1;
61
+ const height = max_frame_height_minus_1 + 1;
62
+
63
+ this.#updateSize(width, height);
64
+
65
+ const codec = [
66
+ "av01",
67
+ seqProfile.toString(16),
68
+ decimalTwoDigits(seqLevelIdx) +
69
+ (sequenceHeader.seq_tier[0] ? "H" : "M"),
70
+ decimalTwoDigits(BitDepth),
71
+ monoChrome ? "1" : "0",
72
+ (subsamplingX ? "1" : "0") +
73
+ (subsamplingY ? "1" : "0") +
74
+ chromaSamplePosition.toString(),
75
+ decimalTwoDigits(colorPrimaries),
76
+ decimalTwoDigits(transferCharacteristics),
77
+ decimalTwoDigits(matrixCoefficients),
78
+ colorRange ? "1" : "0",
79
+ ].join(".");
80
+ this.#decoder.configure({
81
+ codec,
82
+ optimizeForLatency: true,
83
+ });
84
+ }
85
+
86
+ decode(packet: ScrcpyMediaStreamPacket): void {
87
+ if (packet.type === "configuration") {
88
+ return;
89
+ }
90
+
91
+ this.#configure(packet.data);
92
+ this.#decoder.decode(
93
+ new EncodedVideoChunk({
94
+ // Treat `undefined` as `key`, otherwise it won't decode.
95
+ type: packet.keyframe === false ? "delta" : "key",
96
+ timestamp: 0,
97
+ data: packet.data,
98
+ }),
99
+ );
100
+ }
101
+ }
@@ -0,0 +1,42 @@
1
+ import { h264ParseConfiguration } from "@yume-chan/scrcpy";
2
+
3
+ import { H26xDecoder } from "./h26x.js";
4
+ import { hexTwoDigits } from "./utils.js";
5
+
6
+ export class H264Decoder extends H26xDecoder {
7
+ #decoder: VideoDecoder;
8
+ #updateSize: (width: number, height: number) => void;
9
+
10
+ constructor(
11
+ decoder: VideoDecoder,
12
+ updateSize: (width: number, height: number) => void,
13
+ ) {
14
+ super(decoder);
15
+ this.#decoder = decoder;
16
+ this.#updateSize = updateSize;
17
+ }
18
+
19
+ override configure(data: Uint8Array): void {
20
+ const {
21
+ profileIndex,
22
+ constraintSet,
23
+ levelIndex,
24
+ croppedWidth,
25
+ croppedHeight,
26
+ } = h264ParseConfiguration(data);
27
+
28
+ this.#updateSize(croppedWidth, croppedHeight);
29
+
30
+ // https://www.rfc-editor.org/rfc/rfc6381#section-3.3
31
+ // ISO Base Media File Format Name Space
32
+ const codec =
33
+ "avc1." +
34
+ hexTwoDigits(profileIndex) +
35
+ hexTwoDigits(constraintSet) +
36
+ hexTwoDigits(levelIndex);
37
+ this.#decoder.configure({
38
+ codec: codec,
39
+ optimizeForLatency: true,
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,50 @@
1
+ import { getUint32LittleEndian } from "@yume-chan/no-data-view";
2
+ import { h265ParseConfiguration } from "@yume-chan/scrcpy";
3
+
4
+ import { H26xDecoder } from "./h26x.js";
5
+ import { hexDigits } from "./utils.js";
6
+
7
+ export class H265Decoder extends H26xDecoder {
8
+ #decoder: VideoDecoder;
9
+ #updateSize: (width: number, height: number) => void;
10
+
11
+ constructor(
12
+ decoder: VideoDecoder,
13
+ updateSize: (width: number, height: number) => void,
14
+ ) {
15
+ super(decoder);
16
+ this.#decoder = decoder;
17
+ this.#updateSize = updateSize;
18
+ }
19
+
20
+ override configure(data: Uint8Array): void {
21
+ const {
22
+ generalProfileSpace,
23
+ generalProfileIndex,
24
+ generalProfileCompatibilitySet,
25
+ generalTierFlag,
26
+ generalLevelIndex,
27
+ generalConstraintSet,
28
+ croppedWidth,
29
+ croppedHeight,
30
+ } = h265ParseConfiguration(data);
31
+
32
+ this.#updateSize(croppedWidth, croppedHeight);
33
+
34
+ const codec = [
35
+ "hev1",
36
+ ["", "A", "B", "C"][generalProfileSpace]! +
37
+ generalProfileIndex.toString(),
38
+ hexDigits(getUint32LittleEndian(generalProfileCompatibilitySet, 0)),
39
+ (generalTierFlag ? "H" : "L") + generalLevelIndex.toString(),
40
+ ...Array.from(generalConstraintSet, hexDigits),
41
+ ].join(".");
42
+ this.#decoder.configure({
43
+ codec,
44
+ // Microsoft Edge requires explicit size to work
45
+ codedWidth: croppedWidth,
46
+ codedHeight: croppedHeight,
47
+ optimizeForLatency: true,
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,45 @@
1
+ import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
2
+
3
+ import type { CodecDecoder } from "./type.js";
4
+
5
+ export abstract class H26xDecoder implements CodecDecoder {
6
+ #config: Uint8Array | undefined;
7
+ #decoder: VideoDecoder;
8
+
9
+ constructor(decoder: VideoDecoder) {
10
+ this.#decoder = decoder;
11
+ }
12
+
13
+ abstract configure(data: Uint8Array): void;
14
+
15
+ decode(packet: ScrcpyMediaStreamPacket): void {
16
+ if (packet.type === "configuration") {
17
+ this.#config = packet.data;
18
+ this.configure(packet.data);
19
+ return;
20
+ }
21
+
22
+ // For H.264 and H.265, when the stream is in Annex B format
23
+ // (which Scrcpy uses, as Android MediaCodec produces),
24
+ // configuration data needs to be combined with the first frame data.
25
+ // https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type
26
+ let data: Uint8Array;
27
+ if (this.#config !== undefined) {
28
+ data = new Uint8Array(this.#config.length + packet.data.length);
29
+ data.set(this.#config, 0);
30
+ data.set(packet.data, this.#config.length);
31
+ this.#config = undefined;
32
+ } else {
33
+ data = packet.data;
34
+ }
35
+
36
+ this.#decoder.decode(
37
+ new EncodedVideoChunk({
38
+ // Treat `undefined` as `key`, otherwise won't decode.
39
+ type: packet.keyframe === false ? "delta" : "key",
40
+ timestamp: 0,
41
+ data,
42
+ }),
43
+ );
44
+ }
45
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./av1.js";
2
+ export * from "./h264.js";
3
+ export * from "./h265.js";
4
+ export * from "./h26x.js";
5
+ export * from "./type.js";
6
+ export * from "./utils.js";
@@ -0,0 +1,12 @@
1
+ import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
2
+
3
+ export interface CodecDecoder {
4
+ decode(packet: ScrcpyMediaStreamPacket): void;
5
+ }
6
+
7
+ export interface CodecDecoderConstructor {
8
+ new (
9
+ decoder: VideoDecoder,
10
+ updateSize: (width: number, height: number) => void,
11
+ ): CodecDecoder;
12
+ }
@@ -0,0 +1,11 @@
1
+ export function hexDigits(value: number) {
2
+ return value.toString(16).toUpperCase();
3
+ }
4
+
5
+ export function hexTwoDigits(value: number) {
6
+ return value.toString(16).toUpperCase().padStart(2, "0");
7
+ }
8
+
9
+ export function decimalTwoDigits(value: number) {
10
+ return value.toString(10).padStart(2, "0");
11
+ }