AVFoundation框架解析(二十五) —— 播放茵汰、錄制和合并視頻簡單示例(一)

版本記錄

版本號 時間
V1.0 2020.08.13 星期四

前言

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支持(二)
16. AVFoundation框架解析(十六)—— 一個簡單示例之播放乡范、錄制以及混合視頻(一)
17. AVFoundation框架解析(十七)—— 一個簡單示例之播放、錄制以及混合視頻之源碼及效果展示(二)
18. AVFoundation框架解析(十八)—— AVAudioEngine之基本概覽(一)
19. AVFoundation框架解析(十九)—— AVAudioEngine之詳細說明和一個簡單示例(二)
20. AVFoundation框架解析(二十)—— AVAudioEngine之詳細說明和一個簡單示例源碼(三)
21. AVFoundation框架解析(二十一)—— 一個簡單的視頻流預(yù)覽和播放示例之解析(一)
22. AVFoundation框架解析(二十二)—— 一個簡單的視頻流預(yù)覽和播放示例之源碼(二)
23. AVFoundation框架解析(二十三) —— 向視頻層添加疊加層和動畫(一)
24. AVFoundation框架解析(二十四) —— 向視頻層添加疊加層和動畫(二)

開始

首先看下主要內(nèi)容:

在本教程中页响,了解在帶有AV FoundationiOS上使用視頻的基礎(chǔ)知識篓足。 您將播放,錄制甚至進行一些簡短的視頻編輯闰蚕。內(nèi)容來自翻譯栈拖。

接著看一下寫作環(huán)境

Swift 5, iOS 13, Xcode 11

下面就是正文了。

以編程方式錄制視頻并播放是您可以用手機完成的最酷的事情之一没陡。但是涩哟,幾乎沒有足夠的應(yīng)用程序提供此功能索赏,您可以使用AV Foundation框架輕松添加。

2010OS X Lion(10.7)iOS 4起贴彼,AV Foundation就已成為macOSiOS的一部分潜腻。自那時以來,它的發(fā)展相當(dāng)可觀器仗,迄今為止已有100多個類融涣。

本教程通過介紹媒體播放和一些輕量編輯,使您開始使用AV Foundation精钮。特別是威鹿,您將學(xué)習(xí)如何:

  • 從媒體庫中選擇并播放視頻。
  • 錄制視頻并將其保存到媒體庫轨香。
  • 將多個剪輯合并為一個具有自定義soundtrack的完整視頻忽你。

避免在模擬器上運行本教程中的代碼,因為您將無法采集視頻臂容。另外科雳,您需要找出一種將視頻手動添加到媒體庫的方法。換句話說脓杉,您確實需要在設(shè)備上測試此代碼糟秘!

為此,您需要是注冊的Apple開發(fā)人員registered Apple developer丽已。免費帳戶可以在本教程中正常工作蚌堵。

下面就開始啦!

打開入門項目沛婴,簡單看一下吼畏。 該項目包含一個storyboard和幾個帶有UI的視圖控制器,用于簡單的視頻播放和錄制應(yīng)用程序嘁灯。

主屏幕包含下面的三個按鈕泻蚊,這些按鈕可用于segue到其他視圖控制器:

  • 選擇并播放視頻
  • 錄制并保存視頻
  • 合并視頻

構(gòu)建并運行和測試按鈕。 初始場景中只有三個按鈕可以執(zhí)行任何操作丑婿,但是您很快就會對其進行更改性雄!


Selecting and Playing Video

主屏幕上的Select and Play Video按鈕可以連接到PlayVideoController。 在本教程的這一部分中羹奉,您將添加代碼以選擇一個視頻文件并進行播放秒旋。

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

import AVKit
import MobileCoreServices

導(dǎo)入AVKit可讓您訪問AVPlayer诀拭,用以播放選定的視頻迁筛。 MobileCoreServices包含預(yù)定義的常量,例如kUTTypeMovie耕挨,稍后將需要它們细卧。

