ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [WWDC20] AVAssetWriter를 이용한 fragmented MPEG-4 Content
    iOS Swift 2023. 6. 8. 14:34

    iOS14에 AVAssetWriter에 fragmented MPEG-4 파일 형식의 미디어 데이터를 출력할 수 있도록 기능이 추가되었습니다. Apple HLS는 MPEG-2 전송 스트림, ADTS 및 MPEG 오디오와 같은 형식을 지원하지만 이 기능은 Fragmented MP4에만 해당됩니다. 

    해당 강의에서 VOD 와 라이브 스트리밍에 어떻게 사용될 수 있는 예시를 보여줍니다. 

    Fragmented MPEG-4(fMP4)는 ISO에 정한 미디어 파일 포맷으로 2016년 부터 Apple HTTP Live Streaming에서 지원되어왔습니다. 

    간단하게 전통적인 MP4 파일과 fMP4 파일을 살펴보면 다음과 같은 구조를 같습니다. 

     

     

    MP4 파일에는 파일 타입, 모든 샘플 데이터에 대한 정보를 구성하는 Movie 와 샘플데이터로 이루어진 Movie Data 가 있습니다. Movie에는 오디오나 비디오 코텍과 재생에 관련된 타이밍에 대한 정보와 샘플데이터에 대한 위치를 나타내는 참조가 포함되어 있습니다. 파일 타입을 제외하고 나머지 요소의 순서는 상관없이 어디든 올 수 있습니다. 이렇게 파일의 구조가 분리되어 있으면 라이브 캡쳐 중 앱이 죽었을 때  일반적인 MP4 파일의 경우 문제가 발생하지만 fMP4 형식의 파일은 Movie Fragment가 정상적인 부분은 재생할 수 있습니다. 

    fMP4 파일은 파일 타입이 먼저 오고 Movie 박스가 오고 이후 조각에 대한 Movie 박스가 옵니다. 이후 샘플 데이터를 담고 있는 Movie Data 가 위치합니다. 이제 Movie에는 오디오나 비디오에 대한 정보만 들어가고 실제 샘플 데이터에 대한 참조는 Movie Fragment에서 나누어 참조하게 됩니다. 전통적인 AVAssetWriter가 파일을 지정해 출력이 파일로 저장되도록 하는 반면 파일을 쓰지않고 프레그먼트 데이터만 출력하기 때문에 outputURL은 사용하지 않고 출력 컨텐츠의 타입을 지정하기만 하면 됩니다. fMP4는 MP4에 대한 설정으로 항상 AVFileType.MP4로 설정합니다. 다음 AVAssetWriteInput을 생성합니다. 이 예에서는 미디어 샘플을 인코딩하기 위한 compressionSettings 를 제공하는데 Passthrough로 동작시킬 때는 outputSettings 에 nil을 설정할 수 있습니다. 

    // Instantiate asset writer
    let assetWriter = AVAssetWriter(contentType: UTType(AVFileType.mp4.rawValue)!)
    
    // Add inputs - 패스스루로 동작시킬려면 outputSettings에 nil 값 적용 
    let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: compressionSettings)
    			
    assetWriter.add(videoInput)

    이제 미디어 데이터를 fMP4 형식으로 출력하려면 출력 파일의 타입프로파일을 AppleHLS 또는 CMAF 호환 프로필 중 선택하고 preferredOutputSegmentInterval을 설정하는 데 해당 간격으로 세그먼트가 출력됩니다. 여기서는 6초에 하나의 세그먼트가 출력되도록 합니다.  initialSegmentStartTime 과 Asset Writer의 delegate 도 설정합니다. 

    assetWriter.outputFileTypeProfile = .mpeg4AppleHLS
    
    assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 6.0, preferredTimescale: 1)
    
    assetWriter.initialSegmentStartTime = myInitialSegmentStartTime
    
    assetWriter.delegate = myDelegateObject

    delegate 메서드는 preferredOutputSegmentInterval에 설정한 시간 마다 딜리게이터 함수를 통해서 세그먼트 데이터와 세그먼트 타입, 필요에 따라 세그먼트 리포트를 전달합니다. 해당 딜리게이트 함수는 다음과 같습니다. 

    optional func assetWriter(_ writer: AVAssetWriter, 
    				didOutputSegmentData segmentData: Data, 
                    segmentType: AVAssetSegmentType)
    
    
    optional func assetWriter(_ writer: AVAssetWriter, 
    				didOutputSegmentData segmentData: Data, 
                    segmentType: AVAssetSegmentType, 
                    segmentReport: AVAssetSegmentReport?)

    해당 딜리게이트에서 AVAssetSegmentType을 반환하는데 이는 다음과 같은 값을 갖습니다. 

     

    public enum AVAssetSegmentType : Int {
        case initialization = 1 
        case separable = 2
    }

    AVAssetSegmentType 은 두 가지 종류가 있는데 initalization 과 separable입니다. initialization은 파일 타입과 Movie 로 구성된 프레그먼트를 반환하고 separable은 하나의 Movie Fragment 와 Movie Data를 반환합니다. 

    HLS는 재생할 파일에 대한 playlist를 가지고 있는데 아래와 같은 형식을 같고 HLS이 파일을 참고해 영상을 재생합니다. 

    #EXTM3U
    #EXT-X-TARGETDURATION:6
    #EXT-X-VERSION:7
    #EXT-X-MEDIA-SEQUENCE:1
    #EXT-X-PLAYLIST-TYPE:VOD
    #EXT-X-INDEPENDENT-SEGMENTS
    #EXT-X-MAP:URI="fileSequence0.mp4"
    #EXTINF:6.00000,
    fileSequence1.m4s
    #EXTINF:6.00000,
    fileSequence2.m4s
    #EXTINF:6.00000,
    fileSequence3.m4s
    #EXTINF:6.00000,
    fileSequence4.m4s
    #EXTINF:6.00000,
    fileSequence5.m4s
    #EXTINF:6.00000,
    fileSequence6.m4s
    #EXTINF:6.00000,
    fileSequence7.m4s
    #EXTINF:6.00000,
    fileSequence8.m4s
    #EXTINF:6.00000,

    여기에는 URL과 재생시간에 관한 정보가 들어 있습니다. AVAssetWriter는 이 파일을 만들지 않기 때문에 딜리게이트의 메서드가 호출 될 때마다 AVAssetSegmentReport를 통해서 해당 파일을 만들어야 합니다. AVAssetSegmentReport에서 제공되는 정보를 이용해 playlist와 I-frame playlist를 만들 수 있습니다. (실제 해당 샘플코드를 통해서 prog_index.m3u8 파일 생성되도록 구현되어 있습니다.)  

    Passthrough 로 세그먼트를 나눌 경우 문제가 발생하게 되는데 모든 세그먼트에 MovieData의 시작은 Sync 프레임(I  프레임)이 되어야 하는데 세그먼트 간격으로 설정한 값에 따라 새로 생성되는 세그먼트가 sync가 아닐 때 문제가 발생합니다. CMAF는 모든 세그먼트가 동기화 샘플로 시작하도록 요구하고 Apple HLS도 이를 선호하기 때문입니다. 이 규칙은 비디오 뿐만 아니라 USAC 오디오와 같이 샘플 종속성이 있는 오디오에도 그래도 적용됩니다. 따라서 Passthrough의 경우 미디어 데이터가 지정된 것보다 훨씬 더 긴 시간동안 출력될 수 있습니다. 

     

     

    결과적을 Passthrough 방식으로 세그먼트를 분할하는 방법은 HLS에 적합하지 않습니다. 해결책 중 하나는 비디오 샘플을 동시에 인코딩하는 겁니다. 처음에 말했던 것 처럼 AVAssetWriteInput에서 압축에 대한 설정을 할 수 있는데 설정을 지정하면 인코딩이 됩니다. 인코딩 모드에서는 원하는 출력 세그먼트 간격을 초가하는 비디오 샘플은 sync 샘플로 강제 인코딩됩니다. 

    다른 방법으로는 Passthough 모드에서 flushSegment를 호출하는 방법입니다. flushSegment는 이전 호출 이후에 추가된 모든 샘플을  포함하는 미디어 데이터를 출력하는 데 sync 샘플 이전에 flushSegment를 호출해 다음 flushSegment를 호출할 때 sync 샘플로 시작할 수 있게 할 수 있습니다. 하지만 오디오 데이터와 동기화 문제가 있을 수 있기 때문에 오디오와 비디오를 분리해야 할 수 도 있습니다. 

     

    AVAssetTrack에는 오디오 트랙에 대한 샘플 종속성 여부를 나타내는 새로운 속성이 있습니다. 

    extension AVAssetTrack {
           /* indicates whether this audio track has dependencies (e.g. kAudioFormatMPEGD_USAC) */
        open var hasAudioSampleDependencies: Bool { get }
    }

    이 속성을 사용하면 트랙에 샘플 종속성이 있는 지 미리 검사할 수 있습니다.

     

    앞에서 fMP 형식에 출력 타입 프로파일을 설정할 때 Apple HLS와 CMAF 호환 프로파일 중에 선택할 수 있다고 했습니다. CMAF는 fragmented MP4 세그먼트를 구성하는 제약 조건의 집합으로 스트리밍을 위한 표준입니다. Apple HLS를 포함하여 여러 플레이어에서 지원됩니다. Apple HLS만을 대상으로 하는 경우 일부 제약 조건이 필요하지 않을 수 있습니다. 그러나 미디어 라이브러리에 대한 더 많은 사용자를 원한다면 CMAF를 고려 할 수 도 있습니다. 

     

    이제 HLS가 오디오 priming(프라이밍)을 처리하는 방법에 대해 살펴봅니다. 이 다이어 그램은 AAC 오디오의 파형을 보여줍니다. AAC 오디오 코덱은 인코딩 알고리즘의 특성으로 인해 오디오 샘플을 올바르게 인코딩 및 디코딩하기 위해 소스 PCM 오디오 샘플 이상의 데이터가 필요합니다. 이러한 이유로 인코더는 첫 번째 실제 오디오 샘플 전에 무음을 추가하는데 이를 프라이밍이라고 합니다. AAC에 대한 가장 일반적인 프라이밍은 2,112 샘플이며 44,100이라고 가정할 때 약 48밀리초의 무음이 들어가게 됩니다.

    오디오와 비디오가 모두 시간 0에서 시작한다고 가정하면 오디오 프라이밍에 해 오디오와 비디오 사이에 약간의 딜레이가 발생하고 처음 무음이 재생됩니다. 오디오의 baseMediaDecodeTime을 뒤로 이동시켜 동기화를 시킬 수 있다고 생각하지만 baseMediaDecodeTime은 양의 정수 값이므로 0보다 이전일 수 없습니다. 한가지 해결책은 오디오와 비디오를 동일한 시간 오프셋만큼 앞으로 이동하는 것입니다. 이렇게 하면 오디오의 baseMediaDecodeTime을 프라임 크기 만큼 뒤로 이동할 수 있습니다. 시작 시간이 0이 아니더라도 HLS에서는 비디오 샘플의 가장 빠른 프레젠테이션 시간에 재생이 시작되서 시작 시간까지 기다리지 않습니다. 

     

    이것은 정확한 오디오와 비디오의 동기화 뿐만 아니라 프라이밍 지속 시간이 다른 오디오들에 대해서 타임스탬프를 매치시키는 게 중요합니다. 따라서 Apple HLS 프로파일을 지정한다면 모든 샘플에 일정 시간을 추가하여 미디어 시간을 이동하는 것이 좋습니다. 초기 세그먼트 시작 시간도 동일한 시간만큼 이동해야 합니다. 프라이밍이 1초보다 적어 몇 초면 충분하지만 애플의 Media File Segment 툴이 10초 오프셋을 주고 있어서, 10초를 주는 걸 추천합니다. 이 영상의 샘플코드 또한 10초의 offset을 주고 있습니다. 

    해당 샘플코드에 대한 분석글은 다음에 올리도록 하겠습니다. 

    댓글

Designed by Tistory.