NewNOVA is on Android: the Windows player on your phone, updating itself.Get the APKWindows 0.2.0

ADR 0007: NOVA on Android and iPhone

Expo and React Native with system engines now, libmpv later; licensing per store.

As writtenPublished unedited from the NOVA repository. Links to other repository files open on GitHub.Engineering record · docs/adr/0007-mobile-architecture.md

  • Status: Proposed, 2026-10-18. The UI stack is accepted for the prototype. The engine choice is pending M2 measurements.
  • Scope: phones and tablets. The Windows app, its updater and its release flow are unchanged.
  • Not legal advice. The licensing sections need counsel before any store submission.

Context: what exists and what can move

An audit of main (3731b03) sorted the code by where it can go:

AreaTodayPortable to mobile?
nova-domain (queue, resume policy)Pure RustYes, logic only. It needs FFI (UniFFI) to reach Swift/Kotlin, or a TS port.
nova-application (queue/progress services over ports)Pure RustYes, same as above
nova-playback-api (contract and state reducer)Pure RustYes. It is the reference for the mobile contract (apps/mobile/src/playback/types.ts).
nova-storage (SQLite, media identity via file hashing)Rust + rusqlitePartly. Identity hashing assumes paths and random file access. iOS security-scoped URLs and Android SAF content:// URIs need platform adapters.
nova-library (folder scanner)Walks directoriesNo on iOS: there is no folder scanning outside the app sandbox or user-picked folders. On Android it would have to be rewritten on top of SAF tree URIs.
nova-playback-mpvLoads libmpv-2.dll at runtime, renders through wid into a Win32 child windowNo. This is a Windows binary and Windows window embedding. The FFI shape and the event loop are reusable ideas, not reusable code.
apps/desktop/src-tauri (z-order, previews, updater)Tauri 2 + Win32No. The updater model (signed latest.json and self-replacement) is not allowed on either app store.
apps/desktop/src (React UI)WebView over native videoNo as a UI. Pure modules port: seek policy, actions, time formatting.
Brand, tokens, icon familyCSS variables, SVGYes, as tokens and paths (done: apps/mobile/src/design/tokens.ts, components/icons.tsx).

The shipped Windows engine is a GPL build (ADR 0003, option A). No part of that binary can ship on mobile.

Decision 1 — app stack: Expo + React Native, with native engine modules

Two realistic options:

A. Expo/React Native UI on both platforms, with native playback modules (Swift, Kotlin)B. Expo for prototypes only, then native apps (SwiftUI + Jetpack Compose)
Owner previews on an iPhone without an Apple accountYes (Expo Go now; development builds later)Only the throwaway prototype
Design consistencyOne implementation of NOVA’s componentsTwo implementations to keep identical by hand
Platform feelNative stack, native modals, system pickers, VideoView; gestures and animation on the UI thread (RNGH, Reanimated)Best possible
Codec reach and seek precisionSet by the engine module, not the UI layer. Same as B.Same as A
Native code neededEngine modules, video surface, media session, PiP glueEverything
MaintenanceOne UI codebase, plus two engine modulesThree UI codebases (Windows, iOS, Android)
RiskRN rendering of the timeline or player may fall short of the quality bar. Mitigation: the player surface can become a native view component without rewriting the app.Cost and drift between platforms

Chosen: A.

  • Expo’s Modules API lets each engine live in Swift or Kotlin behind one TS contract, so nothing playback-critical runs in JavaScript.
  • Revisit at M3: if real-device review shows the RN player surface is not good enough, move the player screen (only) to a native view.

Decision 2 — engines: system engine plus libmpv, behind one contract (pending M2 data)

Candidates per platform:

