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) andtelnet <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 anroSGNodemust 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.jpgtopkg:/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,- itemSpacingmust be arrays in XML –- [x,y], not- "x,y".
4.3 Focus & navigation
- PosterGridauto‑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/:idjust‑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/:idagain and setvideo.content.urlon 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 &hecfor 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 Dialoginput (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 everycreateis matched by a removal. | 
| Texture cache overflow | Down‑scale thumbnails on server; 320×180 is plenty on HD. | 
| 4K video stalls | Use bandwidth_saver=falsein manifest, supply multipleStreamentries 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_CRASHafter Video | Forgot to remove the Videonode before scene closed. | m.top.removeChild(m.video)incleanupVideo(). | 
| 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.
