AVFoundation框架解析(十六)—— 一個簡單示例之播放招狸、錄制以及混合視頻(一)

版本記錄

版本號 時間
V1.0 2018.08.18

前言

AVFoundation框架是ios中很重要的框架婿滓,所有與視頻音頻相關(guān)的軟硬件控制都在這個框架里面甜孤,接下來這幾篇就主要對這個框架進行介紹和講解协饲。感興趣的可以看我上幾篇。
1. AVFoundation框架解析(一)—— 基本概覽
2. AVFoundation框架解析(二)—— 實現(xiàn)視頻預(yù)覽錄制保存到相冊
3. AVFoundation框架解析(三)—— 幾個關(guān)鍵問題之關(guān)于框架的深度概括
4. AVFoundation框架解析(四)—— 幾個關(guān)鍵問題之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 幾個關(guān)鍵問題之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 視頻音頻的合成(一)
7. AVFoundation框架解析(七)—— 視頻組合和音頻混合調(diào)試
8. AVFoundation框架解析(八)—— 優(yōu)化用戶的播放體驗
9. AVFoundation框架解析(九)—— AVFoundation的變化(一)
10. AVFoundation框架解析(十)—— AVFoundation的變化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的變化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的變化(四)
13. AVFoundation框架解析(十三)—— 構(gòu)建基本播放應(yīng)用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)
15. AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)

開始

這一篇我們就一起看一下播放缴川、錄制以及混合視頻茉稠。

注意:本文寫作環(huán)境Swift 4, iOS 11, Xcode 9。

錄制視頻把夸,并以編程方式播放它們蜜宪,這是您可以使用手機做的最酷的事情之一械巡,但沒有足夠的應(yīng)用程序使用它腕柜。 要做到這一點,需要AV Foundation框架嘹狞,該框架自OS X Lion(10.7)以來一直是macOS的一部分,自2010年iOS 4以來一直是iOS的一部分誓竿。

AV Foundation已經(jīng)有了很大的發(fā)展磅网,目前有超過100個類。 本教程介紹了媒體播放和一些輕量級編輯筷屡,以幫助您開始使用AV Foundation涧偷。 特別是,您將學(xué)習:

  • 從媒體庫中選擇并播放視頻毙死。
  • 錄制視頻并將其保存到媒體庫燎潮。
  • 將多個視頻合并為一個組合視頻,并配有自定義聲音扼倘!

本工程包括一個storyboard和幾個VC确封,UI用來處理簡單的視頻播放以及錄制。

主屏幕包含下面的三個按鈕唉锌,它們與其他VC相對應(yīng):

  • Select and Play Video
  • Record and Save Video
  • Merge Video

Build并Run這個工程隅肥,只有初始場景上的三個按鈕可以做點事情竿奏,但你很快就會改變它袄简!


Select and Play Video - 選擇并播放視頻

主屏幕上的Select and Play Video按鈕將seguesPlayVideoController。 在本教程的這一部分中泛啸,您將添加代碼以選擇視頻文件并進行播放绿语。

首先打開PlayVideoViewController.swift,然后在文件頂部添加以下import語句:

import AVKit
import MobileCoreServices

通過導(dǎo)入AVKit候址,您可以訪問播放所選視頻的AVPlayer對象吕粹。 MobileCoreServices包含預(yù)定義的常量,例如kUTTypeMovie岗仑,您在選擇視頻時需要這些常量匹耕。

接下來,向下滾動到文件末尾并添加以下類擴展荠雕。 確保將這些添加到文件的最底部稳其,在類聲明的花括號之外:

// MARK: - UIImagePickerControllerDelegate
extension PlayVideoViewController: UIImagePickerControllerDelegate {
}

// MARK: - UINavigationControllerDelegate
extension PlayVideoViewController: UINavigationControllerDelegate {
}

這些擴展設(shè)置PlayVideoViewController以采用UIImagePickerControllerDelegateUINavigationControllerDelegate協(xié)議。 您將使用系統(tǒng)提供的UIImagePickerController來允許用戶瀏覽照片庫中的視頻炸卑,并且該類通過這些委托協(xié)議與您的應(yīng)用程序進行通信既鞠。 雖然該類被命名為“image picker”,但請放心盖文,它也適用于視頻嘱蛋!

