get naturalSize and list resolution video m3u8 - ios

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.

Related

AVFoundation over Cellular Data

When I use this code below and I pull my https video link from Firebase over Wifi everything is smooth, the video immediately plays with zero issues. When I use this same code over Cellular everything moves extremely slow, like the video pauses and takes forever to load.
If it plays from file wether I'm on Cellular or Wifi shouldn't matter. What is the issue here?
DataModel:
class Video {
var httpsStr: String?
var videoURL: URL?
convenience init(dict: [String: Any] {
self.init()
if let httpsStr = dict["httpsStr"] as? String {
self.httpsStr = httpsStr
let url = URL(string: httpsStr)!
let assetKeys = [ "playable", "duration"]
let asset = AVURLAsset(url: url)
asset.loadValuesAsynchronously(forKeys: assetKeys, completionHandler: {
DispatchQueue.main.async {
self.videoURL = asset.url
// save videoURL to FileManager to play video from disk
}
})
}
}
}
Firebase Pull:
ref.observeSingleEvent(of: .value) { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
let video = Video(dict: dict)
self.video = video
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.playVideo()
}
}
Play Video:
func playVideo() {
// init AVPlayer ...
guard let videoURL = self.video.videoURL else { return }
let lastPathComponent = videoURL.lastPathComponent
let file = FileManager...appendingPathComponent(lastPathComponent)
if FileManager.default.fileExists(atPath: file.path) {
let asset = AVAsset(url: file)
play(asset)
} else {
let asset = AVAsset(url: videoURL)
play(asset)
}
}
func play(_ asset: AVAsset) {
self.playerItem = AVPlayerItem(asset: asset)
self.player?.automaticallyWaitsToMinimizeStalling = false // I also set this to true
self.playerItem?.preferredForwardBufferDuration = TimeInterval(1)
self.player?.replaceCurrentItem(with: playerItem!)
// play video
}
I followed this answer and now everything seems to work smoothly while on Cellular Data. I needed to include the tracks property in the assetKeys.
You create an asset from a URL using AVURLAsset. Creating the asset,
however, does not necessarily mean that it’s ready for use. To be
used, an asset must have loaded its tracks.
class Video {
var httpsStr: String?
var videoURL: URL?
convenience init(dict: [String: Any] {
self.init()
if let httpsStr = dict["httpsStr"] as? String {
self.httpsStr = httpsStr
let url = URL(string: httpsStr)!
let assetKeys = ["playable", "duration", "tracks"] // <----- "tracks" added here
let asset = AVURLAsset(url: url)
asset.loadValuesAsynchronously(forKeys: assetKeys, completionHandler: {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: "tracks", error: &error)
switch status {
case .loaded:
// Sucessfully loaded, continue processing
DispatchQueue.main.async {
self.videoURL = asset.url
// save videoURL to FileManager to play video from disk
}
case .failed:
// Examine NSError pointer to determine failure
print("Error", error?.localizedDescription as Any)
default:
// Handle all other cases
print("default")
}
})
}
}
}

iOS download and play mp3 file

