@yume-chan/scrcpy-decoder-shared 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 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-shared</h1>
6
+
7
+ <p align="center">
8
+ Shared types and utilities for Scrcpy video decoders.
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-shared">
19
+ <img alt="Package Size" src="https://img.shields.io/bundlephobia/minzip/%40yume-chan%2Fscrcpy-decoder-shared">
20
+ </a>
21
+ <a href="https://www.npmjs.com/package/@yume-chan/scrcpy-decoder-shared">
22
+ <img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/scrcpy-decoder-shared?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>
@@ -0,0 +1,34 @@
1
+ export interface ScrcpyVideoDecoderPerformanceCounterInterface {
2
+ /**
3
+ * Gets the number of times the decoder has been reset to catch up new keyframes.
4
+ */
5
+ readonly decoderResetCount: number;
6
+ /**
7
+ * Gets the number of frames decoded by the decoder.
8
+ */
9
+ readonly framesDecoded: number;
10
+ /**
11
+ * Gets the number of frames skipped by the decoder.
12
+ */
13
+ readonly framesSkippedDecoding: number;
14
+ }
15
+ export declare class ScrcpyVideoDecoderPerformanceCounter implements ScrcpyVideoDecoderPerformanceCounterInterface {
16
+ #private;
17
+ /**
18
+ * Gets the number of times the decoder has been reset to catch up new keyframes.
19
+ */
20
+ get decoderResetCount(): number;
21
+ /**
22
+ * Gets the number of frames decoded by the decoder.
23
+ */
24
+ get framesDecoded(): number;
25
+ /**
26
+ * Gets the number of frames skipped by the decoder.
27
+ */
28
+ get framesSkippedDecoding(): number;
29
+ increaseResetCount(): void;
30
+ increaseFramesDecoded(): void;
31
+ addFramesDecoded(count: number): void;
32
+ addFramesSkippedDecoding(count: number): void;
33
+ }
34
+ //# sourceMappingURL=decoder-counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder-counter.d.ts","sourceRoot":"","sources":["../src/decoder-counter.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,6CAA6C;IAC1D;;OAEG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;OAEG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;CAC1C;AAED,qBAAa,oCAAqC,YAAW,6CAA6C;;IAEtG;;OAEG;IACH,IAAI,iBAAiB,WAEpB;IAGD;;OAEG;IACH,IAAI,aAAa,WAEhB;IAGD;;OAEG;IACH,IAAI,qBAAqB,WAExB;IAED,kBAAkB;IAIlB,qBAAqB;IAIrB,gBAAgB,CAAC,KAAK,EAAE,MAAM;IAI9B,wBAAwB,CAAC,KAAK,EAAE,MAAM;CAGzC"}
@@ -0,0 +1,36 @@
1
+ export class ScrcpyVideoDecoderPerformanceCounter {
2
+ #decoderResetCount = 0;
3
+ /**
4
+ * Gets the number of times the decoder has been reset to catch up new keyframes.
5
+ */
6
+ get decoderResetCount() {
7
+ return this.#decoderResetCount;
8
+ }
9
+ #framesDecoded = 0;
10
+ /**
11
+ * Gets the number of frames decoded by the decoder.
12
+ */
13
+ get framesDecoded() {
14
+ return this.#framesDecoded;
15
+ }
16
+ #framesSkippedDecoding = 0;
17
+ /**
18
+ * Gets the number of frames skipped by the decoder.
19
+ */
20
+ get framesSkippedDecoding() {
21
+ return this.#framesSkippedDecoding;
22
+ }
23
+ increaseResetCount() {
24
+ this.#decoderResetCount += 1;
25
+ }
26
+ increaseFramesDecoded() {
27
+ this.#framesDecoded += 1;
28
+ }
29
+ addFramesDecoded(count) {
30
+ this.#framesDecoded += count;
31
+ }
32
+ addFramesSkippedDecoding(count) {
33
+ this.#framesSkippedDecoding += count;
34
+ }
35
+ }
36
+ //# sourceMappingURL=decoder-counter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder-counter.js","sourceRoot":"","sources":["../src/decoder-counter.ts"],"names":[],"mappings":"AAeA,MAAM,OAAO,oCAAoC;IAC7C,kBAAkB,GAAG,CAAC,CAAC;IACvB;;OAEG;IACH,IAAI,iBAAiB;QACjB,OAAO,IAAI,CAAC,kBAAkB,CAAC;IACnC,CAAC;IAED,cAAc,GAAG,CAAC,CAAC;IACnB;;OAEG;IACH,IAAI,aAAa;QACb,OAAO,IAAI,CAAC,cAAc,CAAC;IAC/B,CAAC;IAED,sBAAsB,GAAG,CAAC,CAAC;IAC3B;;OAEG;IACH,IAAI,qBAAqB;QACrB,OAAO,IAAI,CAAC,sBAAsB,CAAC;IACvC,CAAC;IAED,kBAAkB;QACd,IAAI,CAAC,kBAAkB,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,qBAAqB;QACjB,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,gBAAgB,CAAC,KAAa;QAC1B,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC;IACjC,CAAC;IAED,wBAAwB,CAAC,KAAa;QAClC,IAAI,CAAC,sBAAsB,IAAI,KAAK,CAAC;IACzC,CAAC;CACJ"}
@@ -0,0 +1,28 @@
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+ import type { Event } from "@yume-chan/event";
3
+ import type { ScrcpyMediaStreamPacket, ScrcpyVideoCodecId, ScrcpyVideoSize } from "@yume-chan/scrcpy";
4
+ import type { WritableStream } from "@yume-chan/stream-extra";
5
+ import type { ScrcpyVideoDecoderPerformanceCounterInterface } from "./decoder-counter.js";
6
+ import type { ScrcpyVideoDecoderPauseControllerInterface } from "./pause-controller.js";
7
+ import type { ScrcpyVideoRendererPerformanceCounterInterface } from "./renderer-counter.js";
8
+ export interface ScrcpyVideoDecoderCapability {
9
+ maxProfile?: number;
10
+ maxLevel?: number;
11
+ }
12
+ export interface ScrcpyVideoDecoder extends ScrcpyVideoDecoderPerformanceCounterInterface, ScrcpyVideoRendererPerformanceCounterInterface, ScrcpyVideoDecoderPauseControllerInterface, ScrcpyVideoSize {
13
+ readonly type: ScrcpyVideoDecoder.Type;
14
+ readonly onTypeChange?: Event<ScrcpyVideoDecoder.Type> | undefined;
15
+ readonly rendererType: ScrcpyVideoDecoder.RendererType;
16
+ readonly onRendererTypeChange?: Event<ScrcpyVideoDecoder.RendererType> | undefined;
17
+ readonly writable: WritableStream<ScrcpyMediaStreamPacket>;
18
+ dispose?(): MaybePromiseLike<undefined>;
19
+ }
20
+ export declare namespace ScrcpyVideoDecoder {
21
+ type Type = "software" | "hardware";
22
+ type RendererType = "software" | "hardware";
23
+ }
24
+ export interface ScrcpyVideoDecoderConstructor {
25
+ readonly capabilities: Record<string, ScrcpyVideoDecoderCapability>;
26
+ new (codec: ScrcpyVideoCodecId): ScrcpyVideoDecoder;
27
+ }
28
+ //# sourceMappingURL=decoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../src/decoder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EACR,uBAAuB,EACvB,kBAAkB,EAClB,eAAe,EAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,OAAO,KAAK,EAAE,6CAA6C,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,KAAK,EAAE,0CAA0C,EAAE,MAAM,uBAAuB,CAAC;AACxF,OAAO,KAAK,EAAE,8CAA8C,EAAE,MAAM,uBAAuB,CAAC;AAE5F,MAAM,WAAW,4BAA4B;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBACb,SACI,6CAA6C,EAC7C,8CAA8C,EAC9C,0CAA0C,EAC1C,eAAe;IACnB,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC;IACvC,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;IAEnE,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC,YAAY,CAAC;IACvD,QAAQ,CAAC,oBAAoB,CAAC,EACxB,KAAK,CAAC,kBAAkB,CAAC,YAAY,CAAC,GACtC,SAAS,CAAC;IAEhB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,uBAAuB,CAAC,CAAC;IAE3D,OAAO,CAAC,IAAI,gBAAgB,CAAC,SAAS,CAAC,CAAC;CAC3C;AAED,yBAAiB,kBAAkB,CAAC;IAChC,KAAY,IAAI,GAAG,UAAU,GAAG,UAAU,CAAC;IAE3C,KAAY,YAAY,GAAG,UAAU,GAAG,UAAU,CAAC;CACtD;AAED,MAAM,WAAW,6BAA6B;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,4BAA4B,CAAC,CAAC;IAEpE,KAAK,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CAAC;CACvD"}
package/esm/decoder.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=decoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder.js","sourceRoot":"","sources":["../src/decoder.ts"],"names":[],"mappings":""}
package/esm/gl.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare function createCanvas(): HTMLCanvasElement | OffscreenCanvas;
2
+ export declare function glCreateContext(canvas: HTMLCanvasElement | OffscreenCanvas, attributes?: WebGLContextAttributes): WebGLRenderingContext | WebGL2RenderingContext | null;
3
+ export declare function glLoseContext(context: WebGLRenderingContext): void;
4
+ export declare function glIsSupported(attributes?: WebGLContextAttributes): boolean;
5
+ //# sourceMappingURL=gl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gl.d.ts","sourceRoot":"","sources":["../src/gl.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,wCAQ3B;AAED,wBAAgB,eAAe,CAC3B,MAAM,EAAE,iBAAiB,GAAG,eAAe,EAC3C,UAAU,CAAC,EAAE,sBAAsB,GACpC,qBAAqB,GAAG,sBAAsB,GAAG,IAAI,CA6CvD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,qBAAqB,QAM3D;AAED,wBAAgB,aAAa,CAAC,UAAU,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAS1E"}
package/esm/gl.js ADDED
@@ -0,0 +1,58 @@
1
+ export function createCanvas() {
2
+ if (typeof document !== "undefined") {
3
+ return document.createElement("canvas");
4
+ }
5
+ if (typeof OffscreenCanvas !== "undefined") {
6
+ return new OffscreenCanvas(1, 1);
7
+ }
8
+ throw new Error("no canvas input found nor any canvas can be created");
9
+ }
10
+ export function glCreateContext(canvas, attributes) {
11
+ // `HTMLCanvasElement.getContext` returns `null` for unsupported `contextId`,
12
+ // but `OffscreenCanvas.getContext` will throw errors,
13
+ // Support both cases using both `try...catch...` and `if`
14
+ try {
15
+ const context = canvas.getContext("webgl2", attributes);
16
+ if (context) {
17
+ return context;
18
+ }
19
+ }
20
+ catch {
21
+ // ignore
22
+ }
23
+ try {
24
+ const context = canvas.getContext("webgl", attributes);
25
+ if (context) {
26
+ return context;
27
+ }
28
+ }
29
+ catch {
30
+ // ignore
31
+ }
32
+ // Support very old browsers just in case
33
+ // `OffscreenCanvas` doesn't support `experimental-webgl`
34
+ if (canvas instanceof HTMLCanvasElement) {
35
+ const context = canvas.getContext("experimental-webgl", attributes);
36
+ if (context) {
37
+ return context;
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+ export function glLoseContext(context) {
43
+ try {
44
+ context.getExtension("WEBGL_lose_context")?.loseContext();
45
+ }
46
+ catch {
47
+ // ignore
48
+ }
49
+ }
50
+ export function glIsSupported(attributes) {
51
+ const canvas = createCanvas();
52
+ const gl = glCreateContext(canvas, attributes);
53
+ if (gl) {
54
+ glLoseContext(gl);
55
+ }
56
+ return !!gl;
57
+ }
58
+ //# sourceMappingURL=gl.js.map
package/esm/gl.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gl.js","sourceRoot":"","sources":["../src/gl.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY;IACxB,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC;IACD,IAAI,OAAO,eAAe,KAAK,WAAW,EAAE,CAAC;QACzC,OAAO,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,eAAe,CAC3B,MAA2C,EAC3C,UAAmC;IAEnC,6EAA6E;IAC7E,sDAAsD;IACtD,0DAA0D;IAE1D,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAC7B,QAAQ,EACR,UAAU,CACoB,CAAC;QAEnC,IAAI,OAAO,EAAE,CAAC;YACV,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,SAAS;IACb,CAAC;IAED,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAC7B,OAAO,EACP,UAAU,CACmB,CAAC;QAElC,IAAI,OAAO,EAAE,CAAC;YACV,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,SAAS;IACb,CAAC;IAED,yCAAyC;IACzC,yDAAyD;IACzD,IAAI,MAAM,YAAY,iBAAiB,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAC7B,oBAAoB,EACpB,UAAU,CACmB,CAAC;QAElC,IAAI,OAAO,EAAE,CAAC;YACV,OAAO,OAAO,CAAC;QACnB,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAA8B;IACxD,IAAI,CAAC;QACD,OAAO,CAAC,YAAY,CAAC,oBAAoB,CAAC,EAAE,WAAW,EAAE,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACL,SAAS;IACb,CAAC;AACL,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,UAAmC;IAC7D,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAE9B,MAAM,EAAE,GAAG,eAAe,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC/C,IAAI,EAAE,EAAE,CAAC;QACL,aAAa,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,CAAC,CAAC,EAAE,CAAC;AAChB,CAAC"}
package/esm/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./decoder-counter.js";
2
+ export * from "./decoder.js";
3
+ export * from "./gl.js";
4
+ export * from "./pause-controller.js";
5
+ export * from "./renderer-counter.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC"}
package/esm/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./decoder-counter.js";
2
+ export * from "./decoder.js";
3
+ export * from "./gl.js";
4
+ export * from "./pause-controller.js";
5
+ export * from "./renderer-counter.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,42 @@
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+ import type { ScrcpyMediaStreamConfigurationPacket, ScrcpyMediaStreamDataPacket, ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
3
+ import { TransformStream } from "@yume-chan/stream-extra";
4
+ export interface ScrcpyVideoDecoderPauseControllerInterface {
5
+ readonly paused: boolean;
6
+ /**
7
+ * Pause the decoder.
8
+ *
9
+ * It has a higher priority than {@link trackDocumentVisibility}.
10
+ * After calling `pause`, the decoder can only be resumed by calling {@link resume}.
11
+ */
12
+ pause(): void;
13
+ /**
14
+ * Resume the decoder if it was paused.
15
+ */
16
+ resume(): MaybePromiseLike<undefined>;
17
+ /**
18
+ * Pause the decoder when the document becomes invisible,
19
+ * and resume when the document becomes visible,
20
+ * to save system resources and battery.
21
+ *
22
+ * @param document A document to track
23
+ * @returns A function to stop tracking, and resume the decoder (if it was not explicitly paused)
24
+ */
25
+ trackDocumentVisibility(document: Document): () => undefined;
26
+ }
27
+ export declare class ScrcpyVideoDecoderPauseController extends TransformStream<ScrcpyVideoDecoderPauseController.Input, ScrcpyVideoDecoderPauseController.Output> implements ScrcpyVideoDecoderPauseControllerInterface {
28
+ #private;
29
+ get paused(): boolean;
30
+ constructor();
31
+ pause(): void;
32
+ resume(): undefined;
33
+ trackDocumentVisibility(document: Document): () => undefined;
34
+ dispose(): void;
35
+ }
36
+ export declare namespace ScrcpyVideoDecoderPauseController {
37
+ type Input = ScrcpyMediaStreamPacket;
38
+ type Output = ScrcpyMediaStreamConfigurationPacket | (ScrcpyMediaStreamDataPacket & {
39
+ skipRendering: boolean;
40
+ });
41
+ }
42
+ //# sourceMappingURL=pause-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pause-controller.d.ts","sourceRoot":"","sources":["../src/pause-controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,KAAK,EACR,oCAAoC,EACpC,2BAA2B,EAC3B,uBAAuB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,WAAW,0CAA0C;IACvD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,KAAK,IAAI,IAAI,CAAC;IAEd;;OAEG;IACH,MAAM,IAAI,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAEtC;;;;;;;OAOG;IACH,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,SAAS,CAAC;CAChE;AAED,qBAAa,iCACT,SAAQ,eAAe,CACnB,iCAAiC,CAAC,KAAK,EACvC,iCAAiC,CAAC,MAAM,CAE5C,YAAW,0CAA0C;;IAMrD,IAAI,MAAM,YAET;;IA6FD,KAAK,IAAI,IAAI;IAiDb,MAAM,IAAI,SAAS;IAMnB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,SAAS;IAsC5D,OAAO;CAaV;AAED,yBAAiB,iCAAiC,CAAC;IAC/C,KAAY,KAAK,GAAG,uBAAuB,CAAC;IAC5C,KAAY,MAAM,GACZ,oCAAoC,GACpC,CAAC,2BAA2B,GAAG;QAAE,aAAa,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACpE"}
@@ -0,0 +1,172 @@
1
+ import { PromiseResolver } from "@yume-chan/async";
2
+ import { TransformStream } from "@yume-chan/stream-extra";
3
+ export class ScrcpyVideoDecoderPauseController extends TransformStream {
4
+ #controller;
5
+ #paused = false;
6
+ #pausedExplicitly = false;
7
+ get paused() {
8
+ return this.#paused;
9
+ }
10
+ /**
11
+ * Store incoming configuration change when paused,
12
+ * to recreate the decoder on resume
13
+ */
14
+ #pendingConfiguration;
15
+ /**
16
+ * Store incoming frames when paused, so the latest frame can be rendered on resume
17
+ * Because non-key frames require their previous frames to be decoded,
18
+ * we need to store several frames.
19
+ *
20
+ * There can be two situations:
21
+ *
22
+ * 1. **All pending frames are non-key frames:**
23
+ * the decoder still holds the previous frames to decode them directly.
24
+ *
25
+ * 2. **A keyframe is encountered while pausing:**
26
+ * The list is cleared before pushing the keyframe.
27
+ * The decoder can start decoding from the keyframe directly.
28
+ */
29
+ #pendingFrames = [];
30
+ /** Block incoming frames while resuming */
31
+ #resuming;
32
+ #disposed = false;
33
+ constructor() {
34
+ let controller;
35
+ super({
36
+ start: (controller_) => {
37
+ controller = controller_;
38
+ },
39
+ transform: async (packet) => {
40
+ if (this.#disposed) {
41
+ return;
42
+ }
43
+ if (this.#paused) {
44
+ switch (packet.type) {
45
+ case "configuration":
46
+ this.#pendingConfiguration = packet;
47
+ this.#pendingFrames.length = 0;
48
+ break;
49
+ case "data":
50
+ if (packet.keyframe) {
51
+ this.#pendingFrames.length = 0;
52
+ }
53
+ // Generally there won't be too many non-key frames
54
+ // (because that's bad for video quality),
55
+ // Also all frames are required for proper decoding
56
+ this.#pendingFrames.push(packet);
57
+ break;
58
+ }
59
+ return;
60
+ }
61
+ await this.#resuming;
62
+ if (this.#disposed) {
63
+ return;
64
+ }
65
+ switch (packet.type) {
66
+ case "configuration":
67
+ controller.enqueue(packet);
68
+ break;
69
+ case "data":
70
+ controller.enqueue({
71
+ ...packet,
72
+ skipRendering: false,
73
+ });
74
+ break;
75
+ }
76
+ },
77
+ });
78
+ this.#controller = controller;
79
+ }
80
+ #pauseInternal(explicitly) {
81
+ if (this.#disposed) {
82
+ throw new Error("Attempt to pause a closed decoder");
83
+ }
84
+ this.#paused = true;
85
+ if (explicitly) {
86
+ this.#pausedExplicitly = true;
87
+ }
88
+ }
89
+ pause() {
90
+ this.#pauseInternal(true);
91
+ }
92
+ #resumeInternal(explicitly) {
93
+ if (this.#disposed) {
94
+ throw new Error("Attempt to resume a closed decoder");
95
+ }
96
+ if (!this.#paused) {
97
+ return;
98
+ }
99
+ // Visibility tracker (explicitly = false) can't resume
100
+ // a decoder paused by user (#pausedExplicitly = true)
101
+ if (this.#pausedExplicitly && !explicitly) {
102
+ return;
103
+ }
104
+ const resolver = new PromiseResolver();
105
+ this.#resuming = resolver.promise;
106
+ this.#paused = false;
107
+ this.#pausedExplicitly = false;
108
+ if (this.#pendingConfiguration) {
109
+ this.#controller.enqueue(this.#pendingConfiguration);
110
+ this.#pendingConfiguration = undefined;
111
+ }
112
+ // `#pendingFrames` won't change during iteration
113
+ // because it can only change when `#paused` is `true`,
114
+ // but the code above already sets that to `false`
115
+ for (const [index, frame] of this.#pendingFrames.entries()) {
116
+ // All pending frames except the last one don't need to be rendered
117
+ // because they are decoded in quick succession by the decoder
118
+ // and won't be visible
119
+ this.#controller.enqueue({
120
+ ...frame,
121
+ skipRendering: index !== this.#pendingFrames.length - 1,
122
+ });
123
+ }
124
+ this.#pendingFrames.length = 0;
125
+ resolver.resolve(undefined);
126
+ this.#resuming = undefined;
127
+ }
128
+ resume() {
129
+ this.#resumeInternal(true);
130
+ }
131
+ #disposeVisibilityTracker;
132
+ trackDocumentVisibility(document) {
133
+ // Can only track one document
134
+ this.#disposeVisibilityTracker?.();
135
+ const handleVisibilityChange = () => {
136
+ if (document.visibilityState === "hidden") {
137
+ this.#pauseInternal(false);
138
+ }
139
+ else {
140
+ void this.#resumeInternal(false);
141
+ }
142
+ };
143
+ handleVisibilityChange();
144
+ document.addEventListener("visibilitychange", handleVisibilityChange);
145
+ let disposed = false;
146
+ this.#disposeVisibilityTracker = () => {
147
+ // Make sure the visibility tracker is only disposed once
148
+ // Do not resume if it's paused by another visibility tracker
149
+ if (disposed) {
150
+ return;
151
+ }
152
+ disposed = true;
153
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
154
+ // Do not resume if decoder is already disposed
155
+ if (!this.#disposed) {
156
+ void this.#resumeInternal(false);
157
+ }
158
+ };
159
+ return this.#disposeVisibilityTracker;
160
+ }
161
+ dispose() {
162
+ if (this.#disposed) {
163
+ return;
164
+ }
165
+ this.#disposed = true;
166
+ this.#controller.terminate();
167
+ this.#pendingConfiguration = undefined;
168
+ this.#pendingFrames.length = 0;
169
+ this.#disposeVisibilityTracker?.();
170
+ }
171
+ }
172
+ //# sourceMappingURL=pause-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pause-controller.js","sourceRoot":"","sources":["../src/pause-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ export interface ScrcpyVideoRendererPerformanceCounterInterface {
2
+ /**
3
+ * Gets the number of frames that have been drawn on the renderer.
4
+ */
5
+ readonly framesRendered: number;
6
+ /**
7
+ * Gets the number of frames that's visible to the user.
8
+ *
9
+ * Multiple frames might be rendered during one vertical sync interval,
10
+ * but only the last of them is represented to the user.
11
+ * This costs some performance but reduces latency by 1 frame.
12
+ *
13
+ * Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
14
+ * https://issues.chromium.org/issues/41483010
15
+ */
16
+ readonly framesDisplayed: number;
17
+ /**
18
+ * Gets the number of frames that wasn't drawn on the renderer
19
+ * because the renderer can't keep up
20
+ */
21
+ readonly framesSkippedRendering: number;
22
+ }
23
+ export declare class ScrcpyVideoRendererPerformanceCounter implements ScrcpyVideoRendererPerformanceCounterInterface {
24
+ #private;
25
+ /**
26
+ * Gets the number of frames that have been drawn on the renderer.
27
+ */
28
+ get framesRendered(): number;
29
+ /**
30
+ * Gets the number of frames that's visible to the user.
31
+ *
32
+ * Multiple frames might be rendered during one vertical sync interval,
33
+ * but only the last of them is represented to the user.
34
+ * This costs some performance but reduces latency by 1 frame.
35
+ *
36
+ * Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
37
+ * https://issues.chromium.org/issues/41483010
38
+ */
39
+ get framesDisplayed(): number;
40
+ /**
41
+ * Gets the number of frames that wasn't drawn on the renderer
42
+ * because the renderer can't keep up
43
+ */
44
+ get framesSkippedRendering(): number;
45
+ constructor();
46
+ increaseFramesRendered(): void;
47
+ increaseFramesSkippedRendering(): void;
48
+ addFramesSkippedRendering(count: number): void;
49
+ dispose(): void;
50
+ }
51
+ //# sourceMappingURL=renderer-counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer-counter.d.ts","sourceRoot":"","sources":["../src/renderer-counter.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,8CAA8C;IAC3D;;OAEG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;;;;;OASG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CAC3C;AAED,qBAAa,qCAAsC,YAAW,8CAA8C;;IAExG;;OAEG;IACH,IAAI,cAAc,WAEjB;IAQD;;;;;;;;;OASG;IACH,IAAI,eAAe,WAElB;IAGD;;;OAGG;IACH,IAAI,sBAAsB,WAEzB;;IAiCD,sBAAsB;IAKtB,8BAA8B;IAI9B,yBAAyB,CAAC,KAAK,EAAE,MAAM;IAIvC,OAAO;CAMV"}
@@ -0,0 +1,76 @@
1
+ export class ScrcpyVideoRendererPerformanceCounter {
2
+ #framesRendered = 0;
3
+ /**
4
+ * Gets the number of frames that have been drawn on the renderer.
5
+ */
6
+ get framesRendered() {
7
+ return this.#framesRendered;
8
+ }
9
+ /**
10
+ * `true` if the next vertical sync will display a different frame
11
+ */
12
+ #hasNewFrame = false;
13
+ #framesDisplayed = 0;
14
+ /**
15
+ * Gets the number of frames that's visible to the user.
16
+ *
17
+ * Multiple frames might be rendered during one vertical sync interval,
18
+ * but only the last of them is represented to the user.
19
+ * This costs some performance but reduces latency by 1 frame.
20
+ *
21
+ * Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
22
+ * https://issues.chromium.org/issues/41483010
23
+ */
24
+ get framesDisplayed() {
25
+ return this.#framesDisplayed;
26
+ }
27
+ #framesSkippedRendering = 0;
28
+ /**
29
+ * Gets the number of frames that wasn't drawn on the renderer
30
+ * because the renderer can't keep up
31
+ */
32
+ get framesSkippedRendering() {
33
+ return this.#framesSkippedRendering;
34
+ }
35
+ #animationFrameId;
36
+ constructor() {
37
+ // `requestAnimationFrame` is available in Web Worker
38
+ // https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/requestAnimationFrame
39
+ try {
40
+ this.#animationFrameId = requestAnimationFrame(this.#handleAnimationFrame);
41
+ }
42
+ catch {
43
+ // Chrome has a bug that `requestAnimationFrame` doesn't work in nested Workers
44
+ // https://issues.chromium.org/issues/41483010
45
+ // Because we need actual vertical sync to count presented frames,
46
+ // `setTimeout` with a fixed delay also doesn't work.
47
+ // In this case just leave `framesDisplayed` at `0`
48
+ }
49
+ }
50
+ #handleAnimationFrame = () => {
51
+ // Animation frame handler is called on every vertical sync interval.
52
+ // Only then a frame is visible to the user.
53
+ if (this.#hasNewFrame) {
54
+ this.#framesDisplayed += 1;
55
+ this.#hasNewFrame = false;
56
+ }
57
+ this.#animationFrameId = requestAnimationFrame(this.#handleAnimationFrame);
58
+ };
59
+ increaseFramesRendered() {
60
+ this.#framesRendered += 1;
61
+ this.#hasNewFrame = true;
62
+ }
63
+ increaseFramesSkippedRendering() {
64
+ this.#framesSkippedRendering += 1;
65
+ }
66
+ addFramesSkippedRendering(count) {
67
+ this.#framesSkippedRendering += count;
68
+ }
69
+ dispose() {
70
+ // `0` is a valid value for RAF ID
71
+ if (this.#animationFrameId !== undefined) {
72
+ cancelAnimationFrame(this.#animationFrameId);
73
+ }
74
+ }
75
+ }
76
+ //# sourceMappingURL=renderer-counter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer-counter.js","sourceRoot":"","sources":["../src/renderer-counter.ts"],"names":[],"mappings":"AAuBA,MAAM,OAAO,qCAAqC;IAC9C,eAAe,GAAG,CAAC,CAAC;IACpB;;OAEG;IACH,IAAI,cAAc;QACd,OAAO,IAAI,CAAC,eAAe,CAAC;IAChC,CAAC;IAED;;OAEG;IACH,YAAY,GAAG,KAAK,CAAC;IAErB,gBAAgB,GAAG,CAAC,CAAC;IACrB;;;;;;;;;OASG;IACH,IAAI,eAAe;QACf,OAAO,IAAI,CAAC,gBAAgB,CAAC;IACjC,CAAC;IAED,uBAAuB,GAAG,CAAC,CAAC;IAC5B;;;OAGG;IACH,IAAI,sBAAsB;QACtB,OAAO,IAAI,CAAC,uBAAuB,CAAC;IACxC,CAAC;IAED,iBAAiB,CAAqB;IAEtC;QACI,qDAAqD;QACrD,oGAAoG;QACpG,IAAI,CAAC;YACD,IAAI,CAAC,iBAAiB,GAAG,qBAAqB,CAC1C,IAAI,CAAC,qBAAqB,CAC7B,CAAC;QACN,CAAC;QAAC,MAAM,CAAC;YACL,+EAA+E;YAC/E,8CAA8C;YAC9C,kEAAkE;YAClE,qDAAqD;YACrD,mDAAmD;QACvD,CAAC;IACL,CAAC;IAED,qBAAqB,GAAG,GAAG,EAAE;QACzB,qEAAqE;QACrE,4CAA4C;QAC5C,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC;YAC3B,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC9B,CAAC;QAED,IAAI,CAAC,iBAAiB,GAAG,qBAAqB,CAC1C,IAAI,CAAC,qBAAqB,CAC7B,CAAC;IACN,CAAC,CAAC;IAEF,sBAAsB;QAClB,IAAI,CAAC,eAAe,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IAC7B,CAAC;IAED,8BAA8B;QAC1B,IAAI,CAAC,uBAAuB,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,yBAAyB,CAAC,KAAa;QACnC,IAAI,CAAC,uBAAuB,IAAI,KAAK,CAAC;IAC1C,CAAC;IAED,OAAO;QACH,kCAAkC;QAClC,IAAI,IAAI,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACvC,oBAAoB,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACjD,CAAC;IACL,CAAC;CACJ"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@yume-chan/scrcpy-decoder-shared",
3
+ "version": "3.0.0-beta.0",
4
+ "description": "Shared types and utilities for Scrcpy video decoders.",
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-shared#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/yume-chan/ya-webadb.git",
21
+ "directory": "libraries/scrcpy-decoder-shared"
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/event": "3.0.0-beta.0",
33
+ "@yume-chan/scrcpy": "3.0.0-beta.0",
34
+ "@yume-chan/stream-extra": "3.0.0-beta.0"
35
+ },
36
+ "devDependencies": {
37
+ "prettier": "^3.8.4",
38
+ "typescript": "^6.0.3",
39
+ "@yume-chan/eslint-config": "^1.0.0",
40
+ "@yume-chan/tsconfig": "^1.0.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.build.json",
44
+ "lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4"
45
+ }
46
+ }
@@ -0,0 +1,56 @@
1
+ export interface ScrcpyVideoDecoderPerformanceCounterInterface {
2
+ /**
3
+ * Gets the number of times the decoder has been reset to catch up new keyframes.
4
+ */
5
+ readonly decoderResetCount: number;
6
+ /**
7
+ * Gets the number of frames decoded by the decoder.
8
+ */
9
+ readonly framesDecoded: number;
10
+ /**
11
+ * Gets the number of frames skipped by the decoder.
12
+ */
13
+ readonly framesSkippedDecoding: number;
14
+ }
15
+
16
+ export class ScrcpyVideoDecoderPerformanceCounter implements ScrcpyVideoDecoderPerformanceCounterInterface {
17
+ #decoderResetCount = 0;
18
+ /**
19
+ * Gets the number of times the decoder has been reset to catch up new keyframes.
20
+ */
21
+ get decoderResetCount() {
22
+ return this.#decoderResetCount;
23
+ }
24
+
25
+ #framesDecoded = 0;
26
+ /**
27
+ * Gets the number of frames decoded by the decoder.
28
+ */
29
+ get framesDecoded() {
30
+ return this.#framesDecoded;
31
+ }
32
+
33
+ #framesSkippedDecoding = 0;
34
+ /**
35
+ * Gets the number of frames skipped by the decoder.
36
+ */
37
+ get framesSkippedDecoding() {
38
+ return this.#framesSkippedDecoding;
39
+ }
40
+
41
+ increaseResetCount() {
42
+ this.#decoderResetCount += 1;
43
+ }
44
+
45
+ increaseFramesDecoded() {
46
+ this.#framesDecoded += 1;
47
+ }
48
+
49
+ addFramesDecoded(count: number) {
50
+ this.#framesDecoded += count;
51
+ }
52
+
53
+ addFramesSkippedDecoding(count: number) {
54
+ this.#framesSkippedDecoding += count;
55
+ }
56
+ }
package/src/decoder.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+ import type { Event } from "@yume-chan/event";
3
+ import type {
4
+ ScrcpyMediaStreamPacket,
5
+ ScrcpyVideoCodecId,
6
+ ScrcpyVideoSize,
7
+ } from "@yume-chan/scrcpy";
8
+ import type { WritableStream } from "@yume-chan/stream-extra";
9
+
10
+ import type { ScrcpyVideoDecoderPerformanceCounterInterface } from "./decoder-counter.js";
11
+ import type { ScrcpyVideoDecoderPauseControllerInterface } from "./pause-controller.js";
12
+ import type { ScrcpyVideoRendererPerformanceCounterInterface } from "./renderer-counter.js";
13
+
14
+ export interface ScrcpyVideoDecoderCapability {
15
+ maxProfile?: number;
16
+ maxLevel?: number;
17
+ }
18
+
19
+ export interface ScrcpyVideoDecoder
20
+ extends
21
+ ScrcpyVideoDecoderPerformanceCounterInterface,
22
+ ScrcpyVideoRendererPerformanceCounterInterface,
23
+ ScrcpyVideoDecoderPauseControllerInterface,
24
+ ScrcpyVideoSize {
25
+ readonly type: ScrcpyVideoDecoder.Type;
26
+ readonly onTypeChange?: Event<ScrcpyVideoDecoder.Type> | undefined;
27
+
28
+ readonly rendererType: ScrcpyVideoDecoder.RendererType;
29
+ readonly onRendererTypeChange?:
30
+ | Event<ScrcpyVideoDecoder.RendererType>
31
+ | undefined;
32
+
33
+ readonly writable: WritableStream<ScrcpyMediaStreamPacket>;
34
+
35
+ dispose?(): MaybePromiseLike<undefined>;
36
+ }
37
+
38
+ export namespace ScrcpyVideoDecoder {
39
+ export type Type = "software" | "hardware";
40
+
41
+ export type RendererType = "software" | "hardware";
42
+ }
43
+
44
+ export interface ScrcpyVideoDecoderConstructor {
45
+ readonly capabilities: Record<string, ScrcpyVideoDecoderCapability>;
46
+
47
+ new (codec: ScrcpyVideoCodecId): ScrcpyVideoDecoder;
48
+ }
package/src/gl.ts ADDED
@@ -0,0 +1,78 @@
1
+ export function createCanvas() {
2
+ if (typeof document !== "undefined") {
3
+ return document.createElement("canvas");
4
+ }
5
+ if (typeof OffscreenCanvas !== "undefined") {
6
+ return new OffscreenCanvas(1, 1);
7
+ }
8
+ throw new Error("no canvas input found nor any canvas can be created");
9
+ }
10
+
11
+ export function glCreateContext(
12
+ canvas: HTMLCanvasElement | OffscreenCanvas,
13
+ attributes?: WebGLContextAttributes,
14
+ ): WebGLRenderingContext | WebGL2RenderingContext | null {
15
+ // `HTMLCanvasElement.getContext` returns `null` for unsupported `contextId`,
16
+ // but `OffscreenCanvas.getContext` will throw errors,
17
+ // Support both cases using both `try...catch...` and `if`
18
+
19
+ try {
20
+ const context = canvas.getContext(
21
+ "webgl2",
22
+ attributes,
23
+ ) as WebGL2RenderingContext | null;
24
+
25
+ if (context) {
26
+ return context;
27
+ }
28
+ } catch {
29
+ // ignore
30
+ }
31
+
32
+ try {
33
+ const context = canvas.getContext(
34
+ "webgl",
35
+ attributes,
36
+ ) as WebGLRenderingContext | null;
37
+
38
+ if (context) {
39
+ return context;
40
+ }
41
+ } catch {
42
+ // ignore
43
+ }
44
+
45
+ // Support very old browsers just in case
46
+ // `OffscreenCanvas` doesn't support `experimental-webgl`
47
+ if (canvas instanceof HTMLCanvasElement) {
48
+ const context = canvas.getContext(
49
+ "experimental-webgl",
50
+ attributes,
51
+ ) as WebGLRenderingContext | null;
52
+
53
+ if (context) {
54
+ return context;
55
+ }
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ export function glLoseContext(context: WebGLRenderingContext) {
62
+ try {
63
+ context.getExtension("WEBGL_lose_context")?.loseContext();
64
+ } catch {
65
+ // ignore
66
+ }
67
+ }
68
+
69
+ export function glIsSupported(attributes?: WebGLContextAttributes): boolean {
70
+ const canvas = createCanvas();
71
+
72
+ const gl = glCreateContext(canvas, attributes);
73
+ if (gl) {
74
+ glLoseContext(gl);
75
+ }
76
+
77
+ return !!gl;
78
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./decoder-counter.js";
2
+ export * from "./decoder.js";
3
+ export * from "./gl.js";
4
+ export * from "./pause-controller.js";
5
+ export * from "./renderer-counter.js";
@@ -0,0 +1,257 @@
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+ import { PromiseResolver } from "@yume-chan/async";
3
+ import type {
4
+ ScrcpyMediaStreamConfigurationPacket,
5
+ ScrcpyMediaStreamDataPacket,
6
+ ScrcpyMediaStreamPacket,
7
+ } from "@yume-chan/scrcpy";
8
+ import type { TransformStreamDefaultController } from "@yume-chan/stream-extra";
9
+ import { TransformStream } from "@yume-chan/stream-extra";
10
+
11
+ export interface ScrcpyVideoDecoderPauseControllerInterface {
12
+ readonly paused: boolean;
13
+
14
+ /**
15
+ * Pause the decoder.
16
+ *
17
+ * It has a higher priority than {@link trackDocumentVisibility}.
18
+ * After calling `pause`, the decoder can only be resumed by calling {@link resume}.
19
+ */
20
+ pause(): void;
21
+
22
+ /**
23
+ * Resume the decoder if it was paused.
24
+ */
25
+ resume(): MaybePromiseLike<undefined>;
26
+
27
+ /**
28
+ * Pause the decoder when the document becomes invisible,
29
+ * and resume when the document becomes visible,
30
+ * to save system resources and battery.
31
+ *
32
+ * @param document A document to track
33
+ * @returns A function to stop tracking, and resume the decoder (if it was not explicitly paused)
34
+ */
35
+ trackDocumentVisibility(document: Document): () => undefined;
36
+ }
37
+
38
+ export class ScrcpyVideoDecoderPauseController
39
+ extends TransformStream<
40
+ ScrcpyVideoDecoderPauseController.Input,
41
+ ScrcpyVideoDecoderPauseController.Output
42
+ >
43
+ implements ScrcpyVideoDecoderPauseControllerInterface
44
+ {
45
+ #controller: TransformStreamDefaultController<ScrcpyVideoDecoderPauseController.Output>;
46
+
47
+ #paused = false;
48
+ #pausedExplicitly = false;
49
+ get paused() {
50
+ return this.#paused;
51
+ }
52
+
53
+ /**
54
+ * Store incoming configuration change when paused,
55
+ * to recreate the decoder on resume
56
+ */
57
+ #pendingConfiguration: ScrcpyMediaStreamConfigurationPacket | undefined;
58
+ /**
59
+ * Store incoming frames when paused, so the latest frame can be rendered on resume
60
+ * Because non-key frames require their previous frames to be decoded,
61
+ * we need to store several frames.
62
+ *
63
+ * There can be two situations:
64
+ *
65
+ * 1. **All pending frames are non-key frames:**
66
+ * the decoder still holds the previous frames to decode them directly.
67
+ *
68
+ * 2. **A keyframe is encountered while pausing:**
69
+ * The list is cleared before pushing the keyframe.
70
+ * The decoder can start decoding from the keyframe directly.
71
+ */
72
+ #pendingFrames: ScrcpyMediaStreamDataPacket[] = [];
73
+
74
+ /** Block incoming frames while resuming */
75
+ #resuming: Promise<undefined> | undefined;
76
+
77
+ #disposed = false;
78
+
79
+ constructor() {
80
+ let controller!: TransformStreamDefaultController<ScrcpyVideoDecoderPauseController.Output>;
81
+
82
+ super({
83
+ start: (controller_) => {
84
+ controller = controller_;
85
+ },
86
+ transform: async (packet) => {
87
+ if (this.#disposed) {
88
+ return;
89
+ }
90
+
91
+ if (this.#paused) {
92
+ switch (packet.type) {
93
+ case "configuration":
94
+ this.#pendingConfiguration = packet;
95
+ this.#pendingFrames.length = 0;
96
+ break;
97
+ case "data":
98
+ if (packet.keyframe) {
99
+ this.#pendingFrames.length = 0;
100
+ }
101
+ // Generally there won't be too many non-key frames
102
+ // (because that's bad for video quality),
103
+ // Also all frames are required for proper decoding
104
+ this.#pendingFrames.push(packet);
105
+ break;
106
+ }
107
+ return;
108
+ }
109
+
110
+ await this.#resuming;
111
+
112
+ if (this.#disposed) {
113
+ return;
114
+ }
115
+
116
+ switch (packet.type) {
117
+ case "configuration":
118
+ controller.enqueue(packet);
119
+ break;
120
+ case "data":
121
+ controller.enqueue({
122
+ ...packet,
123
+ skipRendering: false,
124
+ });
125
+ break;
126
+ }
127
+ },
128
+ });
129
+
130
+ this.#controller = controller;
131
+ }
132
+
133
+ #pauseInternal(explicitly: boolean): void {
134
+ if (this.#disposed) {
135
+ throw new Error("Attempt to pause a closed decoder");
136
+ }
137
+
138
+ this.#paused = true;
139
+ if (explicitly) {
140
+ this.#pausedExplicitly = true;
141
+ }
142
+ }
143
+
144
+ pause(): void {
145
+ this.#pauseInternal(true);
146
+ }
147
+
148
+ #resumeInternal(explicitly: boolean): undefined {
149
+ if (this.#disposed) {
150
+ throw new Error("Attempt to resume a closed decoder");
151
+ }
152
+
153
+ if (!this.#paused) {
154
+ return;
155
+ }
156
+
157
+ // Visibility tracker (explicitly = false) can't resume
158
+ // a decoder paused by user (#pausedExplicitly = true)
159
+ if (this.#pausedExplicitly && !explicitly) {
160
+ return;
161
+ }
162
+
163
+ const resolver = new PromiseResolver<undefined>();
164
+ this.#resuming = resolver.promise;
165
+
166
+ this.#paused = false;
167
+ this.#pausedExplicitly = false;
168
+
169
+ if (this.#pendingConfiguration) {
170
+ this.#controller.enqueue(this.#pendingConfiguration);
171
+ this.#pendingConfiguration = undefined;
172
+ }
173
+
174
+ // `#pendingFrames` won't change during iteration
175
+ // because it can only change when `#paused` is `true`,
176
+ // but the code above already sets that to `false`
177
+ for (const [index, frame] of this.#pendingFrames.entries()) {
178
+ // All pending frames except the last one don't need to be rendered
179
+ // because they are decoded in quick succession by the decoder
180
+ // and won't be visible
181
+ this.#controller.enqueue({
182
+ ...frame,
183
+ skipRendering: index !== this.#pendingFrames.length - 1,
184
+ });
185
+ }
186
+
187
+ this.#pendingFrames.length = 0;
188
+
189
+ resolver.resolve(undefined);
190
+ this.#resuming = undefined;
191
+ }
192
+
193
+ resume(): undefined {
194
+ this.#resumeInternal(true);
195
+ }
196
+
197
+ #disposeVisibilityTracker: (() => undefined) | undefined;
198
+
199
+ trackDocumentVisibility(document: Document): () => undefined {
200
+ // Can only track one document
201
+ this.#disposeVisibilityTracker?.();
202
+
203
+ const handleVisibilityChange = () => {
204
+ if (document.visibilityState === "hidden") {
205
+ this.#pauseInternal(false);
206
+ } else {
207
+ void this.#resumeInternal(false);
208
+ }
209
+ };
210
+
211
+ handleVisibilityChange();
212
+ document.addEventListener("visibilitychange", handleVisibilityChange);
213
+
214
+ let disposed = false;
215
+
216
+ this.#disposeVisibilityTracker = () => {
217
+ // Make sure the visibility tracker is only disposed once
218
+ // Do not resume if it's paused by another visibility tracker
219
+ if (disposed) {
220
+ return;
221
+ }
222
+ disposed = true;
223
+
224
+ document.removeEventListener(
225
+ "visibilitychange",
226
+ handleVisibilityChange,
227
+ );
228
+
229
+ // Do not resume if decoder is already disposed
230
+ if (!this.#disposed) {
231
+ void this.#resumeInternal(false);
232
+ }
233
+ };
234
+ return this.#disposeVisibilityTracker;
235
+ }
236
+
237
+ dispose() {
238
+ if (this.#disposed) {
239
+ return;
240
+ }
241
+
242
+ this.#disposed = true;
243
+ this.#controller.terminate();
244
+
245
+ this.#pendingConfiguration = undefined;
246
+ this.#pendingFrames.length = 0;
247
+
248
+ this.#disposeVisibilityTracker?.();
249
+ }
250
+ }
251
+
252
+ export namespace ScrcpyVideoDecoderPauseController {
253
+ export type Input = ScrcpyMediaStreamPacket;
254
+ export type Output =
255
+ | ScrcpyMediaStreamConfigurationPacket
256
+ | (ScrcpyMediaStreamDataPacket & { skipRendering: boolean });
257
+ }
@@ -0,0 +1,112 @@
1
+ export interface ScrcpyVideoRendererPerformanceCounterInterface {
2
+ /**
3
+ * Gets the number of frames that have been drawn on the renderer.
4
+ */
5
+ readonly framesRendered: number;
6
+ /**
7
+ * Gets the number of frames that's visible to the user.
8
+ *
9
+ * Multiple frames might be rendered during one vertical sync interval,
10
+ * but only the last of them is represented to the user.
11
+ * This costs some performance but reduces latency by 1 frame.
12
+ *
13
+ * Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
14
+ * https://issues.chromium.org/issues/41483010
15
+ */
16
+ readonly framesDisplayed: number;
17
+ /**
18
+ * Gets the number of frames that wasn't drawn on the renderer
19
+ * because the renderer can't keep up
20
+ */
21
+ readonly framesSkippedRendering: number;
22
+ }
23
+
24
+ export class ScrcpyVideoRendererPerformanceCounter implements ScrcpyVideoRendererPerformanceCounterInterface {
25
+ #framesRendered = 0;
26
+ /**
27
+ * Gets the number of frames that have been drawn on the renderer.
28
+ */
29
+ get framesRendered() {
30
+ return this.#framesRendered;
31
+ }
32
+
33
+ /**
34
+ * `true` if the next vertical sync will display a different frame
35
+ */
36
+ #hasNewFrame = false;
37
+
38
+ #framesDisplayed = 0;
39
+ /**
40
+ * Gets the number of frames that's visible to the user.
41
+ *
42
+ * Multiple frames might be rendered during one vertical sync interval,
43
+ * but only the last of them is represented to the user.
44
+ * This costs some performance but reduces latency by 1 frame.
45
+ *
46
+ * Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
47
+ * https://issues.chromium.org/issues/41483010
48
+ */
49
+ get framesDisplayed() {
50
+ return this.#framesDisplayed;
51
+ }
52
+
53
+ #framesSkippedRendering = 0;
54
+ /**
55
+ * Gets the number of frames that wasn't drawn on the renderer
56
+ * because the renderer can't keep up
57
+ */
58
+ get framesSkippedRendering() {
59
+ return this.#framesSkippedRendering;
60
+ }
61
+
62
+ #animationFrameId: number | undefined;
63
+
64
+ constructor() {
65
+ // `requestAnimationFrame` is available in Web Worker
66
+ // https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/requestAnimationFrame
67
+ try {
68
+ this.#animationFrameId = requestAnimationFrame(
69
+ this.#handleAnimationFrame,
70
+ );
71
+ } catch {
72
+ // Chrome has a bug that `requestAnimationFrame` doesn't work in nested Workers
73
+ // https://issues.chromium.org/issues/41483010
74
+ // Because we need actual vertical sync to count presented frames,
75
+ // `setTimeout` with a fixed delay also doesn't work.
76
+ // In this case just leave `framesDisplayed` at `0`
77
+ }
78
+ }
79
+
80
+ #handleAnimationFrame = () => {
81
+ // Animation frame handler is called on every vertical sync interval.
82
+ // Only then a frame is visible to the user.
83
+ if (this.#hasNewFrame) {
84
+ this.#framesDisplayed += 1;
85
+ this.#hasNewFrame = false;
86
+ }
87
+
88
+ this.#animationFrameId = requestAnimationFrame(
89
+ this.#handleAnimationFrame,
90
+ );
91
+ };
92
+
93
+ increaseFramesRendered() {
94
+ this.#framesRendered += 1;
95
+ this.#hasNewFrame = true;
96
+ }
97
+
98
+ increaseFramesSkippedRendering() {
99
+ this.#framesSkippedRendering += 1;
100
+ }
101
+
102
+ addFramesSkippedRendering(count: number) {
103
+ this.#framesSkippedRendering += count;
104
+ }
105
+
106
+ dispose() {
107
+ // `0` is a valid value for RAF ID
108
+ if (this.#animationFrameId !== undefined) {
109
+ cancelAnimationFrame(this.#animationFrameId);
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "lib": [
5
+ "ESNext",
6
+ "DOM"
7
+ ]
8
+ }
9
+ }