diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/flv/packet/VideoTag.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/flv/packet/VideoTag.kt index 2a8b24fef..e8e414ed5 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/flv/packet/VideoTag.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/flv/packet/VideoTag.kt @@ -18,8 +18,8 @@ package io.github.thibaultbee.streampack.internal.muxers.flv.packet import android.media.MediaFormat import io.github.thibaultbee.streampack.data.VideoConfig import io.github.thibaultbee.streampack.internal.utils.av.video.avc.AVCDecoderConfigurationRecord -import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCDecoderConfigurationRecord import io.github.thibaultbee.streampack.internal.utils.av.video.getStartCodeSize +import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCDecoderConfigurationRecord import io.github.thibaultbee.streampack.internal.utils.av.video.removeStartCode import io.github.thibaultbee.streampack.internal.utils.extensions.put import io.github.thibaultbee.streampack.internal.utils.extensions.putInt24 @@ -104,13 +104,11 @@ class VideoTag( AVCDecoderConfigurationRecord.fromParameterSets(buffers[0], buffers[1]) .write(buffer) } + MediaFormat.MIMETYPE_VIDEO_HEVC -> { - HEVCDecoderConfigurationRecord.fromParameterSets( - buffers[0], - buffers[1], - buffers[2] - ).write(buffer) + HEVCDecoderConfigurationRecord.fromParameterSets(buffers).write(buffer) } + else -> { throw IOException("Mimetype ${videoConfig.mimeType} is not supported") } @@ -131,9 +129,11 @@ class VideoTag( MediaFormat.MIMETYPE_VIDEO_AVC -> { AVCDecoderConfigurationRecord.getSize(buffers[0], buffers[1]) } + MediaFormat.MIMETYPE_VIDEO_HEVC -> { HEVCDecoderConfigurationRecord.getSize(buffers[0], buffers[1], buffers[2]) } + else -> { throw IOException("Mimetype ${videoConfig.mimeType} is not supported") } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/MP4MuxerHelper.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/MP4MuxerHelper.kt index a1411a5ac..bf55ab5ea 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/MP4MuxerHelper.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/MP4MuxerHelper.kt @@ -27,7 +27,7 @@ class MP4MuxerHelper : IMuxerHelper { class AudioMP4MuxerHelper : IAudioMuxerHelper { /** - * Get TS Muxer supported audio encoders list + * Get MP4 Muxer supported audio encoders list */ override val supportedEncoders = listOf(MediaFormat.MIMETYPE_AUDIO_AAC) @@ -39,10 +39,11 @@ class AudioMP4MuxerHelper : IAudioMuxerHelper { class VideoMP4MuxerHelper : IVideoMuxerHelper { /** - * Get TS Muxer supported video encoders list + * Get MP4 Muxer supported video encoders list */ override val supportedEncoders = listOf( - MediaFormat.MIMETYPE_VIDEO_AVC + MediaFormat.MIMETYPE_VIDEO_AVC, + MediaFormat.MIMETYPE_VIDEO_HEVC ) } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCConfigurationBox.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCConfigurationBox.kt new file mode 100644 index 000000000..b8fcd4b51 --- /dev/null +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCConfigurationBox.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Thibault B. + * + * 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 io.github.thibaultbee.streampack.internal.muxers.mp4.boxes + +import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCDecoderConfigurationRecord +import java.nio.ByteBuffer + +class HEVCConfigurationBox(private val config: HEVCDecoderConfigurationRecord) : Box("hvcC") { + override val size: Int = super.size + config.size + + override fun write(output: ByteBuffer) { + super.write(output) + config.write(output) + } +} \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/SampleEntry.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/SampleEntry.kt index e502e9e1a..ecec92221 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/SampleEntry.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/SampleEntry.kt @@ -104,6 +104,35 @@ class AVCSampleEntry( pasp ) +class HEVCSampleEntry( + resolution: Size, + horizontalResolution: Int = 72, + verticalResolution: Int = 72, + frameCount: Short = 1, + compressorName: String? = "HEVC Coding", + depth: Short = 0x0018, + hvcC: HEVCConfigurationBox, + btrt: BitRateBox? = null, + extensionDescriptorsBox: List = emptyList(), + clap: CleanApertureBox? = null, + pasp: PixelAspectRatioBox? = null +) : VisualSampleEntry( + "hvc1", + resolution, + horizontalResolution, + verticalResolution, + frameCount, + compressorName, + depth, + mutableListOf(hvcC).apply { + btrt?.let { add(it) } + addAll(extensionDescriptorsBox) + }, + clap, + pasp +) + + class MP4AudioSampleEntry( channelCount: Short, sampleSize: Short, diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/models/TrackChunks.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/models/TrackChunks.kt index ee31aa01f..91841a95a 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/models/TrackChunks.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/muxers/mp4/models/TrackChunks.kt @@ -21,7 +21,31 @@ import io.github.thibaultbee.streampack.data.AudioConfig import io.github.thibaultbee.streampack.data.Config import io.github.thibaultbee.streampack.data.VideoConfig import io.github.thibaultbee.streampack.internal.data.Frame -import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.* +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.AVCConfigurationBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.AVCSampleEntry +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.ChunkLargeOffsetBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.DataEntryUrlBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.DataInformationBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.DataReferenceBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.ESDSBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.HEVCConfigurationBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.HEVCSampleEntry +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.MP4AudioSampleEntry +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.MediaBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.MediaHeaderBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.MediaInformationBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.SampleDescriptionBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.SampleSizeBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.SampleTableBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.SampleToChunkBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.SyncSampleBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.TimeToSampleBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.TrackBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.TrackExtendsBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.TrackFragmentBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.TrackFragmentHeaderBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.TrackHeaderBox +import io.github.thibaultbee.streampack.internal.muxers.mp4.boxes.TrackRunBox import io.github.thibaultbee.streampack.internal.muxers.mp4.utils.createHandlerBox import io.github.thibaultbee.streampack.internal.muxers.mp4.utils.createTypeMediaHeaderBox import io.github.thibaultbee.streampack.internal.utils.TimeUtils @@ -29,6 +53,7 @@ import io.github.thibaultbee.streampack.internal.utils.av.descriptors.AudioSpeci import io.github.thibaultbee.streampack.internal.utils.av.descriptors.ESDescriptor import io.github.thibaultbee.streampack.internal.utils.av.descriptors.SLConfigDescriptor import io.github.thibaultbee.streampack.internal.utils.av.video.avc.AVCDecoderConfigurationRecord +import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCDecoderConfigurationRecord import io.github.thibaultbee.streampack.internal.utils.extensions.clone import java.nio.ByteBuffer @@ -51,6 +76,10 @@ class TrackChunks( this.extra.size == 2 } + MediaFormat.MIMETYPE_VIDEO_HEVC -> { + this.extra.size == 3 + } + MediaFormat.MIMETYPE_AUDIO_AAC -> { this.extra.size == 1 } @@ -162,7 +191,7 @@ class TrackChunks( it.id, it.numOfSamples, 1 - ) + ) ) } } catch (e: NoSuchElementException) { @@ -172,7 +201,7 @@ class TrackChunks( it.id, it.numOfSamples, 1 - ) + ) ) } } @@ -238,6 +267,21 @@ class TrackChunks( ), ) } + + MediaFormat.MIMETYPE_VIDEO_HEVC -> { + val extra = this.extra + require(extra.size == 3) { "For HEVC, extra must contains 3 parameter sets" } + (track.config as VideoConfig) + HEVCSampleEntry( + track.config.resolution, + hvcC = HEVCConfigurationBox( + HEVCDecoderConfigurationRecord.fromParameterSets( + extra.flatten() + ) + ), + ) + } + MediaFormat.MIMETYPE_AUDIO_AAC -> { (track.config as AudioConfig) MP4AudioSampleEntry( @@ -260,6 +304,7 @@ class TrackChunks( ) ) } + else -> throw IllegalArgumentException("Unsupported mimeType") } return SampleDescriptionBox(sampleEntry) diff --git a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/hevc/HEVCDecoderConfigurationRecord.kt b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/hevc/HEVCDecoderConfigurationRecord.kt index 9e8319f64..668ae22b1 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/hevc/HEVCDecoderConfigurationRecord.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/internal/utils/av/video/hevc/HEVCDecoderConfigurationRecord.kt @@ -22,6 +22,7 @@ import io.github.thibaultbee.streampack.internal.utils.extensions.put import io.github.thibaultbee.streampack.internal.utils.extensions.putLong48 import io.github.thibaultbee.streampack.internal.utils.extensions.putShort import io.github.thibaultbee.streampack.internal.utils.extensions.shl +import io.github.thibaultbee.streampack.internal.utils.extensions.shr import java.nio.ByteBuffer data class HEVCDecoderConfigurationRecord( @@ -42,13 +43,9 @@ data class HEVCDecoderConfigurationRecord( private val numTemporalLayers: Byte = 0, // 0 = Unknown private val temporalIdNested: Boolean = false, private val lengthSizeMinusOne: Byte = 3, - private val sps: List, - private val pps: List, - private val vps: List + private val parameterSets: List, ) { - private val spsNoStartCode = sps.map { it.removeStartCode() } - private val ppsNoStartCode = pps.map { it.removeStartCode() } - private val vpsNoStartCode = vps.map { it.removeStartCode() } + val size: Int = getSize(parameterSets) fun write(buffer: ByteBuffer) { buffer.put(configurationVersion) // configurationVersion @@ -77,17 +74,10 @@ data class HEVCDecoderConfigurationRecord( or lengthSizeMinusOne.toInt() ) // constantFrameRate 2 bits = 1 for stable / numTemporalLayers 3 bits / temporalIdNested 1 bit / lengthSizeMinusOne 2 bits - buffer.put(spsNoStartCode.size + ppsNoStartCode.size + vpsNoStartCode.size) // numOfArrays - spsNoStartCode.forEach { writeArray(buffer, it, NalUnitType.SPS) } - ppsNoStartCode.forEach { writeArray(buffer, it, NalUnitType.PPS) } - vpsNoStartCode.forEach { writeArray(buffer, it, NalUnitType.VPS) } - } - - private fun writeArray(buffer: ByteBuffer, nalUnit: ByteBuffer, nalUnitType: NalUnitType) { - buffer.put((1 shl 7) or nalUnitType.value.toInt()) // array_completeness + reserved 1bit + naluType 6 bytes - buffer.putShort(1) // numNalus - buffer.putShort(nalUnit.remaining().toShort()) // nalUnitLength - buffer.put(nalUnit) + buffer.put(parameterSets.size) // numOfArrays + parameterSets.forEach { + it.write(buffer) + } } companion object { @@ -108,13 +98,19 @@ data class HEVCDecoderConfigurationRecord( sps: List, pps: List, vps: List - ): HEVCDecoderConfigurationRecord { - val spsNoStartCode = sps.map { it.removeStartCode() } - val ppsNoStartCode = pps.map { it.removeStartCode() } - val vpsNoStartCode = vps.map { it.removeStartCode() } + ) = fromParameterSets(sps + pps + vps) + fun fromParameterSets( + parameterSets: List + ): HEVCDecoderConfigurationRecord { + val nalUnitParameterSets = parameterSets.map { + val nalType = (it[it.getStartCodeSize()] shr 1 and 0x3F).toByte() + val type = NalUnit.Type.fromValue(nalType) + NalUnit(type, it) + } // profile_tier_level - val parsedSps = SequenceParameterSets.parse(spsNoStartCode.first()) + val parsedSps = + SequenceParameterSets.parse(nalUnitParameterSets.first { it.type == NalUnit.Type.SPS }.noStartCodeData) return HEVCDecoderConfigurationRecord( generalProfileSpace = parsedSps.profileTierLevel.generalProfileSpace, generalTierFlag = parsedSps.profileTierLevel.generalTierFlag, @@ -126,9 +122,7 @@ data class HEVCDecoderConfigurationRecord( bitDepthLumaMinus8 = parsedSps.bitDepthLumaMinus8, bitDepthChromaMinus8 = parsedSps.bitDepthChromaMinus8, // TODO get minSpatialSegmentationIdc from VUI - sps = spsNoStartCode, - pps = ppsNoStartCode, - vps = vpsNoStartCode + parameterSets = nalUnitParameterSets ) } @@ -161,11 +155,42 @@ data class HEVCDecoderConfigurationRecord( return size } + + fun getSize(parameterSets: List): Int { + var size = + HEVC_DECODER_CONFIGURATION_RECORD_SIZE + parameterSets.forEach { + size += it.noStartCodeData.remaining() + HEVC_PARAMETER_SET_HEADER_SIZE + } + return size + } } - enum class NalUnitType(val value: Byte) { - VPS(32), - SPS(33), - PPS(34) + data class NalUnit(val type: Type, val data: ByteBuffer, val completeness: Boolean = true) { + val noStartCodeData: ByteBuffer = data.removeStartCode() + + fun write(buffer: ByteBuffer) { + buffer.put((completeness shl 7) or type.value.toInt()) // array_completeness + reserved 1bit + naluType 6 bytes + buffer.putShort(1) // numNalus + buffer.putShort(noStartCodeData.remaining().toShort()) // nalUnitLength + buffer.put(noStartCodeData) + } + + enum class Type(val value: Byte) { + VPS(32), + SPS(33), + PPS(34), + AUD(35), + EOS(36), + EOB(37), + FD(38), + PREFIX_SEI(39), + SUFFIX_SEI(40); + + companion object { + fun fromValue(value: Byte) = values().first { it.value == value } + + } + } } } \ No newline at end of file diff --git a/core/src/test/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCConfigurationBoxTest.kt b/core/src/test/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCConfigurationBoxTest.kt new file mode 100644 index 000000000..e4055ffd1 --- /dev/null +++ b/core/src/test/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCConfigurationBoxTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 Thibault B. + * + * 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 io.github.thibaultbee.streampack.internal.muxers.mp4.boxes + +import io.github.thibaultbee.streampack.internal.utils.av.video.ChromaFormat +import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCDecoderConfigurationRecord +import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCProfile +import io.github.thibaultbee.streampack.internal.utils.extensions.extractArray +import io.github.thibaultbee.streampack.utils.ResourcesUtils +import org.junit.Assert.assertArrayEquals +import org.junit.Test +import java.nio.ByteBuffer + +class HEVCConfigurationBoxTest { + @Test + fun `write valid hvcC test`() { + val vps = ByteBuffer.wrap( + byteArrayOf( + 0x40, + 0x01, + 0x0C, + 0x01, + 0xFF.toByte(), + 0xFF.toByte(), + 0x01, + 0x60, + 0x00, + 0x00, + 0x03, + 0x00, + 0x80.toByte(), + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x03, + 0x00, + 0x78, + 0x9D.toByte(), + 0xC0.toByte(), + 0x90.toByte() + ) + ) + val sps = ByteBuffer.wrap( + byteArrayOf( + 0x42, + 0x01, + 0x01, + 0x01, + 0x60, + 0x00, + 0x00, + 0x03, + 0x00, + 0x80.toByte(), + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x03, + 0x00, + 0x78, + 0xA0.toByte(), + 0x03, + 0xC0.toByte(), + 0x80.toByte(), + 0x32, + 0x16, + 0x59, + 0xDE.toByte(), + 0x49, + 0x1B, + 0x6B, + 0x80.toByte(), + 0x40, + 0x00, + 0x00, + 0xFA.toByte(), + 0x00, + 0x00, + 0x17, + 0x70, + 0x02 + ) + ) + val pps = ByteBuffer.wrap( + byteArrayOf( + 0x44, + 0x01, + 0xC1.toByte(), + 0x73, + 0xD1.toByte(), + 0x89.toByte() + ) + ) + val expectedBuffer = ResourcesUtils.readMP4ByteBuffer("hvcC.box") + val hvcC = HEVCConfigurationBox( + HEVCDecoderConfigurationRecord( + configurationVersion = 1, + generalProfileSpace = 0, + generalTierFlag = false, + generalProfileIdc = HEVCProfile.MAIN, + generalProfileCompatibilityFlags = 0x60000000, + generalConstraintIndicatorFlags = 0x800000000000, + generalLevelIdc = 120, + minSpatialSegmentationIdc = 0, + parallelismType = 0, + chromaFormat = ChromaFormat.YUV420, + bitDepthLumaMinus8 = 0, + bitDepthChromaMinus8 = 0, + averageFrameRate = 0, + constantFrameRate = 0, + numTemporalLayers = 1, + temporalIdNested = true, + lengthSizeMinusOne = 3, + parameterSets = listOf( + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.VPS, + vps + ), + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.SPS, + sps + ), + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.PPS, + pps + ) + ) + ) + ) + val buffer = hvcC.toByteBuffer() + assertArrayEquals(expectedBuffer.extractArray(), buffer.extractArray()) + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCSampleEntryTest.kt b/core/src/test/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCSampleEntryTest.kt new file mode 100644 index 000000000..973798d2f --- /dev/null +++ b/core/src/test/java/io/github/thibaultbee/streampack/internal/muxers/mp4/boxes/HEVCSampleEntryTest.kt @@ -0,0 +1,146 @@ +package io.github.thibaultbee.streampack.internal.muxers.mp4.boxes + +import io.github.thibaultbee.streampack.internal.utils.av.video.ChromaFormat +import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCDecoderConfigurationRecord +import io.github.thibaultbee.streampack.internal.utils.av.video.hevc.HEVCProfile +import io.github.thibaultbee.streampack.internal.utils.extensions.extractArray +import io.github.thibaultbee.streampack.utils.MockUtils +import io.github.thibaultbee.streampack.utils.ResourcesUtils +import org.junit.Assert +import org.junit.Test +import java.nio.ByteBuffer + +class HEVCSampleEntryTest { + @Test + fun `write valid avc1 test`() { + val vps = ByteBuffer.wrap( + byteArrayOf( + 0x40, + 0x01, + 0x0C, + 0x01, + 0xFF.toByte(), + 0xFF.toByte(), + 0x01, + 0x60, + 0x00, + 0x00, + 0x03, + 0x00, + 0x80.toByte(), + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x03, + 0x00, + 0x78, + 0x9D.toByte(), + 0xC0.toByte(), + 0x90.toByte() + ) + ) + val sps = ByteBuffer.wrap( + byteArrayOf( + 0x42, + 0x01, + 0x01, + 0x01, + 0x60, + 0x00, + 0x00, + 0x03, + 0x00, + 0x80.toByte(), + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x03, + 0x00, + 0x78, + 0xA0.toByte(), + 0x03, + 0xC0.toByte(), + 0x80.toByte(), + 0x32, + 0x16, + 0x59, + 0xDE.toByte(), + 0x49, + 0x1B, + 0x6B, + 0x80.toByte(), + 0x40, + 0x00, + 0x00, + 0xFA.toByte(), + 0x00, + 0x00, + 0x17, + 0x70, + 0x02 + ) + ) + val pps = ByteBuffer.wrap( + byteArrayOf( + 0x44, + 0x01, + 0xC1.toByte(), + 0x73, + 0xD1.toByte(), + 0x89.toByte() + ) + ) + + val expectedBuffer = ResourcesUtils.readMP4ByteBuffer("hvc1.box") + val hvcC = HEVCConfigurationBox( + HEVCDecoderConfigurationRecord( + configurationVersion = 1, + generalProfileSpace = 0, + generalTierFlag = false, + generalProfileIdc = HEVCProfile.MAIN, + generalProfileCompatibilityFlags = 0x60000000, + generalConstraintIndicatorFlags = 0x800000000000, + generalLevelIdc = 120, + minSpatialSegmentationIdc = 0, + parallelismType = 0, + chromaFormat = ChromaFormat.YUV420, + bitDepthLumaMinus8 = 0, + bitDepthChromaMinus8 = 0, + averageFrameRate = 0, + constantFrameRate = 0, + numTemporalLayers = 1, + temporalIdNested = true, + lengthSizeMinusOne = 3, + parameterSets = listOf( + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.VPS, + vps + ), + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.SPS, + sps + ), + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.PPS, + pps + ) + ) + ) + ) + val btrt = BitRateBox(25244, 801424, 402208) + + val hvc1 = + HEVCSampleEntry( + MockUtils.mockSize(1920, 800), + compressorName = null, + hvcC = hvcC, + btrt = btrt + ) + val buffer = hvc1.toByteBuffer() + Assert.assertArrayEquals(expectedBuffer.extractArray(), buffer.extractArray()) + } +} \ No newline at end of file diff --git a/core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/AVCDecoderConfigurationRecordTest.kt b/core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/avc/AVCDecoderConfigurationRecordTest.kt similarity index 97% rename from core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/AVCDecoderConfigurationRecordTest.kt rename to core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/avc/AVCDecoderConfigurationRecordTest.kt index e8e0c3785..54e0ebfb5 100644 --- a/core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/AVCDecoderConfigurationRecordTest.kt +++ b/core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/avc/AVCDecoderConfigurationRecordTest.kt @@ -1,6 +1,5 @@ -package io.github.thibaultbee.streampack.internal.utils.av.video +package io.github.thibaultbee.streampack.internal.utils.av.video.avc -import io.github.thibaultbee.streampack.internal.utils.av.video.avc.AVCDecoderConfigurationRecord import io.github.thibaultbee.streampack.internal.utils.extensions.extractArray import io.github.thibaultbee.streampack.utils.ResourcesUtils import org.junit.Assert.assertArrayEquals diff --git a/core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/hevc/HEVCDecoderConfigurationRecordTest.kt b/core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/hevc/HEVCDecoderConfigurationRecordTest.kt new file mode 100644 index 000000000..a5bf4cf55 --- /dev/null +++ b/core/src/test/java/io/github/thibaultbee/streampack/internal/utils/av/video/hevc/HEVCDecoderConfigurationRecordTest.kt @@ -0,0 +1,137 @@ +package io.github.thibaultbee.streampack.internal.utils.av.video.hevc + +import io.github.thibaultbee.streampack.internal.utils.av.video.ChromaFormat +import io.github.thibaultbee.streampack.internal.utils.extensions.extractArray +import io.github.thibaultbee.streampack.utils.ResourcesUtils +import org.junit.Assert +import org.junit.Test +import java.nio.ByteBuffer + +class HEVCDecoderConfigurationRecordTest { + @Test + fun `write valid HEVCDecoderConfigurationRecord from constructor test`() { + val vps = ByteBuffer.wrap( + byteArrayOf( + 0x40, + 0x01, + 0x0C, + 0x01, + 0xFF.toByte(), + 0xFF.toByte(), + 0x01, + 0x60, + 0x00, + 0x00, + 0x03, + 0x00, + 0x80.toByte(), + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x03, + 0x00, + 0x78, + 0x9D.toByte(), + 0xC0.toByte(), + 0x90.toByte() + ) + ) + val sps = ByteBuffer.wrap( + byteArrayOf( + 0x42, + 0x01, + 0x01, + 0x01, + 0x60, + 0x00, + 0x00, + 0x03, + 0x00, + 0x80.toByte(), + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x03, + 0x00, + 0x78, + 0xA0.toByte(), + 0x03, + 0xC0.toByte(), + 0x80.toByte(), + 0x32, + 0x16, + 0x59, + 0xDE.toByte(), + 0x49, + 0x1B, + 0x6B, + 0x80.toByte(), + 0x40, + 0x00, + 0x00, + 0xFA.toByte(), + 0x00, + 0x00, + 0x17, + 0x70, + 0x02 + ) + ) + val pps = ByteBuffer.wrap( + byteArrayOf( + 0x44, + 0x01, + 0xC1.toByte(), + 0x73, + 0xD1.toByte(), + 0x89.toByte() + ) + ) + val expectedBuffer = + ResourcesUtils.readByteBuffer("test-samples/video/HEVCDecoderConfigurationRecord") + val hevcDecoderConfigurationRecord = + HEVCDecoderConfigurationRecord( + configurationVersion = 1, + generalProfileSpace = 0, + generalTierFlag = false, + generalProfileIdc = HEVCProfile.MAIN, + generalProfileCompatibilityFlags = 0x60000000, + generalConstraintIndicatorFlags = 0x800000000000, + generalLevelIdc = 120, + minSpatialSegmentationIdc = 0, + parallelismType = 0, + chromaFormat = ChromaFormat.YUV420, + bitDepthLumaMinus8 = 0, + bitDepthChromaMinus8 = 0, + averageFrameRate = 0, + constantFrameRate = 0, + numTemporalLayers = 1, + temporalIdNested = true, + lengthSizeMinusOne = 3, + parameterSets = listOf( + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.VPS, + vps + ), + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.SPS, + sps + ), + HEVCDecoderConfigurationRecord.NalUnit( + HEVCDecoderConfigurationRecord.NalUnit.Type.PPS, + pps + ) + ) + ) + + val buffer = ByteBuffer.allocateDirect(hevcDecoderConfigurationRecord.size) + hevcDecoderConfigurationRecord.write(buffer) + buffer.rewind() + + Assert.assertArrayEquals(expectedBuffer.extractArray(), buffer.extractArray()) + } +} \ No newline at end of file diff --git a/core/src/test/resources/test-samples/muxer/mp4/hvc1.box b/core/src/test/resources/test-samples/muxer/mp4/hvc1.box new file mode 100644 index 000000000..209f57979 Binary files /dev/null and b/core/src/test/resources/test-samples/muxer/mp4/hvc1.box differ diff --git a/core/src/test/resources/test-samples/muxer/mp4/hvcC.box b/core/src/test/resources/test-samples/muxer/mp4/hvcC.box new file mode 100644 index 000000000..c2c0fb337 Binary files /dev/null and b/core/src/test/resources/test-samples/muxer/mp4/hvcC.box differ diff --git a/core/src/test/resources/test-samples/video/HEVCDecoderConfigurationRecord b/core/src/test/resources/test-samples/video/HEVCDecoderConfigurationRecord new file mode 100644 index 000000000..3c652fc0d Binary files /dev/null and b/core/src/test/resources/test-samples/video/HEVCDecoderConfigurationRecord differ