AVFoundationつかって動画のh264,aacデータ直読み込み

mp4ファイルから動画、音声データ編集なしでh264,aacデータのまま取ってサーバに送信したいですが、実装してみたら結構ハマったところがあったので、メモ用として整理しました。

## ポイント

- AVAssetReaderTrackOutputのoutputSettings nil設定することで処理していないmp4生動画と音声データが取れる
- samplebuffer一つでh264 frame一つになるが、aacは複数の可能性がある!!!

# コード説明

## assetからvideo track、audio track取得


``` swift
let asset = AVURLAsset(url: url)
guard let assetReader = try? AVAssetReader(asset: asset) else {
  print("Unable to create AVAssetReader")
  return
}

// Get video track
let videoTrack = asset.tracks(withMediaType: .video).first
// outputSettingsがnil設定することで動画のもとエンコードされたh264データがとれます
let videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack!, outputSettings: nil)
videoReaderOutput.alwaysCopiesSampleData = false
assetReader.add(videoReaderOutput)

let audioTrack = asset.tracks(withMediaType: .audio).first
// outputSettingsがnil設定することでaudioのもとエンコードされたaacデータがとれます
let audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack!, outputSettings: nil)
audioReaderOutput.alwaysCopiesSampleData = false
assetReader.add(audioReaderOutput)
    
```

## 読み込み

``` swift
// 読み込み開始
assetReader?.startReading()

// loopしてvideo,audio buffer続ける
let videoSampleBuffer = self.videoReaderOutput?.copyNextSampleBuffer()
let audioSampleBuffer = self.audioReaderOutput?.copyNextSampleBuffer()
```

## SampleBuffer処理

### 動画buffer処理

注: 1sampleBufferが1frameになります

```Swift:  samplebufferからdata取得
extension CMBlockBuffer {
  var data: Data? {
    var length: Int = 0
    var pointer: UnsafeMutablePointer<Int8>?
    guard CMBlockBufferGetDataPointer(self, atOffset: 0, lengthAtOffsetOut: nil, totalLengthOut: &length, dataPointerOut: &pointer) == noErr,
          let p = pointer else {
      return nil
    }
    return Data(bytes: p, count: length)
  }
  
  var length: Int {
    return CMBlockBufferGetDataLength(self)
  }
}
```

```Swift: h264関連データ取得

// h264 data
guard let bufferData = CMSampleBufferGetDataBuffer(buffer)?.data else {
  return nil
}

guard let attachments = CMSampleBufferGetSampleAttachmentsArray(buffer, createIfNecessary: true) as? NSArray else { return nil }
guard let attachment = attachments[0] as? NSDictionary else {
  return nil
}
// key frameかどうか
let isKeyframe = !(attachment[kCMSampleAttachmentKey_DependsOnOthers] as? Bool ?? true)

// presentationTimeStamp
let pts = buffer.presentationTimeStamp.seconds.isFinite ? UInt64(buffer.presentationTimeStamp.seconds * 1000) :  0
// decodeTimeStamp
let dts = buffer.decodeTimeStamp.seconds.isFinite ? UInt64(buffer.decodeTimeStamp.seconds * 1000) :  pts

let frame = VideoFrame(data: bufferData, isKeyframe: isKeyframe, pts: pts, dts: dts)
return frame
```

### 音声buffer処理

注: samplebuffer一つでaac frame複数の可能性があります


```swift: samplebuffer内のすべてのaacデータ取得
func getAACData(from sampleBuffer: CMSampleBuffer) -> Data? {
  guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else {
    print("Could not get block buffer from sample buffer")
    return nil
  }
  
  let length = CMBlockBufferGetDataLength(blockBuffer)
  var dataPointer: UnsafeMutablePointer<Int8>? = nil
  
  CMBlockBufferGetDataPointer(blockBuffer, atOffset: 0, lengthAtOffsetOut: nil, totalLengthOut: nil, dataPointerOut: &dataPointer)
  
  if let dataPointer = dataPointer {
    return Data(bytes: dataPointer, count: length)
  }
  
  return nil
}
```

```swift: samplebuffer内のaacデータ処理
// samplebuffer内で複数のaac sampleデータがあるので、その数を取得
let numSamplesInBuffer = CMSampleBufferGetNumSamples(buffer);

// samplebufferから入ってるすべてのaac data取り出す
guard let aacData = getAACData(from: buffer) else { return nil}

var audioFrames: [AudioFrame] = []
var offset = 0

for i in 0..<numSamplesInBuffer {
  // sample indexからこのsampleのdata size取得
  let size = CMSampleBufferGetSampleSize(buffer, at: i)
  // sample indexからこのsampleのdata取得
  let data = aacData.subdata(in: offset..<offset + size)

  //  presentationTimeStamp
  var timingInfo: CMSampleTimingInfo = CMSampleTimingInfo()
  CMSampleBufferGetSampleTimingInfo(buffer, at: i, timingInfoOut: &timingInfo)
  let pts = timingInfo.presentationTimeStamp.seconds.isFinite ? UInt64(timingInfo.presentationTimeStamp.seconds * 1000) : 0
  
  let audioFrame = AudioFrame(data: data, pts: pts)
  audioFrames.append(audioFrame)
  
  offset += size
}
```