Playing AES encrypted hls stream using avplayer - ios swift - ios

i'm trying to play an AES encrypted stream in AVPlayer.. typically a link of the key is delivered to the player inside the M3U8 playlist.. in my scenario the key is divided in half.. the first half is delivered by the server and i should append the other half inside the app to decrypt when playing
i've already done this on Android, is there a way to do it also on iOS?
This is the playlist:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=284x160
chunklist_w670540365_b200000.m3u8?t=57b5b16d3824d
#EXT-X-STREAM-INF:BANDWIDTH=850000,RESOLUTION=640x360
chunklist_w670540365_b850000.m3u8?t=57b5b16d3824d
And this is the chunk list:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:11
#EXT-X-MEDIA-SEQUENCE:13544
#EXT-X-KEY:METHOD=AES-128,URI="http://example.com/api/getEncryptionKey?t=57b5b16d3824d"
#EXTINF:9.6,
media_w670540365_b200000_13544.ts?t=57b5b16d3824d
#EXTINF:9.6,
media_w670540365_b200000_13545.ts?t=57b5b16d3824d
#EXTINF:10.56,
media_w670540365_b200000_13546.ts?t=57b5b16d3824d
This is what AVPlayer does:
1- the playlist gets downloaded and a chunk list is selected
2- the player downloads the chunk list
3- the decryption key to decrypt the chunks is downloaded
4- the player begins downloading the chunks sequentially to play them
5- every chunk is decrypted and played
What i need to do is:
after the 3rd step when the player calls the api to get the encryption key using this link: 'http://example.com/api/getEncryptionKey?t=57b5b16d3824d', i want to intercept the response and append the other half of the key
Is it possible?

yes, it is very much possible! I recently did it in one of my projects.
Whenever AVPlayer loads the encrypted video, it tries to load decryption key from the URL mentioned in prog_index.m3u8. If AVPlayer is not able to play video with the fetched key or if it didn't get the key at all on the specified url, it calls a delegate method from AVAssetResourceLoaderDelegate that is
public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool {
return shouldLoadOrRenewRequestedResource(resourceLoadingRequest: renewalRequest)
}
and,
public func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
return shouldLoadOrRenewRequestedResource(resourceLoadingRequest: loadingRequest)
}
which of course differ in the cases they are being called. Prior one is called when the player should wait for loading resource and later one is called when the player needs to renew the resource.
func shouldLoadOrRenewRequestedResource(resourceLoadingRequest: AVAssetResourceLoadingRequest) -> Bool {
guard var url = resourceLoadingRequest.request.url else {
return false
}
//FETCH THE KEY FROM NETWORK CALL/KEYSTORE, CONVERT IT TO DATA AND FINISH LOADING OF RESOURCE WITH THAT DATA, IN YOUR CASE JOIN THE OTHER HALF OF THE KEY TO ACTUAL KEY (you can get the first half from the url above)
resourceLoadingRequest.dataRequest?.respond(with: keyData)
resourceLoadingRequest.finishLoading()
return true;
}}
Once you'll return true with the actual key, the video will be played instantly.

While trying with Azure Media services the following sample works fine. Here we are adding the token as part of AVURLAsset options.
var options = [String: [String: String]]()
if (!token.isEmpty) {
let headers = ["Authorization": "Bearer " + token!]
options = ["AVURLAssetHTTPHeaderFieldsKey": headers]
}
let avAsset = AVURLAsset(url: videoUrl, options: options)
let avItem = AVPlayerItem(asset: avAsset)
let player = AVPlayer(playerItem: avItem)
let playerFrame = view.viewWithTag(1)?.frame
controller.player = player
if (autoPlay.isOn) {
player.rate = 1
}
controller.view.frame = playerFrame ?? CGRect(x: 0, y: 0, width: view.frame.width , height: 250)
addChild(controller)
view.viewWithTag(1)?.addSubview(controller.view)
controller.didMove(toParent: self)
The complete working sample can be found from
https://github.com/Azure-Samples/media-services-3rdparty-player-samples/tree/master/src/avplayer