接下來尉桩,在文件末尾添加以下類擴展名。 確保將它們添加到文件的最底部贪庙,類聲明的花括號之外:

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

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

這些擴展將PlayVideoViewController設(shè)置為遵循UIImagePickerControllerDelegateUINavigationControllerDelegate協(xié)議蜘犁。

您將使用系統(tǒng)提供的UIImagePickerController來讓用戶瀏覽照片庫中的視頻。 該類通過這些委托協(xié)議傳達回您的應(yīng)用程序止邮。 盡管該班級被稱為image picker这橙,但請放心,它也可以用于視頻农尖!

接下來析恋,回到PlayVideoViewController的主類定義,然后將以下代碼添加到playVideo(_ :)

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

這是對VideoHelper中名為startMediaBrowser(delegate:sourceType :)helper方法的調(diào)用盛卡。此調(diào)用將打開圖像選擇器,將委托設(shè)置為self筑凫。 .savedPhotosAlbum的源類型選擇從相機膠卷中選擇圖像滑沧。稍后,您將在VideoHelper中添加自己的幫助器工具巍实。

要查看此方法的含義滓技,請打開VideoHelper.swift。它執(zhí)行以下操作:

  • 1) 檢查源在設(shè)備上是否存在棚潦。來源包括相機膠卷令漂,相機本身和完整的照片庫。每當(dāng)您使用UIImagePickerController選擇媒體時丸边,此檢查都是必不可少的叠必。如果不這樣做,則可能會嘗試從不存在的來源中選擇媒體妹窖,這通常會導(dǎo)致崩潰纬朝。
  • 2) 如果所需的源可用,它將創(chuàng)建UIImagePickerController并設(shè)置其源和媒體類型骄呼。由于只想選擇視頻共苛,因此代碼將類型限制為kUTTypeMovie
  • 3) 最后蜓萄,它以模態(tài)形式呈現(xiàn)UIImagePickerController隅茎。

現(xiàn)在,您準(zhǔn)備好為您的項目再一次嘗試嫉沽!構(gòu)建并運行辟犀。在第一個屏幕上點擊Select and Play Video,然后在第二個屏幕上點擊Play Video耻蛇。相機膠卷將像這樣彈出:

看到視頻列表后踪蹬,選擇一個胞此。 您將轉(zhuǎn)到另一個屏幕,其中詳細顯示了視頻以及Cancel跃捣,PlayChoose按鈕漱牵。 點擊Play按鈕,毫不奇怪疚漆,視頻將播放酣胀。

但是,如果點擊Choose按鈕娶聘,則該應(yīng)用程序僅返回到Play Video屏幕闻镶! 這是因為您尚未實現(xiàn)任何委托方法來處理從選擇器中選擇視頻。

返回Xcode丸升,再次打開PlayVideoViewController.swift并找到UIImagePickerControllerDelegate擴展铆农。 然后添加以下委托方法實現(xiàn):

func imagePickerController(
  _ picker: UIImagePickerController,
  didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
  // 1
  guard
    let mediaType = info[UIImagePickerController.InfoKey.mediaType] as? String,
    mediaType == (kUTTypeMovie as String),
    let url = info[UIImagePickerController.InfoKey.mediaURL] 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的媒體類型,并確保它是視頻狡耻。
  • 2) 接下來墩剖,關(guān)閉圖像選擇器。
  • 3) 在完成塊(completion block)中夷狰,創(chuàng)建一個AVPlayerViewController來播放媒體岭皂。

構(gòu)建并運行。 點擊Select and Play Video沼头,然后點擊Play Video爷绘,然后從列表中選擇一個視頻。 該視頻將在媒體播放器中播放进倍。


Recording Video

現(xiàn)在您可以播放視頻了土至,現(xiàn)在該使用設(shè)備的攝像機錄制視頻并將其保存到媒體庫中了。

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

import MobileCoreServices

