ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • iOS 에서 H.264 Elementary Stream to MPEG4 포맷으로 변환
    iOS Swift 2023. 6. 7. 16:41

    RTP로 H.264 영상을 수신하게 되면 iOS는 해당 스트림을 재생할 수 없습니다. iOS는 MPEG-4 포맷의 데이터를 CMSampleBuffer 형태로 관리하는 데 이를 위해서는 RTP로 수신된 데이터의 NALU을 파싱해 해당 CMSampleBuffer 형태로 변환해 주어야 합니다. 

    우선 CMSampleBuffer에 대해서 살펴 보겠습니다. 

    CMSampleBuffer

    CMSampleBuffer

    샘플버퍼는 미디어 파이프라인을 통해 샘플데이터를 이동하는데 사용하는 CoreFoundation 객체입니다. CMSampleBuffer 인스턴스에는 특정 미디어 타입의 압축되거나 압축되지 않은 샘플이 하나 이상 포함되어 있습니다. 해당 샘플 버퍼가 포함할 수 있는 객체는 CMBlockBuffer, CVImageBffer 입니다. 또한 해당 샘플 버퍼에 대한 정보를 위한 CMVideoFormatDescription, 시간 정보를 위한 CMTime 값이 추가됩니다. 간단하게 아래와 같은 구조를 갖습니다. 

    해당 CMSampleBuffer는 AVSampleBufferDisplayLayer를 통해서 디스플레이 될 수 있습니다. 

    AVSampleBufferDisplayLayer에는 디코더가 내장되어 있어 해당 H.264 스트림을 디코딩에 영상을 화면에 보여 줍니다. 

     

    H.264 Syntax

    다음으로 살펴볼 내용은 H.264 가 네트워크로 통해서 들어온 데이터 스트림에 대한 내용입니다. RTP 를 통해 H.264 스트림은 NAL 타입으 스트림으로 NAL 유닛의 연속적인 스트림입니다. 해당 영상에서 SPS, PPS 와 같은 H.264 스트림에 대한 파라미터 셋과 실제 비디오 프레임에 대한 슬라이스가 포함 되어 있습니다. 우리는 이 NAL 스트림을 MPEG4 스트림으로 변환해 CMSampleBuffer 형식으로 만드는 게 목적입니다. NAL Unit 의 값을 파싱해 해당 유닛의 타입을 확인 할 수 있는데 보통 아래와 같은 형태를 갖습니다. 

    NAL Unit 타입은 NAL 유닛의 첫번째 바이트의 하위 5비트로 판단합니다. 

    public enum NaluType : UInt8 { //5bit
        case kSlice = 1
        case kIdr = 5
        case kSei = 6
        case kSps = 7
        case kPps = 8
    }
    
    public func parseNaluType(byte: UInt8) -> NaluType? {
        return NaluType(rawValue: (byte & 1f))
    }

    CMSampleFormatDescription

    가장 먼저 해야할 일은 H.264의 파라미터 셋인 SPS와 PPS를 가지고 CMSampleFormatDescription을 생성해야 합니다. 

        //read sps + pps
        var sps: UnsafePointer<UInt8>? = nil
        var spsLength: Int32 = 0
        if !reader.readNalUnit(buffer: &sps, count: &spsLength) {
            print("read fail to sps")
            return nil
        }
    
        var pps: UnsafePointer<UInt8>? = nil
        var ppsLength: Int32 = 0
        
        if !reader.readNalUnit(buffer: &pps, count: &ppsLength) {
            print("read fail to pps")
            return nil
        }
    
        var description: CMVideoFormatDescription? = nil
        let  parameterSetPointers : [UnsafePointer<UInt8>] = [sps!, pps!]
        let parameterSetSize: [Int] = [Int(spsLength), Int(ppsLength)]
        let status = CMVideoFormatDescriptionCreateFromH264ParameterSets(allocator: kCFAllocatorDefault,
                                                            parameterSetCount: 2,
                                                            parameterSetPointers: parameterSetPointers,
                                                            parameterSetSizes:
                                                                parameterSetSize,
                                                            nalUnitHeaderLength: 4,
                                                            formatDescriptionOut: &description)

    위에서 처럼 parameterSetPoints : [UnsafePoint<UInt8>] = [sps!, pps!]를 전달해 파라미터셋을 전달해 CMVideoFormatDescriptionCreateFromH264ParameterSets() 함수를 호출합니다. 성공하면 인수로 전달된 description에 해당 

    영상에 대한 CMVideoFormatDescription이 생성됩니다. 

    해당 CMVideoFormatDescription은 이제 비디오 프레임에서 계속 사용됩니다. 만약 sps, pps 값이 변경된다면 그 때 다시 생성해 설정해 줍니다. 

     

    이제 CMBlockBuffer설정해 수신된 VideoFrame을 MPEG4 형태로 변경해 주어야 합니다. MPEG4 포맷은 상위 2바이트를 해당 영상의 크기를 지정하는 데 사용하고 다음에 비디오 프레임을 추가하는 방식으로 동작합니다. 

     //수신된 데이터를 저장할 블럭 버퍼 생성
        var block_buffer: CMBlockBuffer? = nil
        let block_allocator = CMMemoryPoolGetAllocator(memory_pool)
        var status = CMBlockBufferCreateWithMemoryBlock(allocator: kCFAllocatorDefault,
                                                        memoryBlock: nil,
                                                        blockLength: reader.remainBytes(),
                                                        blockAllocator: block_allocator,
                                                        customBlockSource: nil,
                                                        offsetToData: 0,
                                                        dataLength: reader.remainBytes(),
                                                        flags: kCMBlockBufferAssureMemoryNowFlag,
                                                        blockBufferOut: &block_buffer)
        
        if status != kCMBlockBufferNoErr {
            print("fail to create block buffer.")
            return false
        }
        
        var contiguous_buffer : CMBlockBuffer? = nil
        if(!CMBlockBufferIsRangeContiguous(block_buffer!, atOffset: 0, length: 0)) {
            status = CMBlockBufferCreateContiguous(allocator: kCFAllocatorDefault,
                                                   sourceBuffer: block_buffer!,
                                                   blockAllocator: block_allocator,
                                                   customBlockSource: nil,
                                                   offsetToData: 0,
                                                   dataLength: 0,
                                                   flags: 0,
                                                   blockBufferOut: &contiguous_buffer)
            if status != noErr {
                print("fail to flatten non-contiguous block buffer: \(status)")
                return false
            }
        } else {
            contiguous_buffer = block_buffer
            block_buffer = nil
        }
        
        //블럭 버퍼의 시작 위치와 크기 확인
        var block_buffer_size: Int = 0
        var data_ptr: UnsafeMutablePointer<Int8>? = nil
        status = CMBlockBufferGetDataPointer(contiguous_buffer!,
                                             atOffset: 0,
                                             lengthAtOffsetOut: nil,
                                             totalLengthOut: &block_buffer_size,
                                             dataPointerOut: &data_ptr)
        if status != kCMBlockBufferNoErr {
            print("fail to get block buffer data pointer")
            return false
        }
        
        //저장할 크기와 다른 블럭버퍼가 할당되었다면 에러 처리
        if block_buffer_size != reader.remainBytes() {
            print("allocation buffer size is narrow")
            return false
            
        }
        
        guard let ptr = data_ptr?.withMemoryRebound(to: UInt8.self, capacity: block_buffer_size, {
                                                        return $0 }) else { return false }
        //버퍼의 시작위치와 버퍼의 크기를 이용해 AVCC 버퍼 형식으로 변환할 writer 생성
        let writer = AvccBufferWriter(buffer: ptr, count: block_buffer_size)
        while (reader.remainBytes() > 0) {
            var nalu_data_ptr: UnsafePointer<UInt8>? = nil
            var nalu_data_len: Int32 = 0
            //NalUnit의 정보를 읽고 해당 데이터를 avcc 형식으로 버퍼에 쓰기
            if reader.readNalUnit(buffer: &nalu_data_ptr, count: &nalu_data_len) {
                _ = writer.writeNalu(data: nalu_data_ptr!, count: Int(nalu_data_len))
            }
        }
        
        let timestamp = Int64(presentationTime * kNumNanosecsPerSec)
        var timing = CMSampleTimingInfo(duration: .invalid,
                                        presentationTimeStamp: CMTimeMake(value: timestamp, timescale: Int32(kNumNanosecsPerSec)),
                                        decodeTimeStamp: .invalid)
        //out_sample_buffer 에 CMSampleBuffer 생성
        status = CMSampleBufferCreate(allocator: kCFAllocatorDefault,
                                      dataBuffer: contiguous_buffer,
                                      dataReady: true,
                                      makeDataReadyCallback: nil,
                                      refcon: nil,
                                      formatDescription: video_format,
                                      sampleCount: 1,
                                      sampleTimingEntryCount: 1,
                                      sampleTimingArray: &timing,
                                      sampleSizeEntryCount: 0,
                                      sampleSizeArray: nil,
                                      sampleBufferOut: &out_sample_buffer);

    이제 생성된 블록을 AVSampleBufferDisplayLink에 enque를 호출하면 해당 영상에 디스플레이 됩니다. 다음에는 AVSampleBufferDisplayLink를 사용하지 않고 video toolbox 를 이용해 H.264 디코더를 구현하는 방법에 대해 살펴보겠습니다.

    댓글

Designed by Tistory.