diff --git a/common/src/main/java/com/pedro/common/BitBuffer.kt b/common/src/main/java/com/pedro/common/BitBuffer.kt index 79d7d968b3..217ffbe344 100644 --- a/common/src/main/java/com/pedro/common/BitBuffer.kt +++ b/common/src/main/java/com/pedro/common/BitBuffer.kt @@ -16,26 +16,114 @@ package com.pedro.common +import java.nio.ByteBuffer +import kotlin.math.ceil + /** * Created by pedro on 9/12/23. * */ -class BitBuffer(private val buffer: ByteArray) { +class BitBuffer(val buffer: ByteBuffer) { + var bufferPosition = buffer.position() * Byte.SIZE_BITS + private set + private val bufferEnd = buffer.limit() * Byte.SIZE_BITS + + val bitRemaining: Int + get() = bufferEnd - bufferPosition - fun getBits(offset: Int, size: Int): Int { - val startIndex = offset / 8 - val startBit = offset % 8 - var result = 0 - var bitNum = startBit + val remaining: Int + get() = ceil(bitRemaining.toDouble() / Byte.SIZE_BITS).toInt() + private fun getBits(size: Int): Long { + var result = 0L for (i in 0 until size) { - val nextByte = (startBit + i) % 8 < startBit - val currentIndex = startIndex + if (nextByte) 1 else 0 - val currentBit = (startBit + i) % 8 + val bitPosition = bufferPosition + i + val currentIndex = bitPosition / 8 + val currentBit = bitPosition % 8 val bitValue = (buffer[currentIndex].toInt() ushr 7 - currentBit) and 0x01 - result = (result shl 1) or bitValue - bitNum++ + result = (result shl 1) or bitValue.toLong() } + bufferPosition += size return result } + + fun getBool() = getBits(1) == 1L + + fun getInt(i: Int) = getBits(i).toInt() + + fun getLong(i: Int) = getBits(i) + + fun skip(i: Int) { + bufferPosition += i + } + + fun skipBool() = skip(1) + + fun readUE(): Int { + var leadingZeroBits = 0 + while (!getBool()) { + leadingZeroBits++ + } + return if (leadingZeroBits > 0) { + (1 shl leadingZeroBits) - 1 + getInt(leadingZeroBits) + } else { + 0 + } + } + + fun readUVLC(): Int { + var leadingZeros = 0 + var value = 0 + var currentIndex = bufferPosition / 8 + var currentBit = 7 - bufferPosition % 8 + + while (buffer[currentIndex].toInt() and (1 shl currentBit) == 0) { + leadingZeros++ + if (currentBit == 0) { + currentIndex++ + currentBit = 7 + } else { + currentBit-- + } + } + + repeat((0 until leadingZeros + 1).count()) { + if (currentBit == 0) { + currentIndex++ + currentBit = 7 + } else { + currentBit-- + } + + value = (value shl 1) or ((buffer[currentIndex].toInt() ushr currentBit) and 1) + } + bufferPosition += 2 * leadingZeros + 1 + return value + } + + fun resetPosition() { + bufferPosition = buffer.position() * Byte.SIZE_BITS + } + + companion object { + + fun extractRbsp(buffer: ByteBuffer, headerLength: Int): ByteBuffer { + val rbsp = ByteBuffer.allocateDirect(buffer.remaining()) + + val indices = buffer.indicesOf(byteArrayOf(0x00, 0x00, 0x03)) + + rbsp.put(buffer, 0, headerLength) + + var previous = buffer.position() + indices.forEach { + rbsp.put(buffer, previous, it + 2 - previous) + previous = it + 3 // skip emulation_prevention_three_byte + } + rbsp.put(buffer, previous, buffer.limit() - previous) + + rbsp.limit(rbsp.position()) + rbsp.rewind() + return rbsp + } + } } \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/Extensions.kt b/common/src/main/java/com/pedro/common/Extensions.kt index 59ed327d91..b3ce33630e 100644 --- a/common/src/main/java/com/pedro/common/Extensions.kt +++ b/common/src/main/java/com/pedro/common/Extensions.kt @@ -200,7 +200,7 @@ fun Int.toUInt32(): ByteArray { } fun Long.toUInt64(): ByteArray { - return byteArrayOf((this ushr 56).toByte(), (this ushr 48).toByte(), (this ushr 48).toByte(), (this ushr 32).toByte(), (this ushr 24).toByte(), (this ushr 16).toByte(), (this ushr 8).toByte(), this.toByte()) + return byteArrayOf((this ushr 56).toByte(), (this ushr 48).toByte(), (this ushr 40).toByte(), (this ushr 32).toByte(), (this ushr 24).toByte(), (this ushr 16).toByte(), (this ushr 8).toByte(), this.toByte()) } fun Int.toUInt32LittleEndian(): ByteArray = Integer.reverseBytes(this).toUInt32() @@ -310,4 +310,29 @@ fun List.combine(): ByteArray { fun SecureRandom.nextBytes(size: Int): ByteArray { return ByteArray(size).apply { nextBytes(this) } +} + +fun Boolean.toInt() = if (this) 1 else 0 + +fun ByteBuffer.indicesOf(prefix: ByteArray): List { + if (prefix.isEmpty()) return emptyList() + val indices = mutableListOf() + + outer@ for (i in 0 until this.limit() - prefix.size + 1) { + for (j in prefix.indices) { + if (this.get(i + j) != prefix[j]) { + continue@outer + } + } + indices.add(i) + } + return indices +} + +fun ByteBuffer.put(buffer: ByteBuffer, offset: Int, length: Int) { + val limit = buffer.limit() + buffer.position(offset) + buffer.limit(offset + length) + this.put(buffer) + buffer.limit(limit) } \ No newline at end of file diff --git a/common/src/test/java/com/pedro/common/BitBufferTest.kt b/common/src/test/java/com/pedro/common/BitBufferTest.kt index 35bf73a436..5d8533697f 100644 --- a/common/src/test/java/com/pedro/common/BitBufferTest.kt +++ b/common/src/test/java/com/pedro/common/BitBufferTest.kt @@ -16,23 +16,223 @@ package com.pedro.common -import junit.framework.TestCase.assertEquals +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test +import java.nio.ByteBuffer /** * Created by pedro on 10/12/23. */ class BitBufferTest { + private fun bitBufferOf(vararg bytes: Int): BitBuffer { + return BitBuffer(ByteBuffer.wrap(ByteArray(bytes.size) { bytes[it].toByte() })) + } + @Test fun checkGetBits() { - val buffer = byteArrayOf(0x00, 0x00, 0x00, 0x24, 0x4f, 0x7e, 0x7f, 0x00, 0x68.toByte(), 0x83.toByte(), 0x00, 0x83.toByte(), 0x02) + val buffer = ByteBuffer.wrap(byteArrayOf(0x00, 0x00, 0x00, 0x24, 0x4f, 0x7e, 0x7f, 0x00, 0x68.toByte(), 0x83.toByte(), 0x00, 0x83.toByte(), 0x02)) val bitBuffer = BitBuffer(buffer) - val result = bitBuffer.getBits(24, 5) - val result2 = bitBuffer.getBits(29, 4) - val result3 = bitBuffer.getBits(29 + 4, 4) + bitBuffer.skip(24) + val result = bitBuffer.getInt(5) + val result2 = bitBuffer.getInt(4) + val result3 = bitBuffer.getLong(4) assertEquals(0x04, result) assertEquals(8, result2) - assertEquals(9, result3) + assertEquals(9L, result3) + } + + @Test + fun `GIVEN a single byte WHEN getInt 8 THEN read the whole byte`() { + val bitBuffer = bitBufferOf(0xAB) + assertEquals(0xAB, bitBuffer.getInt(8)) + assertEquals(8, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN two bytes WHEN getInt across the byte boundary THEN combine both bytes`() { + val bitBuffer = bitBufferOf(0xAB, 0xCD) + assertEquals(0xABCD, bitBuffer.getInt(16)) + } + + @Test + fun `GIVEN two bytes WHEN getInt of a size that is not byte aligned THEN read the high bits`() { + val bitBuffer = bitBufferOf(0xAB, 0xCD) + assertEquals(0xABC, bitBuffer.getInt(12)) + assertEquals(12, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN an unaligned offset WHEN getInt crosses a byte boundary THEN read the right bits`() { + val bitBuffer = bitBufferOf(0xAB, 0xCD) + bitBuffer.skip(4) + assertEquals(0xBC, bitBuffer.getInt(8)) + assertEquals(12, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a byte WHEN getBool repeatedly THEN read every bit as a boolean`() { + val bitBuffer = bitBufferOf(0xA0) // 1010 0000 + assertTrue(bitBuffer.getBool()) + assertFalse(bitBuffer.getBool()) + assertTrue(bitBuffer.getBool()) + assertFalse(bitBuffer.getBool()) + assertEquals(4, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN six bytes WHEN getLong 48 THEN read a value bigger than an Int`() { + val bitBuffer = bitBufferOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06) + assertEquals(0x010203040506L, bitBuffer.getLong(48)) + } + + @Test + fun `GIVEN all bits set WHEN getLong 63 THEN return Long MAX_VALUE`() { + val bitBuffer = bitBufferOf(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF) + assertEquals(Long.MAX_VALUE, bitBuffer.getLong(63)) + } + + @Test + fun `GIVEN all bits set WHEN getLong 64 THEN keep the whole bit pattern`() { + val bitBuffer = bitBufferOf(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF) + assertEquals(-1L, bitBuffer.getLong(64)) + } + + @Test + fun `GIVEN getInt 0 WHEN read THEN return 0 and do not move`() { + val bitBuffer = bitBufferOf(0xAB) + assertEquals(0, bitBuffer.getInt(0)) + assertEquals(0, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a buffer WHEN skip THEN advance position and keep reads aligned`() { + val bitBuffer = bitBufferOf(0x12, 0x34, 0x56) + assertEquals(0, bitBuffer.bufferPosition) + bitBuffer.skip(8) + assertEquals(8, bitBuffer.bufferPosition) + assertEquals(0x34, bitBuffer.getInt(8)) + assertEquals(16, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a buffer WHEN skipBool THEN advance a single bit`() { + val bitBuffer = bitBufferOf(0x40) // 0100 0000 + bitBuffer.skipBool() + assertEquals(1, bitBuffer.bufferPosition) + assertTrue(bitBuffer.getBool()) + } + + @Test + fun `GIVEN a buffer WHEN reading THEN bitRemaining and remaining are updated`() { + val bitBuffer = bitBufferOf(0x00, 0x00, 0x00) + assertEquals(24, bitBuffer.bitRemaining) + assertEquals(3, bitBuffer.remaining) + bitBuffer.skip(4) + assertEquals(20, bitBuffer.bitRemaining) + assertEquals(3, bitBuffer.remaining) // ceil(20 / 8) + bitBuffer.getInt(4) + assertEquals(16, bitBuffer.bitRemaining) + assertEquals(2, bitBuffer.remaining) + bitBuffer.skip(16) + assertEquals(0, bitBuffer.bitRemaining) + assertEquals(0, bitBuffer.remaining) + } + + @Test + fun `GIVEN a consumed buffer WHEN resetPosition THEN read again from the start`() { + val bitBuffer = bitBufferOf(0x5A, 0x3C) + assertEquals(0x5A, bitBuffer.getInt(8)) + assertEquals(8, bitBuffer.bufferPosition) + bitBuffer.resetPosition() + assertEquals(0, bitBuffer.bufferPosition) + assertEquals(0x5A, bitBuffer.getInt(8)) + } + + @Test + fun `GIVEN a ByteBuffer with a non zero position WHEN create BitBuffer THEN start at that position`() { + val buffer = ByteBuffer.wrap(byteArrayOf(0x11, 0x22, 0x33)) + buffer.position(1) + val bitBuffer = BitBuffer(buffer) + assertEquals(8, bitBuffer.bufferPosition) + assertEquals(16, bitBuffer.bitRemaining) + assertEquals(0x22, bitBuffer.getInt(8)) + bitBuffer.resetPosition() + assertEquals(8, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN an exp golomb stream WHEN readUE THEN decode each code number`() { + val bitBuffer = bitBufferOf(0xA6) // 1 010 011 0 -> 0, 1, 2 + assertEquals(0, bitBuffer.readUE()) + assertEquals(1, bitBuffer.readUE()) + assertEquals(2, bitBuffer.readUE()) + } + + @Test + fun `GIVEN a longer exp golomb code WHEN readUE THEN decode the value`() { + val bitBuffer = bitBufferOf(0x20) // 00100 000 -> 3 + assertEquals(3, bitBuffer.readUE()) + assertEquals(5, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a uvlc with no leading zeros WHEN readUVLC THEN return 0 and consume one bit`() { + val bitBuffer = bitBufferOf(0x80) // 1000 0000 + assertEquals(0, bitBuffer.readUVLC()) + assertEquals(1, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a uvlc with leading zeros WHEN readUVLC THEN decode value and consume 2n+1 bits`() { + val bitBuffer = bitBufferOf(0x60) // 0110 0000 + assertEquals(2, bitBuffer.readUVLC()) + assertEquals(3, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a uvlc whose value crosses a byte boundary WHEN readUVLC THEN decode it`() { + // 0000 1 101 | 10... -> 4 leading zeros, terminator, value bits span into the second byte + val bitBuffer = bitBufferOf(0x0D, 0x80) + assertEquals(22, bitBuffer.readUVLC()) + assertEquals(9, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a uvlc whose leading zeros cross a byte boundary WHEN readUVLC THEN decode it`() { + // 8 leading zeros (whole first byte), terminator on the second byte, value spans into the third + val bitBuffer = bitBufferOf(0x00, 0xD5, 0x40) + assertEquals(341, bitBuffer.readUVLC()) + assertEquals(17, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN an unaligned position WHEN readUVLC THEN decode from that bit offset`() { + // skip 4 bits, then 0 1 11 -> 1 leading zero, terminator, value = 3 + val bitBuffer = bitBufferOf(0x07) + bitBuffer.skip(4) + assertEquals(3, bitBuffer.readUVLC()) + assertEquals(7, bitBuffer.bufferPosition) + } + + @Test + fun `GIVEN a stream with emulation prevention bytes WHEN extractRbsp THEN remove them`() { + val source = ByteBuffer.wrap(byteArrayOf(0xAA.toByte(), 0xBB.toByte(), 0x00, 0x00, 0x03, 0x01)) + val rbsp = BitBuffer.extractRbsp(source, 1) + val out = ByteArray(rbsp.remaining()) + rbsp.get(out) + assertArrayEquals(byteArrayOf(0xAA.toByte(), 0xBB.toByte(), 0x00, 0x00, 0x01), out) + } + + @Test + fun `GIVEN a stream without emulation bytes WHEN extractRbsp THEN keep it untouched`() { + val source = ByteBuffer.wrap(byteArrayOf(0x01, 0x02, 0x03)) + val rbsp = BitBuffer.extractRbsp(source, 0) + val out = ByteArray(rbsp.remaining()) + rbsp.get(out) + assertArrayEquals(byteArrayOf(0x01, 0x02, 0x03), out) } -} \ No newline at end of file +} diff --git a/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/SPSH265Parser.kt b/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/SPSH265Parser.kt index 4ef25b237e..076bf7d0d2 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/SPSH265Parser.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/SPSH265Parser.kt @@ -16,7 +16,8 @@ package com.pedro.rtmp.flv.video.config -import com.pedro.rtmp.utils.BitBuffer +import com.pedro.common.BitBuffer +import com.pedro.common.toInt import java.nio.ByteBuffer /** @@ -44,44 +45,44 @@ class SPSH265Parser { val rbsp = BitBuffer.extractRbsp(sps, 2) val bitBuffer = BitBuffer(rbsp) //Dropping nal_unit_header - bitBuffer.getLong(16) + bitBuffer.skip(16) //sps_video_parameter_set_id - bitBuffer.get(4) + bitBuffer.skip(4) //sps_max_sub_layers_minus1 - val maxSubLayersMinus1 = bitBuffer.get(3) + val maxSubLayersMinus1 = bitBuffer.getInt(3) //sps_temporal_id_nesting_flag - bitBuffer.get(1) + bitBuffer.skipBool() //start profile_tier_level - generalProfileSpace = bitBuffer.get(2).toInt() - generalTierFlag = if (bitBuffer.getBool()) 1 else 0 - generalProfileIdc = bitBuffer.getShort(5).toInt() + generalProfileSpace = bitBuffer.getInt(2) + generalTierFlag = bitBuffer.getBool().toInt() + generalProfileIdc = bitBuffer.getInt(5) generalProfileCompatibilityFlags = bitBuffer.getInt(32) generalConstraintIndicatorFlags = bitBuffer.getLong(48) - generalLevelIdc = bitBuffer.get(8).toInt() + generalLevelIdc = bitBuffer.getInt(8) val subLayerProfilePresentFlag = mutableListOf() val subLayerLevelPresentFlag = mutableListOf() - for (i in 0 until maxSubLayersMinus1) { + repeat((0 until maxSubLayersMinus1).count()) { subLayerProfilePresentFlag.add(bitBuffer.getBool()) subLayerLevelPresentFlag.add(bitBuffer.getBool()) } if (maxSubLayersMinus1 > 0) { - for (i in maxSubLayersMinus1..8) { - bitBuffer.getLong(2) // reserved_zero_2bits + repeat((maxSubLayersMinus1..8).count()) { + bitBuffer.skip(2) // reserved_zero_2bits } } for (i in 0 until maxSubLayersMinus1) { if (subLayerProfilePresentFlag[i]) { - bitBuffer.getLong(32) // skip - bitBuffer.getLong(32) // skip - bitBuffer.getLong(24) // skip + bitBuffer.skip(32) // skip + bitBuffer.skip(32) // skip + bitBuffer.skip(24) // skip } if (subLayerLevelPresentFlag[i]) { - bitBuffer.getLong(8) // skip + bitBuffer.skip(8) // skip } } //end profile_tier_level @@ -92,7 +93,7 @@ class SPSH265Parser { chromaFormat = bitBuffer.readUE() if (chromaFormat == 3) { //separate_colour_plane_flag - bitBuffer.getBool() + bitBuffer.skipBool() } //pic_width_in_luma_samples bitBuffer.readUE() @@ -114,7 +115,6 @@ class SPSH265Parser { bitDepthLumaMinus8 = bitBuffer.readUE() //bit_depth_chroma_minus8 bitDepthChromaMinus8 = bitBuffer.readUE() - //The buffer continue but we don't need read more } } \ No newline at end of file diff --git a/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigAV1.kt b/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigAV1.kt index 126a263a98..0311466a49 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigAV1.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigAV1.kt @@ -18,6 +18,7 @@ package com.pedro.rtmp.flv.video.config import com.pedro.common.BitBuffer import com.pedro.common.av1.Av1Parser +import com.pedro.common.toInt import java.nio.ByteBuffer /** @@ -55,220 +56,152 @@ class VideoSpecificConfigAV1(private val sequenceObu: ByteArray) { fun write(buffer: ByteArray, offset: Int) { val obuData = av1Parser.getObus(sequenceObu)[0].data - val bitBuffer = BitBuffer(obuData) - var index = 0 + val bitBuffer = BitBuffer(ByteBuffer.wrap(obuData)) - val seqProfile = bitBuffer.getBits(index, 3) - index += 3 - index += 1 - val reducedStillPictureHeader = bitBuffer.getBits(index, 1) - index += 1 + val seqProfile = bitBuffer.getInt(3) + bitBuffer.skipBool() + val reducedStillPictureHeader = bitBuffer.getBool() var seqLevelIdx = 0 - var seqTier = 0 - var initialDisplayDelayPresentFlag = 0 + var seqTier = false + var initialDisplayDelayPresentFlag = false var initialPresentationDelay = 0 - if (reducedStillPictureHeader == 1) { - seqLevelIdx = bitBuffer.getBits(index, 5) - index += 5 + if (reducedStillPictureHeader) { + seqLevelIdx = bitBuffer.getInt(5) } else { - val timingInfoPresentFlag = bitBuffer.getBits(index, 1) - index += 1 - var decoderModelInfoPresentFlag = 0 + val timingInfoPresentFlag = bitBuffer.getBool() + var decoderModelInfoPresentFlag = false var bufferDelayLengthMinus1 = 0 - if (timingInfoPresentFlag == 1) { - index += 64 - val equalPictureInterval = bitBuffer.getBits(index, 1) - index += 1 - if (equalPictureInterval == 1) { - val uvlc = readUVLC(obuData, index) - index += uvlc.second + if (timingInfoPresentFlag) { + bitBuffer.skip(64) + val equalPictureInterval = bitBuffer.getBool() + if (equalPictureInterval) { + bitBuffer.readUVLC() } - decoderModelInfoPresentFlag = bitBuffer.getBits(index, 1) - index += 1 - if (decoderModelInfoPresentFlag == 1) { - bufferDelayLengthMinus1 = bitBuffer.getBits(index, 5) - index += 5 - index += 42 //skip this + decoderModelInfoPresentFlag = bitBuffer.getBool() + if (decoderModelInfoPresentFlag) { + bufferDelayLengthMinus1 = bitBuffer.getInt(5) + bitBuffer.skip(42) //skip this } } - initialDisplayDelayPresentFlag = bitBuffer.getBits(index, 1) - index += 1 - val operatingPointsCntMinus1 = bitBuffer.getBits(index, 5) - index += 5 + initialDisplayDelayPresentFlag = bitBuffer.getBool() + val operatingPointsCntMinus1 = bitBuffer.getInt(5) for (i in 0..operatingPointsCntMinus1) { - index += 12 //skip - val levelIdx = bitBuffer.getBits(index, 5) - index += 5 + bitBuffer.skip(12) //skip + val levelIdx = bitBuffer.getInt(5) if (i == 0) seqLevelIdx = levelIdx if (levelIdx > 7) { - val sTier = bitBuffer.getBits(index, 1) - index += 1 + val sTier = bitBuffer.getBool() if (i == 0) seqTier = sTier } - if (decoderModelInfoPresentFlag == 1) { - val decoderModelPresentForThisOp = bitBuffer.getBits(index, 1) - index += 1 - if (decoderModelPresentForThisOp == 1) { + if (decoderModelInfoPresentFlag) { + val decoderModelPresentForThisOp = bitBuffer.getBool() + if (decoderModelPresentForThisOp) { val n = bufferDelayLengthMinus1 + 1 - index += n * 2 + 1 //skip this + bitBuffer.skip(n * 2 + 1) //skip this } } - if (initialDisplayDelayPresentFlag == 1) { - val initialDisplayDelayPresentForThisOp = bitBuffer.getBits(index, 1) - index += 1 - if (initialDisplayDelayPresentForThisOp == 1) { - val initialDisplayDelayMinus1 = bitBuffer.getBits(index, 4) - index += 4 + if (initialDisplayDelayPresentFlag) { + val initialDisplayDelayPresentForThisOp = bitBuffer.getBool() + if (initialDisplayDelayPresentForThisOp) { + val initialDisplayDelayMinus1 = bitBuffer.getInt(4) if (i == 0) initialPresentationDelay = initialDisplayDelayMinus1 } } } } - val frameWidthBitsMinus1 = bitBuffer.getBits(index, 4) - index += 4 - val frameHeightBitsMinus1 = bitBuffer.getBits(index, 4) - index += 4 - index += frameWidthBitsMinus1 + 1 + frameHeightBitsMinus1 + 1 - var frameIdNumbersPresentFlag = 0 - if (reducedStillPictureHeader != 1) { - frameIdNumbersPresentFlag = bitBuffer.getBits(index, 1) - index += 1 + val frameWidthBitsMinus1 = bitBuffer.getInt(4) + val frameHeightBitsMinus1 = bitBuffer.getInt(4) + bitBuffer.skip(frameWidthBitsMinus1 + 1 + frameHeightBitsMinus1 + 1) + var frameIdNumbersPresentFlag = false + if (!reducedStillPictureHeader) { + frameIdNumbersPresentFlag = bitBuffer.getBool() } - if (frameIdNumbersPresentFlag == 1) index += 7 - index += 3 - if (reducedStillPictureHeader != 1) { - index += 4 - val enableOrderHint = bitBuffer.getBits(index, 1) - index += 1 - if (enableOrderHint == 1) index += 2 - val seqChooseScreenContentTools = bitBuffer.getBits(index, 1) - index += 1 - var seqForceScreenContentTools = 2 - if (seqChooseScreenContentTools != 1) { - seqForceScreenContentTools = bitBuffer.getBits(index, 1) - index += 1 + if (frameIdNumbersPresentFlag) bitBuffer.skip(7) + bitBuffer.skip(3) + if (!reducedStillPictureHeader) { + bitBuffer.skip(4) + val enableOrderHint = bitBuffer.getBool() + if (enableOrderHint) bitBuffer.skip(2) + val seqChooseScreenContentTools = bitBuffer.getBool() + val seqForceScreenContentTools = seqChooseScreenContentTools || bitBuffer.getBool() + if (seqForceScreenContentTools) { + val seqChooseIntegerMv = bitBuffer.getBool() + if (!seqChooseIntegerMv) bitBuffer.skipBool() } - if (seqForceScreenContentTools > 0) { - val seqChooseIntegerMv = bitBuffer.getBits(index, 1) - index += 1 - if (seqChooseIntegerMv != 1) index += 1 - } - if (enableOrderHint == 1) index += 3 + if (enableOrderHint) bitBuffer.skip(3) } - index += 3 + bitBuffer.skip(3) //config color - val highBitDepth = bitBuffer.getBits(index, 1) - index += 1 - var twelveBit = 0 + val highBitDepth = bitBuffer.getBool() + var twelveBit = false var bitDepth = 0 - if (seqProfile == 2 && highBitDepth == 1) { - twelveBit = bitBuffer.getBits(index, 1) - index += 1 - bitDepth = if (twelveBit == 1) 12 else 10 + if (seqProfile == 2 && highBitDepth) { + twelveBit = bitBuffer.getBool() + bitDepth = if (twelveBit) 12 else 10 } else if (seqProfile <= 2) { - bitDepth = if (highBitDepth == 1) 10 else 8 + bitDepth = if (highBitDepth) 10 else 8 } val monochrome = if (seqProfile == 1) { - 0 + false } else { - val chrome = bitBuffer.getBits(index, 1) - index += 1 + val chrome = bitBuffer.getBool() chrome } - val colorDescriptionPresentFlag = bitBuffer.getBits(index, 1) - index += 1 + val colorDescriptionPresentFlag = bitBuffer.getBool() var colorPrimaries = 0 var transferCharacteristics = 0 var matrixCoefficients = 0 - if (colorDescriptionPresentFlag == 1) { - colorPrimaries = bitBuffer.getBits(index, 8) - index += 8 - transferCharacteristics = bitBuffer.getBits(index, 8) - index += 8 - matrixCoefficients = bitBuffer.getBits(index, 8) - index += 8 + if (colorDescriptionPresentFlag) { + colorPrimaries = bitBuffer.getInt(8) + transferCharacteristics = bitBuffer.getInt(8) + matrixCoefficients = bitBuffer.getInt(8) } - val subsamplingX: Int - val subsamplingY: Int - var samplePosition = 0 - if (monochrome == 1) { - index += 1 - subsamplingX = 1 - subsamplingY = 1 + val subsamplingX: Boolean + val subsamplingY: Boolean + var samplePosition = false + if (monochrome) { + bitBuffer.getBool() + subsamplingX = true + subsamplingY = true } else if (colorPrimaries == 1 && transferCharacteristics == 1 && matrixCoefficients == 1) { - subsamplingX = 0 - subsamplingY = 0 + subsamplingX = false + subsamplingY = false } else { - index += 1 + bitBuffer.skipBool() if (seqProfile == 0) { - subsamplingX = 1 - subsamplingY = 1 + subsamplingX = true + subsamplingY = true } else if (seqProfile == 1) { - subsamplingX = 0 - subsamplingY = 0 + subsamplingX = false + subsamplingY = false } else { if (bitDepth == 12) { - subsamplingX = bitBuffer.getBits(index, 1) - index += 1 - if (subsamplingX == 1) { - subsamplingY = bitBuffer.getBits(index, 1) - index += 1 + subsamplingX = bitBuffer.getBool() + subsamplingY = if (subsamplingX) { + bitBuffer.getBool() } else { - subsamplingY = 0 + false } } else { - subsamplingX = 1 - subsamplingY = 0 + subsamplingX = true + subsamplingY = false } - if (subsamplingX == 1 && subsamplingY == 1) { - samplePosition = bitBuffer.getBits(index, 1) - index += 1 + if (subsamplingX && subsamplingY) { + samplePosition = bitBuffer.getBool() } } } - index += 1 //finish config color - index += 1 val data = ByteBuffer.wrap(buffer, offset, size) data.put(0x81.toByte()) //marker and version data.put(((seqProfile shl 5) or seqLevelIdx).toByte()) data.put( - ((seqTier shl 7) or (highBitDepth shl 6) or (twelveBit shl 5) or (monochrome shl 4) or - (subsamplingX shl 3) or (subsamplingY shl 2) or samplePosition).toByte() + ((seqTier.toInt() shl 7) or (highBitDepth.toInt() shl 6) or (twelveBit.toInt() shl 5) or (monochrome.toInt() shl 4) or + (subsamplingX.toInt() shl 3) or (subsamplingY.toInt() shl 2) or samplePosition.toInt()).toByte() ) val reserved = 0 - data.put(((reserved shl 5) or (initialDisplayDelayPresentFlag shl 4) or initialPresentationDelay).toByte()) + data.put(((reserved shl 5) or (initialDisplayDelayPresentFlag.toInt() shl 4) or initialPresentationDelay).toByte()) data.put(sequenceObu) } - - private fun readUVLC(byteArray: ByteArray, offset: Int): Pair { - var leadingZeros = 0 - var value = 0 - var currentIndex = offset / 8 - var currentBit = 7 - offset % 8 - - while (byteArray[currentIndex].toInt() and (1 shl currentBit) == 0) { - leadingZeros++ - if (currentBit == 0) { - currentIndex++ - currentBit = 7 - } else { - currentBit-- - } - } - - for (i in 0 until leadingZeros + 1) { - if (currentBit == 0) { - currentIndex++ - currentBit = 7 - } else { - currentBit-- - } - - value = (value shl 1) or ((byteArray[currentIndex].toInt() ushr currentBit) and 1) - } - - return Pair(value, offset + leadingZeros + 1) - } } \ No newline at end of file diff --git a/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigHEVC.kt b/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigHEVC.kt index eb69b8b696..20d58cac53 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigHEVC.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/flv/video/config/VideoSpecificConfigHEVC.kt @@ -16,7 +16,7 @@ package com.pedro.rtmp.flv.video.config -import com.pedro.rtmp.utils.toByteArray +import com.pedro.common.toUInt64 import java.nio.ByteBuffer /** @@ -89,7 +89,7 @@ class VideoSpecificConfigHEVC( data.putInt(generalProfileCompatibilityFlags) val generalConstraintIndicatorFlags = spsParsed.generalConstraintIndicatorFlags - data.put(generalConstraintIndicatorFlags.toByteArray().sliceArray(2 until Long.SIZE_BYTES)) + data.put(generalConstraintIndicatorFlags.toUInt64().sliceArray(2 until Long.SIZE_BYTES)) val generalLevelIdc = spsParsed.generalLevelIdc data.put(generalLevelIdc.toByte()) diff --git a/rtmp/src/main/java/com/pedro/rtmp/utils/BitBuffer.kt b/rtmp/src/main/java/com/pedro/rtmp/utils/BitBuffer.kt deleted file mode 100644 index 7a28a588a9..0000000000 --- a/rtmp/src/main/java/com/pedro/rtmp/utils/BitBuffer.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pedro.rtmp.utils - -import java.nio.ByteBuffer -import kotlin.math.ceil - -/** - * Created by pedro on 16/08/23. - * - */ -class BitBuffer(val buffer: ByteBuffer) { - private var bufferPosition = buffer.position() * Byte.SIZE_BITS - private val bufferEnd = buffer.limit() * Byte.SIZE_BITS - - val hasRemaining: Boolean - get() = bitRemaining > 0 - - val bitRemaining: Int - get() = bufferEnd - bufferPosition - - val remaining: Int - get() = ceil(bitRemaining.toDouble() / Byte.SIZE_BITS).toInt() - - fun getBool(): Boolean { - return getInt(1) == 1 - } - - fun get(i: Int) = getLong(i).toByte() - - fun getShort(i: Int) = getLong(i).toShort() - - fun getInt(i: Int) = getLong(i).toInt() - - fun getLong(i: Int): Long { - if (!hasRemaining) { - throw IllegalStateException("No more bits to read") - } - - val b = buffer[bufferPosition / Byte.SIZE_BITS] - val v = if (b < 0) b + 256 else b.toInt() - val left = Byte.SIZE_BITS - bufferPosition % Byte.SIZE_BITS - var rc: Long - if (i <= left) { - rc = - ((v shl (bufferPosition % Byte.SIZE_BITS) and 0xFF) shr ((bufferPosition % Byte.SIZE_BITS) + (left - i))).toLong() - bufferPosition += i - } else { - val then = i - left - rc = getLong(left) - rc = rc shl then - rc += getLong(then) - } - - buffer.position(ceil(bufferPosition.toDouble() / Byte.SIZE_BITS).toInt()) - return rc - } - - fun readUE(): Int { - var leadingZeroBits = 0 - while (!getBool()) { - leadingZeroBits++ - } - return if (leadingZeroBits > 0) { - (1 shl leadingZeroBits) - 1 + getInt(leadingZeroBits) - } else { - 0 - } - } - - companion object { - - fun extractRbsp(buffer: ByteBuffer, headerLength: Int): ByteBuffer { - val rbsp = ByteBuffer.allocateDirect(buffer.remaining()) - - val indices = buffer.indicesOf(byteArrayOf(0x00, 0x00, 0x03)) - - rbsp.put(buffer, 0, headerLength) - - var previous = buffer.position() - indices.forEach { - rbsp.put(buffer, previous, it + 2 - previous) - previous = it + 3 // skip emulation_prevention_three_byte - } - rbsp.put(buffer, previous, buffer.limit() - previous) - - rbsp.limit(rbsp.position()) - rbsp.rewind() - return rbsp - } - } -} \ No newline at end of file diff --git a/rtmp/src/main/java/com/pedro/rtmp/utils/Utils.kt b/rtmp/src/main/java/com/pedro/rtmp/utils/Utils.kt deleted file mode 100644 index 5726572683..0000000000 --- a/rtmp/src/main/java/com/pedro/rtmp/utils/Utils.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pedro.rtmp.utils - -import java.nio.ByteBuffer - -/** - * Created by pedro on 20/04/21. - */ - -fun Long.toByteArray(): ByteArray { - val buffer = ByteBuffer.allocate(Long.SIZE_BYTES) - return buffer.putLong(this).array() -} - -fun ByteBuffer.indicesOf(prefix: ByteArray): List { - if (prefix.isEmpty()) { - return emptyList() - } - - val indices = mutableListOf() - - outer@ for (i in 0 until this.limit() - prefix.size + 1) { - for (j in prefix.indices) { - if (this.get(i + j) != prefix[j]) { - continue@outer - } - } - indices.add(i) - } - return indices -} - -fun ByteBuffer.put(buffer: ByteBuffer, offset: Int, length: Int) { - val limit = buffer.limit() - buffer.position(offset) - buffer.limit(offset + length) - this.put(buffer) - buffer.limit(limit) -} \ No newline at end of file diff --git a/rtmp/src/test/java/com/pedro/rtmp/flv/video/VideoConfigTest.kt b/rtmp/src/test/java/com/pedro/rtmp/flv/video/VideoConfigTest.kt index b322f42319..9b5fc1c1f6 100644 --- a/rtmp/src/test/java/com/pedro/rtmp/flv/video/VideoConfigTest.kt +++ b/rtmp/src/test/java/com/pedro/rtmp/flv/video/VideoConfigTest.kt @@ -59,4 +59,15 @@ class VideoConfigTest { config.write(data, 0) assertArrayEquals(expectedConfig, data) } + + @Test + fun `GIVEN obu sequence with seq_choose_screen_content_tools set WHEN create a video config THEN keep bitstream aligned`() { + val obuSequence = byteArrayOf(0x0a, 0x08, 0x40, 0x00, 0x00, 0x08, 0x00, 0x00, 0xc6.toByte(), 0x20) + val expectedConfig = byteArrayOf(0x81.toByte(), 0x41, 0x68, 0x00, 0x0a, 0x08, 0x40, 0x00, 0x00, 0x08, 0x00, 0x00, 0xc6.toByte(), 0x20) + + val config = VideoSpecificConfigAV1(obuSequence) + val data = ByteArray(config.size) + config.write(data, 0) + assertArrayEquals(expectedConfig, data) + } } \ No newline at end of file