Get video URL from AVPlayer in UIWebView - ios

I am trying to detect URL of current video playing from UIWebView, with this method :
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool
{
NotificationCenter.default.addObserver(self, selector: #selector(playerItemBecameCurrent(notification:)), name: NSNotification.Name("AVPlayerItemBecameCurrentNotification"), object: nil)
//urlTextField.text = webView.request?.url?.absoluteString
return true
}
func playerItemBecameCurrent(notification:Notification) {
let playerItem: AVPlayerItem? = notification.object as? AVPlayerItem
if playerItem == nil {
return
}
// Break down the AVPlayerItem to get to the path
let asset: AVURLAsset? = (playerItem?.asset as? AVURLAsset)
let url: URL? = asset?.url
let path: String? = url?.absoluteString
print(path!)
}
This code works but the video URL doesn't contain any video file extension:
for example here is YouTube and Vimeo video ULRs :
https://r5---sn-ab5l6nzr.googlevideo.com/videoplayback?mime=video%2Fmp4&requiressl=yes&clen=27257208&mn=sn-ab5l6nzr&mm=31&mv=m&mt=1499534990&key=yt6&ms=au&source=youtube&ip=107.182.226.163&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Cratebypass%2Crequiressl%2Csource%2Cexpire&initcwndbps=3565000&id=o-ACG22m-TtwyC8tSG_AHUJk3qOPvhUm1X_-qRjy07pjIx&ei=-hZhWZXcJ_aV8gS2tbCQDg&lmt=1497638262979584&ratebypass=yes&gir=yes&pl=25&expire=1499556698&dur=316.093&signature=94C8ED3E4AF64A7AC9F1B7E226454823B236CA55.C18F5F5D9D314B73EDD01DFC4EA3CEA1621AC6C7&ipbits=0&itag=18&cpn=Wb3RPNfJRcFx5P-x&c=MWEB&cver=1.20170706&ptk=TiTV2010%2Buser&oid=1nI2Jk44kRu4HLE6kGDUrA&ptchn=E2VhHmwp3Y_wZvgz_LJvfg&pltype=content
and Vimeo :
https://36skyfiregce-vimeo.akamaized.net/exp=1499539063~acl=%2F222646835%2F%2A~hmac=dc3caeb64501f537e2795ee8e121aed75a7eed9ba4e1b921fe92c757fe06b314/222646835/video/778109097,778109103,778109102,778109101/master.m3u8?f=dash

Firstly i want to tell you you're coding is working fine.
Mirror Server
It is because during the uploading, downloading and streaming process google is using a mirror server with domain name googlevideo.com.
You accessing the link video while in streaming state. You will get that mirror link only.
Youtube videos resided inside this mirror server.
Same like that Vimeo is using mirror server akamaized.net
Refer the SO POST For clarification
Decoding The Video Link From Mirror URL
So in this condition what we need to have is unique id of video in youtube and same can be accessed by appending the url https://www.youtube.com/watch?v=id_here
For decoding the unique id from this mirror url is impossible as by SO Answer
They have added this mirror server concept, is for having some security.
As per youtube T&C.. section 5B here -- http://www.youtube.com/static?template=terms
Downloading YouTube content is not in compliance with the YouTube
Tearms of Service.
There are some private API like PSYouTubeExtractor.
I think you need to have URL of video for downloading purpose.

You can get MIME type and raw data like this:
func playerItemBecameCurrent(notification:Notification) {
guard let playerItem = notification.object as? AVPlayerItem,
let asset = playerItem.asset as? AVURLAsset else {return}
let url = asset.url
var response: URLResponse? = nil
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
try? NSURLConnection.sendSynchronousRequest(request, returning: &response)
print(response?.mimeType)
//not the best way to do it
//let data = try? Data(contentsOf: asset.url)
}
Then you can choose appropriate extension that matches MIME type. You can get all supported types by calling AVURLAsset.audiovisualMIMETypes()

Related

Azure Media Service Fairplay DRM AVPlayer swift implementation

I am trying to play a Fairplay DRM protected (encrypted through Azure Media Services) HLS video stream on iOS Device.
I have used the code and process described in the following links:
https://icapps.com/blog/how-integrate-basic-hls-stream-fairplay
https://gist.github.com/fousa/5709fb7c84e5b53dbdae508c9cb4fadc
Following is the code I have written for this.
import UIKit
import AVFoundation
class ViewController: UIViewController, AVAssetResourceLoaderDelegate {
#IBOutlet weak var videoView: UIView!
var player: AVPlayer!
override func viewDidLoad() {
super.viewDidLoad()
let streamURL = "someexampleurl.com/stream.m3u8"
if let url = URL(string: streamURL) {
//2. Create AVPlayer object
let asset = AVURLAsset(url: url)
let queue = DispatchQueue(label: "Some queue")
asset.resourceLoader.setDelegate(self, queue: queue)
let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
//3. Create AVPlayerLayer object
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.videoView.bounds //bounds of the view in which AVPlayer should be displayed
playerLayer.videoGravity = .resizeAspect
//4. Add playerLayer to view's layer
self.videoView.layer.addSublayer(playerLayer)
//5. Play Video
player.play()
}
// Do any additional setup after loading the view.
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
// We first check if a url is set in the manifest.
guard let url = loadingRequest.request.url else {
print("🔑", #function, "Unable to read the url/host data.")
loadingRequest.finishLoading(with: NSError(domain: "com.error", code: -1, userInfo:
nil))
return false
}
print("🔑", #function, url)
// When the url is correctly found we try to load the certificate date. Watch out! For this
// example the certificate resides inside the bundle. But it should be preferably fetched from
// the server.
guard
let certificateURL = Bundle.main.url(forResource: "certfps", withExtension: "cer"),
let certificateData = try? Data(contentsOf: certificateURL) else {
print("🔑", #function, "Unable to read the certificate data.")
loadingRequest.finishLoading(with: NSError(domain: "com.error", code: -2, userInfo: nil))
return false
}
// Request the Server Playback Context.
let contentId = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
guard
let contentIdData = contentId.data(using: String.Encoding.utf8),
let spcData = try? loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData, options: nil),
let dataRequest = loadingRequest.dataRequest else {
loadingRequest.finishLoading(with: NSError(domain: "com.error", code: -3, userInfo: nil))
print("🔑", #function, "Unable to read the SPC data.")
return false
}
// Request the Content Key Context from the Key Server Module.
let ckcURL = URL(string: "https://xxxxx.keydelivery.northeurope.media.azure.net/FairPlay/?kid=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")!
var request = URLRequest(url: ckcURL)
request.httpMethod = "POST"
let assetIDString = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
let postString = "spc=\(spcData.base64EncodedString())&assetId=\(assetIDString)"
request.setValue(String(postString.count), forHTTPHeaderField: "Content-Length")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = postString.data(using: .ascii, allowLossyConversion: true)
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: request) { data, response, error in
if let data = data {
// The CKC is correctly returned and is now send to the `AVPlayer` instance so we
// can continue to play the stream.
if var responseString = String(data: data, encoding: .utf8) {
responseString = responseString.replacingOccurrences(of: "<ckc>", with: "").replacingOccurrences(of: "</ckc>", with: "")
var ckcData = Data(base64Encoded: responseString)!
dataRequest.respond(with: ckcData)
loadingRequest.finishLoading()
} else {
// print("Error encountered while fetching FairPlay license for URL: \(self.drmUrl), \(error?.localizedDescription ?? "Unknown error")")
}
task.resume()
return true
}
}
Everything above works but in the CKC response I get
{
"Error": {
"Message": "Failed content key policy evaluation.",
"Code": "AuthorizationPolicyEvaluationFailure"
}
}
Can anyone please here let me know what I am missing here, this is my first time trying this out
so I could be making a very obvious mistake so please bear with that.
Any help regarding this would be really great (I have been hitting my head on this for multiple days now.)
Thanks.
One thing that will probably help with troubleshooting is to enable the license delivery logging. You can do this in the Azure portal by going to your Media Services account, in the Monitoring section go to Diagnostic settings. Click 'Add diagnostic setting'. Give the setting a name and then, at least initially, tell it to archive to a storage account. Log the 'KeyDeliveryRequests'. Once you save this reproduce the issue. Then go to your Storage account and look for the log result. The Storage container ‘insights-logs-keydeliveryrequests’ will contain the logs.
you can add request header parameter like "authorization" (probably a base 64 token called JWT), "mimetype" in making CKC request, it would work.
Finally, I figured the thing I was missing was not passing the JWT in the "Authorization" header for the CKC request.
Passing the JWT did the trick. :)
Note: JWT stands for the JSON web token generated during the media encryption in azure media services.

Can we manually download/cache the transport stream while streaming m3u8 file

I am trying to play transport stream from m3u8 file. My requirement is to process the downloaded data before giving it to AVPlayer. For that I am using a proxy server[GCDWebserver] to intercept all the request. In proxy server I will download the data process it and feed it back.
I was able to download the media file and also have tried returning data using GCDWebServerDataResponse(data: apiData, contentType: apiResponse.mimeType ?? ""). But player is not playing the media content.
I am using GCDWebserver as my proxy server.
I have created an instance of AVPlayerItem with the following url
http://34.55.7.151:8080/
which is actually my local servers ip & port number.
/// Initialise AVPlayer Item
let url = URL(string: "http://10.155.177.151:8080/")!
let playerItem = AVPlayerItem(url: url)
player = AVPlayer(playerItem: playerItem)
player.automaticallyWaitsToMinimizeStalling = false
let playerViewController = AVPlayerViewController()
playerViewController.player = player
DispatchQueue.main.async {
self.present(playerViewController, animated: true) {
playerViewController.player!.play()
}
}
/// Http proxy handler
webServer.addDefaultHandler(forMethod: "GET", request: GCDWebServerRequest.self) { (request, completion) in
let mediaUrl = URL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!
if self.session == nil {
self.session = URLSession(configuration: URLSessionConfiguration.default)
}
let dataTask = self.session?.dataTask(with: mediaUrl, completionHandler: { (data, response, error) in
if let apiData = data, let apiResponse = response {
completion(GCDWebServerDataResponse(data: apiData, contentType: apiResponse.mimeType ?? ""))
} else {
completion(GCDWebServerDataResponse(text: "Error"))
}
})
dataTask!.resume()
}
Something similar has been implemented here: https://github.com/StyleShare/HLSCachingReverseProxyServer
It downloads the segments while streaming, if the segment is available locally, it will use that instead.

Loading Microsoft Office documents in WKWebView

I have been using UIWebView to display Microsoft Office documents (Word, PowerPoint, Excel) in my application for a while but Apple has recently deprecated the UIWebView class. I am trying to switch to WKWebView but Word, Excel, and Powerpoint documents are not rendering properly in WKWebView.
Using UIWebView to display an Excel document (worked great):
let data: Data
//data is assigned bytes of Excel file
let webView = UIWebView()
webView.load(data, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", textEncodingName: "UTF-8", baseURL: Bundle.main.bundleURL)
Attempting to use WKWebView to do the same thing (displays a bunch of nonsense characters instead of the Excel file):
let data: Data
//data is assigned bytes of Excel file
let webView = WKWebView.init()
webView.load(data, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", characterEncodingName: "UTF-8", baseURL: Bundle.main.bundleURL)
Due to the nature of my use case, I cannot save the data to disk for security reasons so I cannot use methods like this:
webView.loadFileURL(<#T##URL: URL##URL#>, allowingReadAccessTo: <#T##URL#>)
I also cannot use QuickLook (QLPreviewController) because it again requires a URL.
---------------------------------------------------------------EDIT---------------------------------------------------------
I am also aware of this method of passing the data in via a string URL but unless someone can prove that the data is never written to disk, I cannot accept it as an answer:
let data: Data
//data is assigned bytes of Excel file
let webView = WKWebView.init()
let urlStr = "data:\(fileTypeInfo.mimeType);base64," + data.base64EncodedString()
let url = URL(string: urlStr)!
let request = URLRequest(url: url)
webView.load(request)
This feels like a bug in WKWebView.load(_ data: Data, mimeType MIMEType: String, characterEncodingName: String, baseURL: URL) -> WKNavigation?. It should work in the way we were trying to use it but here is how we got around the issue:
Declare your WKWebView and a custom scheme name
let webView: WKWebView
let customSchemeName = "custom-scheme-name"
Create a subclass of WKURLSchemeHandler. We are using the webView to display a single document (PDF, Word, PowerPoint, or Excel) for the life of the webView so we pass in that document as Data and the FileTypeInfo which is a custom class we made that has the file's MIME type among other things, in the init of the WKURLSchemeHandler.
private class ExampleWKURLSchemeHandler: NSObject, WKURLSchemeHandler {
private let data: Data
private let fileTypeInfo: FileTypeInfo
init(data: Data, fileTypeInfo: FileTypeInfo) {
self.data = data
self.fileTypeInfo = fileTypeInfo
super.init()
}
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
if let url = urlSchemeTask.request.url, let scheme = url.scheme, scheme == customSchemeName {
let response = URLResponse.init(url: url, mimeType: fileTypeInfo.mimeType, expectedContentLength: data.count, textEncodingName: nil)
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
//any teardown code you may need
}
}
Instantiate your webView with the custom scheme handler class you just made:
let webViewConfiguration = WKWebViewConfiguration()
let webViewSchemeHandler = ExampleWKURLSchemeHandler.init(data: data, fileTypeInfo: fileTypeInfo)
webViewConfiguration.setURLSchemeHandler(webViewSchemeHandler, forURLScheme: customSchemeName)
self.webView = WKWebView.init(frame: .zero, configuration: webViewConfiguration)
Tell the webView to load the document using a URL that matches your custom scheme. You can pass whatever you want in the url after the customSchemeName prefix but for our use case we didn't need to because we already passed the document that we wanted to display in the initializer of the WKSchemeHandler:
guard let url = URL.init(string: "\(customSchemeName):/123") else {
fatalError()
}
webView.load(URLRequest.init(url: url))

How to play instagram video using AVPlayer

I am trying to play an instagram video from shared link , I have used following code but it doesn't stream , is there any additional step required ? Any API which will get the source video URL like graph API on Facebook ?
The URL is like this https://instagram.com/p/BOzamYMA-vb/
let videoURL = NSURL(string: "https://instagram.com/p/BOzamYMA-vb/")
let player = AVPlayer(url: videoURL! as URL)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
playerViewController.player!.play()
}
This opens player but doesn't play video
Any help is appreciated.
You are giving a web page URL to AVPlayer, this can't work, you have to use the URL for the video itself.
Using the Fuzi HTML parsing library you can get the video URL from the web page.
The trick is to find the URL in the HTML.
For instagram, we can find it with this xpath: "//meta[#property='og:video']/#content".
Example:
import Fuzi
let link = "https://instagram.com/p/BOzamYMA-vb/"
if let pageURL = URL(string: link),
let data = try? Data(contentsOf: pageURL),
let doc = try? HTMLDocument(data: data)
{
let items = doc.xpath("//meta[#property='og:video']/#content")
if let item = items.first,
let url = URL(string: item.stringValue)
{
print(url)
}
}
This gets the URL from the page, "http://scontent-cdg2-1.cdninstagram.com/t50.2886-16/15872432_382094838793088_926533688240373760_n.mp4", and you can use it to download or stream the video like you would usually do.

Video Streaming Fails when using AVAssetResourceLoader

We are using video streaming in our Swift IOS application and it works very well. The problem is that we would like to use AVAssetResourceLoader so we can make the requests to the streaming server using our own URLSession rather than whatever AVPlayer uses (I have been completely unable to find out what session AVPlayer uses or how to influence what session it uses.)
The exact behavior is that shouldWaitForLoadingOfRequestedResource is called once for two bytes on on the .m3u8 file then it is called again asking for the whole file, and then (based on the start time) the correct .ts file is requested. After we fetch the .ts file the video player simply doesn't do anything further.
This happens whether or not we use a sample streaming video file "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8" or our own server. The sequence is identical if we don't use AVAssetResourceLoader (we can tell from our server.) right up until the .ts file is requested. At that point when we don't use the custom loader the AvPlayer brings up the video and keeps requesting .ts files. If we comment out all other interactions with the AVPlayer including setting the initial time the behavior is identical so I am only going to include the code from viewDidLoad and shouldWaitForLoadingOfRequestedResource.
Again if we simply remove the "xyzzy" prefix so that AVAssetResourceLoader isn't used, everything works. Also, I guess importantly, if we target a video file that is not a streaming file everything works either way.
One more thing our transformation of the mime type for the .ts file produced some kind of weird dynamic uti, but this doesn't seem to have anything to do with the problem because even if we hardcode the uti the same thing happens.
override func viewDidLoad() {
super.viewDidLoad()
avPlayer = AVPlayer()
avPlayerLayer = AVPlayerLayer(player: avPlayer)
videoView.layer.insertSublayer(avPlayerLayer, at: 0)
videoView.backgroundColor = UIColor.black
url = URL(string: "xyzzy" + currentPatient.videoURL())!
let asset = AVURLAsset(url: url)
asset.resourceLoader.setDelegate(self, queue: DispatchQueue.main)
let item = AVPlayerItem(asset: asset)
let avPlayerItem = item
avPlayer.replaceCurrentItem(with: avPlayerItem)
videoScrollView.delegate = self
videoScrollView.minimumZoomScale = 1.0
videoScrollView.maximumZoomScale = 6.0
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
let urlString = loadingRequest.request.url?.absoluteString
let urlComponents = urlString?.components(separatedBy: "xyzzy")
let url = URL(string: urlComponents![1])
let request = loadingRequest.dataRequest!
let infoRequest = loadingRequest.contentInformationRequest
let task = globalSession.dataTask(with: url!) { (data, response, error) in
self.avPlayerThread.async {
if error == nil && data != nil {
let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, response?.mimeType as! CFString, nil)
if let infoRequest = infoRequest {
infoRequest.contentType = uti?.takeRetainedValue() as? String
if request.requestsAllDataToEndOfResource == false {
infoRequest.contentLength = Int64(request.requestedLength)
} else {
infoRequest.contentLength = Int64((data?.count)!)
}
infoRequest.isByteRangeAccessSupported = true
}
if infoRequest == nil || request.requestsAllDataToEndOfResource == true {
loadingRequest.dataRequest?.respond(with: data!)
}
loadingRequest.finishLoading()
} else {
print ("error \(error)")
loadingRequest.finishLoading(with: error)
}
}
}
task.resume()
return true
}

Resources