Related

Bitmovin iOS PreprocessHttpRequestDelegate not called

I set a simple player with Bitmovin player iOS like this:
func bind(videoPlayer: BitmovinPlayer.Player, url: URL) {
// Update variables
self.videoPlayer = videoPlayer
self.videoPlayer?.add(listener: self)
self.videoPlayer?.config.networkConfiguration.preprocessHttpRequestDelegate = self
self.url = url
let analyticsConfig = BitmovinAnalyticsConfig(key: Constant.BitmovinAnalytics.LICENSE_KEY)
analyticsConfig.isLive = episode?.liveValue ?? false
analyticsCollector = BitmovinPlayerCollector(config: analyticsConfig)
analyticsCollector.attachPlayer(player: videoPlayer)
}
This method builds the player, but when I run a video the following method does not trigger the delegate:
func preprocessHttpRequest(_ type: String, httpRequest: HttpRequest, completionHandler: #escaping (HttpRequest) -> Void)
Other listeners work as expected.
Somebody had the same issue? I follow all the runcode looking for duplicate delegates but I just set the player once. No documentation or examples found for this feature.
On iOS, the preprocessHttpRequest is only supported for DRM requests (like DRM license acquisition) as per the Bitmovin Player iOS API documentation https://bitmovin.com/docs/player/api-reference/ios/ios-sdk-api-reference-v3#/player/ios/3/docs/Protocols/PreprocessHttpRequestDelegate.html:
Only changing DRM requests is currently supported.

AVPlayer with Visualizer

I need to create a Audio Player for streamed URL (m3u8 format). I have created music player using AVPlayer. But I need to show visualizer for streamed song. I have tried different solution but not found any working example of it.
I have created visualizer using AVAudioPlayer(averagePower) but it won't support streamed URL.
Any help to show visualizer for AVPlayer? Thanks in advance.
I have also tried using MYAudioTapProcessor which most of the people suggested, but for streamed URL, tracks always returns null.
Added the MYAudioTapProcessor.h and MYAudioTapProcessor.m in project
//Initialization of player
let playerItem = AVPlayerItem( url:NSURL( string:"https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8" ) as! URL )
let audioPlayer: AVPlayer = AVPlayer(playerItem:playerItem)
//Added periodic time observer
audioPlayer!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if audioPlayer!.currentItem?.status == .readyToPlay
{
if let playerItem: AVPlayerItem = audioPlayer!.currentItem {
print(playerItem.asset.tracks.count)
if (playerItem.asset.tracks) != nil {
self.tapProcessor = MYAudioTapProcessor(avPlayerItem: playerItem)
playerItem.audioMix = self.tapProcessor.audioMix
self.tapProcessor.delegate = self
}
}
}
}
//Delegate callback method for MYAudioTapProcessor
func audioTabProcessor(_ audioTabProcessor: MYAudioTapProcessor!, hasNewLeftChannelValue leftChannelValue: Float, rightChannelValue: Float) {
print("volume: \(leftChannelValue) : \(rightChannelValue)")
volumeSlider.value = leftChannelValue
}
Also tried by adding the "Track" observer.
playerItem.addObserver(self, forKeyPath: "tracks", options: NSKeyValueObservingOptions.new, context: nil);
Now if play mp3 file, the callback method calls but for m3u8 callback method doesn't call. The main reason for failing m3u8 URL is it always show tracks array count zero whereas for mp3 files tracks array has one item.
You cannot get tracks for HLS via AVPLayer. You should use progressive download or local file for getting audio tracks while playing media.

Downloading and playing offline HLS Content - iOS 10

