引言
当涉及到音视频编辑时,媒体资源的提取和组合是至关重要的环节。在iOS平台上,AVFoundation框架提供了丰富而强大的功能,使得媒体资源的操作变得轻松而高效。从原始的媒体中提取片段,然后将它们巧妙地组合成一个完整的作品,这是音视频编辑过程中的常见任务之一。在这篇博客中,我们将深入探讨iOS AVFoundation框架中的媒体组合功能,探索其如何为开发者提供丰富的工具和技术,帮助他们实现创意无限的音视频编辑项目。
概述
上图是关于媒体功能中的核心类,以及类直接的关系图。有关资源组合的功能就源于AVAsset的子类AVComposition。一个组合就是将多种媒体资源组合成一个自定义的临时排列,再将这个临时排列视为一个可呈现的独立媒体项目。就比如AVAsset对象,组合相当于包含了一个或多个给定类型的媒体轨道的容器。AVComposition中的轨道都是AVAssetTrack的子类AVCompositionTrack。一个组合轨道本身由一个或多个媒体片段组成,由AVCompositionTrackSegment类定义,代表这个组合中的实际媒体区域。
组合后的对象关系如下:
AVComposition和AVCompositionTrack都是不可变对象,提供对资源的只读操作。这些对象提供了一个合适的接口让应用程序的一部分可以进行播放或处理。不过,当创建自己的组合时,就需要使用AVMutableComposition和AVMutableCompositionTrack所提供的可变子类。这些对象提供的类接口需要操作轨道和轨道分段,这样我们就可以创建所需的临时排列了。
基础方法
这个基础的实例会将两个视频片段中的前5秒内容提取出来,并按照组合视频轨道的顺序进行排序。还会从MP3文件中奖音频轨道整合到视频中,期间会用到Core Media框架中定义的CMTime数据类型作为时间格式,相关内容可以查看其它博客。
let composition = AVMutableComposition()
var videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!
var audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
上面的示例创建了一个AVMutableComposition并用它的addMutableTrackWithMediaType:preferredTrackID:方法添加了两个轨道对象。当创建组合轨道时,开发者必须指明它所能支持的媒体类型,并给出一个轨道标识符。设置preferredTrackID:参数为CMPersistentTrackID,这是一个32位的整数值。虽然我们可以传递任意标识符作为参数,这个标识符在我们之后需要返回轨道时会用到,不过一般来说都是赋给它一个kCMPersistentTrackID_Invalid常量。这个有着奇怪名字的常量的意思是我们需要创建一个合适轨道ID的任务委托给框架,标识符会以1..n排列。
现在我们已经实现了一个组合资源:
下一步就是将独立的媒体片段插入到组合的轨道中。
//1.创建资源
let goldenGateAsset = AVURLAsset(url: URL(string: "1")!, options: nil)
let teaGardenAsset = AVURLAsset(url: URL(string: "2")!, options: nil)
let soundTrackAsset = AVURLAsset(url: URL(string: "3")!, options: nil)
//2.定义插入点
var cursorTime = CMTime.zero
//3.定义片段时长
let videoDuration = CMTime(value: 5, timescale: 1)
let videoTimeRange = CMTimeRange(start: cursorTime, duration: videoDuration)
//4.提取资源中的视频轨道并插入到组合中的视频轨道
let goldenGateAssetTrack = goldenGateAsset.tracks(withMediaType: .video).first!
do {
try videoTrack.insertTimeRange(videoTimeRange, of: goldenGateAssetTrack, at: cursorTime)
} catch {
print("Error inserting time range: \(error)")
}
//5.调整插入时间
cursorTime = CMTimeAdd(cursorTime, videoDuration)
//6.提取资源中的视频轨道并插入到组合中的视频轨道
let teaGardenAssetTrack = teaGardenAsset.tracks(withMediaType: .video).first!
do {
try videoTrack.insertTimeRange(videoTimeRange, of: teaGardenAssetTrack, at: cursorTime)
} catch {
print("Error inserting time range: \(error)")
}
//7.调整插入时间和时长
cursorTime = CMTime.zero
let audioDuration = composition.duration
let audioTimeRange = CMTimeRangeMake(start: cursorTime, duration: audioDuration)
//8.提取音频轨道并插入到组合中的音频轨道
let soundTrackAssetTrack = soundTrackAsset.tracks(withMediaType: .audio).first!
do {
try audioTrack?.insertTimeRange(audioTimeRange, of: soundTrackAssetTrack, at: cursorTime)
} catch {
print("Error inserting time range: \(error)")
}
- 首先我们创建了3个AVAsset资源,当然这里面是模拟创建的,其中前2个表示视频,第3个表示音频。
- 定义了资源的插入时间点。
- 定义每个视频片段资源的插入时长。
- 提取第1个视频资源的视频轨道,默认视频资源只有一个视频轨道,插入到组合的视频轨道。
- 调整下一个视频资源的插入时间为上一个视频资源的结束时间点。
- 同样获取第2个视频资源的视频轨道,插入到组合的视频轨道。
- 调整插入时间为0,并设置音频的时长。
- 提取音频资源的音频轨道并插入到组合的音频轨道中。
这样我们的组合就构建完成了:
使用示例
下面我们将着色创建一个视频编辑的应用程序,接下来的博客也将围绕这个程序不断的添加和完善视频编辑的功能。
项目介绍
应用程序将包含两个不同的部分,一个是视频播放器,我们只需在之前博客的视频播放器中稍作改动,一个是可以选择媒体和允许媒体排列组合的视频编辑部分,重点会放在视频编辑的部分。
播放器
播放器和视频播放相关博客的播放器大致相同,只是原来传入播放器的是视频地址,而现在传入的需要是一个完整的AVPlayerItem。因此需要重写了init方法,并且添加另一个用于替换当前播放AVPlayerItem的方法。
init方法:
override init() {
super.init()
self.player = AVPlayer(playerItem: playerItem)
if let player = player {
playerView = PHPlayerView(player: player)
}
addObserverForPlayerItem()
}
/// 自定义初始化方法
///
/// - Parameters:
/// - playerItem: AVPlayerItem
init(playerItem: AVPlayerItem? = nil) {
super.init()
self.playerItem = playerItem
self.player = AVPlayer(playerItem: playerItem)
if let player = player {
playerView = PHPlayerView(player: player)
}
addObserverForPlayerItem()
}
替换当前AVPlayerItem方法:
/// AVPlayer的同名方法,替换当前播的资源
///
/// - Parameters:
/// - playerItem: AVPlayerItem
func replaceCurrentItem(playerItem:AVPlayerItem?) {
guard let player = self.player else { return }
self.playerItem = playerItem
player.replaceCurrentItem(with: playerItem)
addObserverForPlayerItem()
}
/// 为AVPlayerItem添加监听
func addObserverForPlayerItem() {
guard let playerItem = playerItem else { return }
playerItem.addObserver(self, forKeyPath: status_keypath, context: &playerItemContext)
}
另外我们将播放进度的监听由原来的0.5改为了1/60秒,因为我们需要使用它来同步动画,而不仅仅是显示当前时间。
/// 监听播放进度
func addPlayerItemTimeObserver() {
guard let player = player else { return }
let interval = CMTimeMakeWithSeconds(1/60.0, preferredTimescale: Int32(NSEC_PER_SEC))
let queue = DispatchQueue.main
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: queue, using: {[weak self] time in
guard let self = self else { return }
guard let playerItem = self.playerItem else { return }
guard let delegate = self.delegate else { return }
let currentTime = CMTimeGetSeconds(time)
let duration = CMTimeGetSeconds(playerItem.duration)
delegate.setCuttentTime(time: currentTime, duration: duration)
})
}
编辑器
编辑器由两部分构成,媒体资源选择器,和媒体资源编辑区域。
博客的示例项目中,我们只获取了视频媒体资源,音频媒体资源的做法与视频完全相同,只是传入的mediaType为audio。
媒体资源选择器为一个简单的列表,点击加号后会根据所选择的媒体资源创建一个PHMediaItem,PHMediaItem是一个基类,它的子类又分为PHVideoItem和PHAudioItem,后续的功能我们也许会用到PHAudioItem,但目前我们只需要使用PHVideoItem即可。
媒体资源编辑器就是页面除播放器以外的下半部分,有显示媒体选择器的按钮,和控制播放器播放和暂停的按钮,以及一个显示媒体剪辑状态的时间轴区域。
创建组合
我们的核心任务就是通过页面上的一些操作来创建一个媒体组合,首先声明一个PHComposition协议,协议中定义了两个方法分别用来生成组合的可播放版本和可导出版本。
import UIKit
import AVFoundation
protocol PHComposition {
/// 协议方法-生成AVPlayerItem
///
/// - Returns: 返回一个可播放的AVPlayerItem
func makePlayerItem() -> AVPlayerItem?
/// 协议方法-生成AVAssetExportSession
///
/// - Returns: 返回一个可导出的AVAssetExportSession
func makeAssetExportSession() -> AVAssetExportSession?
}
创建一个遵循PHComposition协议的类,并提供协议方法的实现。
// 负责创建 视频的可播放资源和可导出资源
import UIKit
import AVFoundation
class PHBaseComposition: NSObject,PHComposition {
//只读composition
private var compostion:AVComposition?
//自定义初始化
init(compostion: AVComposition? = nil) {
self.compostion = compostion
}
//MARK: PHComposition - 生成 AVPlayerItem
func makePlayerItem() -> AVPlayerItem? {
if let compostion = compostion {
let playerItem = AVPlayerItem(asset: compostion)
return playerItem
}
return nil
}
//MARK: PHComposition - 生成 AVAssetExportSession
func makeAssetExportSession() -> AVAssetExportSession? {
return nil
}
}
创建一个组合的构建器,同样我们创建一个协议,负责来创建遵循PHComposition协议的对象。
import UIKit
protocol PHCompositionBuilder {
/// 协议方法-生成一个遵循PHComposition协议的对象
///
/// - Returns: 返回一个最新PHComposition协议的对象
func buildComposition() -> PHComposition?
}
这个协议的具体方法由PHBaseCompositionBuilder来实现,代码如下。
import UIKit
import AVFoundation
class PHBaseCompositionBuilder: NSObject,PHCompositionBuilder {
/// 时间线
var timeLine:PHTimeLine!
/// composition
private var composition = AVMutableComposition()
init(timeLine: PHTimeLine!) {
self.timeLine = timeLine
}
//MARK: PHCompositionBuilder - 生成 PHComposition
func buildComposition() -> PHComposition? {
addCompositionTrack(mediaType: .video, mediaItems: timeLine.videoItmes)
return PHBaseComposition(compostion: self.composition)
}
/// 私有方法-添加媒体资源轨道
/// - Parameters:
/// - mediaType: 媒体类型
/// - mediaItems: 媒体媒体资源数组
/// - Returns: 返回一个可播放的AVPlayerItem
private func addCompositionTrack(mediaType:AVMediaType,mediaItems:[PHMediaItem]?) {
if PHIsEmpty(array: mediaItems) {
return
}
let trackID = kCMPersistentTrackID_Invalid
guard let compositionTrack = composition.addMutableTrack(withMediaType: mediaType, preferredTrackID: trackID) else { return }
//设置起始时间
var cursorTime = CMTime.zero
guard let mediaItems = mediaItems else { return }
for item in mediaItems {
//这里默认时间都是从0开始
guard let asset = item.asset else { continue }
guard let assetTrack = asset.tracks(withMediaType: mediaType).first else { continue }
do {
try compositionTrack.insertTimeRange(item.timeRange, of: assetTrack, at: cursorTime)
} catch {
print("addCompositionTrack error")
}
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration)
}
}
}
- PHBaseCompositionBuilder在初始化的时候会默认创建一个AVMutableComposition对象用于媒体编辑操作。
- 获取timeLine对象中的所有视频资源,调用addCompositionTrack方法进行拼接。
- addCompositionTrack方法中首先判断了传入的资源数组是否为空。
- 当媒体资源数组不为空的时候,从composition中获取对应的媒体轨道。
- 设置起始时间,遍历媒体资源数组,从每个资源中获取对应的媒体轨道并添加到组合媒体轨道中。
- 修改下一个媒体资源的插入起始时间。
实现播放组合媒体
选择媒体资源
点击页面上的加号按钮,显示媒体选择列表,点击列表后会将选择的媒体资源创建为PHMediaItem并添加到当前的timeLine对应的资源数组下。
//MARK: 显示选择视频视图
@objc func showItemPickerView() {
let resourcePickerView = PHResourcePickerView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height * 0.5, width: 150.0, height: 200.0))
resourcePickerView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0)
resourcePickerView.layer.masksToBounds = true
resourcePickerView.layer.cornerRadius = 5.0
resourcePickerView.layer.borderColor = UIColor.white.cgColor
resourcePickerView.layer.borderWidth = 1.0
resourcePickerView.showDialog()
resourcePickerView.addMediaItemBlock = { [weak self] mediaItem in
guard let self = self else { return }
if var videoItmes = timeLine.videoItmes {
videoItmes.append(mediaItem as! PHVideoItem)
timeLine.videoItmes = videoItmes
} else {
var videoItmes = [PHVideoItem]()
videoItmes.append(mediaItem as! PHVideoItem)
timeLine.videoItmes = videoItmes
}
self.collectionView?.reloadData()
self.needReplay = true
}
}
点击播放按钮
点击播放按钮后判断是否有可播放资源,再进行播放。播放分为两种情况,从头开始播放和暂停后的继续播放。
//MARK: 播放按钮点击
@objc func playerButtonOnlick(button:UIButton) {
if PHIsEmpty(array: timeLine.videoItmes) {
playerButton.isSelected = false
return
}
button.isSelected = !button.isSelected
//回调
guard let delegate = self.delegate else { return }
if button.isSelected {
if needReplay {
player()
needReplay = false
} else {
delegate.play()
}
} else {
delegate.pause()
}
}
func player() {
guard let delegate = self.delegate else { return }
let compositionBuilder = PHBaseCompositionBuilder(timeLine: timeLine)
let composition = compositionBuilder.buildComposition()
let playerItem = composition?.makePlayerItem()
delegate.replaceCurrentItem(playerItem: playerItem)
}
同步播放进度
PHEditorView视频编辑器遵循了PHControlDelegate协议,这里我们只关注setCuttentTime和playbackComplete方法。
setCuttentTime方法用来同步编辑器时间轴的进度。
playbackComplete用来同步播放按钮的状态。
extension PHEditorView:PHControlDelegate{
func playpause(currentTime: TimeInterval) {
}
func playstart(duration: TimeInterval) {
}
func setCuttentTime(time: TimeInterval, duration: TimeInterval) {
let origin_offsetX = -UIScreen.main.bounds.width * 0.5
self.collectionView?.contentOffset = CGPointMake(origin_offsetX + time * item_size.width, 0.0)
}
func playbackComplete() {
self.playerButton.isSelected = false
}
}
结语
在示例项目中,我们仅仅涉及了视频媒体资源,并默认这些资源都是单轨道的。然而,在实际的应用开发中,我们可能会面对更加复杂的情况,涉及到多种类型的媒体资源,以及多轨道的组合。iOS AVFoundation框架为我们提供了强大的工具和灵活的接口,让我们能够处理各种各样的媒体资源,并将它们巧妙地组合成为精彩纷呈的作品。通过深入理解和灵活运用AVFoundation框架,我们可以实现更加复杂和令人惊叹的音视频编辑应用,为用户带来全新的体验和享受。在今后的开发过程中,让我们继续探索和挖掘AVFoundation框架的潜力,创造出更加优秀和创新的音视频编辑应用!
项目地址:PHEditorPlayer: AV Foundation 音视频编辑