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

Flutter调用iOS原生保存视频到相册后加载失败问题排查

问题:iOS相册视频路径获取不稳定导致Flutter播放失败

我正在实现以下功能:

  • 录制视频(自动保存至APP内存)
  • 将视频文件从APP内存移至相册(同时保存相册中新视频的路径)
  • 从相册中播放视频

步骤1和3用Flutter实现,步骤2用iOS原生Swift开发。这个功能时好时坏,异常时步骤3会抛出错误:PlatformException(VideoError, Failed to load video: The requested URL was not found on this server., null, null)。用步骤2获取的路径创建File(path)也会提示文件不存在:(OS Error: No such file or directory, errno = 2)。

怀疑是iOS App Sandbox导致,但不确定为何时好时坏,求修复代码。


现有实现代码

Flutter端步骤1(录制视频)

使用camera插件,代码与官方示例基本一致,获取XFile后调用iOS原生方法保存到相册并获取路径。

iOS原生代码(步骤2:保存视频到相册并返回路径)

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let videoSavingChannel = FlutterMethodChannel(name: "app.package.name/camera/video_to_gallery",
                                                  binaryMessenger: controller.binaryMessenger)
    videoSavingChannel.setMethodCallHandler({
        [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
        guard call.method == "saveVideoToGallery" else {
            result(FlutterMethodNotImplemented)
            return
        }
        
        let args = call.arguments as? Dictionary<String, Any>
        guard let fileUrl = args?["fileURL"] else {
            result(nil)
            return
        }
        
        self?.saveVideoToGallery(fileURL: fileUrl as! String, result: result)
    })
    
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func saveVideoToGallery(fileURL: String, result: @escaping FlutterResult) {
    PHPhotoLibrary.shared().performChanges({
        PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: fileURL))
    }) { saved, error in
        if saved {
            let fetchOptions = PHFetchOptions()
            fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
            
            PHAsset.fetchAssets(with: .video, options: fetchOptions).firstObject?.getURL { urlFromGallery in
                guard let absoluteUrl = urlFromGallery?.absoluteString else {
                    result(nil)
                    return
                }
                result(absoluteUrl)
            }
        }
    }
}

PHAsset扩展(获取视频URL)

extension PHAsset {
    
    func getURL(completionHandler : @escaping ((_ responseURL : URL?) -> Void)){
        if self.mediaType == .image {
            let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
            options.canHandleAdjustmentData = {(adjustment: PHAdjustmentData) -> Bool in
                return true
            }
            self.requestContentEditingInput(with: options, completionHandler: {(contentEditingInput: PHContentEditingInput?, info: [AnyHashable : Any]) -> Void in
                completionHandler(contentEditingInput!.fullSizeImageURL as URL?)
            })
        } else if self.mediaType == .video {
            let options: PHVideoRequestOptions = PHVideoRequestOptions()
            options.version = .original
            PHImageManager.default().requestAVAsset(forVideo: self, options: options, resultHandler: {(asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) -> Void in
                if let urlAsset = asset as? AVURLAsset {
                    let localVideoUrl: URL = urlAsset.url as URL
                    completionHandler(localVideoUrl)
                } else {
                    completionHandler(nil)
                }
            })
        }
    }
}

Flutter端步骤3(播放视频)

使用video_player插件:

VideoPlayerController controller =
        VideoPlayerController.file(File(_videoPathFromGallery));

controller.initialize().then((_) {
    //...其他逻辑
});

问题原因分析与修复方案

核心问题

  1. 路径获取逻辑错误:依赖creationDate排序取第一个Asset,若相册存在其他刚创建的视频,会拿到错误的Asset,返回无效路径。
  2. 路径格式不兼容:返回absoluteString(带file://前缀)给Flutter,File构造函数需要纯本地路径,解析错误导致文件不存在。
  3. Sandbox临时路径回收:requestAVAsset返回的可能是系统临时沙盒路径,后续会被回收,导致访问失败。
  4. 权限与错误处理缺失:未处理相册权限、Asset获取失败等场景,强制解包易引发崩溃。

修复代码

1. 修正iOS端保存逻辑:精准获取刚创建的Asset

不再依赖排序,直接从变更请求中拿到新创建的Asset,确保路径正确:

func saveVideoToGallery(fileURL: String, result: @escaping FlutterResult) {
    let videoURL = URL(fileURLWithPath: fileURL)
    var placeholder: PHObjectPlaceholder?
    
    PHPhotoLibrary.shared().performChanges({
        let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
        placeholder = request?.placeholderForCreatedAsset
    }) { saved, error in
        guard saved, let placeholder = placeholder else {
            result(error?.localizedDescription ?? "视频保存失败")
            return
        }
        
        // 通过唯一标识符获取刚创建的视频Asset
        let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [placeholder.localIdentifier], options: nil)
        guard let asset = fetchResult.firstObject else {
            result("未找到刚保存的视频")
            return
        }
        
        asset.getURL { urlFromGallery in
            guard let url = urlFromGallery else {
                result("无法获取视频路径")
                return
            }
            // 返回纯本地路径(不带file://前缀)给Flutter
            result(url.path)
        }
    }
}

2. 修正Flutter端播放逻辑

直接用返回的本地路径构造File:

VideoPlayerController controller = VideoPlayerController.file(File(_videoPathFromGallery));

controller.initialize().then((_) {
    //...其他逻辑
});

3. 完善PHAsset扩展:添加错误处理与iCloud支持

extension PHAsset {
    func getURL(completionHandler : @escaping ((_ responseURL : URL?) -> Void)){
        if self.mediaType == .image {
            let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
            options.canHandleAdjustmentData = {(adjustment: PHAdjustmentData) -> Bool in
                return true
            }
            self.requestContentEditingInput(with: options) { contentEditingInput, info in
                guard let input = contentEditingInput else {
                    completionHandler(nil)
                    return
                }
                completionHandler(input.fullSizeImageURL)
            }
        } else if self.mediaType == .video {
            let options: PHVideoRequestOptions = PHVideoRequestOptions()
            options.version = .original
            options.isNetworkAccessAllowed = true // 允许访问iCloud中的视频
            PHImageManager.default().requestAVAsset(forVideo: self, options: options) { asset, audioMix, info in
                guard let urlAsset = asset as? AVURLAsset else {
                    completionHandler(nil)
                    return
                }
                completionHandler(urlAsset.url)
            }
        } else {
            completionHandler(nil)
        }
    }
}

4. 配置相册权限

在Info.plist中添加权限描述:

<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要访问相册保存录制的视频</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册播放视频</string>

内容的提问来源于stack exchange,提问作者Karzel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 17:40:40