@yume-chan/scrcpy-decoder-webcodecs 0.0.17 → 0.0.18
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/CHANGELOG.json +12 -0
- package/CHANGELOG.md +8 -1
- package/LICENSE +1 -1
- package/README.md +8 -6
- package/esm/index.d.ts +7 -5
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +48 -32
- package/esm/index.js.map +1 -1
- package/package.json +17 -13
- package/src/index.ts +62 -40
- package/tsconfig.build.json +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/.rush/temp/package-deps_build.json +0 -14
- package/.rush/temp/package-deps_build_watch.json +0 -14
- package/.rush/temp/shrinkwrap-deps.json +0 -288
- package/esm/tinyh264/index.d.ts +0 -18
- package/esm/tinyh264/index.d.ts.map +0 -1
- package/esm/tinyh264/index.js +0 -86
- package/esm/tinyh264/index.js.map +0 -1
- package/esm/tinyh264/worker.d.ts +0 -2
- package/esm/tinyh264/worker.d.ts.map +0 -1
- package/esm/tinyh264/worker.js +0 -3
- package/esm/tinyh264/worker.js.map +0 -1
- package/esm/tinyh264/wrapper.d.ts +0 -21
- package/esm/tinyh264/wrapper.d.ts.map +0 -1
- package/esm/tinyh264/wrapper.js +0 -74
- package/esm/tinyh264/wrapper.js.map +0 -1
- package/jest.config.js +0 -13
- package/scrcpy-decoder-tinyh264.build.log +0 -1
- package/scrcpy-decoder-webcodecs.build.log +0 -1
- package/tsconfig.json +0 -16
- package/tsconfig.test.json +0 -8
package/CHANGELOG.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yume-chan/scrcpy-decoder-webcodecs",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.0.18",
|
|
6
|
+
"tag": "@yume-chan/scrcpy-decoder-webcodecs_v0.0.18",
|
|
7
|
+
"date": "Wed, 25 Jan 2023 21:33:49 GMT",
|
|
8
|
+
"comments": {
|
|
9
|
+
"none": [
|
|
10
|
+
{
|
|
11
|
+
"comment": "Change to not use vertical sync to minimize latency"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.0.17",
|
|
6
18
|
"tag": "@yume-chan/scrcpy-decoder-webcodecs_v0.0.17",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Change Log - @yume-chan/scrcpy-decoder-webcodecs
|
|
2
2
|
|
|
3
|
-
This log was last generated on
|
|
3
|
+
This log was last generated on Wed, 25 Jan 2023 21:33:49 GMT and should not be manually modified.
|
|
4
|
+
|
|
5
|
+
## 0.0.18
|
|
6
|
+
Wed, 25 Jan 2023 21:33:49 GMT
|
|
7
|
+
|
|
8
|
+
### Updates
|
|
9
|
+
|
|
10
|
+
- Change to not use vertical sync to minimize latency
|
|
4
11
|
|
|
5
12
|
## 0.0.17
|
|
6
13
|
Tue, 18 Oct 2022 09:32:30 GMT
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @yume-chan/scrcpy-decoder-webcodecs
|
|
2
2
|
|
|
3
|
-
Decode and render
|
|
3
|
+
Decode and render H.264 streams using the [WebCodecs API](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API).
|
|
4
4
|
|
|
5
|
-
It has no dependencies and high performance, but
|
|
5
|
+
It has no dependencies and high performance, but is only available on recent versions of Chrome.
|
|
6
6
|
|
|
7
7
|
**WARNING:** The public API is UNSTABLE. If you have any questions, please open an issue.
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
9
|
+
- [Compatibility](#compatibility)
|
|
10
|
+
- [Usage](#usage)
|
|
11
11
|
|
|
12
12
|
## Compatibility
|
|
13
13
|
|
|
@@ -17,11 +17,13 @@ It has no dependencies and high performance, but are only available on recent ve
|
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
19
|
|
|
20
|
+
It draws frames onto `decoder.renderer` (a `<canvas>` element), you can insert it anywhere you want to display the video.
|
|
21
|
+
|
|
20
22
|
```ts
|
|
21
23
|
const decoder = new WebCodecsDecoder();
|
|
22
|
-
document.body.appendChild(decoder.
|
|
24
|
+
document.body.appendChild(decoder.renderer);
|
|
23
25
|
|
|
24
26
|
videoPacketStream // from `@yume-chan/scrcpy`
|
|
25
27
|
.pipeTo(decoder.writable)
|
|
26
|
-
.catch(() => {
|
|
28
|
+
.catch(() => {});
|
|
27
29
|
```
|
package/esm/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { WritableStream } from
|
|
1
|
+
import { type ScrcpyVideoStreamPacket } from "@yume-chan/scrcpy";
|
|
2
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
3
3
|
export declare class WebCodecsDecoder {
|
|
4
4
|
readonly maxProfile: undefined;
|
|
5
5
|
readonly maxLevel: undefined;
|
|
@@ -9,12 +9,14 @@ export declare class WebCodecsDecoder {
|
|
|
9
9
|
get renderer(): HTMLCanvasElement;
|
|
10
10
|
private _frameRendered;
|
|
11
11
|
get frameRendered(): number;
|
|
12
|
+
private _frameSkipped;
|
|
13
|
+
get frameSkipped(): number;
|
|
12
14
|
private context;
|
|
13
15
|
private decoder;
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
+
private currentFrameRendered;
|
|
17
|
+
private animationFrameId;
|
|
16
18
|
constructor();
|
|
17
|
-
private
|
|
19
|
+
private onFramePresented;
|
|
18
20
|
private configure;
|
|
19
21
|
dispose(): void;
|
|
20
22
|
}
|
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,KAAK,uBAAuB,EAC/B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAMzD,qBAAa,gBAAgB;IAGzB,SAAgB,UAAU,YAAa;IACvC,SAAgB,QAAQ,YAAa;IAErC,OAAO,CAAC,SAAS,CAA0C;IAC3D,IAAW,QAAQ,4CAElB;IAED,OAAO,CAAC,SAAS,CAAoB;IACrC,IAAW,QAAQ,sBAElB;IAED,OAAO,CAAC,cAAc,CAAK;IAC3B,IAAW,aAAa,WAEvB;IAED,OAAO,CAAC,aAAa,CAAK;IAC1B,IAAW,YAAY,WAEtB;IAED,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,OAAO,CAAe;IAE9B,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,gBAAgB,CAAK;;IAqD7B,OAAO,CAAC,gBAAgB,CAGtB;IAEF,OAAO,CAAC,SAAS;IAiBV,OAAO;CAIjB"}
|
package/esm/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { WritableStream } from
|
|
1
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
2
2
|
function toHex(value) {
|
|
3
|
-
return value.toString(16).padStart(2,
|
|
3
|
+
return value.toString(16).padStart(2, "0").toUpperCase();
|
|
4
4
|
}
|
|
5
5
|
export class WebCodecsDecoder {
|
|
6
6
|
// Usually, browsers can decode most configurations,
|
|
@@ -8,58 +8,72 @@ export class WebCodecsDecoder {
|
|
|
8
8
|
maxProfile = undefined;
|
|
9
9
|
maxLevel = undefined;
|
|
10
10
|
_writable;
|
|
11
|
-
get writable() {
|
|
11
|
+
get writable() {
|
|
12
|
+
return this._writable;
|
|
13
|
+
}
|
|
12
14
|
_renderer;
|
|
13
|
-
get renderer() {
|
|
15
|
+
get renderer() {
|
|
16
|
+
return this._renderer;
|
|
17
|
+
}
|
|
14
18
|
_frameRendered = 0;
|
|
15
|
-
get frameRendered() {
|
|
19
|
+
get frameRendered() {
|
|
20
|
+
return this._frameRendered;
|
|
21
|
+
}
|
|
22
|
+
_frameSkipped = 0;
|
|
23
|
+
get frameSkipped() {
|
|
24
|
+
return this._frameSkipped;
|
|
25
|
+
}
|
|
16
26
|
context;
|
|
17
27
|
decoder;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
animationFrame = 0;
|
|
28
|
+
currentFrameRendered = false;
|
|
29
|
+
animationFrameId = 0;
|
|
21
30
|
constructor() {
|
|
22
|
-
this._renderer = document.createElement(
|
|
23
|
-
this.context = this._renderer.getContext(
|
|
31
|
+
this._renderer = document.createElement("canvas");
|
|
32
|
+
this.context = this._renderer.getContext("2d");
|
|
24
33
|
this.decoder = new VideoDecoder({
|
|
25
34
|
output: (frame) => {
|
|
26
|
-
if (this.
|
|
27
|
-
this.
|
|
35
|
+
if (this.currentFrameRendered) {
|
|
36
|
+
this._frameSkipped += 1;
|
|
28
37
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.render();
|
|
38
|
+
else {
|
|
39
|
+
this.currentFrameRendered = true;
|
|
40
|
+
this._frameRendered += 1;
|
|
33
41
|
}
|
|
42
|
+
// PERF: H.264 renderer may draw multiple frames in one vertical sync interval to minimize latency.
|
|
43
|
+
// When multiple frames are drawn in one vertical sync interval,
|
|
44
|
+
// only the last one is visible to users.
|
|
45
|
+
// But this ensures users can always see the most up-to-date screen.
|
|
46
|
+
// This is also the behavior of official Scrcpy client.
|
|
47
|
+
// https://github.com/Genymobile/scrcpy/issues/3679
|
|
48
|
+
this.context.drawImage(frame, 0, 0);
|
|
49
|
+
frame.close();
|
|
50
|
+
},
|
|
51
|
+
error(e) {
|
|
52
|
+
void e;
|
|
34
53
|
},
|
|
35
|
-
error() { },
|
|
36
54
|
});
|
|
37
55
|
this._writable = new WritableStream({
|
|
38
|
-
write:
|
|
56
|
+
write: (packet) => {
|
|
39
57
|
switch (packet.type) {
|
|
40
|
-
case
|
|
58
|
+
case "configuration":
|
|
41
59
|
this.configure(packet.data);
|
|
42
60
|
break;
|
|
43
|
-
case
|
|
61
|
+
case "frame":
|
|
44
62
|
this.decoder.decode(new EncodedVideoChunk({
|
|
45
63
|
// Treat `undefined` as `key`, otherwise won't decode.
|
|
46
|
-
type: packet.keyframe === false ?
|
|
64
|
+
type: packet.keyframe === false ? "delta" : "key",
|
|
47
65
|
timestamp: 0,
|
|
48
66
|
data: packet.data,
|
|
49
67
|
}));
|
|
50
68
|
break;
|
|
51
69
|
}
|
|
52
|
-
}
|
|
70
|
+
},
|
|
53
71
|
});
|
|
72
|
+
this.onFramePresented();
|
|
54
73
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.context.drawImage(this.lastFrame, 0, 0);
|
|
59
|
-
this.lastFrame.close();
|
|
60
|
-
this.lastFrame = undefined;
|
|
61
|
-
}
|
|
62
|
-
this.animationFrame = requestAnimationFrame(this.render);
|
|
74
|
+
onFramePresented = () => {
|
|
75
|
+
this.currentFrameRendered = false;
|
|
76
|
+
this.animationFrameId = requestAnimationFrame(this.onFramePresented);
|
|
63
77
|
};
|
|
64
78
|
configure(config) {
|
|
65
79
|
const { profileIndex, constraintSet, levelIndex } = config;
|
|
@@ -67,14 +81,16 @@ export class WebCodecsDecoder {
|
|
|
67
81
|
this._renderer.height = config.croppedHeight;
|
|
68
82
|
// https://www.rfc-editor.org/rfc/rfc6381#section-3.3
|
|
69
83
|
// ISO Base Media File Format Name Space
|
|
70
|
-
const codec = `avc1.${[profileIndex, constraintSet, levelIndex]
|
|
84
|
+
const codec = `avc1.${[profileIndex, constraintSet, levelIndex]
|
|
85
|
+
.map(toHex)
|
|
86
|
+
.join("")}`;
|
|
71
87
|
this.decoder.configure({
|
|
72
88
|
codec: codec,
|
|
73
89
|
optimizeForLatency: true,
|
|
74
90
|
});
|
|
75
91
|
}
|
|
76
92
|
dispose() {
|
|
77
|
-
cancelAnimationFrame(this.
|
|
93
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
78
94
|
this.decoder.close();
|
|
79
95
|
}
|
|
80
96
|
}
|
package/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yume-chan/scrcpy-decoder-webcodecs",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.18",
|
|
4
|
+
"description": "Raw H.264 stream decoder and renderer using WebCodecs API (requires modern browser).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adb",
|
|
7
7
|
"android-phone",
|
|
8
|
-
"scrcpy
|
|
8
|
+
"scrcpy",
|
|
9
|
+
"scrcpy-decoder"
|
|
9
10
|
],
|
|
10
11
|
"license": "MIT",
|
|
11
12
|
"author": {
|
|
@@ -26,22 +27,25 @@
|
|
|
26
27
|
"main": "esm/index.js",
|
|
27
28
|
"types": "esm/index.d.ts",
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@types/dom-webcodecs": "^0.1.
|
|
30
|
-
"@yume-chan/
|
|
31
|
-
"@yume-chan/
|
|
32
|
-
"tslib": "^2.4.
|
|
30
|
+
"@types/dom-webcodecs": "^0.1.5",
|
|
31
|
+
"@yume-chan/scrcpy": "^0.0.18",
|
|
32
|
+
"@yume-chan/stream-extra": "^0.0.18",
|
|
33
|
+
"tslib": "^2.4.1"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@jest/globals": "^
|
|
36
|
-
"@yume-chan/
|
|
36
|
+
"@jest/globals": "^29.3.1",
|
|
37
|
+
"@yume-chan/eslint-config": "^1.0.0",
|
|
38
|
+
"@yume-chan/tsconfig": "^1.0.0",
|
|
37
39
|
"cross-env": "^7.0.3",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
40
|
+
"eslint": "^8.31.0",
|
|
41
|
+
"jest": "^29.3.1",
|
|
42
|
+
"ts-jest": "^29.0.4",
|
|
43
|
+
"typescript": "^4.9.4"
|
|
41
44
|
},
|
|
42
45
|
"scripts": {
|
|
43
46
|
"build": "tsc -b tsconfig.build.json",
|
|
44
47
|
"build:watch": "tsc -b tsconfig.build.json",
|
|
45
|
-
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage"
|
|
48
|
+
"//test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
49
|
+
"lint": "eslint src/**/*.ts --fix"
|
|
46
50
|
}
|
|
47
51
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type H264Configuration,
|
|
3
|
+
type ScrcpyVideoStreamPacket,
|
|
4
|
+
} from "@yume-chan/scrcpy";
|
|
5
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
3
6
|
|
|
4
7
|
function toHex(value: number) {
|
|
5
|
-
return value.toString(16).padStart(2,
|
|
8
|
+
return value.toString(16).padStart(2, "0").toUpperCase();
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export class WebCodecsDecoder {
|
|
@@ -12,68 +15,85 @@ export class WebCodecsDecoder {
|
|
|
12
15
|
public readonly maxLevel = undefined;
|
|
13
16
|
|
|
14
17
|
private _writable: WritableStream<ScrcpyVideoStreamPacket>;
|
|
15
|
-
public get writable() {
|
|
18
|
+
public get writable() {
|
|
19
|
+
return this._writable;
|
|
20
|
+
}
|
|
16
21
|
|
|
17
22
|
private _renderer: HTMLCanvasElement;
|
|
18
|
-
public get renderer() {
|
|
23
|
+
public get renderer() {
|
|
24
|
+
return this._renderer;
|
|
25
|
+
}
|
|
19
26
|
|
|
20
27
|
private _frameRendered = 0;
|
|
21
|
-
public get frameRendered() {
|
|
28
|
+
public get frameRendered() {
|
|
29
|
+
return this._frameRendered;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private _frameSkipped = 0;
|
|
33
|
+
public get frameSkipped() {
|
|
34
|
+
return this._frameSkipped;
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
private context: CanvasRenderingContext2D;
|
|
24
38
|
private decoder: VideoDecoder;
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
private animationFrame: number = 0;
|
|
40
|
+
private currentFrameRendered = false;
|
|
41
|
+
private animationFrameId = 0;
|
|
29
42
|
|
|
30
43
|
public constructor() {
|
|
31
|
-
this._renderer = document.createElement(
|
|
44
|
+
this._renderer = document.createElement("canvas");
|
|
32
45
|
|
|
33
|
-
this.context = this._renderer.getContext(
|
|
46
|
+
this.context = this._renderer.getContext("2d")!;
|
|
34
47
|
this.decoder = new VideoDecoder({
|
|
35
48
|
output: (frame) => {
|
|
36
|
-
if (this.
|
|
37
|
-
this.
|
|
49
|
+
if (this.currentFrameRendered) {
|
|
50
|
+
this._frameSkipped += 1;
|
|
51
|
+
} else {
|
|
52
|
+
this.currentFrameRendered = true;
|
|
53
|
+
this._frameRendered += 1;
|
|
38
54
|
}
|
|
39
|
-
this.lastFrame = frame;
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
// PERF: H.264 renderer may draw multiple frames in one vertical sync interval to minimize latency.
|
|
57
|
+
// When multiple frames are drawn in one vertical sync interval,
|
|
58
|
+
// only the last one is visible to users.
|
|
59
|
+
// But this ensures users can always see the most up-to-date screen.
|
|
60
|
+
// This is also the behavior of official Scrcpy client.
|
|
61
|
+
// https://github.com/Genymobile/scrcpy/issues/3679
|
|
62
|
+
this.context.drawImage(frame, 0, 0);
|
|
63
|
+
frame.close();
|
|
64
|
+
},
|
|
65
|
+
error(e) {
|
|
66
|
+
void e;
|
|
45
67
|
},
|
|
46
|
-
error() { },
|
|
47
68
|
});
|
|
48
69
|
|
|
49
70
|
this._writable = new WritableStream<ScrcpyVideoStreamPacket>({
|
|
50
|
-
write:
|
|
71
|
+
write: (packet) => {
|
|
51
72
|
switch (packet.type) {
|
|
52
|
-
case
|
|
73
|
+
case "configuration":
|
|
53
74
|
this.configure(packet.data);
|
|
54
75
|
break;
|
|
55
|
-
case
|
|
56
|
-
this.decoder.decode(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
case "frame":
|
|
77
|
+
this.decoder.decode(
|
|
78
|
+
new EncodedVideoChunk({
|
|
79
|
+
// Treat `undefined` as `key`, otherwise won't decode.
|
|
80
|
+
type:
|
|
81
|
+
packet.keyframe === false ? "delta" : "key",
|
|
82
|
+
timestamp: 0,
|
|
83
|
+
data: packet.data,
|
|
84
|
+
})
|
|
85
|
+
);
|
|
62
86
|
break;
|
|
63
87
|
}
|
|
64
|
-
}
|
|
88
|
+
},
|
|
65
89
|
});
|
|
66
|
-
}
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this._frameRendered += 1;
|
|
71
|
-
this.context.drawImage(this.lastFrame, 0, 0);
|
|
72
|
-
this.lastFrame.close();
|
|
73
|
-
this.lastFrame = undefined;
|
|
74
|
-
}
|
|
91
|
+
this.onFramePresented();
|
|
92
|
+
}
|
|
75
93
|
|
|
76
|
-
|
|
94
|
+
private onFramePresented = () => {
|
|
95
|
+
this.currentFrameRendered = false;
|
|
96
|
+
this.animationFrameId = requestAnimationFrame(this.onFramePresented);
|
|
77
97
|
};
|
|
78
98
|
|
|
79
99
|
private configure(config: H264Configuration) {
|
|
@@ -84,7 +104,9 @@ export class WebCodecsDecoder {
|
|
|
84
104
|
|
|
85
105
|
// https://www.rfc-editor.org/rfc/rfc6381#section-3.3
|
|
86
106
|
// ISO Base Media File Format Name Space
|
|
87
|
-
const codec = `avc1.${[profileIndex, constraintSet, levelIndex]
|
|
107
|
+
const codec = `avc1.${[profileIndex, constraintSet, levelIndex]
|
|
108
|
+
.map(toHex)
|
|
109
|
+
.join("")}`;
|
|
88
110
|
this.decoder.configure({
|
|
89
111
|
codec: codec,
|
|
90
112
|
optimizeForLatency: true,
|
|
@@ -92,7 +114,7 @@ export class WebCodecsDecoder {
|
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
public dispose() {
|
|
95
|
-
cancelAnimationFrame(this.
|
|
117
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
96
118
|
this.decoder.close();
|
|
97
119
|
}
|
|
98
120
|
}
|
package/tsconfig.build.json
CHANGED