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().

minimost.calls.reset_all_screenshares_ended() None[source]

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 the private_channel_members table.

  • Public channels: not callable; returns [].

Parameters:

channel (str) – The channel identifier.

Returns:

List of usernames in the channel, or [] for public channels.

Return type:

list of str

minimost.calls.initiate_call()[source]

Initiate a new call in a channel.

Route: POST /calls/initiate

Creates 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) and participants (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/incoming

Polled 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_TIMEOUT seconds.

Returns:

JSON array of call objects with call_id, channel, initiator, and started_ts.

Return type:

flask.Response (application/json)

minimost.calls.accept_call(call_id)[source]

Accept an incoming call.

Route: POST /calls/<call_id>/accept

Updates 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 status and participants (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>/reject

Marks 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>/end

Marks 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>/invite

Any 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>/signal

Stores an offer, answer, or ICE candidate in the call_signals table. The recipient retrieves pending signals by polling GET /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 id of 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, and ts.

Return type:

flask.Response (application/json)

minimost.calls.set_screenshare(call_id)[source]

Mark the current user as the call’s active screen sharer, or clear it.

Route: POST /calls/<call_id>/screenshare

Under the WebRTC transport the screen video travels peer-to-peer, so this endpoint exists only to record who is sharing in the screenshare_user column. Clients poll GET /calls/<call_id>/state to read it, which drives the single-sharer policy and the viewer UI label.

Request body (JSON):

on (bool): true to claim the screen, false to 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>/state

Polled 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 participants list.

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>/media

Accepts raw binary data (application/octet-stream) from a MediaRecorder running 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 their SourceBuffer. Subsequent chunks are stored with is_init=0 and identified by their SQLite auto-increment id.

All chunks are stored in the shared presence.db call_media table 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): The MediaRecorder MIME type, e.g. "video/webm;codecs=vp8,opus". Required when init=1.

Parameters:

call_id (str) – UUID of the call.

Returns:

JSON with status and seq (-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 SQLite id is greater than after.

All chunks are read from the shared presence.db call_media table, so this works correctly across all gunicorn workers.

Query parameters:

sender (str): Username of the participant whose stream to receive. Required. after (int, optional): id of 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), and chunks (list of {seq, data} objects).

Return type:

flask.Response (application/json)

minimost.calls.start_screenshare()[source]

Start a standalone screen share in a channel.

Route: POST /screenshare/start

Creates a screenshares record in 'active' state. Unlike calls, no acceptance by viewers is required — any channel member can watch immediately by polling GET /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)

minimost.calls.stop_screenshare(share_id)[source]

End a standalone screen share.

Route: POST /screenshare/<share_id>/stop

Marks the share as 'ended' and purges its buffered media so share_media does 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)

minimost.calls.active_screenshares()[source]

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, and started_ts.

Return type:

flask.Response (application/json)

minimost.calls.send_share_signal(share_id)[source]

Send a WebRTC signaling message for a standalone screen share.

Route: POST /screenshare/<share_id>/signal

Mirrors send_signal() but for the viewer-initiated one-to-many screen-share topology. Viewers send an offer (and ICE candidates) to the sharer; the sharer replies with an answer (and ICE candidates). Rows are stored in the shared call_signals table keyed by share_id in the call_id column.

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)

minimost.calls.get_share_signals(share_id)[source]

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 id of 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, and ts.

Return type:

flask.Response (application/json)

minimost.calls.upload_share_media(share_id)[source]

Receive a binary media chunk from the screen sharer.

Route: POST /screenshare/<share_id>/media

Identical semantics to POST /calls/<id>/media but for standalone screen shares. The first chunk must be sent with X-Init: 1 and X-Mime: <mimeType> so viewers can initialise their SourceBuffer.

Parameters:

share_id (str) – UUID of the screen share.

Returns:

JSON with status and seq.

Return type:

flask.Response (application/json)

minimost.calls.get_share_media(share_id)[source]

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 whose id is greater than after.

Query parameters:

after (int, optional): id of 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, and active (bool).

Return type:

flask.Response (application/json)