Skip to content

Instantly share code, notes, and snippets.

@einfallstoll
Created May 3, 2016 06:13

Revisions

  1. einfallstoll created this gist May 3, 2016.
    166 changes: 166 additions & 0 deletions tutorial.markdown
    Original 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.**