Thursday, April 30, 2020

Handling Authenticated Streams in Roku

Following on from my previous posts about Roku, it's possible (obviously) to access content which is "hidden" on password-protected sites.  Generally, these will provide a login process which will either give you a cookie or a token to be passed back as a header.  In Roku, you need to first simulate the login process, then attach the relevant data onto your Video player.

Because every situation is different and - for obvious reasons - I can't give a truly working example, I'm just going to present code snippets of how this is done.

Storing Credentials

I'm not going to discuss the UI of getting user credentials, but it's painful enough to do that you aren't going to want to ask your users to do this more than once.  You will then want to store them.

It seems the preferred approach to "storing" things on a Roku is to place them in the "Repository" which is a small amount of memory put aside for "your application" (technically, all applications published by the same publisher).  By "small amount", I mean 16K.  Now, I succesfully programmed a TRS-80 for three years with nothing more than 12K but by today's standards - particularly when you are talking about a streaming device that buffers - 16K is nothing.  But there it is.  It is certainly enough to track user credentials - and from there you could store everything off-premises.

There is an roRegistry object in Roku, but it seems that for our purposes you don't actually want that.  The Repository is divided into sections and the roRegistry is just there to enable you to remember which sections you created.  Instead, we want to go straight for an roRegistrySection which is where we can store individual key/value pairs.

We can create a section and store credentials so:
auth = CreateObject("roRegistrySection", "Authentication")auth.Write("username",username)auth.Write("password",password)
Here it is assumed that you have already done the work of obtaining the user's credentials and just want to store them.  Note that if the "Authentication" section already exists, this retrieves it and overwrites the fields.

In the same way, we can read the credentials back later, probably in a different part of the code:
auth = CreateObject("roRegistrySection", "Authentication")username = auth.Read("username")password = auth.Read("password")
Depending on how you structure your code, you can choose to share the roRegistrySection or not.

Creating an HTTP Agent

Internally, obviously, Roku does a lot of stuff with HTTP.  As we saw with the video example, you can create a "content node" with a URL, point a video player at it, and you will be streaming in seconds.

But what if you want to do more pedestrian, browser-like HTTP?  For that there are roHttpAgent and, in particular, roUrlTransfer objects.  An roUrlTransfer object is like an embedded browser, HttpClient in Java or XMLHttpRequest in Javascript.

In essence, it's very easy to use:
m.http = CreateObject("roUrlTransfer")m.http.SetUrl(fullUrl)res = m.http.GetToString()
which returns the body of the response as a string.

It's also possible to do an AJAX-like call by specifying AsyncGetToString():
m.http = CreateObject("roUrlTransfer")m.http.SetUrl(fullUrl)m.http.SetMessagePort(m.port)sent = m.http.AsyncGetToString()
This just returns a boolean to indicate whether it was able to initiate a transfer.  The important thing here is that, before sending the request, we attach the object to a "message port" - i.e. an event handler.  The system will then send this port an roUrlEvent when the response - success or failure - comes back.

We already have an event loop so we can just add code to it.  We end up with a structure something like this:
while(true)  msg = wait(0, m.port)  msgType = type(msg)  if msgType = "roUrlEvent"    body = msg.GetString()  end ifend while
Now, in order to do authentication, we (generally) need to do a POST rather than a GET and we need to enable cookies on the roUrlTransfer object.  For some reason, a synchronous POST operation is supported but it is impossible to extract the body.  Since we will probably want some portion of the body, we need to do asynchronous requests.  Updating our code from before, we can do this:
m.http = CreateObject("roUrlTransfer")m.http.SetUrl(postUrl)m.http.EnableCookies()m.http.SetMessagePort(m.port)m.http.AddHeader("Content-Type","application/x-www-form-urlencoded")sent = m.http.AsyncPostFromString(body)
Note that we have explicitly enabled cookies here and also specified a content type.  This latter is a concern of the server so you will need to check what it is expecting, but obviously it should match the format of the body that you are sending.

Cookies

If you actually want the cookies you can obtain them from the roUrlTransfer object while handling the response as follows:
m.cookies = m.http.GetCookies(domain,"/")
This returns an associative array of cookies which is somewhat tricky to manipulate but I'm sure you can figure it out (hint: you probably don't actually need any more than this).

XML

Your body may come back in many different forms (HTML, XML, JSON, ...).  Because mine came back as XML, I looked into that case and not any of the others.

You can parse XML by creating an XML element.  Note that the XML element is "stateful" in that you call parse() on the element and it updates its internal state.  This isn't that big a deal; it's just that it's not the way that parsers in Java and Javascript work; there you create a parser explicitly and it creates elements as it parses the text.
xp = CreateObject("roXMLElement")xp.Parse(body)
You can then traverse the body in much the way you would expect, although remember that this is an element not a document and thus it has the outermost name returned in the XML.  Thus when you call getNamedElements(name) the elements are the ones embedded in the document element.
attrs = xp.GetAttributes()name = xp.GetName()elts = xp.GetElementChildren()namedElts = xp.GetNamedElements("Tag")hasAtt = xp.HasAttribute("x")
Strangely, there isn't a GetAttribute method - I don't know why not.  But it's easy enough to write one:
function GetAttr(elt, attr)    attrs = elt.GetAttributes()    for each a in attrs        if a = attr           return attrs[a]        end if    end forend function
This returns the value "invalid" (like null in Java or undefined in Javascript) if it drops through to the bottom.

