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((_) { //...其他逻辑 });
问题原因分析与修复方案
核心问题
- 路径获取逻辑错误:依赖
creationDate排序取第一个Asset,若相册存在其他刚创建的视频,会拿到错误的Asset,返回无效路径。 - 路径格式不兼容:返回
absoluteString(带file://前缀)给Flutter,File构造函数需要纯本地路径,解析错误导致文件不存在。 - Sandbox临时路径回收:
requestAVAsset返回的可能是系统临时沙盒路径,后续会被回收,导致访问失败。 - 权限与错误处理缺失:未处理相册权限、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
相关产品推荐
相关产品推荐

