33
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ドワンゴ 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

また、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に変換する、という流れです。最後に作成したUIImageWKInterfaceImageにセットすることで、映像フレームを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.png

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

まとめ

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

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

33
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?