Reputation: 821
I want to play back a raw .h264 stream using the Webcodecs API and extract specific frames. It is saved as a .h264 file, essentially just the video data before being muxed into a .mp4 container format.
It should be possible to play this back using the new WebCodecs API. I fail at providing the avc1.64001e
VideoDecoder with the properly formatted EncodedVideoChunk. How can I do so? I know the resolution, framerate and frame count, but how do I take this information and create an EncodedVideoChunk from that?
I have a potentially gigabytes big local file, which I receive via:
const readableStream = this.data.file.stream();
const reader = readableStream.getReader();
How to feed this into VideoDecoder: decode()
?
I know you can let mp4box.js
handle the demuxing and providing of chunks. But I have the data already demuxed. So how do I overcome this step, besides muxing and feeding it through mp4box.js
, only to have it demux again?
Upvotes: 1
Views: 821
Reputation: 11963
In order to easily decode it is convenient to split stream grouping SPS + PPS + IDR, this could be done with :
for (let i = 0; i < buffer.length; i++) {
if (buffer[i] === 0 && buffer[i+1] === 0 && buffer[i+2] === 0 && buffer[i+3] === 1 && (buffer[i+4]&0x1f !== 7) && (buffer[i+4]&0x1f) !== 8) {
if (i !== start) {
// process a frame
}
start = i;
}
}
A simple decoding, with minimal error case management, setting type and codec to dummy values, using annexb will let VideoDecoder to overide values with frame content, could be :
async function process(data) {
if (decoder.state !== "configured") {
await decoder.configure({ codec: "avc1.64001e" });
}
const chunk = new EncodedVideoChunk({
timestamp: (performance.now() + performance.timeOrigin) * 1000,
type: "key",
data,
});
return decoder.decode(chunk);
}
Putting all together:
const decoder = new VideoDecoder({
output: (frame) => {
canvas.getContext("2d").drawImage(frame, 0, 0, frame.displayWidth, frame.displayHeight);
frame.close();
},
error: (e) => console.warn(e.message),
})
async function process(data) {
if (decoder.state !== "configured") {
const config = { codec: "avc1.4d002a" };
await decoder.configure(config);
}
const chunk = new EncodedVideoChunk({
timestamp: (performance.now() + performance.timeOrigin) * 1000,
type: "key",
data,
});
return decoder.decode(chunk);
}
function load() {
const file = document.getElementById("file").files[0];
const stream = file.stream();
const reader = stream.getReader();
let buffer = new Uint8Array();
return reader.read().then(async function processChunk({done, value}) {
if (done) {
return process(buffer);
} else {
buffer = new Uint8Array([...buffer, ...value]);
let start = 0;
for (let i = 0; i < buffer.length; i++) {
if (buffer[i] === 0 && buffer[i+1] === 0 && buffer[i+2] === 0 && buffer[i+3] === 1 && (buffer[i+4]&0x1f !== 7) && (buffer[i+4]&0x1f) !== 8) {
if (i !== start) {
const frame = buffer.slice(start, i);
await process(frame);
}
start = i;
}
}
buffer = buffer.slice(start);
return reader.read().then(processChunk);
}
});
}
<input type="file" id="file" onchange="load()" />
<canvas id="canvas"></canvas>
Upvotes: 1