TLS

For secure websites, Roku documentation says you need to add the following magic.  I didn't investigate any of this, just did it.  If this doesn't work for you, there is documentation around this topic.
m.http.SetCertificatesFile("common:/certs/ca-bundle.crt")m.http.AddHeader("X-Roku-Reserved-Dev-Id", "")m.http.InitClientCertificates()

Sharing your Cookies

Having successfully logged in and recovered a token or cookie, you need to attach it to the video player.  Although, you don't actually attach it to the video player for reasons that become clear if you think about it long enough; instead, you attach them to the content node which has the secure URL in it.  Thus, if you have a content playlist, each one can come from a different secure website with its own cookies and headers.

The content node seems to be generally poorly documented in the Roku documentation.  Probably the best I've found is this buried in the video documentation.  Comment if you have a better reference.

Anyway, as written there, you can configure your content node from your roUrlTransfer object by just copying the fields:
m.content.url = urlToPlaym.content.HttpCookies = m.http.GetCookies(domain,"/")m.content.HttpCertificatesFile = "common:/certs/ca-bundle.crt"m.content.HttpHeaders = ["X-Roku-Reserved-Dev-Id:"]


Wednesday, April 29, 2020

A Working Video Application

Looking at these tools, I'm not quite sure where to start. As I often do, when forced into a corner I try and find a working example.  Roku offer a github repository of samples, one of which is a VideoExample.  Unfortunately, at least in my environment, the official sample doesn't work any better than mine - it starts and then hangs trying to load the video.  It doesn't help that their example has suffered software rot and references a URL that even curl can't load - although the problem is just that the server has been made secure in the meantime and the URL needs to be changed to reflect that.

There are a number of other problems with the sample, including only supporting SD resolution, requiring the Roku to switch to 720p - which my monitor does not support.

I went and looked at the access logs - the file is not being referenced.

Using wireshark seemed to show up nothing, but it can be a difficult tool to use and I'm not 100% sure I'm capturing all the traffic I need too.

So what else can we do?  Well, the debugger looks promising.

Debugging BrightScript

I don't really know what I'm doing, so I'm guessing based on the description in the previous post.

I run up the application and wait for it to hang.  While I'm doing this, it spits out a bunch of debug statements:

04-19 19:53:03.969 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0)
04-19 19:53:03.972 [beacon.signal] |AppCompileInitiate --------> TimeBase(3 ms)
04-19 19:53:03.973 [scrpt.cmpl] Compiling 'Hello World', id 'dev'
04-19 19:53:03.992 [scrpt.load.mkup] Loading markup dev 'Hello World'
04-19 19:53:03.999 [scrpt.unload.mkup] Unloading markup dev 'Hello World'
04-19 19:53:04.008 [scrpt.parse.mkup.time] Parsed markup dev 'Hello World' in 16 milliseconds

------ Compiling dev 'Hello World' ------
04-19 19:53:04.028 [scrpt.ctx.cmpl.time] Compiled 'Hello World', id 'dev' in 13 milliseconds
04-19 19:53:04.041 [scrpt.proc.mkup.time] Processed markup dev 'Hello World' in 9 milliseconds
04-19 19:53:04.046 [beacon.signal] |AppCompileComplete --------> Duration(74 ms), 2.14 KiP
04-19 19:53:04.084 [ui.frm.plugin.running.enter] Entering PLUGIN_RUNNING for dev
04-19 19:53:04.086 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0)
04-19 19:53:06.727 [scrpt.ctx.run.enter] UI: Entering 'Hello World', id 'dev'

------ Running dev 'Hello World' main ------
in showChannelSGScreen

OK, well, so far, so good.  We have the message telling us it is setting up the screen.  But then ... nothing.

The instructions say to press "Ctrl C", so I do that and am rewarded with a slew of information and then a debugger prompt.  Excellent.

^C
BrightScript Micro Debugger.
Enter any BrightScript statement, debug commands, or HELP.

Suspending threads...
Thread selected:  0*   pkg:/source/Main.brs(20)                msg = wait(0, m.port)

Current Function:
012:      m.port = CreateObject("roMessagePort")
013:      screen.setMessagePort(m.port)
014:
015:      'Create a scene and load /components/helloworld.xml'
016:      scene = screen.CreateScene("HelloWorld")
017:      screen.show()
018:
019:      while(true)
020:*         msg = wait(0, m.port)
021:          msgType = type(msg)
022:          if msgType = "roSGScreenEvent"
023:              if msg.isScreenClosed() then return
024:          end if
Break in 20
020:         msg = wait(0, m.port)
Backtrace:
#0  Function main() As Void
   file/line: pkg:/source/Main.brs(20)
Local Variables:
global           Interface:ifGlobal
m                roAssociativeArray refcnt=2 count:1
screen           roSGScreen refcnt=1
scene            roSGNode:HelloWorld refcnt=1
msg              <uninitialized>
msgtype          <uninitialized>
Threads:
ID    Location                                Source Code
 0*   pkg:/source/Main.brs(20)                msg = wait(0, m.port)
  *selected

