個(gè)人做旅游網(wǎng)站的意義百度首頁(yè)排名優(yōu)化服務(wù)
引言
當(dāng)涉及到音視頻編輯時(shí),媒體資源的提取和組合是至關(guān)重要的環(huán)節(jié)。在iOS平臺(tái)上,AVFoundation框架提供了豐富而強(qiáng)大的功能,使得媒體資源的操作變得輕松而高效。從原始的媒體中提取片段,然后將它們巧妙地組合成一個(gè)完整的作品,這是音視頻編輯過(guò)程中的常見(jiàn)任務(wù)之一。在這篇博客中,我們將深入探討iOS AVFoundation框架中的媒體組合功能,探索其如何為開(kāi)發(fā)者提供豐富的工具和技術(shù),幫助他們實(shí)現(xiàn)創(chuàng)意無(wú)限的音視頻編輯項(xiàng)目。
概述

上圖是關(guān)于媒體功能中的核心類(lèi),以及類(lèi)直接的關(guān)系圖。有關(guān)資源組合的功能就源于AVAsset的子類(lèi)AVComposition。一個(gè)組合就是將多種媒體資源組合成一個(gè)自定義的臨時(shí)排列,再將這個(gè)臨時(shí)排列視為一個(gè)可呈現(xiàn)的獨(dú)立媒體項(xiàng)目。就比如AVAsset對(duì)象,組合相當(dāng)于包含了一個(gè)或多個(gè)給定類(lèi)型的媒體軌道的容器。AVComposition中的軌道都是AVAssetTrack的子類(lèi)AVCompositionTrack。一個(gè)組合軌道本身由一個(gè)或多個(gè)媒體片段組成,由AVCompositionTrackSegment類(lèi)定義,代表這個(gè)組合中的實(shí)際媒體區(qū)域。
組合后的對(duì)象關(guān)系如下:

AVComposition和AVCompositionTrack都是不可變對(duì)象,提供對(duì)資源的只讀操作。這些對(duì)象提供了一個(gè)合適的接口讓?xiě)?yīng)用程序的一部分可以進(jìn)行播放或處理。不過(guò),當(dāng)創(chuàng)建自己的組合時(shí),就需要使用AVMutableComposition和AVMutableCompositionTrack所提供的可變子類(lèi)。這些對(duì)象提供的類(lèi)接口需要操作軌道和軌道分段,這樣我們就可以創(chuàng)建所需的臨時(shí)排列了。
基礎(chǔ)方法
這個(gè)基礎(chǔ)的實(shí)例會(huì)將兩個(gè)視頻片段中的前5秒內(nèi)容提取出來(lái),并按照組合視頻軌道的順序進(jìn)行排序。還會(huì)從MP3文件中獎(jiǎng)音頻軌道整合到視頻中,期間會(huì)用到Core Media框架中定義的CMTime數(shù)據(jù)類(lèi)型作為時(shí)間格式,相關(guān)內(nèi)容可以查看其它博客。
let composition = AVMutableComposition()var videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!var audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
上面的示例創(chuàng)建了一個(gè)AVMutableComposition并用它的addMutableTrackWithMediaType:preferredTrackID:方法添加了兩個(gè)軌道對(duì)象。當(dāng)創(chuàng)建組合軌道時(shí),開(kāi)發(fā)者必須指明它所能支持的媒體類(lèi)型,并給出一個(gè)軌道標(biāo)識(shí)符。設(shè)置preferredTrackID:參數(shù)為CMPersistentTrackID,這是一個(gè)32位的整數(shù)值。雖然我們可以傳遞任意標(biāo)識(shí)符作為參數(shù),這個(gè)標(biāo)識(shí)符在我們之后需要返回軌道時(shí)會(huì)用到,不過(guò)一般來(lái)說(shuō)都是賦給它一個(gè)kCMPersistentTrackID_Invalid常量。這個(gè)有著奇怪名字的常量的意思是我們需要?jiǎng)?chuàng)建一個(gè)合適軌道ID的任務(wù)委托給框架,標(biāo)識(shí)符會(huì)以1..n排列。
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)組合資源:

