> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: video room and call quality — WebRTC, recording QC, and SIP dial-out

> Diagnose degraded video rooms and recordings — frozen or missing video, echo, buffer stalls, the video.recording.degraded QC verdict, and video.dial_out.failed — by mapping each symptom to the layer that causes it.

# Troubleshooting: video room and call quality — WebRTC, recording QC, and SIP dial-out

A video-quality complaint is different from a video room that never
connects: the room opens, participants join, and then the experience is
broken — the video freezes while audio carries on, one participant drops
out, the recording lands flagged as degraded, or a PSTN dial-out into the
room never bridges. None of those are fixed by re-creating the room. Each
quality class maps to a specific layer — WebRTC media negotiation, the
participant's network path, the track publish grant, the recording
pipeline, or the SIP dial-out leg — and this page works each one as a
decision path.

Video rooms run over WebRTC on Orbit Media, not over SIP RTP like a trunk
call: media setup is ICE/STUN/TURN election instead of SDP address
advertisement, and the dominant codec family is H.264/H.265 video rather
than telephone audio. That makes the diagnosis paths different from
[Troubleshooting: voice call quality](/troubleshooting/voice-call-quality) —
read that page only for the PSTN leg of a dial-out, never for the in-room
media. For the create/join/record contract itself, start with the
[Video channel page](/channels/video) and the
[Video API reference](/api-reference/video).

## Symptom → layer index

Find the symptom row first, then go to the section below that works that
layer.

