From 831e838f74424f68592d4ae5f57f82c4304efd0b Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:06:11 +0100 Subject: [PATCH] Implement workaround for https://issues.chromium.org/issues/470109459 --- src/codec-data.ts | 93 ++++++++++++++++++++++++++++++++++------------- src/media-sink.ts | 20 +++++++++- 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/src/codec-data.ts b/src/codec-data.ts index 2fd8ca7..26fa41e 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -24,6 +24,7 @@ import { toUint8Array, getChromiumVersion, isChromium, + setUint24, } from './misc'; import { PacketType } from './packet'; import { MetadataTags } from './metadata'; @@ -161,38 +162,65 @@ const removeEmulationPreventionBytes = (data: Uint8Array) => { return new Uint8Array(result); }; -/** Converts an AVC packet in Annex B format to length-prefixed format. */ -export const transformAnnexBToLengthPrefixed = (packetData: Uint8Array) => { - const NAL_UNIT_LENGTH_SIZE = 4; +const ANNEX_B_START_CODE = new Uint8Array([0, 0, 0, 1]); - const nalUnits = findNalUnitsInAnnexB(packetData); - - if (nalUnits.length === 0) { - // If no NAL units were found, it's not valid Annex B data - return null; - } - - let totalSize = 0; - for (const nalUnit of nalUnits) { - totalSize += NAL_UNIT_LENGTH_SIZE + nalUnit.byteLength; - } - - const avccData = new Uint8Array(totalSize); - const dataView = new DataView(avccData.buffer); +export const concatNalUnitsInAnnexB = (nalUnits: Uint8Array[]) => { + const totalLength = nalUnits.reduce((a, b) => a + ANNEX_B_START_CODE.byteLength + b.byteLength, 0); + const result = new Uint8Array(totalLength); let offset = 0; - // Write each NAL unit with its length prefix for (const nalUnit of nalUnits) { - const length = nalUnit.byteLength; + result.set(ANNEX_B_START_CODE, offset); + offset += ANNEX_B_START_CODE.byteLength; - dataView.setUint32(offset, length, false); - offset += 4; - - avccData.set(nalUnit, offset); + result.set(nalUnit, offset); offset += nalUnit.byteLength; } - return avccData; + return result; +}; + +export const concatNalUnitsInLengthPrefixed = (nalUnits: Uint8Array[], lengthSize: 1 | 2 | 3 | 4) => { + const totalLength = nalUnits.reduce((a, b) => a + lengthSize + b.byteLength, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + + for (const nalUnit of nalUnits) { + const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength); + + switch (lengthSize) { + case 1: + dataView.setUint8(offset, nalUnit.byteLength); + break; + case 2: + dataView.setUint16(offset, nalUnit.byteLength, false); + break; + case 3: + setUint24(dataView, offset, nalUnit.byteLength, false); + break; + case 4: + dataView.setUint32(offset, nalUnit.byteLength, false); + break; + } + + offset += lengthSize; + + result.set(nalUnit, offset); + offset += nalUnit.byteLength; + } + + return result; +}; + +/** Converts an AVC packet in Annex B format to length-prefixed format. */ +export const transformAnnexBToLengthPrefixed = (packetData: Uint8Array) => { + const nalUnits = findNalUnitsInAnnexB(packetData); + if (nalUnits.length === 0) { + // It's not valid Annex B data + return null; + } + + return concatNalUnitsInLengthPrefixed(nalUnits, 4); }; // Data specified in ISO 14496-15 @@ -227,7 +255,22 @@ export const extractAvcNalUnits = (packetData: Uint8Array, decoderConfig: VideoD } }; -const extractNalUnitTypeForAvc = (data: Uint8Array) => { +export const concatAvcNalUnits = (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[4]! & 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 extractNalUnitTypeForAvc = (data: Uint8Array) => { return data[0]! & 0x1F; }; diff --git a/src/media-sink.ts b/src/media-sink.ts index 711d0c4..539606e 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -8,9 +8,12 @@ import { parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, VideoCodec, AudioCodec } from './codec'; import { + concatAvcNalUnits, deserializeAvcDecoderConfigurationRecord, determineVideoPacketType, + extractAvcNalUnits, extractHevcNalUnits, + extractNalUnitTypeForAvc, extractNalUnitTypeForHevc, HevcNalUnitType, parseAvcSps, @@ -928,8 +931,6 @@ class VideoDecoderWrapper extends DecoderWrapper { this.raslSkipped = true; } - this.currentPacketIndex++; - if (this.customDecoder) { this.customDecoderQueueSize++; void this.customDecoderCallSerializer @@ -942,9 +943,24 @@ class VideoDecoderWrapper extends DecoderWrapper { insertSorted(this.inputTimestamps, packet.timestamp, x => x); } + // Workaround for https://issues.chromium.org/issues/470109459 + if (isChromium() && this.currentPacketIndex === 0 && this.codec === 'avc') { + const nalUnits = extractAvcNalUnits(packet.data, this.decoderConfig); + const filteredNalUnits = nalUnits.filter((x) => { + const type = extractNalUnitTypeForAvc(x); + // These trip up Chromium's key frame detection, so let's strip them + return !(type >= 20 && type <= 31); + }); + + const newData = concatAvcNalUnits(filteredNalUnits, this.decoderConfig); + packet = new EncodedPacket(newData, packet.type, packet.timestamp, packet.duration); + } + this.decoder.decode(packet.toEncodedVideoChunk()); this.decodeAlphaData(packet); } + + this.currentPacketIndex++; } decodeAlphaData(packet: EncodedPacket) {