Skip to content

Commit

Permalink
add AVCDecoderConfigurationRecord parser
Browse files Browse the repository at this point in the history
  • Loading branch information
huiping192 committed May 12, 2024
1 parent 9ad6dbb commit 43e2cb1
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 0 deletions.
150 changes: 150 additions & 0 deletions Sources/HPRTMP/Util/AVCDecoderConfigurationRecord.swift
Original file line number Diff line number Diff line change
@@ -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..<numOfSPS {
guard data.count > 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..<numOfPPS {
guard data.count > 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
}
}
102 changes: 102 additions & 0 deletions Tests/HPRTMPTests/util/AVCDecoderConfigurationRecordTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 43e2cb1

Please sign in to comment.