Scriptable
Scriptable

Reputation: 19758

Play video from Apple TV Top Shelf

I have created a simple AppleTV project to display a number of videos by category, browsing and playing videos is working fine. It has been implemented as a client-server application using TVML and TVJS, so most of the application logic is in the Javascript files. These are statically generated in the background from dynamic content on a regular basis.

I've then added a TopShelf extension to the application which pulls in some featured videos from an API, this is also working fine, Pulls in the videos as expected.

The problem I am having is detecting and reacting to a user selecting a video from the top shelf. I have created a URL scheme exampletvapp:// which I have registered in my plist file.

I have also added the displayURL and playURL's to the TVContentItems. When selecting one of the videos, my app is launched as expected but does not actually handle the playback of that video.

In my AppDelegate file I have added print statements to print the launchOptions and also added a openUrl function, which does get called but only logs this if I close the application and select something from the top shelf quickly before the app closes and I lose the debug session.

// AppDelegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

    print("App Launch: \(launchOptions)")
    window = UIWindow(frame: UIScreen.mainScreen().bounds)

    let appControllerContext = TVApplicationControllerContext()

    guard let javaScriptURL = NSURL(string: AppDelegate.TVBootURL) else {
        fatalError("unable to create NSURL")
    }

    appControllerContext.javaScriptApplicationURL = javaScriptURL
    appControllerContext.launchOptions["BASEURL"] = AppDelegate.TVBaseURL

    if let options = launchOptions {
        for (kind, value) in options {
            if let kindStr = kind as? String {
                appControllerContext.launchOptions[kindStr] = value
            }
        }
    }

    appController = TVApplicationController(context: appControllerContext, window: self.window, delegate: self)

    return true
}

    func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {

        guard let javaScriptURL = NSURL(string: AppDelegate.TVBootURL) else {
            fatalError("unable to create NSURL")
        }

        let appControllerContext = TVApplicationControllerContext()
        appControllerContext.javaScriptApplicationURL = javaScriptURL
        appControllerContext.launchOptions["BASEURL"] = AppDelegate.TVBaseURL


        for (kind, value) in options {
            appControllerContext.launchOptions[kind] = value
        }

        appController = TVApplicationController(context: appControllerContext, window: self.window, delegate: self)

        return true
    }

    func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool {
        print("handle Url called")
    }

Javascript:

// Application.js
App.onLaunch = function(options) {
    var javascriptFiles = [
        `${options.BASEURL}js/ResourceLoader.js`,
        `${options.BASEURL}js/Presenter.js`
    ];

    console.log("options", options);

I have added a console.log to the JS file to check if the options are being passed over, but it's not logging anything. my intentions here was to create a Player object with a playlist of just the selected video from the TopShelf. but as the URL only seems to pass the ID over, It would mean that I need to make an ajax request to get the video information even though I had it in the TVContentItem already.

So my question is: Am I supposed to natively add the video from topshelf into a playlist and play it (with Swift) or pass the details on to the javascript side for it to play it. If it's the javascript is there a way to see the output from the console.log? As I dont see any which is making it quite difficult to work with.

UPDATE:

I've got a little further with this, I discovered (via the Apple Developer Forums) that you can attach Safari to the JS Context in the simulator for Apple TV via the Develop > Devices menu. I've done this and so far the URL is not being passed to the JS via the options. The url is passed to the openURL delegate function which needs to be either passed to the JS somehow or I need to natively play the video.

I'm currently investigating natively playing the video by passing all the required information in the displayURL to create a TVContentItem. I cannot see a way to pass this object to the JS from the openURL method unless I create a new context and instance of the controller (which may not even work)

Upvotes: 4

Views: 1459

Answers (2)

Bryan Scott
Bryan Scott

Reputation: 4737

I too just added a Top Shelf extension to my TVML-based app.

If you are using Apple's TVML Catalog example as a base, then the following should work in the AppDelegate to get the URL into the JavaScript context:

func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {

    if url.host == nil { return true }

    let urlString = url.absoluteString

    NSLog(urlString) // Log to XCode

    appController?.evaluateInJavaScriptContext({(evaluation: JSContext) -> Void in

        // Log to JS log (Safari)
        evaluation.evaluateScript("console.log('url string: \(urlString)')")

        // obviously we need to do something more interesting here


        },
        completion: {(Bool) -> Void in }

    )

    return true

}

My plan is to write a function in JavaScript that processes the URL and passes the results to my chosen TVML template. I have one for live shows and one for recordings, which are the two primary items I'm featuring in my Top Shelf extension.

Upvotes: 0

Scriptable
Scriptable

Reputation: 19758

I finally got this working natively. I did also manage to get it working by passing the openUrl to the JS but it seemed less reliable for me. I'll look further into that at a later point, I just needed this working for now.

The code below creates an AVPlayer and AVPlayerViewController and presents that ViewController and dismisses it when its finished. I haven't found any issues with this method so far, but I am still testing.

// AppDelegate.swift
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {

    let urlString = "http:\(url.path!)"
    let url = NSURL(string: urlString)!
    print("opening URL: \(url)")

    let item = AVPlayerItem(URL: url)

    // Need to listen to the end of the player so we can dismiss the AVPlayer
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerDidFinishPlaying:", name: AVPlayerItemDidPlayToEndTimeNotification, object: item)

    let player = AVPlayer(playerItem: item)
    player.actionAtItemEnd = AVPlayerActionAtItemEnd.None
    let playerVC = AVPlayerViewController()
    playerVC.player = player

    appController?.navigationController.presentViewController(playerVC, animated: true, completion: nil)
    player.play()

    return true
}

func playerDidFinishPlaying (note: NSNotification) {
    // Dimiss the AVPlayer View Controller from the Navigation Controller
    appController?.navigationController.dismissViewControllerAnimated(true, completion: nil)
}

Upvotes: 2

Related Questions