Skip to content

feat(roku): add Roku platform support - #3

Open
namidan wants to merge 4 commits into
plexinc:mainfrom
namiml:feat/roku-support
Open

feat(roku): add Roku platform support#3
namidan wants to merge 4 commits into
plexinc:mainfrom
namiml:feat/roku-support

Conversation

@namidan

@namidan namidan commented Aug 22, 2026

Copy link
Copy Markdown

Proposed changes

copilot:summary

Adds Roku as a supported platform (--platform roku), following the integration patterns established by the tvOS and Vega ports. Contributed by teams Nami and rku.dev.

  • Driver (drivers/RokuDriver.kt + maestro/roku/): Roku devices are driven over the External Control Protocol (an HTTP API on device port 8060) — no on-device agent. View hierarchy comes from /query/app-ui (SceneGraph XML parsed into scene-absolute bounds), input is D-pad keypresses (tapOn sends Select; swipes/scrolls become directional presses), text is typed via character-by-character LIT_ keypresses, and screenshots go through the dev web server (digest auth). launchApp is a cold launch: an already-running channel is exited to the home screen first.
  • Device discovery: DeviceService.listRokuDevices() surfaces devices through the normal listing/selection path — a MAESTRO_ROKU_HOST pin (fast reachability probe) plus an opt-in SSDP LAN scan (MAESTRO_ROKU_DISCOVERY=true). No new global CLI flags and no TestCommand special-casing.
  • Platform wiring: Platform.ROKU, DeviceSpec.Roku, RokuLocale, session managers (CLI + MCP), pickers, start-device, AppValidator.
  • New KeyCodes Remote Info (the * options button), Remote Instant Replay, Remote Search — mapped on Roku and Android; Remote Menu maps to Roku's options button.
  • Studio TV mode auto-enables for Roku, like tvOS.
  • e2e fixture: a BrightScript demo channel (e2e/roku_demo_app/) mirroring the tvOS/Vega demo apps — same screens, labels, and testIDs (Home menu, 2×2 navigation grid, native-Keyboard text input, programmatic-focus test) — with three mirrored flows under e2e/workspaces/roku_demo_app/.
  • Docs: FORK.md platform entry (device setup, env vars, key names) and README feature bullet.

Notable device-behavior quirks handled (all reproduced on hardware): the dev server's multipart parser ignores form parts carrying a per-part Content-Length header (so the screenshot form is assembled by hand with an explicit digest handshake), the screenshot file is written asynchronously (the download polls its ETag against the pre-generation value), and ECP keypress path segments must be percent-encoded (a form-encoded space would type a literal +).

Testing

  • Unit tests (14, in maestro-client/src/test/java/maestro/roku/): app-ui parser (bounds math, translation offsets, RowListItem duplicate-drop, focus state), key mapping, SSDP response parsing, ECP path-segment encoding, and the dev server's digest challenge. ./gradlew :maestro-client:test --tests "maestro.roku.*"
  • On-device validation against a physical Roku Streaming Stick 4K (Roku OS 14) in developer mode: all three demo-channel flows pass end-to-end (maestro test --platform roku e2e/workspaces/roku_demo_app/), covering launch, all four D-pad directions, focus asserts (focused: true/false by id), programmatic focus, inputText with spaces/capitals, eraseText, Back-driven screen returns, and takeScreenshot (captures visually verified against the asserted screen state).
  • CLI smoke tests: list-devices shows a pinned Roku (and degrades gracefully in ~5s when the host is unreachable); start-device --platform roku without MAESTRO_ROKU_HOST produces an actionable error.
  • Full-project compile and the maestro-client/orchestra/cli/maestro-test suites pass (one pre-existing FlutterWebSemanticsIdentifierTest failure exists on main independently of this change).

