@yume-chan/scrcpy-decoder-h264bsd 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.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/esm/core.d.ts +12 -0
- package/esm/core.d.ts.map +1 -0
- package/esm/core.js +112 -0
- package/esm/core.js.map +1 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +2 -0
- package/esm/index.js.map +1 -0
- package/esm/main.d.ts +81 -0
- package/esm/main.d.ts.map +1 -0
- package/esm/main.js +250 -0
- package/esm/main.js.map +1 -0
- package/esm/worker.d.ts +2 -0
- package/esm/worker.d.ts.map +1 -0
- package/esm/worker.js +5 -0
- package/esm/worker.js.map +1 -0
- package/package.json +52 -0
- package/src/core.ts +147 -0
- package/src/index.ts +1 -0
- package/src/libs.d.ts +16 -0
- package/src/main.ts +368 -0
- package/src/worker.ts +7 -0
- package/tsconfig.build.json +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 Simon Chan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img alt="Tango" src="https://raw.githubusercontent.com/yume-chan/ya-webadb/main/.github/logo.svg" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@yume-chan/scrcpy-decoder-h264bsd</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Raw H.264 stream decoder and renderer wrapping H264BSD and YUVCanvas packages (software decoding).
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/yume-chan/ya-webadb/blob/main/LICENSE">
|
|
13
|
+
<img alt="MIT License" src="https://img.shields.io/github/license/yume-chan/ya-webadb">
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://github.com/yume-chan/ya-webadb/releases">
|
|
16
|
+
<img alt="GitHub release" src="https://img.shields.io/github/v/release/yume-chan/ya-webadb?logo=github">
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://bundlephobia.com/package/@yume-chan/scrcpy-decoder-h264bsd">
|
|
19
|
+
<img alt="Package Size" src="https://img.shields.io/bundlephobia/minzip/%40yume-chan%2Fscrcpy-decoder-h264bsd">
|
|
20
|
+
</a>
|
|
21
|
+
<a href="https://www.npmjs.com/package/@yume-chan/scrcpy-decoder-h264bsd">
|
|
22
|
+
<img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/scrcpy-decoder-h264bsd?logo=npm">
|
|
23
|
+
</a>
|
|
24
|
+
<a href="https://tangoadb.dev/discord">
|
|
25
|
+
<img alt="Discord" src="https://img.shields.io/discord/1120215514732564502?logo=discord&logoColor=%23ffffff&label=Discord">
|
|
26
|
+
</a>
|
|
27
|
+
<a href="https://tangoadb.dev/wechat">
|
|
28
|
+
<img alt="WeChat Group" src="https://img.shields.io/badge/WeChat-Group-%2307C160?logo=wechat&logoColor=white">
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
This package is part of [Tango ADB](https://github.com/yume-chan/ya-webadb). Generally you need multiple packages to build a complete ADB client that can run on Web browsers and Node.js. Read the documentation for more information.
|
|
33
|
+
|
|
34
|
+
## Documentation
|
|
35
|
+
|
|
36
|
+
Check the latest documentation at https://tangoadb.dev/scrcpy/video/tiny-h264/
|
|
37
|
+
|
|
38
|
+
## Sponsors
|
|
39
|
+
|
|
40
|
+
[Become a backer](https://opencollective.com/ya-webadb) and get your image on our README on GitHub with a link to your site.
|
|
41
|
+
|
|
42
|
+
<a href="https://opencollective.com/ya-webadb/backer/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/0/avatar.svg?requireActive=false"></a>
|
|
43
|
+
<a href="https://opencollective.com/ya-webadb/backer/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/1/avatar.svg?requireActive=false"></a>
|
|
44
|
+
<a href="https://opencollective.com/ya-webadb/backer/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/2/avatar.svg?requireActive=false"></a>
|
|
45
|
+
<a href="https://opencollective.com/ya-webadb/backer/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/3/avatar.svg?requireActive=false"></a>
|
|
46
|
+
<a href="https://opencollective.com/ya-webadb/backer/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/4/avatar.svg?requireActive=false"></a>
|
|
47
|
+
<a href="https://opencollective.com/ya-webadb/backer/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/5/avatar.svg?requireActive=false"></a>
|
|
48
|
+
<a href="https://opencollective.com/ya-webadb/backer/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/6/avatar.svg?requireActive=false"></a>
|
|
49
|
+
<a href="https://opencollective.com/ya-webadb/backer/7/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/7/avatar.svg?requireActive=false"></a>
|
|
50
|
+
<a href="https://opencollective.com/ya-webadb/backer/8/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/8/avatar.svg?requireActive=false"></a>
|
|
51
|
+
<a href="https://opencollective.com/ya-webadb/backer/9/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/9/avatar.svg?requireActive=false"></a>
|
|
52
|
+
<a href="https://opencollective.com/ya-webadb/backer/10/website?requireActive=false" target="_blank"><img src="https://opencollective.com/ya-webadb/backer/10/avatar.svg?requireActive=false"></a>
|
package/esm/core.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class DecoderRenderer {
|
|
2
|
+
#private;
|
|
3
|
+
constructor(canvas: HTMLCanvasElement | OffscreenCanvas, webGl: boolean, onSizeChanged: (size: {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}) => unknown);
|
|
7
|
+
initialize(): Promise<void>;
|
|
8
|
+
decode(data: Uint8Array, skipRendering: boolean): number;
|
|
9
|
+
flush(skipRendering: boolean): number;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAqBA,qBAAa,eAAe;;gBASpB,MAAM,EAAE,iBAAiB,GAAG,eAAe,EAC3C,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO;IAYjE,UAAU;IAwDhB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,OAAO;IA8B/C,KAAK,CAAC,aAAa,EAAE,OAAO;IAS5B,OAAO;CAOV"}
|
package/esm/core.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import initialize from "@yume-chan/h264bsd";
|
|
2
|
+
import { ScrcpyVideoSizeImpl } from "@yume-chan/scrcpy";
|
|
3
|
+
import YUVCanvas from "yuv-canvas";
|
|
4
|
+
let h264bsdModulePromise;
|
|
5
|
+
let h264bsdModule;
|
|
6
|
+
async function initializeH264bsd() {
|
|
7
|
+
if (!h264bsdModulePromise) {
|
|
8
|
+
h264bsdModulePromise = initialize().then((module) => (h264bsdModule = module));
|
|
9
|
+
}
|
|
10
|
+
return h264bsdModulePromise;
|
|
11
|
+
}
|
|
12
|
+
export class DecoderRenderer {
|
|
13
|
+
#decoder;
|
|
14
|
+
#canvas;
|
|
15
|
+
#renderer;
|
|
16
|
+
#size;
|
|
17
|
+
constructor(canvas, webGl, onSizeChanged) {
|
|
18
|
+
this.#canvas = canvas;
|
|
19
|
+
// yuv-canvas supports detecting WebGL support by creating a <canvas> itself
|
|
20
|
+
// But this doesn't work in Web Worker (with OffscreenCanvas)
|
|
21
|
+
// so we implement our own check here
|
|
22
|
+
this.#renderer = YUVCanvas.attach(canvas, { webGL: webGl });
|
|
23
|
+
this.#size = new ScrcpyVideoSizeImpl();
|
|
24
|
+
this.#size.sizeChanged(onSizeChanged);
|
|
25
|
+
}
|
|
26
|
+
async initialize() {
|
|
27
|
+
const h264bsd = await initializeH264bsd();
|
|
28
|
+
this.#decoder = new h264bsd.Decoder();
|
|
29
|
+
}
|
|
30
|
+
#draw(picture) {
|
|
31
|
+
const width = picture.info.picWidth;
|
|
32
|
+
const height = picture.info.picHeight;
|
|
33
|
+
this.#size.setSize(width, height);
|
|
34
|
+
const chromaWidth = width / 2;
|
|
35
|
+
const chromaHeight = height / 2;
|
|
36
|
+
const uOffset = width * height;
|
|
37
|
+
const vOffset = uOffset + chromaWidth * chromaHeight;
|
|
38
|
+
this.#renderer.drawFrame({
|
|
39
|
+
format: {
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
chromaWidth,
|
|
43
|
+
chromaHeight,
|
|
44
|
+
cropLeft: picture.info.cropParams.cropLeftOffset,
|
|
45
|
+
cropWidth: picture.info.cropParams.cropOutWidth,
|
|
46
|
+
cropTop: picture.info.cropParams.cropTopOffset,
|
|
47
|
+
cropHeight: picture.info.cropParams.cropOutHeight,
|
|
48
|
+
displayWidth: picture.info.cropParams.cropOutWidth,
|
|
49
|
+
displayHeight: picture.info.cropParams.cropOutHeight,
|
|
50
|
+
},
|
|
51
|
+
y: {
|
|
52
|
+
bytes: picture.bytes.subarray(0, uOffset),
|
|
53
|
+
stride: width,
|
|
54
|
+
},
|
|
55
|
+
u: {
|
|
56
|
+
bytes: picture.bytes.subarray(uOffset, vOffset),
|
|
57
|
+
stride: chromaWidth,
|
|
58
|
+
},
|
|
59
|
+
v: {
|
|
60
|
+
bytes: picture.bytes.subarray(vOffset),
|
|
61
|
+
stride: chromaWidth,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
#getLastPicture(result) {
|
|
66
|
+
let { picture } = result;
|
|
67
|
+
if (picture) {
|
|
68
|
+
for (let i = 0; i < result.extraPictureCount; i += 1) {
|
|
69
|
+
picture = this.#decoder.getNextPicture();
|
|
70
|
+
}
|
|
71
|
+
return [result.extraPictureCount + 1, picture];
|
|
72
|
+
}
|
|
73
|
+
return [0, undefined];
|
|
74
|
+
}
|
|
75
|
+
decode(data, skipRendering) {
|
|
76
|
+
let offset = 0;
|
|
77
|
+
let length = data.length;
|
|
78
|
+
let picture;
|
|
79
|
+
let framesDecoded = 0;
|
|
80
|
+
while (length) {
|
|
81
|
+
const result = this.#decoder.decode(data.subarray(offset), 0);
|
|
82
|
+
if (result.code >= h264bsdModule.DecodeResultCode.Error) {
|
|
83
|
+
throw new Error("Decode failed. Code: " + result.code);
|
|
84
|
+
}
|
|
85
|
+
let pictureCount;
|
|
86
|
+
[pictureCount, picture] = this.#getLastPicture(result);
|
|
87
|
+
framesDecoded += pictureCount;
|
|
88
|
+
offset += result.read;
|
|
89
|
+
length -= result.read;
|
|
90
|
+
}
|
|
91
|
+
if (!picture || skipRendering) {
|
|
92
|
+
return framesDecoded;
|
|
93
|
+
}
|
|
94
|
+
this.#draw(picture);
|
|
95
|
+
return framesDecoded;
|
|
96
|
+
}
|
|
97
|
+
flush(skipRendering) {
|
|
98
|
+
const result = this.#decoder.flush();
|
|
99
|
+
const [pictureCount, picture] = this.#getLastPicture(result);
|
|
100
|
+
if (picture && !skipRendering) {
|
|
101
|
+
this.#draw(picture);
|
|
102
|
+
}
|
|
103
|
+
return pictureCount;
|
|
104
|
+
}
|
|
105
|
+
dispose() {
|
|
106
|
+
this.#decoder.delete();
|
|
107
|
+
this.#size.dispose();
|
|
108
|
+
this.#canvas.width = 0;
|
|
109
|
+
this.#canvas.height = 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=core.js.map
|
package/esm/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
|
package/esm/index.js
ADDED
package/esm/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
|
package/esm/main.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ScrcpyVideoDecoder, ScrcpyVideoDecoderCapability } from "@yume-chan/scrcpy-decoder-shared";
|
|
2
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
3
|
+
export declare const noop: () => void;
|
|
4
|
+
export declare class H264BsdDecoder implements ScrcpyVideoDecoder {
|
|
5
|
+
#private;
|
|
6
|
+
static readonly capabilities: Record<string, ScrcpyVideoDecoderCapability>;
|
|
7
|
+
get type(): "software";
|
|
8
|
+
get canvas(): HTMLCanvasElement | OffscreenCanvas;
|
|
9
|
+
get rendererType(): "software" | "hardware";
|
|
10
|
+
get paused(): boolean;
|
|
11
|
+
get writable(): WritableStream<import("@yume-chan/scrcpy").ScrcpyMediaStreamPacket>;
|
|
12
|
+
get width(): number;
|
|
13
|
+
get height(): number;
|
|
14
|
+
get sizeChanged(): import("@yume-chan/event").Event<{
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Gets the number of times the decoder has been reset to catch up new keyframes.
|
|
20
|
+
*/
|
|
21
|
+
get decoderResetCount(): number;
|
|
22
|
+
/**
|
|
23
|
+
* Gets the number of frames decoded by the decoder.
|
|
24
|
+
*/
|
|
25
|
+
get framesDecoded(): number;
|
|
26
|
+
/**
|
|
27
|
+
* Gets the number of frames skipped by the decoder.
|
|
28
|
+
*/
|
|
29
|
+
get framesSkippedDecoding(): number;
|
|
30
|
+
/**
|
|
31
|
+
* Gets the number of frames that have been drawn on the renderer.
|
|
32
|
+
*/
|
|
33
|
+
get framesRendered(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Gets the number of frames that's visible to the user.
|
|
36
|
+
*
|
|
37
|
+
* Multiple frames might be rendered during one vertical sync interval,
|
|
38
|
+
* but only the last of them is represented to the user.
|
|
39
|
+
* This costs some performance but reduces latency by 1 frame.
|
|
40
|
+
*
|
|
41
|
+
* Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
|
|
42
|
+
* https://issues.chromium.org/issues/41483010
|
|
43
|
+
*/
|
|
44
|
+
get framesDisplayed(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Gets the number of frames that wasn't drawn on the renderer
|
|
47
|
+
* because the renderer can't keep up
|
|
48
|
+
*/
|
|
49
|
+
get framesSkippedRendering(): number;
|
|
50
|
+
constructor({ canvas, worker }?: H264BsdDecoder.Options);
|
|
51
|
+
pause(): void;
|
|
52
|
+
resume(): undefined;
|
|
53
|
+
trackDocumentVisibility(document: Document): () => undefined;
|
|
54
|
+
dispose(): Promise<undefined>;
|
|
55
|
+
}
|
|
56
|
+
export declare namespace H264BsdDecoder {
|
|
57
|
+
interface Options {
|
|
58
|
+
/**
|
|
59
|
+
* Optional render target canvas element or offscreen canvas.
|
|
60
|
+
* If not provided, a new `<canvas>` (when DOM is available)
|
|
61
|
+
* or a `OffscreenCanvas` will be created.
|
|
62
|
+
*/
|
|
63
|
+
canvas?: HTMLCanvasElement | OffscreenCanvas | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Whether to create a Web Worker to run the decoder.
|
|
66
|
+
*
|
|
67
|
+
* Value can be:
|
|
68
|
+
*
|
|
69
|
+
* * `"auto"`: Create a Web Worker if currently running in main thread
|
|
70
|
+
* * `true`: Always create a Web Worker.
|
|
71
|
+
* Note that if it's currently already running in a Web Worker,
|
|
72
|
+
* The `canvas` option must be provided with an `OffscreenCanvas`,
|
|
73
|
+
* and the `OffscreenCanvas` will be transferred to the new Worker.
|
|
74
|
+
* * `false`: Never create a Web Worker
|
|
75
|
+
*
|
|
76
|
+
* The default value is `"auto"`.
|
|
77
|
+
*/
|
|
78
|
+
worker?: "auto" | boolean;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACR,kBAAkB,EAClB,4BAA4B,EAC/B,MAAM,kCAAkC,CAAC;AAS1C,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAKzD,eAAO,MAAM,IAAI,YAEhB,CAAC;AAIF,qBAAa,cAAe,YAAW,kBAAkB;;IACrD,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,4BAA4B,CAAC,CAMpE;IAEN,IAAI,IAAI,IACG,UAAU,CACpB;IAID,IAAI,MAAM,wCAET;IAGD,IAAI,YAAY,4BAEf;IAGD,IAAI,MAAM,YAET;IACD,IAAI,QAAQ,wEAEX;IAQD,IAAI,KAAK,WAER;IACD,IAAI,MAAM,WAET;IACD,IAAI,WAAW;;;OAEd;IAGD;;OAEG;IACH,IAAI,iBAAiB,WAEpB;IACD;;OAEG;IACH,IAAI,aAAa,WAEhB;IACD;;OAEG;IACH,IAAI,qBAAqB,WAExB;IAED;;OAEG;IACH,IAAI,cAAc,WAEjB;IACD;;;;;;;;;OASG;IACH,IAAI,eAAe,WAElB;IACD;;;OAGG;IACH,IAAI,sBAAsB,WAEzB;gBAKW,EAAE,MAAM,EAAE,MAAM,EAAE,GAAE,cAAc,CAAC,OAAY;IAkL3D,KAAK,IAAI,IAAI;IAIb,MAAM,IAAI,SAAS;IAInB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,SAAS;IAItD,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;CAwBtC;AAED,yBAAiB,cAAc,CAAC;IAC5B,UAAiB,OAAO;QACpB;;;;WAIG;QACH,MAAM,CAAC,EAAE,iBAAiB,GAAG,eAAe,GAAG,SAAS,CAAC;QAEzD;;;;;;;;;;;;;WAaG;QACH,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;KAC7B;CACJ"}
|
package/esm/main.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { AndroidAvcLevel, AndroidAvcProfile, ScrcpyVideoSizeImpl, } from "@yume-chan/scrcpy";
|
|
2
|
+
import { createCanvas, glIsSupported, ScrcpyVideoDecoderPauseController, ScrcpyVideoDecoderPerformanceCounter, ScrcpyVideoRendererPerformanceCounter, } from "@yume-chan/scrcpy-decoder-shared";
|
|
3
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
4
|
+
import * as Comlink from "comlink";
|
|
5
|
+
import { DecoderRenderer } from "./core.js";
|
|
6
|
+
export const noop = () => {
|
|
7
|
+
// no-op
|
|
8
|
+
};
|
|
9
|
+
const isMainThread = typeof window !== "undefined";
|
|
10
|
+
export class H264BsdDecoder {
|
|
11
|
+
static capabilities = {
|
|
12
|
+
h264: {
|
|
13
|
+
maxProfile: AndroidAvcProfile.Baseline,
|
|
14
|
+
maxLevel: AndroidAvcLevel.Level4,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
get type() {
|
|
18
|
+
return "software";
|
|
19
|
+
}
|
|
20
|
+
#canvas;
|
|
21
|
+
#canvasTransferred = false;
|
|
22
|
+
get canvas() {
|
|
23
|
+
return this.#canvas;
|
|
24
|
+
}
|
|
25
|
+
#rendererType;
|
|
26
|
+
get rendererType() {
|
|
27
|
+
return this.#rendererType;
|
|
28
|
+
}
|
|
29
|
+
#pause = new ScrcpyVideoDecoderPauseController();
|
|
30
|
+
get paused() {
|
|
31
|
+
return this.#pause.paused;
|
|
32
|
+
}
|
|
33
|
+
get writable() {
|
|
34
|
+
return this.#pause.writable;
|
|
35
|
+
}
|
|
36
|
+
#writableController;
|
|
37
|
+
#queue = [];
|
|
38
|
+
#enqueuing = false;
|
|
39
|
+
#size = new ScrcpyVideoSizeImpl();
|
|
40
|
+
get width() {
|
|
41
|
+
return this.#size.width;
|
|
42
|
+
}
|
|
43
|
+
get height() {
|
|
44
|
+
return this.#size.height;
|
|
45
|
+
}
|
|
46
|
+
get sizeChanged() {
|
|
47
|
+
return this.#size.sizeChanged;
|
|
48
|
+
}
|
|
49
|
+
#decoderCounter = new ScrcpyVideoDecoderPerformanceCounter();
|
|
50
|
+
/**
|
|
51
|
+
* Gets the number of times the decoder has been reset to catch up new keyframes.
|
|
52
|
+
*/
|
|
53
|
+
get decoderResetCount() {
|
|
54
|
+
return this.#decoderCounter.decoderResetCount;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Gets the number of frames decoded by the decoder.
|
|
58
|
+
*/
|
|
59
|
+
get framesDecoded() {
|
|
60
|
+
return this.#decoderCounter.framesDecoded;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Gets the number of frames skipped by the decoder.
|
|
64
|
+
*/
|
|
65
|
+
get framesSkippedDecoding() {
|
|
66
|
+
return this.#decoderCounter.framesSkippedDecoding;
|
|
67
|
+
}
|
|
68
|
+
#rendererCounter = new ScrcpyVideoRendererPerformanceCounter();
|
|
69
|
+
/**
|
|
70
|
+
* Gets the number of frames that have been drawn on the renderer.
|
|
71
|
+
*/
|
|
72
|
+
get framesRendered() {
|
|
73
|
+
return this.#rendererCounter.framesRendered;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Gets the number of frames that's visible to the user.
|
|
77
|
+
*
|
|
78
|
+
* Multiple frames might be rendered during one vertical sync interval,
|
|
79
|
+
* but only the last of them is represented to the user.
|
|
80
|
+
* This costs some performance but reduces latency by 1 frame.
|
|
81
|
+
*
|
|
82
|
+
* Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
|
|
83
|
+
* https://issues.chromium.org/issues/41483010
|
|
84
|
+
*/
|
|
85
|
+
get framesDisplayed() {
|
|
86
|
+
return this.#rendererCounter.framesDisplayed;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Gets the number of frames that wasn't drawn on the renderer
|
|
90
|
+
* because the renderer can't keep up
|
|
91
|
+
*/
|
|
92
|
+
get framesSkippedRendering() {
|
|
93
|
+
return this.#rendererCounter.framesSkippedRendering;
|
|
94
|
+
}
|
|
95
|
+
#worker;
|
|
96
|
+
#decoder;
|
|
97
|
+
constructor({ canvas, worker } = {}) {
|
|
98
|
+
switch (worker) {
|
|
99
|
+
case "auto":
|
|
100
|
+
case undefined:
|
|
101
|
+
worker = isMainThread;
|
|
102
|
+
break;
|
|
103
|
+
case true:
|
|
104
|
+
if (!isMainThread && !canvas) {
|
|
105
|
+
throw new Error("`canvas` is required when `worker` is `true` and running in Web Worker");
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (canvas) {
|
|
110
|
+
this.#canvas = canvas;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.#canvas = createCanvas();
|
|
114
|
+
}
|
|
115
|
+
// Disallow software rendering.
|
|
116
|
+
// yuv-canvas also supports 2d canvas
|
|
117
|
+
// which is faster than software-based WebGL.
|
|
118
|
+
const webGl = glIsSupported({ failIfMajorPerformanceCaveat: true });
|
|
119
|
+
this.#rendererType = webGl ? "hardware" : "software";
|
|
120
|
+
if (worker) {
|
|
121
|
+
this.#decoder = this.#createRemoteDecoder(webGl);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.#decoder = this.#createLocalDecoder(webGl);
|
|
125
|
+
}
|
|
126
|
+
void this.#pause.readable
|
|
127
|
+
.pipeTo(new WritableStream({
|
|
128
|
+
start: (controller) => {
|
|
129
|
+
this.#writableController = controller;
|
|
130
|
+
},
|
|
131
|
+
write: async (packet) => {
|
|
132
|
+
if (packet.type === "configuration") {
|
|
133
|
+
this.#decoderCounter.addFramesSkippedDecoding(this.#queue.length);
|
|
134
|
+
this.#queue.length = 0;
|
|
135
|
+
const decoder = await this.#decoder;
|
|
136
|
+
const frames = await decoder.flush(true);
|
|
137
|
+
this.#rendererCounter.addFramesSkippedRendering(frames);
|
|
138
|
+
// Shouldn't produce any frames
|
|
139
|
+
await decoder.decode(packet.data, false);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (packet.keyframe && this.#queue.length) {
|
|
143
|
+
this.#decoderCounter.addFramesSkippedDecoding(this.#queue.length);
|
|
144
|
+
this.#decoderCounter.increaseResetCount();
|
|
145
|
+
this.#queue.length = 0;
|
|
146
|
+
}
|
|
147
|
+
this.#queue.push(packet);
|
|
148
|
+
void this.#decode();
|
|
149
|
+
},
|
|
150
|
+
}))
|
|
151
|
+
.catch(noop);
|
|
152
|
+
}
|
|
153
|
+
async #createRemoteDecoder(webGl) {
|
|
154
|
+
let canvas = this.#canvas;
|
|
155
|
+
if (isMainThread && canvas instanceof HTMLCanvasElement) {
|
|
156
|
+
canvas = canvas.transferControlToOffscreen();
|
|
157
|
+
this.#canvasTransferred = true;
|
|
158
|
+
}
|
|
159
|
+
const worker = new Worker(new URL("./worker.js", import.meta.url), {
|
|
160
|
+
type: "module",
|
|
161
|
+
});
|
|
162
|
+
this.#worker = worker;
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
const abortController = new AbortController();
|
|
165
|
+
worker.addEventListener("message", (e) => {
|
|
166
|
+
if (e.data === "ready") {
|
|
167
|
+
abortController.abort();
|
|
168
|
+
resolve();
|
|
169
|
+
}
|
|
170
|
+
}, { signal: abortController.signal });
|
|
171
|
+
worker.addEventListener("error", (e) => {
|
|
172
|
+
abortController.abort();
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
|
174
|
+
reject(e.error);
|
|
175
|
+
}, { signal: abortController.signal });
|
|
176
|
+
});
|
|
177
|
+
const Constructor = Comlink.wrap(worker);
|
|
178
|
+
const decoder = await new Constructor(Comlink.transfer(canvas, [canvas]), webGl, Comlink.proxy(this.#handleSizeChange));
|
|
179
|
+
await decoder.initialize();
|
|
180
|
+
return decoder;
|
|
181
|
+
}
|
|
182
|
+
async #createLocalDecoder(webGl) {
|
|
183
|
+
const decoder = new DecoderRenderer(this.#canvas, webGl, this.#handleSizeChange);
|
|
184
|
+
await decoder.initialize();
|
|
185
|
+
return decoder;
|
|
186
|
+
}
|
|
187
|
+
#handleSizeChange = ({ width, height, }) => {
|
|
188
|
+
this.#size.setSize(width, height);
|
|
189
|
+
};
|
|
190
|
+
async #decode() {
|
|
191
|
+
if (this.#enqueuing) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.#enqueuing = true;
|
|
195
|
+
try {
|
|
196
|
+
const decoder = await this.#decoder;
|
|
197
|
+
let chunk;
|
|
198
|
+
while ((chunk = this.#queue.shift())) {
|
|
199
|
+
const framesDecoded = await decoder.decode(chunk.data, chunk.skipRendering);
|
|
200
|
+
if (framesDecoded) {
|
|
201
|
+
this.#decoderCounter.addFramesDecoded(framesDecoded);
|
|
202
|
+
if (chunk.skipRendering) {
|
|
203
|
+
this.#rendererCounter.addFramesSkippedRendering(framesDecoded);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
this.#rendererCounter.increaseFramesRendered();
|
|
207
|
+
this.#rendererCounter.addFramesSkippedRendering(framesDecoded - 1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
this.#writableController.error(e);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.#enqueuing = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
pause() {
|
|
220
|
+
this.#pause.pause();
|
|
221
|
+
}
|
|
222
|
+
resume() {
|
|
223
|
+
this.#pause.resume();
|
|
224
|
+
}
|
|
225
|
+
trackDocumentVisibility(document) {
|
|
226
|
+
return this.#pause.trackDocumentVisibility(document);
|
|
227
|
+
}
|
|
228
|
+
async dispose() {
|
|
229
|
+
// This class doesn't need to guard against multiple dispose calls
|
|
230
|
+
// since most of the logic is already handled in `#pause`
|
|
231
|
+
this.#pause.dispose();
|
|
232
|
+
try {
|
|
233
|
+
const decoder = await this.#decoder;
|
|
234
|
+
await decoder.flush(false);
|
|
235
|
+
await decoder.dispose();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// ignore
|
|
239
|
+
}
|
|
240
|
+
this.#worker?.terminate();
|
|
241
|
+
this.#rendererCounter.dispose();
|
|
242
|
+
this.#size.dispose();
|
|
243
|
+
if (!this.#canvasTransferred) {
|
|
244
|
+
// Can't change a transferred canvas
|
|
245
|
+
this.#canvas.width = 0;
|
|
246
|
+
this.#canvas.height = 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=main.js.map
|
package/esm/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
|
package/esm/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":""}
|
package/esm/worker.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAEnC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;AAEhC,WAAW,CAAC,OAAO,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yume-chan/scrcpy-decoder-h264bsd",
|
|
3
|
+
"version": "3.0.0-beta.0",
|
|
4
|
+
"description": "Raw H.264 stream decoder and renderer wrapping H264BSD and YUVCanvas packages (software decoding).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"adb",
|
|
7
|
+
"android-phone",
|
|
8
|
+
"scrcpy",
|
|
9
|
+
"scrcpy-decoder"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Simon Chan",
|
|
14
|
+
"email": "cnsimonchan@live.com",
|
|
15
|
+
"url": "https://chensi.moe/blog"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/yume-chan/ya-webadb/tree/main/libraries/scrcpy-decoder-h264bsd#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
|
21
|
+
"directory": "libraries/scrcpy-decoder-h264bsd"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "esm/index.js",
|
|
28
|
+
"types": "esm/index.d.ts",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@yume-chan/async": "^4.1.3",
|
|
32
|
+
"@yume-chan/h264bsd": "^1.1.1",
|
|
33
|
+
"comlink": "^4.4.2",
|
|
34
|
+
"yuv-buffer": "^1.0.1",
|
|
35
|
+
"yuv-canvas": "^1.3.0",
|
|
36
|
+
"@yume-chan/event": "3.0.0-beta.0",
|
|
37
|
+
"@yume-chan/media-codec": "3.0.0-beta.0",
|
|
38
|
+
"@yume-chan/scrcpy-decoder-shared": "3.0.0-beta.0",
|
|
39
|
+
"@yume-chan/scrcpy": "3.0.0-beta.0",
|
|
40
|
+
"@yume-chan/stream-extra": "3.0.0-beta.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"prettier": "^3.8.4",
|
|
44
|
+
"typescript": "^6.0.3",
|
|
45
|
+
"@yume-chan/tsconfig": "^1.0.0",
|
|
46
|
+
"@yume-chan/eslint-config": "^1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Decoder,
|
|
3
|
+
FlushResult,
|
|
4
|
+
MainModule,
|
|
5
|
+
Picture,
|
|
6
|
+
} from "@yume-chan/h264bsd";
|
|
7
|
+
import initialize from "@yume-chan/h264bsd";
|
|
8
|
+
import { ScrcpyVideoSizeImpl } from "@yume-chan/scrcpy";
|
|
9
|
+
import YUVCanvas from "yuv-canvas";
|
|
10
|
+
|
|
11
|
+
let h264bsdModulePromise: Promise<MainModule> | undefined;
|
|
12
|
+
let h264bsdModule: MainModule | undefined;
|
|
13
|
+
async function initializeH264bsd(): Promise<MainModule> {
|
|
14
|
+
if (!h264bsdModulePromise) {
|
|
15
|
+
h264bsdModulePromise = initialize().then(
|
|
16
|
+
(module) => (h264bsdModule = module),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return h264bsdModulePromise;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DecoderRenderer {
|
|
23
|
+
#decoder!: Decoder;
|
|
24
|
+
|
|
25
|
+
#canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
26
|
+
#renderer: YUVCanvas;
|
|
27
|
+
|
|
28
|
+
#size: ScrcpyVideoSizeImpl;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
32
|
+
webGl: boolean,
|
|
33
|
+
onSizeChanged: (size: { width: number; height: number }) => unknown,
|
|
34
|
+
) {
|
|
35
|
+
this.#canvas = canvas;
|
|
36
|
+
// yuv-canvas supports detecting WebGL support by creating a <canvas> itself
|
|
37
|
+
// But this doesn't work in Web Worker (with OffscreenCanvas)
|
|
38
|
+
// so we implement our own check here
|
|
39
|
+
this.#renderer = YUVCanvas.attach(canvas, { webGL: webGl });
|
|
40
|
+
|
|
41
|
+
this.#size = new ScrcpyVideoSizeImpl();
|
|
42
|
+
this.#size.sizeChanged(onSizeChanged);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async initialize() {
|
|
46
|
+
const h264bsd = await initializeH264bsd();
|
|
47
|
+
this.#decoder = new h264bsd.Decoder();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#draw(picture: Picture) {
|
|
51
|
+
const width = picture.info.picWidth;
|
|
52
|
+
const height = picture.info.picHeight;
|
|
53
|
+
|
|
54
|
+
this.#size.setSize(width, height);
|
|
55
|
+
|
|
56
|
+
const chromaWidth = width / 2;
|
|
57
|
+
const chromaHeight = height / 2;
|
|
58
|
+
|
|
59
|
+
const uOffset = width * height;
|
|
60
|
+
const vOffset = uOffset + chromaWidth * chromaHeight;
|
|
61
|
+
|
|
62
|
+
this.#renderer.drawFrame({
|
|
63
|
+
format: {
|
|
64
|
+
width,
|
|
65
|
+
height,
|
|
66
|
+
chromaWidth,
|
|
67
|
+
chromaHeight,
|
|
68
|
+
cropLeft: picture.info.cropParams.cropLeftOffset,
|
|
69
|
+
cropWidth: picture.info.cropParams.cropOutWidth,
|
|
70
|
+
cropTop: picture.info.cropParams.cropTopOffset,
|
|
71
|
+
cropHeight: picture.info.cropParams.cropOutHeight,
|
|
72
|
+
displayWidth: picture.info.cropParams.cropOutWidth,
|
|
73
|
+
displayHeight: picture.info.cropParams.cropOutHeight,
|
|
74
|
+
},
|
|
75
|
+
y: {
|
|
76
|
+
bytes: picture.bytes.subarray(0, uOffset),
|
|
77
|
+
stride: width,
|
|
78
|
+
},
|
|
79
|
+
u: {
|
|
80
|
+
bytes: picture.bytes.subarray(uOffset, vOffset),
|
|
81
|
+
stride: chromaWidth,
|
|
82
|
+
},
|
|
83
|
+
v: {
|
|
84
|
+
bytes: picture.bytes.subarray(vOffset),
|
|
85
|
+
stride: chromaWidth,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#getLastPicture(result: FlushResult): [number, Picture | undefined] {
|
|
91
|
+
let { picture } = result;
|
|
92
|
+
if (picture) {
|
|
93
|
+
for (let i = 0; i < result.extraPictureCount; i += 1) {
|
|
94
|
+
picture = this.#decoder.getNextPicture()!;
|
|
95
|
+
}
|
|
96
|
+
return [result.extraPictureCount + 1, picture];
|
|
97
|
+
}
|
|
98
|
+
return [0, undefined];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
decode(data: Uint8Array, skipRendering: boolean) {
|
|
102
|
+
let offset = 0;
|
|
103
|
+
let length = data.length;
|
|
104
|
+
let picture: Picture | undefined;
|
|
105
|
+
|
|
106
|
+
let framesDecoded = 0;
|
|
107
|
+
|
|
108
|
+
while (length) {
|
|
109
|
+
const result = this.#decoder.decode(data.subarray(offset), 0);
|
|
110
|
+
if (result.code >= h264bsdModule!.DecodeResultCode.Error) {
|
|
111
|
+
throw new Error("Decode failed. Code: " + result.code);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let pictureCount: number;
|
|
115
|
+
[pictureCount, picture] = this.#getLastPicture(result);
|
|
116
|
+
framesDecoded += pictureCount;
|
|
117
|
+
|
|
118
|
+
offset += result.read;
|
|
119
|
+
length -= result.read;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!picture || skipRendering) {
|
|
123
|
+
return framesDecoded;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.#draw(picture);
|
|
127
|
+
|
|
128
|
+
return framesDecoded;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
flush(skipRendering: boolean) {
|
|
132
|
+
const result = this.#decoder.flush();
|
|
133
|
+
const [pictureCount, picture] = this.#getLastPicture(result);
|
|
134
|
+
if (picture && !skipRendering) {
|
|
135
|
+
this.#draw(picture);
|
|
136
|
+
}
|
|
137
|
+
return pictureCount;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
dispose() {
|
|
141
|
+
this.#decoder.delete();
|
|
142
|
+
this.#size.dispose();
|
|
143
|
+
|
|
144
|
+
this.#canvas.width = 0;
|
|
145
|
+
this.#canvas.height = 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./main.js";
|
package/src/libs.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module "yuv-canvas" {
|
|
2
|
+
import type { YUVFrame } from "yuv-buffer";
|
|
3
|
+
|
|
4
|
+
export interface YUVCanvasOptions {
|
|
5
|
+
webGL?: boolean | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default class YUVCanvas {
|
|
9
|
+
static attach(
|
|
10
|
+
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
11
|
+
options?: YUVCanvasOptions,
|
|
12
|
+
): YUVCanvas;
|
|
13
|
+
|
|
14
|
+
drawFrame(data: YUVFrame): void;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AndroidAvcLevel,
|
|
3
|
+
AndroidAvcProfile,
|
|
4
|
+
ScrcpyVideoSizeImpl,
|
|
5
|
+
} from "@yume-chan/scrcpy";
|
|
6
|
+
import type {
|
|
7
|
+
ScrcpyVideoDecoder,
|
|
8
|
+
ScrcpyVideoDecoderCapability,
|
|
9
|
+
} from "@yume-chan/scrcpy-decoder-shared";
|
|
10
|
+
import {
|
|
11
|
+
createCanvas,
|
|
12
|
+
glIsSupported,
|
|
13
|
+
ScrcpyVideoDecoderPauseController,
|
|
14
|
+
ScrcpyVideoDecoderPerformanceCounter,
|
|
15
|
+
ScrcpyVideoRendererPerformanceCounter,
|
|
16
|
+
} from "@yume-chan/scrcpy-decoder-shared";
|
|
17
|
+
import type { WritableStreamDefaultController } from "@yume-chan/stream-extra";
|
|
18
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
19
|
+
import * as Comlink from "comlink";
|
|
20
|
+
|
|
21
|
+
import { DecoderRenderer } from "./core.js";
|
|
22
|
+
|
|
23
|
+
export const noop = () => {
|
|
24
|
+
// no-op
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isMainThread = typeof window !== "undefined";
|
|
28
|
+
|
|
29
|
+
export class H264BsdDecoder implements ScrcpyVideoDecoder {
|
|
30
|
+
static readonly capabilities: Record<string, ScrcpyVideoDecoderCapability> =
|
|
31
|
+
{
|
|
32
|
+
h264: {
|
|
33
|
+
maxProfile: AndroidAvcProfile.Baseline,
|
|
34
|
+
maxLevel: AndroidAvcLevel.Level4,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
get type() {
|
|
39
|
+
return "software" as const;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
43
|
+
#canvasTransferred = false;
|
|
44
|
+
get canvas() {
|
|
45
|
+
return this.#canvas;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#rendererType: "software" | "hardware";
|
|
49
|
+
get rendererType() {
|
|
50
|
+
return this.#rendererType;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#pause = new ScrcpyVideoDecoderPauseController();
|
|
54
|
+
get paused() {
|
|
55
|
+
return this.#pause.paused;
|
|
56
|
+
}
|
|
57
|
+
get writable() {
|
|
58
|
+
return this.#pause.writable;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#writableController!: WritableStreamDefaultController;
|
|
62
|
+
#queue: (ScrcpyVideoDecoderPauseController.Output & { type: "data" })[] =
|
|
63
|
+
[];
|
|
64
|
+
#enqueuing = false;
|
|
65
|
+
|
|
66
|
+
#size = new ScrcpyVideoSizeImpl();
|
|
67
|
+
get width() {
|
|
68
|
+
return this.#size.width;
|
|
69
|
+
}
|
|
70
|
+
get height() {
|
|
71
|
+
return this.#size.height;
|
|
72
|
+
}
|
|
73
|
+
get sizeChanged() {
|
|
74
|
+
return this.#size.sizeChanged;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#decoderCounter = new ScrcpyVideoDecoderPerformanceCounter();
|
|
78
|
+
/**
|
|
79
|
+
* Gets the number of times the decoder has been reset to catch up new keyframes.
|
|
80
|
+
*/
|
|
81
|
+
get decoderResetCount() {
|
|
82
|
+
return this.#decoderCounter.decoderResetCount;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets the number of frames decoded by the decoder.
|
|
86
|
+
*/
|
|
87
|
+
get framesDecoded() {
|
|
88
|
+
return this.#decoderCounter.framesDecoded;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Gets the number of frames skipped by the decoder.
|
|
92
|
+
*/
|
|
93
|
+
get framesSkippedDecoding() {
|
|
94
|
+
return this.#decoderCounter.framesSkippedDecoding;
|
|
95
|
+
}
|
|
96
|
+
#rendererCounter = new ScrcpyVideoRendererPerformanceCounter();
|
|
97
|
+
/**
|
|
98
|
+
* Gets the number of frames that have been drawn on the renderer.
|
|
99
|
+
*/
|
|
100
|
+
get framesRendered() {
|
|
101
|
+
return this.#rendererCounter.framesRendered;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Gets the number of frames that's visible to the user.
|
|
105
|
+
*
|
|
106
|
+
* Multiple frames might be rendered during one vertical sync interval,
|
|
107
|
+
* but only the last of them is represented to the user.
|
|
108
|
+
* This costs some performance but reduces latency by 1 frame.
|
|
109
|
+
*
|
|
110
|
+
* Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
|
|
111
|
+
* https://issues.chromium.org/issues/41483010
|
|
112
|
+
*/
|
|
113
|
+
get framesDisplayed() {
|
|
114
|
+
return this.#rendererCounter.framesDisplayed;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Gets the number of frames that wasn't drawn on the renderer
|
|
118
|
+
* because the renderer can't keep up
|
|
119
|
+
*/
|
|
120
|
+
get framesSkippedRendering() {
|
|
121
|
+
return this.#rendererCounter.framesSkippedRendering;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#worker: Worker | undefined;
|
|
125
|
+
#decoder: Promise<DecoderRenderer | Comlink.Remote<DecoderRenderer>>;
|
|
126
|
+
|
|
127
|
+
constructor({ canvas, worker }: H264BsdDecoder.Options = {}) {
|
|
128
|
+
switch (worker) {
|
|
129
|
+
case "auto":
|
|
130
|
+
case undefined:
|
|
131
|
+
worker = isMainThread;
|
|
132
|
+
break;
|
|
133
|
+
case true:
|
|
134
|
+
if (!isMainThread && !canvas) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"`canvas` is required when `worker` is `true` and running in Web Worker",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (canvas) {
|
|
143
|
+
this.#canvas = canvas;
|
|
144
|
+
} else {
|
|
145
|
+
this.#canvas = createCanvas();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Disallow software rendering.
|
|
149
|
+
// yuv-canvas also supports 2d canvas
|
|
150
|
+
// which is faster than software-based WebGL.
|
|
151
|
+
const webGl = glIsSupported({ failIfMajorPerformanceCaveat: true });
|
|
152
|
+
this.#rendererType = webGl ? "hardware" : "software";
|
|
153
|
+
|
|
154
|
+
if (worker) {
|
|
155
|
+
this.#decoder = this.#createRemoteDecoder(webGl);
|
|
156
|
+
} else {
|
|
157
|
+
this.#decoder = this.#createLocalDecoder(webGl);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
void this.#pause.readable
|
|
161
|
+
.pipeTo(
|
|
162
|
+
new WritableStream({
|
|
163
|
+
start: (controller) => {
|
|
164
|
+
this.#writableController = controller;
|
|
165
|
+
},
|
|
166
|
+
write: async (packet) => {
|
|
167
|
+
if (packet.type === "configuration") {
|
|
168
|
+
this.#decoderCounter.addFramesSkippedDecoding(
|
|
169
|
+
this.#queue.length,
|
|
170
|
+
);
|
|
171
|
+
this.#queue.length = 0;
|
|
172
|
+
|
|
173
|
+
const decoder = await this.#decoder;
|
|
174
|
+
|
|
175
|
+
const frames = await decoder.flush(true);
|
|
176
|
+
this.#rendererCounter.addFramesSkippedRendering(
|
|
177
|
+
frames,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Shouldn't produce any frames
|
|
181
|
+
await decoder.decode(packet.data, false);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (packet.keyframe && this.#queue.length) {
|
|
186
|
+
this.#decoderCounter.addFramesSkippedDecoding(
|
|
187
|
+
this.#queue.length,
|
|
188
|
+
);
|
|
189
|
+
this.#decoderCounter.increaseResetCount();
|
|
190
|
+
this.#queue.length = 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.#queue.push(packet);
|
|
194
|
+
void this.#decode();
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
)
|
|
198
|
+
.catch(noop);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async #createRemoteDecoder(webGl: boolean) {
|
|
202
|
+
let canvas = this.#canvas;
|
|
203
|
+
if (isMainThread && canvas instanceof HTMLCanvasElement) {
|
|
204
|
+
canvas = canvas.transferControlToOffscreen();
|
|
205
|
+
this.#canvasTransferred = true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const worker = new Worker(new URL("./worker.js", import.meta.url), {
|
|
209
|
+
type: "module",
|
|
210
|
+
});
|
|
211
|
+
this.#worker = worker;
|
|
212
|
+
|
|
213
|
+
await new Promise<void>((resolve, reject) => {
|
|
214
|
+
const abortController = new AbortController();
|
|
215
|
+
worker.addEventListener(
|
|
216
|
+
"message",
|
|
217
|
+
(e) => {
|
|
218
|
+
if (e.data === "ready") {
|
|
219
|
+
abortController.abort();
|
|
220
|
+
resolve();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{ signal: abortController.signal },
|
|
224
|
+
);
|
|
225
|
+
worker.addEventListener(
|
|
226
|
+
"error",
|
|
227
|
+
(e) => {
|
|
228
|
+
abortController.abort();
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
|
230
|
+
reject(e.error);
|
|
231
|
+
},
|
|
232
|
+
{ signal: abortController.signal },
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const Constructor = Comlink.wrap<typeof DecoderRenderer>(worker);
|
|
237
|
+
|
|
238
|
+
const decoder = await new Constructor(
|
|
239
|
+
Comlink.transfer(canvas, [canvas]),
|
|
240
|
+
webGl,
|
|
241
|
+
Comlink.proxy(this.#handleSizeChange),
|
|
242
|
+
);
|
|
243
|
+
await decoder.initialize();
|
|
244
|
+
return decoder;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async #createLocalDecoder(webGl: boolean) {
|
|
248
|
+
const decoder = new DecoderRenderer(
|
|
249
|
+
this.#canvas,
|
|
250
|
+
webGl,
|
|
251
|
+
this.#handleSizeChange,
|
|
252
|
+
);
|
|
253
|
+
await decoder.initialize();
|
|
254
|
+
return decoder;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#handleSizeChange = ({
|
|
258
|
+
width,
|
|
259
|
+
height,
|
|
260
|
+
}: {
|
|
261
|
+
width: number;
|
|
262
|
+
height: number;
|
|
263
|
+
}) => {
|
|
264
|
+
this.#size.setSize(width, height);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
async #decode() {
|
|
268
|
+
if (this.#enqueuing) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.#enqueuing = true;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const decoder = await this.#decoder;
|
|
275
|
+
|
|
276
|
+
let chunk:
|
|
277
|
+
| (ScrcpyVideoDecoderPauseController.Output & { type: "data" })
|
|
278
|
+
| undefined;
|
|
279
|
+
while ((chunk = this.#queue.shift())) {
|
|
280
|
+
const framesDecoded = await decoder.decode(
|
|
281
|
+
chunk.data,
|
|
282
|
+
chunk.skipRendering,
|
|
283
|
+
);
|
|
284
|
+
if (framesDecoded) {
|
|
285
|
+
this.#decoderCounter.addFramesDecoded(framesDecoded);
|
|
286
|
+
if (chunk.skipRendering) {
|
|
287
|
+
this.#rendererCounter.addFramesSkippedRendering(
|
|
288
|
+
framesDecoded,
|
|
289
|
+
);
|
|
290
|
+
} else {
|
|
291
|
+
this.#rendererCounter.increaseFramesRendered();
|
|
292
|
+
this.#rendererCounter.addFramesSkippedRendering(
|
|
293
|
+
framesDecoded - 1,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (e) {
|
|
299
|
+
this.#writableController.error(e);
|
|
300
|
+
} finally {
|
|
301
|
+
this.#enqueuing = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
pause(): void {
|
|
306
|
+
this.#pause.pause();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
resume(): undefined {
|
|
310
|
+
this.#pause.resume();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
trackDocumentVisibility(document: Document): () => undefined {
|
|
314
|
+
return this.#pause.trackDocumentVisibility(document);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async dispose(): Promise<undefined> {
|
|
318
|
+
// This class doesn't need to guard against multiple dispose calls
|
|
319
|
+
// since most of the logic is already handled in `#pause`
|
|
320
|
+
this.#pause.dispose();
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const decoder = await this.#decoder;
|
|
324
|
+
await decoder.flush(false);
|
|
325
|
+
await decoder.dispose();
|
|
326
|
+
} catch {
|
|
327
|
+
// ignore
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.#worker?.terminate();
|
|
331
|
+
|
|
332
|
+
this.#rendererCounter.dispose();
|
|
333
|
+
this.#size.dispose();
|
|
334
|
+
|
|
335
|
+
if (!this.#canvasTransferred) {
|
|
336
|
+
// Can't change a transferred canvas
|
|
337
|
+
this.#canvas.width = 0;
|
|
338
|
+
this.#canvas.height = 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export namespace H264BsdDecoder {
|
|
344
|
+
export interface Options {
|
|
345
|
+
/**
|
|
346
|
+
* Optional render target canvas element or offscreen canvas.
|
|
347
|
+
* If not provided, a new `<canvas>` (when DOM is available)
|
|
348
|
+
* or a `OffscreenCanvas` will be created.
|
|
349
|
+
*/
|
|
350
|
+
canvas?: HTMLCanvasElement | OffscreenCanvas | undefined;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Whether to create a Web Worker to run the decoder.
|
|
354
|
+
*
|
|
355
|
+
* Value can be:
|
|
356
|
+
*
|
|
357
|
+
* * `"auto"`: Create a Web Worker if currently running in main thread
|
|
358
|
+
* * `true`: Always create a Web Worker.
|
|
359
|
+
* Note that if it's currently already running in a Web Worker,
|
|
360
|
+
* The `canvas` option must be provided with an `OffscreenCanvas`,
|
|
361
|
+
* and the `OffscreenCanvas` will be transferred to the new Worker.
|
|
362
|
+
* * `false`: Never create a Web Worker
|
|
363
|
+
*
|
|
364
|
+
* The default value is `"auto"`.
|
|
365
|
+
*/
|
|
366
|
+
worker?: "auto" | boolean;
|
|
367
|
+
}
|
|
368
|
+
}
|
package/src/worker.ts
ADDED