@yume-chan/scrcpy-decoder-webcodecs 1.0.0 → 2.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 +31 -0
- package/LICENSE +1 -1
- package/esm/video/decoder.d.ts +6 -4
- package/esm/video/decoder.d.ts.map +1 -1
- package/esm/video/decoder.js +17 -74
- package/esm/video/decoder.js.map +1 -1
- package/esm/video/pool.d.ts +7 -0
- package/esm/video/pool.d.ts.map +1 -0
- package/esm/video/pool.js +30 -0
- package/esm/video/pool.js.map +1 -0
- package/esm/video/render/bitmap.d.ts +2 -2
- package/esm/video/render/bitmap.d.ts.map +1 -1
- package/esm/video/render/bitmap.js +2 -2
- package/esm/video/render/bitmap.js.map +1 -1
- package/esm/video/render/canvas.d.ts +2 -2
- package/esm/video/render/canvas.d.ts.map +1 -1
- package/esm/video/render/canvas.js +1 -1
- package/esm/video/render/canvas.js.map +1 -1
- package/esm/video/render/insertable-stream.d.ts +2 -2
- package/esm/video/render/insertable-stream.d.ts.map +1 -1
- package/esm/video/render/insertable-stream.js +1 -1
- package/esm/video/render/insertable-stream.js.map +1 -1
- package/esm/video/render/type.d.ts +1 -1
- package/esm/video/render/type.d.ts.map +1 -1
- package/esm/video/render/webgl.d.ts +2 -2
- package/esm/video/render/webgl.d.ts.map +1 -1
- package/esm/video/render/webgl.js +4 -4
- package/esm/video/render/webgl.js.map +1 -1
- package/esm/video/snapshot.d.ts +6 -0
- package/esm/video/snapshot.d.ts.map +1 -0
- package/esm/video/snapshot.js +41 -0
- package/esm/video/snapshot.js.map +1 -0
- package/package.json +8 -8
- package/src/video/decoder.ts +24 -88
- package/src/video/pool.ts +37 -0
- package/src/video/render/bitmap.ts +2 -2
- package/src/video/render/canvas.ts +2 -4
- package/src/video/render/insertable-stream.ts +2 -4
- package/src/video/render/type.ts +1 -1
- package/src/video/render/webgl.ts +4 -4
- package/src/video/snapshot.ts +41 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/video/decoder.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StickyEventEmitter } from "@yume-chan/event";
|
|
2
2
|
import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
|
|
3
3
|
import { ScrcpyVideoCodecId } from "@yume-chan/scrcpy";
|
|
4
4
|
import type {
|
|
@@ -10,87 +10,9 @@ import { WritableStream } from "@yume-chan/stream-extra";
|
|
|
10
10
|
|
|
11
11
|
import { Av1Codec, H264Decoder, H265Decoder } from "./codec/index.js";
|
|
12
12
|
import type { CodecDecoder } from "./codec/type.js";
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
}
|
|
13
|
+
import { Pool } from "./pool.js";
|
|
14
|
+
import type { VideoFrameRenderer } from "./render/index.js";
|
|
15
|
+
import { VideoFrameCapturer } from "./snapshot.js";
|
|
94
16
|
|
|
95
17
|
const VideoFrameCapturerPool =
|
|
96
18
|
/* #__PURE__ */
|
|
@@ -123,7 +45,7 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
|
123
45
|
#error: Error | undefined;
|
|
124
46
|
#controller!: WritableStreamDefaultController;
|
|
125
47
|
|
|
126
|
-
#renderer:
|
|
48
|
+
#renderer: VideoFrameRenderer;
|
|
127
49
|
get renderer() {
|
|
128
50
|
return this.#renderer;
|
|
129
51
|
}
|
|
@@ -139,11 +61,21 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
|
139
61
|
return this.#framesSkipped;
|
|
140
62
|
}
|
|
141
63
|
|
|
142
|
-
#sizeChanged = new
|
|
64
|
+
#sizeChanged = new StickyEventEmitter<{ width: number; height: number }>();
|
|
143
65
|
get sizeChanged() {
|
|
144
66
|
return this.#sizeChanged.event;
|
|
145
67
|
}
|
|
146
68
|
|
|
69
|
+
#width: number = 0;
|
|
70
|
+
get width() {
|
|
71
|
+
return this.#width;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#height: number = 0;
|
|
75
|
+
get height() {
|
|
76
|
+
return this.#height;
|
|
77
|
+
}
|
|
78
|
+
|
|
147
79
|
#decoder: VideoDecoder;
|
|
148
80
|
|
|
149
81
|
#drawing = false;
|
|
@@ -218,7 +150,7 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
|
218
150
|
},
|
|
219
151
|
});
|
|
220
152
|
|
|
221
|
-
this.#
|
|
153
|
+
this.#handleAnimationFrame();
|
|
222
154
|
}
|
|
223
155
|
|
|
224
156
|
#setError(error: Error) {
|
|
@@ -261,16 +193,20 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
|
261
193
|
|
|
262
194
|
#updateSize = (width: number, height: number) => {
|
|
263
195
|
this.#renderer.setSize(width, height);
|
|
196
|
+
this.#width = width;
|
|
197
|
+
this.#height = height;
|
|
264
198
|
this.#sizeChanged.fire({ width, height });
|
|
265
199
|
};
|
|
266
200
|
|
|
267
|
-
#
|
|
201
|
+
#handleAnimationFrame = () => {
|
|
268
202
|
if (this.#framesDraw > 0) {
|
|
269
203
|
this.#framesPresented += 1;
|
|
270
204
|
this.#framesSkipped += this.#framesDraw - 1;
|
|
271
205
|
this.#framesDraw = 0;
|
|
272
206
|
}
|
|
273
|
-
this.#animationFrameId = requestAnimationFrame(
|
|
207
|
+
this.#animationFrameId = requestAnimationFrame(
|
|
208
|
+
this.#handleAnimationFrame,
|
|
209
|
+
);
|
|
274
210
|
};
|
|
275
211
|
|
|
276
212
|
async snapshot() {
|
|
@@ -302,6 +238,6 @@ export namespace WebCodecsVideoDecoder {
|
|
|
302
238
|
*/
|
|
303
239
|
codec: ScrcpyVideoCodecId;
|
|
304
240
|
|
|
305
|
-
renderer:
|
|
241
|
+
renderer: VideoFrameRenderer;
|
|
306
242
|
}
|
|
307
243
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class Pool<T> {
|
|
2
|
+
#controller!: ReadableStreamDefaultController<T>;
|
|
3
|
+
#readable = new ReadableStream<T>(
|
|
4
|
+
{
|
|
5
|
+
start: (controller) => {
|
|
6
|
+
this.#controller = controller;
|
|
7
|
+
},
|
|
8
|
+
pull: (controller) => {
|
|
9
|
+
controller.enqueue(this.#initializer());
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
{ highWaterMark: 0 },
|
|
13
|
+
);
|
|
14
|
+
#reader = this.#readable.getReader();
|
|
15
|
+
|
|
16
|
+
#initializer: () => T;
|
|
17
|
+
|
|
18
|
+
#size = 0;
|
|
19
|
+
#capacity: number;
|
|
20
|
+
|
|
21
|
+
constructor(initializer: () => T, capacity: number) {
|
|
22
|
+
this.#initializer = initializer;
|
|
23
|
+
this.#capacity = capacity;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async borrow() {
|
|
27
|
+
const result = await this.#reader.read();
|
|
28
|
+
return result.value!;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return(value: T) {
|
|
32
|
+
if (this.#size < this.#capacity) {
|
|
33
|
+
this.#controller.enqueue(value);
|
|
34
|
+
this.#size += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CanvasVideoFrameRenderer } from "./canvas.js";
|
|
2
2
|
|
|
3
|
-
export class
|
|
3
|
+
export class BitmapVideoFrameRenderer extends CanvasVideoFrameRenderer {
|
|
4
4
|
#context: ImageBitmapRenderingContext;
|
|
5
5
|
|
|
6
6
|
constructor(canvas?: HTMLCanvasElement | OffscreenCanvas) {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { VideoFrameRenderer } from "./type.js";
|
|
4
4
|
|
|
5
|
-
export abstract class
|
|
6
|
-
implements WebCodecsVideoDecoderRenderer
|
|
7
|
-
{
|
|
5
|
+
export abstract class CanvasVideoFrameRenderer implements VideoFrameRenderer {
|
|
8
6
|
#canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
9
7
|
get canvas() {
|
|
10
8
|
return this.#canvas;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cspell: ignore insertable
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { VideoFrameRenderer } from "./type.js";
|
|
4
4
|
|
|
5
5
|
declare class MediaStreamTrackGenerator extends MediaStreamTrack {
|
|
6
6
|
constructor(options: { kind: "audio" | "video" });
|
|
@@ -8,9 +8,7 @@ declare class MediaStreamTrackGenerator extends MediaStreamTrack {
|
|
|
8
8
|
writable: WritableStream<VideoFrame>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export class
|
|
12
|
-
implements WebCodecsVideoDecoderRenderer
|
|
13
|
-
{
|
|
11
|
+
export class InsertableStreamVideoFrameRenderer implements VideoFrameRenderer {
|
|
14
12
|
static get isSupported() {
|
|
15
13
|
return typeof MediaStreamTrackGenerator !== "undefined";
|
|
16
14
|
}
|
package/src/video/render/type.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCanvas } from "@yume-chan/scrcpy-decoder-tinyh264";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { CanvasVideoFrameRenderer } from "./canvas.js";
|
|
4
4
|
|
|
5
5
|
const Resolved = Promise.resolve();
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ function createContext(
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export class
|
|
27
|
+
export class WebGLVideoFrameRenderer extends CanvasVideoFrameRenderer {
|
|
28
28
|
static vertexShaderSource = `
|
|
29
29
|
attribute vec2 xy;
|
|
30
30
|
|
|
@@ -77,7 +77,7 @@ export class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRendere
|
|
|
77
77
|
const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
|
|
78
78
|
gl.shaderSource(
|
|
79
79
|
vertexShader,
|
|
80
|
-
|
|
80
|
+
WebGLVideoFrameRenderer.vertexShaderSource,
|
|
81
81
|
);
|
|
82
82
|
gl.compileShader(vertexShader);
|
|
83
83
|
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
@@ -87,7 +87,7 @@ export class WebGLWebCodecsDecoderRenderer extends CanvasWebCodecsDecoderRendere
|
|
|
87
87
|
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
|
|
88
88
|
gl.shaderSource(
|
|
89
89
|
fragmentShader,
|
|
90
|
-
|
|
90
|
+
WebGLVideoFrameRenderer.fragmentShaderSource,
|
|
91
91
|
);
|
|
92
92
|
gl.compileShader(fragmentShader);
|
|
93
93
|
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class VideoFrameCapturer {
|
|
2
|
+
#canvas: OffscreenCanvas | HTMLCanvasElement;
|
|
3
|
+
#context: ImageBitmapRenderingContext;
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
7
|
+
this.#canvas = new OffscreenCanvas(1, 1);
|
|
8
|
+
} else {
|
|
9
|
+
this.#canvas = document.createElement("canvas");
|
|
10
|
+
this.#canvas.width = 1;
|
|
11
|
+
this.#canvas.height = 1;
|
|
12
|
+
}
|
|
13
|
+
this.#context = this.#canvas.getContext("bitmaprenderer", {
|
|
14
|
+
alpha: false,
|
|
15
|
+
})!;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async capture(frame: VideoFrame): Promise<Blob> {
|
|
19
|
+
this.#canvas.width = frame.displayWidth;
|
|
20
|
+
this.#canvas.height = frame.displayHeight;
|
|
21
|
+
|
|
22
|
+
const bitmap = await createImageBitmap(frame);
|
|
23
|
+
this.#context.transferFromImageBitmap(bitmap);
|
|
24
|
+
|
|
25
|
+
if (this.#canvas instanceof OffscreenCanvas) {
|
|
26
|
+
return await this.#canvas.convertToBlob({
|
|
27
|
+
type: "image/png",
|
|
28
|
+
});
|
|
29
|
+
} else {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
(this.#canvas as HTMLCanvasElement).toBlob((blob) => {
|
|
32
|
+
if (!blob) {
|
|
33
|
+
reject(new Error("Failed to convert canvas to blob"));
|
|
34
|
+
} else {
|
|
35
|
+
resolve(blob);
|
|
36
|
+
}
|
|
37
|
+
}, "image/png");
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|