然后背捌,將以下內(nèi)容添加到文件末尾:

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

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

它采用與PlayVideoViewController相同的協(xié)議毙籽。

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

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

它使用與PlayVideoViewController中相同的helper方法毡庆,除了它訪問.camera以指示圖像選擇器以內(nèi)置照相機模式打開坑赡。

構(gòu)建并運行以查看您到目前為止所擁有的。

轉(zhuǎn)到Record屏幕么抗,然后點擊Record Video毅否。 代替相機圖庫,將打開相機用戶界面蝇刀。 當(dāng)alert對話框詢問攝像機權(quán)限和麥克風(fēng)權(quán)限時螟加,單擊OK

最后,點擊屏幕底部的紅色錄制按鈕開始錄制視頻捆探; 完成錄制后然爆,再次點按它。

現(xiàn)在黍图,您有兩個選擇:使用錄制的視頻或重錄曾雕。 點擊Use Video。 您會注意到助被,它只是關(guān)閉了視圖控制器剖张。 這是因為-您猜對了-您尚未實現(xiàn)適當(dāng)?shù)奈蟹椒▉韺浿频囊曨l保存到媒體庫。


Saving Video

返回RecordVideoViewController.swift揩环,將以下方法添加到UIImagePickerControllerDelegate擴展中:

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

不必擔(dān)心該錯誤-很快就會解決搔弄。

  • 1) 和以前一樣,委托方法為您提供指向視頻的URL丰滑。
  • 2) 驗證該應(yīng)用程序可以將文件保存到設(shè)備的相冊中顾犹。
  • 3) 如果可以,請保存吨枉。

SDK所提供的UISaveVideoAtPathToSavedPhotosAlbum功能可將視頻保存到設(shè)備的相冊中蹦渣。 您向其傳遞要保存的視頻的路徑以及要回調(diào)的目標(biā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: UIAlertAction.Style.cancel,
    handler: nil))
  present(alert, animated: true, completion: nil)
}

回調(diào)(callback)方法僅向用戶顯示alert认臊,并根據(jù)錯誤狀態(tài)宣布是否保存了視頻文件圃庭。

構(gòu)建并運行。 錄制視頻失晴,并在錄制完成后選擇Use Video剧腻。 如果詢問您是否有權(quán)保存到視頻庫,請點擊OK涂屁。 當(dāng)Video was saved彈窗彈出時书在,您就成功將視頻保存到了照片庫!

現(xiàn)在您可以播放視頻和錄制視頻了拆又,該是下一步了儒旬,嘗試一些簡單的視頻編輯了。


Merging Videos

該應(yīng)用程序的最后一項功能是進行一些編輯帖族。您的用戶將從音樂庫中選擇兩個視頻和一首歌曲栈源,然后該應(yīng)用將合并兩個視頻并混入音樂。

該項目已經(jīng)在MergeVideoViewController.swift中實現(xiàn)了入門實施竖般,其代碼類似于您編寫的用于播放視頻的代碼甚垦。最大的區(qū)別是,合并時,用戶必須選擇兩個視頻艰亮。該部分已經(jīng)設(shè)置好闭翩,因此用戶可以進行兩個選擇,這些選擇將存儲在firstAssetsecondAsset中迄埃。

下一步是添加功能以選擇音頻文件疗韵。

1. Selecting the Audio File

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實例,并將其顯示為模式視圖控制器骇钦。

構(gòu)建并運行宛渐。 現(xiàn)在,點擊Merge Video眯搭,然后點擊Load Audio以訪問設(shè)備上的音頻庫窥翩。

當(dāng)然,您的設(shè)備上需要一些音頻文件鳞仙。 否則寇蚊,列表將為空。 這些歌曲還必須實際顯示在設(shè)備上棍好,因此請確保您不嘗試從云中加載歌曲仗岸。

從列表中選擇一首歌曲,您會發(fā)現(xiàn)沒有任何反應(yīng)借笙。 那就對了扒怖! MPMediaPickerController需要委托方法!