Does this need e2e tests? Covered, but not in e2e/demo_app/ — the Flutter demo app can't run on Roku, so this PR follows the tvOS/Vega precedent instead: a dedicated fixture app (e2e/roku_demo_app/, BrightScript) plus flows in e2e/workspaces/roku_demo_app/, mirroring those apps' screens, labels, and testIDs. Flows require physical Roku hardware (there is no emulator), so they are not wired into CI — same as the Vega suite.

Issues fixed

None — new platform capability.

namidan and others added 4 commits August 22, 2026 15:35
Port the Roku driver from namiml/Maestro (commit ff38700) and integrate it
following the fork's Vega/tvOS platform patterns:

- ECP client, SSDP/env device discovery, app-ui parser, and key mapping live
  in maestro-client's maestro/roku/ package (no separate Gradle module)
- Devices surface through DeviceService.listRokuDevices() (MAESTRO_ROKU_HOST
  pin + opt-in MAESTRO_ROKU_DISCOVERY SSDP scan), so the standard
  --platform roku / --device <ip> selection works with no global CLI flags
  or TestCommand special-casing
- RokuDriver: D-pad input, LIT_ text entry, SceneGraph view hierarchy with
  scene-absolute bounds, digest-auth screenshots, app-ui-based settle waits
- New KeyCodes (Remote Info / Instant Replay / Search) mapped on Roku and
  Android; Remote Menu maps to Roku's * (options) button
- MCP session wiring and Studio TV mode auto-on for Roku
- Unit tests for the parser, key mapping, and SSDP parsing
- BrightScript demo channel (e2e/roku_demo_app) with navigation/focus flows
- FORK.md, README, and AGENTS.md entries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validated against a physical Roku Streaming Stick 4K (Roku OS 14). Three
fixes, each reproduced and verified on the device:

- launchApp is now a cold launch: ECP's /launch resumes an already-running
  channel with its state intact, so exit to the home screen first (Maestro's
  launchApp contract; matches VegaDriver's terminate-then-launch)
- Screenshot generation: the dev server's multipart parser silently ignores
  form parts carrying a per-part Content-Length header (which OkHttp's
  MultipartBody always adds) and also requires an empty 'archive' field —
  build the form body by hand and do the digest handshake explicitly with an
  empty-body probe, verifying the 'Screenshot ok' confirmation
- Screenshot download: poll /pkgs/dev.jpg's ETag against the pre-generation
  value (the capture file is written asynchronously) and cache-bust the URL,
  so a stale prior capture is never returned as the result

All three e2e demo-channel flows plus a screenshot flow pass against the
real device; roku unit tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebuild e2e/roku_demo_app to mirror the tvOS/Vega demo apps — same screens,
labels, and testIDs — and mirror all three flows:

- Home menu (menu-navigation / menu-text-input / menu-focus) opening one test
  screen at a time, with Back returning Home
- Navigation Test: 2x2 grid (grid-top-left/right/bottom-left/right) covering
  all four D-pad directions via an explicit focus-transition map
- Text Input Test: a native Keyboard node (text-field) focused on entry so
  inputText (ECP LIT_) and eraseText (Backspace) land in it directly, with a
  typed-label echoing the text
- Focus Test: programmatic focus (setFocus on entry) lands on focus-button-2

Driver fix shaken out by the text-input flow: ECP keypress path segments were
form-encoded, so a space became '+' and LIT_+ typed a literal plus — percent-
encode path segments instead ('Hello Roku' now types correctly).

Channel fix found on hardware: creating the hidden TextInputScreen's Keyboard
steals input focus after HomeScreen's init has claimed it, leaving D-pad keys
routed to the invisible keyboard — MainScene re-asserts the Home menu's focus
once all screens exist (focusDefault interface function).

All three mirrored flows pass against a physical Streaming Stick 4K; roku
unit tests (14) pass, including new coverage for the path-segment encoding
and the dev server's digest challenge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Credit the Roku platform contribution to the Nami team (nami.ml / rku.dev)
in the README feature list and the FORK.md platform entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant