Sanitize initial HEVC packet to avoid Chromium's validation logic from tripping up (#314)

This commit is contained in:
Vanilagy
2026-04-29 11:37:56 +02:00
parent 06a89ed085
commit 2d49122277
3 changed files with 141 additions and 20 deletions
+9 -9
View File
@@ -11,23 +11,22 @@
document.body.append(fileInput); document.body.append(fileInput);
fileInput.addEventListener('change', async () => { fileInput.addEventListener('change', async () => {
/*
const file = fileInput.files[0]; const file = fileInput.files[0];
const input = new Mediabunny.Input({ const input = new Mediabunny.Input({
formats: Mediabunny.ALL_FORMATS, formats: Mediabunny.ALL_FORMATS,
source: new Mediabunny.BlobSource(file), source: new Mediabunny.BlobSource(file),
}); });
const track = await input.getPrimaryAudioTrack(); const track = await input.getPrimaryVideoTrack();
const sink3 = new Mediabunny.EncodedPacketSink(track); const sink = new Mediabunny.EncodedPacketSink(track);
console.log((await sink3.getFirstPacket()).data.join(', ')); const packet = await sink.getFirstKeyPacket();
return;
console.log(await track.getDurationFromMetadata(), await track.computeDuration());
//console.log(await input.getDurationFromMetadata(), await input.computeDuration()); console.log(packet.data.join(', '));
return;
*/
const config = await track.getDecoderConfig();
console.log(config.description.join(', '));
/*
const input = new Mediabunny.Input({ const input = new Mediabunny.Input({
source: new Mediabunny.UrlSource('https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8'), source: new Mediabunny.UrlSource('https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8'),
formats: Mediabunny.ALL_FORMATS, formats: Mediabunny.ALL_FORMATS,
@@ -36,6 +35,7 @@
const videoTrack = await input.getPrimaryVideoTrack(); const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.EncodedPacketSink(videoTrack); const sink = new Mediabunny.EncodedPacketSink(videoTrack);
console.log(await sink.getFirstPacket()); console.log(await sink.getFirstPacket());
*/
/* /*
return return
+112
View File
@@ -866,6 +866,21 @@ export type HevcSpsInfo = {
minSpatialSegmentationIdc: number; minSpatialSegmentationIdc: number;
}; };
export const concatHevcNalUnits = (nalUnits: Uint8Array[], decoderConfig: VideoDecoderConfig) => {
if (decoderConfig.description) {
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
const bytes = toUint8Array(decoderConfig.description);
const lengthSizeMinusOne = bytes[21]! & 0b11;
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
return concatNalUnitsInLengthPrefixed(nalUnits, lengthSize);
} else {
// Stream is in Annex B format
return concatNalUnitsInAnnexB(nalUnits);
}
};
export const iterateHevcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => { export const iterateHevcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => {
if (decoderConfig.description) { if (decoderConfig.description) {
const bytes = toUint8Array(decoderConfig.description); const bytes = toUint8Array(decoderConfig.description);
@@ -1602,6 +1617,103 @@ export const deserializeHevcDecoderConfigurationRecord = (data: Uint8Array): Hev
} }
}; };
enum HevcNaluOrderState {
audAllowed,
beforeFirstVcl,
afterFirstVcl,
eoBitstreamAllowed,
noMoreDataAllowed,
}
// This function sanitzes the contents of an HEVC packet such that
// https://source.chromium.org/chromium/chromium/src/+/main:media/formats/mp4/hevc.cc's validation logic does not trip
// up on its contents. The validation is often too strict and rejects packets that Chromium could decode just fine.
// Chromium code retrieved on 2026-04-29.
// See https://issues.chromium.org/issues/507611247.
export const sanitizeHevcPacketForChromium = (
packetData: Uint8Array,
decoderConfig: VideoDecoderConfig,
): Uint8Array | null => {
const removedNalUnits = new Set<number>();
let orderState: HevcNaluOrderState = HevcNaluOrderState.audAllowed;
for (const loc of iterateHevcNalUnits(packetData, decoderConfig)) {
if (orderState === HevcNaluOrderState.noMoreDataAllowed) {
removedNalUnits.add(loc.offset);
continue;
}
const type = extractNalUnitTypeForHevc(packetData[loc.offset]!);
if (orderState === HevcNaluOrderState.eoBitstreamAllowed && type !== 37 /* EOB_NUT */) {
removedNalUnits.add(loc.offset);
continue;
}
let remove = false;
if (type === 35) { // AUD_NUT
if (orderState > HevcNaluOrderState.audAllowed) {
remove = true;
} else {
orderState = HevcNaluOrderState.beforeFirstVcl;
}
} else if (type <= 31) { // VCL (0-31)
if (orderState > HevcNaluOrderState.afterFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.afterFirstVcl;
}
} else if (type === 36) { // EOS_NUT
if (orderState !== HevcNaluOrderState.afterFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.eoBitstreamAllowed;
}
} else if (type === 37) { // EOB_NUT
if (orderState < HevcNaluOrderState.afterFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.noMoreDataAllowed;
}
} else if (
type === 32 || type === 33 || type === 34 || type === 39
|| (type >= 41 && type <= 44) || (type >= 48 && type <= 55)
) { // VPS, SPS, PPS, PREFIX_SEI, RSV_NVCL41..44, UNSPEC48..55
if (orderState > HevcNaluOrderState.beforeFirstVcl) {
remove = true;
} else {
orderState = HevcNaluOrderState.beforeFirstVcl;
}
} else if (
type === 38 || type === 40
|| (type >= 45 && type <= 47) || (type >= 56 && type <= 63)
) { // FD, SUFFIX_SEI, RSV_NVCL45..47, UNSPEC56..63
if (orderState < HevcNaluOrderState.afterFirstVcl) {
remove = true;
}
}
if (remove) {
removedNalUnits.add(loc.offset);
}
}
// If nothing violated the rules, return null to signal that
if (removedNalUnits.size === 0) {
return null;
}
const filteredNalUnits: Uint8Array[] = [];
for (const loc of iterateHevcNalUnits(packetData, decoderConfig)) {
if (!removedNalUnits.has(loc.offset)) {
filteredNalUnits.push(packetData.subarray(loc.offset, loc.offset + loc.length));
}
}
return concatHevcNalUnits(filteredNalUnits, decoderConfig);
};
export type Vp9CodecInfo = { export type Vp9CodecInfo = {
profile: number; profile: number;
level: number; level: number;
+20 -11
View File
@@ -17,6 +17,7 @@ import {
iterateAvcNalUnits, iterateAvcNalUnits,
iterateHevcNalUnits, iterateHevcNalUnits,
parseAvcSps, parseAvcSps,
sanitizeHevcPacketForChromium,
} from './codec-data'; } from './codec-data';
import { CustomVideoDecoder, customVideoDecoders, CustomAudioDecoder, customAudioDecoders } from './custom-coder'; import { CustomVideoDecoder, customVideoDecoders, CustomAudioDecoder, customAudioDecoders } from './custom-coder';
import { InputDisposedError } from './input'; import { InputDisposedError } from './input';
@@ -980,20 +981,28 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
insertSorted(this.inputTimestamps, packet.timestamp, x => x); insertSorted(this.inputTimestamps, packet.timestamp, x => x);
} }
// Workaround for https://issues.chromium.org/issues/470109459 if (isChromium() && this.currentPacketIndex === 0) {
if (isChromium() && this.currentPacketIndex === 0 && this.codec === 'avc') { if (this.codec === 'avc') {
const filteredNalUnits: Uint8Array[] = []; // Workaround for https://issues.chromium.org/issues/470109459
const filteredNalUnits: Uint8Array[] = [];
for (const loc of iterateAvcNalUnits(packet.data, this.decoderConfig)) { for (const loc of iterateAvcNalUnits(packet.data, this.decoderConfig)) {
const type = extractNalUnitTypeForAvc(packet.data[loc.offset]!); const type = extractNalUnitTypeForAvc(packet.data[loc.offset]!);
// These trip up Chromium's key frame detection, so let's strip them // These trip up Chromium's key frame detection, so let's strip them
if (!(type >= 20 && type <= 31)) { if (!(type >= 20 && type <= 31)) {
filteredNalUnits.push(packet.data.subarray(loc.offset, loc.offset + loc.length)); filteredNalUnits.push(packet.data.subarray(loc.offset, loc.offset + loc.length));
}
}
const newData = concatAvcNalUnits(filteredNalUnits, this.decoderConfig);
packet = new EncodedPacket(newData, packet.type, packet.timestamp, packet.duration);
} else if (this.codec === 'hevc') {
// Workaround for https://issues.chromium.org/issues/507611247
const sanitizedData = sanitizeHevcPacketForChromium(packet.data, this.decoderConfig);
if (sanitizedData) {
packet = new EncodedPacket(sanitizedData, packet.type, packet.timestamp, packet.duration);
} }
} }
const newData = concatAvcNalUnits(filteredNalUnits, this.decoderConfig);
packet = new EncodedPacket(newData, packet.type, packet.timestamp, packet.duration);
} }
this.decoder.decode(packet.toEncodedVideoChunk()); this.decoder.decode(packet.toEncodedVideoChunk());