Brightscript Debugger>

What I think I want to do is dig into the video component, but how to find it?  There's a command bsc which lists all the active components.  Let's try that.

Brightscript Debugger> bsc
                 roGlobal refcnt=1  [<roGlobal>]
       roAssociativeArray refcnt=2  [{port:<roMessagePort>}]
               roSGScreen refcnt=1  [<roSGScreen>]
                   roList refcnt=1  [L[]]
            roMessagePort refcnt=2  [<roMessagePort>]
      roSGNode:HelloWorld refcnt=1  [<roSGNode>]
6 component(s) total.

On second thoughts, I'm intimidated by that.  I'm not quite sure what do with that.  Let's try another tack.  The listing above showed me the variables that are in scope, so let's try looking into those.  The scene seems like the root of everything good, so let's print that and see what we get.

Brightscript Debugger> print scene
<Component: roSGNode:PlayVideo> =
{
    backExitsScene: true
    backgroundColor: 589505535
    backgroundUri: ""
    currentDesignResolution: <Component: roAssociativeArray>
    dialog: <Component: roInvalid>
    limitBackgroundToUIResolution: true
    childRenderOrder: "last"
    clippingRect: <Component: roAssociativeArray>
    enableRenderTracking: false
    inheritParentOpacity: true
    inheritParentTransform: true
    muteAudioGuide: false
    opacity: 1
    renderPass: 0
    renderTracking: "disabled"
    rotation: 0
    scale: <Component: roArray>
    scaleRotateCenter: <Component: roArray>
    translation: <Component: roArray>
    visible: true
    change: <Component: roAssociativeArray>
    focusable: true
    focusedChild: <Component: roSGNode:Video>
    id: ""
}

I feel this should have a list of children but I'm not really seeing that.  But there is a focusedChild which is the Video component, so that seems like it might be worth further investigation.

Brightscript Debugger> print scene.focusedChild
<Component: roSGNode:Video> =
{
    allowOptionsKeyOverride: false
    alwaysShowVideoPlanes: false
    audioFormat: ""
    audioTrack: ""
    autoPlayAfterSeek: true
    availableAudioTracks: <Component: roArray>
    availableSubtitleTracks: <Component: roArray>
    bifDisplay: <Component: roSGNode:BifDisplay>
    bufferingBar: <Component: roSGNode:ProgressBar>
    bufferingBarVisibilityAuto: true
    bufferingBarVisibilityBounds: <Component: roAssociativeArray>
    bufferingBarVisibilityHint: false
    bufferingStatus: invalid
    bufferingTextColor: 0
    capFontPath: ""
    capFontSize: 0
    captionStyle: invalid
    cgms: "norestriction"
    clipId: 0
    completedStreamInfo: invalid
    content: <Component: roInvalid>
    contentBlocked: false
    contentIndex: -1
    contentIsPlaylist: false
    control: "play"
    currentAudioTrack: ""
    currentSubtitleTrack: ""
    disableScreenSaver: false
    downloadedSegment: invalid
    duration: 0
    enableScreenSaverWhilePlaying: false
    enableTrickPlay: true
    enableUI: true
    errorCode: -4
    errorInfo: <Component: roAssociativeArray>
    errorMsg: "No streams were provided for playback."
    errorStr: "player agent play"
    globalCaptionMode: "Instant replay"
    height: 0
    licenseStatus: invalid
    loop: false
    manifestData: invalid
    maxVideoDecodeResolution: <Component: roArray>
    mute: false
    nextContentIndex: -1
    notificationInterval: 0.5
    pauseBufferEnd: 0
    pauseBufferOverflow: false
    pauseBufferPosition: 0
    pauseBufferStart: 0
    position: 0
    retrievingBar: <Component: roSGNode:ProgressBar>
    retrievingBarVisibilityAuto: true
    retrievingBarVisibilityBounds: <Component: roAssociativeArray>
    retrievingBarVisibilityHint: true
    retrievingTextColor: 0
    seek: 0
    seekClip: invalid
    state: "error"
    streamInfo: invalid
    streamingSegment: invalid
    subtitleTrack: ""
    supplementaryAudioVolume: 50
    suppressCaptions: false
    thumbnailTiles: invalid
    timedMetaData: invalid
    timedMetaData2: invalid
    timedMetaDataSelectionKeys: <Component: roArray>
    timeToStartStreaming: 0
    tracks: <Component: roArray>
    trickPlayBackground: <Component: roInvalid>
    trickPlayBar: <Component: roSGNode:TrickPlayBar>
    trickPlayBarVisibilityAuto: true
    trickPlayBarVisibilityHint: false
    videoFormat: ""
    width: 0
    childRenderOrder: "last"
    clippingRect: <Component: roAssociativeArray>
    enableRenderTracking: false
    inheritParentOpacity: true
    inheritParentTransform: true
    muteAudioGuide: false
    opacity: 1
    renderPass: 0
    renderTracking: "disabled"
    rotation: 0
    scale: <Component: roArray>
    scaleRotateCenter: <Component: roArray>
    translation: <Component: roArray>
    visible: true
    change: <Component: roAssociativeArray>
    focusable: true
    focusedChild: <Component: roSGNode:Video>
    id: "playit"
}