要實現(xiàn)它們业稼,請在文件底部找到MPMediaPickerControllerDelegate擴展盗痒,然后向其中添加以下兩個方法:

func mediaPicker(
  _ mediaPicker: MPMediaPickerController,
  didPickMediaItems mediaItemCollection: MPMediaItemCollection
) {
  // 1
  dismiss(animated: true) {
    // 2
    let selectedSongs = mediaItemCollection.items
    guard let song = selectedSongs.first else { return }
    
    // 3
    let title: String
    let message: String
    if let url = song.value(forProperty: MPMediaItemPropertyAssetURL) as? URL {
      self.audioAsset = AVAsset(url: url)
      title = "Asset Loaded"
      message = "Audio Loaded"
    } else {
      self.audioAsset = nil
      title = "Asset Not Available"
      message = "Audio Not Loaded"
    }

    // 4
    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) {
  // 5
  dismiss(animated: true, completion: nil)
}

上面的代碼類似于UIImagePickerController的委托方法。 它的作用是:

  • 1) 像以前一樣dismiss選擇器低散。
  • 2) 找到選定的歌曲俯邓,然后從中找到第一首(如果選擇了多首)。
  • 3) 獲取指向支持歌曲的媒體資源的URL谦纱。 然后使AVAsset指向所選的歌曲看成。
  • 4) 最后,對于mediaPicker(_:didPickMediaItems :)跨嘉,顯示alert以指示資源是否已成功加載川慌。
  • 5) 在取消媒體選擇器的情況下吃嘿,只需關(guān)閉視圖控制器即可。

構(gòu)建并運行梦重,然后轉(zhuǎn)到Merge Videos屏幕兑燥。 選擇一個音頻文件,您將看到Audio Loaded消息琴拧。

現(xiàn)在降瞳,您的所有資源都已正確加載,是時候?qū)⒏鞣N媒體文件合并為一個文件了蚓胸。 但是在進入該代碼之前挣饥,您需要進行一些設(shè)置。

2. Merging Completion Handler

您將很快編寫代碼以合并您的資產(chǎn)沛膳。 這將需要一個完成處理程序扔枫,以將最終視頻保存到相冊中。 您將首先添加它锹安。

MergeVideoViewController.swift文件的頂部添加以下import語句:

import Photos

然后短荐,將以下方法添加到MergeVideoViewController

func exportDidFinish(_ session: AVAssetExportSession) {
  // 1
  activityMonitor.stopAnimating()
  firstAsset = nil
  secondAsset = nil
  audioAsset = nil

  // 2
  guard
    session.status == AVAssetExportSession.Status.completed,
    let outputURL = session.outputURL
    else { return }

  // 3
  let saveVideoToPhotos = {
    // 4
    let changes: () -> Void = {
      PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
    }
    PHPhotoLibrary.shared().performChanges(changes) { saved, error in
      DispatchQueue.main.async {
        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: UIAlertAction.Style.cancel,
          handler: nil))
        self.present(alert, animated: true, completion: nil)
      }
    }
  }
    
  // 5
  if PHPhotoLibrary.authorizationStatus() != .authorized {
    PHPhotoLibrary.requestAuthorization { status in
      if status == .authorized {
        saveVideoToPhotos()
      }
    }
  } else {
    saveVideoToPhotos()
  }
}

該代碼的作用如下:

  • 1) 有一個spinner,用于在處理資產(chǎn)時進行動畫處理叹哭。 這將停止spinner忍宋,然后清除資源并準(zhǔn)備選擇新資源。
  • 2) 確保處理完成风罩,并且有結(jié)果視頻的URL糠排。
  • 3) 創(chuàng)建一個閉包
  • 4) 告訴照片庫從顯示的視頻發(fā)出create request,然后顯示alert以表明成功或失敗超升。
  • 5) 檢查是否有訪問照片庫的權(quán)限乳讥。 如果沒有權(quán)限,則在運行保存視頻的閉包之前要求它廓俭。 否則,只要已授予許可唉工,就立即運行閉包研乒。

