How to identify a movie that plays in WKWebView?

I would like to know if it is possible to detect a movie that plays in WKWebView?

Also, would I like to know the exact open stream URL?

+6
source share
2 answers

Since the solution to this question required a lot of research and different approaches, I would like to document it so that others follow my thoughts. If you're just interested in the final solution, find some fancy headlines.

The application I started with was pretty simple. This is a one-time application that imports WebKit and opens WKWebView using NSURL :

 import UIKit import WebKit class ViewController: UIViewController { var webView: WKWebView! override func viewDidAppear(animated: Bool) { webView = WKWebView() view = webView let request = NSURLRequest(URL: NSURL(string: "http://tinas-burger.tumblr.com/post/133991473113")!) webView.loadRequest(request) } } 

The URL includes a video that is (view) protected by JavaScript. I really haven’t seen the video yet, it was only the first thing I discovered. Remember to add NSAppTransportSecurity and NSAllowsArbitraryLoads to your Info.plist or you will see a blank page.

WKNavigationDelegate

WKNavigationDelegate will not notify you of the video being played. Therefore, setting webView.navigationDelegate = self and implementing the protocol will not produce the desired results.

NSNotificationCenter

I suggested that there should be an event like SomeVideoPlayerDidOpen . Unfortunately, there were none, but the SomeViewDidOpen event might have, so I started checking the hierarchy of views:

 UIWindow UIWindow WKWebView WKScrollView ... ... UIWindow UIWindow UIView AVPlayerView UITransitionView UIView UIView UIView ... UIView ... AVTouchIgnoringView ... 

As expected, an additional UIWindow will be added, which can have an event and hell, yes, it really is!

I added viewDidAppear: adding a new observer:

 NSNotificationCenter.defaultCenter().addObserver(self, selector: "windowDidBecomeVisible:", name: UIWindowDidBecomeVisibleNotification, object: nil) 