下一步就是將獨(dú)立的媒體片段插入到組合的軌道中。
//1.創(chuàng)建資源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.定義插入點(diǎn)var cursorTime = CMTime.zero//3.定義片段時(shí)長(zhǎng)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.調(diào)整插入時(shí)間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.調(diào)整插入時(shí)間和時(shí)長(zhǎng)cursorTime = CMTime.zerolet audioDuration = composition.durationlet 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)")}
- 首先我們創(chuàng)建了3個(gè)AVAsset資源,當(dāng)然這里面是模擬創(chuàng)建的,其中前2個(gè)表示視頻,第3個(gè)表示音頻。
- 定義了資源的插入時(shí)間點(diǎn)。
- 定義每個(gè)視頻片段資源的插入時(shí)長(zhǎng)。
- 提取第1個(gè)視頻資源的視頻軌道,默認(rèn)視頻資源只有一個(gè)視頻軌道,插入到組合的視頻軌道。
- 調(diào)整下一個(gè)視頻資源的插入時(shí)間為上一個(gè)視頻資源的結(jié)束時(shí)間點(diǎn)。
- 同樣獲取第2個(gè)視頻資源的視頻軌道,插入到組合的視頻軌道。
- 調(diào)整插入時(shí)間為0,并設(shè)置音頻的時(shí)長(zhǎng)。
- 提取音頻資源的音頻軌道并插入到組合的音頻軌道中。
這樣我們的組合就構(gòu)建完成了:

使用示例
下面我們將著色創(chuàng)建一個(gè)視頻編輯的應(yīng)用程序,接下來(lái)的博客也將圍繞這個(gè)程序不斷的添加和完善視頻編輯的功能。
項(xiàng)目介紹
應(yīng)用程序?qū)瑑蓚€(gè)不同的部分,一個(gè)是視頻播放器,我們只需在之前博客的視頻播放器中稍作改動(dòng),一個(gè)是可以選擇媒體和允許媒體排列組合的視頻編輯部分,重點(diǎn)會(huì)放在視頻編輯的部分。
播放器
播放器和視頻播放相關(guān)博客的播放器大致相同,只是原來(lái)傳入播放器的是視頻地址,而現(xiàn)在傳入的需要是一個(gè)完整的AVPlayerItem。因此需要重寫(xiě)了init方法,并且添加另一個(gè)用于替換當(dāng)前播放AVPlayerItem的方法。
init方法:
override init() {super.init()self.player = AVPlayer(playerItem: playerItem)if let player = player {playerView = PHPlayerView(player: player)}addObserverForPlayerItem()}/// 自定義初始化方法////// - Parameters:/// - playerItem: AVPlayerIteminit(playerItem: AVPlayerItem? = nil) {super.init()self.playerItem = playerItemself.player = AVPlayer(playerItem: playerItem)if let player = player {playerView = PHPlayerView(player: player)}addObserverForPlayerItem()}
替換當(dāng)前AVPlayerItem方法:
/// AVPlayer的同名方法,替換當(dāng)前播的資源////// - Parameters:/// - playerItem: AVPlayerItemfunc replaceCurrentItem(playerItem:AVPlayerItem?) {guard let player = self.player else { return }self.playerItem = playerItemplayer.replaceCurrentItem(with: playerItem)addObserverForPlayerItem()}/// 為AVPlayerItem添加監(jiān)聽(tīng)func addObserverForPlayerItem() {guard let playerItem = playerItem else { return }playerItem.addObserver(self, forKeyPath: status_keypath, context: &playerItemContext)}
另外我們將播放進(jìn)度的監(jiān)聽(tīng)由原來(lái)的0.5改為了1/60秒,因?yàn)槲覀冃枰褂盟鼇?lái)同步動(dòng)畫(huà),而不僅僅是顯示當(dāng)前時(shí)間。
/// 監(jiān)聽(tīng)播放進(jìn)度f(wàn)unc addPlayerItemTimeObserver() {guard let player = player else { return }let interval = CMTimeMakeWithSeconds(1/60.0, preferredTimescale: Int32(NSEC_PER_SEC))let queue = DispatchQueue.maintimeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: queue, using: {[weak self] time inguard 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)})}
編輯器
編輯器由兩部分構(gòu)成,媒體資源選擇器,和媒體資源編輯區(qū)域。
博客的示例項(xiàng)目中,我們只獲取了視頻媒體資源,音頻媒體資源的做法與視頻完全相同,只是傳入的mediaType為audio。
媒體資源選擇器為一個(gè)簡(jiǎn)單的列表,點(diǎn)擊加號(hào)后會(huì)根據(jù)所選擇的媒體資源創(chuàng)建一個(gè)PHMediaItem,PHMediaItem是一個(gè)基類(lèi),它的子類(lèi)又分為PHVideoItem和PHAudioItem,后續(xù)的功能我們也許會(huì)用到PHAudioItem,但目前我們只需要使用PHVideoItem即可。

