mp4 파일중 스트리밍이 지원되지 않는 파일이 존재하였고, 이를 최적화 시켜주는 C라이브러리를 Swift로 컨버젼했음
Mp4 atoms
mp4 파일 중 스트리밍이 되지 않는 파일들이 존재하는데, 이는 mp4 파일을 구성하는 요소인 atom 배치에 따른 것이었다. 데이터를 담고있는 mdat atom과 데이터에 대한 정보를 담고 있는 moov atom의 순서에 따라 스트리밍 가능 / 불가능 여부가 결정되는데, 스트리밍이 가능한 mp4는 moov atom이 mdat atom보다 앞에 배치되어 있었고, 스트리밍이 불가한 mp4는 moov atom이 파일의 가장 뒷부분에 붙어있었다. 이를 수정하여 mp4를 스트리밍해줄 수 있는 방법을 찾아보았다.
AVAssetExportSession
애플에서 제공하는 기능으로, 세션에 shouldOptimizeForNetworkUse 값을 true로 주면 moov atom의 위치를 옮겨서 스트리밍이 가능해지도록 만들 수 있다. 하지만 input과 output이 모두 파일이며, 파일을 write하는데 시간이 걸리기 때문에 요구하는 성능을 만족시킬 수 없었다.
qtfaststart.c
C 라이브러리로 byte 단위의 연산을 통해 moov atom을 찾고 이동시켜주는 기능을 제공해준다. 나는 이 라이브러리를 swift로 컨버전하여 사용했다.
코드
QTFastStart.swift
import Foundation
/// Optimize mp4 data for audio streaming.
/// return optimized data if success.
/// return original data if failure.
class QTFastStart {
private var FREE_ATOM: Int { fourCcToInt("free") }
private var JUNK_ATOM: Int { fourCcToInt("junk") }
private var MDAT_ATOM: Int { fourCcToInt("mdat") }
private var MOOV_ATOM: Int { fourCcToInt("moov") }
private var PNOT_ATOM: Int { fourCcToInt("pnot") }
private var SKIP_ATOM: Int { fourCcToInt("skip") }
private var WIDE_ATOM: Int { fourCcToInt("wide") }
private var PICT_ATOM: Int { fourCcToInt("pict") }
private var FTYP_ATOM: Int { fourCcToInt("ftyp") }
private var UUID_ATOM: Int { fourCcToInt("uuid") }
private var CMOV_ATOM: Int { fourCcToInt("cmov") }
private var STCO_ATOM: Int { fourCcToInt("stco") }
private var CO64_ATOM: Int { fourCcToInt("c064") }
private let ATOM_PREAMBLE_SIZE: Int = 8
private func fourCcToInt(_ fourCc: String) -> Int {
let data = fourCc.data(using: .ascii)!
return Int(bigEndian: Int(data: data))
}
func readAndFill(_ data: inout ByteBuffer, _ buffer: inout ByteBuffer) -> Bool {
buffer.clear()
do {
let slicedData: Data = try data.readBytes(buffer.length)
buffer.writeBytes(slicedData)
buffer.rewind()
return true
} catch {
return false
}
}
func process(_ data: Data) -> Data {
var dataBytes = ByteBuffer(data: data)
var atomBytes = ByteBuffer(size: ATOM_PREAMBLE_SIZE)
var atomType: Int = 0
var atomSize: Int = 0
var startOffset: Int = 0
let lastOffset: Int
var ftypAtom: ByteBuffer? = nil
var moovAtom: ByteBuffer
let moovAtomSize: Int
// mp4의 atom을 조회하여 moov atom이 최하단에 있는지 체크
do {
while readAndFill(&dataBytes, &atomBytes) {
atomSize = Int(try atomBytes.getInteger() as UInt32)
atomType = Int(try atomBytes.getInteger() as UInt32)
if atomType == FTYP_ATOM {
let ftypAtomSize = atomSize
ftypAtom = ByteBuffer(size: ftypAtomSize)
atomBytes.rewind()
ftypAtom!.writeBytes(try atomBytes.readBytes(atomBytes.length))
let ftypData = try dataBytes.readBytes(ftypAtom!.bytesAvailable)
ftypAtom!.writeBytes(ftypData)
ftypAtom!.rewind()
startOffset = dataBytes.position
} else {
dataBytes.position = dataBytes.position + atomSize - ATOM_PREAMBLE_SIZE
}
if atomType != FREE_ATOM
&& atomType != JUNK_ATOM
&& atomType != MDAT_ATOM
&& atomType != MOOV_ATOM
&& atomType != PNOT_ATOM
&& atomType != SKIP_ATOM
&& atomType != WIDE_ATOM
&& atomType != PICT_ATOM
&& atomType != UUID_ATOM
&& atomType != FTYP_ATOM {
print("Encountered non-QT top-level atom")
break
}
if (atomSize < 8) {
break
}
}
if atomType != MOOV_ATOM {
print("last atom in file was not a moov atom")
return data
}
// moov atom이 최하단에 있음을 확인 후 moovAtom 전체를 로드함
moovAtomSize = atomSize
lastOffset = dataBytes.length - moovAtomSize // moov 뒤에 더이상 데이터가 없다는 가정 (qt-faststart.c)
dataBytes.position = lastOffset
moovAtom = ByteBuffer(size: moovAtomSize)
if !readAndFill(&dataBytes, &moovAtom) {
print("moov parse error")
return data
}
if try Int(moovAtom.getInteger(12) as UInt32) == CMOV_ATOM {
print("Compressed moov atom not supported")
return data
}
// stco 또는 co64 atom을 찾기 위해 moov atom을 검사
while(moovAtom.bytesAvailable >= 8) {
let atomHead = moovAtom.position
atomType = Int(try moovAtom.getInteger(atomHead + 4) as UInt32)
if !(atomType == STCO_ATOM || atomType == CO64_ATOM) {
moovAtom.position = moovAtom.position + 1
continue
}
atomSize = Int(try moovAtom.getInteger(atomHead) as UInt32)
if atomSize > moovAtom.bytesAvailable {
print("bad atom size")
return data
}
moovAtom.position = atomHead + 12 // skip size (4 bytes), type (4 bytes), version (1 byte) and flags (3 bytes)
if moovAtom.bytesAvailable < 4 {
print("malformed atom")
return data
}
let offsetCount = Int(try moovAtom.getInteger() as UInt32)
if atomType == STCO_ATOM {
print("patching stco atom")
if moovAtom.bytesAvailable < offsetCount * 4 {
print("bad atom size/element count")
return data
}
for _ in 0..<offsetCount {
let currentOffset = Int( try moovAtom.getInteger(moovAtom.position) as UInt32)
let newOffset = currentOffset + moovAtomSize
if currentOffset < 0 && newOffset >= 0 {
print("Unsupported file exception")
return data
}
moovAtom.put(UInt32(newOffset).bigEndian)
}
} else if atomType == CO64_ATOM {
print("patching co64 atom")
if moovAtom.bytesAvailable < offsetCount * 8 {
print("bad atom size/element count")
return data
}
for _ in 0..<offsetCount {
let currentOffset = try moovAtom.getInteger(moovAtom.position) as Int
moovAtom.put(currentOffset + moovAtomSize)
}
}
}
dataBytes.position = startOffset // ftyp atom 뒷부분으로 이동
var outData = Data()
guard let ftypAtom = ftypAtom else { return data }
print("writing ftyp atom")
outData.append(ftypAtom.data)
print("writing new moov atom")
outData.append(moovAtom.data)
print("write rest of data")
let restOfData = try dataBytes.readBytes(dataBytes.length - startOffset)
outData.append(restOfData)
return outData
} catch {
print(error)
return data
}
}
}
ByteBuffer
Data에서 Byte단위의 연산을 편하게 하기 위해 ByteBuffer도 구현했다.
import Foundation
class ByteBuffer {
enum Error: Swift.Error {
case eof
case parse
}
private(set) var data = Data()
open var position: Int = 0
open var bytesAvailable: Int {
data.count - position
}
open var length: Int {
get {
data.count
}
set {
switch true {
case (data.count < newValue):
data.append(Data(count: newValue - data.count))
case (newValue < data.count):
data = data.subdata(in: 0..<newValue)
default:
break
}
}
}
open subscript(i: Int) -> UInt8 {
get {
data[i]
}
set {
data[i] = newValue
}
}
init(data: Data) {
self.data = data
position = 0
}
init(size: Int) {
data = Data(repeating: 0x00, count: size)
position = 0
}
@discardableResult
open func clear() -> Self {
position = 0
return self
}
open func rewind() {
position = 0
}
open func readBytes(_ length: Int) throws -> Data {
guard length <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
position += length
return data.subdata(in: position - length..<position)
}
@discardableResult
open func writeBytes(_ value: Data) -> Self {
if position == data.count {
data.append(value)
position = data.count
return self
}
let length: Int = min(data.count, value.count)
data[position..<position + length] = value[0..<length]
if length == data.count {
data.append(value[length..<value.count])
}
position += value.count
return self
}
@discardableResult
open func put<T: FixedWidthInteger>(_ value: T) -> ByteBuffer {
writeBytes(value.data)
}
@discardableResult
open func put(_ value: Float) -> ByteBuffer {
writeBytes(Data(value.data.reversed()))
}
@discardableResult
open func put(_ value: Double) -> ByteBuffer {
writeBytes(Data(value.data.reversed()))
}
open func getInteger<T: FixedWidthInteger>() throws -> T {
let sizeOfInteger = MemoryLayout<T>.size
guard sizeOfInteger <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
position += sizeOfInteger
return T(data: data[position - sizeOfInteger..<position]).bigEndian
}
open func getInteger<T: FixedWidthInteger>(_ index: Int) throws -> T {
let sizeOfInteger = MemoryLayout<T>.size
guard sizeOfInteger + index <= length else {
throw ByteBuffer.Error.eof
}
return T(data: data[index..<index+sizeOfInteger]).bigEndian
}
open func getFloat() throws -> Float {
let sizeOfFloat = MemoryLayout<UInt32>.size
guard sizeOfFloat <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
position += sizeOfFloat
return Float(data: Data(data.subdata(in: position - sizeOfFloat..<position).reversed()))
}
open func getDouble() throws -> Double {
let sizeOfDouble = MemoryLayout<UInt64>.size
guard sizeOfDouble <= bytesAvailable else {
throw ByteBuffer.Error.eof
}
position += sizeOfDouble
return Double(data: Data(data.subdata(in: position - sizeOfDouble..<position).reversed()))
}
}
extension ExpressibleByIntegerLiteral {
var data: Data {
var value: Self = self
return Data(bytes: &value, count: MemoryLayout<Self>.size)
}
init(data: Data) {
let diff: Int = MemoryLayout<Self>.size - data.count
if 0 < diff {
var buffer = Data(repeating: 0, count: diff)
buffer.append(data)
self = buffer.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: Self.self).pointee }
return
}
self = data.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: Self.self).pointee }
}
init(data: Slice<Data>) {
self.init(data: Data(data))
}
}
Usage
process가 에러 없이 동작했다면 moov atom이 수정된 data를 리턴하고, 그렇지 않을 경우 원본 데이터를 리턴함. 에러 핸들링 수정할 예정
let data = 문제가 있는 mp4의 데이터
let processedData = QTFastStart().process(data)