# Group calls (WebRTC) Group calls let you record remote guests via WebRTC. The host starts a call; guests join by link or code. Recordings can be added as segments in the episode editor. You can also **schedule a meeting** ahead of time: HarborFM reserves a join link and code, you invite guests, then start the meeting when you are ready. Ad-hoc **Start Group Call** stays available separately. **Note:** Group calls require the optional **webrtc-service** and correct configuration. If you don't see the Record or group-call UI, see the main [README](../blob/main/README.md) section "WebRTC (group calls)" and [Deployment: Docker](Deployment.md#docker-and-compose). ## Scheduled meetings Schedule a group call for a future start time from the **episode editor**. HarborFM reserves a join link and 4-digit code (and dial-in details when phone dial-in is enabled) so guests can prepare before you go live. ### Limits - **One scheduled meeting per episode** at a time. - **Up to 50 active scheduled meetings** per account (across all shows). - Schedule or reschedule at most **1 year** ahead. - Guests can use the link or code from **1 hour before** the start until **4 hours after**. Outside that window the join page shows a clear status (too early or expired). ### Schedule and manage 1. Open the episode editor. In the summary card, use **Schedule** (or the meeting panel when WebRTC is enabled). 2. Pick a start time and save. HarborFM emails you a confirmation if your account has an email address. 3. From **Manage**, you can: - Copy the join code and dial-in number (when enabled) - Copy a share link (optional guest name for join prefill) - Send email invites (name + email) - See invited guests and remove invites - Update the start time or cancel the meeting 4. When the join window is open, use **Start Meeting** on the episode summary. That starts the reserved meeting (same code and link). **Start Group Call** remains a separate ad-hoc call with its own code. Invite creates are rate limited (default **5 per 5 minutes** per user) so share links and emails cannot be spammed. ### Guest experience - **Too early** - The join page says the meeting opens 1 hour before the scheduled start. - **Waiting for host** - Inside the window, before you start the meeting, guests see that the host has not started yet. The page refreshes join status about every 30 seconds. - **Live** - After you start the meeting, guests join the call as usual. - **Expired / ended / cancelled** - The join page explains that the meeting is no longer available. ### Emails and calendar - **Creator confirmation** on create (if the creator account has email). - **Guest invite** emails with the join link, code, dial-in when enabled, an ICS calendar attachment, and a Google Calendar link. Reply-To and the calendar organizer use the host email when available. Local times use a cast member’s private time zone when the invite is tied to cast; otherwise the host zone or UTC. - **4-hour reminder** emails go to emailed invitees and to the host who scheduled the call. - **Reschedule** and **cancel** emails go to guests who were invited by email (not link-only shares). - When the episode is **published for the first time**, emailed invitees get a one-time notice (not the host). Title or description edits do not send meeting emails. - **Guest episode review** - When the episode first becomes scheduled (or published and unlisted), HarborFM emails preview links to the meeting host, emailed invitees, and episode cast with an email. See [Episodes: Guest episode review](Episodes.md#guest-episode-review). - Invite and reminder emails can report **open** status when Email Event Tracking is on in Settings (default on). Schedule Meeting shows open status per invitee. ### See also - [Starting and joining calls](#starting-and-joining-calls) - [Phone dial-in](#phone-dial-in) - [Episodes](Episodes.md) ## Phone dial-in HarborFM supports Zoom-style phone dial-in via **Telnyx** (DID + IVR + bidirectional media) into the same mediasoup room as browser guests. Each phone caller is its own producer so multitrack recording stays intact. ### Live Telnyx (PSTN) 1. In Settings → WebRTC: enable dial-in, set the phone number, consent prompt, Telnyx API key, and Call Control connection ID. 2. Point the Telnyx Call Control App webhook to `https://your-host/api/call/dial-in/webhook`. 3. Set **WebRTC public WebSocket URL** to a URL Telnyx can reach (e.g. `wss://your-host/webrtc-ws`). Production nginx/Caddy already proxies `/webrtc-ws/`. For local tunnels through Vite, `/webrtc-ws` is proxied to the webrtc service. 4. Start a group call, dial the DID, enter the **4-digit join code**. The phone appears on the roster; audio bridges both ways (no FakeDialIn tone). Requires `WEBRTC_SERVICE_SECRET` (used to mint media WebSocket tokens). Optional: set `DIAL_IN_IVR_NAME` on the main app to override the spoken welcome brand (default `HarborFM Podcasting`), e.g. `Welcome to Acme Podcasts. Please enter the four-digit call code.` **HD Voice:** In Settings → WebRTC, leave **Prefer HD Voice** on so HarborFM asks Telnyx for L16 @ 16 kHz on the media bridge. Also put **G722** first in the Telnyx Connection codec list for the phone↔Telnyx leg. Callers still need carrier HD Voice / VoLTE; AirPods alone do not enable wideband. ### FakeDialIn (development / e2e) Without a Telnyx API key, or for automated tests, HarborFM can inject **fake phone callers** when `DIAL_IN_FAKE=1` on both the main app and webrtc-service. 1. Start a group call and note the **4-digit join code**. 2. `POST /api/call/dial-in/fake/join` with `{ "joinCode": "1234", "displayName": "Phone Guest" }`. 3. The caller appears on the roster with `source: "phone"` and is recorded as its **own multitrack segment** when you Record. Leave with `POST /api/call/dial-in/fake/leave` (`participantId` / `sessionId`). Fake IVR webhooks also work against `POST /api/call/dial-in/webhook` when `DIAL_IN_FAKE=1`. ## Starting and joining calls Group calls use WebRTC so you can record remote guests in real time. The **host** starts a call from the episode editor; **guests** join via a link or a short code. For a reserved time and invites, see [Scheduled meetings](#scheduled-meetings). ### Starting a call (host) 1. Open the **episode editor** for the episode you want to record. 2. Use **Start Group Call** for an ad-hoc call, or **Start Meeting** if you already scheduled one and the join window is open. The app may ask for microphone (and optionally camera) permission. 3. Once the call room is created, you'll see a **join link** and often a **4-digit code**. For ad-hoc calls, share those with guests. For scheduled meetings, guests already have the reserved link and code from invites or Manage. 4. When guests have joined, you can start **recording** from the in-call controls. See [Call settings and recordings](#call-settings-and-recordings). Guest join links use the podcast **linking or managed domain** when configured (e.g. `https://your-show.example/call/join/...`) instead of the app hostname. Join pages serve episode/podcast Open Graph meta for link previews. If you don't see the group-call option, WebRTC may not be enabled or configured on the instance. See the main [README](../blob/main/README.md) ("WebRTC (group calls)") and ensure `WEBRTC_ENABLED`, `WEBRTC_SERVICE_URL`, and `WEBRTC_PUBLIC_WS_URL` are set. After Hostname is set in Settings, **WebRTC Settings** values take precedence over `WEBRTC_*` env vars (env still seeds empty settings). Changing Hostname updates the public WebSocket URL when it still pointed at the previous host. ### Joining a call (guest) 1. Open the **join link** in your browser (or enter the **4-digit code** on the call-join page if the host shared it). 2. Allow microphone (and camera if requested) when the browser asks. 3. You'll enter the same room as the host. Wait for the host to start recording when ready. Guests see a **recording banner** with elapsed time above "You're In The Call" while recording is active. Keep the tab active when possible; on mobile, returning to the tab remounts the mediasoup room if the producer died while backgrounded so you can produce again. ### Troubleshooting - **No audio / can't connect** - The server must have UDP ports open for WebRTC (default range 40000–40200, or 41000–41100 in Docker). Behind NAT, the admin must set `MEDIASOUP_ANNOUNCED_IP` to the server's public IP. See main [README](../blob/main/README.md) and [Deployment](Deployment.md). - **No "Record" or group-call UI** - Confirm `WEBRTC_ENABLED` and the WebRTC URLs are set; check server and webrtc-service logs (`docker compose logs webrtc` or `pm2 logs webrtc`). - **Guest drops from participant list after ~10 minutes** - Guests send a signaling WebSocket heartbeat every 30s (same as the host) so reverse proxies do not idle-drop `/api/call/ws`. Ensure your proxy allows long-lived WebSockets. - **Silent after switching apps on mobile** - Returning to the tab should recreate the producer; if audio stays silent, leave and rejoin or have the host restart the call. ### See also - [Scheduled meetings](#scheduled-meetings) - [Call settings and recordings](#call-settings-and-recordings) - [Segments: Recording segments](Segments.md#recording-segments) - Main [README](../blob/main/README.md) - WebRTC (group calls) ## Call settings and recordings During a group call you can use in-call controls, chat, and soundboard, and save the call as a recording that becomes a segment. ### In-call controls - **Mute / unmute** - Toggle your microphone. - **Leave** - Leave the call (host may have additional options to end the call for everyone). - **Settings** - Mic selector, auto gain control, listen-to-self, volume, and related options. - **Chat** - In-call group chat. - **Soundboard** - Short audio clips with waveform preview and search; play into the call. - **Wake lock** - On mobile, the join page tries to keep the screen awake during calls. ### Show Notes for guests Show Notes are **shared with guests by default**. Participants can open **See Show Notes** to view planned topics and durations (switch the episode panel to Host only if you want them private). Notes update live over the call WebSocket. See [Episodes: Show Notes](Episodes.md#main-areas). ### Recording the call 1. As the **host**, start the call and wait for guests to join (see [Starting and joining calls](#starting-and-joining-calls)). 2. Click **Record** (or "Start recording") in the call UI. All participants' audio is captured (multitrack; guest remounts create a new producer so segments stay time-aligned). 3. When finished, click **Stop recording**. The webrtc-service writes the recording to disk; the main app then makes it available. 4. The recording can be **added as a segment** to the current episode. Follow the prompt or use the episode editor to attach the recording; it will appear in the segment list like any other segment. You can then trim, reorder, or combine it with other segments and run **Build Final Episode**. See [Segments](Segments.md). ### Host-away behavior If the host leaves or disconnects, the call may stay open for a short grace period (configurable by the instance; page refreshes use a longer remount grace so the call is less likely to end). If the host is away too long, the call may end. Recording is typically tied to the host; if the host leaves during recording, behavior may vary (e.g. recording stops). See main [README](../blob/main/README.md) for `HOST_AWAY_GRACE_*` and `HOST_AWAY_CHECK_INTERVAL_MS`. ### See also - [Starting and joining calls](#starting-and-joining-calls) - [Segments: Making the final episode](Segments.md#making-the-final-episode) - [Episodes](Episodes.md) - Show Notes - Main [README](../blob/main/README.md) - WebRTC (group calls)