Stream, network or phone? Three real live-stream incidents, taken apart
“I couldn’t hear the host” can mean three different things. The stream may have broken for everyone. The viewer’s own network may have dropped it on the way in. Or the media may have arrived fine, and the viewer’s phone couldn’t keep up with it. Each answer leads to a different fix, and to a different reply to the customer.
You can tell them apart by putting the person who complained next to everyone else who was watching the same people at the same moment, stream by stream, and look at what each of them actually received.
The examples below come from real live sessions; names have been changed. Nobody filed a ticket for any of them. For each one we picked a viewer and wrote down the complaint they would have been right to send. Times are minutes from the start of the session.
1. The host’s stream broke for everyone
The first room had two people on camera, Bruno and Iris, and about 38 viewers watching each of them. One of those viewers was Hugo.
About 20 minutes in, every viewer began losing Bruno’s video. For the next minute the typical viewer lost between a third and a half of his video packets. His audio broke up too: for 36 of the 37 viewers, the player was patching gaps in his voice with synthesized sound. That is choppy speech rather than silence, which is exactly what Hugo would have heard.
Iris was in the same room, sending to the same viewers, and her stream was fine.
Bruno’s own phone shows the order of events. At 19 min 56 s, its video encoder reported that it was limited by bandwidth. A few seconds later it stopped sending its HD layer and kept only a small 180×320 one. For the next minute the server’s receiver reports back to the phone counted as much as 16% of its upload packets as lost, and the phone cut its sending to about a tenth. The HD layer came back about a minute and a half later.
Nothing else on Bruno’s side was struggling. His phone downloaded Iris’s stream in the same minute and lost almost nothing, his app kept drawing at 60 frames per second, and WebRTC gave “bandwidth” as its reason for lowering quality, not “cpu”. Iris’s upload in the same minute lost under half a percent.
So the trouble started on Bruno’s upload, a few seconds before the viewers noticed. Viewers lost more of his video than the server reported losing on the way in, which we can’t fully explain from the client side, but the fault was clearly upstream of every viewer. A server-wide problem would most likely have hit Iris too. About ten minutes later it happened again, more briefly. The round-trip time on his upload jumped past a second, and every viewer with an audio reading in that slice lost part of his voice. Our complaint view words this answer as “Not their fault — the room lost it too.”
Timeline from minute 18 to minute 23 with lanes for five of the 37 viewers, the co-host's stream as they received it, and the host's own upload. The host's upload lane breaks first, at 19 minutes 56 seconds, and all five viewer lanes break a few seconds later, from minute 20 to about minute 21. The co-host's lane stays unbroken.
2. One viewer’s connection
The second room had about 45 recordings and two people on camera. Felix watched in a browser.
Sixteen minutes in, Felix lost 27% of the video packets from both people on camera. The loss faded over the next three minutes, and by minute 21 it was under 1%. One of the two voices needed heavy patching during the first of those minutes.
Nobody else had a problem. In each of those minutes, 14 or 15 other people were watching the same two cameras, and none of them lost more than 2%.
The server rated Felix’s connection “poor” or worse for more than half of a four-minute stretch. His browser estimated a 2 Mbps downlink for the whole session, while the other browsers that reported an estimate mostly read 10.
Loss that hits every sender for one receiver, and no other receiver, sits on the path between the server and that receiver. We can’t tell from here whether it was Wi-Fi, the access network or the ISP. In the product, this answer reads “Their own connection.”
Timeline from minute 13 to minute 23 with one lane per viewer. Only one viewer's lane is shaded, from minute 16 to minute 20, where he lost 27%, then 10%, 4% and 3% of video packets. The 14 other viewers of the same cameras are clear from minute 16 on.
3. One viewer’s phone
The third room had 14 recordings. Nina joined from an Android phone and stayed about nine and a half minutes.
The video reached her phone fine: her packet loss was no worse than anyone else’s. Her phone then threw away roughly half the frames it received, so every camera stuttered. None of the 11 or 12 other viewers of those cameras dropped more than about 1% in any minute. Her audio played normally.
Her phone was struggling. Most of its main-thread readings showed stalls longer than half a second, with a median of 768 ms, and no other phone in the room had more than one. Its thermal state went from nominal to serious within a minute and a half of joining, and reached critical about six minutes in.
Here the network delivered and the device couldn’t keep up, which the product calls “Their own device.” Any fix is probably on the client: cheaper rendering, fewer tiles decoding at once, or lower layers for weaker phones.
Timeline covering the nine and a half minutes after one viewer joined. Her lane is shaded in every minute, where her phone dropped 15–65% of the video frames it received. A thermal lane under it goes from nominal to serious at 1:20 and to critical at 6:20. The lanes for the 11–12 other viewers of the same cameras have no shading.
When the data doesn’t sort cleanly
Some viewers are guilty on both counts. In the first room, during a quieter stretch later on, three Android viewers were each dropping a quarter to a third of one camera’s frames. Each was also losing 3–9% of packets on at least one camera, while their JavaScript threads stalled for over half a second in a large share of readings. Either cause could explain what they saw. The honest answer is “Two explanations fit.”, and a support reply should name both rather than pick one.
Missing data needs the same care. If a viewer’s recording has a hole at that moment, say while the app sat in the background, the answer is “No data for them then.”, which is a different statement from “nothing went wrong”.
The traps we checked
- Too few witnesses: each verdict rests on 11 or more other viewers of the same streams.
- Clock skew: nearly every client’s clock was within three seconds of the server’s.
- Our own collection stalls, which can make a healthy client look as if it went dark: none of the verdicts rests on missing data, only on counters that arrived.
- Backgrounded apps and tabs, which fake frozen video: Bruno and Nina stayed in the foreground. Felix’s tab logged no visibility change during his bad minutes, and a hidden tab doesn’t produce packet loss anyway.
What to capture on the client to answer this yourself
You don’t need our product for this. You need:
- Per-track receive counters every few seconds, tagged with the publisher: packets received and lost, frames received, decoded and dropped, bytes received. For audio, add concealed samples, silent concealed samples (so you can subtract ordinary silence) and total samples played. Store cumulative values and compute deltas later.
- The publisher’s own send side: packets sent per simulcast layer, the loss and round-trip time the server reports back, and
qualityLimitationReason. That is what separated “Bruno’s upload” from “the server” above. - Device pressure: main-thread stalls or long tasks, UI frame rate, thermal state.
- App lifecycle: foreground, background and tab visibility changes with timestamps, so you can discount those stretches instead of scoring them.
- The viewer’s own network view: round-trip time, downlink estimate, and the server’s quality rating for the viewer’s own connection.
- Two clocks on every record: client time and server receive time. With both, you can estimate skew and tell a gap in your own collection from a client that went quiet.
- Identity on both ends: who observed each reading, and whose track it was.
In a browser, one getStats() call on the peer connection covers most of the receive side:
// every 5 s, for every inbound track on the connection
for (const s of (await pc.getStats()).values()) {
if (s.type !== 'inbound-rtp') continue
record({
track: s.trackIdentifier,
kind: s.kind,
packetsReceived: s.packetsReceived,
packetsLost: s.packetsLost,
// video
framesReceived: s.framesReceived,
framesDecoded: s.framesDecoded,
framesDropped: s.framesDropped,
// audio
concealedSamples: s.concealedSamples,
silentConcealedSamples: s.silentConcealedSamples,
totalSamplesDuration: s.totalSamplesDuration,
// Chromium also reports freezeCount / totalFreezesDuration; other engines may omit them
monotonicMs: performance.now(),
wallClockMs: Date.now(),
})
}Then, for any complaint, ask three questions in order. Did most other viewers lose the same publisher at the same moment? Did only this viewer lose it, with bad network readings? Or did media arrive while their device readings went bad?
About Rewitness
We build Rewitness, which records this data through an SDK and lines up every viewer against the rest of the room. You type who complained and roughly when, and it answers with one of the verdicts above plus the figures behind it. It infers server-side causes from what clients saw, because it doesn’t read the media server’s logs.