媒體資源編輯器就是頁(yè)面除播放器以外的下半部分,有顯示媒體選擇器的按鈕,和控制播放器播放和暫停的按鈕,以及一個(gè)顯示媒體剪輯狀態(tài)的時(shí)間軸區(qū)域。

創(chuàng)建組合
我們的核心任務(wù)就是通過(guò)頁(yè)面上的一些操作來(lái)創(chuàng)建一個(gè)媒體組合,首先聲明一個(gè)PHComposition協(xié)議,協(xié)議中定義了兩個(gè)方法分別用來(lái)生成組合的可播放版本和可導(dǎo)出版本。
import UIKit
import AVFoundationprotocol PHComposition {/// 協(xié)議方法-生成AVPlayerItem////// - Returns: 返回一個(gè)可播放的AVPlayerItemfunc makePlayerItem() -> AVPlayerItem?/// 協(xié)議方法-生成AVAssetExportSession////// - Returns: 返回一個(gè)可導(dǎo)出的AVAssetExportSessionfunc makeAssetExportSession() -> AVAssetExportSession?}
創(chuàng)建一個(gè)遵循PHComposition協(xié)議的類(lèi),并提供協(xié)議方法的實(shí)現(xiàn)。
// 負(fù)責(zé)創(chuàng)建 視頻的可播放資源和可導(dǎo)出資源import UIKit
import AVFoundationclass PHBaseComposition: NSObject,PHComposition {//只讀compositionprivate var compostion:AVComposition?//自定義初始化init(compostion: AVComposition? = nil) {self.compostion = compostion}//MARK: PHComposition - 生成 AVPlayerItemfunc makePlayerItem() -> AVPlayerItem? {if let compostion = compostion {let playerItem = AVPlayerItem(asset: compostion)return playerItem}return nil}//MARK: PHComposition - 生成 AVAssetExportSessionfunc makeAssetExportSession() -> AVAssetExportSession? {return nil}
}
創(chuàng)建一個(gè)組合的構(gòu)建器,同樣我們創(chuàng)建一個(gè)協(xié)議,負(fù)責(zé)來(lái)創(chuàng)建遵循PHComposition協(xié)議的對(duì)象。
import UIKitprotocol PHCompositionBuilder {/// 協(xié)議方法-生成一個(gè)遵循PHComposition協(xié)議的對(duì)象////// - Returns: 返回一個(gè)最新PHComposition協(xié)議的對(duì)象func buildComposition() -> PHComposition?
}
這個(gè)協(xié)議的具體方法由PHBaseCompositionBuilder來(lái)實(shí)現(xiàn),代碼如下。
import UIKit
import AVFoundationclass PHBaseCompositionBuilder: NSObject,PHCompositionBuilder {/// 時(shí)間線(xiàn)var timeLine:PHTimeLine!/// compositionprivate var composition = AVMutableComposition()init(timeLine: PHTimeLine!) {self.timeLine = timeLine}//MARK: PHCompositionBuilder - 生成 PHCompositionfunc buildComposition() -> PHComposition? {addCompositionTrack(mediaType: .video, mediaItems: timeLine.videoItmes)return PHBaseComposition(compostion: self.composition)}/// 私有方法-添加媒體資源軌道/// - Parameters:/// - mediaType: 媒體類(lèi)型/// - mediaItems: 媒體媒體資源數(shù)組/// - Returns: 返回一個(gè)可播放的AVPlayerItemprivate func addCompositionTrack(mediaType:AVMediaType,mediaItems:[PHMediaItem]?) {if PHIsEmpty(array: mediaItems) {return}let trackID = kCMPersistentTrackID_Invalidguard let compositionTrack = composition.addMutableTrack(withMediaType: mediaType, preferredTrackID: trackID) else { return }//設(shè)置起始時(shí)間var cursorTime = CMTime.zeroguard let mediaItems = mediaItems else { return }for item in mediaItems {//這里默認(rèn)時(shí)間都是從0開(kāi)始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在初始化的時(shí)候會(huì)默認(rèn)創(chuàng)建一個(gè)AVMutableComposition對(duì)象用于媒體編輯操作。
- 獲取timeLine對(duì)象中的所有視頻資源,調(diào)用addCompositionTrack方法進(jìn)行拼接。
- addCompositionTrack方法中首先判斷了傳入的資源數(shù)組是否為空。
- 當(dāng)媒體資源數(shù)組不為空的時(shí)候,從composition中獲取對(duì)應(yīng)的媒體軌道。
- 設(shè)置起始時(shí)間,遍歷媒體資源數(shù)組,從每個(gè)資源中獲取對(duì)應(yīng)的媒體軌道并添加到組合媒體軌道中。
- 修改下一個(gè)媒體資源的插入起始時(shí)間。
實(shí)現(xiàn)播放組合媒體
選擇媒體資源
點(diǎn)擊頁(yè)面上的加號(hào)按鈕,顯示媒體選擇列表,點(diǎn)擊列表后會(huì)將選擇的媒體資源創(chuàng)建為PHMediaItem并添加到當(dāng)前的timeLine對(duì)應(yīng)的資源數(shù)組下。
//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 = trueresourcePickerView.layer.cornerRadius = 5.0resourcePickerView.layer.borderColor = UIColor.white.cgColorresourcePickerView.layer.borderWidth = 1.0resourcePickerView.showDialog()resourcePickerView.addMediaItemBlock = { [weak self] mediaItem inguard 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}}
點(diǎn)擊播放按鈕
點(diǎn)擊播放按鈕后判斷是否有可播放資源,再進(jìn)行播放。播放分為兩種情況,從頭開(kāi)始播放和暫停后的繼續(xù)播放。
//MARK: 播放按鈕點(diǎn)擊@objc func playerButtonOnlick(button:UIButton) {if PHIsEmpty(array: timeLine.videoItmes) {playerButton.isSelected = falsereturn}button.isSelected = !button.isSelected//回調(diào)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)}
同步播放進(jìn)度
PHEditorView視頻編輯器遵循了PHControlDelegate協(xié)議,這里我們只關(guān)注setCuttentTime和playbackComplete方法。
setCuttentTime方法用來(lái)同步編輯器時(shí)間軸的進(jìn)度。
playbackComplete用來(lái)同步播放按鈕的狀態(tài)。
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.5self.collectionView?.contentOffset = CGPointMake(origin_offsetX + time * item_size.width, 0.0)}func playbackComplete() {self.playerButton.isSelected = false}}
結(jié)語(yǔ)
在示例項(xiàng)目中,我們僅僅涉及了視頻媒體資源,并默認(rèn)這些資源都是單軌道的。然而,在實(shí)際的應(yīng)用開(kāi)發(fā)中,我們可能會(huì)面對(duì)更加復(fù)雜的情況,涉及到多種類(lèi)型的媒體資源,以及多軌道的組合。iOS AVFoundation框架為我們提供了強(qiáng)大的工具和靈活的接口,讓我們能夠處理各種各樣的媒體資源,并將它們巧妙地組合成為精彩紛呈的作品。通過(guò)深入理解和靈活運(yùn)用AVFoundation框架,我們可以實(shí)現(xiàn)更加復(fù)雜和令人驚嘆的音視頻編輯應(yīng)用,為用戶(hù)帶來(lái)全新的體驗(yàn)和享受。在今后的開(kāi)發(fā)過(guò)程中,讓我們繼續(xù)探索和挖掘AVFoundation框架的潛力,創(chuàng)造出更加優(yōu)秀和創(chuàng)新的音視頻編輯應(yīng)用!
項(xiàng)目地址:PHEditorPlayer: AV Foundation 音視頻編輯