@yume-chan/scrcpy-decoder-webcodecs 2.5.3 → 3.0.0-beta.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/LICENSE +1 -1
- package/README.md +1 -1
- package/esm/video/codec/av1.d.ts +4 -6
- package/esm/video/codec/av1.d.ts.map +1 -1
- package/esm/video/codec/av1.js +29 -95
- package/esm/video/codec/av1.js.map +1 -1
- package/esm/video/codec/h264.d.ts +4 -6
- package/esm/video/codec/h264.d.ts.map +1 -1
- package/esm/video/codec/h264.js +11 -21
- package/esm/video/codec/h264.js.map +1 -1
- package/esm/video/codec/h265.d.ts +4 -6
- package/esm/video/codec/h265.d.ts.map +1 -1
- package/esm/video/codec/h265.js +13 -27
- package/esm/video/codec/h265.js.map +1 -1
- package/esm/video/codec/h26x.d.ts +9 -6
- package/esm/video/codec/h26x.d.ts.map +1 -1
- package/esm/video/codec/h26x.js +25 -69
- package/esm/video/codec/h26x.js.map +1 -1
- package/esm/video/codec/index.d.ts +0 -1
- package/esm/video/codec/index.d.ts.map +1 -1
- package/esm/video/codec/index.js +0 -1
- package/esm/video/codec/index.js.map +1 -1
- package/esm/video/codec/type.d.ts +23 -8
- package/esm/video/codec/type.d.ts.map +1 -1
- package/esm/video/decoder.d.ts +62 -15
- package/esm/video/decoder.d.ts.map +1 -1
- package/esm/video/decoder.js +170 -134
- package/esm/video/decoder.js.map +1 -1
- package/esm/video/index.d.ts +1 -0
- package/esm/video/index.d.ts.map +1 -1
- package/esm/video/index.js +1 -0
- package/esm/video/index.js.map +1 -1
- package/esm/video/render/auto-canvas.d.ts +26 -0
- package/esm/video/render/auto-canvas.d.ts.map +1 -0
- package/esm/video/render/auto-canvas.js +80 -0
- package/esm/video/render/auto-canvas.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 +18 -8
- package/esm/video/render/bitmap.js.map +1 -1
- package/esm/video/render/canvas.d.ts +38 -4
- package/esm/video/render/canvas.d.ts.map +1 -1
- package/esm/video/render/canvas.js +97 -10
- package/esm/video/render/canvas.js.map +1 -1
- package/esm/video/render/flow-control.d.ts +32 -0
- package/esm/video/render/flow-control.d.ts.map +1 -0
- package/esm/video/render/flow-control.js +139 -0
- package/esm/video/render/flow-control.js.map +1 -0
- package/esm/video/render/index.d.ts +2 -0
- package/esm/video/render/index.d.ts.map +1 -1
- package/esm/video/render/index.js +2 -0
- package/esm/video/render/index.js.map +1 -1
- package/esm/video/render/insertable-stream.d.ts +15 -3
- package/esm/video/render/insertable-stream.d.ts.map +1 -1
- package/esm/video/render/insertable-stream.js +29 -11
- package/esm/video/render/insertable-stream.js.map +1 -1
- package/esm/video/render/redraw.d.ts +31 -0
- package/esm/video/render/redraw.d.ts.map +1 -0
- package/esm/video/render/redraw.js +75 -0
- package/esm/video/render/redraw.js.map +1 -0
- package/esm/video/render/type.d.ts +8 -2
- package/esm/video/render/type.d.ts.map +1 -1
- package/esm/video/render/webgl.d.ts +38 -5
- package/esm/video/render/webgl.d.ts.map +1 -1
- package/esm/video/render/webgl.js +228 -59
- package/esm/video/render/webgl.js.map +1 -1
- package/esm/video/utils/index.d.ts +4 -0
- package/esm/video/utils/index.d.ts.map +1 -0
- package/esm/video/utils/index.js +4 -0
- package/esm/video/utils/index.js.map +1 -0
- package/esm/video/utils/snapshot.d.ts +2 -0
- package/esm/video/utils/snapshot.d.ts.map +1 -0
- package/esm/video/utils/snapshot.js +18 -0
- package/esm/video/utils/snapshot.js.map +1 -0
- package/esm/video/utils/timestamp.d.ts +19 -0
- package/esm/video/utils/timestamp.d.ts.map +1 -0
- package/esm/video/utils/timestamp.js +87 -0
- package/esm/video/utils/timestamp.js.map +1 -0
- package/esm/video/utils/video-decoder-stream.d.ts +30 -0
- package/esm/video/utils/video-decoder-stream.d.ts.map +1 -0
- package/esm/video/utils/video-decoder-stream.js +284 -0
- package/esm/video/utils/video-decoder-stream.js.map +1 -0
- package/package.json +12 -11
- package/src/video/codec/av1.ts +41 -131
- package/src/video/codec/h264.ts +15 -37
- package/src/video/codec/h265.ts +16 -45
- package/src/video/codec/h26x.ts +45 -96
- package/src/video/codec/index.ts +0 -1
- package/src/video/codec/type.ts +32 -12
- package/src/video/decoder.ts +203 -165
- package/src/video/index.ts +1 -0
- package/src/video/render/auto-canvas.ts +105 -0
- package/src/video/render/bitmap.ts +23 -9
- package/src/video/render/canvas.ts +157 -12
- package/src/video/render/flow-control.ts +182 -0
- package/src/video/render/index.ts +2 -0
- package/src/video/render/insertable-stream.ts +50 -10
- package/src/video/render/redraw.ts +88 -0
- package/src/video/render/type.ts +10 -2
- package/src/video/render/webgl.ts +316 -80
- package/src/video/utils/index.ts +3 -0
- package/src/video/utils/snapshot.ts +22 -0
- package/src/video/utils/timestamp.ts +108 -0
- package/src/video/utils/video-decoder-stream.ts +406 -0
- package/tsconfig.build.json +0 -14
- package/CHANGELOG.md +0 -140
- package/esm/video/codec/utils.d.ts +0 -4
- package/esm/video/codec/utils.d.ts.map +0 -1
- package/esm/video/codec/utils.js +0 -10
- package/esm/video/codec/utils.js.map +0 -1
- package/esm/video/snapshot.d.ts +0 -6
- package/esm/video/snapshot.d.ts.map +0 -1
- package/esm/video/snapshot.js +0 -41
- package/esm/video/snapshot.js.map +0 -1
- package/src/video/codec/utils.ts +0 -11
- package/src/video/snapshot.ts +0 -41
- package/tsconfig.build.tsbuildinfo +0 -1
package/src/video/decoder.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { ScrcpyVideoCodecId } from "@yume-chan/scrcpy";
|
|
1
|
+
import { pipe } from "@yume-chan/event";
|
|
2
|
+
import { ScrcpyVideoCodecId, ScrcpyVideoSizeImpl } from "@yume-chan/scrcpy";
|
|
4
3
|
import type {
|
|
5
4
|
ScrcpyVideoDecoder,
|
|
6
5
|
ScrcpyVideoDecoderCapability,
|
|
7
|
-
} from "@yume-chan/scrcpy-decoder-
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
} from "@yume-chan/scrcpy-decoder-shared";
|
|
7
|
+
import { ScrcpyVideoDecoderPauseController } from "@yume-chan/scrcpy-decoder-shared";
|
|
8
|
+
import { InspectStream } from "@yume-chan/stream-extra";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Av1TransformStream,
|
|
12
|
+
H264TransformStream,
|
|
13
|
+
H265TransformStream,
|
|
14
|
+
} from "./codec/index.js";
|
|
15
|
+
import type { CodecTransformStream } from "./codec/type.js";
|
|
14
16
|
import type { VideoFrameRenderer } from "./render/index.js";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import {
|
|
18
|
+
AutoCanvasRenderer,
|
|
19
|
+
BitmapVideoFrameRenderer,
|
|
20
|
+
RendererController,
|
|
21
|
+
} from "./render/index.js";
|
|
22
|
+
import { TimestampTransforms, VideoDecoderStream } from "./utils/index.js";
|
|
20
23
|
|
|
21
24
|
export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
22
25
|
static get isSupported() {
|
|
@@ -30,6 +33,8 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
|
30
33
|
av1: {},
|
|
31
34
|
};
|
|
32
35
|
|
|
36
|
+
// #region parameters
|
|
37
|
+
|
|
33
38
|
#codec: ScrcpyVideoCodecId;
|
|
34
39
|
get codec() {
|
|
35
40
|
return this.#codec;
|
|
@@ -39,214 +44,247 @@ export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
|
|
39
44
|
get renderer() {
|
|
40
45
|
return this.#renderer;
|
|
41
46
|
}
|
|
47
|
+
get rendererType() {
|
|
48
|
+
return this.#renderer.type;
|
|
49
|
+
}
|
|
50
|
+
get onRendererTypeChange() {
|
|
51
|
+
return this.#renderer.onTypeChanged;
|
|
52
|
+
}
|
|
42
53
|
|
|
43
|
-
#
|
|
54
|
+
// #endregion parameters
|
|
44
55
|
|
|
45
|
-
#
|
|
56
|
+
// #region pause controller
|
|
46
57
|
|
|
47
|
-
#
|
|
58
|
+
#pause = new ScrcpyVideoDecoderPauseController();
|
|
59
|
+
get paused() {
|
|
60
|
+
return this.#pause.paused;
|
|
61
|
+
}
|
|
48
62
|
get writable() {
|
|
49
|
-
return this.#writable;
|
|
63
|
+
return this.#pause.writable;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
#
|
|
53
|
-
#controller!: WritableStreamDefaultController;
|
|
66
|
+
// #endregion pause controller
|
|
54
67
|
|
|
55
|
-
#
|
|
56
|
-
#framesPresented = 0;
|
|
57
|
-
get framesRendered() {
|
|
58
|
-
return this.#framesPresented;
|
|
59
|
-
}
|
|
68
|
+
// #region size
|
|
60
69
|
|
|
61
|
-
#
|
|
62
|
-
get
|
|
63
|
-
return this.#
|
|
70
|
+
#size = new ScrcpyVideoSizeImpl();
|
|
71
|
+
get width() {
|
|
72
|
+
return this.#size.width;
|
|
73
|
+
}
|
|
74
|
+
get height() {
|
|
75
|
+
return this.#size.height;
|
|
64
76
|
}
|
|
65
|
-
|
|
66
|
-
#sizeChanged = new StickyEventEmitter<{ width: number; height: number }>();
|
|
67
77
|
get sizeChanged() {
|
|
68
|
-
return this.#sizeChanged
|
|
78
|
+
return this.#size.sizeChanged;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
// #endregion size
|
|
82
|
+
|
|
83
|
+
// #region raw decoder
|
|
84
|
+
|
|
85
|
+
#rawDecoder = new VideoDecoderStream();
|
|
86
|
+
get type() {
|
|
87
|
+
return this.#rawDecoder.hardwareAcceleration !== "prefer-software"
|
|
88
|
+
? "hardware"
|
|
89
|
+
: "software";
|
|
90
|
+
}
|
|
91
|
+
#onTypeChange = pipe(
|
|
92
|
+
this.#rawDecoder.onHardwareAccelerationChange,
|
|
93
|
+
(value) => (value !== "prefer-software" ? "hardware" : "software"),
|
|
94
|
+
);
|
|
95
|
+
get onTypeChange() {
|
|
96
|
+
return this.#onTypeChange;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Gets the number of frames waiting to be decoded.
|
|
100
|
+
*/
|
|
101
|
+
get decodeQueueSize() {
|
|
102
|
+
return this.#rawDecoder.decodeQueueSize;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Gets an event when a frame is dequeued (either decoded or discarded).
|
|
106
|
+
*/
|
|
107
|
+
get onDequeue() {
|
|
108
|
+
return this.#rawDecoder.onDequeue;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Gets the number of times the decoder has been reset to catch up new keyframes.
|
|
112
|
+
*/
|
|
113
|
+
get decoderResetCount() {
|
|
114
|
+
return this.#rawDecoder.decoderResetCount;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Gets the number of frames decoded by the decoder.
|
|
118
|
+
*/
|
|
119
|
+
get framesDecoded() {
|
|
120
|
+
return this.#rawDecoder.framesDecoded;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Gets the number of frames skipped by the decoder.
|
|
124
|
+
*/
|
|
125
|
+
get framesSkippedDecoding() {
|
|
126
|
+
return this.#rawDecoder.framesSkippedDecoding;
|
|
74
127
|
}
|
|
75
128
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
129
|
+
// #endregion raw decoder
|
|
130
|
+
|
|
131
|
+
#timestampTransforms = new TimestampTransforms();
|
|
132
|
+
/**
|
|
133
|
+
* Gets the total time spent processing and decoding frames in milliseconds.
|
|
134
|
+
*/
|
|
135
|
+
get totalDecodeTime() {
|
|
136
|
+
return this.#timestampTransforms.totalDecodeTime;
|
|
79
137
|
}
|
|
80
138
|
|
|
81
|
-
#
|
|
139
|
+
// #region renderer
|
|
82
140
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
141
|
+
#renderController = new RendererController();
|
|
142
|
+
/**
|
|
143
|
+
* Gets the number of frames that have been drawn on the renderer.
|
|
144
|
+
*/
|
|
145
|
+
get framesRendered() {
|
|
146
|
+
return this.#renderController.framesRendered;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Gets the number of frames that's visible to the user.
|
|
150
|
+
*
|
|
151
|
+
* Multiple frames might be rendered during one vertical sync interval,
|
|
152
|
+
* but only the last of them is represented to the user.
|
|
153
|
+
* This costs some performance but reduces latency by 1 frame.
|
|
154
|
+
*
|
|
155
|
+
* Might be `0` if the renderer is in a nested Web Worker on Chrome due to a Chrome bug.
|
|
156
|
+
* https://issues.chromium.org/issues/41483010
|
|
157
|
+
*/
|
|
158
|
+
get framesDisplayed() {
|
|
159
|
+
return this.#renderController.framesDisplayed;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Gets the number of frames that wasn't drawn on the renderer
|
|
163
|
+
* because the renderer can't keep up
|
|
164
|
+
*/
|
|
165
|
+
get framesSkippedRendering() {
|
|
166
|
+
return this.#renderController.framesSkippedRendering;
|
|
167
|
+
}
|
|
86
168
|
|
|
87
|
-
#
|
|
169
|
+
// #endregion renderer
|
|
88
170
|
|
|
89
171
|
/**
|
|
90
172
|
* Create a new WebCodecs video decoder.
|
|
91
173
|
*/
|
|
92
174
|
constructor({
|
|
93
175
|
codec,
|
|
94
|
-
renderer,
|
|
95
|
-
|
|
176
|
+
renderer = new AutoCanvasRenderer(),
|
|
177
|
+
hardwareAcceleration = "no-preference",
|
|
178
|
+
optimizeForLatency = true,
|
|
96
179
|
}: WebCodecsVideoDecoder.Options) {
|
|
97
180
|
this.#codec = codec;
|
|
98
181
|
this.#renderer = renderer;
|
|
99
|
-
this.#options = options;
|
|
100
|
-
|
|
101
|
-
this.#decoder = new VideoDecoder({
|
|
102
|
-
output: (frame) => {
|
|
103
|
-
this.#captureFrame?.close();
|
|
104
|
-
// PERF: `VideoFrame#clone` is cheap
|
|
105
|
-
this.#captureFrame = frame.clone();
|
|
106
|
-
|
|
107
|
-
if (this.#drawing) {
|
|
108
|
-
if (this.#nextFrame) {
|
|
109
|
-
this.#nextFrame.close();
|
|
110
|
-
this.#framesSkipped += 1;
|
|
111
|
-
}
|
|
112
|
-
this.#nextFrame = frame;
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
void this.#draw(frame);
|
|
117
|
-
},
|
|
118
|
-
error: (error) => {
|
|
119
|
-
this.#setError(error);
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
182
|
|
|
183
|
+
let codecTransform: CodecTransformStream;
|
|
123
184
|
switch (this.#codec) {
|
|
124
185
|
case ScrcpyVideoCodecId.H264:
|
|
125
|
-
|
|
126
|
-
this.#decoder,
|
|
127
|
-
this.#updateSize,
|
|
128
|
-
this.#options,
|
|
129
|
-
);
|
|
186
|
+
codecTransform = new H264TransformStream();
|
|
130
187
|
break;
|
|
131
188
|
case ScrcpyVideoCodecId.H265:
|
|
132
|
-
|
|
133
|
-
this.#decoder,
|
|
134
|
-
this.#updateSize,
|
|
135
|
-
this.#options,
|
|
136
|
-
);
|
|
189
|
+
codecTransform = new H265TransformStream();
|
|
137
190
|
break;
|
|
138
191
|
case ScrcpyVideoCodecId.AV1:
|
|
139
|
-
|
|
140
|
-
this.#decoder,
|
|
141
|
-
this.#updateSize,
|
|
142
|
-
this.#options,
|
|
143
|
-
);
|
|
192
|
+
codecTransform = new Av1TransformStream();
|
|
144
193
|
break;
|
|
145
194
|
default:
|
|
146
|
-
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
196
|
+
throw new Error(`Unsupported codec: ${this.#codec}`);
|
|
147
197
|
}
|
|
148
198
|
|
|
149
|
-
this.#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
void this.#pause.readable
|
|
200
|
+
// Add timestamp
|
|
201
|
+
.pipeThrough(this.#timestampTransforms.addTimestamp)
|
|
202
|
+
// Convert Scrcpy packets to `VideoDecoder` config/chunk
|
|
203
|
+
.pipeThrough(codecTransform)
|
|
204
|
+
// Insert extra `VideoDecoder` config and intercept size changes
|
|
205
|
+
.pipeThrough(
|
|
206
|
+
new InspectStream((chunk): undefined => {
|
|
207
|
+
if ("codec" in chunk) {
|
|
208
|
+
chunk.hardwareAcceleration = hardwareAcceleration;
|
|
209
|
+
chunk.optimizeForLatency = optimizeForLatency;
|
|
210
|
+
|
|
211
|
+
this.#size.setSize(chunk.codedWidth, chunk.codedHeight);
|
|
212
|
+
}
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
// Decode `VideoDecoder` config/chunk to `VideoFrame`s
|
|
216
|
+
.pipeThrough(this.#rawDecoder)
|
|
217
|
+
// Track decoding time and filter skipped frames
|
|
218
|
+
.pipeThrough(this.#timestampTransforms.consumeTimestamp)
|
|
219
|
+
// Skip frames if renderer can't keep up
|
|
220
|
+
.pipeThrough(this.#renderController)
|
|
221
|
+
// Render
|
|
222
|
+
.pipeTo(renderer.writable)
|
|
223
|
+
// Errors will be handled by source stream
|
|
224
|
+
.catch(() => {});
|
|
175
225
|
}
|
|
176
226
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.#drawing = true;
|
|
180
|
-
// PERF: Draw every frame to minimize latency at cost of performance.
|
|
181
|
-
// When multiple frames are drawn in one vertical sync interval,
|
|
182
|
-
// only the last one is visible to users.
|
|
183
|
-
// But this ensures users can always see the most up-to-date screen.
|
|
184
|
-
// This is also the behavior of official Scrcpy client.
|
|
185
|
-
// https://github.com/Genymobile/scrcpy/issues/3679
|
|
186
|
-
this.#updateSize(frame.displayWidth, frame.displayHeight);
|
|
187
|
-
await this.#renderer.draw(frame);
|
|
188
|
-
this.#framesDraw += 1;
|
|
189
|
-
frame.close();
|
|
190
|
-
|
|
191
|
-
if (this.#nextFrame) {
|
|
192
|
-
const frame = this.#nextFrame;
|
|
193
|
-
this.#nextFrame = undefined;
|
|
194
|
-
await this.#draw(frame);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
this.#drawing = false;
|
|
198
|
-
} catch (error) {
|
|
199
|
-
this.#setError(error as Error);
|
|
200
|
-
}
|
|
227
|
+
pause(): void {
|
|
228
|
+
this.#pause.pause();
|
|
201
229
|
}
|
|
202
230
|
|
|
203
|
-
|
|
204
|
-
this.#
|
|
205
|
-
|
|
206
|
-
this.#height = height;
|
|
207
|
-
this.#sizeChanged.fire({ width, height });
|
|
208
|
-
};
|
|
231
|
+
resume(): undefined {
|
|
232
|
+
this.#pause.resume();
|
|
233
|
+
}
|
|
209
234
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.#framesSkipped += this.#framesDraw - 1;
|
|
214
|
-
this.#framesDraw = 0;
|
|
215
|
-
}
|
|
216
|
-
this.#animationFrameId = requestAnimationFrame(
|
|
217
|
-
this.#handleAnimationFrame,
|
|
218
|
-
);
|
|
219
|
-
};
|
|
235
|
+
trackDocumentVisibility(document: Document): () => undefined {
|
|
236
|
+
return this.#pause.trackDocumentVisibility(document);
|
|
237
|
+
}
|
|
220
238
|
|
|
221
|
-
async snapshot() {
|
|
222
|
-
const frame = this.#captureFrame;
|
|
239
|
+
async snapshot(options?: ImageEncodeOptions) {
|
|
240
|
+
const frame = this.#renderController.captureFrame;
|
|
223
241
|
if (!frame) {
|
|
224
242
|
return undefined;
|
|
225
243
|
}
|
|
226
244
|
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
245
|
+
// First check if the renderer can provide a snapshot natively
|
|
246
|
+
const blob = await this.#renderer.snapshot?.(options);
|
|
247
|
+
if (blob) {
|
|
248
|
+
return blob;
|
|
249
|
+
}
|
|
232
250
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
// Create a BitmapVideoFrameRenderer to draw the frame
|
|
252
|
+
const renderer = new BitmapVideoFrameRenderer();
|
|
253
|
+
try {
|
|
254
|
+
const writer = renderer.writable.getWriter();
|
|
255
|
+
// Draw the frame
|
|
256
|
+
// `renderer` will close the passed in `frame`, so make a clone
|
|
257
|
+
await writer.write(frame.clone());
|
|
258
|
+
await writer.close();
|
|
259
|
+
// BitmapVideoFrameRenderer.snapshot will definitely return a value
|
|
260
|
+
return await renderer.snapshot(options);
|
|
261
|
+
} finally {
|
|
262
|
+
renderer.dispose();
|
|
237
263
|
}
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
dispose(): undefined {
|
|
267
|
+
// Most cleanup happens automatically when `writable` ends
|
|
268
|
+
// (in each stream's `close` callback).
|
|
269
|
+
// This method cleanup things that still available after `writable` ends
|
|
270
|
+
|
|
271
|
+
this.#pause.dispose();
|
|
272
|
+
this.#size.dispose();
|
|
273
|
+
this.#renderController.dispose();
|
|
274
|
+
this.#renderer.dispose?.();
|
|
240
275
|
}
|
|
241
276
|
}
|
|
242
277
|
|
|
243
278
|
export namespace WebCodecsVideoDecoder {
|
|
244
|
-
export interface Options extends
|
|
279
|
+
export interface Options extends Pick<
|
|
280
|
+
VideoDecoderConfig,
|
|
281
|
+
"hardwareAcceleration" | "optimizeForLatency"
|
|
282
|
+
> {
|
|
245
283
|
/**
|
|
246
284
|
* The video codec to decode
|
|
247
285
|
*/
|
|
248
286
|
codec: ScrcpyVideoCodecId;
|
|
249
287
|
|
|
250
|
-
renderer
|
|
288
|
+
renderer?: VideoFrameRenderer | undefined;
|
|
251
289
|
}
|
|
252
290
|
}
|
package/src/video/index.ts
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { StickyEventEmitter } from "@yume-chan/event";
|
|
2
|
+
import type { ScrcpyVideoDecoder } from "@yume-chan/scrcpy-decoder-shared";
|
|
3
|
+
import { createCanvas } from "@yume-chan/scrcpy-decoder-shared";
|
|
4
|
+
import { WritableStream } from "@yume-chan/stream-extra";
|
|
5
|
+
|
|
6
|
+
import { BitmapVideoFrameRenderer } from "./bitmap.js";
|
|
7
|
+
import type { CanvasVideoFrameRenderer } from "./canvas.js";
|
|
8
|
+
import type { VideoFrameRenderer } from "./type.js";
|
|
9
|
+
import { WebGLVideoFrameRenderer } from "./webgl.js";
|
|
10
|
+
|
|
11
|
+
export class AutoCanvasRenderer implements VideoFrameRenderer {
|
|
12
|
+
#inner!: CanvasVideoFrameRenderer;
|
|
13
|
+
#innerWriter!: WritableStreamDefaultWriter<VideoFrame>;
|
|
14
|
+
|
|
15
|
+
get type() {
|
|
16
|
+
return this.#inner.type;
|
|
17
|
+
}
|
|
18
|
+
#onTypeChanged = new StickyEventEmitter<ScrcpyVideoDecoder.RendererType>();
|
|
19
|
+
get onTypeChanged() {
|
|
20
|
+
return this.#onTypeChanged.event;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#canvas!: HTMLCanvasElement | OffscreenCanvas;
|
|
24
|
+
/**
|
|
25
|
+
* Gets the canvas used by the renderer.
|
|
26
|
+
*
|
|
27
|
+
* The `canvas` property might change when the renderer switches between WebGL and 2D rendering.
|
|
28
|
+
* Specify a `createCanvas` option, or listen to the `onTypeChanged` event to handle the canvas change.
|
|
29
|
+
*/
|
|
30
|
+
get canvas() {
|
|
31
|
+
return this.#canvas;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#lastFrame: VideoFrame | undefined;
|
|
35
|
+
#writableController!: WritableStreamDefaultController;
|
|
36
|
+
#writable = new WritableStream<VideoFrame>({
|
|
37
|
+
start: (controller) => {
|
|
38
|
+
this.#writableController = controller;
|
|
39
|
+
},
|
|
40
|
+
write: async (frame) => {
|
|
41
|
+
this.#lastFrame?.close();
|
|
42
|
+
this.#lastFrame = frame.clone();
|
|
43
|
+
|
|
44
|
+
await this.#innerWriter.write(frame);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
get writable() {
|
|
48
|
+
return this.#writable;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
constructor(options?: AutoCanvasRenderer.Options) {
|
|
52
|
+
this.#createInner(options, WebGLVideoFrameRenderer.isSupported);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#createInner(
|
|
56
|
+
options: AutoCanvasRenderer.Options | undefined,
|
|
57
|
+
webgl: boolean,
|
|
58
|
+
) {
|
|
59
|
+
this.#canvas = options?.createCanvas?.() ?? createCanvas();
|
|
60
|
+
if (webgl) {
|
|
61
|
+
const webgl = new WebGLVideoFrameRenderer({
|
|
62
|
+
...options,
|
|
63
|
+
canvas: this.#canvas,
|
|
64
|
+
});
|
|
65
|
+
webgl.onContextLost(() => {
|
|
66
|
+
this.#createInner(options, false);
|
|
67
|
+
if (this.#lastFrame) {
|
|
68
|
+
this.#innerWriter
|
|
69
|
+
.write(this.#lastFrame.clone())
|
|
70
|
+
.catch((e) => this.#writableController.error(e));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
this.#inner = webgl;
|
|
74
|
+
} else {
|
|
75
|
+
this.#inner = new BitmapVideoFrameRenderer({
|
|
76
|
+
...options,
|
|
77
|
+
canvas: this.#canvas,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
this.#innerWriter = this.#inner.writable.getWriter();
|
|
81
|
+
this.#onTypeChanged.fire(this.#inner.type);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
snapshot(options?: ImageEncodeOptions): Promise<Blob | undefined> {
|
|
85
|
+
return this.#inner.snapshot(options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
dispose() {
|
|
89
|
+
this.#onTypeChanged.dispose();
|
|
90
|
+
|
|
91
|
+
this.#lastFrame?.close();
|
|
92
|
+
this.#lastFrame = undefined;
|
|
93
|
+
|
|
94
|
+
return this.#inner.dispose();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export namespace AutoCanvasRenderer {
|
|
99
|
+
export interface Options extends Omit<
|
|
100
|
+
WebGLVideoFrameRenderer.Options,
|
|
101
|
+
"canvas"
|
|
102
|
+
> {
|
|
103
|
+
createCanvas?: () => HTMLCanvasElement | OffscreenCanvas;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import { CanvasVideoFrameRenderer } from "./canvas.js";
|
|
2
2
|
|
|
3
3
|
export class BitmapVideoFrameRenderer extends CanvasVideoFrameRenderer {
|
|
4
|
+
override get type() {
|
|
5
|
+
return "software" as const;
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
#context: ImageBitmapRenderingContext;
|
|
5
9
|
|
|
6
|
-
constructor(
|
|
7
|
-
super(
|
|
10
|
+
constructor(options?: CanvasVideoFrameRenderer.Options) {
|
|
11
|
+
super(async (frame) => {
|
|
12
|
+
const bitmap = await createImageBitmap(frame);
|
|
13
|
+
this.#context.transferFromImageBitmap(bitmap);
|
|
14
|
+
bitmap.close();
|
|
15
|
+
}, options);
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
const context = (this.canvas as HTMLCanvasElement).getContext(
|
|
18
|
+
"bitmaprenderer",
|
|
19
|
+
{
|
|
20
|
+
// Avoid alpha:false, which can be expensive
|
|
21
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#avoid_alphafalse_which_can_be_expensive
|
|
22
|
+
alpha: true,
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
if (!context) {
|
|
26
|
+
// This can happen if the canvas already has a context of a different type
|
|
27
|
+
throw new Error("Failed to create rendering context");
|
|
28
|
+
}
|
|
13
29
|
|
|
14
|
-
|
|
15
|
-
const bitmap = await createImageBitmap(frame);
|
|
16
|
-
this.#context.transferFromImageBitmap(bitmap);
|
|
30
|
+
this.#context = context;
|
|
17
31
|
}
|
|
18
32
|
}
|