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.
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.
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.
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.
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
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!)
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With