【问题标题】:Have permission to read video files but not image files in Swift?有权在 Swift 中读取视频文件但不能读取图像文件?
【发布时间】:2018-11-13 04:18:54
【问题描述】:

我在 Swift 中编写了一个图像选择器 (NOT UIImagePickerController),按“完成”后,我返回所选 PHAsset(图像或视频或两者)的 URL,然后将它们上传到firebase 存储。

图像的 URL 如下所示:file:///var/mobile/Media/DCIM/101APPLE/IMG_1239.PNG

视频的网址如下所示:file:///var/mobile/Media/DCIM/101APPLE/IMG_1227.MOV

然后我显示 PHAsset。播放视频资产没有问题:

let player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = view.frame
playerLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(playerLayer)
player.play()

但当我尝试使用其 URL 显示图像时,我收到“无权读取文件”错误:

do {
    let data = try Data(contentsOf: url)
    imageView.image = UIImage(data: data)
} catch {
    print(error)
}

我确实有权访问用户的照片库,这就是图像选择器工作正常的原因。我只是无法使用它们的 URL 读取图像文件。

我在想,也许我应该将图像资源复制到一个临时文件并从那里读取它,但是为什么视频资源是可读的而不是图像资源是没有任何意义的。

-------------更新--------------

这就是我获取 PHAsset 网址的方式

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

【问题讨论】:

  • 它与图像与视频无关。您将无法使用其 URL 直接访问视频数据,就像您无法从其 URL 访问图像数据一样。
  • @rmaddy,有道理。你认为我的图像选择器应该将选定的资产复制到一个临时文件然后从那里读取吗?我有其他选项,例如返回所选资产的数据,但是当我尝试这样做时会导致内存泄漏
  • 您无权使用其 URL 复制资产。您需要使用 AVFoundation 的 API 来访问资产的数据。
  • @K.Wu 在显示图像时,您有asset 参考还是只有url
  • @staticVoidMan,仅 URL。实际上,我愿意更改图像选择器返回的数据。起初我的图像选择器通过调用imageManager.requestImageData... 返回资产的数据,但是对于像 GIF 图像这样的大图像,内存使用量很大,每次选择 GIF 时它会多消耗 20+MB 的内存。上传后,内存使用量一直保持不变,因此,如果用户一次上传 1 个 GIF 并重复执行此操作,应用程序将因内存泄漏而崩溃。这就是我的图像选择器返回 URL 的原因。你觉得我应该这样做吗?

标签: ios swift phasset avasset


【解决方案1】:

iOS 中的应用程序在沙盒模式下运行,这会阻止它访问自己范围之外的文件。
一个网址,例如:

file:///var/mobile/Media/DCIM/101APPLE/...

不在您的应用范围内,因此无法通过Data(contentsOf:) 直接访问。

但是,AVPlayer 可以在urls 之外访问,因为它具有特殊权限,这就是为什么您可以使用url 加载视频资产的原因。

要从url 加载图像,我们会使用PHAssets.fetchAssets(withALAssetURLs:options:) 中的url 来取回PHAsset 以加载它,但该方法在iOS 11 中已被弃用。

作为替代方案,PHAssets.fetchAssets(withLocalIdentifiers:options:) 可用,但要使用它,您需要提供localIdentifier
为此,您应该保存PHAssets localIdentifiers,而不是保存url


总结:

  1. 获取您的PHAssets
  2. 保存asset.localIdentifiers 而不是他们的urls
  3. 需要时,使用[1] 取回PHAsset

    PHAssets.fetchAssets(withLocalIdentifiers:options:)
    
  4. 根据mediaType处理PHAsset

    • 图片[2]

      PHImageManager().requestImageData(for:options:resultHandler:)
      
    • 对于视频[3]

      PHImageManager().requestAVAsset(forVideo:options:resultHandler:)
      

一般示例:

警告: fatalError() 已被用于在您的设置出现问题时故意终止应用程序。
如果它崩溃,请检查您的Info.plist 以获取NSPhotoLibraryUsageDescription 和/或通过PHPhotoLibrary.requestAuthorization(_:) 访问照片库的权限状态

  1. 获取最新资产的演示功能(图像/视频;您决定)

    func getLatestAsset(for type: PHAssetMediaType) {
        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate",
                                                         ascending: false)]
        fetchOptions.fetchLimit = 2
    
        let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: type,
                                                             options: fetchOptions)
    
        guard let asset = fetchResult.firstObject else { fatalError("Unable to fetch photos") }
    
        //save `localIdentifier` for later use (app relaunch or whatever)
        let assetIdentifier = asset.localIdentifier
    
        print(assetIdentifier)
    
        //Just a demo that you can get the same asset based on a local identifier
        loadAsset(identifier: assetIdentifier)
    }
    
  2. 基于localIdentifier加载资产的函数

    func loadAsset(identifier: String) {
        guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier],
                                              options: nil).firstObject
            else {
                fatalError("No Asset found with identifier: \(identifier)")
        }
    
        print(asset.localIdentifier)
    
        //Do something with the asset now
        if asset.mediaType == .image {
            loadImage(asset: asset)
        }
        else if asset.mediaType == .video {
            loadVideo(asset: asset)
        }
    }
    
  3. PHAsset 加载图像

    func loadImage(asset: PHAsset) {
        PHImageManager().requestImageData(for: asset, options: nil) { (data, string, orientation, userInfo) in
            guard let data = data else { fatalError("Image Data is nil") }
    
            DispatchQueue.main.async {
                self.imageView.image = UIImage(data: data)
            }
        }
    }
    
  4. PHAsset 加载视频

    func loadVideo(asset: PHAsset) {
        PHImageManager().requestAVAsset(forVideo: asset, options: nil) { (avAsset, audioMix, userInfo) in
            guard let avAsset = avAsset else { fatalError("AVAsset is nil") }
    
            let playerItem = AVPlayerItem(asset: avAsset)
            let player = AVPlayer(playerItem: playerItem)
            let playerLayer = AVPlayerLayer(player: player)
            playerLayer.frame = self.view.frame
            playerLayer.videoGravity = .resizeAspectFill
    
            DispatchQueue.main.async {
                self.view.layer.addSublayer(playerLayer)
                player.play()
            }
        }
    }
    

PS:您将不再需要getURL(completionHandler:),因为您的整个逻辑将基于localIdentifiers 而不是urls,我会说这要好得多。
Urls 只是,有点,善变,所以让我们至少在这里避免它们。

【讨论】:

  • 你是一个很棒的人,这是一个很好的答案!谢谢!
  • @K.Wu 谢谢,这不过是我的荣幸 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-05-09
  • 2020-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-05
  • 2019-06-22
相关资源
最近更新 更多