現(xiàn)在,您將添加一些代碼到merge(_:)淋硝。 因為有很多代碼雹熬,所以您將分步完成。


Merging: Step 1

在此步驟中谣膳,您將視頻合并為一個長視頻竿报。

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

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

activityMonitor.startAnimating()

// 1
let mixComposition = AVMutableComposition()

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

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

// 5
// TODO: PASTE CODE A

在上面的代碼中:

  • 1) 您創(chuàng)建一個AVMutableComposition來保存您的視頻和音頻軌道。
  • 2) 接下來继谚,為視頻創(chuàng)建一個AVMutableCompositionTrack并將其添加到您的AVMutableComposition中烈菌。
  • 3) 然后,將視頻從第一個視頻資產(chǎn)插入到此軌道。

請注意芽世,insertTimeRange(_:ofTrack:atStartTime :)允許您將視頻的一部分而不是整個部分插入到您的主要合成作品中挚赊。這樣,您可以將視頻修剪到您選擇的時間范圍济瓢。

在這種情況下荠割,您要插入整個視頻,因此創(chuàng)建一個從CMTime.zero到視頻資源持續(xù)時間的時間范圍旺矾。

  • 4) 接下來蔑鹦,您對第二個視頻資產(chǎn)執(zhí)行相同的操作。

請注意箕宙,代碼是如何在時間.zero插入firstAsset的嚎朽,然后在第一個視頻的末尾插入secondAsset的。這是因為本教程假定您要一個接一個地使用視頻資源扒吁,但是您也可以通過播放時間范圍來重疊資產(chǎn)火鼻。

  • 5) // TODO: PASTE CODE A是一個標(biāo)記-您將在下一部分中用代碼替換此行。

在此步驟中雕崩,您將設(shè)置兩個單獨的AVMutableCompositionTrack實例】鳎現(xiàn)在,您需要將AVMutableVideoCompositionLayerInstruction應(yīng)用于每個軌道盼铁,以便進行一些編輯粗蔚。


Merging the Videos: Step 2

接下來是將instructions添加到composition中,以告訴您如何合并資源饶火。

merge(_ :)中的上面的跟蹤代碼之后添加下一部分代碼鹏控。將// TODO:PASTE CODE A替換為以下代碼:

// 6
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRangeMake(
  start: .zero,
  duration: CMTimeAdd(firstAsset.duration, secondAsset.duration))

// 7
let firstInstruction = AVMutableVideoCompositionLayerInstruction(
  assetTrack: firstTrack)
firstInstruction.setOpacity(0.0, at: firstAsset.duration)
let secondInstruction = AVMutableVideoCompositionLayerInstruction(
  assetTrack: secondTrack)

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

// 9                          
// TODO: PASTE CODE B

這是這段代碼中發(fā)生的事情:

  • 6) 首先,設(shè)置mainInstruction來包裝整個instructions肤寝。請注意当辐,此處的總時間是第一個資源的總長與第二個資源的總長之和。
  • 7) 接下來鲤看,您設(shè)置了兩個instructions缘揪,每個資源對應(yīng)一個instructions。第一個視頻的instructions需要額外添加:您將其不透明度最后設(shè)置為0义桂,以便在第二個視頻開始時不可見找筝。
  • 8) 現(xiàn)在,您已經(jīng)有了第一條和第二條軌道的AVMutableVideoCompositionLayerInstruction實例慷吊,您只需將它們添加到mainInstruction中袖裕。接下來,將mainInstruction添加到AVMutableVideoComposition實例的指令屬性溉瓶。您還可以將合成的幀頻設(shè)置為30幀/秒急鳄。
  • 9) // TODO: PASTE CODE B是一個標(biāo)記-您將在下一部分中用代碼替換此行谤民。

