Tiny H264 decoder
Decode and render H.264 streams in Web browsers using TinyH264, the (now deprecated and removed) Android H.264 software decoder.
It's slow, and only supports H.264 main profile at level 4, but works on most browsers.
- npm
- Yarn
- pnpm
npm install @yume-chan/scrcpy-decoder-tinyh264
yarn add @yume-chan/scrcpy-decoder-tinyh264
pnpm add @yume-chan/scrcpy-decoder-tinyh264
Performance
There are two aspects of performance:
Decoding
tinyh264
package is used for decoding, which compiles the C code into WebAssembly, and runs it in a Web Worker. This way, the main thread is not blocked by the decoding process.
Because H.264 is a complex codec, decoding it is CPU-intensive. It might not be able to keep up with the video frame rate on low-end devices.
Rendering
Tiny H264 decoder outputs raw YUV frames, which needs to be converted to RGB for rendering.
yuv-canvas
package is used to do the conversion and rendering. When supported, it uses a WebGL shader to accelerate the conversion, so it's very fast. But on unsupported devices, it falls back to a software implementation, which is super slow.
Limit profile/level
Because it only supports H.264 Baseline level 4 codec, but many newer devices default to higher profiles/levels, you must limit it using the codecOptions
option:
import { TinyH264Decoder } from "@yume-chan/scrcpy-decoder-tinyh264";
const H264Capabilities = TinyH264Decoder.capabilities.h264;
const options = new ScrcpyOptions1_24({
// other options...
codecOptions: new CodecOptions({
profile: H264Capabilities.maxProfile,
level: H264Capabilities.maxLevel,
}),
});
However, it will fail on some very old devices that doesn't even support Baseline level 4 codec. If that happens, You can retry starting the server without the codecOptions
option.
try {
await startServer(
new ScrcpyOptions1_24({
// other options...
codecOptions: new CodecOptions({
profile: H264Capabilities.maxProfile,
level: H264Capabilities.maxLevel,
}),
})
);
} catch (e) {
await startServer(
new ScrcpyOptions1_24({
// other options...
})
);
}
Usage
import { TinyH264Decoder } from "@yume-chan/scrcpy-decoder-tinyh264";
const decoder = new TinyH264Decoder();
document.body.appendChild(decoder.renderer);
videoPacketStream // from previous step
.pipeTo(decoder.writable)
.catch(() => {});
The renderer
field is a HTMLCanvasElement
that displays the video frames. You can append it to any element in the DOM.
Handle size changes
When the device orientation changes, Scrcpy server will recreate a new video encoder with the new size. The decoder can handle this automatically, and update the canvas size.
However, the video size is also useful for other purposes, like injecting touch events. The sizeChanged
event will be emitted when the video size changes:
decoder.sizeChanged(({ width, height }) => {
console.log(width, height);
});
Rendering metrics
The decoder will try to render every frame when it arrives, to minimize latency.
However, when the video frame rate is higher than the display refresh rate, or when the hardware can't keep up, some frames will be dropped.
The framesRendered
and framesSkipped
fields provides accumulated rendering metrics:
setInterval(() => {
console.log(decoder.framesRendered, decoder.framesSkipped);
}, 1000);