비디오 인코딩 및 디코딩에 대한 VideoToolbox의 추가 제어 - 섹션 2.

본고에서 나는 MacOS 응용 프로그램의 구조를 소개했는데 이 응용 프로그램은VideoToolbox를 사용하여 영상을 인코딩하고 디코딩한다.이번에는 실제 인코딩 과정에 중점을 두고 싶습니다.나는 또한 기존 코드를 개선하고 재구성하는 방법을 연구하고, 이러한 프로젝트를 구축할 수 있는 여러 가지 방법을 기억할 것이다.
우리가 지난 글에서 이 프로젝트를 떠났을 때, 우리는 카메라에서 나온 영상을 인코딩해서 직접 다른 사람에게 인코딩할 수 있는 맥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 discussionthis one가 매우 유용하다는 것을 발견할 수 있다.
여기서 이 문제를 다시 한 번 이야기하다.
이 문서에 포함된 저장소here를 사용할 수 있습니다.
앨런 알라드는 유럽에서 앞장서는 독립 컨설팅 회사인 아이빈 테크놀로지의 개발상으로 동영상 기술과 미디어 발행에 전문적으로 종사한다.
Dellteam of video developers은 개발 및 구현 과정에서 도움이 필요한 경우 기꺼이 도움을 드립니다.만약 당신에게 어떤 문제나 의견이 있다면, 평론 부분에서 우리에게 메시지를 남겨 주십시오.

좋은 웹페이지 즐겨찾기