Since iOS 10, Apple has provided the support for downloading HLS (m3u8) video for offline viewing.
My question is: Is it necessary that we can only download HLS when it is being played ? Or we can just download when user press download button and show progress.
Does anyone has implemented this in Objective C version? Actually my previous App is made in Objective C. Now I want to add support for downloading HLS rather than MP4 (previously I was downloading MP4 for offline view).
I am really desperate to this. Please share thoughts or any code if implemented.
I used the apple code guid to download HLS content with the following code:
var configuration: URLSessionConfiguration?
var downloadSession: AVAssetDownloadURLSession?
var downloadIdentifier = "\(Bundle.main.bundleIdentifier!).background"
func setupAssetDownload(videoUrl: String) {
// Create new background session configuration.
configuration = URLSessionConfiguration.background(withIdentifier: downloadIdentifier)
// Create a new AVAssetDownloadURLSession with background configuration, delegate, and queue
downloadSession = AVAssetDownloadURLSession(configuration: configuration!,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
if let url = URL(string: videoUrl){
let asset = AVURLAsset(url: url)
// Create new AVAssetDownloadTask for the desired asset
let downloadTask = downloadSession?.makeAssetDownloadTask(asset: asset,
assetTitle: "Some Title",
assetArtworkData: nil,
options: nil)
// Start task and begin download
downloadTask?.resume()
}
}//end method
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
// Do not move the asset from the download location
UserDefaults.standard.set(location.relativePath, forKey: "testVideoPath")
}
if you don't understand what's going on, read up about it here:
https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/MediaPlaybackGuide/Contents/Resources/en.lproj/HTTPLiveStreaming/HTTPLiveStreaming.html
now you can use the stored HSL content to play the video in AVPlayer with the following code:
//get the saved link from the user defaults
let savedLink = UserDefaults.standard.string(forKey: "testVideoPath")
let baseUrl = URL(fileURLWithPath: NSHomeDirectory()) //app's home directory
let assetUrl = baseUrl.appendingPathComponent(savedLink!) //append the saved link to home path
now use the path to play video in AVPlayer
let avAssest = AVAsset(url: assetUrl)
let playerItem = AVPlayerItem(asset: avAssest)
let player = AVPlayer(playerItem: playerItem) // video path coming from above function
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true, completion: {
player.play()
})
The only way you can do this is to set up an HTTP server to serve the files locally after you've downloaded them.
The Live playlist uses a sliding-window. You need to periodically reload it after target-duration time and download only the new segments as they appear in the list (they will be removed at a later time).
Here are some related answers: Can IOS devices stream m3u8 segmented video from the local file system using html5 video and phonegap/cordova?
You can easily download an HLS stream with AVAssetDownloadURLSession makeAssetDownloadTask. Have a look at the AssetPersistenceManager in Apples Sample code: https://developer.apple.com/library/content/samplecode/HLSCatalog/Introduction/Intro.html
It should be fairly straight forward to use the Objective C version of the api.
Yes, you can download video stream served over HLS and watch it later.
There is a very straight forward sample app (HLSCatalog) from apple on this. The code is fairly simple. you can find it here - https://developer.apple.com/services-account/download?path=/Developer_Tools/FairPlay_Streaming_Server_SDK_v3.1/FairPlay_Streaming_Server_SDK_v3.1.zip
You can find more about offline HLS streaming here.

How to send cookie with AVPlayer url in iOS?