好的,現(xiàn)在您已經(jīng)合并了兩個視頻文件攒岛。是時候為他們添加一些聲音了赖临!


Merging the Audio: Step 3

要使片段具有音樂風(fēng)格,請將以下代碼添加到merge(_ :)灾锯。將// TODO:PASTE CODE B替換為以下代碼:

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

// 11
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")

// 12
guard let exporter = AVAssetExportSession(
  asset: mixComposition,
  presetName: AVAssetExportPresetHighestQuality)
  else { return }
exporter.outputURL = url
exporter.outputFileType = AVFileType.mov
exporter.shouldOptimizeForNetworkUse = true
exporter.videoComposition = mainComposition

// 13
exporter.exportAsynchronously {
  DispatchQueue.main.async {
    self.exportDidFinish(exporter)
  }
}

上面的代碼是這樣的:

  • 10) 與視頻軌道相似兢榨,您可以為音頻創(chuàng)建一個新軌道并將其添加到主要composition中。您將音頻時間范圍設(shè)置為第一和第二視頻的時長之和顺饮,因為這將是視頻的完整長度吵聪。
  • 11) 在保存最終視頻之前,需要一個保存文件的路徑兼雄。根據(jù)當(dāng)前日期和時間創(chuàng)建一個唯一的文件名吟逝,該文件名指向documents文件夾中的文件。
  • 12) 渲染并導(dǎo)出合并的視頻赦肋。為此块攒,您將創(chuàng)建一個AVAssetExportSession,它將對合成的內(nèi)容進行轉(zhuǎn)碼以創(chuàng)建由指定的導(dǎo)出預(yù)設(shè)描述的格式輸出佃乘。由于您已經(jīng)配置了AVMutableVideoComposition囱井,因此您只需將其分配給導(dǎo)出器即可。
  • 13) 在使用包含源媒體趣避,導(dǎo)出presetNameoutputFileType的資源初始化導(dǎo)出會話后庞呕,可以通過調(diào)用exportAsynchronously()運行導(dǎo)出。

因為代碼異步執(zhí)行導(dǎo)出程帕,所以此方法立即返回住练。無論導(dǎo)出失敗,完成還是用戶取消愁拭,代碼都會調(diào)用您提供給exportAsynchronously()的完成處理程序柑土。

完成后振亮,導(dǎo)出的狀態(tài)status屬性指示導(dǎo)出是否成功完成芒珠。如果失敗鸥跟,則導(dǎo)出器的error屬性的值將提供有關(guān)失敗原因的其他信息峰搪。

AVComposition組合了來自多個基于文件的來源的媒體數(shù)據(jù)伍掀。在最高層偎快,AVComposition是軌道的集合囊咏,每個軌道都呈現(xiàn)特定類型的媒體株茶,例如音頻或視頻来涨。 AVCompositionTrack的實例表示單個軌道。

同樣启盛,AVMutableCompositionAVMutableCompositionTrack也提供了用于構(gòu)建合成的更高級別的接口蹦掐。這些對象提供了您之前見過的插入技羔,移除和縮放操作,這些操作將再次出現(xiàn)卧抗。

最后藤滥,構(gòu)建并運行。

選擇兩個視頻和一個音頻文件社裆,然后合并選定的文件拙绊。您會看到一條Video Saved消息,表明合成成功泳秀。此時标沪,您的新視頻將出現(xiàn)在相冊中。

轉(zhuǎn)到相冊或使用應(yīng)用程序中的Select and Play Video屏幕瀏覽嗜傅,您可能會注意到合并視頻中的一些方向問題金句。 豎屏視頻可能處于橫向模式,有時視頻會倒置吕嘀。

這是由于默認的AVAsset方向违寞。 使用默認的iPhone相機應(yīng)用程序記錄的所有電影和圖像文件都將視頻幀設(shè)置為橫向,因此iPhone以橫向模式保存媒體偶房。 接下來趁曼,您將解決這些問題。


Orienting Video