接下來,返回PlayVideoViewController的主類定義,并從VideoHelper添加對helper方法的調(diào)用以打開圖像選擇器洒敏。 稍后龄恋,您將在VideoHelper中添加自己的幫助工具。 將以下代碼添加到playVideo(_ :)

VideoHelper.startMediaBrowser(delegate: self, sourceType: .savedPhotosAlbum)

在上面的代碼中桐玻,您確保點擊Play Video將打開UIImagePickerController篙挽,允許用戶從媒體庫中選擇視頻文件。

要查看此方法的內(nèi)容镊靴,請打開VideoHelper.swift铣卡。它執(zhí)行以下操作:

  • 1)檢查設(shè)備上是否有.savedPhotosAlbum源。其他來源是相機本身和照片庫偏竟。每當您使用UIImagePickerController選擇媒體時煮落,此檢查都是必不可少的。如果不這樣做踊谋,您可能會嘗試從不存在的媒體庫中選擇媒體蝉仇,從而導(dǎo)致崩潰或其他意外問題。
  • 2)如果您想要的源可用殖蚕,它將創(chuàng)建一個UIImagePickerController對象并設(shè)置其源和媒體類型轿衔。
  • 3)最后,它以模態(tài)方式呈現(xiàn)UIImagePickerController睦疫。

現(xiàn)在害驹,Build項目并運行。點擊第一個屏幕上的Select and Play Video蛤育,然后點擊第二個屏幕上的Play Video宛官,您應(yīng)該看到您的視頻顯示類似于以下屏幕截圖。

看到視頻列表后瓦糕,請選擇一個底洗。 您將進入另一個詳細顯示視頻的屏幕,以及取消咕娄,播放和選擇的按鈕亥揖。 如果您點按播放按鈕,視頻將會播放圣勒。 但是费变,如果您點擊選擇按鈕,應(yīng)用程序只會返回播放視頻屏幕灾而! 這是因為您沒有實現(xiàn)任何委托方法來處理從選擇器中選擇視頻的問題胡控。

回到Xcode,向下滾動到PlayVideoViewController.swift中的UIImagePickerControllerDelegate類擴展旁趟,并添加以下委托方法實現(xiàn):

func imagePickerController(_ picker: UIImagePickerController, 
                           didFinishPickingMediaWithInfo info: [String : Any]) {
  // 1
  guard 
    let mediaType = info[UIImagePickerControllerMediaType] as? String,
    mediaType == (kUTTypeMovie as String),
    let url = info[UIImagePickerControllerMediaURL] as? URL
    else { 
      return 
  }
  
  // 2
  dismiss(animated: true) {
    //3
    let player = AVPlayer(url: url)
    let vcPlayer = AVPlayerViewController()
    vcPlayer.player = player
    self.present(vcPlayer, animated: true, completion: nil)
  }
}

以下是您在此方法中所做的事情:

  • 1)您將獲得所選媒體和URL的媒體類型昼激。 你確保它類型是movie庇绽。
  • 2)你dismiss了image picker
  • 3)在完成的block中橙困,您將創(chuàng)建一個AVPlayerViewController來播放媒體瞧掺。

Build并運行。 點擊Select and Play Video凡傅,然后Play Video辟狈,然后從列表中選擇一個視頻。 您應(yīng)該能夠在媒體播放器中看到視頻播放夏跷。


Record and Save Video - 錄制并保存視頻

現(xiàn)在您已經(jīng)進行了視頻播放哼转,現(xiàn)在可以使用設(shè)備的相機錄制視頻并將其保存到媒體庫。

打開RecordVideoViewController.swift槽华,并添加以下導(dǎo)入:

import MobileCoreServices

您還需要遵守與PlayVideoViewController相同的協(xié)議壹蔓,方法是在文件末尾添加以下內(nèi)容:

extension RecordVideoViewController: UIImagePickerControllerDelegate {
}

extension RecordVideoViewController: UINavigationControllerDelegate {
}

將以下代碼添加到record(_:)

VideoHelper.startMediaBrowser(delegate: self, sourceType: .camera)