I don't know about you, but what jumps out to me in this are the error fields:

    errorCode: -4
    errorInfo: <Component: roAssociativeArray>
    errorMsg: "No streams were provided for playback."
    errorStr: "player agent play"

No streams were provided for playback?  What?  Were you not looking?  Oh, and by the way, thanks for hiding a fairly important error message deep in your object hierarchy rather than printing it out on the console where we can all benefit ...

One Day Later

Not really knowing what else to do, I walked away.  But I kept turning this over in my mind and from time to time googling this and that as they occurred to me.

I checked my model number and OS level in the Settings/System/About screen to make sure that everything I thought was supported really was and, yes, I'm on the latest version of the OS and my model - a 2710X - is still supported.

The console stream - while not telling me about the small matter of there not being any streams - did keep telling me that my streamformat wasn't valid.  I couldn't - and still can't - figure out why that would be.  But it made me start to think about XML and types.  In XML everything is an element or a string.  But that doesn't mean that the two are interchangeable.

And it occurred to me that although I was setting the content to what seemed like a reasonable value, that the Roku might be expecting an element rather than a string.  Comparing what I'd done to what the sample application had done, I realized that they had done a lot more in their component's init() method, possibly for a good reason.

I reworked my playvideo.xml as follows:
<?xml version="1.0" encoding="utf-8" ?><component name="PlayVideo" extends="Scene">     <children>      <ContentNode id="damian" url="http://www.gmmapowell.com/downloads/Damian2018.mp4" StreamFormat="mp4"      />      <Video id="playit"         control="play"      width="0"       height="0"       />    </children><script type="text/brightscript" ><![CDATA[  function init()    video = m.top.findNode("playit")    video.content = m.top.findNode("damian")    video.setFocus(true)    video.control = "play"  end function]]></script></component>
This still creates the two components using XML, but connects them using BrightScript and "finding" the content node to attach it to the video node.

What do you know? It works.

Summary

Debugging a Roku application is much like debugging anything else - it's equal parts tools, investigation and inspiration.  This problem was solved by finding out what the problem was using the debugger, then applying research and inspiration to understand what could be the issue and then fixing it.

The result is the ability to what some quality Chiefs' rugby

Debugging a Roku Application

Given that our code didn't work first time, how do we go about debugging it?

Eclipse integration

In the console view in Eclipse, there's an option to view the Roku console.  When this is selected, it is necessary to enter the IP address of the Roku device.  Then the console connects and displays all the information being logged.  I have had issues with this "disconnecting" and then it seems to be hard to convince it to reconnect.

The Roku issues a series of debug messages as it loads, compiles and starts applications.  These all appear in the console.  All the messages from the translation and compilation process, including any errors and warnings, are also copied onto the console.

Print Statements

For logging purposes, it is possible to just use BrightScript print statements.  These are automatically channeled into the console stream and will show up in Eclipse.

Debugger

The Roku box also offers an embedded debugger which operates on port 8085.  This is automatically chanelled through the Console View and there is a debugging console provided.

I haven't yet found any way to set an explicit breakpoint in the code1, but from the console it is possible to type CTRL-C at any point to interrupt the flow.  The debugger then offers a series of commands that can be used to investigate what's going on.

Check Access Logs

With regard to our specific problem, it's possible to consider the server end and see if the file has been accessed from the server.

Wireshark

Because this is clearly a network problem, we need to try and debug the communications traffic.  The Roku guide actually has a whole page on the topic.  This discusses SSL decryption in some detail; but my MP4 is not on a secure site so it should be possible to debug this without having to decrypt the SSL packets.

Summary

There are a range of tools for debugging Roku applications.  Let's see if we can apply these to our problems.

It turns out you don't add breakpoints as such, but insert STOP into your code, much like using debugger in JavaScript

Playing a Video on Roku

Time to move on to a new project.  We're going to start with a simple goal: can we play a random video I happen to have on a server?  My reading through the manual suggests it should be quite easy to play a video located on an internet server providing it isn't behind any kind of security.

Creating a Project

The first step is to create a new project using the Eclipse new project wizard.  This done, we need to add some poster and splash screen images.  The poster images are three different resolutions of the image that appears in the channel view.  The splash screen images are the ones that appear when the application launches.

Based on the Hello, World sample, we can see that the sizes are as follows:

SD (low resolution)
poster 214x144
splash 720x480

HD (high definition)
poster 290x218
splash 1280x720

FHD (full high definition)
poster 540x405
splash 1920x1080

Presumably at some point a 4K resolution will be added.

I'm not really a graphical person, so I just took an image I already had and stretched it this way and that in order to "make it fit", not worrying too much about the aspect ratios.  I created these and put them in a new "images" folder under the project root.  In the Hello, World sample, the posters are PNG and the splash screens are JPG.  I'm not sure if there's a good reason for that, but I copied it anyway1.

I then opened the manifest in Eclipse and used the "visual" mode to select these images.

The Main Script

Looking at the Hello, World sample again, I don't really see a lot different between the Main code there and what I (think I) want to do.  So I'm just going to copy it over into my project, delete some of the random bits and change the Scene name from "HelloWorld" to "PlayVideo" which is what I'm going to call my component.

The Scene

Under components, I'm going to create a new XML file called playvideo.xml which is going to define the layout for the screen.  This is obviously based on the helloworld.xml example, but it is going to differ considerably more than the Main.brs script.

