[Swift]AVFoundationで動画に音声を追加する
関わっているアプリ開発のプロジェクトで無音動画に音声を結合する必要がありました。AVFoundationまわりの情報が少なく特にSwiftのサンプルコードなどがあまりないなと思い備忘録をかねてまとめます。
サンプルコードでは無音声動画と音声の結合をしていますが、音声ありでも処理は変わりません。その場合は差し替えの処理も可能です。
前提としてAVAsset/AVMutableCompositionをある程度理解しておく必要はあります。
AVFoundation Programming Guide
以下サンプルコードです。(必要箇所だけ抜き出しているのでコンパイルが通るかは?)
import UIKit
import Foundation
import AVFoundation
import AVKit
class MovieMakerViewController: UIViewController {
let dispatchQueue = DispatchQueue(label: "queue")
let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
let videoUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("videotmp.mp4")
let soundUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("soundtmp.caf")
let movieUrl = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!).appendingPathComponent("sample.mp4")//output
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let composition = AVMutableComposition.init()
//--------------------
//source video
let asset = AVURLAsset.init(url: self.videoUrl)
let range = CMTimeRangeMake(start: CMTime.zero, duration: asset.duration)
let videoTrack = asset.tracks(withMediaType: .video).first
// let audioTrack = asset.tracks(withMediaType: .audio).first//無音性動画の場合エラーになる
let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
// let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
try? compositionVideoTrack?.insertTimeRange(range, of: videoTrack!, at: CMTime.zero)
// try? compositionAudioTrack?.insertTimeRange(range, of: audioTrack!, at: CMTime.zero)
let instruction = AVMutableVideoCompositionInstruction.init()
instruction.timeRange = range
let layerInstruction = AVMutableVideoCompositionLayerInstruction.init(assetTrack: compositionVideoTrack!)
//--------------------
//source sound
let soundAsset = AVURLAsset.init(url: self.soundUrl)
let soundTrack = soundAsset.tracks(withMediaType: .audio).first
let compositionSoundTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
try? compositionSoundTrack?.insertTimeRange(range, of: soundTrack!, at: CMTime.zero)
//--------------------
//composite
let transform = videoTrack!.preferredTransform
layerInstruction.setTransform(transform, at: CMTime.zero)
instruction.layerInstructions = [layerInstruction]
let videoComposition = AVMutableVideoComposition.init()
videoComposition.renderSize = videoTrack!.naturalSize
videoComposition.instructions = [instruction]
videoComposition.frameDuration = CMTime.init(value: 1, timescale: 60)
if FileManager.default.fileExists(atPath: self.movieUrl.path) {
try? FileManager.default.removeItem(at: self.movieUrl)
}
let session = AVAssetExportSession.init(asset: composition, presetName: AVAssetExportPresetHighestQuality)
session?.outputURL = self.movieUrl
session?.outputFileType = .mp4
session?.videoComposition = videoComposition
session?.exportAsynchronously(completionHandler: {
if session?.status == AVAssetExportSession.Status.completed {
DispatchQueue.global(qos: .default).async {
DispatchQueue.main.async {
print("finished")
}
}
}
})
DispatchQueue.global(qos: .default).async {
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer: Timer) in
print("\(Int((session?.progress ?? 0) * 100.0))%")
//完了したらtimer.invalidate()を実行
})
}
}
}
}
もともとの無音声動画と音声は同じdurationという前提なので再生時間はそのまま抜き出しています。
AVの時間情報
let asset = AVURLAsset.init(url: self.videoUrl)
let range = CMTimeRangeMake(start: CMTime.zero, duration: asset.duration)
AVAssetで動画でも音声でも時間情報を取り出すことができます。CMTimeを操作すること自由に再生時間を制御できますが、動画と音声で考え方が違います。CMTimeは動画であればフレームレート、音声であればサンプルレートを時間基準を算定します。(このあたりは機会があれば)
またこのサンプルではViewがLoadされてからメインキュー内で動画が作成されていますが、本来はメインとは別で行われるべきです。実際には自分でキューを作ってその中で処理をしていますが、サンプルコードでは省略しています。以下がその名残です。
またその場合、ユーザーに処理状況をお知らせする必要があります。UIまわりはメインキューでないと反映されないのでDispatchQueue.main.async内で処理状況を知らせるUI要素をするとよいです。進行状況はsession.progressで取得できます。
let dispatchQueue = DispatchQueue(label: "queue")
(略)
DispatchQueue.global(qos: .default).async {
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer: Timer) in
print("\(Int((session?.progress ?? 0) * 100.0))%")
//完了したらtimer.invalidate()を実行
})
}
}
手順としては
- AVAssetで再生時間に関する情報を抜き出す
- AVMutableCompositionにVideoTrackとAudioTrackを追加する
- AVAssetExportSessionに合成(composition)のための情報を渡してエクスポートする
これを応用すれば音声を差し替えたり、動画同士を連結することもできるようになります。それはまたの機会に。