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

  1. Quick‐start (TL;DR build + run checklist)
  2. Runtime architecture & thread model
  3. Filesystem, paths & “missing PHY” errors
  4. SceneGraph (UI) in practice
  5. Networking patterns that don’t dead‑lock
  6. Video playback pipeline
  7. End‑to‑end control flow of our channel
  8. Debugging & profiling cheat‑sheet
  9. Packaging, signing & store submission
  10. Performance & memory tuning
  11. Common fatal errors and how we fixed them
  12. Hard‑won conventions for this code‑base
  13. Useful third‑party tools & links

1  Quick‑start

StepCommand / actionNotes
1. Install SDK environmentbrew install zip curl (mac) or any zip + curl.No official CLI; we zip+POST.
2. Buildzip -qr dev.zip manifest components source locale imagesKeep paths flat – Roku rejects nested zips.
3. Sideloadcurl -u roku:dev -F "mysubmit=Install" -F "archive=@dev.zip" http://<ROKU‑IP>/plugin_installEnable Dev Mode on the box first.
4. Attach debuggerstelnet <IP> 8085 (BrightScript) and telnet <IP> 8080 (SceneGraph).Two separate REPLs.
5. Reload after editsRerun 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 pathStorage typeLifetimeUsage
pkg:/Read‑only, in‑packageForeverAll resources you ship.
tmp:/RAM (~512 MB)Until power‑cycleCookie jars, downloaded thumbs.
cachefs:/Flash, LRUPersists rebootsLarge 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 own bg.jpg to pkg:/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 set
    video.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

ActionCommandTypical output
Dump node treesgnodes allBounds, focus, loadStatus.
Start network loglogrendezvous onHTTP get to <url> took 34 ms.
Measure allocationssgperf start → user flows → sgperf reportcreate 612 + op 304 @ 0.0% rendezvous.
Inspect BrightScript errorTelnet 8085 – stack trace auto‑printed.runtime error &hec for invalid interface.

9  Packaging & store submission

  1. Telnet to port 8080 → package (creates pkg:/signed.pkg).
  2. Download via http://<ip>/pkgs/signed.pkg.
  3. Upload to Roku Partner Dashboard.
  4. Mandatory assets:
    • HD & FHD icons (540×405 / 1080×810).
    • Splash screen ≤ 1 MB.
  5. 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

SymptomFix
Channel launch > 3 sConvert PNG UI images to JPEG; postpone network calls until after first frame.
Gradual FPS lossUse sgperf; ensure every create is matched by a removal.
Texture cache overflowDown‑scale thumbnails on server; 320×180 is plenty on HD.
4K video stallsUse bandwidth_saver=false in manifest, supply multiple Stream entries with different bitrates.

11  Top fatal errors we hit & cures

ErrorRoot causeFix
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 VideoForgot to remove the Video node before scene closed.m.top.removeChild(m.video) in cleanupVideo().
compile error &h02Emoji / 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

  1. Keep every zip – we needed a dozen or more roll‑backs during the 161
    iterations.

13  Handy external resources


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.

Leave a Reply

Your email address will not be published. Required fields are marked *