Below is a full‑length “survival manual” that collects everything I
ran into (and fixed) over roughly 70 hours, plus the official best‑practice
material people usually end up hunting for later. It is intended to be:
Table of contents
- Quick‐start (TL;DR build + run checklist)
- Runtime architecture & thread model
- Filesystem, paths & “missing PHY” errors
- SceneGraph (UI) in practice
- Networking patterns that don’t dead‑lock
- Video playback pipeline
- End‑to‑end control flow of our channel
- Debugging & profiling cheat‑sheet
- Packaging, signing & store submission
- Performance & memory tuning
- Common fatal errors and how we fixed them
- Hard‑won conventions for this code‑base
- Useful third‑party tools & links
1 Quick‑start
Step | Command / action | Notes |
---|---|---|
1. Install SDK environment | brew install zip curl (mac) or any zip + curl. | No official CLI; we zip+POST. |
2. Build | zip -qr dev.zip manifest components source locale images | Keep paths flat – Roku rejects nested zips. |
3. Sideload | curl -u roku:dev -F "mysubmit=Install" -F "archive=@dev.zip" http://<ROKU‑IP>/plugin_install | Enable Dev Mode on the box first. |
4. Attach debuggers | telnet <IP> 8085 (BrightScript) and telnet <IP> 8080 (SceneGraph). | Two separate REPLs. |
5. Reload after edits | Rerun steps 2 + 3 – the box auto‑reboots the channel. | About 2 s over LAN. |
2 Runtime architecture & thread model
main() -- BrightScript on App Thread
│
├── roSGScreen [render thread]
│ └── SceneGraph tree (UI nodes)
│
└── Task pool ( N worker threads )
└── your FetchTask / crypto / JSON parsing
- Only one render thread – any BrightScript that touches fields of an
roSGNode
must run there. - Tasks are full BrightScript interpreters without access to the
SceneGraph; communicate exclusively via observed fields or message
ports.
3 Filesystem & “missing PHY” warnings
Virtual path | Storage type | Lifetime | Usage |
---|---|---|---|
pkg:/ | Read‑only, in‑package | Forever | All resources you ship. |
tmp:/ | RAM (~512 MB) | Until power‑cycle | Cookie jars, downloaded thumbs. |
cachefs:/ | Flash, LRU | Persists reboots | Large data (images, manifests). |
Error we saw:
*** ERROR: Missing or invalid PHY: '/RokuOS/Artwork/SceneGraph/GenevaTheme/Base/HD/background.png'
- That URI exists only on developer firmware; retail units don’t bundle
Geneva. Fix: copy your ownbg.jpg
topkg:/images/
and reference
it in XML.
4 SceneGraph in practice
4.1 Component anatomy
<!-- /components/BrowseScene.xml -->
<component name="BrowseScene" extends="Scene">
<script type="text/brightscript" uri="pkg:/components/BrowseScene.brs"/>
<children>
<Rectangle id="bg" color="#222222ff" width="1280" height="720"/>
<PosterGrid id="grid"
translation="[96,96]"
itemSize="[320,180]"
itemSpacing="[16,16]"/>
</children>
</component>
' /components/BrowseScene.brs
sub init()
m.grid = m.top.findNode("grid")
fetchVideos() ' async
end sub
4.2 “Type mismatch” gotchas
translation
,itemSize
,itemSpacing
must be arrays in XML –[x,y]
, not"x,y"
.
4.3 Focus & navigation
PosterGrid
auto‑wraps on arrow keys.- After video finish we restored focus via:
sub cleanup()
m.video.control = "stop"
m.top.removeChild(m.video)
m.grid.jumpToItem(m.lastIdx)
m.grid.setFocus(true)
end sub
5 Networking patterns
5.1 Reusable FetchTask
' /components/FetchTask.brs
sub init()
m.top.functionName = "run"
end sub
function run() as void
req = CreateObject("roUrlTransfer")
req.Url = m.top.url
if m.top.cookieJar <> invalid then req.SetCookieJar(m.top.cookieJar)
rsp = req.GetToString()
m.top.result = rsp <> invalid ? ParseJSON(rsp) : { error: "network" }
end function
- Exposed fields:
url
(in),cookieJar?
(in),result
(out). - Observed on the Scene:
m.fetch = CreateObject("roSGNode","FetchTask")
m.fetch.url = BASE + "/viewer/videos"
m.fetch.observeField("result","onVideos")
m.fetch.control = "run"
5.2 Streaming URL fetch
- For each tile we call
/viewer/stream/:id
just‑in‑time when the user
presses OK. - Response shape:
{ url: "...m3u8", format: "hls" }
.
6 Video playback pipeline
sub play(item as object)
m.video = CreateObject("roSGNode","Video")
m.video.observeField("state","onVideoState")
m.video.observeField("errorMsg","onVideoError")
meta = CreateObject("roSGNode","ContentNode")
meta.title = item.title
meta.url = item.streamUrl
meta.streamformat = item.streamFormat ' "hls"
m.video.content = meta
m.video.width = 1280 : m.video.height = 720
m.top.appendChild(m.video)
m.video.control = "play"
end sub
Error diagnostics:
errorMsg="transport failure: ..."
→ expired signed URL.errorMsg="manifest load failed"
→ backend returned HTML, not M3U8.- To auto‑retry: call
/viewer/stream/:id
again and setvideo.content.url
on the fly.
7 End‑to‑end flow of our channel
main.brs
└─ LoginScene
└─ SignInTask → POST /viewer/login
sets global.cookieJar + global.token
⇒ on 200, scene.close=true
└─ BrowseScene
FetchTask → GET /viewer/videos
For each tile:
on OK →
FetchTask → GET /viewer/stream/:id
on result → play Video
└─ on Video.state=finished → destroy Video, jump focus back
8 Debugging & profiling
Action | Command | Typical output |
---|---|---|
Dump node tree | sgnodes all | Bounds, focus, loadStatus. |
Start network log | logrendezvous on | HTTP get to <url> took 34 ms . |
Measure allocations | sgperf start → user flows → sgperf report | create 612 + op 304 @ 0.0% rendezvous . |
Inspect BrightScript error | Telnet 8085 – stack trace auto‑printed. | runtime error &hec for invalid interface. |
9 Packaging & store submission
- Telnet to port 8080 →
package
(createspkg:/signed.pkg
). - Download via
http://<ip>/pkgs/signed.pkg
. - Upload to Roku Partner Dashboard.
- Mandatory assets:
- HD & FHD icons (540×405 / 1080×810).
- Splash screen ≤ 1 MB.
- Checklist that causes auto‑reject:
- Any HTTP (non‑TLS) media segment while video is playing.
- Crashes on Home/Back key spam.
- Unhandled
Dialog
input (must dismiss on Back).
10 Performance & memory
Symptom | Fix |
---|---|
Channel launch > 3 s | Convert PNG UI images to JPEG; postpone network calls until after first frame. |
Gradual FPS loss | Use sgperf ; ensure every create is matched by a removal. |
Texture cache overflow | Down‑scale thumbnails on server; 320×180 is plenty on HD. |
4K video stalls | Use bandwidth_saver=false in manifest, supply multiple Stream entries with different bitrates. |
11 Top fatal errors we hit & cures
Error | Root cause | Fix |
---|---|---|
runtime error &hec (“Dot Operator attempted with invalid…”) | Accessing a node that was never created or already removed. | Guard with if item <> invalid . |
EXIT_BRIGHTSCRIPT_CRASH after Video | Forgot to remove the Video node before scene closed. | m.top.removeChild(m.video) in cleanupVideo() . |
compile error &h02 | Emoji / smart quote in comment, or mismatched For /Next . | Strip non‑ASCII; run brightscript_warnings 999 . |
“Type mismatch occurred when setting ‘itemSize’” | Provided string "320,180" not array [320,180] . | Fix XML attribute syntax. |
12 House rules for this repo
- Keep every zip – we needed a dozen or more roll‑backs during the 161
iterations.
13 Handy external resources
- VS Code “Roku” extension – syntax highlight, sideload shortcut.
https://marketplace.visualstudio.com/items?itemName=KelvinGadson.vscode-brightscript-grammar - Roku Remote CLI – send keypresses from shell (
roku-remote press ok
).
https://github.com/lvcabral/roku-remote-cli - Stream Tester channel – sideloaded app that checks HLS/DASH streams
on‑device.
https://developer.roku.com/docs/developer-program/media-playback/stream-tester.md
The TL;DR takeaway
Roku development is 80 % learning the engine’s invisible rules and 20 %
writing BrightScript. This doc captures the invisible parts.
Keep it bookmarked; future You will thank current You when the next
mystery shows up at 2 A.M.