アプリなどを開発するブログ

React Native / Swift / Ruby on Railsなどの学習メモ。


iOSでストリーミング再生中の動画キャプチャを撮りたいが方法がない

やりたいこと

サーバー上にあるm3u8ファイルをiPhoneアプリでストリーミング再生してる。
ボタンを押すと今映ってる映像の写真を撮ってローカルに保存したい。
一見、以下のような処理で簡単に取得できそうである。

let rect = view.bounds

UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context: CGContextRef = UIGraphicsGetCurrentContext()
view.layer.renderInContext(context)
let capturedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

が、真っ黒な画像が出力されるだけ。

色々調べたけど

stackoverflow.com

色々試したけどできないっぽいよという回答。

stackoverflow.com

A) UIGetScreenImage() という非公開メソッドで撮れるけど、リジェクトの可能性もあるよ
B) AVPlayerLayerで再生して、AVAssetImageGenerator の copyCGImageAtTime で撮れるよ
  -> ただしこれは、サーバー上の動画ファイルには使えないみたい

stackoverflow.com ここに載ってる方法でやってみたけど、imageRefが取得できない。
「できた!」って言ってる人は多分ローカルの動画ファイルでやってる気がする。

var url = NSURL(string: Constants.streamUrls[0])
if var asset = AVAsset.assetWithURL(url) as? AVAsset {
    var imageGenerator:AVAssetImageGenerator = AVAssetImageGenerator(asset: asset)
    var time = CMTimeMake(1, 1)
    var imageRef = imageGenerator.copyCGImageAtTime(time, actualTime: nil, error: nil)
    var thumbnail = UIImage(CGImage: imageRef)
    return thumbnail!
}        

以下のようなエラーを吐いてしまう。

Optional(Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo=0x17046c140 {NSUnderlyingError=0x174243840 "The operation couldn’t be completed. (OSStatus error -12782.)", NSLocalizedFailureReason=An unknown error occurred (-12782), NSLocalizedDescription=The operation could not be completed})

stackoverflow.com こちらもうまくいかない。

AVAssetImageGeneratorを使っての取得も試みたが、ダメだった。
恐らくこちらも有効なのはローカルのビデオのみで、リモートの動画はだめくさい?

func captureFromPlayer(player: AVPlayer, maxSize: CGSize) -> UIImage? {
        var actualTime: CMTime = CMTimeMake(0, 0)
        var error: NSError?
        
        var generator: AVAssetImageGenerator = AVAssetImageGenerator(asset: player.currentItem.asset)
        
        generator.maximumSize = maxSize
        
        println("player.currentTime() :\(player.currentTime())")

        
        var cgIm: CGImageRef = generator.copyCGImageAtTime(player.currentTime(), actualTime: &actualTime, error: &error)
        var image = UIImage(CGImage: cgIm)
        if error != nil {
            println("erorr : \(error?.localizedDescription)")
            println("CMTimeGetSeconds : \(CMTimeGetSeconds(actualTime))")
            if let current = streamView.player?.currentTime() {
                CMTimeGetSeconds(current)
            }
            
            return nil
        }
        return image
    }
    

こっちもやってみたけど、そもそもtracksが空。

func setupReader() {
        let url = NSURL(string: urlString)
        let asset:AVURLAsset = AVURLAsset(URL: url, options: nil)
        asset.loadValuesAsynchronouslyForKeys(["tracks"], completionHandler: {
            dispatch_sync(dispatch_get_main_queue(), { () -> () in
                var videoTrack: AVAssetTrack? = nil
                var tracks = asset.tracksWithMediaType(AVMediaTypeVideo)
                if tracks.count == 1 {
                    if let track = tracks[0] as? AVAssetTrack {
                        var error: NSError?
                        self.movieReader = AVAssetReader(asset: asset, error: &error)
                        if error != nil {
                            println("error: \(error)")
                        }
                        
                        var key:String = kCVPixelBufferPixelFormatTypeKey as String
                        var value:Int = kCVPixelFormatType_4444AYpCbCr16 as Int
                        var videoSettings = [key: value]
                        
                        
                        self.movieReader?.addOutput(AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoSettings))
                        self.movieReader?.startReading()
                    }
                }
            })
        })
    }

iOS7から実装されたスナップショットを撮るメソッドを試す

こんなんあったんですね。

let capture: UIView = view.snapshotViewAfterScreenUpdates(true)

スクリーン自体のスナップショットを撮影するメソッドも、UIScreenに実装されている。

let capture = UIScreen.mainScreen().snapshotViewAfterScreenUpdates(true)

だがこれも、UIViewをUIImageに変換するこちらのお決まりのメソッドに投げると、
AVPlayerLayerの所だけが真っ黒になって返ってくる。

func viewToImage(capturedView:UIView) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(capturedView.bounds.size, capturedView.opaque, 0.0)
        capturedView.layer.renderInContext(UIGraphicsGetCurrentContext())
        var img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img
    }

ちなみに、キャプチャのUIView自体は、ビデオの動画もバッチリ表示されている。
UIImageにする段階で、ビデオ動画がすっぽり抜け落ちる。

なんでや!なんでなんや!
うーむ。。

こんなのもあった

stackoverflow.com

これもだめだった

func snapByOpenGL() -> UIImage {
        if let time = streamView.playerItem?.currentTime() {
            if var pixelBuffer: CVPixelBufferRef = streamView.output?.copyPixelBufferForItemTime(time, itemTimeForDisplay: nil) {
                var ciImage = CIImage(CVPixelBuffer: pixelBuffer)
                
                var temporaryContext: CIContext = CIContext(options: nil)
                var rect = CGRectMake(0, 0, CGFloat(CVPixelBufferGetWidth(pixelBuffer)), CGFloat(CVPixelBufferGetHeight(pixelBuffer)))


                var videoImage: CGImageRef = temporaryContext.createCGImage(ciImage, fromRect: rect)
                var image = UIImage(CGImage: videoImage)
                return image!
            }
        }
        
        return UIImage()
    }