11

TL;DR: How can I play a video if i have it stored in Assets.xcassets?

I have around 50 videos that i want to store in an app. I will then use on demand resource fetching to make my app size lighter. But I can only achieve this by keeping the videos in Assets.xcassets. And I can't find a way to load videos from there because AVPlayer only seems to accept url, and I'm not really sure how I can get that for a locally stored asset like that.

UPDATE:

So I did further digging and found out that its actually not possible to store videos in the assets catalogue and still be able to use them. I ended up having to move them out of the assets catalogue. As for the on demand resource fetching, it's still possible for resources outside the catalogue. You can find a similar approach here.

danialzahid94
  • 4,103
  • 2
  • 19
  • 31

4 Answers4

6

Now it is possible to load videos inside Assets.xcassets and still be able to play them. Tested on Xcode 12.0.1 Swift 5

import UIKit
import AVKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func createLocalUrl(for filename: String, ofType: String) -> URL? {
        let fileManager = FileManager.default
        let cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
        let url = cacheDirectory.appendingPathComponent("\(filename).\(ofType)")
        
        guard fileManager.fileExists(atPath: url.path) else {
            guard let video = NSDataAsset(name: filename)  else { return nil }
            fileManager.createFile(atPath: url.path, contents: video.data, attributes: nil)
            return url
        }
        
        return url
    }
    
    @IBAction func playLocalVideo(_ sender: Any) {
        guard let videoURL = createLocalUrl(for: "video", ofType: "mp4") else {
            return
        }
        
        // Create an AVPlayer, passing it the local video url path
        let player = AVPlayer(url: videoURL as URL)
        let controller = AVPlayerViewController()
        controller.player = player
        present(controller, animated: true) {
            player.play()
        }
    }
}

Assets.xcassets image

Credit to how create a local url: https://stackoverflow.com/a/39748919/4267092

Katia Maeda
  • 61
  • 1
  • 4
  • 1
    I'm not sure of that the benefits of storing these videos in an asset folder outweighs the benefit that the videos need to be copied in order to be played. – Matthew Knippen Jan 19 '21 at 21:49
  • @MatthewKnippen I assume you meant "outweighs the disadvantage". In other words, storing a video as part of assets has a performance penalty, because it cannot be played directly from assets. i.e. when the app runs the video has to be copied before it can be played. – whitneyland Apr 09 '23 at 22:41
2

You can store any data you like in an Asset Catalog. However you can only load it as NSDataAsset which gives you an NSData. As a result you have do some URL resource handler shenanigans to get a URL that reads directly from the NSData.

It goes something like this.

let videoUrl = URL(string: "my-custom-scheme-not-important://\(assetName)")!
let asset = AVURLAsset(url: videoUrl)
guard let dataAsset = NSDataAsset(name: assetName) else {
    fatalError("Data asset not found with name \(assetName)")
}
let resourceDelegate = DataAssetAVResourceLoader(dataAsset: dataAsset)
assetLoaderDelegate = resourceDelegate
asset.resourceLoader.setDelegate(resourceDelegate, queue: .global(qos: .userInteractive))

let item = AVPlayerItem(asset: asset)

...

@objc
fileprivate class DataAssetAVResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
    let dataAsset: NSDataAsset
    let data: Data
    
    init(dataAsset: NSDataAsset) {
        self.dataAsset = dataAsset
        data = dataAsset.data
    }
    
    @objc
    public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
        if let infoRequest = loadingRequest.contentInformationRequest {
            infoRequest.contentType = kUTTypeQuickTimeMovie as String
            infoRequest.contentLength = Int64(data.count)
            infoRequest.isByteRangeAccessSupported = true
        }
        if let dataRequest = loadingRequest.dataRequest {
            let range = Range(NSRange(location: Int(dataRequest.requestedOffset), length: dataRequest.requestedLength))!
            dataRequest.respond(with: data.subdata(in:range))
            loadingRequest.finishLoading()
            return true
        }
        
        return false
    }
}
Marc Palmer
  • 507
  • 2
  • 9
  • Brilliant approach, I'd clarify that keeping assetLoaderDelegate in a persisted memory is needed for the AVAssetResourceLoaderDelegate callbacks to work – user363349 Sep 20 '22 at 09:22
