This commit is contained in:
eric
2026-03-29 02:57:08 -05:00
parent ef87c43b33
commit e3ac026710
15 changed files with 799 additions and 222 deletions

View File

@@ -0,0 +1,169 @@
/**
* 解析 scrcpy raw_streamAVCC4 字节大端长度 + NAL并用 WebCodecs 画到 Canvas。
* 与官方 scrcpy / escrcpy 设备端编码一致;浏览器需 Chromium 系且支持 VideoDecoderH.264)。
*/
function concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array {
const o = new Uint8Array(a.length + b.length);
o.set(a, 0);
o.set(b, a.length);
return o;
}
function readU32be(b: Uint8Array, o: number): number {
return (b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3];
}
function buildAvcC(sps: Uint8Array, pps: Uint8Array): Uint8Array {
const spsBody = sps.subarray(1);
const ppsBody = pps.subarray(1);
const out = new Uint8Array(11 + spsBody.length + 3 + ppsBody.length);
let p = 0;
out[p++] = 1;
out[p++] = sps[1];
out[p++] = sps[2];
out[p++] = sps[3];
out[p++] = 0xfc | 3;
out[p++] = 0xe0 | 1;
out[p++] = (spsBody.length >> 8) & 0xff;
out[p++] = spsBody.length & 0xff;
out.set(spsBody, p);
p += spsBody.length;
out[p++] = 1;
out[p++] = (ppsBody.length >> 8) & 0xff;
out[p++] = ppsBody.length & 0xff;
out.set(ppsBody, p);
return out;
}
function codecStringFromSps(sps: Uint8Array): string {
const h = (n: number) => n.toString(16).padStart(2, "0").toUpperCase();
return `avc1.${h(sps[1])}${h(sps[2])}${h(sps[3])}`;
}
export class ScrcpyH264Decoder {
private buf: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
private decoder: VideoDecoder | null = null;
private configured = false;
private configuring = false;
private ts = 0;
private sps: Uint8Array | null = null;
private pps: Uint8Array | null = null;
constructor(
private readonly canvas: HTMLCanvasElement,
private readonly onDecodeError: (msg: string) => void,
) {}
reset(): void {
this.buf = new Uint8Array(0);
this.sps = null;
this.pps = null;
this.configured = false;
this.configuring = false;
this.ts = 0;
if (this.decoder) {
try {
this.decoder.close();
} catch {
/* ignore */
}
this.decoder = null;
}
}
push(chunk: ArrayBuffer): void {
const add = new Uint8Array(chunk.byteLength);
add.set(new Uint8Array(chunk));
this.buf = concatBuffers(this.buf, add);
this.drainAvcc();
}
private drainAvcc(): void {
const b = this.buf;
let o = 0;
while (o + 4 <= b.length) {
const len = readU32be(b, o);
if (len <= 0 || len > 6 * 1024 * 1024) {
this.buf = new Uint8Array(b.subarray(o + 1));
o = 0;
continue;
}
if (o + 4 + len > b.length) break;
const nal = b.subarray(o + 4, o + 4 + len);
o += 4 + len;
this.handleNal(nal);
}
if (o > 0) this.buf = new Uint8Array(b.subarray(o));
}
private handleNal(nal: Uint8Array): void {
if (nal.length < 2) return;
const t = nal[0] & 0x1f;
if (t === 7) this.sps = new Uint8Array(nal);
else if (t === 8) this.pps = new Uint8Array(nal);
if (this.sps && this.pps && !this.configured && !this.configuring) {
void this.configure();
}
if (!this.configured || !this.decoder) return;
if (t !== 1 && t !== 5 && t !== 7 && t !== 8) return;
if (t === 7 || t === 8) return;
const key = t === 5;
const data = new Uint8Array(4 + nal.length);
new DataView(data.buffer).setUint32(0, nal.length, false);
data.set(nal, 4);
try {
const chunk = new EncodedVideoChunk({
type: key ? "key" : "delta",
timestamp: this.ts,
data,
});
this.ts += 33_333;
this.decoder.decode(chunk);
} catch (e) {
this.onDecodeError((e as Error).message || String(e));
}
}
private async configure(): Promise<void> {
if (!this.sps || !this.pps || this.configured || this.configuring) return;
this.configuring = true;
try {
const avcc = buildAvcC(this.sps, this.pps);
const codec = codecStringFromSps(this.sps);
const dec = new VideoDecoder({
output: (frame) => {
const ctx = this.canvas.getContext("2d");
if (!ctx) {
frame.close();
return;
}
if (this.canvas.width !== frame.displayWidth || this.canvas.height !== frame.displayHeight) {
this.canvas.width = frame.displayWidth;
this.canvas.height = frame.displayHeight;
}
ctx.drawImage(frame, 0, 0);
frame.close();
},
error: (e) => this.onDecodeError(String((e as DOMException).message || e)),
});
await dec.configure({
codec,
description: avcc,
optimizeForLatency: true,
} as VideoDecoderConfig);
this.decoder = dec;
this.configured = true;
} catch (e) {
this.onDecodeError((e as Error).message || String(e));
} finally {
this.configuring = false;
}
}
}
export function webCodecsH264Supported(): boolean {
return typeof VideoDecoder !== "undefined" && typeof VideoFrame !== "undefined";
}