Skip to content

Commit db2f96c

Browse files
authored
feat(webcodecs): reset the decoder on every keyframe (#826)
1 parent 0236f2e commit db2f96c

4 files changed

Lines changed: 114 additions & 34 deletions

File tree

libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export class Av1Codec implements CodecDecoder {
99
#updateSize: (width: number, height: number) => void;
1010
#options: CodecDecoderOptions | undefined;
1111

12+
#config: VideoDecoderConfig | undefined;
13+
#configured = false;
14+
1215
constructor(
1316
decoder: VideoDecoder,
1417
updateSize: (width: number, height: number) => void,
@@ -19,7 +22,7 @@ export class Av1Codec implements CodecDecoder {
1922
this.#options = options;
2023
}
2124

22-
#configure(data: Uint8Array) {
25+
#parseConfig(data: Uint8Array) {
2326
const parser = new Av1(data);
2427
const sequenceHeader = parser.searchSequenceHeaderObu();
2528

@@ -80,24 +83,49 @@ export class Av1Codec implements CodecDecoder {
8083
decimalTwoDigits(matrixCoefficients),
8184
colorRange ? "1" : "0",
8285
].join(".");
83-
this.#decoder.configure({
86+
this.#config = {
8487
codec,
8588
hardwareAcceleration:
8689
this.#options?.hardwareAcceleration ?? "no-preference",
8790
optimizeForLatency: true,
88-
});
91+
};
92+
this.#configured = false;
8993
}
9094

