Play, record và merge video trong iOS
Introduction
Trong bài viết này, chúng ta sẽ tìm hiểu về các thao tác cơ bản với video trong iOS sử dụng framework AVFoundation. Bao gồm play, record và thậm chí một chút edit video. AVFoundation là một framework cơ bản, là một phần của macOS từ OS X Lion (10.7) và iOS 4 từ năm 2010.
Đến nay AVFoundation ngày càng được hoàn thiện với hơn 100 class hỗ trợ việc xử lý audio và video. Bài viết này sẽ tập trung vào việc sử dụng các class của framework này để:
- Select và play video từ media library.
- Record và save video vào media library.
- Merge hai video thành một video hoàn chỉnh, có thể thêm audio soundtrack.
Để thực hành tutorial này, chúng ta sẽ cần một device thật, chứ không thể dùng simulator. Đơn giản là vì chúng ta không thể dùng simulator để mở camera và record video. Và để run app trên device thật, chúng ta lại cần một tài khoản developer Apple, tài khoản free là đủ.
Getting Started
Mở Xcode lên, tạo project Swift mới với tên tùy ý.
Trong Main.storyboard, embed default view controller vào một UINavigationController.
Tạo mới 3 UIButton với action show segue tới 3 view controller mới tương ứng với:
- Select and play video (PlayVideoViewController.swift).
- Record and save video (RecordVideoViewController.swift).
- Merge video (MergeVideoViewController.swift).
Build và run app trên device thật, ta được giao diện đơn giản như sau:
Select and play video
Button Select and play video có action khi tap vào sẽ segue đến PlayVideoViewController
. Trong phần này, chúng ta sẽ viết code để select và play một video từ media library.
Bắt đầu bằng việc mở file PlayVideoViewController.swift và thêm các import sau vào đầu file:
import AVKit
import MobileCoreServices
Import AVKit cho phép chúng ta sử dụng class AVPlayer
hỗ trợ việc select và play video. Còn MobileCoreServices
để sử dụng các constant được define trước như kUTTypeMovie
- constant để chỉ định media type dạng video khi select từ media library.
Tiếp theo, thêm các extension sau và conform hai protocol UIImagePickerControllerDelegate
và UINavigationControllerDelegate
. Chúng ta sẽ sử dụng UIImagePickerViewController
mặc định của iOS để browse video từ media library. Mặc dù tên class này là ImagePicker nhưng nó cũng cho phép pick cả video. Sau khi select xong, class này sẽ call lại các method của delegate protocol. Từ đó chúng ta có thể handle video hoặc ảnh vừa được chọn một cách tùy ý.
// MARK: - UIImagePickerControllerDelegate methods
extension PlayVideoViewController: UIImagePickerControllerDelegate {
}
// MARK: - UINavigationControllerDelegate methods
extension PlayVideoViewController: UINavigationControllerDelegate {
}
Từ Main.storyboard, thêm mới một button Select video vào PlayVideoViewController và tạo @IBAction func selectVideoButtonTapped(_ sender: Any)
tương ứng cho nó ở view controller.
Trong function này, thêm đoạn code sau:
// 1.
guard UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) else {
print("Media library is not available.")
return
}
// 2.
let imagePicker = UIImagePickerController()
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.mediaTypes = [kUTTypeMovie as String]
imagePicker.allowsEditing = true
imagePicker.delegate = self
// 3.
present(imagePicker, animated: true, completion: nil)
Khi tap vào button Select video, chúng ta sẽ present màn hình browse đến media library. Cụ thể:
- Check xem source type
.savedPhotosAlbum
(media library) có available trên device hay không. Ngoài media library còn có source photo từ camera của device. - Nếu source type đó available, tạo một object của class
UIImagePickerController
và set các property cần thiết như delegate, sourceType, mediaTypes... - Cuối cùng, present modally image picker.
Build và run app, tap button Select and play video ở màn hình đầu tiên rồi tap button Select video, chúng ta sẽ thấy giao diện select video từ media library như sau:
Bởi vì ở trên chúng ta đã set property mediaTypes
bằng kUTTypeMovie
nên khi open media library đã tự động filter chỉ hiển thị các video. Khi chọn một video, hệ thống sẽ tự động hiển thị một màn hình video detail với các button canncel, play và choose. Tap button play sẽ play video. Tuy nhiên nếu tap button choose thì sẽ quay lại màn hình PlayVideoViewController mà không có bất kỳ action xảy ra. Sở dĩ như vậy vì chúng ta chưa implement bất kỳ method delegate nào để handle video được chọn từ picker.
Quay trở lại Xcode, trong extension UIImagePickerControllerDelegate
, implement thêm method delegate sau:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
// 1.
guard let mediaType = info[.mediaType] as? String,
mediaType == (kUTTypeMovie as String),
let url = info[.mediaURL] as? URL else {
return
}
// 2.
dismiss(animated: true) {
// 3.
let player = AVPlayer(url: url)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true, completion: nil)
}
}
- Check media type của media vừa được select và unwrap lấy giá trị url của media đó.
- Dismiss image picker.
- Trong completion block, tạo mới object
AVPlayerViewController
vàAVPlayer
để play video rồi present lên.
Build và run project, chọn một video, chúng ta sẽ play được video đó:
Record and save video
Ở phần này, chúng ta sẽ implement phần record video và save nó xuống media library.
Mở file RecordVideoViewController.swift, cũng cần import MobileCoreServices.
import MobileCoreServices
Implement tương tự phần play video, chúng ta thêm các extension.
// MARK: - UINavigationControllerDelegate methods
extension RecordVideoViewController: UIImagePickerControllerDelegate {
}
// MARK: - UINavigationControllerDelegate methods
extension RecordVideoViewController: UINavigationControllerDelegate {
}
Và thêm button Record video và funtion handle button tương ứng:
@IBAction private func recordVideoButtonTapped(_ sender: Any) {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
print("Camera is not available.")
return
}
let imagePicker = UIImagePickerController()
imagePicker.sourceType = .camera
imagePicker.mediaTypes = [kUTTypeMovie as String]
imagePicker.allowsEditing = true
imagePicker.delegate = self
present(imagePicker, animated: true, completion: nil)
}
Ở trên chúng ta đã request mở camera của device để quay video nên để tránh crash app khi tap vào button Record video, chúng ta cần thêm các key, value sau vào Info.plist để mô tả việc xin quyền truy cập camera và microphone.
<key>NSMicrophoneUsageDescription</key>
<string>Allow microphone access description</string>
<key>NSCameraUsageDescription</key>
<string>Allow camera access description</string>
Build và run app xem chúng ta có gì:
Chuyển đến màn hình RecordVideoViewController và tap vào button Record video. Thay vì hiển thị màn hình Photo Gallery, giao diện camera hiện lên. Lúc này system dialog hiện lên hỏi xin user quyền cho phép sử dụng camera và microphone, tap OK. Sau khi record video có thể retake video hoặc tap button Use Video để select video vừa quay. Giao diện camera sẽ dismiss nhưng không có action gì xảy ra cả, cũng giống như trên, vì chúng ta chưa implement method delegate để handle.
Trong extension UIImagePickerControllerDelegate
thêm method sau:
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),
let url = info[UIImagePickerController.InfoKey.mediaURL] as? URL,
UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(url.path) else {
return
}
UISaveVideoAtPathToSavedPhotosAlbum(url.path, self, #selector(video(_:didFinishSavingWithError:contextInfo:)), nil)
}
Cũng giống phần trước, method này trả về URL trỏ đến địa chỉ của video vừa được record, việc của chúng ta chỉ là verify các thông tin hợp lệ và check xem có thể save video đó xuống media library được hay không.
UISaveVideoAtPathToSavedPhotosAlbum
là funtion cung cấp bởi iOS SDK UIKit để save video xuống Photos Album. Chúng ta cần truyền vào function này các param như videoPath, target, completionSelector và contextInfo.
Tiếp theo, chúng ta implement target action completionSelector
sau khi save video complete.
@objc private 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 alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
present(alertController, animated: true, completion: nil)
}
Một việc nữa cần phải làm, đó lại thêm key NSPhotoLibraryAddUsageDescription vào Info.plist. Build và run app. Nếu save thành công sẽ hiện ra alert Video was saved.
Merge video
Việc đầu tiên để merge video đó là phải select hai video từ media library. Sử dụng lại code select video từ phần Select and play video viết thành method return Bool như sau:
private func loadVideo() -> Bool {
guard UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) else {
print("Media library is not available.")
return false
}
let imagePicker = UIImagePickerController()
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.mediaTypes = [kUTTypeMovie as String]
imagePicker.allowsEditing = true
imagePicker.delegate = self
present(imagePicker, animated: true, completion: nil)
return true
}
Tiếp theo, ở Main.storyboard, thêm 4 button vào MergeVideoViewController.
Và 4 action method tương ứng. Sử dụng hai flag Bool loadingFirstVideo
và loadingSecondVideo
để phân biệt việc load video thứ nhất và thứ hai. Và các biến lưu video, audio asset sau khi select.
private var loadingFirstVideo = false
private var loadingSecondVideo = false
private var firstAsset: AVAsset?
private var secondAsset: AVAsset?
private var audioAsset: AVAsset?
@IBAction private func loadFirstVideoButtonTapped(_ sender: Any) {
if loadVideo() {
loadingFirstVideo = true
}
}
@IBAction private func loadSecondVideoButtonTapped(_ sender: Any) {
if loadVideo() {
loadingSecondVideo = false
}
}
@IBAction private func loadAudioButtonTapped(_ sender: Any) {
}
@IBAction private func mergeVideoButtonTapped(_ sender: Any) {
}
Method handle select video.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
dismiss(animated: true, completion: nil)
guard let mediaType = info[.mediaType] as? String,
mediaType == (kUTTypeMovie as String),
let url = info[.mediaURL] as? URL else {
return
}
let asset = AVAsset(url: url)
var message = ""
if loadingFirstVideo {
message = "First loaded"
firstAsset = asset
loadingFirstVideo = false
} else if loadingSecondVideo {
message = "Video two loaded"
secondAsset = asset
loadingFirstVideo = false
} else {
message = "No video loaded"
}
let alertController = UIAlertController(title: "Asset Loaded", message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
present(alertController, animated: true, completion: nil)
}
Để load audio, import thêm MediaPlayer, trong method loadAudioButtonTapped(_ sender: Any)
, implement đoạn code sau:
let mediaPickerController = MPMediaPickerController(mediaTypes: .any)
mediaPickerController.delegate = self
mediaPickerController.prompt = "Select Audio"
present(mediaPickerController, animated: true, completion: nil)
Để xin quyền truy cập kho audio của device, chúng ta cũng cần thêm key, value cho NSAppleMusicUsageDescription
.
<key>NSAppleMusicUsageDescription</key>
<string>Allow Apple Music access description</string>
Và MergeVideoViewController cũng cần comform delegate MPMediaPickerControllerDelegate
.
// MARK: - MPMediaPickerControllerDelegate methods
extension MergeVideoViewController: MPMediaPickerControllerDelegate {
}
Khi tap vào button Load audio, giao diện select audio của Apple Music hiện lên.
Trong extension MPMediaPickerControllerDelegate
, override method để handle audio sau khi pick.
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
if let assetURL = url {
self.audioAsset = AVAsset(url: assetURL)
}
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)
}
Trong method trên, chúng ta lấy ra audio đầu tiên chọn được, check URL và lưu vào biến audioAsset
và hiển thị message alert tương ứng khi load thành công hoặc fail.
Export and merge
Để export video đã merge, import Photos và thêm method sau vào MergeVideoViewController.swift:
private func exportDidFinish(_ session: AVAssetExportSession) {
// Cleanup assets
firstAsset = nil
secondAsset = nil
audioAsset = nil
guard session.status == .completed,
let outputURL = session.outputURL else {
return
}
// Closure to save video to Media Library
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 alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
}
// Ensure permission to access Media Library
if PHPhotoLibrary.authorizationStatus() != .authorized {
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
saveVideoToPhotos()
}
}
} else {
saveVideoToPhotos()
}
}
Hoàn thiện method mergeVideoButtonTapped(_ sender: Any)
:
@IBAction private func mergeVideoButtonTapped(_ sender: Any) {
guard let firstAsset = firstAsset,
let secondAsset = secondAsset else {
return
}
// 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: .video,
preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else {
return
}
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
}
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
}
// 3 - Audio track
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: AVMediaType.audio)[0] ,
at: .zero)
} 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)
}
}
}
Run app và select hai video cùng với một audio tùy ý, tap button Merge and save, alert message Video saved hiện lên. Có thể dùng ngay chức năng play video ở trên để play video vừa được merge.
Tutorial này còn một số vấn đề về video orientation và xử lý các khoảng đen còn thừa xong video. Các bạn có thể tìm hiểu thêm ở bài viết gốc:
All Rights Reserved