And added the appropriate method:

 func windowDidBecomeVisible(notification: NSNotification) { for mainWindow in UIApplication.sharedApplication().windows { for mainWindowSubview in mainWindow.subviews { // this will print: // 1: `WKWebView` + `[WKScrollView]` // 2: `UIView` + `[]` print("\(mainWindowSubview) \(mainWindowSubview.subviews)") } 

As expected, it returns a hierarchy of views, as we checked before. But, unfortunately, it looks like AVPlayerView will be created later.

If you trust your application that only UIWindow will open it, it is a media player, you will end at this point. But this decision will not allow me to sleep at night, so let go deeper ...

Event injection

We need to receive notification of the addition of AVPlayerView to this nameless UIView . It seems pretty obvious that AVPlayerView should be a subclass of UIView , but since it is not officially documented by Apple, I checked the iOS Runtime Headers for AVPlayerView and this is definitely a UIView .

Now that we know that AVPlayerView is a subclass of UIView , it will probably be added to the unnamed UIView by calling addSubview: Therefore, we need to receive a notification about the added view. Unfortunately, UIView does not provide events for observation. But he calls the didAddSubview: method, which can be very convenient.

So, let's verify that a AVPlayerView will be added somewhere in our application and will send a notification:

 let originalDidAddSubviewMethod = class_getInstanceMethod(UIView.self, "didAddSubview:") let originalDidAddSubviewImplementation = method_getImplementation(originalDidAddSubviewMethod) typealias DidAddSubviewCFunction = @convention(c) (AnyObject, Selector, UIView) -> Void let castedOriginalDidAddSubviewImplementation = unsafeBitCast(originalDidAddSubviewImplementation, DidAddSubviewCFunction.self) let newDidAddSubviewImplementationBlock: @convention(block) (AnyObject!, UIView) -> Void = { (view: AnyObject!, subview: UIView) -> Void in castedOriginalDidAddSubviewImplementation(view, "didAddsubview:", subview) if object_getClass(view).description() == "AVPlayerView" { NSNotificationCenter.defaultCenter().postNotificationName("PlayerWillOpen", object: nil) } } let newDidAddSubviewImplementation = imp_implementationWithBlock(unsafeBitCast(newDidAddSubviewImplementationBlock, AnyObject.self)) method_setImplementation(originalDidAddSubviewMethod, newDidAddSubviewImplementation) 

Now we can observe the notification and receive the corresponding event:

 NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerWillOpen:", name: "PlayerWillOpen", object: nil) func playerWillOpen(notification: NSNotification) { print("A Player will be opened now") } 

Best Notification Injection

Since AVPlayerView not deleted, but only freed, we will have to rewrite our code a bit and enter some notifications in AVPlayerViewController . Thus, we will have as many notifications as we want, for example: PlayerWillAppear and PlayerWillDisappear :

 let originalViewWillAppearMethod = class_getInstanceMethod(UIViewController.self, "viewWillAppear:") let originalViewWillAppearImplementation = method_getImplementation(originalViewWillAppearMethod) typealias ViewWillAppearCFunction = @convention(c) (UIViewController, Selector, Bool) -> Void let castedOriginalViewWillAppearImplementation = unsafeBitCast(originalViewWillAppearImplementation, ViewWillAppearCFunction.self) let newViewWillAppearImplementationBlock: @convention(block) (UIViewController!, Bool) -> Void = { (viewController: UIViewController!, animated: Bool) -> Void in castedOriginalViewWillAppearImplementation(viewController, "viewWillAppear:", animated) if viewController is AVPlayerViewController { NSNotificationCenter.defaultCenter().postNotificationName("PlayerWillAppear", object: nil) } } let newViewWillAppearImplementation = imp_implementationWithBlock(unsafeBitCast(newViewWillAppearImplementationBlock, AnyObject.self)) method_setImplementation(originalViewWillAppearMethod, newViewWillAppearImplementation) let originalViewWillDisappearMethod = class_getInstanceMethod(UIViewController.self, "viewWillDisappear:") let originalViewWillDisappearImplementation = method_getImplementation(originalViewWillDisappearMethod) typealias ViewWillDisappearCFunction = @convention(c) (UIViewController, Selector, Bool) -> Void let castedOriginalViewWillDisappearImplementation = unsafeBitCast(originalViewWillDisappearImplementation, ViewWillDisappearCFunction.self) let newViewWillDisappearImplementationBlock: @convention(block) (UIViewController!, Bool) -> Void = { (viewController: UIViewController!, animated: Bool) -> Void in castedOriginalViewWillDisappearImplementation(viewController, "viewWillDisappear:", animated) if viewController is AVPlayerViewController { NSNotificationCenter.defaultCenter().postNotificationName("PlayerWillDisappear", object: nil) } } let newViewWillDisappearImplementation = imp_implementationWithBlock(unsafeBitCast(newViewWillDisappearImplementationBlock, AnyObject.self)) method_setImplementation(originalViewWillDisappearMethod, newViewWillDisappearImplementation) 

Now we can observe these two notifications and go well:

  NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerWillAppear:", name: "PlayerWillAppear", object: nil) NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerWillDisappear:", name: "PlayerWillDisappear", object: nil) func playerWillAppear(notification: NSNotification) { print("A Player will be opened now") } func playerWillDisappear(notification: NSNotification) { print("A Player will be closed now") } 

Video url

I spent a couple of hours digging through some of the iOS Runtime headers to figure out where I can find the URL pointing to the video, but I could not find it. When I delved into some of the source files of WebKit itself, I had to refuse and admit that there is no easy way to do this, although I believe that it is hidden somewhere and can be achieved, but most likely only with great difficulty.

+9
source

I tried to add (add) some user script to WKWebView , this method is a bit safer than the swizzling method:

Here is the related code:

 let contentController = WKUserContentController() if let jsSource = NSBundle.mainBundle().URLForResource("video_play_messenger", withExtension: "js"), let jsSourceString = try? String(contentsOfURL: jsSource) { let userScript = WKUserScript(source: jsSourceString, injectionTime: .AtDocumentEnd, forMainFrameOnly: true) contentController.addUserScript(userScript) contentController.addScriptMessageHandler(self, name: "callbackHandler") } let webConfiguration = WKWebViewConfiguration() webConfiguration.userContentController = contentController webView = WKWebView(frame: CGRect.zero, configuration: webConfiguration) let request = NSURLRequest(URL: NSURL(string: "URL_FOR_VIDEO")!) webView.loadRequest(request) 

For the controller, WKWebView is compatible with WKScriptMessageHandler and we implement this method:

 func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { if message.name == "callbackHandler" { if let messageString = message.body as? String where messageString == "VideoIsPlaying" { // Vide is being played } } } 

Add video_play_messenger.js to the project:

 function videoTags() { return document.getElementsByTagName("video"); } function setupVideoPlayingHandler() { try { var videos = videoTags() for (var i = 0; i < videos.length; i++) { videos.item(i).onplaying = function() { webkit.messageHandlers.callbackHandler.postMessage("VideoIsPlaying"); } } } catch (error) { console.log(error); } } function setupVidePlayingListener() { // If we have video tags, setup onplaying handler if (videoTags().length > 0) { setupVideoPlayingHandler(); return } // Otherwise, wait for 100ms and check again. setTimeout(setupVidePlayingListener, 100); } setupVidePlayingListener(); 

Link: http://www.kinderas.com/technology/2014/6/15/wkwebview-and-javascript-in-ios-8-using-swift

+4
source

All Articles