它使用與PlayVideoViewController相同的helper方法,但它訪問.camera而不是錄制視頻猫态。

Build并運行以查看到目前為止您所擁有的內(nèi)容佣蓉。

轉(zhuǎn)到Record屏幕,然后點擊Record Video亲雪。 相機UI將打開勇凭,而不是照片庫。 當alert對話框詢問攝像機權(quán)限和麥克風權(quán)限時义辕,單擊OK虾标。 通過點擊屏幕底部的紅色記錄按鈕開始錄制視頻,并在完成錄制后再次點擊它终息。

現(xiàn)在您可以選擇使用錄制的視頻或重新錄制夺巩。 點按Use Video贞让。 您會注意到它只是取消了視圖控制器周崭。 那是因為 - 您猜對了 - 您還沒有實現(xiàn)適當?shù)奈蟹椒▽浿频囊曨l保存到媒體庫。

將以下方法添加到底部的UIImagePickerControllerDelegate類擴展中:

func imagePickerController(_ picker: UIImagePickerController, 
                           didFinishPickingMediaWithInfo info: [String : Any]) {
  dismiss(animated: true, completion: nil)
  
  guard 
    let mediaType = info[UIImagePickerControllerMediaType] as? String,
    mediaType == (kUTTypeMovie as String),
    let url = info[UIImagePickerControllerMediaURL] as? URL,
    UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(url.path)
    else {
      return
  }
  
  // Handle a movie capture
  UISaveVideoAtPathToSavedPhotosAlbum(
    url.path, 
    self, 
    #selector(video(_:didFinishSavingWithError:contextInfo:)), 
    nil)
}

不要擔心最后一行代碼的錯誤喳张,你很快就會解決這個問題续镇。

和以前一樣,委托方法會為您提供指向視頻的URL销部。 您確認應(yīng)用程序可以將文件保存到設(shè)備的相冊中摸航,如果是,請保存舅桩。

UISaveVideoAtPathToSavedPhotosAlbum是SDK提供的用于將視頻保存到相冊的功能酱虎。 作為參數(shù),您將路徑傳遞給要保存的視頻以及要回調(diào)的目標和操作擂涛,這將告知您保存操作的狀態(tài)读串。

接下來將回調(diào)的實現(xiàn)添加到主類定義中:

@objc func video(_ videoPath: String, didFinishSavingWithError error: Error?, contextInfo info: AnyObject) {
  let title = (error == nil) ? "Success" : "Error"
  let message = (error == nil) ? "Video was saved" : "Video failed to save"
  
  let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
  alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.cancel, handler: nil))
  present(alert, animated: true, completion: nil)
}

回調(diào)方法只是向用戶顯示alert彈窗,根據(jù)錯誤狀態(tài)通知是否保存了視頻文件。

Build并運行恢暖。 錄制視頻排监,然后在錄制完成后選擇Use Video。 如果您被要求獲得保存到視頻庫的權(quán)限杰捂,請點按OK舆床。 如果彈出Video was saved彈窗,表明您已將視頻成功保存到照片庫嫁佳!

現(xiàn)在您可以播放視頻和錄制視頻挨队,現(xiàn)在是時候采取下一步并嘗試一些輕量級視頻編輯。


Merging Videos - 混合視頻

該應(yīng)用程序的最后一項功能是進行一些編輯蒿往。您的用戶將從音樂庫中選擇兩個視頻和一首歌曲瞒瘸,該應(yīng)用程序?qū)⒑喜⑦@兩個視頻并混合音樂。

該項目已在MergeVideoViewController.swift中有一個初始實現(xiàn)熄浓。這里的代碼類似于您為播放視頻而編寫的代碼情臭。最大的區(qū)別在于合并時,用戶需要選擇兩個視頻赌蔑。該部分已經(jīng)設(shè)置好俯在,因此用戶可以進行兩個選擇,這些選擇將存儲在firstAssetsecondAsset中娃惯。

下一步是添加選擇音頻文件的功能跷乐。

UIImagePickerController僅提供從媒體庫中選擇視頻和圖像的功能。要從音樂庫中選擇音頻文件趾浅,您將使用MPMediaPickerController愕提。它的工作原理與UIImagePickerController基本相同,但它不是圖像和視頻皿哨,而是訪問媒體庫中的音頻文件浅侨。

