In iOS 10, Apple added offline HLS. In the documentation, they mention:
Important: Downloaded HLS assets are stored on disk in a private
bundle format. This bundle format may change over time, and developers
should not attempt to access or store files within the bundle
directly, but should instead use AVFoundation and other iOS APIs to
interact with downloaded assets.
It appears the access to information about these files is limited. I'm trying to find the size of the stored file. Here is what I do. After download finishes, I save the relative path
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
//Save path
video?.downloadPath = location.relativePath
}
later I reconstruct the file path as follows
if let assetPath = workout.downloadPath {
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
This works:
try FileManager.default.removeItem(at: assetURL)
This does not and returns an error that the file doesn't exist:
let att = try FileManager.default.attributesOfItem(atPath: assetURL.absoluteString)
I can load in the video asset as follows and play it offline with:
let avAsset = AVURLAsset(url: assetURL)
But this returns me an empty array:
let tracks = avAsset.tracks(withMediaType: AVMediaTypeVideo)
Once again I'm just trying to get the file size of an offline HLS asset. It appears the other answers on SO for getting a file size using FileManager don't work for these nor do the answers for getting the size of a loaded AVAsset. Thanks in advance.
Swift 5.3 Solution
Here is how to calculate offline HLS (.movpkg) File Size:
/// Calculates HLS File Size.
/// - Parameter directoryPath: file directory path.
/// - Returns: Human Redable File Size.
func getHLSFileSize(at directoryPath: String) -> String? {
var result: String? = nil
let properties: [URLResourceKey] = [.isRegularFileKey,
.totalFileAllocatedSizeKey,
/*.fileAllocatedSizeKey*/]
guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath),
includingPropertiesForKeys: properties,
options: .skipsHiddenFiles,
errorHandler: nil) else {
return nil
}
let urls: [URL] = enumerator
.compactMap { $0 as? URL }
.filter { $0.absoluteString.contains(".frag") }
let regularFileResources: [URLResourceValues] = urls
.compactMap { try? $0.resourceValues(forKeys: Set(properties)) }
.filter { $0.isRegularFile == true }
let sizes: [Int64] = regularFileResources
.compactMap { $0.totalFileAllocatedSize! /* ?? $0.fileAllocatedSize */ }
.compactMap { Int64($0) }
let size = sizes.reduce(0, +)
result = ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file)
return result
}
Usage
if let url = URL(string: localFileLocation),
let size = self.getHLSFileSize(at: url.path) {
result = String(size)
}
The only way is to sum all files sizes inside a folder where your downloaded content is stored.
- (NSUInteger)hlsFileSize:(NSURL *)fileURL {
NSUInteger size = 0;
let enumerator = [NSFileManager.defaultManager enumeratorAtURL:fileURL includingPropertiesForKeys:nil options:0 errorHandler:nil];
for (NSURL *url in enumerator) {
NSError *error = nil;
// Get values
let resourceValues = [url resourceValuesForKeys:#[NSURLIsRegularFileKey, NSURLFileAllocatedSizeKey, NSURLNameKey] error:&error];
// Skip unregular files
let isRegularFile = [resourceValues[NSURLIsRegularFileKey] boolValue];
if (!isRegularFile) {
continue;
}
let fileAllocatedSize = [resourceValues[NSURLFileAllocatedSizeKey] unsignedLongLongValue];
size += fileAllocatedSize;
}
return size;
}
Try this instead:
let att = try FileManager.default.attributesOfItem(atPath: assetURL.path)
Related
I'm making an application with one Core Data entity, Park, which is decoded from the data returned from an API request and stores the image url and local file/download location (if it has been downloaded) for each image as attributes. I created a computed property that returns a dictionary of ImageInfoObjects (which is a struct that basically just bundles the information together) based on the stored attributes. The first time I run the app, everything works fine but when I close the app and run it again it gives me the error "the file couldn't be opened because there is no such file". So I know there must be an issue with the way I'm storing the file paths in Core Data, or the way I'm reading them back to display the images. Any help would be appreciated. Code snippets below.
The method which catches the error:
func displayPhoto(_ object: ImageInfoObject, imageView: UIImageView) {
guard let location = object.downloadLocation else { return }
do {
let imageData = try Data(contentsOf: location)
let image = UIImage(data: imageData)
DispatchQueue.main.async {
imageView.image = image
}
} catch (let error) {
print(error)
}
}
The URLSessionDownloadDelegate method which is called once each image is finished downloading:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
let sourceURL = downloadTask.originalRequest?.url,
let download = self.photoDownloads[sourceURL] else {
fatalError()
}
let lastPathComponent = sourceURL.lastPathComponent
let destinationURL = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.copyItem(at: location, to: destinationURL)
let index = download.imageInfoObject.index
let newImageInfoObject = ImageInfoObject(url: sourceURL, index: index, downloadLocation: destinationURL)
self.park?.photoInfoObjects[index] = newImageInfoObject
switch newImageInfoObject.index {
case 1:
displayPhoto(newImageInfoObject, imageView: self.photo1View)
case 2:
displayPhoto(newImageInfoObject, imageView: self.photo2View)
default:
break
}
try context?.save()
} catch {
print(error)
}
}
The computed property that stores and retrieves the paths from the CoreData entity. The NSManaged attributes are self.photo1_url, self.photo1_location, self.photo2_url, and self.photo2_location.
public var photoInfoObjects: Dictionary<Int, ImageInfoObject> {
get {
var dictionary: Dictionary<Int, ImageInfoObject> = [:]
var object: ImageInfoObject
if let photo1_url = self.photo1_url,
let url = URL(string: photo1_url) {
if let photo1_location = self.photo1_location {
let location = URL(fileURLWithPath: photo1_location)
object = ImageInfoObject(url: url, index: 1, downloadLocation: location)
object.isDownloaded = true
} else {
object = ImageInfoObject(url: url, index: 1)
}
dictionary[1] = object
}
var object2: ImageInfoObject
if let photo2_url = self.photo2_url,
let url = URL(string: photo2_url) {
if let photo2_location = self.photo2_location {
let location = URL(fileURLWithPath: photo2_location)
object2 = ImageInfoObject(url: url, index: 2, downloadLocation: location)
object2.isDownloaded = true
} else {
object2 = ImageInfoObject(url: url, index: 2)
}
dictionary[2] = object2
}
return dictionary
}
set {
self.photo1_url = newValue[1]?.url.absoluteString
self.photo1_location = newValue[1]?.downloadLocation?.path
self.photo2_url = newValue[2]?.url.absoluteString
self.photo2_location = newValue[2]?.downloadLocation?.path
}
}
I play video m3u8.
I try uselet videoAssetSource = AVAsset(url: videoURL) but videoAssetSource.tracks(withMediaType: .video).count always return 0.
When I use link mp4 this is successful.
How to get list quality link m3u8 support and change quality when playing video.
You will have to create your own respective models for resolutions but then a code like this should work.
/// Downloads the stream file and converts it to the raw playlist.
/// - Parameter completion: In successful case should return the `RawPlalist` which contains the url with which was the request performed
/// and the string representation of the downloaded file as `content: String` parameter.
func getPlaylist(from url: URL, completion: #escaping (Result<RawPlaylist, Error>) -> Void) {
task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data, let string = String(data: data, encoding: .utf8) {
completion(.success(RawPlaylist(url: url, content: string)))
} else {
completion(.failure(PlayerException.MEDIA_ERR_DECODE)) // Probably an MP4 file.
}
}
task?.resume()
}
/// Iterates over the provided playlist contetn and fetches all the stream info data under the `#EXT-X-STREAM-INF"` key.
/// - Parameter playlist: Playlist object obtained from the stream url.
/// - Returns: All available stream resolutions for respective bandwidth.
func getStreamResolutions(from playlist: RawPlaylist) -> [StreamResolution] {
var resolutions = [StreamResolution]()
playlist.content.enumerateLines { line, shouldStop in
let infoline = line.replacingOccurrences(of: "#EXT-X-STREAM-INF", with: "")
let infoItems = infoline.components(separatedBy: ",")
let bandwidthItem = infoItems.first(where: { $0.contains(":BANDWIDTH") })
let resolutionItem = infoItems.first(where: { $0.contains("RESOLUTION")})
if let bandwidth = bandwidthItem?.components(separatedBy: "=").last,
let numericBandwidth = Double(bandwidth),
let resolution = resolutionItem?.components(separatedBy: "=").last?.components(separatedBy: "x"),
let strignWidth = resolution.first,
let stringHeight = resolution.last,
let width = Double(strignWidth),
let height = Double(stringHeight) {
resolutions.append(StreamResolution(maxBandwidth: numericBandwidth,
averageBandwidth: numericBandwidth,
resolution: CGSize(width: width, height: height)))
}
}
return resolutions
}
}
You need to subscribe an observer to the property tracks on a player item:
//Define this variable globally
var observers:[NSKeyValueObservation]? = [NSKeyValueObservation]()
//Find tracks
let videoAssetSource = AVAsset(url: videoURL)
let playerItem = AVPlayerItem(asset: videoAssetSource)
let tracksObserver = self.playerItem.observe(\.tracks, options: [.old, .new]) { (item, change) in
for track in item.tracks {
let _assetTrack:AVAssetTrack? = track.assetTrack
if let assetTrack = _assetTrack {
if assetTrack.mediaType == .video {
//we found a video track
}
}
}
}
//Keep observer reference
observers?.append(tracksObserver)
Im using Swift 4 block-based key value observer, but you can use the observeValue(forKeyPath:…) if you want.
I was learning to use ARKIT and I was wondering if there is a way to add reference images(images to be recognised) from within the app(based on the user's choice). As per the documentation, this can be done by adding the reference images to the Assets(during the development phase) which limits the usability of the app. I was wondering if there is a way where we can download/add these images based on the user's choice and use these images as reference image(within the app).
If you have a look at the documentation for: ARReferenceImage you will note that there are two methods of generating ARReferenceImages manually:
init(CGImage, orientation: CGImagePropertyOrientation, physicalWidth: CGFloat)
init(CVPixelBuffer, orientation: CGImagePropertyOrientation, physicalWidth: CGFloat)
The one that you will need if you are downloading from a Server is the first one, which requires the use of a CGImage.
So any image(s) which you download will need to be converted using this method.
To download images from a Server first you will need to use a URLSession to download these to a location on your device e.g. the Documents Directory.
A simple example of this would look like so:
/// Downloads An Image From A Remote URL
func downloadImageTask(){
//1. Get The URL Of The Image
guard let url = URL(string: "http://www.blackmirrorz.tech/images/BlackMirrorz/blackMirrorzLogo.png") else { return }
//2. Create The Download Session
let downloadSession = URLSession(configuration: URLSession.shared.configuration, delegate: self, delegateQueue: nil)
//3. Create The Download Task & Run It
let downloadTask = downloadSession.downloadTask(with: url)
downloadTask.resume()
}
}
Having created the URLSession you would then need to register for the URLSessionDownloadDelegate and the following method:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
Whereby the location parameter refers to the:
A file URL for the temporary file. Because the file is temporary, you
must either open the file for reading or move it to a permanent
location in your app’s sandbox container directory before returning
from this delegate method.
As such your callback might look like so:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
//1. Create The Filename
let fileURL = getDocumentsDirectory().appendingPathComponent("image.png")
//2. Copy It To The Documents Directory
do {
try FileManager.default.copyItem(at: location, to: fileURL)
print("Successfuly Saved File \(fileURL)")
} catch {
print("Error Saving: \(error)")
}
}
Whereby I use the following function to get the users Documents Directory:
/// Returns The Documents Directory
///
/// - Returns: URL
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
Now we have downloaded the images we would then create a function to retrieve these and return a Set of ARReferenceImage which is needed by ARWorldTrackingConfiguration.
/// Creates A Set Of ARReferenceImages From All PNG Content In The Documents Directory
///
/// - Returns: Set<ARReferenceImage>
func loadedImagesFromDirectoryContents() -> Set<ARReferenceImage>?{
var index = 0
var customReferenceSet = Set<ARReferenceImage>()
let documentsDirectory = getDocumentsDirectory()
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil, options: [])
let filteredContents = directoryContents.filter{ $0.pathExtension == "png" }
filteredContents.forEach { (url) in
do{
//1. Create A Data Object From Our URL
let imageData = try Data(contentsOf: url)
guard let image = UIImage(data: imageData) else { return }
//2. Convert The UIImage To A CGImage
guard let cgImage = image.cgImage else { return }
//3. Get The Width Of The Image
let imageWidth = CGFloat(cgImage.width)
//4. Create A Custom AR Reference Image With A Unique Name
let customARReferenceImage = ARReferenceImage(cgImage, orientation: CGImagePropertyOrientation.up, physicalWidth: imageWidth)
customARReferenceImage.name = "MyCustomARImage\(index)"
//4. Insert The Reference Image Into Our Set
customReferenceSet.insert(customARReferenceImage)
print("ARReference Image == \(customARReferenceImage)")
index += 1
}catch{
print("Error Generating Images == \(error)")
}
}
} catch {
print("Error Reading Directory Contents == \(error)")
}
//5. Return The Set
return customReferenceSet
}
So to put this last function into place you would do the following:
let detectionImages = loadedImagesFromDirectoryContents()
configuration.detectionImages = detectionImages
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
Hope it helps...
I wonder why the contentsOf returns nil for URL from AVURLAsset. After picking from the custom library with Photos framework, I tried to request the asset from PHAsset like the following:
PHCachingImageManager().requestAVAsset(forVideo: asset, options: nil) { (avAsset, _, _) in
DispatchQueue.main.async {
guard let asset = avAsset as? AVURLAsset else {
return
}
print(asset.url) // file:///var/mobile/Media/DCIM/101APPLE/IMG_1513.MP4
}
}
The video with the URL above can be displayed normally with AVPlayer. But when I try to get the data associated with the url using:
do {
let videoData = try Data(contentsOf: mediaURL!)
} catch (let error){
print(error.localizedDescription ?? "") // "The file “IMG_1490.MP4” couldn’t be opened because you don’t have permission to view it."
}
Please find working source code in which i have retrieved recent video from gallery then i have converted that video into AVURLAsset using that AVURLAsset i converted it's data.
Swift 4
override func viewDidLoad() {
super.viewDidLoad()
//For fetching Videos from Photo Library.
let phFetchOptions = PHFetchOptions()
phFetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate",
ascending: false)]
phFetchOptions.predicate = NSPredicate(format: "mediaType == %d",
PHAssetMediaType.video.rawValue)
phFetchOptions.fetchLimit = 2
let videoPhAssetResult = PHAsset.fetchAssets(with:phFetchOptions)
print(videoPhAssetResult.count)
let videoPHAsset = videoPhAssetResult.object(at: 0)
//For Fetching AvAsset from PHAsset and along with that getting Data from AvAsset
PHCachingImageManager().requestAVAsset(forVideo:videoPHAsset , options: nil) { (vidAvAsset, _, _) in
DispatchQueue.main.async {
if vidAvAsset != nil {
let assetURL = vidAvAsset as? AVURLAsset
print(assetURL?.url)
do {
let videoData = try Data(contentsOf:(assetURL?.url)!)
print(videoData)
} catch (let error){
print(error.localizedDescription)
}
}
}
}
}
(Note : Please don’t forget to add Privacy - Photo Library Usage Description permission for Accessing Photo Library of Device or Simulator)
I'm making an app that records video, uploads it to iCloud using CloudKit with a CKAsset, then downloads the file and plays it in an AVPlayer. This is all written in Swift 2.0
I have gotten the data downloaded, and I think I've been able to reference it but I'm not sure. Data/garbage does print when I convert the URL into an NSData object and print it to the console. The video files gets downloaded as a binary file however. I was able to go to the CloudKit dashboard and download the file and append '.mov' to it, and it opened in Quicktime no problem.
So I think my main issue is that I can't work out how to get the video file to actually play, since the file has no extension. I have tried appending '.mov' to the end with URLByAppendingPathExtension() to no avail. Let me know of any ideas!
Upload Video
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
let tempURL = info[UIImagePickerControllerMediaURL] as! NSURL
dismissViewControllerAnimated(true) { () -> Void in
self.uploadVideoToiCloud(tempURL)
print("\n Before Upload: \(tempURL)\n")
}
}
func uploadVideoToiCloud(url: NSURL) {
let videoRecord = CKRecord(recordType: "video", recordID: id)
videoRecord["title"] = "This is the title"
let videoAsset = CKAsset(fileURL: url)
videoRecord["video"] = videoAsset
CKContainer.defaultContainer().privateCloudDatabase.saveRecord(videoRecord) { (record, error) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if error == nil {
print("upload successful")
} else {
print(error!)
}
})
}
}
Download Video
func downloadVideo(id: CKRecordID) {
privateDatabase.fetchRecordWithID(id) { (results, error) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
if error != nil {
print(" Error Fetching Record " + error!.localizedDescription)
} else {
if results != nil {
print("pulled record")
let record = results!
let videoFile = record.objectForKey("video") as! CKAsset
self.videoURL = videoFile.fileURL
print(" After Download: \(self.videoURL!)")
self.videoAsset = AVAsset(URL: self.videoURL!)
self.playVideo()
} else {
print("results Empty")
}
}
}
}
}
The root problem is that AVPlayer expects a file extension, for example .mov, but CKAsset's fileURL property points to a file that lacks an extension. The cleanest solution is to create a hard link, which avoids shuffling megabytes of data around and requires no disk space:
- (NSURL *)videoURL {
return [self createHardLinkToVideoFile];
}
- (NSURL *)createHardLinkToVideoFile {
NSError *err;
if (![self.hardURL checkResourceIsReachableAndReturnError:nil]) {
if (![[NSFileManager defaultManager] linkItemAtURL:self.asset.fileURL toURL:self.hardURL error:&err]) {
// if creating hard link failed it is still possible to create a copy of self.asset.fileURL and return the URL of the copy
}
}
return self.hardURL;
}
- (void)removeHardLinkToVideoFile {
NSError *err;
if ([self.hardURL checkResourceIsReachableAndReturnError:nil]) {
if (![[NSFileManager defaultManager] removeItemAtURL:self.hardURL error:&err]) {
}
}
}
- (NSURL *)hardURL {
return [self.asset.fileURL URLByAppendingPathExtension:#"mov"];
}
Then in the view controller, point AVPlayer to videoURL instead of asset.fileURL.
Solution ended up being that I forgot to specify the filename before I wrote the data to it. I was using URLByAppendingPathExtension and it messed up the URL, ended up using URLByAppendingPathComponent and adding a filename there. Here's the solution that worked for me! Thanks for the comments guys.
func downloadVideo(id: CKRecordID) {
privateDatabase.fetchRecordWithID(id) { (results, error) -> Void in
dispatch_async(dispatch_get_main_queue()) { () -> Void in
if error != nil {
print(" Error Fetching Record " + error!.localizedDescription)
} else {
if results != nil {
print("pulled record")
let record = results as CKRecord!
let videoFile = record.objectForKey("video") as! CKAsset
self.videoURL = videoFile.fileURL as NSURL!
let videoData = NSData(contentsOfURL: self.videoURL!)
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let destinationPath = NSURL(fileURLWithPath: documentsPath).URLByAppendingPathComponent("filename.mov", isDirectory: false) //This is where I messed up.
NSFileManager.defaultManager().createFileAtPath(destinationPath.path!, contents:videoData, attributes:nil)
self.videoURL = destinationPath
self.videoAsset = AVURLAsset(URL: self.videoURL!)
self.playVideo()
} else {
print("results Empty")
}
}
}
}
}
Here's the solution for multiple video download from CloudKit. Using this you can store the video on multiple destination and get easily file path
import AVKit
import CloudKit
var assetForVideo = [CKAsset]()
var videoURLForGetVideo = NSURL()
database.perform(queryForVideo, inZoneWith: nil) { [weak self] record, Error in
guard let records = record, Error == nil else {
return
}
DispatchQueue.main.async { [self] in
self?.assetForVideo = records.compactMap({ $0.value(forKey: "video") as? CKAsset })
for (i,dt) in self!.assetForVideo.enumerated(){
self!.videoURLForGetVideo = (dt.fileURL as NSURL?)!
let videoData = NSData(contentsOf: self!.videoURLForGetVideo as URL)
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let destinationPath = NSURL(fileURLWithPath: documentsPath).appendingPathComponent(self!.assetForVideo.count == i ? "filename\(self!.assetForVideo.count).mov" : "filename\(i+1).mov", isDirectory: false)! as NSURL
FileManager.default.createFile(atPath: destinationPath.path!, contents: videoData as Data?, attributes: nil)
self?.videoURLForGetVideo = destinationPath
self!.videoAssett = AVURLAsset(url: self!.videoURLForGetVideo as URL)
let abc = self!.videoAssett.url
let videoURL = URL(string: "\(abc)")
}
}
}