비디오 인코딩 및 디코딩에 대한 VideoToolbox의 추가 제어 - 섹션 2.
12493 단어 macosvideotoolboxavfoundationswift
우리가 지난 글에서 이 프로젝트를 떠났을 때, 우리는 카메라에서 나온 영상을 인코딩해서 직접 다른 사람에게 인코딩할 수 있는 맥OS 프로그램을 만들었다.이런 유치한 실현에서 우리는 기본적으로 AVFoundation이 당신을 위해 할 수 있는 일과 같은 일을 하고 있으며, 심지어는 AVFoundation이 당신을 위해 할 수 있는 일보다 더 적은 일을 하고 있다.기본적으로 AVFoundation은 하드웨어의 압축을 가속화하고 압축을 풀 수 있는 접근을 제공합니다.그러나 당신이 얻지 못한 것은 인코딩과 디코딩을 미세하게 조정하고 맞춤형으로 만드는 능력이다.VideoToolbox의 모든 의미입니다.인코딩 과정에 어떻게 접근하는지 보여 줍니다.내가 본문에서 한 변경은 나의 지점
encoder-improvements
을 합병한 것이다.나는 내가 채택한 몇 가지 방법의 기존 항목을 다시 한 번 언급했다.이 예에서는 this iOS project 의 인코더 코드를 Objective-C에서 Swift 5로 업데이트했습니다.첫 번째 개선은 인코더를 사용하기 전에 설정하는 것입니다.
func prepareToEncodeFrames() {
let encoderSpecification = [
kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder: true as CFBoolean
] as CFDictionary
let status = VTCompressionSessionCreate(allocator: kCFAllocatorDefault, width: self.width, height: self.height, codecType: kCMVideoCodecType_H264, encoderSpecification: encoderSpecification, imageBufferAttributes: nil, compressedDataAllocator: nil, outputCallback: outputCallback, refcon: Unmanaged.passUnretained(self).toOpaque(), compressionSessionOut: &session)
print("H264Coder init \(status == noErr) \(status)")
// This demonstrates setting a property after the session has been created
guard let compressionSession = session else { return }
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_RealTime, value: kCFBooleanTrue)
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_ProfileLevel, value: kVTProfileLevel_H264_Main_AutoLevel)
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_AllowFrameReordering, value: kCFBooleanFalse)
VTSessionSetProperty(compressionSession, key: kVTCompressionPropertyKey_ExpectedFrameRate, value: CFNumberCreate(kCFAllocatorDefault, CFNumberType.intType, &self.fps))
VTCompressionSessionPrepareToEncodeFrames(compressionSession)
}
여기에는 방법VTCompressionSessionPrepareToEncodeFrames(compressionSession)
을 포장하여 Apple docs에서 이 방법에 대한 내용을 더 많이 읽을 수 있습니다.인코딩을 시작하기 전에, 우리는 이 기회를 이용하여 세션 속성을 설정했다.대부분의 경우 인코더를 만들 때 appDelegate에서 다음과 같이 설정하는 클래스에서 공개된 속성의 예입니다.
// Create encoder here (at the expense of dynamic setting of height and width)
encoder = H264Encoder(width: 1280, height: 720, callback: { encodedBuffer in
// self.sampleBufferNoOpProcessor(encodedBuffer) // Logs the buffers to the console for inspection
// self.decodeCompressedFrame(encodedBuffer) // uncomment to see decoded video
})
encoder?.delegate = self
encoder?.fps = 15
encoder?.prepareToEncodeFrames()
(이곳에서 설명한 바와 같이 앞의 코드에서 우리는 전송된 데이터 버퍼에 따라 동적으로 너비와 높이를 설정할 수 있습니다. 다른 설명을 위해서, 나는 여기에서 너비와 높이를 희생했지만, 다른 프로그램에서 이 기능을 유지하는 방법을 찾아야 할 수도 있습니다.)나중에 처리할 수 있도록 appDelegate에서 압축 데이터를 받고 싶습니다.이를 위해, 인코더에 생성된 두 개의 의뢰 함수에 확장자를 만들었습니다.먼저 인코더 프로토콜:
protocol H264EncoderDelegate: AnyObject {
func dataCallBack(_ data: Data!, frameType: FrameType)
func spsppsDataCallBack(_ sps:Data!, pps: Data!)
}
확장성:extension AppDelegate : H264EncoderDelegate {
func dataCallBack(_ data: Data!, frameType: FrameType) {
let byteHeader:[UInt8] = [0,0,0,1]
var byteHeaderData = Data(byteHeader)
byteHeaderData.append(data)
// Could decode here
// H264Decoder.decode(byteHeaderData)
}
func spsppsDataCallBack(_ sps: Data!, pps: Data!) {
let spsbyteHeader:[UInt8] = [0,0,0,1]
var spsbyteHeaderData = Data(spsbyteHeader)
var ppsbyteHeaderData = Data(spsbyteHeader)
spsbyteHeaderData.append(sps)
ppsbyteHeaderData.append(pps)
// Could decode here
// H264Decoder.decode(spsbyteHeaderData)
// H264Decoder.decode(ppsbyteHeaderData)
}
}
우리는 곧 이 바이트 헤더에 대해 토론할 것이다.여기서 기존 AVManagerDelegate를 깔끔하게 유지하기 위해 확장할 수도 있습니다.
// MARK: - AVManagerDelegate
extension AppDelegate : AVManagerDelegate {
func onSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
cameraView.render(sampleBuffer)
encoder?.encode(sampleBuffer)
}
}
간단히 말하면, 우리는 기존의 버퍼를 버퍼로 인코딩하는 것을 잠시 보류하고, 매번 상술한 버퍼를 호출할 수 있도록 리셋 방법을 확장할 것이다.전체적인 계획에 관심을 가지기 위해 우리가 여기서 하는 것은 데이터 준비(코딩)를 기본 흐름으로 하는 것이다.Sample Buffer에서 받은 데이터는 AVCC 형식이고, 출력하고자 하는 형식은 이른바 첨부파일 B 형식의 기본 흐름입니다.우리가 리셋에서 한 모든 것은 AVCC 형식에서 부록 B 형식으로 전환하는 것과 관련이 있으며, 이 과정의 세부 사항을 여러 방식으로 조정할 수 있습니다.만약 샘플 버퍼에 관건적인 프레임이 포함된다면, 우리는 디코더가 이 프레임을 수신할 때 이 프레임을 어떻게 처리해야 하는지를 설명하는 데이터를 포함할 수도 있다.
리콜의 첫 번째 부분은 다음과 같습니다.
let outputCallback: VTCompressionOutputCallback = { refcon, sourceFrameRefCon, status, infoFlags, sampleBuffer in
guard let refcon = refcon,
status == noErr,
let sampleBuffer = sampleBuffer else {
print("H264Coder outputCallback sampleBuffer NULL or status: \(status)")
return
}
if (!CMSampleBufferDataIsReady(sampleBuffer))
{
print("didCompressH264 data is not ready...");
return;
}
let encoder: H264Encoder = Unmanaged<H264Encoder>.fromOpaque(refcon).takeUnretainedValue()
if(encoder.shouldUnpack) {
var isKeyFrame:Bool = false
// Attempting to get keyFrame
guard let attachmentsArray:CFArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) else { return }
if (CFArrayGetCount(attachmentsArray) > 0) {
let cfDict = CFArrayGetValueAtIndex(attachmentsArray, 0)
let dictRef: CFDictionary = unsafeBitCast(cfDict, to: CFDictionary.self)
let value = CFDictionaryGetValue(dictRef, unsafeBitCast(kCMSampleAttachmentKey_NotSync, to: UnsafeRawPointer.self))
if(value == nil) {
isKeyFrame = true
}
}
(encoder 속성shouldUnpack을 주의하십시오. 이 모든 패키지 해제 코드는 필요에 따라 활성화할 수 있도록 if 문장에 봉인됩니다.)리셋 함수는 호출할 때마다 예시 버퍼를 받습니다. 처리할 준비가 되어 있는지 확인해야 합니다.다음에 우리는 버퍼에 어떤 데이터가 있는지 알아야 한다.이를 위해서는 버퍼에 있는 이른바 '첨부파일' 을 보아야 한다. 본질적으로 데이터 정보를 제공하는 사전이다.사용한 많은 첨부 파일 키의 목록을 볼 수 있습니다. here우리가 필요로 하는 것은
kCMSampleAttachmentKey_NotSync
이다. 우리가 처리하고 있는 데이터가 관건적인 프레임이라는 것을 보여주는 것이 부족하다.(보시다시피 Swift에서 CFDictionary를 사용하면 혼란스러울 수 있습니다.)따라서 관건 프레임 샘플 데이터에 포함된 데이터;일단 우리가 관건적인 프레임을 처리하고 있다는 것을 알게 되면, 우리는 버퍼에서 두 그룹의 데이터를 추출할 수 있다.이것들은 시퀀스 매개 변수 집합과 그림 매개 변수 집합이다.키프레임이 있는 버퍼에는 두 데이터 세트가 모두 포함됩니다.
이러한 정보를 추출하여 SPS 및 PPS 콜백에 전송하는 방법은 다음과 같습니다.
if(isKeyFrame) {
var description: CMFormatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)!
// First, get SPS
var sparamSetCount: size_t = 0
var sparamSetSize: size_t = 0
var sparameterSetPointer: UnsafePointer<UInt8>?
var statusCode: OSStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, parameterSetIndex: 0, parameterSetPointerOut: &sparameterSetPointer, parameterSetSizeOut: &sparamSetSize, parameterSetCountOut: &sparamSetCount, nalUnitHeaderLengthOut: nil)
if(statusCode == noErr) {
// Then, get PPS
var pparamSetCount: size_t = 0
var pparamSetSize: size_t = 0
var pparameterSetPointer: UnsafePointer<UInt8>?
var statusCode: OSStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(description, parameterSetIndex: 0, parameterSetPointerOut: &pparameterSetPointer, parameterSetSizeOut: &pparamSetSize, parameterSetCountOut: &pparamSetCount, nalUnitHeaderLengthOut: nil)
if(statusCode == noErr) {
var sps = NSData(bytes: sparameterSetPointer, length: sparamSetSize)
var pps = NSData(bytes: pparameterSetPointer, length: pparamSetSize)
encoder.delegate?.spsppsDataCallBack(sps as Data, pps: pps as Data)
}
}
}
일단 디코더가 이 정보를 받게 되면, 디코더를 어떻게 정확하게 처리하는지 알게 될 것이다. (앞에서 지적한 바와 같이, 우리는 상술한 리셋에서 디코더를 호출한다.)우리는 앞에서 언급한 바이트 헤더로 돌아갈 수 있다. 기본 흐름에 대해 모든 NAL 단원 (기본적으로 모든 데이터 패키지) 은 반드시 [0,0,0,1]의 바이트 헤더 그룹으로 시작해야 한다.따라서 이 제목으로 데이터를 미리 추가합니다.그런 다음 실제 이미지 데이터를 처리하여 콜백 함수로 보내 키프레임(PFrame) 또는 비키프레임(Iframe)인지 여부를 나타낼 수 있습니다.
var dataBuffer: CMBlockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer)!
var length: size_t = 0
var totalLength: size_t = 0
var bufferDataPointer: UnsafeMutablePointer<Int8>?
var statusCodePtr: OSStatus = CMBlockBufferGetDataPointer(dataBuffer, atOffset: 0, lengthAtOffsetOut: &length, totalLengthOut: &totalLength, dataPointerOut: &bufferDataPointer)
if(statusCodePtr == noErr) {
var bufferOffset: size_t = 0
let AVCCHeaderLength: Int = 4
while(bufferOffset < totalLength - AVCCHeaderLength) {
// Read the NAL unit length
var NALUnitLength: UInt32 = 0
memcpy(&NALUnitLength, bufferDataPointer! + bufferOffset, AVCCHeaderLength)
//Big-Endian to Little-Endian
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength)
var data = NSData(bytes:(bufferDataPointer! + bufferOffset + AVCCHeaderLength), length: Int(Int32(NALUnitLength)))
var frameType: FrameType = .FrameType_PFrame
var dataBytes = Data(bytes: data.bytes, count: data.length)
if((dataBytes[0] & 0x1F) == 5) {
// I-Frame
print("is IFrame")
frameType = .FrameType_IFrame
}
encoder.delegate?.dataCallBack(data as Data, frameType: frameType)
// Move to the next NAL unit in the block buffer
bufferOffset += AVCCHeaderLength + size_t(NALUnitLength);
}
}
}
여기에 몇 가지 중요한 세부 사항이 있다.기본 흐름을 구축할 때, 우리는 모든 데이터 패키지의 길이를 요구로 삼아야 한다.그래서 우리는 이 길이를 얻어야 한다. 위 코드의 주석인 'NAL 단위 길이 읽기' 를 참고하여memcpy를 어떻게 사용해서 이 점을 실현하는지 이해하십시오.그리고 우리는 데이터를 큰 쪽에서 작은 쪽으로 전환해야 한다. 핵심기금회는 이런 방법을 제공했다CFSwapInt32BigToHost(NALUnitLength)
.이 길이를 사용하여 버퍼의 다음 NAL 셀로 이동할 수 있습니다.마지막 세부 사항은 데이터 Bytes 변수에 대한 분석입니다. 이것은 우리가 iframe을 처리하고 있는지 여부를 알 수 있는 편리한 방법입니다. 이것은 데이터 Callback에서 사용할 수 있도록 프레임 type 변수에 대한 정보를 업데이트하는 데 사용됩니다.
그래서 이 단계에서 우리는 기본 데이터 흐름을 처리하는 데 필요한 모든 것을 얻었다.우리는 현재 이 데이터를 우리가 선택한 디코더에 보낼 수 있다.다음 글은 decoder 클래스의 업데이트 버전을 사용하여 데이터를 디코딩할 것입니다.세 번째 부분이 곧 시작됩니다...
깊이 파헤치고 싶은 사람들은 this SO discussion와this one가 매우 유용하다는 것을 발견할 수 있다.
여기서 이 문제를 다시 한 번 이야기하다.
이 문서에 포함된 저장소here를 사용할 수 있습니다.
앨런 알라드는 유럽에서 앞장서는 독립 컨설팅 회사인 아이빈 테크놀로지의 개발상으로 동영상 기술과 미디어 발행에 전문적으로 종사한다.
Dellteam of video developers은 개발 및 구현 과정에서 도움이 필요한 경우 기꺼이 도움을 드립니다.만약 당신에게 어떤 문제나 의견이 있다면, 평론 부분에서 우리에게 메시지를 남겨 주십시오.
Reference
이 문제에 관하여(비디오 인코딩 및 디코딩에 대한 VideoToolbox의 추가 제어 - 섹션 2.), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/video/working-with-videotoolbox-for-more-control-over-video-encoding-and-decoding-part-2-1b5k텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)