minimost.calls
minimost.calls
Voice/video calling over WebRTC, with the call lifecycle and signaling in SQLite.
All call state lives in the shared presence.db. Three tables (created
by init_calls_tables(), called from minimost.presence._init_tables())
store the lifecycle of every call:
calls— one row per call: channel, initiator, lifecycle state, and timestamps.call_participants— one row per (call_id, username): role, acceptance state, and join/leave timestamps. Designed to support future group calls without schema changes.call_signals— WebRTC signaling relay: offer/answer/ICE-candidate messages exchanged between participants during peer-connection setup.
Media travels peer-to-peer over WebRTC (RTCPeerConnection). Flask’s
role is limited to the call lifecycle state machine (the calls and
call_participants tables) and to relaying signaling messages via
POST /calls/<id>/signal / GET /calls/<id>/signals. Because the app
is LAN-only, ICE relies on host candidates with no STUN/TURN servers.
The legacy call_media / share_media tables and the
POST/GET /calls/<id>/media (and /screenshare/<id>/media) relay
routes are retained for one release as a fallback but are no longer used by
the frontend.
Module-level attributes
- calls_bpflask.Blueprint
The Flask Blueprint for all call routes. Registered in
minimost.create_app().
Mark every active standalone screen share as
'ended'and purge media.Called once at application startup so stale share records from a previous server run do not block new shares or leave orphaned media in the database.
- minimost.calls.reset_all_calls_ended() None[source]
Mark every in-progress call as
'ended'and purge orphaned media.Called once at application startup so that stale
'ringing'or'active'call records from a previous server run do not block new calls in the same channels.
- minimost.calls._participants_for_channel(channel: str) list[source]
Return the list of usernames who belong to channel.
DM channels (
"dm:user1:user2:..."): parsed from the channel string.Private channels (
"private:<id>"): looked up via theprivate_channel_memberstable.Public channels: not callable; returns
[].
- minimost.calls.initiate_call()[source]
Initiate a new call in a channel.
Route:
POST /calls/initiateCreates a call record in
'ringing'state and adds participant rows for every member of the channel. The initiator is immediately marked'accepted'; all other participants begin as'pending'.- Request body (JSON):
channel (str): The channel to call in. Must be a DM or private channel that the current user belongs to.
- Returns:
JSON with
call_id(str) andparticipants(list of str).- Return type:
flask.Response (application/json)
- minimost.calls.incoming_calls()[source]
Return calls currently ringing for the current user.
Route:
GET /calls/incomingPolled by the client every second to surface the incoming-call notification. Only returns calls in the
'ringing'state where the current user is a'pending'participant and the call was started within the last_RINGING_TIMEOUTseconds.- Returns:
JSON array of call objects with
call_id,channel,initiator, andstarted_ts.- Return type:
flask.Response (application/json)
- minimost.calls.accept_call(call_id)[source]
Accept an incoming call.
Route:
POST /calls/<call_id>/acceptUpdates the current user’s participant record to
'accepted'and transitions the call to'active'.- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
statusandparticipants(list of accepted usernames).- Return type:
flask.Response (application/json)
- minimost.calls.reject_call(call_id)[source]
Reject an incoming call.
Route:
POST /calls/<call_id>/rejectMarks the current user’s participant record as
'rejected'. When all non-initiator participants have rejected, the call transitions to'rejected'.- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
status.- Return type:
flask.Response (application/json)
- minimost.calls.end_call(call_id)[source]
End or leave a call.
Route:
POST /calls/<call_id>/endMarks the current user’s participant record as
'left'and sets the overall call state to'ended'. Any other participants will see the call end on their next state poll.- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
status.- Return type:
flask.Response (application/json)
- minimost.calls.invite_to_call(call_id)[source]
Invite a registered user to an active call.
Route:
POST /calls/<call_id>/inviteAny accepted participant may invite any registered user. If the target was previously a participant (rejected or left) their row is reset to
'pending'so they receive an incoming-call notification again.- Request body (JSON):
username (str): The user to invite.
- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
status.- Return type:
flask.Response (application/json)
- minimost.calls.send_signal(call_id)[source]
Send a WebRTC signaling message to another participant.
Route:
POST /calls/<call_id>/signalStores an offer, answer, or ICE candidate in the
call_signalstable. The recipient retrieves pending signals by pollingGET /calls/<call_id>/signals.- Request body (JSON):
to (str): Recipient username. type (str):
"offer","answer", or"ice_candidate". payload (object): The SDP object or ICE candidate dict.
- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
status.- Return type:
flask.Response (application/json)
- minimost.calls.get_signals(call_id)[source]
Return WebRTC signals directed at the current user.
Route:
GET /calls/<call_id>/signals?after=<id>Polled by the client during call setup to receive the remote offer, answer, and any ICE candidates. Pass the
idof the last signal already processed as?after=to avoid re-processing old messages.- Parameters:
call_id (str) – UUID of the call.
- Query after:
ID of the last signal already received (default 0).
- Returns:
JSON array of signal objects with
id,from,type,payload, andts.- Return type:
flask.Response (application/json)
Mark the current user as the call’s active screen sharer, or clear it.
Route:
POST /calls/<call_id>/screenshareUnder the WebRTC transport the screen video travels peer-to-peer, so this endpoint exists only to record who is sharing in the
screenshare_usercolumn. Clients pollGET /calls/<call_id>/stateto read it, which drives the single-sharer policy and the viewer UI label.- Request body (JSON):
on (bool):
trueto claim the screen,falseto release it.
- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
status.- Return type:
flask.Response (application/json)
- minimost.calls.call_state(call_id)[source]
Return the current state of a call.
Route:
GET /calls/<call_id>/statePolled every few seconds by active participants to detect remote hang-ups or other state transitions (
'ended','rejected').- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON object with call metadata and a
participantslist.- Return type:
flask.Response (application/json)
- minimost.calls.upload_media(call_id)[source]
Receive a binary media chunk from a call participant.
Route:
POST /calls/<call_id>/mediaAccepts raw binary data (
application/octet-stream) from aMediaRecorderrunning in the browser. The first chunk must be sent with?init=1&mime=<mimeType>; it is stored separately (is_init=1) and always returned to polling receivers so they can initialise theirSourceBuffer. Subsequent chunks are stored withis_init=0and identified by their SQLite auto-incrementid.All chunks are stored in the shared
presence.dbcall_mediatable so that every gunicorn worker can read what any other worker wrote.- Query parameters:
init (str, optional): Set to
"1"to mark this as the initialisation segment. mime (str, optional): TheMediaRecorderMIME type, e.g."video/webm;codecs=vp8,opus". Required wheninit=1.
- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
statusandseq(-1 for the init segment, auto-increment row id for data chunks).- Return type:
flask.Response (application/json)
- minimost.calls.get_media(call_id)[source]
Return buffered media chunks uploaded by a specific sender.
Route:
GET /calls/<call_id>/media?sender=<user>&after=<seq>Polled every 500 ms by the receiving participant. Always returns the most-recent initialisation segment (so late-joining receivers can bootstrap their
SourceBuffer) plus any data chunks whose SQLiteidis greater thanafter.All chunks are read from the shared
presence.dbcall_mediatable, so this works correctly across all gunicorn workers.- Query parameters:
sender (str): Username of the participant whose stream to receive. Required. after (int, optional):
idof the last data chunk already processed. Defaults to-1(return all buffered chunks).
- Parameters:
call_id (str) – UUID of the call.
- Returns:
JSON with
mime_type,init(base64 or null), andchunks(list of{seq, data}objects).- Return type:
flask.Response (application/json)
Start a standalone screen share in a channel.
Route:
POST /screenshare/startCreates a
screensharesrecord in'active'state. Unlike calls, no acceptance by viewers is required — any channel member can watch immediately by pollingGET /screenshare/active.Any previous active share by the same user in the same channel is automatically ended.
- Request body (JSON):
channel (str): The DM or private channel to share into.
- Returns:
JSON with
share_id(str).- Return type:
flask.Response (application/json)
End a standalone screen share.
Route:
POST /screenshare/<share_id>/stopMarks the share as
'ended'and purges its buffered media soshare_mediadoes not grow unboundedly. Only the sharer may call this endpoint.- Parameters:
share_id (str) – UUID of the screen share.
- Returns:
JSON with
status.- Return type:
flask.Response (application/json)
Return all active screen shares in a channel.
Route:
GET /screenshare/active?channel=<channel>Polled every second by the client to detect when a channel member starts or stops sharing their screen. Returns shares for all users, including the caller’s own share if they are currently sharing.
- Query parameters:
channel (str): The channel to query. Required.
- Returns:
JSON array of share objects with
share_id,channel,sharer, andstarted_ts.- Return type:
flask.Response (application/json)
Send a WebRTC signaling message for a standalone screen share.
Route:
POST /screenshare/<share_id>/signalMirrors
send_signal()but for the viewer-initiated one-to-many screen-share topology. Viewers send anoffer(and ICE candidates) to the sharer; the sharer replies with ananswer(and ICE candidates). Rows are stored in the sharedcall_signalstable keyed by share_id in thecall_idcolumn.- Request body (JSON):
to (str): Recipient username (the sharer, or a specific viewer). type (str):
"offer","answer", or"ice_candidate". payload (object): The SDP object or ICE candidate dict.
- Parameters:
share_id (str) – UUID of the screen share.
- Returns:
JSON with
status.- Return type:
flask.Response (application/json)
Return screen-share signaling messages directed at the current user.
Route:
GET /screenshare/<share_id>/signals?after=<id>Polled by both the sharer (to discover new viewer offers and ICE) and each viewer (to receive the answer and ICE). Pass the
idof the last signal already processed as?after=.- Parameters:
share_id (str) – UUID of the screen share.
- Query after:
ID of the last signal already received (default 0).
- Returns:
JSON array of signal objects with
id,from,type,payload, andts.- Return type:
flask.Response (application/json)
Receive a binary media chunk from the screen sharer.
Route:
POST /screenshare/<share_id>/mediaIdentical semantics to
POST /calls/<id>/mediabut for standalone screen shares. The first chunk must be sent withX-Init: 1andX-Mime: <mimeType>so viewers can initialise theirSourceBuffer.- Parameters:
share_id (str) – UUID of the screen share.
- Returns:
JSON with
statusandseq.- Return type:
flask.Response (application/json)
Return buffered screen-share media chunks.
Route:
GET /screenshare/<share_id>/media?after=<seq>Polled by viewers every 500 ms. Always returns the most-recent init segment so late-joining viewers can bootstrap their
SourceBuffer, plus any data chunks whoseidis greater thanafter.- Query parameters:
after (int, optional):
idof the last chunk already processed. Defaults to-1(return all buffered chunks).
- Parameters:
share_id (str) – UUID of the screen share.
- Returns:
JSON with
mime_type,init(base64 or null),chunks, andactive(bool).- Return type:
flask.Response (application/json)