AVAsset有一個PreferredTransform蝴悉,其中包含媒體方向信息彰阴。 每當(dāng)您使用Photos應(yīng)用程序或QuickTime查看媒體文件時,它會將其應(yīng)用于媒體文件拍冠。

在上面的代碼中尿这,您尚未對AVAsets進行轉(zhuǎn)換,因此沒有定向問題庆杜。 幸運的是射众,這很容易解決。

但是晃财,在執(zhí)行此操作之前叨橱,需要在VideoHelper.swiftVideoHelper中添加以下幫助器方法:

static func orientationFromTransform(
  _ transform: CGAffineTransform
) -> (orientation: UIImage.Orientation, isPortrait: Bool) {
  var assetOrientation = UIImage.Orientation.up
  var isPortrait = false
  let tfA = transform.a
  let tfB = transform.b
  let tfC = transform.c
  let tfD = transform.d

  if tfA == 0 && tfB == 1.0 && tfC == -1.0 && tfD == 0 {
    assetOrientation = .right
    isPortrait = true
  } else if tfA == 0 && tfB == -1.0 && tfC == 1.0 && tfD == 0 {
    assetOrientation = .left
    isPortrait = true
  } else if tfA == 1.0 && tfB == 0 && tfC == 0 && tfD == 1.0 {
    assetOrientation = .up
  } else if tfA == -1.0 && tfB == 0 && tfC == 0 && tfD == -1.0 {
    assetOrientation = .down
  }
  return (assetOrientation, isPortrait)
}

此代碼分析仿射變換(affine transform),以確定輸入視頻的方向断盛。

接下來罗洗,添加以下import

import AVFoundation

以及該類的另一個helper方法:

static func videoCompositionInstruction(
  _ track: AVCompositionTrack,
  asset: AVAsset
) -> AVMutableVideoCompositionLayerInstruction {
  // 1
  let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)

  // 2
  let assetTrack = asset.tracks(withMediaType: AVMediaType.video)[0]

  // 3
  let transform = assetTrack.preferredTransform
  let assetInfo = orientationFromTransform(transform)

  var scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.width
  if assetInfo.isPortrait {
    // 4
    scaleToFitRatio = UIScreen.main.bounds.width / assetTrack.naturalSize.height
    let scaleFactor = CGAffineTransform(
      scaleX: scaleToFitRatio,
      y: scaleToFitRatio)
    instruction.setTransform(
      assetTrack.preferredTransform.concatenating(scaleFactor),
      at: .zero)
  } else {
    // 5
    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: .zero)
  }

  return instruction
}

該方法獲取一個軌道和一個資源,并返回一個AVMutableVideoCompositionLayerInstruction钢猛,其中包裝了使視頻朝上的必要仿射變換(affine transform)伙菜。這是逐步進行的操作:

  • 1) 您創(chuàng)建AVMutableVideoCompositionLayerInstruction并將其與軌道關(guān)聯(lián)。
  • 2) 接下來命迈,從您的AVAsset創(chuàng)建AVAssetTrack贩绕。 AVAssetTrack為所有資源提供跟蹤級別的檢查接口火的。您需要此對象來訪問資源的尺寸和preferredTransform
  • 3) 然后淑倾,保存首選的變換(preferred transform)和使視頻適合當(dāng)前屏幕所需的縮放比例馏鹤。您將在以下步驟中使用這些值。
  • 4) 如果視頻是縱向視頻娇哆,則需要重新計算比例因子-默認計算是橫向視頻湃累。然后,您要做的就是應(yīng)用方向旋轉(zhuǎn)和比例變換迂尝。
  • 5) 如果視頻是橫向播放脱茉,則可以采用類似的步驟來應(yīng)用縮放和變換。但是垄开,有一項額外的檢查琴许,因為用戶可能會以左橫向或右橫向制作視頻。

