AppleWatch向け動画プレイヤーを作ろう

ドワンゴ Advent calendar 16日目。

こんにちは。Apple Watchで一発当てるぞ٩( 'ω' )و ！！と意気込んでましたが、公開されたWatchKitを見てやる気がしぼんだ者です。

WatchKitが公開されてApple Watch用アプリを作ることができるようになりましたが、WatchKitに含まれるUIパーツは恐ろしく少なく、なんとMoviePlayerがありません。Mapはあるのに。

腕に液晶画面巻き付けてたらそこで動画を再生させたいと思うのは、誰もが持つ当たり前の欲求だと思います。ですので今回はAppleWatch向けに動画プレイヤーを作ってみたいと思います。

## Watch Appを作る

基本的にWatch AppにはStoryboardで構築したUI部分しか含めることができないため、動画プレイヤー自体はWatch App Extensionの方で実装することになります。そのためWatch App側には動画イメージを表示するための`WKInterfaceImage`と、再生と一時停止を制御するための`WKInterfaceButton`のみ配置します。

![storyboard.png](https://qiita-image-store.s3.amazonaws.com/0/4640/25f800c5-5b76-4993-9814-d26f42edb4af.png)

また、Watch App Extension側には動画リソースとしてvideo.mp4を配置しておきます。（今回はリモートにある動画の再生には未対応）

それでは動画再生のためのコードを実装していきます。まずは動画ファイルを`AVURLAsset`で読込み、次に読み込んだ`AVURLAsset`から映像トラックを取り出したら、`AVAssetReader`を使って映像フレームを読込む準備をします。

```swift
let asset = AVURLAsset(URL: NSURL(fileURLWithPath: self.movieFilePath!), options:[ AVURLAssetPreferPreciseDurationAndTimingKey: true ])
asset.loadValuesAsynchronouslyForKeys(["tracks"]) {
    let tracks = asset.tracksWithMediaType(AVMediaTypeVideo)
    
    let outputSetting = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
    self.trackOutput = AVAssetReaderTrackOutput(track: tracks[0] as AVAssetTrack, outputSettings: outputSetting)
    
    var error: NSError?
    self.assetReader = AVAssetReader(asset: asset, error: &error)
    if error != nil {
        NSLog("failed open movie file. : %@", error!.localizedDescription)
        return
    }
    
    if self.assetReader!.canAddOutput(self.trackOutput) {
        self.assetReader!.addOutput(self.trackOutput)
        self.assetReader!.startReading()
    }
}
```

`AVAssetReader`の読み込みを開始したら、`AVAssetReaderTrackOutput`を経由して`CMSampleBuffer`を取得します。このとき、`CMSampleBuffer`に含まれるPresentationTimestampを元に読み込みが早くなり過ぎないよう再生速度の調整を行っています。このとき動画の全てのフレームをApple Watchに反映させようとすると勇ましく死ぬので、framePerSecondをプロパティに追加し、コマ数を調整できるようにしてあります。

```swift
if let sampleBufferRef = self.trackOutput?.copyNextSampleBuffer() {
    // 速度調整
    let currentSampleTime = CMSampleBufferGetOutputPresentationTimeStamp(sampleBufferRef)
    let diffSampleTimeFromLastFrame = CMTimeSubtract(currentSampleTime, self.previousSampleTime)
    
    let sampleTimeDiff = CMTimeGetSeconds(diffSampleTimeFromLastFrame) as Double
    
    let currentActualTime = CFAbsoluteTimeGetCurrent()
    let actualTimeDiff = (currentActualTime - self.previousActualTime) as Double;
    
    if sampleTimeDiff > actualTimeDiff {
        let waitTime = UInt32((sampleTimeDiff - actualTimeDiff) * 1000000.0)
        usleep(waitTime)
    }
    
    self.previousSampleTime = currentSampleTime
    self.previousActualTime = currentActualTime
    
    // フレームスキップ
    if CMTimeCompare(currentSampleTime, self.nextRenderingTime) < 0 {
        return
    }
    
    self.nextRenderingTime = CMTimeAdd(currentSampleTime, CMTimeMake(1, self.framePerSecond))
     :
}
```

次に取り出した`CMSampleBuffer`から`UIImage`を生成します。まず`CMSampleBuffer`から`CVPixelBuffer`を取得し、`CVPixelBuffer`の各種情報を元に`CGBitmapContext`を生成します。そして`CGBitmapContext`から`CGImage`を生成し、`UIImage`に変換する、という流れです。最後に作成した`UIImage`を`WKInterfaceImage`にセットすることで、映像フレームをAppleWatchに転送します。

```swift
// samplebuffer to image
if let imageBufferRef = CMSampleBufferGetImageBuffer(sampleBufferRef) {
    CVPixelBufferLockBaseAddress(imageBufferRef, 0);
    
    let baseAddress = CVPixelBufferGetBaseAddress(imageBufferRef)
    let bytePerRow = CVPixelBufferGetBytesPerRow(imageBufferRef)
    let width = CVPixelBufferGetWidth(imageBufferRef)
    let height = CVPixelBufferGetHeight(imageBufferRef)
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    
    let context = CGBitmapContextCreate(baseAddress, width, height, 8, bytePerRow, colorSpace, .ByteOrder32Little | CGBitmapInfo(CGImageAlphaInfo.PremultipliedFirst.rawValue))
    
    let cgImageRef = CGBitmapContextCreateImage(context)
    let image = UIImage(CGImage: cgImageRef)
    
    CVPixelBufferUnlockBaseAddress(imageBufferRef, 0);
    
    dispatch_async(dispatch_get_main_queue()) {
        self.imageView?.setImage(image)
        return
    }
}
```

あとは再生、一時停止などの処理を簡単に実装しておきます。

## 結果

simulator上のPlayボタンを押すと動画の再生が開始されます。fps制限を付けずに実行するとwarningが発生しまくり動画再生できませんが、fpsを落とすとそれなりに動作しました。なお、WatchKitにサウンド再生関連のAPIが無いため、今回音は出ません。

![simulator.png](https://qiita-image-store.s3.amazonaws.com/0/4640/35f073d2-8077-1256-087a-89ed908d08cf.png)

今回の実装ではsimulator上であれば10fps程度ならぼちぼち動作しますが、実機ではAppleWatchとiPhone間での画像転送がBLE経由となるため、通信帯域がネックになりそうです。[iOSのBLEの通信速度は10〜50kbps程度](http://d.hatena.ne.jp/shu223/20140110/1389292125)とのことなので、このまま実機に持っていっても多分まともに動作しないんじゃないかなーと思います。実用まで持っていくのであれば画像の圧縮とかバッファリング等が必要そうですね。

## まとめ

というわけでApple Watch向けに簡単な動画プレイヤーを実装してみました。

今回作った実装もろもろはgithubに置いてありますのでよければどうぞ。

https://github.com/saiten/AppleWatchMovie