-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add AVCDecoderConfigurationRecord parser
- Loading branch information
1 parent
9ad6dbb
commit 43e2cb1
Showing
2 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
150 changes: 150 additions & 0 deletions
150
Sources/HPRTMP/Util/AVCDecoderConfigurationRecord.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
102
Tests/HPRTMPTests/util/AVCDecoderConfigurationRecordTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |