/** * 解析 scrcpy raw_stream(AVCC:4 字节大端长度 + NAL)并用 WebCodecs 画到 Canvas。 * 与官方 scrcpy / escrcpy 设备端编码一致;浏览器需 Chromium 系且支持 VideoDecoder(H.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 = 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 { 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"; }