You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

移除视频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)

问题根因分析

从错误信息和代码来看,导致问题的几个关键原因:

  1. 重复生成输出URL:removeMetadataAndGPSInfo中连续两次调用saveVideoToDocumentDirectory,每次调用都会清空整个文档目录并生成新文件名,导致导出会话使用的URL与后续回调传递的URL不一致,引发文件冲突。
  2. 全局清空文档目录:saveVideoToDocumentDirectory中每次都会清空整个文档目录,既危险(误删其他文件)又会导致多次调用时文件被意外删除。
  3. 元数据过滤逻辑不安全:使用强制类型转换$0.key as! AVMetadataKey,若元数据键类型不匹配会直接崩溃,且未完全过滤所有GPS相关元数据。
  4. 权限处理不严谨:saveVideoToPhotoLibrary中未判断权限授权状态,直接执行保存操作,可能在未授权时失败。
  5. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 23:20:55