I spent the last several months solo-building Tuneline, a cross-platform media player, from a single Flutter codebase that ships native apps to macOS, Windows, Linux, Android, Google TV, and iOS. No Electron. Here is the stack and a few things that bit me.
The stack
- Flutter 3.38 / Dart 3.10 — one codebase, six targets.
- media_kit for playback — libmpv on desktop, ExoPlayer on Android. Avoiding per-platform video plugins was the single biggest sanity win.
- Riverpod for state, Hive for local storage, Dio for HTTP.
- Node.js + Prisma backend for the cloud-sync layer, so your library, favorites, and settings replicate across devices.
- GoRouter with a single-route, tab-driven shell so the same layout reflows from a phone to a 10-foot TV UI.
Things that bit me
TV is its own design language. A 10-foot, focus-based UI is not a big phone. D-pad focus traversal, larger hit targets, and a separate Google TV store listing were all non-trivial.
Per-platform video quirks. Desktop (libmpv) and mobile (ExoPlayer) disagree on enough edge cases that a shared abstraction over media_kit earned its keep.
Sync is a distributed-systems problem in disguise. "Set up once, never rebuild it" sounds simple until two devices edit the same data offline. Keeping one canonical decoder for both the socket sync-down and the REST pull saved me from a whole class of drift bugs.
One codebase is not one design. Window management on desktop, picture-in-picture per platform, and safe-area handling on mobile each needed platform-specific care even with a shared core.
The product
Tuneline is a bring-your-own-content player, like VLC — you supply your own playlists and it does not host anything. Every viewing feature is free on one device, and the only paid tier is cloud sync plus multi-device. No subscriptions.
Site: https://tuneline.app — happy to answer any Flutter or cross-platform questions in the comments.
Top comments (0)