Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Load video from Assets.xcassets

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.

like image 479
danialzahid94 Avatar asked Feb 13 '18 13:02

danialzahid94


People also ask

What is assets Xcassets?

xcassets Catalog in Xcode. An asset catalog, simply put, is a single folder in Xcode that you use to organize your app's images, icons, colors, and more. Instead of adding individual images to Xcode's file organizer, you add assets neatly organized in a single catalog.

How do I use Xcassets?

Use of xcassets is the new standard as of Xcode 5 and iOS 7. Import images by clicking on the blue folder called "Images. xcassets" then click on the small "+" plus sign at the bottom of the window that appears. Now choose "Import" to put images in there.

How do I add an asset catalog in Xcode?

If you don't already have an asset catalog in your project, you can create one by right-click on your project and choosing New File. From "iOS" choose "Resource" then Asset Catalog, then click Next and name your catalog. You can now select your new asset catalog in Xcode, and drag pictures directly into it.


3 Answers

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

like image 99
Katia Maeda Avatar answered Oct 01 '22 07:10

Katia Maeda


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!)
like image 31
Hunter Avatar answered Sep 23 '22 05:09

Hunter


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
    }
}
like image 37
Marc Palmer Avatar answered Oct 02 '22 07:10

Marc Palmer