← Back to writing

Architecting Yoban: A Live Karaoke Platform for Seniors

How I built a cross-platform live streaming karaoke app with Agora RTC, synchronized YouTube lyrics, and a senior-friendly design system in Flutter.

The Vision

Yoban (友伴, meaning “friend companion”) started from a simple observation: my grandparents love singing karaoke, but most karaoke apps are designed for young users with tiny text, complex navigation, and overwhelming interfaces. I wanted to build something different — a live streaming karaoke platform where seniors could sing together, listen to each other, and socialize, all from their own homes.

The app has three core experiences:

  • “我要唱歌” (I Want to Sing) — Host a live karaoke room with video or voice-only mode
  • “我要聽歌” (I Want to Listen) — Browse active rooms and join as an audience member
  • “聊天室” (Chat Room) — Topic-based chat rooms for socializing between songs

Architecture Decisions

Why Flutter?

Reaching seniors on whatever device they already own was non-negotiable. Many use Android tablets, some have iPads from their children, and a few prefer just opening a browser. Flutter let me ship to Android, iOS, and web from a single Dart codebase. Hot reload also made it easy to iterate on the UI with real user feedback — I could adjust font sizes and button targets on the fly during testing sessions with actual senior users.

The Real-Time Stack: Agora + Firestore

The app combines two real-time systems for different purposes:

Agora RTC handles the heavy lifting of audio/video streaming. I use their broadcaster-audience model where:

  • Hosts broadcast as clientRoleBroadcaster with camera and microphone access
  • Audience members join as clientRoleAudience to watch and listen
  • YouTube karaoke backing tracks are mixed into the Agora audio stream

Cloud Firestore powers everything else in real-time:

  • Room metadata and discovery (the lobby sees rooms appear/disappear instantly)
  • Chat messages within each room
  • Emoji reactions with a flying animation overlay
  • Playback synchronization so all audience members see the same song position

I initially used Agora RTM (Real-Time Messaging) for room presence tracking, but removed it in favor of Firestore. Having two real-time SDKs for overlapping concerns added complexity without clear benefit. Firestore’s snapshots() streams were sufficient for room state, and the simplification made the codebase significantly easier to maintain.

YouTube Karaoke Integration

This was the most technically interesting piece. Instead of licensing a karaoke catalog, I built a YouTube-based system:

  1. Song selection — Users search and pick any YouTube karaoke video
  2. Video playback — youtube_player_iframe embeds the video for the host
  3. Audio extraction — youtube_explode_dart pulls the raw audio stream for mixing into Agora’s broadcast
  4. Lyrics sync — youtube_transcript_api and youtube_caption_scraper fetch subtitles, which I parse into timed LyricLine objects and display with auto-scrolling via scrollable_positioned_list

The host controls playback and the current song position is written to Firestore. Audience members read that position and sync their own lyrics display accordingly. Only the host can change songs or seek — audience players follow automatically.

Designing for Seniors: “Silver Mode”

The design system was as important as the engineering. I built a custom Material 3 theme I called “Silver Mode”:

  • Warm color palette — Taupe grey (#9E948A) primary with beige (#D0CAC4) backgrounds instead of harsh blues and whites
  • Large typography — Headlines at 32-40px with font weight 700-900
  • Big action buttons — A reusable BigActionButton widget with generous tap targets
  • Simple navigation — The home dashboard presents three large, clearly labeled options instead of a tab bar

Every interaction is designed to be forgiving. Buttons are large enough to tap without precision. Text is readable without glasses for most users. The color contrast meets accessibility standards while still feeling warm rather than clinical.

Room Lifecycle and Cleanup

Live streaming apps have a unique problem: rooms can become orphaned if a host’s app crashes or their phone dies. I solved this with a heartbeat system:

  1. Hosts write a lastActive timestamp to Firestore every 10 seconds
  2. The lobby screen filters rooms where lastActive is within the last 20 seconds
  3. Stale rooms automatically disappear from the lobby without manual cleanup

This is simple but effective. No separate cleanup workers, no cron jobs — just Firestore queries with a timestamp filter.

CI/CD: Automated Multi-Platform Deployment

I set up two GitHub Actions workflows for fully automated releases:

Android releases trigger on version tags (v*.*.*):

Tag v1.2.0 pushed
    ↓
Set up JDK 17 + Flutter
    ↓
Decode signing keystore from GitHub Secrets
    ↓
Build signed release APK
    ↓
Publish to GitHub Releases

Web deployment triggers on pushes to main:

Push to main
    ↓
Build Flutter web with --base-href "/yoban-app/"
    ↓
Deploy to GitHub Pages

The Android workflow handles keystore management through GitHub Secrets — the signing key is base64-encoded and decoded at build time, so credentials never touch the repository.

Biggest Challenges

Synchronizing Playback Across Clients

The hardest problem was keeping the YouTube video, Agora audio stream, and lyrics display in sync across all participants. The host is the source of truth — their current playback position is periodically written to Firestore, and audience clients adjust. But Firestore has variable latency, and YouTube’s player API doesn’t give frame-accurate position callbacks. I ended up using a combination of periodic sync writes and client-side interpolation to keep drift under a second.

Permission Handling Across Platforms

Camera and microphone permissions work differently on Android, iOS, and web. The permission_handler package abstracts most of it, but the UX flow matters: you can’t request permissions before explaining why. I built a setup screen that clearly explains what each permission is for (in large, friendly text) before the system dialog appears. For voice-only rooms, the camera permission is skipped entirely.

State Management with Multiple Real-Time Sources

A live room has state coming from three places simultaneously: Agora (stream status, audio levels), Firestore (messages, reactions, room metadata), and local state (UI controls, playback position). I used Provider with ChangeNotifier for global auth state and StreamBuilder widgets for Firestore streams. Keeping these layers separate — rather than trying to unify them into a single state tree — turned out to be the right call.

Key Takeaways

  1. Design for your actual users — Senior-friendly UX isn’t just bigger fonts. It’s fewer choices, warmer colors, and forgiving interactions
  2. Simplify your real-time stack — Removing Agora RTM in favor of Firestore reduced two overlapping systems to one. Less is more
  3. Automate releases early — Setting up CI/CD for Android signing and web deployment saved countless hours of manual builds
  4. Heartbeat > cleanup jobs — Simple timestamp-based liveness checks can replace complex room management infrastructure

Try the web version at the Yoban landing page.