| Symptom                                                                                                                                   | Layer that causes it                                                                                                | Go to                                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Degraded-recording QC verdict** — a `video.recording.degraded` webhook arrives, or a completed recording is silent, black, or truncated | Recording pipeline — the quality scorer flagged the artifact after the room ended                                   | [Recording QC verdicts](#recording-qc-verdicts)                           |
| **Participant loss** — one participant freezes, drops, or their tile goes dark mid-room                                                   | The participant's network path — ICE path decay, or a firewall blocking the SFU ports                               | [Participant loss and buffer stalls](#participant-loss-and-buffer-stalls) |
| **Buffer stall** — video freezes in bursts, audio keeps flowing                                                                           | Bandwidth floor — the uplink or downlink cannot hold the video bitrate the simulcast ladder selected                | [Participant loss and buffer stalls](#participant-loss-and-buffer-stalls) |
| **No video but audio present** — the participant speaks and their tile shows nothing                                                      | Track publish — the video track was never published, was blocked by the grant, or the endpoint has no usable camera | [No video but audio present](#no-video-but-audio-present)                 |
| **Echo** — one side hears itself, or the PSTN dial-in participant feeds the room back                                                     | Far-end device or the bridged PSTN leg reflecting the stream                                                        | [Echo](#echo)                                                             |
| **SIP dial-out failure** — `POST /:id/dial-out` fails or a `video.dial_out.failed` webhook arrives                                        | The SIP dial-out leg — softswitch trunk rejection, busy/unreachable callee, or ring timeout                         | [SIP dial-out failures](#sip-dial-out-failures)                           |

## Cause map

Every symptom above reduces to one of these causes. Work the symptom row
first; the sections below give the fix path per cause.

| Cause                                                                                                                                 | Produces                                                                             | Fix section                                                               |
| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| NAT without a working relay — the participant sits behind NAT and no STUN/TURN path completes                                         | Participant loss, buffer stalls, or a join that connects and immediately decays      | [Participant loss and buffer stalls](#participant-loss-and-buffer-stalls) |
| STUN/TURN/ICE failure — a corporate firewall blocks the SFU ports or the UDP media path, or a SIP ALG mangles WebRTC-adjacent traffic | Same as above, plus rooms that never connect on one network but work everywhere else | [Participant loss and buffer stalls](#participant-loss-and-buffer-stalls) |
| Bandwidth floor unmet — sustained throughput below what the selected video layer needs                                                | Buffer stalls, frozen tiles, the active-speaker view never settling                  | [Participant loss and buffer stalls](#participant-loss-and-buffer-stalls) |
| Unsent or blocked video track — camera permission denied, publish disabled at join, or the grant tier disallows publishing            | No video but audio present                                                           | [No video but audio present](#no-video-but-audio-present)                 |
| Endpoint capability — a handset or browser with no usable camera, or one that cannot encode the negotiated codec                      | No video but audio present; poor picture on that participant only                    | [No video but audio present](#no-video-but-audio-present)                 |
| H.264/H.265 codec mismatch on the SIP leg — the dialled-in endpoint negotiates a video codec it cannot actually render                | Dial-out connects with audio only, or a `video.dial_out.failed` on codec negotiation | [SIP dial-out failures](#sip-dial-out-failures)                           |
| SIP trunk rejection on the dial-out leg — the callee is busy, unreachable, or the ring timeout fires before answer                    | `video.dial_out.failed`                                                              | [SIP dial-out failures](#sip-dial-out-failures)                           |

## Recording QC verdicts

Orbit scores every completed recording after the room ends. When the
artifact lands — or fails to — with an ambiguous quality signal, a
`video.recording.degraded` webhook fires alongside
`video.recording.completed`:

```json theme={null}
{
  "type": "video.recording.degraded",
  "data": {
    "room_id": "vroom_abc123",
    "session_id": "vsess_abc123",
    "status": "degraded",
    "reasons": ["short_duration"],
    "recording_url": null
  }
}
```

`status` is `"missing"` when no usable artifact was produced at all and
`"degraded"` when an artifact exists but is suspect. The scorer is
deliberately pessimistic — it prefers a false alarm over a silent loss.
Read the `reasons` array first; it maps directly onto the fix:

| Reason code              | What the scorer saw                                       | Where the problem sits                                                                                                                            | What to do                                                                                                                                                                                                   |
| ------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `no_file_results`        | The recording pipeline ended without writing any file     | Recording pipeline, or the room never had a publishable stream to capture                                                                         | Check whether any participant actually published — a room of receive-only viewers produces no recordable composite. If publishers existed, capture the room per the checklist below and escalate             |
| `empty_location`         | An artifact entry was reported but its location was empty | Recording pipeline write step                                                                                                                     | Escalate — the pipeline acknowledged the recording but did not finish the write; nothing on the tenant side changes that                                                                                     |
| `egress_status_failed`   | The recording job itself reported a failure tag           | Recording job — aborted, failed, or hit its limit                                                                                                 | Check `recording_enabled` vs. room duration: a room that hits its duration ceiling mid-recording can abort the job. Re-test in a short room; if the verdict repeats on every room, escalate with the room id |
| `no_participants_joined` | The session recorded zero join events                     | Room usage — recording started on an empty room (an auto-record race or a guest who never joined)                                                 | Confirm anyone published. If your flow starts recording at room create, gate the start on the first `video.participant.joined` instead                                                                       |
| `short_duration`         | The recorded session was under five seconds               | Room usage — recording toggled on and off before anything meaningful was captured (a stale auto-record, or a participant who bounced immediately) | Treat as benign when it matches a quick join-leave; investigate only when full-length meetings flag it                                                                                                       |

One structural rule from the table: `no_participants_joined` and
`short_duration` are usually room-usage signals, not platform faults —
the platform exactly reported what happened. The first three reasons are
different: they mean the recording pipeline said it would produce a file
and did not. Those are the escalate cases.

The QC verdict is also persisted on the room: fetch the room and read
`recording_qc` off the completed session to see exactly what fired, with
its score time — you do not need the webhook receiver to be live to audit
a past meeting.

## Participant loss and buffer stalls

A participant freezes, drops, or their tile darkens mid-room — or the
video stalls in bursts while audio carries on. WebRTC recovers
continuously, so a video freeze with live audio is almost always the
network path, not a broken client: the video layers get shed first.

Work the decision path in order:

1. **Is it one participant, or everyone?** One participant's network is
   the cause when the rest of the room stays clean. Read the live
   per-participant QoS telemetry before changing anything:
   `video.participant.qos` fires when a participant's connection quality
   crosses into a degraded bucket mid-session, and the same data is
   readable on demand at
   `GET /api/v1/video/connection-quality/:name/participants/:identity/stats` —
   each sample carries `quality_label` (`excellent`/`good`/`poor`/`lost`),
   `quality_score` (0–5), `rtt_ms`, and `jitter_ms`. A participant
   pinned at `poor` or `lost` with rising RTT is a network problem;
   everyone in the room degrading together points at the shared path.
2. **Does it only happen on one network?** A room that is fine
   everywhere except one corporate network is the classic firewall
   signature: UDP media blocked, the STUN/TURN election never completes
   to a workable path, and ICE either falls back to a slow path or dies.
   The [NAT glossary entry](/reference/glossary) covers why real-time
   media needs a reachable address after translation. Two tenant-side
   fixes close nearly every case: disable **SIP ALG** on the site
   firewall (it rewrites media-address information "to help" and breaks
   WebRTC-adjacent flows the same way it breaks SIP RTP — the same fix
   the voice page calls out for one-way audio), and allow the SFU's
   published TCP/TLS fallback port so the client can relay when UDP is
   blocked. Forcing relay through TURN is the deliberate, correct
   fallback — a working relay path beats a broken direct path every
   time.
3. **Does it stall exactly when the picture is busiest?** Then the
   bandwidth floor is unmet: the simulcast ladder picked a video layer
   the link cannot hold, the queue backs up, and the video freezes while
   audio (which needs a fraction of the bitrate) keeps flowing. No
   rejoin fixes an undersized uplink — the participant needs the link,
   or a smaller send profile. The QoS sample's `quality_score` dropping
   in step with the stalls confirms this before any platform change.
4. **Is an ICE policy pinned in your client?** If you embed with your
   own LiveKit client configuration rather than the prebuilt embed, an
   over-restrictive ICE election policy (relay-only when no relay is
   reachable, or a pinned candidate family the network blocks) produces
   rooms that connect in the test lab and fail in the field. Correct the
   ICE election policy to let the client contest all candidate types;
   the prebuilt `<orbit-video-room>` element ships defaults that already
   do.

## No video but audio present

The participant speaks, everyone hears them, and their tile shows
nothing. Unlike a stall, this is a track problem: the video track was
never published, or the endpoint cannot produce it.

| Check                                                          | Finding                                                                                                                                               | Fix                                                                                                                                                                                                                                                   |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Did the join token grant publishing?                           | The participant joined as a `viewer` tier, the lobby held them (`lobby_downgraded: true` on the join response), or a waiting-room admit was never run | The token's `permissions.can_publish` is the source of truth — read it off the join response, not the requested tier. Admit the participant, or mint a token with a publishing tier. See the [grant tiers on the Video channel page](/channels/video) |
| Did the browser get camera permission?                         | Permission denied, or the camera is claimed by another application                                                                                    | The endpoint must grant capture; no token change fixes a denied permission                                                                                                                                                                            |
| Can the endpoint actually encode?                              | A handset or virtualized browser with no usable camera, or a device that cannot encode the negotiated H.264/H.265 profile                             | Test on a known-good device. A participant who can only join audio-first stays useful — the room degrades them gracefully                                                                                                                             |
| On a SIP dial-in/dial-out leg, did the callee negotiate video? | The SIP endpoint accepted audio-only, or negotiated a video codec it cannot render                                                                    | Pin the outbound leg's encode profile (`video_fps`, `video_width`, `video_height`, `video_bitrate_kbps` on the dial-out body) to something the far end actually supports — see the next section                                                       |

## Echo

One side hears itself returned a moment after speaking. Echo is almost
never introduced mid-path: a far-end device (speakerphone, handset,
headset) or the bridged PSTN leg is reflecting the stream, and the SFU is
passing it through faithfully. The dialled-in PSTN participant is the
highest-echo suspect — a phone on speaker in the room behaves exactly
like the speakerphone case on the voice side. Isolate by changing one
endpoint variable at a time — headset instead of speakerphone, a
different dialled-in number — and escalate with the room evidence below
when echo follows one specific destination or device.

## SIP dial-out failures

`POST /api/v1/video/rooms-scheduled/:id/dial-out` originates a PSTN or
SIP leg and bridges the callee into the room. The failures divide
cleanly: the request fails synchronously (a 4xx/503 response — read the
error code on the [Video channel page](/channels/video)), or the leg
starts and then fails asynchronously, firing `video.dial_out.failed`:

```json theme={null}
{
  "type": "video.dial_out.failed",
  "data": {
    "room_id": "550e8400-e29b-41d4-a716-446655440000",
    "destination_type": "sip",
    "sip_headers": { "X-Ticket-Id": "zd-48213" },
    "emitted_at": "2026-07-07T12:30:00Z"
  }
}
```

The three production failure shapes, per the event reference:

* **Trunk rejection** — the softswitch rejected the INVITE. Work it with
  [Troubleshooting: SIP trunk registration, health, and failover](/troubleshooting/sip-trunk):
  an unregistered or failing trunk rejects every dial-out that routes
  over it, and a H.264/H.265 mismatch on a `type: "sip"` destination
  surfaces here too — the far end declines the offered video profile.
  Pin the `video_*` encode fields to a profile the destination accepts
  instead of retrying the same offer.
* **Callee busy or unreachable** — the destination never answers into a
  bridgeable state. Verify the number by calling it directly; a busy or
  dead destination is not a room problem.
* **Ring timeout before answer** — the `ringing_timeout_sec` budget ran
  out. Auto-answer chains (IVRs, call queues, voicemail) routinely
  outlast a short budget; raise `ringing_timeout_sec` when the callee is
  expected to take time to reach a human. When the callee answers and
  the room then feels downstream congestion, throttle the outbound leg
  with the `video_bitrate_kbps` profile rather than redialling.

If your dial-out request carried `sip_headers`, the failure webhook
echoes the same map back unchanged — correlate the failure against your
own ticket or account id from the event alone.

## What not to do

* **Do not restart partial recordings in a loop.** When a degraded
  verdict arrives, restarting the recording on the same ended room gains
  nothing — the session is over and the media is gone. Read the
  `reasons` array against the QC table, fix the named cause, and let the
  next room record cleanly.
* **Do not re-create the room for a media problem.** A new room id
  changes nothing about ICE election, the participant's uplink, or a
  firewall — the same join lands in the same network. Diagnose the one
  participant's QoS sample instead.
* **Do not keep redialling a failed SIP leg against the same trunk
  state.** A trunk that is rejecting INVITEs rejects the tenth dial the
  same as the first; fix the trunk or the codec profile, then dial once.
* **Do not paste raw SDP/ICE dumps into a first ticket.** Escalate to
  the SDP and ICE traces only when the platform-side checks above come
  back clean — the QoS sample, QC verdict, and checklist below resolve
  most reports without a packet-level dive.

## What to send support

Work the relevant section first; if the symptom persists, open a ticket
with the full set below — it is what lets support pull the exact room and
session without a back-and-forth:

1. **Your tenant / organization ID** (Settings → Organization, or
   `organizationId` from `GET /api/v1/me`).
2. **Room id** (`room_id` from the room — or off any `video.*` webhook)
   and the **server-side `room_sid`** from the join response or the
   `video.room.started`/`video.room.ended` payload.
3. **The broken window in UTC** — when the room ran and when the symptom
   appeared, so the session and QoS samples line up.
4. **The participant identity** (`identity` from the join token) of the
   affected participant, and their last-known `quality_label` from the
   QoS telemetry if you captured it.
5. **Whether a SIP dial-out is involved** — and if so, the
   `video.dial_out.failed` payload (the echoed `sip_headers` included),
   the `type` (`pstn` or `sip`), and the destination you dialled.
6. For recording complaints, the **full `video.recording.degraded`
   payload** — `status`, the `reasons` array, and the `session_id`.

## See also

* [Video channel: rooms, embeds, recording, and broadcast](/channels/video)
  — room kinds, join tokens and grant tiers, recording, limits, and the
  dial-in/dial-out overview.
* [Video API reference](/api-reference/video) — the request/response
  contract for rooms, recording, connection-quality, and dial-out.
* [Webhook events reference](/reference/webhook-events) — payload
  contracts for `video.recording.degraded`, `video.participant.qos`,
  `video.dial_out.failed`, and the room lifecycle family.
* [Troubleshooting: voice call quality](/troubleshooting/voice-call-quality)
  — the SIP/RTP counterpart for the PSTN leg of a dial-out; one-way
  audio and dead air on a bridged phone participant live there.
* [Troubleshooting: SIP trunk registration, health, and failover](/troubleshooting/sip-trunk)
  — trunk rejection and failover for the dial-out leg.
* [Glossary](/reference/glossary) — the NAT, STUN/TURN, and ICE
  definitions this page builds on.