9195
decode(packet: ScrcpyMediaStreamPacket): void {
9296
if (packet.type === "configuration") {
9397
return;
9498
}
9599

96-
this.#configure(packet.data);
100+
this.#parseConfig(packet.data);
101+
102+
if (!this.#config) {
103+
throw new Error("Decoder not configured");
104+
}
105+
106+
if (packet.keyframe) {
107+
if (this.#decoder.decodeQueueSize) {
108+
// If the device is too slow to decode all frames,
109+
// discard queued frames when next keyframe arrives.
110+
// (can only do this for keyframes because decoding must start from a keyframe)
111+
// This limits the maximum latency to 1 keyframe interval
112+
// (60 frames by default).
113+
this.#decoder.reset();
114+
115+
// `reset` also resets the decoder configuration
116+
// so we need to re-configure it again.
117+
this.#decoder.configure(this.#config);
118+
this.#configured = true;
119+
} else if (!this.#configured) {
120+
this.#decoder.configure(this.#config);
121+
this.#configured = true;
122+
}
123+
}
124+
97125
this.#decoder.decode(
98126
new EncodedVideoChunk({
99-
// Treat `undefined` as `key`, otherwise it won't decode.
100-
type: packet.keyframe === false ? "delta" : "key",
127+
// AV1 requires Scrcpy 2.0 where `keyframe` flag must be set
128+
type: packet.keyframe! ? "key" : "delta",
101129
timestamp: 0,
102130
data: packet.data,
103131
}),

libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { CodecDecoderOptions } from "./type.js";
55
import { hexTwoDigits } from "./utils.js";
66

77
export class H264Decoder extends H26xDecoder {
8-
#decoder: VideoDecoder;
98
#updateSize: (width: number, height: number) => void;
109
#options: CodecDecoderOptions | undefined;
1110

@@ -15,12 +14,11 @@ export class H264Decoder extends H26xDecoder {
1514
options?: CodecDecoderOptions,
1615
) {
1716
super(decoder);
18-
this.#decoder = decoder;
1917
this.#updateSize = updateSize;
2018
this.#options = options;
2119
}
2220

23-
override configure(data: Uint8Array): void {
21+
override configure(data: Uint8Array): VideoDecoderConfig {
2422
const {
2523
profileIndex,
2624
constraintSet,
@@ -38,11 +36,11 @@ export class H264Decoder extends H26xDecoder {
3836
hexTwoDigits(profileIndex) +
3937
hexTwoDigits(constraintSet) +
4038
hexTwoDigits(levelIndex);
41-
this.#decoder.configure({
39+
return {
4240
codec: codec,
4341
hardwareAcceleration:
4442
this.#options?.hardwareAcceleration ?? "no-preference",
4543
optimizeForLatency: true,
46-
});
44+
};
4745
}
4846
}

libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { CodecDecoderOptions } from "./type.js";
66
import { hexDigits } from "./utils.js";
77

88
export class H265Decoder extends H26xDecoder {
9-
#decoder: VideoDecoder;
109
#updateSize: (width: number, height: number) => void;
1110
#options: CodecDecoderOptions | undefined;
1211

@@ -16,12 +15,11 @@ export class H265Decoder extends H26xDecoder {
1615
options?: CodecDecoderOptions,
1716
) {
1817
super(decoder);
19-
this.#decoder = decoder;
2018
this.#updateSize = updateSize;
2119
this.#options = options;
2220
}
2321

24-
override configure(data: Uint8Array): void {
22+
override configure(data: Uint8Array): VideoDecoderConfig {
2523
const {
2624
generalProfileSpace,
2725
generalProfileIndex,
@@ -43,14 +41,14 @@ export class H265Decoder extends H26xDecoder {
4341
(generalTierFlag ? "H" : "L") + generalLevelIndex.toString(),
4442
...Array.from(generalConstraintSet, hexDigits),
4543
].join(".");
46-
this.#decoder.configure({
44+
return {
4745
codec,
4846
// Microsoft Edge requires explicit size to work
4947
codedWidth: croppedWidth,
5048
codedHeight: croppedHeight,
5149
hardwareAcceleration:
5250
this.#options?.hardwareAcceleration ?? "no-preference",
5351
optimizeForLatency: true,
54-
});
52+
};
5553
}
5654
}

libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,100 @@
1-
import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
1+
import type {
2+
ScrcpyMediaStreamDataPacket,
3+
ScrcpyMediaStreamPacket,
4+
} from "@yume-chan/scrcpy";
25

36
import type { CodecDecoder } from "./type.js";
47

58
export abstract class H26xDecoder implements CodecDecoder {
6-
#config: Uint8Array | undefined;
79
#decoder: VideoDecoder;
810

11+
#config: (VideoDecoderConfig & { raw: Uint8Array }) | undefined;
12+
#configured = false;
13+
914
constructor(decoder: VideoDecoder) {
1015
this.#decoder = decoder;
1116
}
1217

13-
abstract configure(data: Uint8Array): void;
18+
abstract configure(data: Uint8Array): VideoDecoderConfig;
1419

15-
decode(packet: ScrcpyMediaStreamPacket): void {
16-
if (packet.type === "configuration") {
17-
this.#config = packet.data;
18-
this.configure(packet.data);
19-
return;
20-
}
20+
#configureAndDecodeFirstKeyframe(
21+
config: VideoDecoderConfig & { raw: Uint8Array },
22+
packet: ScrcpyMediaStreamDataPacket,
23+
) {
24+
this.#decoder.configure(config);
25+
this.#configured = true;
2126

2227
// For H.264 and H.265, when the stream is in Annex B format
2328
// (which Scrcpy uses, as Android MediaCodec produces),
2429
// configuration data needs to be combined with the first frame data.
2530
// https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type
26-
let data: Uint8Array;
27-
if (this.#config !== undefined) {
28-
data = new Uint8Array(this.#config.length + packet.data.length);
29-
data.set(this.#config, 0);
30-
data.set(packet.data, this.#config.length);
31-
this.#config = undefined;
32-
} else {
33-
data = packet.data;
31+
const { raw } = config;
32+
const data = new Uint8Array(raw.length + packet.data.length);
33+
data.set(raw, 0);
34+
data.set(packet.data, raw.length);
35+
36+
this.#decoder.decode(
37+
new EncodedVideoChunk({
38+
type: "key",
39+
timestamp: 0,
40+
data,
41+
}),
42+
);
43+
}
44+
45+
decode(packet: ScrcpyMediaStreamPacket): void {
46+
if (packet.type === "configuration") {
47+
this.#config = {
48+
...this.configure(packet.data),
49+
raw: packet.data,
50+
};
51+
this.#configured = false;
52+
return;
53+
}
54+
55+
if (!this.#config) {
56+
throw new Error("Decoder not configured");
57+
}
58+
59+
if (packet.keyframe) {
60+
if (this.#decoder.decodeQueueSize) {
61+
// If the device is too slow to decode all frames,
62+
// discard queued frames when next keyframe arrives.
63+
// (can only do this for keyframes because decoding must start from a keyframe)
64+
// This limits the maximum latency to 1 keyframe interval
65+
// (60 frames by default).
66+
this.#decoder.reset();
67+
68+
// `reset` also resets the decoder configuration
69+
// so we need to re-configure it again.
70+
this.#configureAndDecodeFirstKeyframe(this.#config, packet);
71+
return;
72+
}
73+
74+
if (!this.#configured) {
75+
this.#configureAndDecodeFirstKeyframe(this.#config, packet);
76+
return;
77+
}
78+
}
79+
80+
if (!this.#configured) {
81+
if (packet.keyframe === undefined) {
82+
// Scrcpy <1.23 doesn't send `keyframe` flag
83+
// Infer the first frame after configuration as keyframe
84+
// (`VideoDecoder` will throw error if it's not)
85+
this.#configureAndDecodeFirstKeyframe(this.#config, packet);
86+
return;
87+
}
88+
89+
throw new Error("Expect a keyframe but got a delta frame");
3490
}
3591

3692
this.#decoder.decode(
3793
new EncodedVideoChunk({
3894
// Treat `undefined` as `key`, otherwise won't decode.
3995
type: packet.keyframe === false ? "delta" : "key",
4096
timestamp: 0,
41-
data,
97+
data: packet.data,
4298
}),
4399
);
44100
}

0 commit comments

Comments
 (0)