由于有兩個橫向溉躲,因此縱橫比將匹配榜田,但視頻可能會旋轉(zhuǎn)180度。對于.down視頻方向的額外檢查可以解決這種情況锻梳。

設(shè)置好helper方法后箭券,在MergeVideoViewController.swift中找到merge(_ :)。找到創(chuàng)建firstInstructionsecondInstruction的位置疑枯,并將它們替換為以下內(nèi)容:

let firstInstruction = VideoHelper.videoCompositionInstruction(
  firstTrack,
  asset: firstAsset)
let secondInstruction = VideoHelper.videoCompositionInstruction(
   secondTrack,
   asset: secondAsset)

上面的更改將使用新的helper函數(shù)并實現(xiàn)所需的旋轉(zhuǎn)修復(fù)程序辩块。

哇-就是這樣!

構(gòu)建并運行荆永。 通過將兩個視頻(以及一個音頻文件)結(jié)合在一起來創(chuàng)建新視頻废亭,當(dāng)您播放視頻時,您會發(fā)現(xiàn)方向問題消失了具钥。

在播放視頻時豆村,AV Foundation為您提供了很大的靈活性。 您還可以應(yīng)用任何類型的CGAffineTransform合并骂删,縮放或定位視頻掌动。

如果您尚未這樣做,請看一下AV Foundation上的WWDC videos宁玫,例如WWDC 2016 session 503, Advances in AVFoundation Playback粗恢。

另外,請務(wù)必查看Apple AVFoundation Framework documentation欧瘪。

后記

本篇主要講述了播放适滓、錄制和合并視頻簡單示例,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市凭迹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苦囱,老刑警劉巖嗅绸,帶你破解...
    沈念sama閱讀 212,332評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撕彤,居然都是意外死亡鱼鸠,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,508評論 3 385
  • 文/潘曉璐 我一進店門羹铅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蚀狰,“玉大人,你說我怎么就攤上這事职员÷樘#” “怎么了?”我有些...
    開封第一講書人閱讀 157,812評論 0 348
  • 文/不壞的土叔 我叫張陵焊切,是天一觀的道長扮授。 經(jīng)常有香客問我,道長专肪,這世上最難降的妖魔是什么刹勃? 我笑而不...
    開封第一講書人閱讀 56,607評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮嚎尤,結(jié)果婚禮上荔仁,老公的妹妹穿的比我還像新娘。我一直安慰自己芽死,他們只是感情好乏梁,可當(dāng)我...
    茶點故事閱讀 65,728評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著收奔,像睡著了一般掌呜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坪哄,一...
    開封第一講書人閱讀 49,919評論 1 290
  • 那天质蕉,我揣著相機與錄音,去河邊找鬼翩肌。 笑死模暗,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的念祭。 我是一名探鬼主播兑宇,決...
    沈念sama閱讀 39,071評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼粱坤!你這毒婦竟也來了隶糕?” 一聲冷哼從身側(cè)響起瓷产,我...
    開封第一講書人閱讀 37,802評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎枚驻,沒想到半個月后濒旦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,256評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡再登,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,576評論 2 327
  • 正文 我和宋清朗相戀三年尔邓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锉矢。...
    茶點故事閱讀 38,712評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡梯嗽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沽损,到底是詐尸還是另有隱情灯节,我是刑警寧澤,帶...
    沈念sama閱讀 34,389評論 4 332
  • 正文 年R本政府宣布缠俺,位于F島的核電站显晶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏壹士。R本人自食惡果不足惜磷雇,卻給世界環(huán)境...
    茶點故事閱讀 40,032評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躏救。 院中可真熱鬧唯笙,春花似錦、人聲如沸盒使。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽少办。三九已至苞慢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間英妓,已是汗流浹背挽放。 一陣腳步聲響...
    開封第一講書人閱讀 32,026評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蔓纠,地道東北人辑畦。 一個月前我還...
    沈念sama閱讀 46,473評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像腿倚,于是被迫代替她去往敵國和親纯出。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,606評論 2 350