В моем приложении для записи видео я пытаюсь реализовать функцию слияния видео, которая принимает массив URL-адресов, которые были недавно записаны в приложении.
extension AVMutableComposition {
func mergeVideo(_ urls: [URL], completion: @escaping (_ url: URL?, _ error: Error?) -> Void) {
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion(nil, nil)
return
}
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
let date = dateFormatter.string(from: Date())
let outputURL = documentDirectory.appendingPathComponent("mergedVideo_\(date).mp4")
// If there is only one video, we dont to touch it to save export time.
if let url = urls.first, urls.count == 1 {
do {
try FileManager().copyItem(at: url, to: outputURL)
completion(outputURL, nil)
} catch let error {
completion(nil, error)
}
return
}
let maxRenderSize = CGSize(width: 1280.0, height: 720.0)
var currentTime = CMTime.zero
var renderSize = CGSize.zero
// Create empty Layer Instructions, that we will be passing to Video Composition and finally to Exporter.
var instructions = [AVMutableVideoCompositionInstruction]()
urls.enumerated().forEach { index, url in
let asset = AVAsset(url: url)
print(asset)
let assetTrack = asset.tracks.first!
// Create instruction for a video and append it to array.
let instruction = AVMutableComposition.instruction(assetTrack, asset: asset, time: currentTime, duration: assetTrack.timeRange.duration, maxRenderSize: maxRenderSize)
instructions.append(instruction.videoCompositionInstruction)
// Set render size (orientation) according first videro.
if index == 0 {
renderSize = instruction.isPortrait ? CGSize(width: maxRenderSize.height, height: maxRenderSize.width) : CGSize(width: maxRenderSize.width, height: maxRenderSize.height)
}
do {
let timeRange = CMTimeRangeMake(start: .zero, duration: assetTrack.timeRange.duration)
// Insert video to Mutable Composition at right time.
try insertTimeRange(timeRange, of: asset, at: currentTime)
currentTime = CMTimeAdd(currentTime, assetTrack.timeRange.duration)
} catch let error {
completion(nil, error)
}
}
// Create Video Composition and pass Layer Instructions to it.
let videoComposition = AVMutableVideoComposition()
videoComposition.instructions = instructions
// Do not forget to set frame duration and render size. It will crash if you dont.
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
videoComposition.renderSize = renderSize
guard let exporter = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetHighestQuality) else {
completion(nil, nil)
return
}
exporter.outputURL = outputURL
exporter.outputFileType = .mp4
// Pass Video Composition to the Exporter.
exporter.videoComposition = videoComposition
exporter.shouldOptimizeForNetworkUse = true
exporter.exportAsynchronously {
DispatchQueue.main.async {
completion(exporter.outputURL, nil)
}
}
}
static func instruction(_ assetTrack: AVAssetTrack, asset: AVAsset, time: CMTime, duration: CMTime, maxRenderSize: CGSize)
-> (videoCompositionInstruction: AVMutableVideoCompositionInstruction, isPortrait: Bool) {
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: assetTrack)
// Find out orientation from preferred transform.
let assetInfo = orientationFromTransform(assetTrack.preferredTransform)
// Calculate scale ratio according orientation.
var scaleRatio = maxRenderSize.width / assetTrack.naturalSize.width
if assetInfo.isPortrait {
scaleRatio = maxRenderSize.height / assetTrack.naturalSize.height
}
// Set correct transform.
var transform = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio)
transform = assetTrack.preferredTransform.concatenating(transform)
layerInstruction.setTransform(transform, at: .zero)
// Create Composition Instruction and pass Layer Instruction to it.
let videoCompositionInstruction = AVMutableVideoCompositionInstruction()
videoCompositionInstruction.timeRange = CMTimeRangeMake(start: time, duration: duration)
videoCompositionInstruction.layerInstructions = [layerInstruction]
return (videoCompositionInstruction, assetInfo.isPortrait)
}
static func orientationFromTransform(_ transform: CGAffineTransform) -> (orientation: UIImage.Orientation, isPortrait: Bool) {
var assetOrientation = UIImage.Orientation.up
var isPortrait = false
switch [transform.a, transform.b, transform.c, transform.d] {
case [0.0, 1.0, -1.0, 0.0]:
assetOrientation = .right
isPortrait = true
case [0.0, -1.0, 1.0, 0.0]:
assetOrientation = .left
isPortrait = true
case [1.0, 0.0, 0.0, 1.0]:
assetOrientation = .up
case [-1.0, 0.0, 0.0, -1.0]:
assetOrientation = .down
default:
break
}
return (assetOrientation, isPortrait)
}
}
Однако, когда я пытаюсь вызвать функцию mergeVideo, я получаю ошибку при попытке развернуть необязательное найденное значение nil. Например, в моем журнале я печатаю значения в массиве URLS. У меня может быть массив, который выглядит следующим образом [file:///private/var/mobile/Containers/Data/Application/FD6FFB6E-36A8-49A9-8892-15BEAC0BA817/tmp/846F105D-A2F7-4F0A-A512-643B0407B962.mp4]
, но получаю сообщение об ошибке The file “846F105D-A2F7-4F0A-A512-643B0407B962.mp4” couldn’t be opened because there is no such file.
Какие-либо предложения о том, как правильно получить доступ к этим недавно сохраненным файлам или какие-либо предложения?