My channel is using a RowList with itemComponentName="CItem" as given below, (based on CItem.xml from some Roku sample code.)
I am seeing on my channel dashboard, that rarely, but consistently, my channel crashes on line 70 of this xml file (about once for every 100 hours played). Unfortunately, the Roku dashboard does not provide any error message, only the line number. I have not been able to reproduce the issue on my own development setup.
The line that crashes is always m.Poster.height = height , and it is odd because the immediately preceding line m.Poster.width = width never crashes, and just preceding the two is a check that both variables height and width are strictly positive.
This issue has been nagging me for more than a year, and I have made the code increasingly defensive, to the point that you see now. Does anyone have an idea what could be the issue?
<?xml version="1.0" encoding="utf-8" ?> <!-- Copyright 2016 Roku Corp. All Rights Reserved. --> <component name="CItem" extends="Group"> <children> <Poster id="poster" loadDisplayMode="scaleToZoom"/> <Rectangle id="overlay" color="#FFFFFF" opacity="0.6" visible="false"/> <Poster id="overlayicon" loadDisplayMode="scaleToZoom" uri="pkg:/images/dk-flag201x150.png" width="201" height="150" visible="false"/> <Label id="label" font="font:SmallSystemFont" text="" color="#383838" maxLines="3" wrap="true" horizAlign="left" /> <Rectangle id="progress" color="#FF001E" opacity="1"/> <Rectangle id="progressbg" color="#C9C9C9" opacity="1"/> <!-- <Label id="description" font="font:SmallBoldSystemFont" text="PLACEHOLDER" /> <Label id="sublabel" font="font:SmallBoldSystemFont" text="PLACEHOLDER" /> --> </children> <interface> <field id="width" type="float" onChange="updateLayout"/> <field id="height" type="float" onChange="updateLayout"/> <field id="itemContent" type="node" onChange="itemContentChanged" /> <field id="labelColor" type="string" alias="label.color"/> </interface> <script type="text/brightscript"> <![CDATA[ sub Init() m.Poster = m.top.findNode("poster") m.Overlay = m.top.findNode("overlay") m.OverlayIcon = m.top.findNode("overlayicon") m.Label = m.top.findNode("label") m.Progress = m.top.findNode("progress") m.ProgressBg = m.top.findNode("progressbg") end sub sub itemContentChanged() m.Label.text = m.top.itemcontent.Title m.Overlay.visible = m.top.itemcontent.showRestrictedToDenmark m.OverlayIcon.visible = m.top.itemcontent.showRestrictedToDenmark if m.top.itemContent.Length > 0 and m.top.itemContent.lastPositionSec > 0 then m.Progress.height = 9 m.ProgressBg.height = 9 else m.Progress.height = 0 m.ProgressBg.height = 0 end if updateLayout() m.Poster.uri = m.top.itemContent.sdposterurl end sub sub updateLayout() height = m.top.height width = m.top.width if height > 0 And width > 0 then m.Poster.width = width m.Poster.height = height <==== This is where the channel crashes m.Overlay.width = width m.Overlay.height = height m.OverlayIcon.translation = [(width - 201) / 2, (height - 150) / 2] m.Label.width = width - 18 m.Label.translation = [9, height + 15] m.Progress.translation = [0, height - m.Progress.height] if m.top.itemContent <> invalid and m.top.itemContent.Length > 0 and m.top.itemContent.lastPositionSec > 0 then m.Progress.width = m.top.itemContent.lastPositionSec / m.top.itemContent.Length * m.top.width end if m.ProgressBg.translation = [m.Progress.width, height - m.Progress.height] m.ProgressBg.width = width - m.Progress.width end if end sub ]]> </script> </component>
<field id="width" type="float" onChange="updateLayout"/>
<field id="height" type="float" onChange="updateLayout"/>
Now you will have a problem. Both width and height lead to the same place. That place must know when width is sent that height will not be populated yet. You never set a "default" of 0 in the above.
You either have to train that "updateLayout" (subroutine) to know when width is sent that height is coming next and always set width before setting height.
....or do like below and it will work the way you have it allowing width and height to be set in any order because you have this...
if height > 0 And width > 0 then
since you did the above it allows setting them in any order but you forgot this part below...
<field id="width" type="float" value="0" alwaysNotify="true" onChange="updateLayout"/>
<field id="height" type="float" value="0" alwaysNotify="true" onChange="updateLayout"/>
Then it will work like you think it should. Use "alwaysNotify" to make sure that if you have changed Uri and the width/height are the same it still updates your progress indicators.
The line that errors is actually this line:
if height > 0 And width > 0 then
height will initially be invalid because you set width first. If you set height first width will be initially invalid. Once again because no "default" value was set. When you set a default value in the field it will prevent the leaking of invalid into your evaluations.
Thank you for your input. However, I believe that something more is going on, let me explain.
As far as I can tell, "float" properties of SGNodes default to 0, if no default value is set, and they cannot take any "invalid" value, only numerical values.
I confirmed the first part, by inserting the following line just above the first if statement in updateLayout:
print "CItem.updateLayout("; width; ", "; height; ")"
And then I get output like this:
CItem.updateLayout( 0 , 0 ) CItem.updateLayout( 384 , 0 ) CItem.updateLayout( 384 , 576 )
So there we can see that the properties default to zero, even in the absence of an explicit default in the XML.
Also, according to your explanation, I would expect to see error every time, at it will always be the case that one of the two properties is set first, causing an invocation of updateLayout() with the other having the default value. Yet, I see no error in the vast majority of cases, in fact, I have never seen the error occur while developing, only though Roku's dashboard.
I do not believe that the if statement is what is generating an error, as I trust Roku's line number indication in the dashboard, though that points oddly at the height assignment.
Your comment about updating progress is interesting, but as far as I can tell, I get an invocation of itemContentChanged, in case any property of the content node is changed, which covers progress (also in cases where width and height are not even re-set to same value.)
Sub UpdateLayout() If m.top.height > 0 and m.top.width > 0 Then ' Only update on change If m.top.height <> m.wasHeight Or m.top.width <> m.wasWidth Then m.wasHeight = m.top.height m.wasWidth = m.top.width ChangeLayout(m.top.width, m.top.height) End If End if End Sub
Sub ChangeLayout(width,height)
... rest of your code here using width and height as if they are pre-populated ...
End Sub
Do your changes inside the ChangeLayout. Then everything should work as expected and you can pass them off as expected values.
If you still have the problem (inside ChangeLayout) after this it has to be something else in your code and the problem shows itself here but it really is endemic of a problem somewhere else.
I am sure it must have relation to something elsewhere in the code. I am searching my brain though, for how it could possibly happen that one assignment of m.Poster.width with a positive value can succeed, but at the very next line, assignment of m.Poster.height also with a positive value could possibly fail.
The only ways I can see, is that if the Poster object does not have a height property (not possibe), that something has re-assigned the m.Poster variable to refer to another object (multithreading), or that the Poster object has been garbage collected, (though that seems unlikely as the enclosing CItem must have a reference to keep the Poster alive.)
None of the above really makes sense to me, though, which is why I am asking here.
I ran a "monkey test", that is, using ECP to simulate an endless sequence of random keypresses, and saw something interesting, copy below.
The first lines are ordinary output from my channel, "Exiting channel" being printed from appMain.brs, as it leaves its event loop, and will terminate.
A number of interesting observations.
I hope that someone can educate me about how to avoid the render thread continuing to execute code after the application main thread having already exited.
As you can see in appMain.brs below, my scene object uses a property "appExit" to signal that the channel should close, as alternative to msg.isScreenClosed(). Based on my other code, I believe that the appExit property was the trigger in this case. Though if my memory serves, I have observed similar crashes in CItem.xml before I introduced the appExit property.
Trace:/tmp/components/VideoScene.brs:2089 Trace:/tmp/components/VideoScene.brs:2094 Trace:/tmp/components/VideoScene.brs:2268 Exiting channel 04-07 22:55:51.241 [beacon.signal] |AppExitInitiate -----------> TimeBase(25292 ms) 04-07 22:55:51.253 [beacon.signal] |AppExitComplete -----------> Duration(19 ms), 287.53 KiP 04-07 22:55:51.254 [beacon.header] __________________________________________ [bightScript Mi0c4-r0o D7e b2u2g:g5e5:5r1.2.54 Enter any BrigeachotSncrip.tre psotatremetn]t, |ApdpLaeubnucgh IcnoimtmiaantdesB ----o-r- -HE-L-P> T.ime Sauses(pen0din g thmreads.s..) 04-07 22:55:51.254 [beacon.report] |AppLaunchComplete ---------> Duration(2166 ms), 224.29 KiP Thread selected: 0* pkg:/components/CItem.xml(51) m.Label.text = m.top.itemcontent.Title 04-07 22:55:51.254 [beacon.report] |AppLoadInitiate -----------> TimeBase(762 ms) 04-07 22:55:51.255 [beacon.report] |AppSplashInitiate ---------> TimeBase(1 ms) 04-07 22:55:51.255 [beacon.report] |AppSplashComplete ---------> Duration(664 ms) 04-07 22:55:51.255 [beacon.report] |AppExitInitiate -----------> TimeBase(25292 ms) 04-07 22:55:51.255 [beacon.report] |AppExitComplete -C-urr-e-n-t- -F-u-n-c-t>i oanu:r tion(19 ms), 287.53 KiP 050: 04-07 22:55:51.255 [beacon.footer] __________________________________________ sub itemContentChanged() 051:* m.Label.text = m.top.itemcontent.Title 052: m.Overlay.visible = m.top.itemcontent.showRestrictedToDenmark 053: m.OverlayIcon.visible = m.top.itemcontent.showRestrictedToDenmark 054: if m.top.itemContent.Length > 0 and m.top.itemContent.lastPositionSec > 0 then 055: m.Progress.height = 9 Source Digest(s): pkg: dev 2.6.64 e9bdf8abe4162a3ea604682342620430 DRTV, Danish National TV Invalid value for left-side of expression. (runtime error &he4) in pkg:/components/CItem.xml(51) Backtrace: #0 Function itemcontentchanged() As Void file/line: pkg:/components/CItem.xml(52) Local Variables: global Interface:ifGlobal m roAssociativeArray refcnt=2 count:2 Threads: ID Location Source Code 0* pkg:/components/CItem.xml(51) m.Label.text = m.top.itemcontent.Title *selected Brightscript Debugger> 04-07 22:55:54.389 [ui.frm.plugin.running.enter] Entering PLUGIN_RUNNING for dev 04-07 22:55:54.390 [beacon.signal] |AppLaunchInitiate ---------> TimeBase(0 ms) Brightscript Debugger> print m <Component: roAssociativeArray> = { global: <Component: roSGNode:Node> top: <Component: roSGNode:CItem> }
Brightscript Debugger> print m.top.itemcontent.Title
Den tynde blå linje | Fiktionsserie | 1 SÆSON
appMain.brs
sub Main(input as Dynamic) print "############################" print "# Start of DR TV Channel #" print "############################" m.port = CreateObject("roMessagePort") screen = CreateObject("roSGScreen") screen.setMessagePort(m.port) roInput = CreateObject("roInput") roInput.SetMessagePort(m.port) m.global = screen.getGlobalNode() m.global.id = "GlobalNode" m.global.addFields({"experiments": input.experiments}) scene = screen.CreateScene("VideoScene") ' Add deep linking support here. Input is an associative array containing ' parameters that the client defines. Examples include "options, contentID, etc." ' See guide here: https://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide ' For example, if a user clicks on an ad for a movie that your app provides, ' you will have mapped that movie to a contentID and you can parse that ID ' out from the input parameter here. ' Call the service provider API to look up ' the content details, or right data from feed for id if input <> invalid then print "input: "; input if input.reason <> invalid then if input.reason = "ad" then print "Channel launched from ad click" 'do ad stuff here end if end if if input.contentID <> invalid then print "contentID is: " + input.contentID scene.commandInput = input else scene.commandInput = input end if end if 'Deep link params screen.show() scene.observeField("appExit", m.port) continue = true while (continue) msg = wait(0, m.port) msgType = type(msg) if msgType = "Invalid" then else if msgType = "roSGScreenEvent" then print "Exiting channel" if msg.isScreenClosed() then continue = false else if msgType = "roSGNodeEvent" then print "Exiting channel" if msg.getField() = "appExit" then continue = false else if msgType = "roInputEvent" then if msg.IsInput() then inputData = msg.GetInfo() print "input: "; inputData if inputData.DoesExist("mediaType") and inputData.DoesExist("contentID") then deeplink = { contentId: inputData.contentID mediaType: inputData.mediaType } print "got input deeplink= "; deeplink scene.commandInput = deeplink end if end if else print "Unrecognized event: " + msgType end if end while end sub
I forgot to mention two other observations in the previous post.
My "monkey test" script is designed to recognize the "Exiting channel" message, and upon seeing that message, it invokes the ECP command http://...:8060/launch/dev, in order to start the channel again, and continue the test.
In this particular instance, my script got a 5xx HTTP code from the ECP command, which along with the garbled log output, suggests to me that this behavior could in part be a bug or shortcoming in the Roku OS itself.
Also, as I typed "quit" in the debugging prompt above, I saw my channel continuing to initialize, for a while making me unsure whether I was debugging the "old" instance in the process of closing, or the "new" instance in the process of initializing. The fact that there was no main thread, together with the fact that the object had actual string data originating from my server, convinces me that I was debugging the "old" instance in some zombie form, though.