@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 +21 -0
- package/README.md +52 -0
- package/esm/decoder-counter.d.ts +34 -0
- package/esm/decoder-counter.d.ts.map +1 -0
- package/esm/decoder-counter.js +36 -0
- package/esm/decoder-counter.js.map +1 -0
- package/esm/decoder.d.ts +28 -0
- package/esm/decoder.d.ts.map +1 -0
- package/esm/decoder.js +2 -0
- package/esm/decoder.js.map +1 -0
- package/esm/gl.d.ts +5 -0
- package/esm/gl.d.ts.map +1 -0
- package/esm/gl.js +58 -0
- package/esm/gl.js.map +1 -0
- package/esm/index.d.ts +6 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +6 -0
- package/esm/index.js.map +1 -0
- package/esm/pause-controller.d.ts +42 -0
- package/esm/pause-controller.d.ts.map +1 -0
- package/esm/pause-controller.js +172 -0
- package/esm/pause-controller.js.map +1 -0
- package/esm/renderer-counter.d.ts +51 -0
- package/esm/renderer-counter.d.ts.map +1 -0
- package/esm/renderer-counter.js +76 -0
- package/esm/renderer-counter.js.map +1 -0
- package/package.json +46 -0
- package/src/decoder-counter.ts +56 -0
- package/src/decoder.ts +48 -0
- package/src/gl.ts +78 -0
- package/src/index.ts +5 -0
- package/src/pause-controller.ts +257 -0
- package/src/renderer-counter.ts +112 -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-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"}
|
package/esm/decoder.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
package/esm/gl.d.ts.map
ADDED
|
@@ -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 @@
|
|
|
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
package/esm/index.js.map
ADDED
|
@@ -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,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
|
+
}
|