ya-webadb/libraries/scrcpy-decoder-webcodecs/src/index.ts
2022-12-22 01:42:24 +08:00

114 lines
3.4 KiB
TypeScript

import {
type H264Configuration,
type ScrcpyVideoStreamPacket,
} from "@yume-chan/scrcpy";
import { WritableStream } from "@yume-chan/stream-extra";
function toHex(value: number) {
return value.toString(16).padStart(2, "0").toUpperCase();
}
export class WebCodecsDecoder {
// Usually, browsers can decode most configurations,
// So let device choose best profile and level for itself.
public readonly maxProfile = undefined;
public readonly maxLevel = undefined;
private _writable: WritableStream<ScrcpyVideoStreamPacket>;
public get writable() {
return this._writable;
}
private _renderer: HTMLCanvasElement;
public get renderer() {
return this._renderer;
}
private _frameRendered = 0;
public get frameRendered() {
return this._frameRendered;
}
private context: CanvasRenderingContext2D;
private decoder: VideoDecoder;
// Limit FPS to system refresh rate
private lastFrame: VideoFrame | undefined;
private animationFrame = 0;
public constructor() {
this._renderer = document.createElement("canvas");
this.context = this._renderer.getContext("2d")!;
this.decoder = new VideoDecoder({
output: (frame) => {
if (this.lastFrame) {
this.lastFrame.close();
}
this.lastFrame = frame;
if (!this.animationFrame) {
// Start render loop on first frame
this.render();
}
},
error(e) {
void e;
},
});
this._writable = new WritableStream<ScrcpyVideoStreamPacket>({
write: (packet) => {
switch (packet.type) {
case "configuration":
this.configure(packet.data);
break;
case "frame":
this.decoder.decode(
new EncodedVideoChunk({
// Treat `undefined` as `key`, otherwise won't decode.
type:
packet.keyframe === false ? "delta" : "key",
timestamp: 0,
data: packet.data,
})
);
break;
}
},
});
}
private render = () => {
if (this.lastFrame) {
this._frameRendered += 1;
this.context.drawImage(this.lastFrame, 0, 0);
this.lastFrame.close();
this.lastFrame = undefined;
}
this.animationFrame = requestAnimationFrame(this.render);
};
private configure(config: H264Configuration) {
const { profileIndex, constraintSet, levelIndex } = config;
this._renderer.width = config.croppedWidth;
this._renderer.height = config.croppedHeight;
// https://www.rfc-editor.org/rfc/rfc6381#section-3.3
// ISO Base Media File Format Name Space
const codec = `avc1.${[profileIndex, constraintSet, levelIndex]
.map(toHex)
.join("")}`;
this.decoder.configure({
codec: codec,
optimizeForLatency: true,
});
}
public dispose() {
cancelAnimationFrame(this.animationFrame);
this.decoder.close();
}
}