ドワンゴ 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
のみ配置します。
また、Watch App Extension側には動画リソースとしてvideo.mp4を配置しておきます。(今回はリモートにある動画の再生には未対応)
それでは動画再生のためのコードを実装していきます。まずは動画ファイルをAVURLAsset
で読込み、次に読み込んだAVURLAsset
から映像トラックを取り出したら、AVAssetReader
を使って映像フレームを読込む準備をします。
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をプロパティに追加し、コマ数を調整できるようにしてあります。
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に転送します。
// 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上であれば10fps程度ならぼちぼち動作しますが、実機ではAppleWatchとiPhone間での画像転送がBLE経由となるため、通信帯域がネックになりそうです。iOSのBLEの通信速度は10〜50kbps程度とのことなので、このまま実機に持っていっても多分まともに動作しないんじゃないかなーと思います。実用まで持っていくのであれば画像の圧縮とかバッファリング等が必要そうですね。
まとめ
というわけでApple Watch向けに簡単な動画プレイヤーを実装してみました。
今回作った実装もろもろはgithubに置いてありますのでよければどうぞ。