1

Based on the answer of Marc Palmer I came up with the following solution, which is a bit „swiftier“.

I extended NSDataAsset to conform to AVAssetResourceLoaderDelegate protocol and I extended AVURLAsset to init using a NSDataAsset.

The last line in the code uses I little bit of Objective-C magic to retain the delegate, because AVURLAsset's ResourceLoader does not do this and Swift does not let you define vars in extensions. Because AVURLAsset is a Objective-C class this should not be a problem. You can read more about objc_setAssociatedObject in this post.

    import Foundation
    import AVFoundation

    #if os(macOS)
        import AppKit
    #else
        import UIKit
    #endif

    extension NSDataAsset:AVAssetResourceLoaderDelegate{
        @objc public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
            
            if let infoRequest = loadingRequest.contentInformationRequest{
                infoRequest.contentType = typeIdentifier
                infoRequest.contentLength = Int64(data.count)
                infoRequest.isByteRangeAccessSupported = true
            }
            
            if let dataRequest = loadingRequest.dataRequest{
                dataRequest.respond(with: data.subdata(in:Int(dataRequest.requestedOffset) ..< Int(dataRequest.requestedOffset) + dataRequest.requestedLength))
                loadingRequest.finishLoading()
                
                return true
            }
            return false
        }
    }
    extension AVURLAsset{
        public convenience init?(_ dataAsset:NSDataAsset){
            guard let name = dataAsset.name.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
                  let url = URL(string:"NSDataAsset://\(name))")
            else {return nil}
            
            self.init(url:url) // not really used!
            self.resourceLoader.setDelegate(dataAsset, queue: .main)
            // Retain the weak delegate for the lifetime of AVURLAsset
            objc_setAssociatedObject(self, "AVURLAsset+NSDataAsset", dataAsset, .OBJC_ASSOCIATION_RETAIN)
        }

  • Just wanted to confirm this did work for me since there are a lot of questions both here and on the Apple developer forums around loading a video file from an asset catalog, and no real confirmation for this technique. But it worked great; the only thing to watch out for is that the type identifier on the item in your asset catalog must be correct, otherwise AVPlayer won’t be able to play it. In my case it was `public.mpeg-4`, i.e. the raw value for https://developer.apple.com/documentation/uniformtypeidentifiers/uttypempeg4movie. – tfe Apr 30 '23 at 19:35
-1

Try using AVAsset as noted in the Apple Developer Documentation.

The AVPlayerItem can load an AVAsset and play it. A snippet from the documentation on loading AVAssets:

func prepareToPlay() {
    let url: URL = // Local or Remote Asset URL
    let asset = AVAsset(url: url)

    let assetKeys = [
        "playable",
        "hasProtectedContent"
    ]
    // Create a new AVPlayerItem with the asset and an
    // array of asset keys to be automatically loaded
    playerItem = AVPlayerItem(asset: asset,
                              automaticallyLoadedAssetKeys: assetKeys)

    // Register as an observer of the player item's status property
    playerItem.addObserver(self,
                           forKeyPath: #keyPath(AVPlayerItem.status),
                           options: [.old, .new],
                           context: &playerItemContext)

    // Associate the player item with the player
    player = AVPlayer(playerItem: playerItem)
}

Getting the URL for your asset may look a little like this:

let urlpath     = Bundle.main.path(forResource: "myvideo", ofType: "mp4")
let url         = NSURL.fileURL(withPath: urlpath!)
Hunter
  • 414
  • 5
  • 15
  • 5
    Thanks for the comment. But this actually doesn't solve my problem. I was trying to store the video in the xcassets catalogue. Upon further research I found out that you can't store videos there. I'll just have to move them out of there. – danialzahid94 Feb 14 '18 at 07:14