Building a Real-Time Mobile Livestreaming App With Flutter, LiveKit and WebRTC

Getting video from one phone to another is close to a solved problem. Pick a real-time framework, publish a track, subscribe to it, and you have a livestream working in an afternoon.
Then someone walks into a lift. The host's app gets backgrounded. A network switches from Wi-Fi to cellular mid-broadcast. A viewer stares at a frozen frame for forty seconds because nothing in the system has noticed the stream is already over.
That gap, between "it works" and "it works when things go wrong," is what Zuzu was built to explore. It's the second project in NUS Labs, and the source is on GitHub.
What the app does
Zuzu combines hosting and viewing in a single iOS and Android app. Any signed-in user can broadcast or watch.
Hosts preview their stream before going live, switch between front and rear cameras, mute the microphone, and end the broadcast. Viewers browse a live-updating list of active streams, join one, send chat messages and heart reactions, and see the viewer count move in real time.
That's the surface. Underneath, the app is continuously managing livestream state, connectivity changes, recovery, and session cleanup for two different kinds of participant.
The architecture
A shared Flutter codebase across iOS and Android, a Node.js backend, and LiveKit as the real-time communication layer over WebRTC.
| Layer | Choice |
|---|---|
| Mobile | Flutter & Dart |
| State management and DI | GetX |
| Backend | Node.js, Express, TypeScript |
| Data access | Prisma |
| Real-time communication | LiveKit |
| Media transport | WebRTC |
| Platforms | iOS, Android |
The design decision worth pausing on: LiveKit doesn't only carry audio and video. It also powers stream discovery, participant presence, viewer counts, chat messages, and reactions.
The obvious alternative is to run media through WebRTC and everything else through a separate socket service. It works, and it's how a lot of apps are built. It also creates a permanent class of bug where two systems disagree about reality: the video is playing but the stream is missing from the list, the host has left but the viewer count still says twelve, a reaction arrives for a broadcast that already ended.
Keeping presence, discovery, and interaction inside the same real-time layer as the media means those states can't drift apart, because there's only one of them.
Four problems worth writing about
Hosts and viewers are different applications
The shortcut is to model a livestream as one connection with a role flag. Zuzu treats them as separate lifecycles, because they fail in completely different ways.
The host lifecycle covers preview, media setup, publishing, reconnection, stream termination, and cleanup. A host's failure modes are about their own device and uplink: the camera didn't initialise, publishing dropped, the app went to background, the broadcast needs to end cleanly so nothing is left dangling.
The viewer lifecycle covers stream discovery, joining, media subscription, host availability, interaction, reconnection, and cleanup. A viewer's failure modes are mostly about somebody else: the host vanished, the subscription failed, the stream ended while they were in it.
Collapsing these into one state machine is the kind of decision that looks efficient in week one and becomes the reason nobody wants to touch the streaming code in month six. Every new edge case has to be reasoned about twice, once for each role, inside the same tangle of conditionals.
Detecting a dead host quickly
When a host disappears, WebRTC will eventually notice. "Eventually" is the problem. Connection timeouts are tuned to avoid false positives on flaky networks, which means they're slow by design, and a viewer sitting on a frozen frame has no way to tell the difference between buffering and abandonment.
Zuzu runs a heartbeat over LiveKit Data Channels. The host emits a periodic signal; viewers watch for it. When it stops, the viewer interface can react without waiting for transport-level timeouts to expire.
The result is that a stream that has effectively ended looks ended. That's a small thing to describe and a large thing for perceived quality, because the alternative is users concluding the app is broken.
Recovery and device transfer
An interrupted broadcast raises a question the system has to answer correctly: is this person starting a new stream, or coming back to one that's still notionally alive?
Zuzu uses active stream state together with device identification to tell those apart. That makes recovery possible after an interruption instead of orphaning the old session and spawning a duplicate, and it opens the door to device-transfer scenarios where a broadcast continues from somewhere else.
Getting this wrong is how you end up with ghost streams in the discovery list that nobody is actually broadcasting to.
Mobile is a hostile environment
The rest of the resilience work is a list of things that are individually unremarkable and collectively the difference between a demo and a product:
- reconnection after network interruptions
- debounce handling so a brief offline blip doesn't trigger a full teardown
- foreground and background transitions
- state recovery when the app resumes
- a wakelock while actively broadcasting, so the screen doesn't sleep mid-stream
None of this appears in a feature list. All of it appears in reviews when it's missing.
The honest trade-offs
Worth knowing before adopting this shape of architecture.
WebRTC economics don't scale like HLS. A peer-to-peer-derived architecture running through an SFU is excellent for low latency and interactive audiences. Costs rise with concurrent participants in a way that segmented HTTP streaming does not. For sub-second interactivity with modest audiences, WebRTC wins clearly. For one host broadcasting to a hundred thousand passive viewers, the calculation changes.
LiveKit is a real dependency. Using it as the unified real-time layer is what buys the state consistency described above, and it means the infrastructure decision, managed service or self-hosted, is now load-bearing.
Hosting is expensive on the device. Continuous capture, encode, and upload consumes battery and generates heat, and a wakelock guarantees the screen stays on throughout. Long broadcasts have real power implications.
The demo scope stops short of production concerns. Zuzu is a Labs project. Recording, transcoding, content moderation, abuse reporting, and adaptive bitrate ladders are not part of it. Any consumer-facing deployment needs most of that list.
Where this architecture belongs
Real-time video is not only an entertainment feature. The same patterns apply anywhere a live view has to be shared and acted on.
- Field service and workforce: a technician streaming a fault to a remote specialist, on exactly the kind of connection this architecture is built to survive.
- Education and learning: live sessions where chat, presence, and participation matter as much as the video itself.
- Commerce and marketplace: live shopping, where reactions and viewer counts are the product and latency directly affects conversion.
- Property and hospitality: remote viewings and inspections conducted live rather than scheduled and repeated.
In every case the interesting engineering is the same: not sending the video, but knowing what's true when the connection stops cooperating.
Read the code
Zuzu is open source: github.com/nustechnology/LiveStream-Zuzu
The full technical breakdown, architecture diagram, and demo video are on the Labs project page.
You can also read the companion post on ClearHear, which takes the opposite approach: real-time processing with the network removed entirely.
If real-time video, live collaboration, or resilient mobile is on your roadmap, talk to our team.


