我正在尝试从照片应用程序中修剪视频。下面是我的代码。错误似乎出在
exportSession.exportAsynchronously
,我无法理解为什么
import UIKit
import AVFoundation
import AVKit
import Photos
import PhotosUI
class ViewController: UIViewController, PHPickerViewControllerDelegate {
func loadVideo(url: URL) -> AVAsset? {
return AVAsset(url: url)
}
func trimVideo(asset: AVAsset, startTime: CMTime, endTime: CMTime, completion: @escaping (URL?, Error?) -> Void) {
// Create an export session with the desired output URL and preset.
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
completion(nil, NSError(domain: "com.yourapp.trimVideo", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create AVAssetExportSession."]))
return
}
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let outputURL = documentsDirectory.appendingPathComponent("trimmedVideo.mp4")
// Remove the existing file if it exists
try? FileManager.default.removeItem(at: outputURL)
exportSession.outputURL = outputURL
exportSession.outputFileType = AVFileType.mp4
exportSession.shouldOptimizeForNetworkUse = true
// Set the time range for trimming
let timeRange = CMTimeRangeFromTimeToTime(start: startTime, end: endTime)
exportSession.timeRange = timeRange
// Perform the export
exportSession.exportAsynchronously {
switch exportSession.status {
case .completed:
completion(exportSession.outputURL, nil)
case .failed:
completion(nil, exportSession.error)
case .cancelled:
completion(nil, NSError(domain: "com.yourapp.trimVideo", code: 2, userInfo: [NSLocalizedDescriptionKey: "Export cancelled"]))
default:
break
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
debugPrint("1")
picker.dismiss(animated: true,completion: nil)
debugPrint("2")
guard let provider = results.first?.itemProvider else { return }
debugPrint("3")
if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
debugPrint("4")
provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [self] (videoURL, error) in
debugPrint("5")
cropVideo(sourceURL1: videoURL as! URL, statTime: 10, endTime: 20)
}
}
}
@IBAction func btnClicked(_ sender: Any) {
var config = PHPickerConfiguration(photoLibrary: .shared())
config.selectionLimit = 1
config.filter = .videos
let vc = PHPickerViewController(configuration: config)
vc.delegate = self
present(vc, animated: true)
}
func cropVideo(sourceURL1: URL, statTime:Float, endTime:Float)
{
debugPrint("21")
let manager = FileManager.default
debugPrint("22")
guard let documentDirectory = try? manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {return}
debugPrint("23")
let mediaType = "mp4"
debugPrint("24")
if mediaType == UTType.movie.identifier || mediaType == "mp4" as String {
debugPrint("25")
let asset = AVAsset(url: sourceURL1 as URL)
let length = Float(asset.duration.value) / Float(asset.duration.timescale)
print("video length: \(length) seconds")
debugPrint("26")
let start = statTime
let end = endTime
debugPrint("27")
var outputURL = documentDirectory.appendingPathComponent("output")
do {
debugPrint("28")
try manager.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil)
outputURL = outputURL.appendingPathComponent("\(UUID().uuidString).\(mediaType)")
}catch let error {
debugPrint("29")
print(error)
}
//Remove existing file
_ = try? manager.removeItem(at: outputURL)
debugPrint("30")
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {return}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
debugPrint("31")
let startTime = CMTime(seconds: Double(start ), preferredTimescale: 1000)
let endTime = CMTime(seconds: Double(end ), preferredTimescale: 1000)
let timeRange = CMTimeRange(start: startTime, end: endTime)
debugPrint("32")
exportSession.timeRange = timeRange
exportSession.exportAsynchronously{
switch exportSession.status {
case .completed:
print("exported at \(outputURL)")
case .failed:
print("failed \(String(describing: exportSession.error))")
case .cancelled:
print("cancelled \(String(describing: exportSession.error))")
default: break
}
}
}
}
}
以下是整个错误
Optional(Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSUnderlyingError=0x2818544b0 {Error Domain=NSOSStatusErrorDomain Code=-16979 "(null)"}, NSLocalizedFailureReason=An unknown error occurred (-16979), NSURL=file:///private/var/mobile/Containers/Shared/AppGroup/35D28117-572C-484E-969C-A9515EF42CDF/File%20Provider%20Storage/photospicker/version=1&uuid=6C291C6C-F254-44F5-83C2-C15980750530&mode=compatible&noloc=0.mp4, NSLocalizedDescription=The operation could not be completed})
我可以确认您遇到的
-16979
错误通常与 iOS 中的沙箱导致的文件访问问题有关。
这里的主要问题是,当您从
PHPickerViewController
获取文件 URL 时,您会收到一个安全范围的 URL。为了访问 URL 后面的文件,您需要首先使用 URL 上的 startAccessingSecurityScopedResource()
方法请求许可。如果此方法返回 true
,则意味着您已被授予访问该资源的权限。
您的代码应如下所示:
provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [self] (url, error) in
debugPrint("5")
guard let url = url else {
print("Could not get video URL: \(error?.localizedDescription ?? "Unknown error")")
return
}
let canAccessResource = url.startAccessingSecurityScopedResource()
if canAccessResource {
defer { url.stopAccessingSecurityScopedResource() }
cropVideo(sourceURL1: url, statTime: 10, endTime: 20)
} else {
print("Could not access security scoped resource")
}
}
其他一些改进和注意事项:
切勿强制强制转换 (
as!
) 可选值,因为如果可选值是 nil
,这可能会导致运行时崩溃。相反,请正确处理 nil
的情况。在您的代码中, (videoURL as! URL)
是有风险的。通过使用 guard let url = url else {...}
,我们可以确保在继续之前拥有有效的 URL。
使用完安全范围的 URL 后,请务必调用
stopAccessingSecurityScopedResource()
。正确管理这些 URL 以避免文件访问和内存方面的潜在问题非常重要。
始终检查
startAccessingSecurityScopedResource()
的结果。如果是 false
,则意味着您无权访问该资源,您应该在代码中适当处理这种情况。
考虑到这些修改和要点,您的代码现在应该可以正确处理文件访问,并有望解决您遇到的问题。如果您仍然看到相同的错误,请分享更多详细信息,我很乐意为您提供帮助。