问题
EDIT: To make things easier to anyone interested in checking out this problem, I added a demo project to this github repository.
Issue
I've seen several questions that had the same error, but none of the solutions found there helped me. Figured I'd try my luck.
I'm trying to export a video as is, mainly to learn about AVFoundation and AVAssetExportSession. My export works perfectly fine on the simulator, but does not work on any iOS device I've tried (namely an iPhone X and an iPhone XR running iOS 12 each). I Mainly followed a Ray Wenderleich tutorial found on this link to perform the Video Exporting: https://www.raywenderlich.com/2734-avfoundation-tutorial-adding-overlays-and-animations-to-videos
Would appreciate any help on the topic. My code is as follows:
Retrieving the URL of a video I've added to the App Bundle called Demo.mp4:
@objc func export() {
let urlString = Bundle.main.path(forResource: "Demo", ofType: ".mp4")!
let url = URL(fileURLWithPath: urlString)
ExportManager.shared.exportWithAVFoundation(url:url) { (outputUrl, errorString) in
if let outputUrl = outputUrl {
self.playVideo(url: outputUrl)
} else if let errorString = errorString {
print("ERROR: \(errorString)")
}
}
}
My Exporting function in ExportManager is as follows (Sorry quite long)
func exportWithAVFoundation(url: URL, completion: @escaping (_ outputUrl: URL?, _ errorString: String?) -> ()) {
let asset = AVAsset(url: url)
print("URL IS \(url)")
guard let avAssetTrack = asset.tracks(withMediaType: .video).first else {
completion(nil, "Couldn't Create Asset Track")
return
}
let mutableComposition = AVMutableComposition()
guard let videoTrack = mutableComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else { return }
try? videoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: avAssetTrack, at: .zero)
videoTrack.preferredTransform = avAssetTrack.preferredTransform
if let audioAssetTrack = asset.tracks(withMediaType: .audio).first {
let audioTrack = mutableComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
try? audioTrack?.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: audioAssetTrack, at: .zero)
}
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
let videoLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
// Fix video orientation
var videoAssetOrientation = UIImage.Orientation.up
var isVideoAssetPortrait = false
let videoTransform = avAssetTrack.preferredTransform
switch (videoTransform.a, videoTransform.b, videoTransform.c, videoTransform.c) {
case (0, 1.0, -1.0, 0):
videoAssetOrientation = .right
isVideoAssetPortrait = true
case(0, -1.0, 1.0, 0):
videoAssetOrientation = .left
isVideoAssetPortrait = true
case(1.0, 0, 0, 1.0):
videoAssetOrientation = .up
case(-1.0, 0, 0, -1.0):
videoAssetOrientation = .down
default:
break
}
var naturalSize = avAssetTrack.naturalSize
switch (videoAssetOrientation, isVideoAssetPortrait) {
case (.right, true):
naturalSize = CGSize(width: avAssetTrack.naturalSize.height, height: avAssetTrack.naturalSize.width)
case (.left, true):
naturalSize = CGSize(width: avAssetTrack.naturalSize.height, height: avAssetTrack.naturalSize.width)
case (.leftMirrored, true):
naturalSize = CGSize(width: avAssetTrack.naturalSize.height, height: avAssetTrack.naturalSize.width)
case (.rightMirrored, true):
naturalSize = CGSize(width: avAssetTrack.naturalSize.height, height: avAssetTrack.naturalSize.width)
default:
break
}
videoLayerInstruction.setTransform(avAssetTrack.preferredTransform, at: .zero)
videoLayerInstruction.setOpacity(0, at: asset.duration)
mainInstruction.layerInstructions = [videoLayerInstruction]
let mainCompositionInstruction = AVMutableVideoComposition()
mainCompositionInstruction.renderSize = naturalSize
mainCompositionInstruction.instructions = [mainInstruction]
mainCompositionInstruction.frameDuration = CMTimeMake(value: 1, timescale: 30);
let documentsDirectoryURL = createPath()
guard let exporter = AVAssetExportSession(asset: mutableComposition, presetName: AVAssetExportPresetHighestQuality) else {
print("Couldnt create AVAssetExportSession")
completion(nil, "Couldn't Create AVAssetExportSession")
return
}
exporter.outputURL = documentsDirectoryURL
exporter.outputFileType = .mov
exporter.shouldOptimizeForNetworkUse = true
exporter.videoComposition = mainCompositionInstruction
exporter.exportAsynchronously {
if let error = exporter.error {
print(error)
completion(nil, error.localizedDescription)
return
}
completion(exporter.outputURL, nil)
print("Finished Exporting")
}
}
Some things I've tried to do have been to add an AudioTrack to the composition (which I didn't include previously). Hasn't helped it work on an actual device, but at least my exported video has audio now.
I also tried to change the Preset Quality to Passthrough as opposed to Highest Quality, as I read from other threads that this could help, but to no avail.
EDIT:
Added createPath function:
func createPath() -> URL {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentDirectory = paths.first!
let myPathDocs = documentDirectory.appending("FinalVideo.mov")
let url = URL(fileURLWithPath: myPathDocs)
if FileManager.default.fileExists(atPath: myPathDocs) {
try? FileManager.default.removeItem(atPath: myPathDocs)
}
return url
}
Note: createPath() just creates a valid path in the directory to save the exported video in. If a file exists at that path prior to exporting, it gets deleted.
回答1:
The problem is that you're appending string to another string resulting in wrong path to the file like file:///var/mobile/Containers/Data/Application/<stripped>/DocumentsFinalVideo.mov
You should use appendingPathComponent() instead:
func createPath() -> URL {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentDirectory = URL(fileURLWithPath: paths.first!)
let url = documentDirectory.appendingPathComponent("FinalVideo.mov")
if FileManager.default.fileExists(atPath: url.path) {
try? FileManager.default.removeItem(at: url)
}
return url
}
回答2:
You must use, default method: appendingPathComponent()
来源:https://stackoverflow.com/questions/57185068/avassetexportsession-not-working-on-devices-but-working-on-simulator-avfoundat