Created
May 3, 2016 06:13
Revisions
-
einfallstoll created this gist
May 3, 2016 .There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,166 @@ # How to detect a movie being played in a WKWebView? I'd like to know wether it's possible to detect a movie being played in the WKWebView? Additionally I'd like to know the exact URL of the opened stream? # Answer Since the solution(s) to this question required a lot of research and different approaches, I'd like to document it here for others to follow my thoughts. *If you're just interested in the final solution, look for some fancy headings.* The app I started with, was pretty simple. It's a Single-View Application that imports `WebKit` and opens a `WKWebView` with some `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 (kind of) protected by JavaScript. *I really haven't seen the video yet, it was just the first I discovered.* Remember to add `NSAppTransportSecurity` and `NSAllowsArbitraryLoads` to your `Info.plist` or you will see a blank page. ## WKNavigationDelegate The `WKNavigationDelegate` won't notify you about a video being played. So setting `webView.navigationDelegate = self` and implementing the protocol won't bring you the desired results. ## NSNotificationCenter I assumed that there must be an event like `SomeVideoPlayerDidOpen`. Unfortunately there wasn't any, but it might have a `SomeViewDidOpen` event, so I started inspecting the view hierarchy: UIWindow UIWindow WKWebView WKScrollView ... ... UIWindow UIWindow UIView AVPlayerView UITransitionView UIView UIView UIView ... UIView ... AVTouchIgnoringView ... As expected there will be an additional `UIWindow` added which *might* have an event and hell yes it does have! I extended `viewDidAppear:` by adding a new observer: NSNotificationCenter.defaultCenter().addObserver(self, selector: "windowDidBecomeVisible:", name: UIWindowDidBecomeVisibleNotification, object: nil) And added the corresponding 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 the view hierarchy as we inspected earlier. But unfortunately it seems like the `AVPlayerView` will be created later. If you trust your application that the only `UIWindow` it'll open is the media player, you're finished at this point. But this solution wouldn't let me sleep at night, so let's go deeper... ## Injecting An Event We need to get notified about the `AVPlayerView` being added to this nameless `UIView`. It seems pretty obvious that `AVPlayerView` must be a subclass of `UIView` but since it's not officially documented by Apple I checked the [iOS Runtime Headers for `AVPlayerView`](https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/AVKit.framework/AVPlayerView.h) and it definitely *is* a `UIView`. Now that we know that `AVPlayerView` is a subclass of `UIView` it will probably added to the nameless `UIView` by calling `addSubview:`. So we'd have to get notified about a view that was added. Unfortunately `UIView` doesn't provide an event for this to be observed. But it *does* call a method called `didAddSubview:` which could be very handy. So let's check wether a `AVPlayerView` will be added somewhere in our application and 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") } ### Better notification injection Since the `AVPlayerView` won't get removed but only deallocated we'll have to rewrite our code a little bit and inject some notifications to the `AVPlayerViewController`. That way we'll have as many notifications as we want, e.g.: `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 are good to go: 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") } ## Final Solution I spent a couple of hours digging some iOS Runtime Headers to guess where I could find the URL pointing to the video. At some point I was really sick at that "try and error" thing so I decided to inject the `init` method of `NSObject` and print out the classname. **Unfortunately this is a work in progress, that isn't really in progress anymore. I guess I've mentioned lots of aspects that'll answer the original question, but aren't really handy at all.**