@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 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 Tue, 18 Oct 2022 09:32:30 GMT and should not be manually modified.
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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020-2022 Simon Chan
3
+ Copyright (c) 2020-2023 Simon Chan
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @yume-chan/scrcpy-decoder-webcodecs
2
2
 
3
- Decode and render video stream using the [WebCodecs API](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API).
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 are only available on recent versions of Chrome.
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
- - [Compatibility](#compatibility)
10
- - [Usage](#usage)
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.element); // It draws frames onto `decoder.element`
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 { ScrcpyVideoStreamPacket } from '@yume-chan/scrcpy';
2
- import { WritableStream } from '@yume-chan/stream-extra';
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 lastFrame;
15
- private animationFrame;
16
+ private currentFrameRendered;
17
+ private animationFrameId;
16
18
  constructor();
17
- private render;
19
+ private onFramePresented;
18
20
  private configure;
19
21
  dispose(): void;
20
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACpF,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,4CAA6B;IAEhD,OAAO,CAAC,SAAS,CAAoB;IACrC,IAAW,QAAQ,sBAA6B;IAEhD,OAAO,CAAC,cAAc,CAAK;IAC3B,IAAW,aAAa,WAAkC;IAE1D,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,OAAO,CAAe;IAG9B,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,cAAc,CAAa;;IAwCnC,OAAO,CAAC,MAAM,CASZ;IAEF,OAAO,CAAC,SAAS;IAeV,OAAO;CAIjB"}
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 '@yume-chan/stream-extra';
1
+ import { WritableStream } from "@yume-chan/stream-extra";
2
2
  function toHex(value) {
3
- return value.toString(16).padStart(2, '0').toUpperCase();
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() { return this._writable; }
11
+ get writable() {
12
+ return this._writable;
13
+ }
12
14
  _renderer;
13
- get renderer() { return this._renderer; }
15
+ get renderer() {
16
+ return this._renderer;
17
+ }
14
18
  _frameRendered = 0;
15
- get frameRendered() { return this._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
- // Limit FPS to system refresh rate
19
- lastFrame;
20
- animationFrame = 0;
28
+ currentFrameRendered = false;
29
+ animationFrameId = 0;
21
30
  constructor() {
22
- this._renderer = document.createElement('canvas');
23
- this.context = this._renderer.getContext('2d');
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.lastFrame) {
27
- this.lastFrame.close();
35
+ if (this.currentFrameRendered) {
36
+ this._frameSkipped += 1;
28
37
  }
29
- this.lastFrame = frame;
30
- if (!this.animationFrame) {
31
- // Start render loop on first frame
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: async (packet) => {
56
+ write: (packet) => {
39
57
  switch (packet.type) {
40
- case 'configuration':
58
+ case "configuration":
41
59
  this.configure(packet.data);
42
60
  break;
43
- case 'frame':
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 ? 'delta' : 'key',
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
- render = () => {
56
- if (this.lastFrame) {
57
- this._frameRendered += 1;
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].map(toHex).join('')}`;
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.animationFrame);
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":"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"}
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.17",
4
- "description": "TypeScript implementation of Scrcpy.",
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-decoder-webcodecs"
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.3",
30
- "@yume-chan/stream-extra": "^0.0.17",
31
- "@yume-chan/scrcpy": "^0.0.17",
32
- "tslib": "^2.4.0"
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": "^28.1.2",
36
- "@yume-chan/ts-package-builder": "^1.0.0",
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
- "jest": "^28.1.2",
39
- "ts-jest": "^28.0.5",
40
- "typescript": "^4.7.4"
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 type { H264Configuration, ScrcpyVideoStreamPacket } from '@yume-chan/scrcpy';
2
- import { WritableStream } from '@yume-chan/stream-extra';
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, '0').toUpperCase();
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() { return this._writable; }
18
+ public get writable() {
19
+ return this._writable;
20
+ }
16
21
 
17
22
  private _renderer: HTMLCanvasElement;
18
- public get renderer() { return this._renderer; }
23
+ public get renderer() {
24
+ return this._renderer;
25
+ }
19
26
 
20
27
  private _frameRendered = 0;
21
- public get frameRendered() { return this._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
- // Limit FPS to system refresh rate
27
- private lastFrame: VideoFrame | undefined;
28
- private animationFrame: number = 0;
40
+ private currentFrameRendered = false;
41
+ private animationFrameId = 0;
29
42
 
30
43
  public constructor() {
31
- this._renderer = document.createElement('canvas');
44
+ this._renderer = document.createElement("canvas");
32
45
 
33
- this.context = this._renderer.getContext('2d')!;
46
+ this.context = this._renderer.getContext("2d")!;
34
47
  this.decoder = new VideoDecoder({
35
48
  output: (frame) => {
36
- if (this.lastFrame) {
37
- this.lastFrame.close();
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
- if (!this.animationFrame) {
42
- // Start render loop on first frame
43
- this.render();
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: async (packet) => {
71
+ write: (packet) => {
51
72
  switch (packet.type) {
52
- case 'configuration':
73
+ case "configuration":
53
74
  this.configure(packet.data);
54
75
  break;
55
- case 'frame':
56
- this.decoder.decode(new EncodedVideoChunk({
57
- // Treat `undefined` as `key`, otherwise won't decode.
58
- type: packet.keyframe === false ? 'delta' : 'key',
59
- timestamp: 0,
60
- data: packet.data,
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
- private render = () => {
69
- if (this.lastFrame) {
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
- this.animationFrame = requestAnimationFrame(this.render);
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].map(toHex).join('')}`;
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.animationFrame);
117
+ cancelAnimationFrame(this.animationFrameId);
96
118
  this.decoder.close();
97
119
  }
98
120
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "./node_modules/@yume-chan/ts-package-builder/tsconfig.base.json",
2
+ "extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "lib": [
5
5
  "ESNext",