As I understand it, the component I really want to use is Video.  Reading the documentation, it seems to me that this, in turn, depends on being provided with some "content".  So we need to create two nodes, one to identify the content and one to actually configure a video player.  Biting it all off in one go, we end up with this:
<component name="PlayVideo" extends="Scene">
    <children>
        <ContentNode id="damian"
            url="http://www.gmmapowell.com/downloads/Damian2018.mp4"
            streamFormat="mp4"
        />
        <Video id="playit"
            content="damian"
            control="play"
            width="0"
            height="0"
        />
    </children>
As before, we have a main component that extends a Scene.  Within that, we have two nodes: the first is the content node; the other is the video node.

The content node needs an id so that the video node can reference it.  The other two fields identify the video and the stream format.

The video node has an id, which I'm not using yet, along with a reference to the content node.  The document states that specifying width and height as 0 will cause the video to take up the entire screen.  It's not clear what other properties need to be set, so I specified the control field to be play, since I want the video to play.

The script portion is a lot simpler:
function init()
  m.top.setFocus(true)
end function
This just simply sets the focus.

Good News and Bad News

On the upside, when I tried exporting this, I saw the splash screen and then it moved on to a video play screen and said it was downloading the video.

Unfortunately, it made no further progress.  It would seem that it is struggling to download the video.  But why?

Something is obviously wrong.  Now comes the big question: how do I figure out what is wrong?  Is there a debugger?  Logging?

Summary

Starting from scratch, I was able to build up a video player project that reached 90% of the goals I had for it - except actually working.


1 Further investigation revealed that there is no apparent reason for this. Both PNG and JPG work fine.

Hello, Goodbye ...

Now that we have understood how the simple "hello, world" app works, it's time to try and mix things up a little by changing something and seeing if we can put our understanding to good use.

So instead of welcoming the world with a nice greeting, we will say goodbye instead.

OK, not too taxing, but the main purpose is to understand that we have a complete deployment cycle working and redeploying actually works.

Changing Hello, World

The only change I'm going to make is on line 5 of helloworld.xml, where I'm going to change the text from "Hello, World" to the equally classic "Goodbye cruel world".  And then redeploy from Eclipse.


That was easy.

Summary

We are indeed on top of this and ready to press on to bigger and better things.

Unpacking "Hello World" for Roku

Having downloaded the hello world zip file and set up Eclipse for Roku development, I'm eager to try something.

So I'm going to try doing "Hello, World" for myself - and then we'll try and tinker with some things.

Creating a Project

The Eclipse plugin comes with a New Project Wizard, so let's see that at work.

I can certainly find the wizard - it's right at the top of the list.  When I open it, there are a lot of options here that I don't understand, but that's not too surprising.

I've unpacked the zip file to a directory, so I point the project there and fill in as few fields as I can manage.  It's a bit disconcerting that I see a number of errors in the console window where I'm running eclipse - including a stack overflow - but it seems to take them all in its stride.


It offers me the "BrightScript" perspective, which seems reasonable enough, so I take that.  It has some "Roku Remote" on the right hand side; we will have to dig into that later.  On the left hand side, I have the normal project explorer, so let's see if we can open some files.


There's a manifest which has its own editor - nice.  There's a makefile that confuses me, mainly because of its simplicity.  There's a single file under Source - "Main.brs" - which presumably does something; I had been considering that the simplest app might just be a splash screen.  There's an XML configuration for a component which seems to be most of the work.  And then there appear to be two images, each at three different resolutions.

Deploying a Project

Per the documentation, the plugin has the ability to deploy a project to the Roku box.  To do this, an option has been added to the "File>Export..." menu.  It's necessary to fill out a form with relevant information - such as IP address and developer password on the Roku box.  Push "Finish" and ...  it fails.  Sadly, it would seem, because of timeout error.  It does tell you how to fix this, but let's just try again.  This time it works.  Great!



Going back to the Roku box, everything seems to work, but I have to admit that it's hard to tell.  We'll need to make a change and redeploy.  But that's for another time.  For now, let's dig into the code that we've got here.

Main.brs

First off, here is the code:
sub Main()
    print "in showChannelSGScreen"

    'Indicate this is a Roku SceneGraph application
    screen = CreateObject("roSGScreen")
    m.port = CreateObject("roMessagePort")
    screen.setMessagePort(m.port)

    'Create a scene and load /components/helloworld.xml
    scene = screen.CreateScene("HelloWorld")
    screen.show()

    while(true)
        msg = wait(0, m.port)
        msgType = type(msg)
        if msgType = "roSGScreenEvent"
            if msg.isScreenClosed() then return
        end if
    end while
end sub
I'm not sure how much detail to go into, so I'm going to assume that either you are familiar with BASIC-like languages or are smart enough to guess at the syntax.  So leaving that aside, together with the debugging statements and comments, this falls into two sections.  The first section is the setup, which is basically as follows:
  • create a new screen object
  • create a new "message port"
  • connect the message port to the screen
  • populate the screen with the "hello, world" scene (as investigated in the next section)
  • make the screen "object" project onto the actual (TV) screen
The "ro" prefix appears to be common to all the components and presumably means "Roku" rather than, say, "read only".

