diff --git a/Sources/HPRTMP/Util/AVCDecoderConfigurationRecord.swift b/Sources/HPRTMP/Util/AVCDecoderConfigurationRecord.swift new file mode 100644 index 0000000..02600d4 --- /dev/null +++ b/Sources/HPRTMP/Util/AVCDecoderConfigurationRecord.swift @@ -0,0 +1,150 @@ +// +// AVCDecoderConfigurationRecord.swift +// +// +// Created by 郭 輝平 on 2023/10/16. +// + +import Foundation + +public struct AVCDecoderConfigurationRecord { + // The version of the AVCDecoderConfigurationRecord, usually set to 1. + private(set) var configurationVersion: UInt8 = 1 + + // Indicates the profile code as per the H.264 specification. + // This is extracted from the SPS NAL unit. + private(set) var avcProfileIndication: UInt8 = 0 + + // Indicates the compatibility of the stream. + // This is also extracted from the SPS NAL unit. + private(set) var profileCompatibility: UInt8 = 0 + + // Indicates the level code as per the H.264 specification. + // This is extracted from the SPS NAL unit. + private(set) var avcLevelIndication: UInt8 = 0 + + // Specifies the NAL unit length size minus one. + // Default is 3, which means the NAL unit length is 4 bytes. + private(set) var lengthSizeMinusOne: UInt8 = 3 + + // An array containing the SPS NAL units. + private(set) var spsList: [Data] = [] + + // An array containing the PPS NAL units. + private(set) var ppsList: [Data] = [] + + // Initialize with avcDecoderConfigurationRecord data + init?(avcDecoderConfigurationRecord data: Data) { + guard data.count > 6 else { + print("Invalid AVCDecoderConfigurationRecord data") + return nil + } + + var index = 0 + + // Parse the header + self.configurationVersion = data[index]; index += 1 + self.avcProfileIndication = data[index]; index += 1 + self.profileCompatibility = data[index]; index += 1 + self.avcLevelIndication = data[index]; index += 1 + self.lengthSizeMinusOne = data[index] & 0x03; index += 1 // Last 2 bits + let numOfSPS = data[index] & 0x1F; index += 1 // Last 5 bits + + // Parse SPS + for _ in 0.. index + 1 else { + print("Invalid SPS data") + return nil + } + + let spsLength = Int(data[index]) << 8 | Int(data[index + 1]) + index += 2 + + guard data.count >= index + spsLength else { + print("Invalid SPS data") + return nil + } + + let spsData = data[index..<(index + spsLength)] + self.spsList.append(Data(spsData)) + index += spsLength + } + + // Parse PPS + guard data.count > index else { + print("Invalid PPS data") + return nil + } + + let numOfPPS = data[index]; index += 1 + + for _ in 0.. index + 1 else { + print("Invalid PPS data") + return nil + } + + let ppsLength = Int(data[index]) << 8 | Int(data[index + 1]) + index += 2 + + guard data.count >= index + ppsLength else { + print("Invalid PPS data") + return nil + } + + let ppsData = data[index..<(index + ppsLength)] + self.ppsList.append(Data(ppsData)) + index += ppsLength + } + } + + // Initialize with SPS and PPS data + init(sps: Data, pps: Data) { + self.avcProfileIndication = sps[1] + self.profileCompatibility = sps[2] + self.avcLevelIndication = sps[3] + + // In the context of media containers like MP4 or streaming protocols like HLS, the lengthSizeMinusOne value is often set to 3, meaning that each NALU length is represented using 4 bytes (3 + 1). However, this value can also be 0, 1, or 2, representing 1, 2, or 3 bytes respectively. + self.lengthSizeMinusOne = 3 + + self.spsList = [sps] + self.ppsList = [pps] + } + + // Method to generate avcDecoderConfigurationRecord data + func generateConfigurationRecord() -> Data { + var body = Data() + + body.append(configurationVersion) + body.append(avcProfileIndication) + body.append(profileCompatibility) + body.append(avcLevelIndication) + + body.append(0b11111100 | (self.lengthSizeMinusOne & 0b00000011)) + + /*sps*/ + + // numOfSequenceParameterSets + let numOfSequenceParameterSets = 0b11100000 | (UInt8(spsList.count) & 0b00011111) + body.append(Data([numOfSequenceParameterSets])) + + for sps in spsList { + // sequenceParameterSetLength + body.append(UInt16(sps.count).bigEndian.data) + // sequenceParameterSetNALUnit + body.append(Data(sps)) + } + + /*pps*/ + // numOfPictureParameterSets + body.append(UInt8(ppsList.count)) + for pps in ppsList { + // pictureParameterSetLength + body.append(UInt16(pps.count).bigEndian.data) + // pictureParameterSetNALUnit + body.append(Data(pps)) + } + + return body + } +} diff --git a/Tests/HPRTMPTests/util/AVCDecoderConfigurationRecordTests.swift b/Tests/HPRTMPTests/util/AVCDecoderConfigurationRecordTests.swift new file mode 100644 index 0000000..f59b5cd --- /dev/null +++ b/Tests/HPRTMPTests/util/AVCDecoderConfigurationRecordTests.swift @@ -0,0 +1,102 @@ +// +// File.swift +// +// +// Created by 郭 輝平 on 2023/10/16. +// + +import Foundation +import XCTest + +@testable import HPRTMP + +class AVCDecoderConfigurationRecordTests: XCTestCase { + + func testInitWithValidData() { + // Prepare a mock AVCDecoderConfigurationRecord data + let mockData: Data = Data([0x01, 0x42, 0x00, 0x1E, 0xFF, 0xE1, 0x00, 0x04, 0x67, 0x42, 0x00, 0x1E, 0x01, 0x00, 0x04, 0x68, 0x00, 0x00, 0x0D]) + + // Initialize AVCDecoderConfigurationRecord + let avcRecord = AVCDecoderConfigurationRecord(avcDecoderConfigurationRecord: mockData) + + // Validate + XCTAssertNotNil(avcRecord) + XCTAssertEqual(avcRecord?.configurationVersion, 1) + XCTAssertEqual(avcRecord?.avcProfileIndication, 66) + XCTAssertEqual(avcRecord?.profileCompatibility, 0) + XCTAssertEqual(avcRecord?.avcLevelIndication, 30) + XCTAssertEqual(avcRecord?.lengthSizeMinusOne, 3) + XCTAssertEqual(avcRecord?.ppsList.count, 1) + XCTAssertEqual(avcRecord?.spsList.count, 1) + XCTAssertEqual(avcRecord?.spsList.first, Data([103, 66, 0, 30])) + XCTAssertEqual(avcRecord?.ppsList.first, Data([104, 0, 0, 13])) + } + + func testInitWithInvalidData() { + // Prepare an invalid mock AVCDecoderConfigurationRecord data + let mockData: Data = Data([1, 66]) // Too short to be valid + + // Initialize AVCDecoderConfigurationRecord + let avcRecord = AVCDecoderConfigurationRecord(avcDecoderConfigurationRecord: mockData) + + // Validate + XCTAssertNil(avcRecord) + } + + func testInitWithSPSAndPPS() { + // Prepare mock SPS and PPS data + let sps: Data = Data([103, 66, 0, 30]) + let pps: Data = Data([104, 0, 0, 13]) + + // Initialize AVCDecoderConfigurationRecord + let avcRecord = AVCDecoderConfigurationRecord(sps: sps, pps: pps) + + // Validate + XCTAssertNotNil(avcRecord) + XCTAssertEqual(avcRecord.configurationVersion, 1) + XCTAssertEqual(avcRecord.avcProfileIndication, 66) + XCTAssertEqual(avcRecord.profileCompatibility, 0) + XCTAssertEqual(avcRecord.avcLevelIndication, 30) + XCTAssertEqual(avcRecord.lengthSizeMinusOne, 3) + XCTAssertEqual(avcRecord.ppsList.count, 1) + XCTAssertEqual(avcRecord.spsList.count, 1) + XCTAssertEqual(avcRecord.spsList.first, sps) + XCTAssertEqual(avcRecord.ppsList.first, pps) + } + + func testGenerateConfigurationRecord() { + // Prepare mock SPS and PPS data + let sps: Data = Data([103, 66, 0, 30]) + let pps: Data = Data([104, 0, 0, 13]) + + // Initialize AVCDecoderConfigurationRecord + let avcRecord = AVCDecoderConfigurationRecord(sps: sps, pps: pps) + + // Generate Configuration Record + let generatedData = avcRecord.generateConfigurationRecord() + + // Validate + // The expected data would depend on how you've implemented generateConfigurationRecord + // For this example, let's assume it concatenates all the fields and SPS/PPS data + var expectedData = Data() + expectedData.append(1) // configurationVersion + expectedData.append(sps[1]) // avcProfileIndication + expectedData.append(sps[2]) // profileCompatibility + expectedData.append(sps[3]) // avcLevelIndication + expectedData.append(0xFF) // 6 bits reserved (111111) + 2 bits lengthSizeMinusOne (11) + expectedData.append(0xE1) // 3 bits reserved (111) + 5 bits numOfSPS (00001) + + // SPS + let spsLengthData = UInt16(sps.count).bigEndian.data + expectedData.append(spsLengthData) + expectedData.append(sps) + + // PPS + expectedData.append(1) // numOfPPS + let ppsLengthData = UInt16(pps.count).bigEndian.data + expectedData.append(ppsLengthData) + expectedData.append(pps) + + XCTAssertEqual(generatedData, expectedData) + } +}