字幕の付いたビデオを
保存する

とりあえず、なんとか字幕を付けたビデオの保存ができたっぽいので、メモっておく。

基本的には、ビデオに字幕を表示するで使った方法で字幕を用意して、それを保存する形なので、まずはそちらに目を通してください。あと、保存に関しては、メディアファイルの書き出しにまとめてあるので、そちらを参照してみてください。

つまり、これらを併せて使うとどのようになるか、ということなので、メモは最小限で。

まずは、字幕を付ける映像ファイルを 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)

ここで、anchorPointposition を指定している。字幕だけのときは、指定しないでデフォルト値でよかったが、うまく表示されないので、それぞれ (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

})