打開MergeVideoViewController.swift并將以下代碼添加到loadAudio(_ :)

let mediaPickerController = MPMediaPickerController(mediaTypes: .any)
mediaPickerController.delegate = self
mediaPickerController.prompt = "Select Audio"
present(mediaPickerController, animated: true, completion: nil)

上面的代碼創(chuàng)建了一個新的MPMediaPickerController實例,并將其顯示為模態(tài)視圖控制器证膨。

Build并運行如输。 現(xiàn)在點擊Merge Video,然后點擊Load Audio以訪問設(shè)備上的音頻庫央勒。 當然不见,您需要在設(shè)備上使用一些音頻文件。 否則崔步,列表將為空稳吮。 歌曲也必須實際存在于設(shè)備上,因此請確保您沒有嘗試從云中加載歌曲井濒。

如果您從列表中選擇一首歌曲灶似,您會發(fā)現(xiàn)沒有任何反應(yīng)慎陵。 那就對了! MPMediaPickerController需要委托方法喻奥! 在文件底部找到MPMediaPickerControllerDelegate類擴展席纽,并添加以下兩個方法:

func mediaPicker(_ mediaPicker: MPMediaPickerController, 
                 didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
  
  dismiss(animated: true) {
    let selectedSongs = mediaItemCollection.items
    guard let song = selectedSongs.first else { return }
    
    let url = song.value(forProperty: MPMediaItemPropertyAssetURL) as? URL
    self.audioAsset = (url == nil) ? nil : AVAsset(url: url!)
    let title = (url == nil) ? "Asset Not Available" : "Asset Loaded"
    let message = (url == nil) ? "Audio Not Loaded" : "Audio Loaded"
    
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler:nil))
    self.present(alert, animated: true, completion: nil)
  }
}

func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) {
  dismiss(animated: true, completion: nil)
}

該代碼與UIImagePickerController的委托方法非常相似。 在確保它是有效的媒體項目后撞蚕,您可以根據(jù)通過MPMediaPickerController選擇的媒體項目設(shè)置音頻資源润梯。 請注意,在dismiss當前控制器之后僅顯示新的視圖控制器很重要甥厦,這就是為什么將上面的代碼包裝在完成處理程序中的原因纺铭。

Build并運行。 轉(zhuǎn)到Merge Videos屏幕刀疙。 選擇一個音頻文件舶赔,如果沒有錯誤,您應(yīng)該看到“Audio Loaded”消息谦秧。

您現(xiàn)在可以正確加載所有資源竟纳。 是時候?qū)⒏鞣N媒體文件合并到一個文件中了。 但是在你進入代碼之前疚鲤,你必須做一些設(shè)置锥累。

1. Export and Merge - 導(dǎo)出和合并

合并資源的代碼需要完成處理程序才能將最終視頻導(dǎo)出到相冊。

將以下代碼添加到MergeVideoViewController

func exportDidFinish(_ session: AVAssetExportSession) {
  
  // Cleanup assets
  activityMonitor.stopAnimating()
  firstAsset = nil
  secondAsset = nil
  audioAsset = nil
  
  guard 
    session.status == AVAssetExportSessionStatus.completed,
    let outputURL = session.outputURL 
    else {
      return
  }
  
  let saveVideoToPhotos = {
    PHPhotoLibrary.shared().performChanges({ 
      PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
    }) { saved, error in
      let success = saved && (error == nil)
      let title = success ? "Success" : "Error"
      let message = success ? "Video saved" : "Failed to save video"
      
      let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.cancel, handler: nil))
      self.present(alert, animated: true, completion: nil)
    }
  }
  
  // Ensure permission to access Photo Library
  if PHPhotoLibrary.authorizationStatus() != .authorized {
    PHPhotoLibrary.requestAuthorization { status in
      if status == .authorized {
        saveVideoToPhotos()
      }
    }
  } else {
    saveVideoToPhotos()
  }
}

