移除视频GPS元数据时触发AVFoundationErrorDomain Code=-11823错误求助
解决AVFoundation保存视频时的"Cannot Save"(Code=-11823)错误
错误详情
Cannot Save
Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save" UserInfo={NSLocalizedRecoverySuggestion=The requested file name is already in use. Try a different file name or location., NSLocalizedDescription=Cannot Save, NSUnderlyingError=0x600000d29860 {Error Domain=NSOSStatusErrorDomain Code=-12101 "(null)"}}
场景说明
实现了一个视频处理类,用于移除视频中的GPS位置元数据并将修改后的视频保存到相册,核心逻辑是先移除元数据再保存到相册。
相关配置(Info.plist)
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
测试调用代码
VideoHandler1.shared.removeMetaData(filePath: videoPath)
问题根因分析
从错误信息和代码来看,导致问题的几个关键原因:
- 重复生成输出URL:
removeMetadataAndGPSInfo中连续两次调用saveVideoToDocumentDirectory,每次调用都会清空整个文档目录并生成新文件名,导致导出会话使用的URL与后续回调传递的URL不一致,引发文件冲突。 - 全局清空文档目录:
saveVideoToDocumentDirectory中每次都会清空整个文档目录,既危险(误删其他文件)又会导致多次调用时文件被意外删除。 - 元数据过滤逻辑不安全:使用强制类型转换
$0.key as! AVMetadataKey,若元数据键类型不匹配会直接崩溃,且未完全过滤所有GPS相关元数据。 - 权限处理不严谨:
saveVideoToPhotoLibrary中未判断权限授权状态,直接执行保存操作,可能在未授权时失败。 - URL转换潜在问题:将
absoluteString转换为URL时,若字符串无效会导致保存失败,且未做错误处理。
修复方案
针对上述问题,逐一修正代码:
1. 复用输出URL
在removeMetadataAndGPSInfo中只调用一次生成输出URL的方法,确保导出会话和回调使用同一个URL。
2. 移除全局清空操作
仅删除当前导出要使用的目标文件,而非整个文档目录,避免误删其他文件。
3. 安全过滤GPS元数据
使用可选绑定处理元数据键,同时覆盖更多GPS相关元数据标签,避免遗漏。
4. 完善权限判断
在保存到相册前检查权限状态,仅在授权通过后执行操作。
5. 优化URL转换
使用URL(fileURLWithPath:)处理本地文件路径,比URL(string:)更可靠。
修正后的完整代码
public typealias StringHandler = (String) -> Void public class VideoHandler1: NSObject { public static let shared = VideoHandler1() var inputPath = "" var outputPath = "" public func removeMetaData(filePath: String) { inputPath = filePath guard let fileURL = URL(fileURLWithPath: filePath) else { print("无效的文件路径") return } removeMetadataAndGPSInfo(from: fileURL) { [weak self] modifiedVideoPath in guard let self = self, !modifiedVideoPath.isEmpty, let modifiedVideoURL = URL(fileURLWithPath: modifiedVideoPath) else { print("无法获取修改后的视频URL") return } self.saveVideoToPhotoLibrary(videoURL: modifiedVideoURL) } } } extension VideoHandler1 { func removeMetadataAndGPSInfo(from videoURL: URL, handler: StringHandler?) { let asset = AVAsset(url: videoURL) guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else { print("创建导出会话失败") handler?("") return } guard let outputURL = generateUniqueOutputURL(for: videoURL) else { print("生成输出URL失败") handler?("") return } exportSession.outputURL = outputURL exportSession.outputFileType = .mp4 // 安全过滤GPS相关元数据 exportSession.metadata = asset.metadata.filter { item in guard let key = item.key as? AVMetadataKey else { return true } return key != .commonKeyLocation && key != .quickTimeMetadataKeyGPSCoordinates && key != .quickTimeMetadataKeyGPSAltitude } exportSession.exportAsynchronously(completionHandler: { [weak self] in defer { // 导出完成后自动删除临时文件 self?.deleteFileFromDocumentDirectory(videoURL: outputURL) } if exportSession.status == .completed { print("元数据和GPS信息移除成功") handler?(outputURL.path) } else if let error = exportSession.error { print("导出失败:\(error.localizedDescription)") handler?("") } }) } func saveVideoToPhotoLibrary(videoURL: URL) { if #available(iOS 14, *) { PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in guard status == .authorized else { print("相册访问未授权") return } PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL) }, completionHandler: { [weak self] success, error in if success { self?.outputPath = videoURL.path print("视频保存到相册成功") } else if let error = error { print("保存视频到相册失败:\(error.localizedDescription)") } }) } } else { PHPhotoLibrary.requestAuthorization { status in guard status == .authorized else { print("相册访问未授权") return } PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL) }, completionHandler: { [weak self] success, error in if success { self?.outputPath = videoURL.path print("视频保存到相册成功") } else if let error = error { print("保存视频到相册失败:\(error.localizedDescription)") } }) } } } func generateUniqueOutputURL(for videoURL: URL) -> URL? { let fileManager = FileManager.default guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { print("未找到文档目录") return nil } // 生成唯一文件名(时间戳+随机字符串) let timestamp = Date().timeIntervalSince1970 let randomString = generateRandomString() let fileName = "\(Int(timestamp))_\(randomString).\(videoURL.pathExtension)" let destinationURL = documentDirectory.appendingPathComponent(fileName) // 如果文件已存在,删除后再使用 do { if fileManager.fileExists(atPath: destinationURL.path) { try fileManager.removeItem(at: destinationURL) } return destinationURL } catch { print("准备输出URL失败:\(error.localizedDescription)") return nil } } func generateRandomString() -> String { let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" return String((0..<10).map { _ in characters.randomElement()! }) } func deleteFileFromDocumentDirectory(videoURL: URL) { let fileManager = FileManager.default guard fileManager.fileExists(atPath: videoURL.path) else { return } do { try fileManager.removeItem(at: videoURL) print("临时文件已删除:\(videoURL)") } catch { print("删除临时文件失败:\(error.localizedDescription)") } } }
额外优化点
- 新增临时文件自动删除逻辑,避免文档目录积累无用文件;
- 完善错误打印信息,便于调试;
- 兼容iOS 14以下的权限请求逻辑;
- 增强元数据过滤覆盖范围,确保彻底移除GPS相关信息。
内容的提问来源于stack exchange,提问作者Anis Mansuri
相关产品推荐
相关产品推荐

