diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/H264AnnexB.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/H264AnnexB.kt index d12aa63c..04ac034d 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/H264AnnexB.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/H264AnnexB.kt @@ -3,7 +3,10 @@ package com.utun.chat.data import java.io.ByteArrayOutputStream import java.nio.ByteBuffer -/** Единый wire-формат AVC: Annex B. Никакого поиска кадра по произвольным байтам после потери блока. */ +/** AVC NAL framing: Annex B со start code 3/4 байта либо big-endian length[4] перед каждым NAL. + * normalize копирует/конвертирует целый access unit <=2 MiB и проверяет границы/forbidden bit; + * это не SPS/декодерная проверка bitstream. Ошибки аргументов бросаются владельцу для диагностики. + * keyFrame возвращает обычный Annex B с SPS/PPS/IDR, без uAV2 config length из StreamVideoCodec. */ object H264AnnexB { const val MAX_FRAME = 2 * 1024 * 1024 private val start = byteArrayOf(0, 0, 0, 1) @@ -20,6 +23,7 @@ object H264AnnexB { require(b.remaining() >= 4) val length = b.int require(length > 0 && length <= b.remaining()) + require((bytes[b.position()].toInt() and 0x80) == 0) out.write(start); out.write(bytes, b.position(), length); b.position(b.position() + length) } require(out.size() <= MAX_FRAME) diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/H264AnnexBTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/H264AnnexBTest.kt index 7bc8cc50..110fe6b0 100644 --- a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/H264AnnexBTest.kt +++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/H264AnnexBTest.kt @@ -30,4 +30,10 @@ class H264AnnexBTest { assertThrows(IllegalArgumentException::class.java) { H264AnnexB.keyFrame(byteArrayOf(0, 0, 1, 0x61, 0x11), sps + pps) } assertThrows(IllegalArgumentException::class.java) { H264AnnexB.normalize(ByteArray(H264AnnexB.MAX_FRAME + 1)) } } + + @Test fun lengthPrefixedNalUsesTheSameForbiddenBitValidationAsAnnexB() { + val invalid = byteArrayOf(0, 0, 0, 2, -1, 1) + assertThrows(IllegalArgumentException::class.java) { H264AnnexB.normalize(invalid) } + assertThrows(IllegalArgumentException::class.java) { H264AnnexB.normalize(byteArrayOf(0, 0, 0, 1, -1, 1)) } + } }