導(dǎo)出成功完成后集歇,上面的代碼會將新導(dǎo)出的視頻保存到相冊中桶略。 您只需在AssetBrowser中顯示輸出視頻,但將輸出視頻復(fù)制到相冊會更容易诲宇,這樣您就可以看到最終輸出际歼。

現(xiàn)在,添加以下代碼到merge(_:)

guard 
  let firstAsset = firstAsset, 
  let secondAsset = secondAsset 
  else {
    return
}

activityMonitor.startAnimating()

// 1 - Create AVMutableComposition object. This object will hold your AVMutableCompositionTrack instances.
let mixComposition = AVMutableComposition()

// 2 - Create two video tracks
guard 
  let firstTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, 
                                                  preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) 
  else {
    return
}
do {
  try firstTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, firstAsset.duration), 
                                 of: firstAsset.tracks(withMediaType: AVMediaType.video)[0], 
                                 at: kCMTimeZero)
} catch {
  print("Failed to load first track")
  return
}

guard 
  let secondTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, 
                                                   preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
  else {
    return
}
do {
  try secondTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, secondAsset.duration), 
                                  of: secondAsset.tracks(withMediaType: AVMediaType.video)[0], 
                                  at: firstAsset.duration)
} catch {
  print("Failed to load second track")
  return
}

// 3 - Audio track
if let loadedAudioAsset = audioAsset {
  let audioTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: 0)
  do {
    try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, 
                                                    CMTimeAdd(firstAsset.duration, 
                                                              secondAsset.duration)),
                                    of: loadedAudioAsset.tracks(withMediaType: AVMediaType.audio)[0] ,
                                    at: kCMTimeZero)
  } catch {
    print("Failed to load Audio track")
  }
}

// 4 - Get path
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, 
                                                       in: .userDomainMask).first else {
  return
}
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
let date = dateFormatter.string(from: Date())
let url = documentDirectory.appendingPathComponent("mergeVideo-\(date).mov")

// 5 - Create Exporter
guard let exporter = AVAssetExportSession(asset: mixComposition, 
                                          presetName: AVAssetExportPresetHighestQuality) else {
  return
}
exporter.outputURL = url
exporter.outputFileType = AVFileType.mov
exporter.shouldOptimizeForNetworkUse = true

// 6 - Perform the Export
exporter.exportAsynchronously() {
  DispatchQueue.main.async {
    self.exportDidFinish(exporter)
  }
}

以下是上述代碼的逐步細分:

  • 1)您創(chuàng)建一個AVMutableComposition對象來保存視頻和音頻軌道以及轉(zhuǎn)換效果姑蓝。
  • 2)接下來鹅心,為視頻創(chuàng)建AVMutableCompositionTrack并將其添加到AVMutableComposition對象。然后將兩個視頻插入新創(chuàng)建的AVMutableCompositionTrack它掂。

請注意巴帮,insertTimeRange(_:ofTrack:atStartTime :)允許您將視頻的一部分插入主要合成而不是整個視頻溯泣。這樣虐秋,您可以將視頻剪裁到您選擇的時間范圍。

在這種情況下垃沦,您要插入整個視頻客给,以便創(chuàng)建從kCMTimeZero到視頻資源持續(xù)時間的時間范圍。 atStartTime參數(shù)允許您將視頻/音頻軌道放置在合成中的任何位置肢簿。注意代碼如何在零時插入firstAsset靶剑,并在第一個視頻的末尾插入secondAsset蜻拨。本教程假設(shè)您希望視頻資源一個接一個。但您也可以通過播放時間范圍來重疊資源桩引。

