@yume-chan/scrcpy 0.0.13 → 0.0.16
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 +45 -0
- package/CHANGELOG.md +25 -1
- package/LICENSE +21 -21
- package/README.md +90 -89
- package/bin/scrcpy-server +0 -0
- package/bin/version.d.ts +1 -1
- package/bin/version.js +1 -1
- package/esm/client.d.ts +14 -2
- package/esm/client.d.ts.map +1 -1
- package/esm/client.js +275 -143
- package/esm/client.js.map +1 -1
- package/esm/connection.d.ts.map +1 -1
- package/esm/connection.js +74 -86
- package/esm/connection.js.map +1 -1
- package/esm/decoder/tinyh264/index.js +48 -48
- package/esm/decoder/tinyh264/index.js.map +1 -1
- package/esm/decoder/tinyh264/wrapper.js +3 -2
- package/esm/decoder/tinyh264/wrapper.js.map +1 -1
- package/esm/decoder/web-codecs/index.js +26 -21
- package/esm/decoder/web-codecs/index.js.map +1 -1
- package/esm/message.d.ts +13 -8
- package/esm/message.d.ts.map +1 -1
- package/esm/message.js +14 -8
- package/esm/message.js.map +1 -1
- package/esm/options/1_16/index.d.ts +8 -4
- package/esm/options/1_16/index.d.ts.map +1 -1
- package/esm/options/1_16/index.js +44 -18
- package/esm/options/1_16/index.js.map +1 -1
- package/esm/options/1_16/sps.js +3 -2
- package/esm/options/1_16/sps.js.map +1 -1
- package/esm/options/1_18.d.ts +3 -2
- package/esm/options/1_18.d.ts.map +1 -1
- package/esm/options/1_18.js +24 -1
- package/esm/options/1_18.js.map +1 -1
- package/esm/options/1_21.js +4 -1
- package/esm/options/1_21.js.map +1 -1
- package/esm/options/1_22.d.ts +3 -3
- package/esm/options/1_22.d.ts.map +1 -1
- package/esm/options/1_22.js +9 -6
- package/esm/options/1_22.js.map +1 -1
- package/esm/options/1_23.js +4 -1
- package/esm/options/1_23.js.map +1 -1
- package/esm/options/1_24.d.ts +9 -0
- package/esm/options/1_24.d.ts.map +1 -0
- package/esm/options/1_24.js +13 -0
- package/esm/options/1_24.js.map +1 -0
- package/esm/options/common.d.ts +2 -0
- package/esm/options/common.d.ts.map +1 -1
- package/esm/options/common.js.map +1 -1
- package/esm/options/index.d.ts +1 -0
- package/esm/options/index.d.ts.map +1 -1
- package/esm/options/index.js +1 -0
- package/esm/options/index.js.map +1 -1
- package/esm/push-server.js +5 -10
- package/esm/push-server.js.map +1 -1
- package/package.json +8 -8
- package/scrcpy.LICENSE +203 -203
- package/scripts/fetch-server.cjs +28 -28
- package/src/client.ts +435 -275
- package/src/codec.ts +35 -35
- package/src/connection.ts +148 -145
- package/src/decoder/index.ts +3 -3
- package/src/decoder/tinyh264/index.ts +112 -112
- package/src/decoder/tinyh264/types.d.ts +137 -137
- package/src/decoder/tinyh264/worker.ts +2 -2
- package/src/decoder/tinyh264/wrapper.ts +89 -89
- package/src/decoder/types.ts +35 -35
- package/src/decoder/web-codecs/index.ts +99 -99
- package/src/index.ts +8 -8
- package/src/message.ts +113 -105
- package/src/options/1_16/index.ts +345 -315
- package/src/options/1_16/sps.ts +300 -300
- package/src/options/1_18.ts +61 -41
- package/src/options/1_21.ts +34 -34
- package/src/options/1_22.ts +78 -80
- package/src/options/1_24.ts +18 -0
- package/src/options/common.ts +66 -63
- package/src/options/index.ts +7 -6
- package/src/push-server.ts +24 -24
- package/src/utils.ts +5 -5
package/src/codec.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
// From https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel
|
|
2
|
-
export enum AndroidCodecProfile {
|
|
3
|
-
Baseline = 0x01,
|
|
4
|
-
Main = 0x02,
|
|
5
|
-
Extended = 0x04,
|
|
6
|
-
High = 0x08,
|
|
7
|
-
High10 = 0x10,
|
|
8
|
-
High422 = 0x20,
|
|
9
|
-
High444 = 0x40,
|
|
10
|
-
ConstrainedBaseline = 0x10000,
|
|
11
|
-
ConstrainedHigh = 0x80000,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export enum AndroidCodecLevel {
|
|
15
|
-
Level1 = 0x01,
|
|
16
|
-
Level1b = 0x02,
|
|
17
|
-
Level11 = 0x04,
|
|
18
|
-
Level12 = 0x08,
|
|
19
|
-
Level13 = 0x10,
|
|
20
|
-
Level2 = 0x20,
|
|
21
|
-
Level21 = 0x40,
|
|
22
|
-
Level22 = 0x80,
|
|
23
|
-
Level3 = 0x100,
|
|
24
|
-
Level31 = 0x200,
|
|
25
|
-
Level32 = 0x400,
|
|
26
|
-
Level4 = 0x800,
|
|
27
|
-
Level41 = 0x1000,
|
|
28
|
-
Level42 = 0x2000,
|
|
29
|
-
Level5 = 0x4000,
|
|
30
|
-
Level51 = 0x8000,
|
|
31
|
-
Level52 = 0x10000,
|
|
32
|
-
Level6 = 0x20000,
|
|
33
|
-
Level61 = 0x40000,
|
|
34
|
-
Level62 = 0x80000,
|
|
35
|
-
}
|
|
1
|
+
// From https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel
|
|
2
|
+
export enum AndroidCodecProfile {
|
|
3
|
+
Baseline = 0x01,
|
|
4
|
+
Main = 0x02,
|
|
5
|
+
Extended = 0x04,
|
|
6
|
+
High = 0x08,
|
|
7
|
+
High10 = 0x10,
|
|
8
|
+
High422 = 0x20,
|
|
9
|
+
High444 = 0x40,
|
|
10
|
+
ConstrainedBaseline = 0x10000,
|
|
11
|
+
ConstrainedHigh = 0x80000,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum AndroidCodecLevel {
|
|
15
|
+
Level1 = 0x01,
|
|
16
|
+
Level1b = 0x02,
|
|
17
|
+
Level11 = 0x04,
|
|
18
|
+
Level12 = 0x08,
|
|
19
|
+
Level13 = 0x10,
|
|
20
|
+
Level2 = 0x20,
|
|
21
|
+
Level21 = 0x40,
|
|
22
|
+
Level22 = 0x80,
|
|
23
|
+
Level3 = 0x100,
|
|
24
|
+
Level31 = 0x200,
|
|
25
|
+
Level32 = 0x400,
|
|
26
|
+
Level4 = 0x800,
|
|
27
|
+
Level41 = 0x1000,
|
|
28
|
+
Level42 = 0x2000,
|
|
29
|
+
Level5 = 0x4000,
|
|
30
|
+
Level51 = 0x8000,
|
|
31
|
+
Level52 = 0x10000,
|
|
32
|
+
Level6 = 0x20000,
|
|
33
|
+
Level61 = 0x40000,
|
|
34
|
+
Level62 = 0x80000,
|
|
35
|
+
}
|
package/src/connection.ts
CHANGED
|
@@ -1,145 +1,148 @@
|
|
|
1
|
-
import type { Adb, AdbSocket } from "@yume-chan/adb";
|
|
2
|
-
import type { Disposable } from "@yume-chan/event";
|
|
3
|
-
import type { ValueOrPromise } from "@yume-chan/struct";
|
|
4
|
-
import { delay } from "./utils.js";
|
|
5
|
-
|
|
6
|
-
export interface ScrcpyClientConnectionOptions {
|
|
7
|
-
control: boolean;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Write a byte on start to detect connection issues
|
|
11
|
-
*/
|
|
12
|
-
sendDummyByte: boolean;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Send device name and size
|
|
16
|
-
*/
|
|
17
|
-
sendDeviceMeta: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export abstract class ScrcpyClientConnection implements Disposable {
|
|
21
|
-
protected device: Adb;
|
|
22
|
-
|
|
23
|
-
protected options: ScrcpyClientConnectionOptions;
|
|
24
|
-
|
|
25
|
-
public constructor(device: Adb, options: ScrcpyClientConnectionOptions) {
|
|
26
|
-
this.device = device;
|
|
27
|
-
this.options = options;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public initialize(): ValueOrPromise<void> { }
|
|
31
|
-
|
|
32
|
-
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]>;
|
|
33
|
-
|
|
34
|
-
public dispose(): void { }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
|
38
|
-
private async connect(): Promise<AdbSocket> {
|
|
39
|
-
return await this.device.createSocket('localabstract:scrcpy');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private async connectAndRetry(): Promise<AdbSocket> {
|
|
43
|
-
for (let i = 0; i < 100; i++) {
|
|
44
|
-
try {
|
|
45
|
-
return await this.connect();
|
|
46
|
-
} catch (e) {
|
|
47
|
-
await delay(100);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
throw new Error(`Can't connect to server after 100 retries`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private async connectVideoStream(): Promise<AdbSocket> {
|
|
54
|
-
const stream = await this.connectAndRetry();
|
|
55
|
-
if (this.options.sendDummyByte) {
|
|
56
|
-
const reader = stream.readable.getReader();
|
|
57
|
-
const { done, value } = await reader.read();
|
|
58
|
-
// server will write a `0` to signal connection success
|
|
59
|
-
if (done || value.byteLength !== 1 || value[0] !== 0) {
|
|
60
|
-
throw new Error('Unexpected response from server');
|
|
61
|
-
}
|
|
62
|
-
reader.releaseLock();
|
|
63
|
-
}
|
|
64
|
-
return stream;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
public async getStreams(): Promise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]> {
|
|
68
|
-
const videoStream = await this.connectVideoStream();
|
|
69
|
-
|
|
70
|
-
let controlStream: AdbSocket | undefined;
|
|
71
|
-
if (this.options.control) {
|
|
72
|
-
controlStream = await this.connectAndRetry();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Server only writes device meta after control socket is connected (if enabled)
|
|
76
|
-
if (this.options.sendDeviceMeta) {
|
|
77
|
-
const reader = videoStream.readable.getReader();
|
|
78
|
-
const { done, value } = await reader.read();
|
|
79
|
-
// 64 bytes device name + 2 bytes video width + 2 bytes video height
|
|
80
|
-
if (done || value.byteLength !== 64 + 2 + 2) {
|
|
81
|
-
throw new Error('Unexpected response from server');
|
|
82
|
-
}
|
|
83
|
-
reader.releaseLock();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return [videoStream, controlStream];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
|
|
91
|
-
private streams!: ReadableStreamDefaultReader<AdbSocket>;
|
|
92
|
-
|
|
93
|
-
private address!: string;
|
|
94
|
-
|
|
95
|
-
public override async initialize(): Promise<void> {
|
|
96
|
-
try {
|
|
97
|
-
// try to unbind first
|
|
98
|
-
await this.device.reverse.remove('localabstract:scrcpy');
|
|
99
|
-
} catch {
|
|
100
|
-
// ignore error
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const queue = new TransformStream<AdbSocket>();
|
|
104
|
-
this.streams = queue.readable.getReader();
|
|
105
|
-
const writer = queue.writable.getWriter();
|
|
106
|
-
this.address = await this.device.reverse.add(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
import type { Adb, AdbSocket } from "@yume-chan/adb";
|
|
2
|
+
import type { Disposable } from "@yume-chan/event";
|
|
3
|
+
import type { ValueOrPromise } from "@yume-chan/struct";
|
|
4
|
+
import { delay } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
export interface ScrcpyClientConnectionOptions {
|
|
7
|
+
control: boolean;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Write a byte on start to detect connection issues
|
|
11
|
+
*/
|
|
12
|
+
sendDummyByte: boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Send device name and size
|
|
16
|
+
*/
|
|
17
|
+
sendDeviceMeta: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export abstract class ScrcpyClientConnection implements Disposable {
|
|
21
|
+
protected device: Adb;
|
|
22
|
+
|
|
23
|
+
protected options: ScrcpyClientConnectionOptions;
|
|
24
|
+
|
|
25
|
+
public constructor(device: Adb, options: ScrcpyClientConnectionOptions) {
|
|
26
|
+
this.device = device;
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public initialize(): ValueOrPromise<void> { }
|
|
31
|
+
|
|
32
|
+
public abstract getStreams(): ValueOrPromise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]>;
|
|
33
|
+
|
|
34
|
+
public dispose(): void { }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ScrcpyClientForwardConnection extends ScrcpyClientConnection {
|
|
38
|
+
private async connect(): Promise<AdbSocket> {
|
|
39
|
+
return await this.device.createSocket('localabstract:scrcpy');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async connectAndRetry(): Promise<AdbSocket> {
|
|
43
|
+
for (let i = 0; i < 100; i++) {
|
|
44
|
+
try {
|
|
45
|
+
return await this.connect();
|
|
46
|
+
} catch (e) {
|
|
47
|
+
await delay(100);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Can't connect to server after 100 retries`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async connectVideoStream(): Promise<AdbSocket> {
|
|
54
|
+
const stream = await this.connectAndRetry();
|
|
55
|
+
if (this.options.sendDummyByte) {
|
|
56
|
+
const reader = stream.readable.getReader();
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
// server will write a `0` to signal connection success
|
|
59
|
+
if (done || value.byteLength !== 1 || value[0] !== 0) {
|
|
60
|
+
throw new Error('Unexpected response from server');
|
|
61
|
+
}
|
|
62
|
+
reader.releaseLock();
|
|
63
|
+
}
|
|
64
|
+
return stream;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async getStreams(): Promise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]> {
|
|
68
|
+
const videoStream = await this.connectVideoStream();
|
|
69
|
+
|
|
70
|
+
let controlStream: AdbSocket | undefined;
|
|
71
|
+
if (this.options.control) {
|
|
72
|
+
controlStream = await this.connectAndRetry();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Server only writes device meta after control socket is connected (if enabled)
|
|
76
|
+
if (this.options.sendDeviceMeta) {
|
|
77
|
+
const reader = videoStream.readable.getReader();
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
// 64 bytes device name + 2 bytes video width + 2 bytes video height
|
|
80
|
+
if (done || value.byteLength !== 64 + 2 + 2) {
|
|
81
|
+
throw new Error('Unexpected response from server');
|
|
82
|
+
}
|
|
83
|
+
reader.releaseLock();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [videoStream, controlStream];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class ScrcpyClientReverseConnection extends ScrcpyClientConnection {
|
|
91
|
+
private streams!: ReadableStreamDefaultReader<AdbSocket>;
|
|
92
|
+
|
|
93
|
+
private address!: string;
|
|
94
|
+
|
|
95
|
+
public override async initialize(): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
// try to unbind first
|
|
98
|
+
await this.device.reverse.remove('localabstract:scrcpy');
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore error
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const queue = new TransformStream<AdbSocket>();
|
|
104
|
+
this.streams = queue.readable.getReader();
|
|
105
|
+
const writer = queue.writable.getWriter();
|
|
106
|
+
this.address = await this.device.reverse.add(
|
|
107
|
+
'localabstract:scrcpy',
|
|
108
|
+
'tcp:27183',
|
|
109
|
+
socket => {
|
|
110
|
+
writer.write(socket);
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async accept(): Promise<AdbSocket> {
|
|
117
|
+
return (await this.streams.read()).value!;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public async getStreams(): Promise<[videoSteam: AdbSocket, controlStream: AdbSocket | undefined]> {
|
|
121
|
+
const videoStream = await this.accept();
|
|
122
|
+
|
|
123
|
+
let controlStream: AdbSocket | undefined;
|
|
124
|
+
if (this.options.control) {
|
|
125
|
+
controlStream = await this.accept();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Server only writes device meta after control socket is connected (if enabled)
|
|
129
|
+
if (this.options.sendDeviceMeta) {
|
|
130
|
+
const reader = videoStream.readable.getReader();
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
// 64 bytes device name + 2 bytes video width + 2 bytes video height
|
|
133
|
+
if (done || value.byteLength !== 64 + 2 + 2) {
|
|
134
|
+
throw new Error('Unexpected response from server');
|
|
135
|
+
}
|
|
136
|
+
reader.releaseLock();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [videoStream, controlStream];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public override dispose() {
|
|
143
|
+
// Don't await this!
|
|
144
|
+
// `reverse.remove`'s response will never arrive
|
|
145
|
+
// before we read all pending data from `videoStream`
|
|
146
|
+
this.device.reverse.remove(this.address);
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/decoder/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './types.js';
|
|
2
|
-
export * from './tinyh264/index.js';
|
|
3
|
-
export * from './web-codecs/index.js';
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './tinyh264/index.js';
|
|
3
|
+
export * from './web-codecs/index.js';
|
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import { WritableStream } from "@yume-chan/adb";
|
|
2
|
-
import { PromiseResolver } from "@yume-chan/async";
|
|
3
|
-
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec.js";
|
|
4
|
-
import type { VideoStreamPacket } from "../../options/index.js";
|
|
5
|
-
import type { H264Configuration, H264Decoder } from "../types.js";
|
|
6
|
-
import { createTinyH264Wrapper, type TinyH264Wrapper } from "./wrapper.js";
|
|
7
|
-
|
|
8
|
-
let cachedInitializePromise: Promise<{ YuvBuffer: typeof import('yuv-buffer'), YuvCanvas: typeof import('yuv-canvas').default; }> | undefined;
|
|
9
|
-
function initialize() {
|
|
10
|
-
if (!cachedInitializePromise) {
|
|
11
|
-
cachedInitializePromise = Promise.all(
|
|
12
|
-
[import('yuv-buffer'), import('yuv-canvas')]
|
|
13
|
-
).then(([YuvBuffer, { default: YuvCanvas }]) => ({
|
|
14
|
-
YuvBuffer, YuvCanvas
|
|
15
|
-
}));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return cachedInitializePromise;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class TinyH264Decoder implements H264Decoder {
|
|
22
|
-
public readonly maxProfile = AndroidCodecProfile.Baseline;
|
|
23
|
-
|
|
24
|
-
public readonly maxLevel = AndroidCodecLevel.Level4;
|
|
25
|
-
|
|
26
|
-
private _renderer: HTMLCanvasElement;
|
|
27
|
-
public get renderer() { return this._renderer; }
|
|
28
|
-
|
|
29
|
-
private _frameRendered = 0;
|
|
30
|
-
public get frameRendered() { return this._frameRendered; }
|
|
31
|
-
|
|
32
|
-
private _writable: WritableStream<VideoStreamPacket>;
|
|
33
|
-
public get writable() { return this._writable; }
|
|
34
|
-
|
|
35
|
-
private _yuvCanvas: import('yuv-canvas').default | undefined;
|
|
36
|
-
private _initializer: PromiseResolver<TinyH264Wrapper> | undefined;
|
|
37
|
-
|
|
38
|
-
public constructor() {
|
|
39
|
-
initialize();
|
|
40
|
-
|
|
41
|
-
this._renderer = document.createElement('canvas');
|
|
42
|
-
|
|
43
|
-
this._writable = new WritableStream<VideoStreamPacket>({
|
|
44
|
-
write: async (packet) => {
|
|
45
|
-
switch (packet.type) {
|
|
46
|
-
case 'configuration':
|
|
47
|
-
this.configure(packet.data);
|
|
48
|
-
break;
|
|
49
|
-
case 'frame':
|
|
50
|
-
if (!this._initializer) {
|
|
51
|
-
throw new Error('Decoder not initialized');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const wrapper = await this._initializer.promise;
|
|
55
|
-
wrapper.feed(packet.data.slice().buffer);
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private async configure(config: H264Configuration) {
|
|
63
|
-
this.dispose();
|
|
64
|
-
|
|
65
|
-
this._initializer = new PromiseResolver<TinyH264Wrapper>();
|
|
66
|
-
const { YuvBuffer, YuvCanvas } = await initialize();
|
|
67
|
-
|
|
68
|
-
if (!this._yuvCanvas) {
|
|
69
|
-
this._yuvCanvas = YuvCanvas.attach(this._renderer);;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const { encodedWidth, encodedHeight } = config;
|
|
73
|
-
const chromaWidth = encodedWidth / 2;
|
|
74
|
-
const chromaHeight = encodedHeight / 2;
|
|
75
|
-
|
|
76
|
-
this._renderer.width = config.croppedWidth;
|
|
77
|
-
this._renderer.height = config.croppedHeight;
|
|
78
|
-
const format = YuvBuffer.format({
|
|
79
|
-
width: encodedWidth,
|
|
80
|
-
height: encodedHeight,
|
|
81
|
-
chromaWidth,
|
|
82
|
-
chromaHeight,
|
|
83
|
-
cropLeft: config.cropLeft,
|
|
84
|
-
cropTop: config.cropTop,
|
|
85
|
-
cropWidth: config.croppedWidth,
|
|
86
|
-
cropHeight: config.croppedHeight,
|
|
87
|
-
displayWidth: config.croppedWidth,
|
|
88
|
-
displayHeight: config.croppedHeight,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const wrapper = await createTinyH264Wrapper();
|
|
92
|
-
this._initializer.resolve(wrapper);
|
|
93
|
-
|
|
94
|
-
const uPlaneOffset = encodedWidth * encodedHeight;
|
|
95
|
-
const vPlaneOffset = uPlaneOffset + chromaWidth * chromaHeight;
|
|
96
|
-
wrapper.onPictureReady(({ data }) => {
|
|
97
|
-
this._frameRendered += 1;
|
|
98
|
-
const array = new Uint8Array(data);
|
|
99
|
-
const frame = YuvBuffer.frame(format,
|
|
100
|
-
YuvBuffer.lumaPlane(format, array, encodedWidth, 0),
|
|
101
|
-
YuvBuffer.chromaPlane(format, array, chromaWidth, uPlaneOffset),
|
|
102
|
-
YuvBuffer.chromaPlane(format, array, chromaWidth, vPlaneOffset)
|
|
103
|
-
);
|
|
104
|
-
this._yuvCanvas!.drawFrame(frame);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
public dispose(): void {
|
|
109
|
-
this._initializer?.promise.then(wrapper => wrapper.dispose());
|
|
110
|
-
this._initializer = undefined;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
1
|
+
import { WritableStream } from "@yume-chan/adb";
|
|
2
|
+
import { PromiseResolver } from "@yume-chan/async";
|
|
3
|
+
import { AndroidCodecLevel, AndroidCodecProfile } from "../../codec.js";
|
|
4
|
+
import type { VideoStreamPacket } from "../../options/index.js";
|
|
5
|
+
import type { H264Configuration, H264Decoder } from "../types.js";
|
|
6
|
+
import { createTinyH264Wrapper, type TinyH264Wrapper } from "./wrapper.js";
|
|
7
|
+
|
|
8
|
+
let cachedInitializePromise: Promise<{ YuvBuffer: typeof import('yuv-buffer'), YuvCanvas: typeof import('yuv-canvas').default; }> | undefined;
|
|
9
|
+
function initialize() {
|
|
10
|
+
if (!cachedInitializePromise) {
|
|
11
|
+
cachedInitializePromise = Promise.all(
|
|
12
|
+
[import('yuv-buffer'), import('yuv-canvas')]
|
|
13
|
+
).then(([YuvBuffer, { default: YuvCanvas }]) => ({
|
|
14
|
+
YuvBuffer, YuvCanvas
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return cachedInitializePromise;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class TinyH264Decoder implements H264Decoder {
|
|
22
|
+
public readonly maxProfile = AndroidCodecProfile.Baseline;
|
|
23
|
+
|
|
24
|
+
public readonly maxLevel = AndroidCodecLevel.Level4;
|
|
25
|
+
|
|
26
|
+
private _renderer: HTMLCanvasElement;
|
|
27
|
+
public get renderer() { return this._renderer; }
|
|
28
|
+
|
|
29
|
+
private _frameRendered = 0;
|
|
30
|
+
public get frameRendered() { return this._frameRendered; }
|
|
31
|
+
|
|
32
|
+
private _writable: WritableStream<VideoStreamPacket>;
|
|
33
|
+
public get writable() { return this._writable; }
|
|
34
|
+
|
|
35
|
+
private _yuvCanvas: import('yuv-canvas').default | undefined;
|
|
36
|
+
private _initializer: PromiseResolver<TinyH264Wrapper> | undefined;
|
|
37
|
+
|
|
38
|
+
public constructor() {
|
|
39
|
+
initialize();
|
|
40
|
+
|
|
41
|
+
this._renderer = document.createElement('canvas');
|
|
42
|
+
|
|
43
|
+
this._writable = new WritableStream<VideoStreamPacket>({
|
|
44
|
+
write: async (packet) => {
|
|
45
|
+
switch (packet.type) {
|
|
46
|
+
case 'configuration':
|
|
47
|
+
this.configure(packet.data);
|
|
48
|
+
break;
|
|
49
|
+
case 'frame':
|
|
50
|
+
if (!this._initializer) {
|
|
51
|
+
throw new Error('Decoder not initialized');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const wrapper = await this._initializer.promise;
|
|
55
|
+
wrapper.feed(packet.data.slice().buffer);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async configure(config: H264Configuration) {
|
|
63
|
+
this.dispose();
|
|
64
|
+
|
|
65
|
+
this._initializer = new PromiseResolver<TinyH264Wrapper>();
|
|
66
|
+
const { YuvBuffer, YuvCanvas } = await initialize();
|
|
67
|
+
|
|
68
|
+
if (!this._yuvCanvas) {
|
|
69
|
+
this._yuvCanvas = YuvCanvas.attach(this._renderer);;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { encodedWidth, encodedHeight } = config;
|
|
73
|
+
const chromaWidth = encodedWidth / 2;
|
|
74
|
+
const chromaHeight = encodedHeight / 2;
|
|
75
|
+
|
|
76
|
+
this._renderer.width = config.croppedWidth;
|
|
77
|
+
this._renderer.height = config.croppedHeight;
|
|
78
|
+
const format = YuvBuffer.format({
|
|
79
|
+
width: encodedWidth,
|
|
80
|
+
height: encodedHeight,
|
|
81
|
+
chromaWidth,
|
|
82
|
+
chromaHeight,
|
|
83
|
+
cropLeft: config.cropLeft,
|
|
84
|
+
cropTop: config.cropTop,
|
|
85
|
+
cropWidth: config.croppedWidth,
|
|
86
|
+
cropHeight: config.croppedHeight,
|
|
87
|
+
displayWidth: config.croppedWidth,
|
|
88
|
+
displayHeight: config.croppedHeight,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const wrapper = await createTinyH264Wrapper();
|
|
92
|
+
this._initializer.resolve(wrapper);
|
|
93
|
+
|
|
94
|
+
const uPlaneOffset = encodedWidth * encodedHeight;
|
|
95
|
+
const vPlaneOffset = uPlaneOffset + chromaWidth * chromaHeight;
|
|
96
|
+
wrapper.onPictureReady(({ data }) => {
|
|
97
|
+
this._frameRendered += 1;
|
|
98
|
+
const array = new Uint8Array(data);
|
|
99
|
+
const frame = YuvBuffer.frame(format,
|
|
100
|
+
YuvBuffer.lumaPlane(format, array, encodedWidth, 0),
|
|
101
|
+
YuvBuffer.chromaPlane(format, array, chromaWidth, uPlaneOffset),
|
|
102
|
+
YuvBuffer.chromaPlane(format, array, chromaWidth, vPlaneOffset)
|
|
103
|
+
);
|
|
104
|
+
this._yuvCanvas!.drawFrame(frame);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public dispose(): void {
|
|
109
|
+
this._initializer?.promise.then(wrapper => wrapper.dispose());
|
|
110
|
+
this._initializer = undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|