@yume-chan/scrcpy-decoder-tinyh264
Version:
Raw H.264 stream decoder and renderer wrapping TinyH264 and YUVCanvas packages (software decoding).
203 lines (176 loc) • 6.3 kB
text/typescript
import { PromiseResolver } from "@yume-chan/async";
import { StickyEventEmitter } from "@yume-chan/event";
import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy";
import {
AndroidAvcLevel,
AndroidAvcProfile,
h264ParseConfiguration,
} from "@yume-chan/scrcpy";
import { WritableStream } from "@yume-chan/stream-extra";
import YuvBuffer from "yuv-buffer";
import YuvCanvas from "yuv-canvas";
import type {
ScrcpyVideoDecoder,
ScrcpyVideoDecoderCapability,
} from "./types.js";
import type { TinyH264Wrapper } from "./wrapper.js";
import { createTinyH264Wrapper } from "./wrapper.js";
const noop = () => {
// no-op
};
export function createCanvas() {
if (typeof document !== "undefined") {
return document.createElement("canvas");
}
if (typeof OffscreenCanvas !== "undefined") {
return new OffscreenCanvas(1, 1);
}
throw new Error("no canvas input found nor any canvas can be created");
}
export class TinyH264Decoder implements ScrcpyVideoDecoder {
static readonly capabilities: Record<string, ScrcpyVideoDecoderCapability> =
{
h264: {
maxProfile: AndroidAvcProfile.Baseline,
maxLevel: AndroidAvcLevel.Level4,
},
};
get renderer() {
return this.
}
get sizeChanged() {
return this.
}
get width() {
return this.
}
get height() {
return this.
}
get framesRendered() {
return this.
}
get framesSkipped() {
return this.
}
get writable() {
return this.
}
constructor({ canvas }: TinyH264Decoder.Options = {}) {
if (canvas) {
this.
} else {
this.
}
this.
write: async (packet) => {
switch (packet.type) {
case "configuration":
await this.
break;
case "data": {
if (!this.
throw new Error("Decoder not configured");
}
const wrapper = await this.
wrapper.feed(packet.data.slice().buffer);
break;
}
}
},
});
}
async
this.dispose();
this.
if (!this.
// yuv-canvas detects WebGL support by creating a <canvas> itself
// not working in worker
const canvas = createCanvas();
const attributes: WebGLContextAttributes = {
// Disallow software rendering.
// Other rendering methods are faster than software-based WebGL.
failIfMajorPerformanceCaveat: true,
};
const gl =
canvas.getContext("webgl2", attributes) ||
canvas.getContext("webgl", attributes);
this.
webGL: !!gl,
});
}
const {
encodedWidth,
encodedHeight,
croppedWidth,
croppedHeight,
cropLeft,
cropTop,
} = h264ParseConfiguration(data);
this.
this.
this.
width: croppedWidth,
height: croppedHeight,
});
// H.264 Baseline profile only supports YUV 420 pixel format
// So chroma width/height is each half of video width/height
const chromaWidth = encodedWidth / 2;
const chromaHeight = encodedHeight / 2;
// YUVCanvas will set canvas size when format changes
const format = YuvBuffer.format({
width: encodedWidth,
height: encodedHeight,
chromaWidth,
chromaHeight,
cropLeft: cropLeft,
cropTop: cropTop,
cropWidth: croppedWidth,
cropHeight: croppedHeight,
displayWidth: croppedWidth,
displayHeight: croppedHeight,
});
const wrapper = await createTinyH264Wrapper();
this.
const uPlaneOffset = encodedWidth * encodedHeight;
const vPlaneOffset = uPlaneOffset + chromaWidth * chromaHeight;
wrapper.onPictureReady(({ data }) => {
this.
const array = new Uint8Array(data);
const frame = YuvBuffer.frame(
format,
YuvBuffer.lumaPlane(format, array, encodedWidth, 0),
YuvBuffer.chromaPlane(format, array, chromaWidth, uPlaneOffset),
YuvBuffer.chromaPlane(format, array, chromaWidth, vPlaneOffset),
);
this.
});
wrapper.feed(data.slice().buffer);
}
dispose(): void {
this.
.then((wrapper) => wrapper.dispose())
// NOOP: It's disposed so nobody cares about the error
.catch(noop);
this.
}
}
export namespace TinyH264Decoder {
export interface Options {
/**
* Optional render target canvas element or offscreen canvas.
* If not provided, a new `<canvas>` (when DOM is available)
* or a `OffscreenCanvas` will be created.
*/
canvas?: HTMLCanvasElement | OffscreenCanvas | undefined;
}
}