要處理時間范圍缎讼,請使用CMTime結(jié)構(gòu)。 CMTime結(jié)構(gòu)是表示時間的非不透明可變結(jié)構(gòu)坑匠,其中時間可以是時間戳或持續(xù)時間血崭。

  • 3)同樣,您可以為音頻創(chuàng)建新軌道并將其添加到主合成中厘灼。這次您將音頻時間范圍設(shè)置為第一個和第二個視頻的持續(xù)時間之和夹纫,因為這將是視頻的完整長度。
  • 4)在保存最終視頻之前设凹,您需要一個保存文件的路徑舰讹。因此,創(chuàng)建一個指向文檔文件夾中文件的唯一文件名(基于當前日期)闪朱。
  • 5)最后月匣,渲染并導(dǎo)出合并的視頻。為此奋姿,您需要創(chuàng)建一個AVAssetExportSession對象桶错,該對象對AVAsset源對象的內(nèi)容進行轉(zhuǎn)碼,以創(chuàng)建由指定導(dǎo)出預(yù)設(shè)描述的格式的輸出胀蛮。
  • 6)使用包含源媒體的資源院刁,導(dǎo)出預(yù)設(shè)名稱(presetName)和輸出文件類型(outputFileType)初始化導(dǎo)出會話后,通過調(diào)用exportAsynchronously()啟動導(dǎo)出運行粪狼。因為代碼異步執(zhí)行導(dǎo)出退腥,所以此方法立即返回。代碼調(diào)用您為exportAsynchronously()提供的完成處理程序再榄,無論導(dǎo)出是失敗狡刘,完成還是用戶取消。完成后困鸥,導(dǎo)出器的status屬性指示導(dǎo)出是否已成功完成嗅蔬。如果失敗,則導(dǎo)出器的error屬性的值提供有關(guān)失敗原因的其他信息疾就。

AVComposition實例組合來自多個基于文件的源的媒體數(shù)據(jù)澜术。 在頂層,AVComposition是一組軌道猬腰,每個軌道都呈現(xiàn)特定類型的媒體鸟废,如音頻或視頻。 AVCompositionTrack的實例表示單個軌道姑荷。

類似地盒延,AVMutableCompositionAVMutableCompositionTrack也提供了用于構(gòu)造合成的更高級別的接口缩擂。 這些對象提供了您已經(jīng)看過的插入,移除和縮放操作添寺。

繼續(xù)胯盯,Build并運行您的項目!

選擇兩個視頻和一個音頻文件并合并所選文件计露。 如果合并成功陨闹,您應(yīng)該看到AVComposition實例組合來自多個基于文件的源的媒體數(shù)據(jù)。 在頂層薄坏,AVComposition是一組軌道趋厉,每個軌道都呈現(xiàn)特定類型的媒體,如音頻或視頻胶坠。 AVCompositionTrack的實例表示單個軌道君账。

類似地,AVMutableComposition和AVMutableCompositionTrack也提供了用于構(gòu)造組合的更高級別的接口沈善。 這些對象提供了您已經(jīng)看過的插入乡数,移除和縮放操作,并且會再次出現(xiàn)闻牡。

繼續(xù)净赴,構(gòu)建并運行您的項目!

選擇兩個視頻和一個音頻文件并合并所選文件罩润。 如果合并成功玖翅,您應(yīng)該看到“視頻已保存”消息。 此時割以,您的新視頻應(yīng)該出現(xiàn)在相冊中金度。消息。 此時严沥,您的新視頻應(yīng)該出現(xiàn)在相冊中猜极。

轉(zhuǎn)到相冊,或使用應(yīng)用程序中的Select and Play Video屏幕進行瀏覽消玄。 您可能會注意到跟伏,雖然應(yīng)用程序合并了視頻,但仍存在一些方向問題翩瓜。 縱向視頻處于橫向模式受扳,有時視頻會上下顛倒。

這是由于默認的AVAsset方向奥溺。 使用默認iPhone相機應(yīng)用程序錄制的所有電影和圖像文件都將視頻幀設(shè)置為橫向辞色,因此iPhone會以橫向模式保存媒體。

2. Video Orientation - 視頻方向

AVAsset具有包含媒體方向信息的preferredTransform屬性浮定,只要您使用Photos應(yīng)用程序或QuickTime查看媒體文件相满,它就會將其應(yīng)用于媒體文件。 在上面的代碼中桦卒,您尚未將變換應(yīng)用于AVAsset對象立美,因此導(dǎo)致問題。

您可以通過將必要的變換應(yīng)用于AVAsset對象來輕松糾正此問題方灾。 但是建蹄,由于您的兩個視頻文件可能具有不同的方向,因此您需要使用兩個單獨的AVMutableCompositionTrack實例裕偿,而不是您最初使用的實例洞慎。

在執(zhí)行此操作之前,請將以下helper方法添加到VideoHelper

