Live: calls, streams, media and TV
Three engines (docs/KITS.md), four kits. Code: convex/lib/live.ts (blocks, kits, words, checks), convex/rooms.ts, calls.ts, livekit.ts, media.ts, screens.ts, screenAuth.ts, liveHttp.ts, push.ts, tables in convex/lib/liveTables.ts, screens in src/kits/live/, the TV link at src/app/tv.tsx, the iPhone screen-share extension in targets/broadcast/.
| Kit | Engine | Blocks |
|---|---|---|
| Calls | Rooms | call (rings people), room mode talk (group call) |
| Live streams | Rooms | room mode stage, media.gallery of recordings |
| Media library | Files | media.gallery, media.player |
| Show on TV | Screens | screen.pair, screen.remote |
Every setting is a block prop, every word is in blueprint.words (defaults in LIVE_WORDS), and every permission is a verb from convex/lib/roles.ts (a room's speak, watch, host; a screen block's pair, control). The server reads props from the saved blueprint, never from the device.
What keeps it safe #
A gallery's files are only readable by people who can see that gallery block. Upload links accept one exact size and type for 10 minutes, and unsaved uploads are deleted hourly. Files that aren't images, video, audio or PDF are stored as plain downloads. Recording links are random. Room passes last 10 minutes (LiveKit renews them for people already in), and a host's remove or send-back holds for 2 hours. Calls are limited to 3 a minute and 20 an hour per person, kids are only rung from their kids spaces, and ringing without a call button needs friendship or "invite". A TV can only get a role that views and plays, connecting one needs "invite", wrong codes are limited to 10 a minute, and a TV removed from the space can't sign back in.
Files in private spaces get links that run out after an hour. Each space can keep 20 GB of media by default.
Recordings land in the "recordings" folder of the space's media library.
Phones #
LiveKit needs a development build (not Expo Go): npx expo run:ios|android or an EAS build.
Screen sharing works on the web, Android and iPhone. On iPhone it goes through a Broadcast Upload Extension in targets/broadcast/ (added to the Xcode project by @bacons/apple-targets on npx expo prebuild): the share button opens iOS's "Start Broadcast" sheet, the extension sends the screen (JPEG, up to 15 fps, longest side 1280px, to keep phones cool) over a socket in the App Group group.<bundle id>, and LiveKit publishes it. The platform owner sets up the App Group.
Ringing a closed phone. Starting a call sends a push notification (convex/push.ts, Expo's push service) to every phone of the people being rung, with Answer and Decline buttons. It is time-sensitive on iPhone (it breaks through Focus) and uses a loud "Calls" channel on Android. If nobody answers within 45 seconds, they get "Missed call from …". An open app rings with the in-app card instead. People are asked to allow notifications the first time they ring someone or answer a call, not at sign-in. The platform owner turns push on once.
Calls ring as a notification, not the full-screen phone-call screen (CallKit on iPhone, a full-screen call intent on Android). That needs VoIP push keys on the server and native call code; it can be added later without changing the call data.
TV #
Open /tv on the TV's browser (or a laptop you AirPlay or cast). It shows a code. Someone with the pair permission types it into a "Show on TV" block; the screen signs in as its own account (ConvexCredentials provider "screen"), joins the space with the block's role and shows a page. Because it's a member, every block works on it, live. screen.remote switches what it shows. Opening /tv while signed in as a person shows any page full screen instead. Codes nobody used are cleared by screens._sweep (hourly cron).
Tests #
tests/live.test.ts (convex-test + vitest, npm test): calls ring, answer, end and time out; a call rings a closed phone by push and dead push tokens are dropped; rooms grant speaker, audience and host; the webhook keeps counts; a TV pairs, shows pages and disconnects; the media library keeps its permissions.