iOS: AVPlayer (AVFoundation)iOS: libmpv (e.g. MPVKit)Android: Media3 ExoPlayerAndroid: libmpv (as in mpv-android)
ContainersMP4/MOV/M4V, HLS. No MKV/WebM/AVI.Everything FFmpeg demuxesMP4, MKV, WebM, TS, FLV, OGG…Everything FFmpeg demuxes
Video codecsH.264, HEVC (incl. 10-bit, Dolby Vision), ProRes on supported devices, AV1 on A17 Pro/M3+ hardwareAll FFmpeg decoders; hardware through VideoToolboxWhatever MediaCodec offers per device, plus FFmpeg extension for audioAll FFmpeg decoders; hardware through MediaCodec
AudioAAC, ALAC, AC-3/E-AC-3 (incl. Atmos), FLAC, Opus. No DTS, no TrueHD.Everything, including DTS/TrueHD (decoded to PCM)Device-dependent; DTS/TrueHD usually missingEverything
SubtitlesWebVTT, tx3g/mov_text, CEA-608. No ASS styling, no PGS.SRT, ASS/SSA with styling, PGS, VobSubSRT, SSA (basic), WebVTT, TTML, PGS (recent)Full libass
Exact seekseek(to:toleranceBefore:.zero, toleranceAfter:.zero) with a completion handlerseek … absolute+exact, measured on desktop (seeking.md)SeekParameters.EXACTSame as desktop
HDR outputNative (HDR10, HLG, Dolby Vision)Hard. Needs gpu-next with Metal/MoltenVK, and EDR is unprovenNative where the device supports itLimited and device-dependent
PiP, AirPlay, lock screen, background audioNativePiP needs a sample-buffer layer bridge; unprovenNative (MediaSession)Must be built by hand
Battery and heatBest (hardware path)Fine with hardware decoding; worse in softwareBestFine with hardware decoding
LicenceSystem frameworkLGPL build required (see below)Apache-2.0LGPL build required
Available in Expo GoYes (expo-video)NoYes (expo-video)No

Proposed: two engines per platform behind Capabilities and Commands (apps/mobile/src/playback/types.ts, mirroring nova-playback-api).

  • System engine by default for files it can play fully: hardware path, HDR, PiP, AirPlay, battery.
  • libmpv for files the system engine cannot play: MKV/WebM on iOS, DTS/TrueHD, ASS/PGS, professional codecs.
  • Routing comes from a probe (container plus streams) before opening. The user can override per file.
  • Consistency rule: a control that only one engine supports is shown everywhere and explained where it is unavailable, never silently hidden. Format claims in the UI are per engine and per platform (see platform-matrix.md).

What could change this: if M2 shows libmpv matching the system engine on HDR, PiP and battery for the corpus, a single-engine design is simpler and must be reconsidered.

Licensing per platform

  • Windows: unchanged (ADR 0003). The 0.1.0/0.2.0 installers are GPL. Option B (LGPL build) is still the plan.
  • iOS, App Store: GPL binaries are not shippable in practice: the App Store terms conflict with GPL distribution (the VLC precedent).
    • Any libmpv/FFmpeg for iOS must be an LGPL configuration built from source: mpv -Dgpl=false, FFmpeg without --enable-gpl or --enable-nonfree.
    • It must ship as dynamic frameworks, so users can relink, with source, build recipe and notices provided.
    • Whether LGPL compliance is achievable in App Store distribution is debated. Counsel must sign off before submission.
  • Android, Google Play: an LGPL libmpv.so is workable (dynamic linking, replaceable .so in the APK, source and notices). No GPL.
  • Codec patents.
    • The system engines use OS-licensed decoders.
    • Shipping FFmpeg software decoders for HEVC, H.264, AAC, AC-3 and others in a store app is a separate patent question per jurisdiction. Prefer hardware or system decoders for those codecs where possible, and have counsel decide whether to disable software decoders for patent-encumbered formats.
    • Proprietary camera RAW SDKs (BRAW, R3D) are not redistributable without their own agreements. They stay “detected and explained”.
  • Expo/React Native and dependencies are MIT, Apache-2.0 or BSD. Archivo is OFL-1.1 (no Reserved Font Name; static instances are allowed). An automated licence report per build is required (M5).

Distribution and updates

  • Store builds (App Store, Play) update only through the stores and must not contain a self-updater.
  • Amended 2026-10-18: the sideloaded Android APK from GitHub updates itself (Android 0.2.0), like the Windows app: GitHub releases API, SHA-256 check, then Android’s package installer, which enforces the same signing key. See docs/release/mobile.md.
  • EAS Update (over-the-air JS) is permitted for JS and asset fixes within store rules. NOVA will not use it to change native behaviour or the app’s purpose. If OTA is used it must be signed (code signing) and disclosed on the privacy page, or else it stays off.
  • Network use: only the Android APK’s update check to GitHub (can be turned off). Nothing else.
  • Privacy labels: “Data Not Collected” is achievable if this stays true.

Consequences

  • apps/mobile is an Expo app outside the pnpm workspace (own npm lockfile), so the desktop build is untouched.
  • Shared logic lives as pure TS in apps/mobile/src/core today, duplicated from desktop for the seek policy. M4 extracts a shared package (or UniFFI bindings to the Rust crates where Rust brings real value: identity hashing, resume and queue rules).
  • iOS development builds with native modules need either a Mac with Xcode (a free Apple ID allows 7-day device installs) or EAS Build with a paid Apple Developer account. Expo Go needs neither.