static func orientationFromTransform(_ transform: CGAffineTransform) 
  -> (orientation: UIImageOrientation, isPortrait: Bool) {
  var assetOrientation = UIImageOrientation.up
  var isPortrait = false
  if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 {
    assetOrientation = .right
    isPortrait = true
  } else if transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 {
    assetOrientation = .left
    isPortrait = true
  } else if transform.a == 1.0 && transform.b == 0 && transform.c == 0 && transform.d == 1.0 {
    assetOrientation = .up
  } else if transform.a == -1.0 && transform.b == 0 && transform.c == 0 && transform.d == -1.0 {
    assetOrientation = .down
  }
  return (assetOrientation, isPortrait)
}

此代碼分析仿射變換affine transform以確定輸入視頻的方向嘿棘。

接下來劲腿,向該類添加一個helper方法:

static func videoCompositionInstruction(_ track: AVCompositionTrack, asset: AVAsset) 
  -> AVMutableVideoCompositionLayerInstruction {
  let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
  let assetTrack = asset.tracks(withMediaType: .video)[0]
  
  let transform = assetTrack.preferredTransform
  let assetInfo = orientationFromTransform(transform)
  
  var scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.width
  if assetInfo.isPortrait {
    scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.height
    let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
    instruction.setTransform(assetTrack.preferredTransform.concatenating(scaleFactor), at: kCMTimeZero)
  } else {
    let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
    var concat = assetTrack.preferredTransform.concatenating(scaleFactor)
      .concatenating(CGAffineTransform(translationX: 0, y: UIScreen.main.bounds.width / 2))
    if assetInfo.orientation == .down {
      let fixUpsideDown = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
      let windowBounds = UIScreen.main.bounds
      let yFix = assetTrack.naturalSize.height + windowBounds.height
      let centerFix = CGAffineTransform(translationX: assetTrack.naturalSize.width, y: yFix)
      concat = fixUpsideDown.concatenating(centerFix).concatenating(scaleFactor)
    }
    instruction.setTransform(concat, at: kCMTimeZero)
  }
  
  return instruction
}

此方法獲取軌道和資源,并返回AVMutableVideoCompositionLayerInstruction鸟妙,其封裝了使視頻正面朝上所需的仿射變換焦人。這是正在做的事情,一步一步看一下:

  • 您創(chuàng)建AVMutableVideoCompositionLayerInstruction并將其與firstTrack關(guān)聯(lián)重父。
  • 接下來花椭,從AVAsset創(chuàng)建AVAssetTrack對象。 AVAssetTrack對象為所有資產(chǎn)提供軌道級別檢查接口房午。您需要此對象才能訪問preferredTransform和資源的維度矿辽。
  • 然后,保存首選變換以及使視頻適合當前屏幕所需的縮放量郭厌。您將在以下步驟中使用這些值嗦锐。
  • 如果視頻是縱向的,則需要重新計算比例因子沪曙,因為默認計算適用于橫向視頻奕污。然后,您需要做的就是應(yīng)用方向旋轉(zhuǎn)和縮放變換液走。
  • 如果視頻是橫向視圖碳默,則應(yīng)用縮放和變換的步驟類似。有一個額外的檢查缘眶,因為視頻可以在landscape leftlandscape right中生成嘱根。因為有“兩個landscape”,寬高比會進行匹配巷懈,但視頻可能會旋轉(zhuǎn)180度该抒。額外檢查.Down的視頻方向?qū)⑻幚泶朔N情況。

設(shè)置好helper方法后顶燕,找到merge(_ :)并在#2和#3部分之間插入以下內(nèi)容:

// 2.1
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, 
                                            CMTimeAdd(firstAsset.duration, secondAsset.duration))

// 2.2
let firstInstruction = VideoHelper.videoCompositionInstruction(firstTrack, asset: firstAsset)
firstInstruction.setOpacity(0.0, at: firstAsset.duration)
let secondInstruction = VideoHelper.videoCompositionInstruction(secondTrack, asset: secondAsset)

// 2.3
mainInstruction.layerInstructions = [firstInstruction, secondInstruction]
let mainComposition = AVMutableVideoComposition()
mainComposition.instructions = [mainInstruction]
mainComposition.frameDuration = CMTimeMake(1, 30)
mainComposition.renderSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

