Repository navigation
Development
The project uses npm workspaces and Enact to share a single React codebase across every platform while keeping platform-specific code isolated.
moonfin/
├── packages/
│ ├── app/ # Shared application code
│ │ └── src/
│ │ ├── App/ # Main application component
│ │ ├── components/ # Reusable UI components
│ │ ├── context/ # React context providers
│ │ ├── hooks/ # Custom React hooks
│ │ ├── services/ # API and service modules
│ │ ├── views/ # Page components
│ │ ├── utils/ # Helpers and key handling
│ │ ├── theme/ # Theme loading and the injected theme stylesheet
│ │ └── styles/ # Global styles and variables
│ ├── platform-tizen/ # Samsung AVPlay, Smart Hub, Tizen storage
│ ├── platform-webos/ # Starfish/HTML5 video, Luna storage
│ ├── platform-vega/ # Fire TV WebView playback, the bridge to the React Native shell
│ ├── platform-xbox/ # The console player's video element, the bridge to the UWP host, controller keys
│ ├── build-tizen/ # Tizen build scripts -> .wgt
│ ├── build-webos/ # webOS build scripts -> .ipk
│ ├── build-vega/ # Vega build script and the React Native shell -> .vpkg
│ ├── build-xbox/ # Xbox build script, the UWP host, the device probe and the console helper -> .msix
│ └── cli-shim/ # Forwards enact and ares-* to the CLIs in tools/
├── tools/ # Build CLIs, installed by postinstall (git-ignored node_modules)
├── package.json # npm workspaces root
└── .eslintrc.js # Shared ESLint config
Shared code in packages/app/ never imports directly from @enact/webos, tizen.*, or webapis.*. Platform-specific behavior is reached through runtime detection instead:
import { isTizen, isWebOS, isVega, isXbox, getPlatform } from './platform';Services such as video, storage, and device profiles ask services/platformModule.js for their implementation, which dynamically imports it from platform-tizen/, platform-webos/, platform-vega/ or platform-xbox/ for the platform getPlatform() reports. Anything that touches a native API belongs in one of those packages, not in packages/app/.
- Enact/Sandstone provides the TV-optimized UI components and Spotlight focus navigation.
-
Tizen uses the AVPlay API for video and
tizen.tvinputdevicefor remote keys. -
webOS uses a shared HTML5 video element with the
audioTracksAPI, and Luna service calls for storage. -
Vega (Fire TV) runs the same web build inside the Vega WebView (Chromium 144), hosted by a small React Native shell in
packages/build-vega/shell/. The shell writes what it knows about the device intowindow.__MOONFIN_VEGA__before page code runs, forwards app state and keys asmoonfin:vegaevents, and the page talks back throughwindow.ReactNativeWebView.postMessage(seeplatform-vega/src/bridge.js). The WebView plays H.264, HEVC, VP9 and native HLS through the webOS player code, with no AV1, Dolby or DTS decoding, so those tracks are transcoded by the server. The page lives atfile:///pkg/assets, whose null origin cant start a Worker from a sibling file, so the build packs the subtitle workers as scripts andbuild-vega/inject/worker-shim.jsstarts them from blobs. A backgrounded app may keep no more than 150 MB and loses the decoder, so the player ends the session and leaves to the details page when the app goes to the background, and the shell can send the stop report itself from the session the page shares with it on every progress tick. The stick rasters on its GPU, so it keeps thehighperformance tier: Home rows, the TV keyboard and details pages all hold 60 fps there, and the whole app sits around 250 MB against Amazon's 420 MB foreground cap. A--debugbuild exposes Chrome DevTools on port 9229 (vega exec vda forward tcp:9229 tcp:9229), which is how those numbers were taken. -
Xbox runs the same web build in a WebView2 hosted by a small UWP app in
packages/build-xbox/shell/, served from the package athttp://moonfin.internal. The host writes what it knows about the console intowindow.__MOONFIN_XBOX__before page code runs, forwards app state, network changes and the media remote asmoonfin:xboxevents, and the page talks back throughwindow.chrome.webview.postMessage(seeplatform-xbox/src/bridge.js). The controller arrives as the gamepad's own key codes, whichplatform-xbox/src/keys.jsturns into the arrows, Enter and Escape the app knows. Video plays in the console's own player through FFmpegInteropX, behind a see-through WebView:platform-xbox/src/nativeVideo.jshands the player a real video element whose properties and events are routed over the bridge, so the shared webOS player code drives it unchanged and keeps drawing the controls and subtitles on top. The device probe underpackages/build-xbox/probe/reports what a console can do, andconsole.jsinstalls and drives a console through its Device Portal. - Packaged apps (
.wgt,.ipk,.vpkgand.msix) bypass CORS restrictions that apply to the browser dev server, except that the Vega WebView does send anullorigin, which stock Jellyfin and Emby accept. - Cross-origin cookies do not persist in either platform's webview, so API keys are used instead.
- UI changes should be tested on real TV hardware when possible. Focus behavior, performance, and video in particular do not fully reproduce in a desktop browser.
Behaviors that have each can cause a headache. Read them before your first change.
-
The oldest TVs run a 2014 browser. The build targets Chrome 38 and Safari 7 (
packages/app/.browserslistrc), which is what webOS 3 and the Tizen 2.4 WebKit are. Syntax gets transpiled and built-ins get polyfilled, but missing DOM features don't.KeyboardEvent.keydoesn't exist there, which is whyutils/keys.jsreadskeyCode. A grouped CSS rule with one selector those engines don't know, like:focus-within, is thrown out whole, so give such selectors a rule of their own. -
Themes can't use CSS variables. The build inlines every
var(--theme-x, fallback)to its fallback, and the old engines ignore custom properties set from JavaScript anyway. Themes reach the screen through the stylesheet thattheme/themeOverrides.jsinjects at runtime, so new themed UI adds its rules there. -
A retail Samsung TV can still be inspected.
sdb shellis locked down, but with the app closed,sdb shell 0 debug MoonfinApp.moonfinlaunches it with a debug port, andsdb forward tcp:9222 tcp:<port>opens that to Chrome DevTools atlocalhost:9222. The port changes every launch.
Moonfin is translated through Weblate. See the Smart-TV translation project rather than editing translation files by hand.
Using Moonfin
Going deeper
For developers