@yume-chan/scrcpy-decoder-webcodecs 0.0.23 → 1.0.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/CHANGELOG.md +40 -2
- package/LICENSE +1 -1
- package/README.md +49 -29
- package/esm/index.d.ts +1 -20
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -181
- package/esm/index.js.map +1 -1
- package/esm/video/codec/av1.d.ts +8 -0
- package/esm/video/codec/av1.d.ts.map +1 -0
- package/esm/video/codec/av1.js +71 -0
- package/esm/video/codec/av1.js.map +1 -0
- package/esm/video/codec/h264.d.ts +7 -0
- package/esm/video/codec/h264.d.ts.map +1 -0
- package/esm/video/codec/h264.js +27 -0
- package/esm/video/codec/h264.js.map +1 -0
- package/esm/video/codec/h265.d.ts +7 -0
- package/esm/video/codec/h265.d.ts.map +1 -0
- package/esm/video/codec/h265.js +33 -0
- package/esm/video/codec/h265.js.map +1 -0
- package/esm/video/codec/h26x.d.ts +9 -0
- package/esm/video/codec/h26x.d.ts.map +1 -0
- package/esm/video/codec/h26x.js +35 -0
- package/esm/video/codec/h26x.js.map +1 -0
- package/esm/video/codec/index.d.ts +7 -0
- package/esm/video/codec/index.d.ts.map +1 -0
- package/esm/video/codec/index.js +7 -0
- package/esm/video/codec/index.js.map +1 -0
- package/esm/video/codec/type.d.ts +8 -0
- package/esm/video/codec/type.d.ts.map +1 -0
- package/esm/video/codec/type.js +2 -0
- package/esm/video/codec/type.js.map +1 -0
- package/esm/video/codec/utils.d.ts +4 -0
- package/esm/video/codec/utils.d.ts.map +1 -0
- package/esm/video/codec/utils.js +10 -0
- package/esm/video/codec/utils.js.map +1 -0
- package/esm/video/decoder.d.ts +35 -0
- package/esm/video/decoder.d.ts.map +1 -0
- package/esm/video/decoder.js +240 -0
- package/esm/video/decoder.js.map +1 -0
- package/esm/video/index.d.ts +4 -0
- package/esm/video/index.d.ts.map +1 -0
- package/esm/video/index.js +4 -0
- package/esm/video/index.js.map +1 -0
- package/esm/video/render/bitmap.d.ts +7 -0
- package/esm/video/render/bitmap.d.ts.map +1 -0
- package/esm/video/render/bitmap.js +15 -0
- package/esm/video/render/bitmap.js.map +1 -0
- package/esm/video/render/canvas.d.ts +9 -0
- package/esm/video/render/canvas.d.ts.map +1 -0
- package/esm/video/render/canvas.js +22 -0
- package/esm/video/render/canvas.js.map +1 -0
- package/esm/video/render/index.d.ts +6 -0
- package/esm/video/render/index.d.ts.map +1 -0
- package/esm/video/render/index.js +7 -0
- package/esm/video/render/index.js.map +1 -0
- package/esm/video/render/insertable-stream.d.ts +10 -0
- package/esm/video/render/insertable-stream.d.ts.map +1 -0
- package/esm/video/render/insertable-stream.js +45 -0
- package/esm/video/render/insertable-stream.js.map +1 -0
- package/esm/video/render/type.d.ts +6 -0
- package/esm/video/render/type.d.ts.map +1 -0
- package/esm/video/render/type.js +2 -0
- package/esm/video/render/type.js.map +1 -0
- package/esm/video/render/webgl.d.ts +17 -0
- package/esm/video/render/webgl.d.ts.map +1 -0
- package/esm/{webgl.js → video/render/webgl.js} +47 -26
- package/esm/video/render/webgl.js.map +1 -0
- package/package.json +12 -17
- package/src/index.ts +1 -247
- package/src/video/codec/av1.ts +101 -0
- package/src/video/codec/h264.ts +42 -0
- package/src/video/codec/h265.ts +50 -0
- package/src/video/codec/h26x.ts +45 -0
- package/src/video/codec/index.ts +6 -0
- package/src/video/codec/type.ts +12 -0
- package/src/video/codec/utils.ts +11 -0
- package/src/video/decoder.ts +307 -0
- package/src/video/index.ts +3 -0
- package/src/video/render/bitmap.ts +18 -0
- package/src/video/render/canvas.ts +29 -0
- package/src/video/render/index.ts +7 -0
- package/src/video/render/insertable-stream.ts +62 -0
- package/src/video/render/type.ts +7 -0
- package/src/{webgl.ts → video/render/webgl.ts} +67 -30
- package/tsconfig.build.json +4 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/CHANGELOG.json +0 -74
- package/esm/bitmap.d.ts +0 -7
- package/esm/bitmap.d.ts.map +0 -1
- package/esm/bitmap.js +0 -17
- package/esm/bitmap.js.map +0 -1
- package/esm/renderer.d.ts +0 -4
- package/esm/renderer.d.ts.map +0 -1
- package/esm/renderer.js +0 -2
- package/esm/renderer.js.map +0 -1
- package/esm/webgl.d.ts +0 -9
- package/esm/webgl.d.ts.map +0 -1
- package/esm/webgl.js.map +0 -1
- package/src/bitmap.ts +0 -24
- package/src/renderer.ts +0 -3
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { EventEmitter } from "@yume-chan/event";
|
|
2
|
+
import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
|
|
3
|
+
import { ScrcpyVideoCodecId } from "@yume-chan/scrcpy";
|
|
4
|
+
import type {
|
|
5
|
+
ScrcpyVideoDecoder,
|
|
6
|
+
ScrcpyVideoDecoderCapability,
|
|
7
|
+
} from "@yume-chan/scrcpy-decoder-tinyh264";
|
|
8
|
+
import type { WritableStreamDefaultController } from "@yume-chan/stream-extra";
|
|
9
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
10
|
+
|
|
11
|
+
import { Av1Codec, H264Decoder, H265Decoder } from "./codec/index.js";
|
|
12
|
+
import type { CodecDecoder } from "./codec/type.js";
|
|
13
|
+
import type { WebCodecsVideoDecoderRenderer } from "./render/index.js";
|
|
14
|
+
|
|
15
|
+
class Pool<T> {
|
|
16
|
+
#controller!: ReadableStreamDefaultController<T>;
|
|
17
|
+
#readable = new ReadableStream<T>(
|
|
18
|
+
{
|
|
19
|
+
start: (controller) => {
|
|
20
|
+
this.#controller = controller;
|
|
21
|
+
},
|
|
22
|
+
pull: (controller) => {
|
|
23
|
+
controller.enqueue(this.#initializer());
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{ highWaterMark: 0 },
|
|
27
|
+
);
|
|
28
|
+
#reader = this.#readable.getReader();
|
|
29
|
+
|
|
30
|
+
#initializer: () => T;
|
|
31
|
+
|
|
32
|
+
#size = 0;
|
|
33
|
+
#capacity: number;
|
|
34
|
+
|
|
35
|
+
constructor(initializer: () => T, capacity: number) {
|
|
36
|
+
this.#initializer = initializer;
|
|
37
|
+
this.#capacity = capacity;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async borrow() {
|
|
41
|
+
const result = await this.#reader.read();
|
|
42
|
+
return result.value!;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return(value: T) {
|
|
46
|
+
if (this.#size < this.#capacity) {
|
|
47
|
+
this.#controller.enqueue(value);
|
|
48
|
+
this.#size += 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class VideoFrameCapturer {
|
|
54
|
+
#canvas: OffscreenCanvas | HTMLCanvasElement;
|
|
55
|
+
#context: ImageBitmapRenderingContext;
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
59
|
+
this.#canvas = new OffscreenCanvas(1, 1);
|
|
60
|
+
} else {
|
|
61
|
+
this.#canvas = document.createElement("canvas");
|
|
62
|
+
this.#canvas.width = 1;
|
|
63
|
+
this.#canvas.height = 1;
|
|
64
|
+
}
|
|
65
|
+
this.#context = this.#canvas.getContext("bitmaprenderer", {
|
|
66
|
+
alpha: false,
|
|
67
|
+
})!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async capture(frame: VideoFrame): Promise<Blob> {
|
|
71
|
+
this.#canvas.width = frame.displayWidth;
|
|
72
|
+
this.#canvas.height = frame.displayHeight;
|
|
73
|
+
|
|
74
|
+
const bitmap = await createImageBitmap(frame);
|
|
75
|
+
this.#context.transferFromImageBitmap(bitmap);
|
|
76
|
+
|
|
77
|
+
if (this.#canvas instanceof OffscreenCanvas) {
|
|
78
|
+
return await this.#canvas.convertToBlob({
|
|
79
|
+
type: "image/png",
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
(this.#canvas as HTMLCanvasElement).toBlob((blob) => {
|
|
84
|
+
if (!blob) {
|
|
85
|
+
reject(new Error("Failed to convert canvas to blob"));
|
|
86
|
+
} else {
|
|
87
|
+
resolve(blob);
|
|
88
|
+
}
|
|
89
|
+
}, "image/png");
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const VideoFrameCapturerPool =
|
|
96
|
+
/* #__PURE__ */
|
|
97
|
+
new Pool(() => new VideoFrameCapturer(), 4);
|
|
98
|
+
|
|
99
|
+
export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
100
|
+
static get isSupported() {
|
|
101
|
+
return typeof globalThis.VideoDecoder !== "undefined";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static readonly capabilities: Record<string, ScrcpyVideoDecoderCapability> =
|
|
105
|
+
{
|
|
106
|
+
h264: {},
|
|
107
|
+
h265: {},
|
|
108
|
+
av1: {},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
#codec: ScrcpyVideoCodecId;
|
|
112
|
+
get codec() {
|
|
113
|
+
return this.#codec;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#codecDecoder: CodecDecoder;
|
|
117
|
+
|
|
118
|
+
#writable: WritableStream<ScrcpyMediaStreamPacket>;
|
|
119
|
+
get writable() {
|
|
120
|
+
return this.#writable;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#error: Error | undefined;
|
|
124
|
+
#controller!: WritableStreamDefaultController;
|
|
125
|
+
|
|
126
|
+
#renderer: WebCodecsVideoDecoderRenderer;
|
|
127
|
+
get renderer() {
|
|
128
|
+
return this.#renderer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#framesDraw = 0;
|
|
132
|
+
#framesPresented = 0;
|
|
133
|
+
get framesRendered() {
|
|
134
|
+
return this.#framesPresented;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#framesSkipped = 0;
|
|
138
|
+
get framesSkipped() {
|
|
139
|
+
return this.#framesSkipped;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#sizeChanged = new EventEmitter<{ width: number; height: number }>();
|
|
143
|
+
get sizeChanged() {
|
|
144
|
+
return this.#sizeChanged.event;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#decoder: VideoDecoder;
|
|
148
|
+
|
|
149
|
+
#drawing = false;
|
|
150
|
+
#nextFrame: VideoFrame | undefined;
|
|
151
|
+
#captureFrame: VideoFrame | undefined;
|
|
152
|
+
|
|
153
|
+
#animationFrameId = 0;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a new WebCodecs video decoder.
|
|
157
|
+
*/
|
|
158
|
+
constructor({ codec, renderer }: WebCodecsVideoDecoder.Options) {
|
|
159
|
+
this.#codec = codec;
|
|
160
|
+
|
|
161
|
+
this.#renderer = renderer;
|
|
162
|
+
|
|
163
|
+
this.#decoder = new VideoDecoder({
|
|
164
|
+
output: (frame) => {
|
|
165
|
+
this.#captureFrame?.close();
|
|
166
|
+
// PERF: `VideoFrame#clone` is cheap
|
|
167
|
+
this.#captureFrame = frame.clone();
|
|
168
|
+
|
|
169
|
+
if (this.#drawing) {
|
|
170
|
+
if (this.#nextFrame) {
|
|
171
|
+
this.#nextFrame.close();
|
|
172
|
+
this.#framesSkipped += 1;
|
|
173
|
+
}
|
|
174
|
+
this.#nextFrame = frame;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
void this.#draw(frame);
|
|
179
|
+
},
|
|
180
|
+
error: (error) => {
|
|
181
|
+
this.#setError(error);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
switch (this.#codec) {
|
|
186
|
+
case ScrcpyVideoCodecId.H264:
|
|
187
|
+
this.#codecDecoder = new H264Decoder(
|
|
188
|
+
this.#decoder,
|
|
189
|
+
this.#updateSize,
|
|
190
|
+
);
|
|
191
|
+
break;
|
|
192
|
+
case ScrcpyVideoCodecId.H265:
|
|
193
|
+
this.#codecDecoder = new H265Decoder(
|
|
194
|
+
this.#decoder,
|
|
195
|
+
this.#updateSize,
|
|
196
|
+
);
|
|
197
|
+
break;
|
|
198
|
+
case ScrcpyVideoCodecId.AV1:
|
|
199
|
+
this.#codecDecoder = new Av1Codec(
|
|
200
|
+
this.#decoder,
|
|
201
|
+
this.#updateSize,
|
|
202
|
+
);
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
throw new Error(`Unsupported codec: ${this.#codec as number}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.#writable = new WritableStream<ScrcpyMediaStreamPacket>({
|
|
209
|
+
start: (controller) => {
|
|
210
|
+
if (this.#error) {
|
|
211
|
+
controller.error(this.#error);
|
|
212
|
+
} else {
|
|
213
|
+
this.#controller = controller;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
write: (packet) => {
|
|
217
|
+
this.#codecDecoder.decode(packet);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
this.#onVerticalSync();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#setError(error: Error) {
|
|
225
|
+
if (this.#controller) {
|
|
226
|
+
try {
|
|
227
|
+
this.#controller.error(error);
|
|
228
|
+
} catch {
|
|
229
|
+
// ignore
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
this.#error = error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async #draw(frame: VideoFrame) {
|
|
237
|
+
try {
|
|
238
|
+
this.#drawing = true;
|
|
239
|
+
// PERF: Draw every frame to minimize latency at cost of performance.
|
|
240
|
+
// When multiple frames are drawn in one vertical sync interval,
|
|
241
|
+
// only the last one is visible to users.
|
|
242
|
+
// But this ensures users can always see the most up-to-date screen.
|
|
243
|
+
// This is also the behavior of official Scrcpy client.
|
|
244
|
+
// https://github.com/Genymobile/scrcpy/issues/3679
|
|
245
|
+
this.#updateSize(frame.displayWidth, frame.displayHeight);
|
|
246
|
+
await this.#renderer.draw(frame);
|
|
247
|
+
this.#framesDraw += 1;
|
|
248
|
+
frame.close();
|
|
249
|
+
|
|
250
|
+
if (this.#nextFrame) {
|
|
251
|
+
const frame = this.#nextFrame;
|
|
252
|
+
this.#nextFrame = undefined;
|
|
253
|
+
await this.#draw(frame);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.#drawing = false;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.#setError(error as Error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#updateSize = (width: number, height: number) => {
|
|
263
|
+
this.#renderer.setSize(width, height);
|
|
264
|
+
this.#sizeChanged.fire({ width, height });
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
#onVerticalSync = () => {
|
|
268
|
+
if (this.#framesDraw > 0) {
|
|
269
|
+
this.#framesPresented += 1;
|
|
270
|
+
this.#framesSkipped += this.#framesDraw - 1;
|
|
271
|
+
this.#framesDraw = 0;
|
|
272
|
+
}
|
|
273
|
+
this.#animationFrameId = requestAnimationFrame(this.#onVerticalSync);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
async snapshot() {
|
|
277
|
+
const frame = this.#captureFrame;
|
|
278
|
+
if (!frame) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const capturer = await VideoFrameCapturerPool.borrow();
|
|
283
|
+
const result = await capturer.capture(frame);
|
|
284
|
+
VideoFrameCapturerPool.return(capturer);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
dispose() {
|
|
289
|
+
cancelAnimationFrame(this.#animationFrameId);
|
|
290
|
+
if (this.#decoder.state !== "closed") {
|
|
291
|
+
this.#decoder.close();
|
|
292
|
+
}
|
|
293
|
+
this.#nextFrame?.close();
|
|
294
|
+
this.#captureFrame?.close();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export namespace WebCodecsVideoDecoder {
|
|
299
|
+
export interface Options {
|
|
300
|
+
/**
|
|
301
|
+
* The video codec to decode
|
|
302
|
+
*/
|
|
303
|
+
codec: ScrcpyVideoCodecId;
|
|
304
|
+
|
|
305
|
+
renderer: WebCodecsVideoDecoderRenderer;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CanvasWebCodecsDecoderRenderer } from "./canvas.js";
|
|
2
|
+
|
|
3
|
+
export class BitmapWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRenderer {
|
|
4
|
+
#context: ImageBitmapRenderingContext;
|
|
5
|
+
|
|
6
|
+
constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
|
|
7
|
+
super(canvas);
|
|
8
|
+
|
|
9
|
+
this.#context = this.canvas.getContext("bitmaprenderer", {
|
|
10
|
+
alpha: false,
|
|
11
|
+
})!;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async draw(frame: VideoFrame): Promise<void> {
|
|
15
|
+
const bitmap = await createImageBitmap(frame);
|
|
16
|
+
this.#context.transferFromImageBitmap(bitmap);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
|
|
2
|
+
|
|
3
|
+
import type { WebCodecsVideoDecoderRenderer } from "./type.js";
|
|
4
|
+
|
|
5
|
+
export abstract class CanvasWebCodecsDecoderRenderer
|
|
6
|
+
implements WebCodecsVideoDecoderRenderer
|
|
7
|
+
{
|
|
8
|
+
#canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
9
|
+
get canvas() {
|
|
10
|
+
return this.#canvas;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
|
|
14
|
+
if (canvas) {
|
|
15
|
+
this.#canvas = canvas;
|
|
16
|
+
} else {
|
|
17
|
+
this.#canvas = createCanvas();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setSize(width: number, height: number): void {
|
|
22
|
+
if (this.#canvas.width !== width || this.#canvas.height !== height) {
|
|
23
|
+
this.#canvas.width = width;
|
|
24
|
+
this.#canvas.height = height;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
abstract draw(frame: VideoFrame): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// cspell: ignore insertable
|
|
2
|
+
|
|
3
|
+
import type { WebCodecsVideoDecoderRenderer } from "./type.js";
|
|
4
|
+
|
|
5
|
+
declare class MediaStreamTrackGenerator extends MediaStreamTrack {
|
|
6
|
+
constructor(options: { kind: "audio" | "video" });
|
|
7
|
+
|
|
8
|
+
writable: WritableStream<VideoFrame>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class InsertableStreamWebCodecsDecoderRenderer
|
|
12
|
+
implements WebCodecsVideoDecoderRenderer
|
|
13
|
+
{
|
|
14
|
+
static get isSupported() {
|
|
15
|
+
return typeof MediaStreamTrackGenerator !== "undefined";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#element: HTMLVideoElement;
|
|
19
|
+
get element() {
|
|
20
|
+
return this.#element;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#generator: MediaStreamTrackGenerator;
|
|
24
|
+
#writer: WritableStreamDefaultWriter<VideoFrame>;
|
|
25
|
+
#stream: MediaStream;
|
|
26
|
+
|
|
27
|
+
constructor(element?: HTMLVideoElement) {
|
|
28
|
+
if (element) {
|
|
29
|
+
this.#element = element;
|
|
30
|
+
} else if (typeof document !== "undefined") {
|
|
31
|
+
this.#element = document.createElement("video");
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"no video element input found nor any video element can be created",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
this.#element.muted = true;
|
|
38
|
+
this.#element.autoplay = true;
|
|
39
|
+
this.#element.disablePictureInPicture = true;
|
|
40
|
+
this.#element.disableRemotePlayback = true;
|
|
41
|
+
|
|
42
|
+
// The spec replaced `MediaStreamTrackGenerator` with `VideoTrackGenerator`.
|
|
43
|
+
// But Chrome has not implemented it yet.
|
|
44
|
+
// https://issues.chromium.org/issues/40058895
|
|
45
|
+
this.#generator = new MediaStreamTrackGenerator({ kind: "video" });
|
|
46
|
+
this.#writer = this.#generator.writable.getWriter();
|
|
47
|
+
|
|
48
|
+
this.#stream = new MediaStream([this.#generator]);
|
|
49
|
+
this.#element.srcObject = this.#stream;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setSize(width: number, height: number): void {
|
|
53
|
+
if (this.#element.width !== width || this.#element.height !== height) {
|
|
54
|
+
this.#element.width = width;
|
|
55
|
+
this.#element.height = height;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async draw(frame: VideoFrame): Promise<void> {
|
|
60
|
+
await this.#writer.write(frame);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,48 +1,84 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
|
|
2
|
+
|
|
3
|
+
import { CanvasWebCodecsDecoderRenderer } from "./canvas.js";
|
|
4
|
+
|
|
5
|
+
const Resolved = Promise.resolve();
|
|
6
|
+
|
|
7
|
+
function createContext(
|
|
8
|
+
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
9
|
+
enableCapture?: boolean,
|
|
10
|
+
): WebGLRenderingContext | null {
|
|
11
|
+
const attributes: WebGLContextAttributes = {
|
|
12
|
+
// Low-power GPU should be enough for video rendering.
|
|
13
|
+
powerPreference: "low-power",
|
|
14
|
+
alpha: false,
|
|
15
|
+
// Disallow software rendering.
|
|
16
|
+
// Other rendering methods are faster than software-based WebGL.
|
|
17
|
+
failIfMajorPerformanceCaveat: true,
|
|
18
|
+
preserveDrawingBuffer: !!enableCapture,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
canvas.getContext("webgl2", attributes) ||
|
|
23
|
+
canvas.getContext("webgl", attributes)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
2
26
|
|
|
3
|
-
export class
|
|
27
|
+
export class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRenderer {
|
|
4
28
|
static vertexShaderSource = `
|
|
5
|
-
attribute vec2 xy;
|
|
29
|
+
attribute vec2 xy;
|
|
6
30
|
|
|
7
|
-
varying highp vec2 uv;
|
|
31
|
+
varying highp vec2 uv;
|
|
8
32
|
|
|
9
|
-
void main(void) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
33
|
+
void main(void) {
|
|
34
|
+
gl_Position = vec4(xy, 0.0, 1.0);
|
|
35
|
+
// Map vertex coordinates (-1 to +1) to UV coordinates (0 to 1).
|
|
36
|
+
// UV coordinates are Y-flipped relative to vertex coordinates.
|
|
37
|
+
uv = vec2((1.0 + xy.x) / 2.0, (1.0 - xy.y) / 2.0);
|
|
38
|
+
}
|
|
15
39
|
`;
|
|
16
40
|
|
|
17
41
|
static fragmentShaderSource = `
|
|
18
|
-
varying highp vec2 uv;
|
|
42
|
+
varying highp vec2 uv;
|
|
19
43
|
|
|
20
|
-
uniform sampler2D texture;
|
|
44
|
+
uniform sampler2D texture;
|
|
21
45
|
|
|
22
|
-
void main(void) {
|
|
23
|
-
|
|
24
|
-
}
|
|
46
|
+
void main(void) {
|
|
47
|
+
gl_FragColor = texture2D(texture, uv);
|
|
48
|
+
}
|
|
25
49
|
`;
|
|
26
50
|
|
|
51
|
+
static get isSupported() {
|
|
52
|
+
const canvas = createCanvas();
|
|
53
|
+
return !!createContext(canvas);
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
#context: WebGLRenderingContext;
|
|
28
57
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Create a new WebGL frame renderer.
|
|
60
|
+
* @param canvas The canvas to render frames to.
|
|
61
|
+
* @param enableCapture
|
|
62
|
+
* Whether to allow capturing the canvas content using APIs like `readPixels` and `toDataURL`.
|
|
63
|
+
* Enable this option may reduce performance.
|
|
64
|
+
*/
|
|
65
|
+
constructor(
|
|
66
|
+
canvas?: HTMLCanvasElement | OffscreenCanvas,
|
|
67
|
+
enableCapture?: boolean,
|
|
68
|
+
) {
|
|
69
|
+
super(canvas);
|
|
70
|
+
|
|
71
|
+
const gl = createContext(this.canvas, enableCapture);
|
|
39
72
|
if (!gl) {
|
|
40
73
|
throw new Error("WebGL not supported");
|
|
41
74
|
}
|
|
42
75
|
this.#context = gl;
|
|
43
76
|
|
|
44
77
|
const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
|
|
45
|
-
gl.shaderSource(
|
|
78
|
+
gl.shaderSource(
|
|
79
|
+
vertexShader,
|
|
80
|
+
WebGLWebCodecsDecoderRenderer.vertexShaderSource,
|
|
81
|
+
);
|
|
46
82
|
gl.compileShader(vertexShader);
|
|
47
83
|
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
48
84
|
throw new Error(gl.getShaderInfoLog(vertexShader)!);
|
|
@@ -51,14 +87,14 @@ void main(void) {
|
|
|
51
87
|
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
|
|
52
88
|
gl.shaderSource(
|
|
53
89
|
fragmentShader,
|
|
54
|
-
|
|
90
|
+
WebGLWebCodecsDecoderRenderer.fragmentShaderSource,
|
|
55
91
|
);
|
|
56
92
|
gl.compileShader(fragmentShader);
|
|
57
93
|
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
58
94
|
throw new Error(gl.getShaderInfoLog(fragmentShader)!);
|
|
59
95
|
}
|
|
60
96
|
|
|
61
|
-
const shaderProgram = gl.createProgram()
|
|
97
|
+
const shaderProgram = gl.createProgram();
|
|
62
98
|
gl.attachShader(shaderProgram, vertexShader);
|
|
63
99
|
gl.attachShader(shaderProgram, fragmentShader);
|
|
64
100
|
gl.linkProgram(shaderProgram);
|
|
@@ -89,7 +125,7 @@ void main(void) {
|
|
|
89
125
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
90
126
|
}
|
|
91
127
|
|
|
92
|
-
draw(frame: VideoFrame) {
|
|
128
|
+
draw(frame: VideoFrame): Promise<void> {
|
|
93
129
|
const gl = this.#context;
|
|
94
130
|
gl.texImage2D(
|
|
95
131
|
gl.TEXTURE_2D,
|
|
@@ -99,9 +135,10 @@ void main(void) {
|
|
|
99
135
|
gl.UNSIGNED_BYTE,
|
|
100
136
|
frame,
|
|
101
137
|
);
|
|
102
|
-
frame.close();
|
|
103
138
|
|
|
104
139
|
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
105
140
|
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
|
|
141
|
+
|
|
142
|
+
return Resolved;
|
|
106
143
|
}
|
|
107
144
|
}
|
package/tsconfig.build.json
CHANGED
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
},
|
|
10
10
|
"references": [
|
|
11
11
|
{
|
|
12
|
-
"path": "../
|
|
12
|
+
"path": "../no-data-view/tsconfig.build.json"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "../scrcpy-decoder-tinyh264/tsconfig.build.json"
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
"path": "../scrcpy/tsconfig.build.json"
|
|
19
|
+
},
|
|
17
20
|
{
|
|
18
21
|
"path": "../stream-extra/tsconfig.build.json"
|
|
19
22
|
}
|