首先凑保,設(shè)置兩個單獨的AVMutableCompositionTrack實例冈爹。這意味著您需要將AVMutableVideoCompositionLayerInstruction應(yīng)用于每個軌道,以便分別固定方向欧引。

2.1:首先频伤,設(shè)置mainInstruction來包裝整套instructions。請注意芝此,此處的總時間是第一個資源的持續(xù)時間和第二個資源的持續(xù)時間之和憋肖。

2.2:接下來,使用您之前定義的helper方法設(shè)置兩條指令 - 每條資源一條婚苹。第一個視頻的指令需要額外添加一個:在最后將其不透明度設(shè)置為0岸更,以便在第二個視頻啟動時變?yōu)椴豢梢姟?/p>

2.3:現(xiàn)在您已經(jīng)擁有了第一個和第二個軌道的AVMutableVideoCompositionLayerInstruction實例,只需將它們添加到主AVMutableVideoCompositionInstruction對象即可膊升。接下來怎炊,將mainInstruction對象添加到AVMutableVideoComposition實例的指令屬性中。您還可以將合成的幀速率設(shè)置為30幀/秒用僧。

現(xiàn)在您已經(jīng)配置了AVMutableVideoComposition對象结胀,您只需將其分配給導(dǎo)出器即可。在第5節(jié)末尾插入以下代碼(就在exportAsynchronously():之前责循。

exporter.videoComposition = mainComposition

Build并運行您的項目糟港。 如果您通過組合兩個視頻(以及可選的音頻文件)來創(chuàng)建新視頻,則會在播放新的合并視頻時看到方向問題消失院仿。

如果一直細心的看到這里秸抚,您現(xiàn)在應(yīng)該很好地了解如何在您的應(yīng)用中播放視頻,錄制視頻以及合并多個視頻和音頻歹垫。

AV Foundation在播放視頻時為您提供了很大的靈活性剥汤。 您還可以應(yīng)用任何類型的CGAffineTransform來合并,縮放或定位視頻排惨。

如果您還不會或者不了解吭敢,我建議您查看AV Foundation上WWDC videos,例如AV Foundation播放中的WWDC 2016 session 503 Advanced暮芭。 另外鹿驼,請務(wù)必查看 Apple AV Foundation Framework documentation

題外話:今天是我的生日辕宏,祝自己生日快樂吧畜晰!希望以后的日子會像數(shù)字880818一樣幸運和吉祥宿百,謝謝大家一如既往的支持和鼓勵~~~

后記

本篇主要講述了播放魄揉、錄制以及混合視頻,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喂分,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子块蚌,更是在濱河造成了極大的恐慌闰非,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匈子,死亡現(xiàn)場離奇詭異河胎,居然都是意外死亡闯袒,警方通過查閱死者的電腦和手機虎敦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來政敢,“玉大人其徙,你說我怎么就攤上這事∨缁В” “怎么了唾那?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長褪尝。 經(jīng)常有香客問我闹获,道長,這世上最難降的妖魔是什么河哑? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任避诽,我火速辦了婚禮,結(jié)果婚禮上璃谨,老公的妹妹穿的比我還像新娘沙庐。我一直安慰自己,他們只是感情好佳吞,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布拱雏。 她就那樣靜靜地躺著,像睡著了一般底扳。 火紅的嫁衣襯著肌膚如雪铸抑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天衷模,我揣著相機與錄音鹊汛,去河邊找鬼。 笑死算芯,一個胖子當著我的面吹牛柒昏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播熙揍,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼职祷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起有梆,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤是尖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后泥耀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饺汹,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年痰催,在試婚紗的時候發(fā)現(xiàn)自己被綠了兜辞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡夸溶,死狀恐怖逸吵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缝裁,我是刑警寧澤扫皱,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站捷绑,受9級特大地震影響韩脑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜粹污,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一段多、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厕怜,春花似錦衩匣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至递雀,卻和暖如春柄延,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缀程。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工搜吧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杨凑。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓滤奈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撩满。 傳聞我的和親對象是個殘疾皇子蜒程,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容