I am looking for the way how can I download and play mp3 file simultaneously.
I can download it, save to local storage and play after.
But, how can I start downloading and playing it simultaneously, and after it will completely download - save it to local storage. Which tools should I use for it?
Currently I use TCBlobDownload to download, after it I save it and AVAudioPlay to play.
You can use AVAssetResourceLoader to plays an audio file as soon as there is enough data while continuing to download.
Configure the delegate of resourceloader
var playerAsset: AVAsset!
if fileURL.pathExtension.count == 0 {
var components = URLComponents(url: fileURL, resolvingAgainstBaseURL: false)!
components.scheme = "fake" // make custom URL scheme
components.path += ".mp3"
playerAsset = AVURLAsset(url: components.url!)
(playerAsset as! AVURLAsset).resourceLoader.setDelegate(self, queue: DispatchQueue.global())
} else {
playerAsset = AVAsset(url: fileURL)
}
let playerItem = AVPlayerItem(asset: playerAsset)
then, read audio's data and responds to the resource loader
// MARK: - AVAssetResourceLoaderDelegate methods
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
if let url = loadingRequest.request.url {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.scheme = NSURLFileScheme // replace with the real URL scheme
components.path = String(components.path.dropLast(4))
if let attributes = try? FileManager.default.attributesOfItem(atPath: components.url!.path),
let fileSize = attributes[FileAttributeKey.size] as? Int64 {
loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true
loadingRequest.contentInformationRequest?.contentType = "audio/mpeg3"
loadingRequest.contentInformationRequest?.contentLength = fileSize
let requestedOffset = loadingRequest.dataRequest!.requestedOffset
let requestedLength = loadingRequest.dataRequest!.requestedLength
if let handle = try? FileHandle(forReadingFrom: components.url!) {
handle.seek(toFileOffset: UInt64(requestedOffset))
let data = handle.readData(ofLength: requestedLength)
loadingRequest.dataRequest?.respond(with: data)
loadingRequest.finishLoading()
return true
} else {
return false
}
} else {
return false
}
} else {
return false
}
}
And if you want to do this with objective c, refer this

iOS offline HLS file size

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)

Fetch music file from Cloudkit

