字幕の付いたビデオを
保存する
とりあえず、なんとか字幕を付けたビデオの保存ができたっぽいので、メモっておく。
基本的には、ビデオに字幕を表示するで使った方法で字幕を用意して、それを保存する形なので、まずはそちらに目を通してください。あと、保存に関しては、メディアファイルの書き出しにまとめてあるので、そちらを参照してみてください。
つまり、これらを併せて使うとどのようになるか、ということなので、メモは最小限で。
まずは、字幕を付ける映像ファイルを AVAsset オブジェクトとして読み込み、AVPlayerItem オブジェクトを作る。これは、時間の同期をさせるのが、AVPlayerItem オブジェクトに対してなので、そうする。
asset = AVAsset.assetWithURL(url)
playerItem = AVPlayerItem.playerItemWithAsset(asset)
そしたら、最終的に書き出すための AVMutableComposition オブジェクトを用意して、asset の中身を入れる。ここでは、元映像のすべてを使うので、ゼロの時間のところに入れる。
composition = AVMutableComposition.composition
composition.insertTimeRange(CMTimeRangeMake(KCMTimeZero,asset.duration),ofAsset:asset,atTime:KCMTimeZero,error:nil)
videoTrack = composition.tracksWithMediaType(AVMediaTypeVideo)[0]
videoSize = CGSizeApplyAffineTransform(videoTrack.naturalSize,videoTrack.preferredTransform)
次に、AVMutableVideoComposition オブジェクトを作り、映像トラックの準備をする。
videoComposition = AVMutableVideoComposition.videoComposition
fps = videoTrack.nominalFrameRate
videoComposition.frameDuration = CMTimeMakeWithSeconds( 1.0 / fps, videoTrack.naturalTimeScale)
videoComposition.renderSize = videoSize
それに対する instruction も準備。
instruction = AVMutableVideoCompositionInstruction.videoCompositionInstruction
instruction.timeRange = CMTimeRangeMake(KCMTimeZero, composition.duration)
layerInstruction = AVMutableVideoCompositionLayerInstruction.videoCompositionLayerInstructionWithAssetTrack(videoTrack)
instruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [instruction]
そしたら、字幕の準備。実際の字幕になる文字から CGImageRef を作る方法については、ビデオに字幕を表示するに書いてあるので、そちらを見てください。
まず、AVSynchronizedLayer を用意して、上で作った playerItem にひもづける。
synchLayer = AVSynchronizedLayer.synchronizedLayerWithPlayerItem(playerItem)
synchLayer.setFrame(CGRectMake(0, 0, videoSize.width, videoSize.height))
synchLayer.setMasksToBounds(false)
synchLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height)
synchLayer.anchorPoint = CGPointMake(0,0)
synchLayer.position = CGPointMake(0,0)
ここで、anchorPoint と position を指定している。字幕だけのときは、指定しないでデフォルト値でよかったが、うまく表示されないので、それぞれ (0,0) を指定した。この辺りに関しては、説明してあるサイト(外部サイト)があったので、そちらを見てください。
次に、字幕を表示する layer を作る。
overlayLayer = CALayer.layer
overlayLayer.frame = CGRectMake(0, 0, @maxWidth, @maxHeight)
overlayLayer.anchorPoint = CGPointMake(0.5,-offsetValue/videoSize.height)
overlayLayer.position = CGPointMake(videoSize.width/2,0)
overlayLayer.contentsGravity = KCAGravityResizeAspect
ここでの anchorPoint と position は、自分のサンプルではこうしないとうまくいかなかった、という値なので、この辺りは、それぞれ試してください。frame は、anchorPoint と position で位置決めをしているので、origin は (0,0) になっている。@maxWidth と @maxHeight は字幕のイメージを作るときに縦横のサイズの最大値を取っていて、その値。
次に、字幕のイメージをキーフレームアニメーションにする処理。subTImages は NSBitmapImageRep の入った配列で、subtitleTimes は、それに対応した表示を切り替えるタイミング (0.0-1.0) が入った配列。ともにビデオに字幕を表示するを参照してください。
anim = CAKeyframeAnimation.animationWithKeyPath("contents")
anim.beginTime = AVCoreAnimationBeginTimeAtZero
anim.duration = CMTimeGetSeconds(playerItem.duration)
anim.values = subTImages.map{|x| x.CGImage}
anim.keyTimes = subtitleTimes
anim.calculationMode = KCAAnimationDiscrete
anim.removedOnCompletion = false
anim.fillMode = KCAFillModeForwards
overlayLayer.addAnimation(anim,forKey:"contents")
そして、字幕 layer を synchLayer に追加。
synchLayer.addSublayer(overlayLayer)
さて、ここからが本番。用意した字幕 layer を、映像トラックに合成する処理。
まずは、映像トラックの映像を表示する videoLayer と、字幕 layer と映像 layer を合成するための superlayer (parentLayer) を用意する。サイズは、映像トラックの映像の大きさと同じにする。
parentLayer = CALayer.layer
videoLayer = CALayer.layer
parentLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height)
videoLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height)
そして、videoLayer と、字幕 layer を含む synchLayer を parentLayer に追加する。
parentLayer.addSublayer(videoLayer)
parentLayer.addSublayer(synchLayer)
anchorPoint と position は、ともに (0,0) にした。
parentLayer.anchorPoint = CGPointMake(0,0)
videoLayer.anchorPoint = CGPointMake(0,0)
parentLayer.position = CGPointMake(0,0)
videoLayer.position = CGPointMake(0,0)
合成の処理は、AVVideoCompositionCoreAnimationTool の videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer(videoLayer,inLayer:parentLayer) で行う。これを videoComposition の animationTool に指定する。
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool.videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer(videoLayer,inLayer:parentLayer)
ここまでできたら、composition の準備は終わったので、書き出しの処理に移る。
書き出しは、メディアファイルの書き出しにあるので、ほぼそのまま。
@exportSession = AVAssetExportSession.alloc.initWithAsset(composition,presetName:AVAssetExportPresetAppleM4V480pSD)
@exportSession.videoComposition = videoComposition
@exportSession.outputURL = url
@exportSession.outputFileType = AVFileTypeAppleM4V
で、実際のアプリケーションでは、進行状況を確認したかったりするので、こんな処理を入れてみた。NSTimer で 1 秒ごとに進行状況をアップデートする。これには、NSProgressIndicator を使うといいでしょう。そのために、@exportSession はインスタンス変数にしてある。
@progressTimer = NSTimer.scheduledTimerWithTimeInterval(1,
target: self,
selector: "progressCheck:",
userInfo: nil,
repeats: true)
実際の書き出しのときは、結果によって別の処理を用意。終わったら、上で作った timer を止める。
@exportSession.exportAsynchronouslyWithCompletionHandler(Proc.new{
case @exportSession.status
when AVAssetExportSessionStatusCompleted
# 成功したときの処理
when AVAssetExportSessionStatusFailed
# 失敗したときの処理
end
@progressTimer.invalidate
})
進行状況の処理はこんな感じ。progress は 0.0 - 1.0 で返ってくるので、このために @mainProgressBar は、setDoubleValue(0.0) で初期値を 0 にして、setMaxValue(1.0) で、最大値を 1.0 に設定しておく。
def progressCheck(userInfo)
@mainProgressBar.setDoubleValue(@exportSession.progress)
end
asset = AVAsset.assetWithURL(url)
playerItem = AVPlayerItem.alloc.initWithAsset(asset)
composition = AVMutableComposition.composition
composition.insertTimeRange(CMTimeRangeMake(KCMTimeZero,asset.duration),ofAsset:asset,atTime:KCMTimeZero,error:nil)
videoTrack = composition.tracksWithMediaType(AVMediaTypeVideo)[0]
videoSize = CGSizeApplyAffineTransform(videoTrack.naturalSize,videoTrack.preferredTransform)
videoComposition = AVMutableVideoComposition.videoComposition
fps = videoTrack.nominalFrameRate
videoComposition.frameDuration = CMTimeMake(1, fps)
videoComposition.renderSize = videoSize
synchLayer = AVSynchronizedLayer.synchronizedLayerWithPlayerItem(playerItem)
synchLayer.setFrame(CGRectMake(0, 0, videoSize.width, videoSize.height))
synchLayer.setMasksToBounds(false)
synchLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height)
synchLayer.anchorPoint = CGPointMake(0,0)
synchLayer.position = CGPointMake(0,0)
overlayLayer = CALayer.layer
overlayLayer.frame = CGRectMake(0, 0, @maxWidth, @maxHeight)
overlayLayer.anchorPoint = CGPointMake(0.5,-offsetValue/videoSize.height)
overlayLayer.position = CGPointMake(videoSize.width/2,0)
overlayLayer.contentsGravity = KCAGravityResizeAspect
anim = CAKeyframeAnimation.animationWithKeyPath("contents")
anim.beginTime = AVCoreAnimationBeginTimeAtZero
anim.duration = CMTimeGetSeconds(playerItem.duration)
anim.values = subTImages.map{|x| x.CGImage}
anim.keyTimes = subtitleTimes
anim.calculationMode = KCAAnimationDiscrete
anim.removedOnCompletion = false
anim.fillMode = KCAFillModeForwards
overlayLayer.addAnimation(anim,forKey:"contents")
synchLayer.addSublayer(overlayLayer)
parentLayer = CALayer.layer
videoLayer = CALayer.layer
parentLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height)
videoLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height)
parentLayer.addSublayer(videoLayer)
parentLayer.addSublayer(synchLayer)
parentLayer.anchorPoint = CGPointMake(0,0)
videoLayer.anchorPoint = CGPointMake(0,0)
parentLayer.position = CGPointMake(0,0)
videoLayer.position = CGPointMake(0,0)
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool.videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer(videoLayer,inLayer:parentLayer)
instruction = AVMutableVideoCompositionInstruction.videoCompositionInstruction
instruction.timeRange = CMTimeRangeMake(KCMTimeZero, composition.duration)
layerInstruction = AVMutableVideoCompositionLayerInstruction.videoCompositionLayerInstructionWithAssetTrack(videoTrack)
instruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [instruction]
#videoComposition.renderScale = 1.0
@exportSession = AVAssetExportSession.alloc.initWithAsset(composition,presetName:AVAssetExportPresetAppleM4V480pSD)
@exportSession.videoComposition = videoComposition
@exportSession.outputURL = @panel.URL
@exportSession.outputFileType = AVFileTypeAppleM4V
@progressTimer = NSTimer.scheduledTimerWithTimeInterval(1,
target: self,
selector: "progressCheck:",
userInfo: nil,
repeats: true)
@exportSession.exportAsynchronouslyWithCompletionHandler(Proc.new{
case @exportSession.status
when AVAssetExportSessionStatusCompleted
when AVAssetExportSessionStatusFailed
end
@progressTimer.invalidate
})