Then the remaining code is a simple event loop which basically just goes around and around until it sees a "close screen" message at which point it exits.  I'm not quite sure what triggers that event; I form the impression from reading the documentation that an application doesn't receive any notification if, for example, the "Home" button is pressed; and certainly the Roku box wouldn't want to be held to ransom by you failing to process the "close screen" event.

One final mysterious item before we carry on: the message port is attached to an object called "m" for some reason.  The actual reason why this done in this code is mysterious to me, but presumably it is just derived from "normal style".  In any case, the variable m is the "this" variable in BrightScript.

helloworld.xml

As with many other programming environments (Android for example), Roku offers an XML alternative to building up a scene using "boilerplate" code.  These components are processed as the application is started and put into a "library" from which they can be accessed, as by "CreateScene" above.

The XML comes with an embedded BrightScript script, but for now we will just consider the actual XML portion:
<?xml version="1.0" encoding="utf-8" ?>
<component name="HelloWorld" extends="Scene">
  <children>
    <Label id="myLabel"
      text="Hello World!"
      width="1280"
      height="720"
      horizAlign="center"
      vertAlign="center"
    />
  </children>
<!-- BrightScript Portion Elided -->
</component>
This is designed to create a "component" which appears to be much the same as a "class" in an object-oriented language.  It defines the name of the component ("HelloWorld") which is the name by which it will be referenced in the component library.  Because this is intended to be a scene, it extends the Scene component.  As with almost all modern UI toolkits, the UI elements form an object (in this case component) hierarchy.

Inside this, it creates a simple Label, showing the desired text.  The width and height seem like random numbers, but presumably correspond to the most common screen resolution (i.e. full HD).  The screen size can be determined from the device info.

The script

Inside this component, there is the ability to embed a script.  As the component is transpiled from XML to BRS, this script appears to be inserted into it; presumably this means you can add anything you want, but the key thing is the ability to add an init method which is called when the component is created.  It is my understanding that this script is executed in the scope of the component and thus has access to things that elements outside the script do not (although there does not seem to be the notion of private and public members per se).

Here is the hello, world script:
function init()
  m.top.setFocus(true)
  m.myLabel = m.top.findNode("myLabel")

  'Set the font size
  m.myLabel.font.size=92

  'Set the color to light blue
  m.myLabel.color="0x72D7EEFF"
end function
As discussed before, m refers to the current component - that is, the HelloWorld component.  top refers to the top of the embedded hierarchy and findNode traverses the hierarchy to find a component with the given id.  I think most of the rest of the code is self-explanatory (with the exception of the color constant which I think is RGBA).

Summary

So far, so good.  I think I've understood (and possibly even explained) the basic operation of the "hello, world" Roku application.

So now let's try and do something a little different.

Roku Tools

I was originally going to use this page to describe all the tools that I discovered, but it turns out that all I really need is the Eclipse plugin.

I use Eclipse for most of my Java development (and VSCode for JavaScript), so if there's an Eclipse plugin on offer, I'm going to take it.

The installation instructions seem fairly clear and obvious.  It suggests using a new workspace which seems a bit extreme, but I generally create a new installation of Eclipse before installing a plugin I've not used before, so I'll take their advice.

Unfortunately, as often seems to happen with Eclipse plugins, there are "unresolved dependencies" and, for reasons beyond my ken, Eclipse cannot handle these the way that Linux packaging tools, or Maven, or Ruby can: you are left to your own devices.

Fortunately, a quick google showed up this thread, which included a comment that was spot on for me: you need to install the dynamic languages toolkit first.  I'm not sure how much of this is actually required, and indeed not all of it would install for me (I'm using a fresh install of 2020-03) but between what would install and the Roku plugin, everything worked.

The Roku plugin contains the usual source editor that you'd expect.  It also includes the ability to export your applications to the device, monitor and debug them.  All of these features will be covered as we try and develop an application to play videos in the next few posts.

Unit Testing

I also found a unit test library, which says it is modelled after JUnit 5.  However, because it seems to be impossible to literally run Roku programs "inside" eclipse, this feels hard to use.  I will probably circle around to it next time I am working with Roku and will probably blog about it then.

Roku Remote

To save you the hassle of having a remote on your desk, you can use the built-in Roku Remote view in Eclipse.

Before this will work, you need to add your device.  Unfortunately, in the default perspective, you can't see all the controls to add the device.  Make it take up the whole screen, configure it, and then bring it back down to gadget size.

As it happens, I found the actual remote on my desk to be more reliable.

Stream Testing

Roku also offer a "Stream Testing" tool.  This enables you to connect to a stream and see if it will play successfully on your Roku.

This is embedded in the Eclipse plug-in, but in order to enable it you need to (one time) install the appropriate channel on your Roku box.  To do this, you need to open the stream tester in a browser and click on the gear towards the top right corner and then click on the big box link that appears and then follow the instructions to install a new channel.

It's then possible to use the Stream Tester either in the browser or in Eclipse.  To use it in Eclipse, select it from the "Roku > Web Tools" menu.

You will first need to connect it to your device using its IP address.  You can then specify the video URL and press Play.  The Roku should open the Stream Testing Channel and display your content.

Options