I try to convert the code which i use to fetch image asset to fetch mp3 file on cloudkit. However, i can't figure the data part. I'm using this library to play audio. It only has one class called "AudioPlayer" so if i want to play music on local folder, it is enough to declare it.
https://github.com/tbaranes/AudioPlayerSwift
func loadSong(completion:#escaping (_ song: AudioPlayer?) -> ()) {
// 1
DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
var song: AudioPlayer!
defer {
completion(song)
}
// 2
guard let asset = self.record["Song_File"] as? CKAsset else {
return
}
let songData: AudioPlayer
do {
songData = try Data(contentsOf: asset.fileURL)
} catch {
return
}
song = AudioPlayer(contentsOf: <#T##URL#>)
}
}
Actually, i can't do clearly what i want. (I wanted to stream a song from CloudKit, this one downloads the ckasset). Below code gets the URL of CkAsset so that i can put it in AVPlayer and play it.
func loadSongURL(completion:#escaping (_ url: URL?) -> ()) {
// 1
DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
var song_url: URL!
defer {
completion(song_url)
}
// 2
guard let asset = self.record["Song_File"] as? CKAsset else {
return
}
let songURL: URL
do {
print(asset.fileURL)
songURL = asset.fileURL
} catch {
return
}
song_url = songURL

How to get all the playlists of a youtube channel using Swift?

my question is not in regards to retrieving videos from a channel in general. I would only like to get all the "playlists" that the channel has created, and retrieve the thumbnail, title, and number of videos of each playlist.
Here's a youtube channel example:
As you can see, there are many created playlists.
As of right now, I'm only able to get the most recent uploaded videos of a channels, in this case just 5.
Using the following code:
let urlString = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=\(playlistID)&key=\(apiKey)"
// Create a NSURL object based on the above string.
let targetURL = NSURL(string: urlString)
// Fetch the playlist from Google.
performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
if HTTPStatusCode == 200 && error == nil
{
// Convert the JSON data into a dictionary.
let resultsDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as! Dictionary<NSObject, AnyObject>
print("resultsDict = \(resultsDict)")
......
}
else
{
print("HTTP Status Code = \(HTTPStatusCode)")
print("Error while loading channel videos: \(error)")
}
})
The output of resultsDict:
[etag: "DsOZ7qVJA4mxdTxZeNzis6uE6ck/0JswUeQp5Wp8607mWPWAfIZaNnM", kind: youtube#playlistItemListResponse, items: (
{
etag = "\"DsOZ7qVJA4mxdTxZeNzis6uE6ck/hQUAFFn45u2V47VqvBg1urbZevU\"";
id = "UUS8cX3kg_pL2jw7cOjGoiBw9Ri_jF-DJp";
kind = "youtube#playlistItem";
snippet = {
channelId = "UCJKOvdk-nVzDAFR_9MF64sw";
channelTitle = AppSpy;
description = "James (#Metal_Slag) punches blocks and butt-stomps critters in neon platformer Super Phantom Cat.\n\nDOWNLOAD FROM THE APP STORE:\nhttps://itunes.apple.com/us/app/super-phantom-cat-be-jumpin/id1041873285?mt=8\n\nSUBSCRIBE TO APPSPY:\nhttps://www.youtube.com/subscription_center?add_user=appspy\n\nVISIT:\nhttp://www.pocketgamer.co.uk";
playlistId = "UUJKOvdk-nVzDAFR_9MF64sw";
position = 0;
publishedAt = "2016-02-12T15:59:10.000Z";
resourceId = {
kind = "youtube#video";
videoId = qkMOjc02NRg;
};
thumbnails = {
default = {
height = 90;
url = "https://i.ytimg.com/vi/qkMOjc02NRg/default.jpg";
width = 120;
};
high = {
height = 360;
url = "https://i.ytimg.com/vi/qkMOjc02NRg/hqdefault.jpg";
width = 480;
};
maxres = {
height = 720;
url = "https://i.ytimg.com/vi/qkMOjc02NRg/maxresdefault.jpg";
width = 1280;
};
medium = {
height = 180;
url = "https://i.ytimg.com/vi/qkMOjc02NRg/mqdefault.jpg";
width = 320;
};
standard = {
height = 480;
url = "https://i.ytimg.com/vi/qkMOjc02NRg/sddefault.jpg";
width = 640;
};
};
title = "MEW-RIO? | Super Phantom Cat iPhone & iPad Preview";
};
},
....
// Display info for most recent remaining 4 videos
....
, nextPageToken: CAUQAA, pageInfo: {
resultsPerPage = 5;
totalResults = 3966;
}]
Instead of getting retrieve the channel's videos, how do I retrieve their playlists instead using the youtube api v3?
Thanks
I see you are using playlistItems from your link. This will only grab videos from a playlist.
follow this link and on the bottom, when you enter part as snippet and channelid as UCJKOvdk-nVzDAFR_9MF64sw. Then click Execute without OAuth. You will get json object of all playlist from that channelid.
In the request it shows you this:
GET https://www.googleapis.com/youtube/v3/playlists?part=snippet&channelId=UCJKOvdk-nVzDAFR_9MF64sw&key={YOUR_API_KEY}
using that url, you should be able to grab it.
This is quite a good tutorial on how to get videos from youtube:
http://www.appcoda.com/youtube-api-ios-tutorial/
I use the function to get playlist from youtube, but it is legacy code(you need to set legacy to true in your build settings of your target to use) not Swift 3.0 (can someone convert?!)
It uses these two pods you must install:
pod "youtube-ios-player-helper", "~> 0.1.4"
pod "iOS-GTLYouTube"
Then import them into your view controller:
import youtube_ios_player_helper
import iOS_GTLYouTube
I have a tab bar with several different table views, and I load a different playlist depending on what tab is selected. First I use the protocol YTPlayerViewDelegate in my table view controller definition:
class YouTubeTableViewController: UITableViewController, YTPlayerViewDelegate {
Then I add some properties to my table view controller, get your api key here https://console.developers.google.com/projectselector/apis/credentials :
var apiKey = "" //place your api key here, get your API key from https://console.developers.google.com/projectselector/apis/credentials
//PLVirhGJQqidpY7n9PJ57FwaW1iogJsFZH = Photographer
//PLVirhGJQqidqjbXhirvXnLZ5aLzpHc0lX = workout
//This array holds all the playlist ID's
var playListArray = ["PLVirhGJQqidpY7n9PJ57FwaW1iogJsFZH","PLVirhGJQqidqjbXhirvXnLZ5aLzpHc0lX"]
//An array to store the videos in your playlist
var videosArray: Array<Dictionary<NSObject, AnyObject>> = []
I then use these to functions, getVideosForPlayList requires a playlistID as as parameter, performGetRequest performs the request to youtube to get the playlist videos:
func getVideosForPlayList(playListID: String) {
// Get the selected channel's playlistID value from the channelsDataArray array and use it for fetching the proper video playlst.
let playlistID = playListID
// Form the request URL string.
let urlString = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=\(playlistID)&key=\(apiKey)"
// Create a NSURL object based on the above string.
let targetURL = NSURL(string: urlString)
// Fetch the playlist from Google.
performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
if HTTPStatusCode == 200 && error == nil {
do {
// Convert the JSON data into a dictionary.
let resultsDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as! Dictionary<NSObject, AnyObject>
// Get all playlist items ("items" array).
let items: Array<Dictionary<NSObject, AnyObject>> = resultsDict["items"] as! Array<Dictionary<NSObject, AnyObject>>
// Use a loop to go through all video items.
for var i=0; i<items.count; ++i {
let playlistSnippetDict = (items[i] as Dictionary<NSObject, AnyObject>)["snippet"] as! Dictionary<NSObject, AnyObject>
// Initialize a new dictionary and store the data of interest.
var desiredPlaylistItemDataDict = Dictionary<NSObject, AnyObject>()
desiredPlaylistItemDataDict["title"] = playlistSnippetDict["title"]
desiredPlaylistItemDataDict["thumbnail"] = ((playlistSnippetDict["thumbnails"] as! Dictionary<NSObject, AnyObject>)["default"] as! Dictionary<NSObject, AnyObject>)["url"]
desiredPlaylistItemDataDict["videoID"] = (playlistSnippetDict["resourceId"] as! Dictionary<NSObject, AnyObject>)["videoId"]
// Append the desiredPlaylistItemDataDict dictionary to the videos array.
self.videosArray.append(desiredPlaylistItemDataDict)
// Reload the tableview.
}
self.tableView.reloadData()
} catch {
print(error)
}
}
else {
print("HTTP Status Code = \(HTTPStatusCode)")
print("Error while loading channel videos: \(error)")
}
})
}
// MARK: Custom method implementation
func performGetRequest(targetURL: NSURL!, completion: (data: NSData?, HTTPStatusCode: Int, error: NSError?) -> Void) {
let request = NSMutableURLRequest(URL: targetURL)
request.HTTPMethod = "GET"
let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: sessionConfiguration)
let task = session.dataTaskWithRequest(request, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completion(data: data, HTTPStatusCode: (response as! NSHTTPURLResponse).statusCode, error: error)
})
})
task.resume()
}
I call the getVideosForPlaylist function from my viewDidAppear function (so a different list is loaded each time, and the selectedIndex of the tabbar is the one just selected). I register my custom cell (I want to customize my youtube cell a bit) in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
let youTubeCell = UINib(nibName: "YouTubeCell", bundle: nil)
self.tableView.registerNib(youTubeCell, forCellReuseIdentifier: "YouTubeCell")
}
override func viewDidAppear(animated: Bool) {
self.videosArray = []
if let tabIndex = self.tabBarController?.selectedIndex,
let playListID = playListArray[tabIndex] as? String{
getVideosForPlayList(playListID)
}
}
In my number of rows in section table view method I make sure I return the number of videos in my videosArray, that changes depending on the channel you load of course:
// MARK: - Table view data source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return videosArray.count
}
In my cellForRowAtIndexPath function I make sure I get the correct video ID for each video loaded and pass this videoID into my loadVideoWithID function:
let video = videosArray[indexPath.row]
let videoID = video["videoID"] as! String
cell.youTubePlayer.loadWithVideoId(videoID)
Here is the code
https://github.com/benSmith1981/youtubeapp

Resources