Is it possible to send cookie with the AVPlayer url?I have a livestream which is AES encrypted and needs a key to decrypt.It will hit the server and the server returns the key only if session is there.So I want to send phpsessionid along with the url to AVPlayer.
Is it possible? I saw AVURLAssetHTTPHeaderFieldsKey.I don't know if it is what I have to set.If so how to do it?
This is how you can set Signed Cookies (Headers) in AVPlayer URL Request :
fileprivate func setPlayRemoteUrl() {
if playUrl.isEmpty { return }
let cookiesArray = HTTPCookieStorage.shared.cookies!
let values = HTTPCookie.requestHeaderFields(with: cookiesArray)
let cookieArrayOptions = ["AVURLAssetHTTPHeaderFieldsKey": values]
let assets = AVURLAsset(url: videoURL! as URL, options: cookieArrayOptions)
let item = AVPlayerItem(asset: assets)
player = AVPlayer(playerItem: item)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
playerLayer?.contentsScale = UIScreen.main.scale
layer.insertSublayer(playerLayer!, at: 0)
}
In your case FPS(FairPlay Streaming) by apple will work. FairPlay Streaming is DRM(Digital Right Management) support where you will get content key along with your content data and you need to pass through delegate which supports AES-128 encrypt. Please refer below link which i shared below
https://developer.apple.com/streaming/fps/
I haven't really tried it myself but it seems there's an API that let's you create AVURLAsset with options. One of the possible option key is AVURLAssetHTTPCookiesKey. You might want to look into that.

Play video from new URL without creating a new AVPlayer object

I'm trying to allow users to be able to cycle through videos, changing the AVPlayer URL on the fly without refreshing the view. However, right now I'm just instantiating AVPlayer objects every time a video is played (resulting in audio to be played over one another), which I feel isn't the best way to do this. Is there a more efficient way similar to changing the image in an imageView?
This is the code where I play the clip:
player = AVPlayer(URL: fileURL)
playerLayer = AVPlayerLayer(player: player)
playerLayer!.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer!)
player!.play()
Do not use an AVPlayer.
Instead use an AVQueuePlayer which allows you to insert and remove items from a queue.
//create local player in setup methods
self.localPlayer = AVQueuePlayer.init()
to add items you can simply use
//optional: clear current queue if playing straight away
self.localPlayer.removeAllItems()
//get url of track
let url : URL? = URL.init(string: "http://urlOfItem")
if url != nil {
let playerItem = AVPlayerItem.init(url: url!)
//you can use the after property to insert
//it at a specific location or leave it nil
self.localPlayer.insert(playerItem, after: nil)
self.localPlayer.play()
}
AVQueuePlayer supports all of the functionality of the AVPlayer but has the added functionality of adding and removing items from a queue.
Use AVPlayerItem to add and remove outputs to an AVPlayer object.
Instead of adding a video to the AVPlayer when you create it, create an empty AVPlayer instance, and then use the addOutput method of the AVPlayerItem class to add the video.
To remove the video and add a new one, use the removeOutput method of the AVPlayerItem class to remove the old video, and then the addOutput method again to insert the new one.
Sample code is available from Apple's developer site at;
https://developer.apple.com/library/prerelease/content/samplecode/AVBasicVideoOutput/Introduction/Intro.html
It provides the same thing I would, were I to post code of my own.
Create AVPlayer Instance globally then override it again when you want to play a new video from new URL.
I am able to accomplish what you are looking for by doing this...
I have a tableView of song names, for which the mp3 files are stored on Parse.com. In didSelectRowAtIndexPath I do...
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
SelectedSongNumber = indexPath.row
grabSong()
}
func grabSong () {
let songQuery = PFQuery(className: "Songs")
songQuery.getObjectInBackgroundWithId(iDArray[SelectedSongNumber], block: {
(object: PFObject?, error : NSError?) -> Void in
if let audioFile = object?["SongFile"] as? PFFile {
let audioFileUrlString: String = audioFile.url!
let audioFileUrl = NSURL(string: audioFileUrlString)!
myAVPlayer = AVPlayer(URL: audioFileUrl)
myAVPlayer.play()
currentUser?.setObject(audioFileUrlString, forKey: "CurrentSongURL")
currentUser?.saveInBackground()
}
})
}
when I run this, i select a row and the song starts playing. If i then wait a few seconds and select a different row, the AVPlayer plays the song from the new cell that i selected and does NOT play one song over the other. My AVPlayer is declared as a public variable for all classes to see.

Resources