Skip to content

Development

Axl Nunez edited this page Oct 9, 2026 · 5 revisions

Development

The project uses npm workspaces and Enact to share a single React codebase across every platform while keeping platform-specific code isolated.

Project structure

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

Platform abstraction

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/.

Developer notes

  • Enact/Sandstone provides the TV-optimized UI components and Spotlight focus navigation.
  • Tizen uses the AVPlay API for video and tizen.tvinputdevice for remote keys.
  • webOS uses a shared HTML5 video element with the audioTracks API, 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 into window.__MOONFIN_VEGA__ before page code runs, forwards app state and keys as moonfin:vega events, and the page talks back through window.ReactNativeWebView.postMessage (see platform-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 at file:///pkg/assets, whose null origin cant start a Worker from a sibling file, so the build packs the subtitle workers as scripts and build-vega/inject/worker-shim.js starts 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 the high performance 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 --debug build 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 at http://moonfin.internal. The host writes what it knows about the console into window.__MOONFIN_XBOX__ before page code runs, forwards app state, network changes and the media remote as moonfin:xbox events, and the page talks back through window.chrome.webview.postMessage (see platform-xbox/src/bridge.js). The controller arrives as the gamepad's own key codes, which platform-xbox/src/keys.js turns 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.js hands 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 under packages/build-xbox/probe/ reports what a console can do, and console.js installs and drives a console through its Device Portal.
  • Packaged apps (.wgt, .ipk, .vpkg and .msix) bypass CORS restrictions that apply to the browser dev server, except that the Vega WebView does send a null origin, 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.

Things that will save you some time

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.key doesn't exist there, which is why utils/keys.js reads keyCode. 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 that theme/themeOverrides.js injects at runtime, so new themed UI adds its rules there.
  • A retail Samsung TV can still be inspected. sdb shell is locked down, but with the app closed, sdb shell 0 debug MoonfinApp.moonfin launches it with a debug port, and sdb forward tcp:9222 tcp:<port> opens that to Chrome DevTools at localhost:9222. The port changes every launch.

Translations

Moonfin is translated through Weblate. See the Smart-TV translation project rather than editing translation files by hand.

Clone this wiki locally