On the "General" tab, you can select the "Debug Video HUD" which shows banners at the top and bottom of the screen, telling you which files it is loading and playing, along with metadata about bit rates, subtitles and the like.

On the Cookies tab, you can provide cookies that you have established elsewhere to access protected content.

On the Headers tab, you can provide any headers that you need to access the content.

There is also a DRM tab which supports certain forms of DRM encryption if you know what you're doing (I don't).

Deploying with a Script

Deploying using the Eclipse Export functionality gets very old, very quickly.  If you don't want to do this, you can use the Makefile that they provide with various samples (it may just work out of the box; I haven't used make in 20 years and I wasn't about to go back).  But I can still read Makefiles, so I dug through the various scripts and reconstructed what it is actually doing which is a zip followed by a curl.  This is what I ended up with:

#!/bin/bash

if [ -z "$PASSWORD" ] ; then
  echo "No PASSWORD"
  exit 1
fi

rm $ZIP
zip -ro $ZIP *
curl --silent --user rokudev:$PASSWORD --digest -F mysubmit=Install -Farchive=@$ZIP http://$ROKUIP/plugin_install

This really does want a password (so that you aren't saving it in a script) but the $ROKUIP and $ZIP could be variables or you could replace them with hardcoded versions.  This script assumes it is running in the root directory of the project and that the ZIP file is going somewhere else.

I then connected this up as an "External Tool" in Eclipse (and passed PASSWORD in as an env var) so that I could push a single button and be deployed.

Summary

The Eclipse plugin is effective but flaky.  If you use Eclipse, you should definitely get it.


Developing Roku Apps

I've had Roku gadgets for a long while now - since they first came out, I think - but their numbering system is so opaque that I currently own a "Roku 1" which is the third generation of Roku boxes that I have personally owned, and something like the eighth overall.

As a developer, I naturally ask myself "what would it take to build one of these apps?"  When I first looked, the answer seemed to be "send us a lot of money and buy a special development machine and we'll see".  Sorry, but I'm not that interested.  (Apologies if I misunderstood and this was never the policy).

But having just moved house, cut the cord and now being completely dependent on streaming services, the incentive to understand it has gone up a notch.  A roku device is - or can be - a pleasure to use, whereas controlling a television from a computer can be a lot of hassle - even with a remote.  Moreover, as we are now in a smaller place, we only have one TV and I have a spare device to play with!

Getting Started

So I looked into this again yesterday and it would seem you can put any modern Roku device into "developer" mode, although I almost fell about laughing at the sequence you need to use on the remote in order to make it happen.

But it works, just like the instructions say.  After noting the IP address, I was able to create my password, reboot and connect over WiFi.  (If you ever do lose the address it's available under Settings and System Information on the About tab, along with other useful information such as your model number and OS Level).


Now to try loading a sample app ... Hmm, hello world looks good.  But there isn't even a README.

Digging around on the website, this article has the relevant set of instructions, so let's try that.  In section 3 it has a link to the ZIP file we need, so we can download that, and see what happens.


Wow! Look at that!  Greeted by my own TV!

What Else Have I Learnt?

Obviously I've done a lot more googling than I have described here and learnt a lot of things.  Hopefully these next few blog posts will impart a taste of that knowledge.  But for now some teasers.

It would seem that Roku devices a programmed in a proprietary language called "BrightScript". Obviously, this language is designed around making it easy, safe and secure to develop applications for Roku devices.  Sadly, it looks a lot like Visual Basic with COM: the language is incredibly like the BASIC I learnt 40 years ago; and the Interface and Component model looks very much like COM.

Applications are written purely in script and then assembled into a ZIP file for sideloading or distributing.  There is no separate "compile" step (although there is presumably a JIT step on the device).

It would seem the process of uploading applications can be automated rather than depending on using a web browser.  The documentation references using a makefile for this purpose but presumably curl or any build tool could do the job.

Leafing quickly through the documentation, there seem to be other tools to round out the experience, such a scene designer and a profiler, but I haven't dug any deeper than that.

There are development plugins for Eclipse, Sublime and Atom, although what they do I have no idea.

What Next?

For me, the big questions are about the development cycle:
  • How easily can I incorporate this into my existing build tools?
  • Can I do everything on a Mac?  Or do I need to deal with some Windows box?
  • How do I track down errors?
  • Is there a unit testing library?
  • What other tools are in the toolset and how do they work?

Contents

This is the first in a series of blog posts.  The others (in order) are:
I did not have an entirely smooth journey into the land of Roku development.  While I won't burden you with all the details of everything I struggled with, I have tried to indicate the problems I ran into and how I solved them.

Wednesday, April 1, 2020

I just upgraded to Java 11 this weekend

I upgraded to Java 11 this weekend.  I'm neither proud not bitter about this per se, but I am bitter about the way in which I was forced into it and the incomplete way in which I ended up doing it.

To be clear, upgrading past Java 8 has been on my list for a while.  However, any number of constraints (Android, AWS Lambda) have been holding me back.  Late last year, Lambda started supporting Java 11 and migrating to that officially went onto my "to do" list.

But I ended up upgrading this weekend in a somewhat awkward halfway-house way because of a number of incompatible bugs in different versions of Java.

The Background

I have an application which has a Java server and both a Java and JavaScript client.  The Java client is supposed to operate on both desktops and Android phones, but leaving aside the fact that Android does not support Java 11 yet, the Android aspect of this is not relevant to this article.

Obviously, being me, I need to test both of these "end-to-end".  And I don't want to do that with any kind of wrapper and framework that takes forever to set up and ends up being very flaky.

So the best bet seemed to run a Web Client inside a Java runtime (the only alternative I seriously considered was to use ChromeDriver to run the tests against a Chrome instance).  In the end, I selected the UI4J library as being the simplest thing to use.

The UI4J library is really just a wrapper around a JavaFX WebView, which is built in to Java 8, but has been packaged as OpenJFX after the Java 9 breakup.  In retrospect, I'm not sure how much it buys me given that it's an extra moving part in this scenario, but upgrading was painful enough without deciding to rearchitect at the same time.

My client and server connect using websockets.  JavaFX supports websockets, although as with everything else in its JavaScript implementation, it can be very difficult to diagnose problems with them.  But I had code that was working and successfully connecting if I ran it in a browser; it just wasn't connecting in my test case.

The Problems

The first problem I had to solve was the websocket not connecting.  Googling for this on the internet revealed that WebSocket support quietly broke between Java8 u202 and u211.  I was running u211.  OK, so I haven't upgraded for a while.  I felt I'd tried at some point and had issues, but that was then.  I downloaded u241 and tried that.

It turned out to be worse.  I'm not quite sure why, but I ran into run-time link errors with UI4J.  Apparently, somebody thought it would be a good idea to change part of the JavaFX API between u211 and u241 - or else UI4J was using an undocumented interface in JavaFX that changed, and they didn't release a revised version that "fixed" that.

No other Java 8 versions are available for download.  Bite the bullet and upgrade!

I downloaded a Java 11 JDK package on my Mac and tried to restart eclipse.  It wouldn't.   What?  I set JAVA_HOME back to Java 8 and tried again.  It still died due to this issue.  I don't know why this is so all-pervading that having this dylib anywhere causes such heartache but it does.  Fortunately, deleting the entire package solved the problem and I was able to go back to Java 8 and re-run Eclipse.

OK, what now?

I downloaded a ".tar.gz" version of the same thing and didn't put it in a central place but just unpacked it inside "Downloads/".  Needless to say, MacOS wasn't impressed by this trick and I spent a lot of time dealing with security warnings.  But having done that, I was able to start Eclipse with Java 8 and configure it internally to have a Java 11 runtime (from Downloads(!)) and only end up with about 250 errors.

The problem, of course, being that JavaFX was no longer included in the Java runtime, so I had to go and find all of the appropriate libraries (JavaFX itself is broken up into modules) - and also upgrade to UI4J version 4.0.

Along the way, it turned out I needed to upgrade a few other packages (BouncyCastle, for instance, needed to go from 1.60 to 1.64) to be compatible with the module requirements.  And I had some SSL and KeyTool code that need upgrading because I'd written it to use internal Sun APIs.

But by the end of the weekend, I'd worked my way through all those problems and my WebSocket successfully connected and I was able to go back to doing TDD with a working test harness.

The Solution

So there I am.  For the time being (at least until they release the next version) I am still "running" Java 8, but Eclipse is the only thing that uses that.  Inside Eclipse, I am configured for language compatibility with Java 8 (to stop myself drifting from an Android-compatible build) while building my projects with a Java 11 JDK and OpenJFX 11 and UI4J 4.0.  Jenkins had no real problems upgrading and is running under (and using) the Linux version of the Java 11 JDK,  And my AWS Lambdas are now using the "java11" runtime.

Update

I spoke too soon.  It turns out that there are bugs in the (it would seem new) implementation of TLS 1.3 in Java 11.  I haven't found the specific bug I encountered described online, but suffice it to say that it took a long time to track down, mainly because I assumed it was a bug in my code.

I have a stress test and, as part of my standard automated build, I run a "sanity" version of the stress test - client and server in one executable, five client threads and ten client sessions.  It runs for about 10-20s and goes through some standard operations.

After the upgrade, it would hang.  After diagnosing a number of timeout issues, it became clear that it was hanging because the server would respond to one request, the client would receive the response but fail to make the next connection.  However, the server responding threads would never terminate while something was chewing up 1000% of CPU (the joys of having 16 cores!).

It turns out that the problem was that the SSLEngine implementation for TLS 1.3 has some kind of issue with multiple threads and goes into some kind of infinite loop and never quite closes a connection - or possibly can't open a new one.  As more threads try and access the SSLEngine, they seem to get sucked in too.

While I wasn't able to isolate this enough to produce a test case to file a bug, I was able to track it down on my machine by pausing all the threads.  As I released individual threads that were "in" SSLEngineImpl, each of them took up a whole processor and my CPU usage jumped to 100%, 200%, 300% ...

The fix is quite easy - and described by any number of articles discussing similar problems - turn off TLS 1.3.  Exactly how you do this depends on what communications infrastructure you are using; I am using Grizzly servers and Apache HTTP clients and this worked for me:
-Djdk.tls.client.protocols=TLSv1.2
Note that because I am running both sides of the connection in a single application, I am able to be very clear that I want to use 1.2.  To specify "NOT 1.3" you need to allow all the other options.