| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- import type { H264Configuration, 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: number = 0;
- public constructor() {
- this._renderer = document.createElement('canvas');
- this.context = this._renderer.getContext('2d', { willReadFrequently: true })!;
- 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() { },
- });
- this._writable = new WritableStream<ScrcpyVideoStreamPacket>({
- write: async (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();
- }
- }
|