(prov. X) in parentheses is a hypothesis only and must not be used as a production rule. Calibrate against real calls (regress vs v2) first.Defines whether a call has a quality problem, from the V4 SDK kVoipSummaryStat end-of-call instrumentation (one doc per leg). For post-hoc analysis, the quality dashboard, alerts, and v2-vs-v4 comparison. Inherits v2's A/V/C + R numbering, swapping only the underlying fields and judgment engine, and adds v4-native capabilities (A06 / V07 / C05–C07 / S class / R33–R35).
Three levers: ① the SDK's own quality verdicts (aqa/vqs/nqs/oqs, 1–5) cross-check / backstop; ② event streams glc/nan turn "share / single-event duration" from estimate into exact; ③ the stl state timeline separates real faults from deliberate user actions (v2's biggest blind spot).
Event streams glc/nan/lma are the (primary) tunable, attributable triggers; SDK scores cross-check and backstop. Confidence tiers:
| Tier | Condition |
|---|---|
| High (confirmed) | ≥1 event trigger and matching SDK score ≤ 🔴待定(prov. 2) |
| Mid (likely) | event trigger only, or SDK score ≤ 🔴待定(prov. 2) only |
| Low (review) | borderline SDK score = 🔴待定(prov. 3) only |
Dashboard default = High + Mid; alerts use High only.
v2 mis-flagged deliberate mute / hold / camera-off as "no audio / black screen". v4 uses stl to strip deliberate actions before judging — the single biggest accuracy gain over v2.
| Action | Signal | Effect |
|---|---|---|
| Mute mic | stl.ty=12 (AudioMute) v=1 | subtract from A03 silence |
| Camera off | stl.ty=11 (VideoMute) v=1 | subtract from V03 black-screen |
| Call on hold | stl.ty=9 (SessionOnHold) v≠0 | subtract from A01/C01 share |
Fixing double-counting: one network event often lights up "loss-share + interruption + freeze" at once, so we split two layers:
| Layer | Members | Flags a bad call? |
|---|---|---|
| Symptom layer (user-perceived) | A·V·C01/C02/C04·S | ✅ flags + deduped |
| Network-condition layer (upstream cause) | 🔹C03/C05/C06/C07 · R01–R12 | ❌ attribution only |
Bad call = OR over the symptom layer; roll up two legs by rid — either leg bad → call bad.
Primary-symptom dedup: when several fire, take one primary by priority (rest are secondary, not re-counted in the problem distribution). Priority (high→low): ① C01/C02 interruption → ② A03/V03 silence·black → ③ A02/V05 single long freeze → ④ A01/V01 freeze share → ⑤ V02/V04 blur·V07 low-fps·A06 degradation → ⑥ A04/V06 slow first frame·C04 short call.
share > X% OR cumulative duration > N s, either one fires (X, N both 🔴TBD). Plus A02/V05/C02 single-event backstops.denominator = vod voipDuration (s)
| # | Problem | Trigger (thresholds 🔴TBD) | SDK check | Fields |
|---|---|---|---|---|
| A01 | High audio-freeze share | Σ glc.pem[ty=7]/(vod×1000) > 🔴待定(prov.5%) OR cum > 🔴待定(prov.N s) | aqa ≤ 🔴待定(prov.2) | glc=glitch array (ty=7 audio freeze, pem=dur ms); vod=in-call dur (s); aqa=audioQualityScoreAvg (1–5) |
| A02 | Single freeze too long | max(glc.pem[ty=7]) > 🔴待定(prov.10000ms) | — | glc.pem=single freeze ms; aft=audioFreezeThreshold ms |
| A03a | Our side silent (this leg) | after removing this leg's mute: arv=0 ∥ glc.ty=1 | aps ≤ 🔴待定(prov.2) | arv=mic capture vol (0=silent); glc.ty=1=MicCaptureFailed; aps=audioPlayScore |
| A03b | Peer silent (needs join) | apv=0 and join rid peer not muted; downgrade if peer leg missing | aps ≤ 🔴待定(prov.2) | apv=playback vol (0=peer silent); rid=roomId |
| A04 | Slow first audio frame | faf > 🔴待定(prov.3000ms) | — | faf=firstAudioFrameDelay ms |
| A05 | Low audio quality (backstop) | — | aqa ≤ 🔴待定(prov.2) OR aqm = 🔴待定(prov.1) | aqm=audioQualityScoreMin |
| A06 | Degraded but no freeze | apo < 🔴待定(prov.80%) and alp+adp+asp+atp high | aqa ≤ 🔴待定(prov.2) | apo=original-packet %; alp/adp/asp/atp=late/dup/shrink/stretch % |
denominator = vpd videoPlayDuration (s); only when vpd>0
| # | Problem | Trigger (thresholds 🔴TBD) | SDK check | Fields |
|---|---|---|---|---|
| V01 | High video-freeze share | vfp > 🔴待定(prov.5%) (SDK-computed) ∥ glc method OR cum > 🔴待定(prov.N s) | vqs ≤ 🔴待定(prov.2) | vfp=videoFreezePercent (=vfd/vpd); vpd=render dur (s); vqs=videoQualityScore |
| V02 | Blur / low quality share | resolution stl.ty=10 v<🔴待定(prov.360) share > 🔴待定(prov.5%) ∥ veq > 🔴待定(prov.35, per-codec) | vqs ≤ 🔴待定(prov.2) | stl.ty=10=resolution (v=px height); veq=encoder QP (lower=better); vec=encoder |
| V03 | Black screen | after removing camera-off: vbs share/dur over 🔴待定 ∥ glc.ty=2 | — | vbs=blackscreen total ms; glc.ty=2=CameraCaptureFailed |
| V04 | Single blur too long | single low-res span > 🔴待定(prov.10000ms) | — | stl.ty=10=resolution event |
| V05 | Single video freeze too long | max(glc.pem[ty=6]) > 🔴待定(prov.10000ms) | — | glc.pem(ty=6)=single video freeze ms |
| V06 | Slow first video frame | fvf > 🔴待定(prov.5000ms) | — | fvf=firstVideoFrameDelay ms |
| V07 | Low fps / jank | vpf < 🔴待定(prov.12fps) ∥ vjp high | vqs ≤ 🔴待定(prov.2) | vpf=render fps; vjp=jank % |
denominator = vod (s) · 🔹 = network-condition layer (attribution only, does not flag a bad call)
| # | Problem | Trigger (thresholds 🔴TBD) | SDK check | Fields |
|---|---|---|---|---|
| C01 | High interruption share | Σ glc.pem[ty=5]/(vod×1000) > 🔴待定(prov.5%) OR cum > 🔴待定(prov.N s) | nqs ≤ 🔴待定(prov.2) | glc(ty=5 Connecting media interrupt); nqs=networkQualityScore |
| C02 | Single interruption too long | max(glc.pem[ty=5]) > 🔴待定(prov.10000ms) | — | glc.pem(ty=5)=single interrupt ms |
| C04 | Short call abnormal drop | iac=1 AND vod < 🔴待定(prov.10s) AND aer≠0 | — | iac=isAccepted; aer=abnormalEndReason (≠0=abnormal) |
| 🔹C03 | High-latency share | Σ nan.pem[ty=3]/(vod×1000) > 🔴待定(prov.5%) | tra > 🔴待定(prov.300) | nan=anomalies (ty=3 RTT spike); tra=transportRTTAvg ms |
| 🔹C05 | Frequent reconnect | rcc > 🔴待定(prov.3) | — | rcc=ICE reallocate count |
| 🔹C06 | High packet-loss share | Σ nan.pem[ty=1 or 2]/(vod×1000) > 🔴待定(prov.5%) | sla/rla > 🔴待定(prov.5) | nan(ty=1 up / 2 down loss); sla/rla=up/down loss avg % |
| 🔹C07 | BWE crash | nan.ty=6 (BWECrash) ≥ 🔴待定(prov.1) ∥ ekb < 🔴待定(prov.500) | — | nan.ty=6=BW dropped >50%; ekb=est. kbps |
Only calls with intent where media never established; normal no-answer / reject / cancel (rsn=3/4/8) are funnel, not bad calls. ⚠️ Depends on the full rsn/aer enum (FootPrintSummaryReason), not yet available.
| # | Problem | Trigger | Fields |
|---|---|---|---|
| S01 | Media setup failed | iac=1 but ict null → media never came up | ict=media-setup time ms (null=fail) |
| S02 | Ring / push failed | invited but irg=0 ∥ itr timeout | irg=isRinging; itr=invite→ring ms |
| S03 | Signaling error | rsc error code ∥ pud empty | rsc=responseCode; pud=peerUid (empty=never paired) |
| S04 | Slow connect | ict > 🔴待定(prov.3000ms) (cause = R31) | ict=media-setup time ms |
nan.ty maps straight to loss/RTT/jitter events; lma inline-splits local / peer / relay; stl supplies network/device/behaviour context. R uses "did it appear" for attribution; only symptom triggers use share thresholds, avoiding double-counting.
| # | Cause | Trigger (🔴TBD) | Fields |
|---|---|---|---|
| R01 | Uplink loss | nan.ty=1 ∥ sla > 🔴待定(prov.5) | nan(ty=1 send-loss spike); sla=send loss avg % |
| R02 | Downlink loss | nan.ty=2 ∥ rla > 🔴待定(prov.5) | rla=recv loss avg % |
| R03 | Relay-link loss | rla high but lma.mdl/lma.pul low → relay segment | lma=last-mile (mdl=my dl loss, pul=peer ul loss) |
| R04/05 | Up/down latency | lma.mur/lma.mdr high ∥ nan.ty=3 | lma.mur/mdr=my last-mile up/down RTT ms |
| R06 | Relay/cross-region latency | tra > 🔴待定(prov.300) but lma segments low | tra=transportRTTAvg ms |
| R07 | High jitter | nan.ty=4 ∥ rja > 🔴待定(prov.100) | rja=recv jitter avg ms |
| R08/09 | Net switch / frequent | stl.ty=0 changes ≥1 / >3 | stl.ty=0=NetworkType (0 none/5 WiFi…) |
| R10 | VPN | stl.ty=1 v=1 | stl.ty=1=VPN event |
| R11 | Insufficient bandwidth | ekb < 🔴待定(prov.500) ∥ okb < 🔴待定(prov.300) ∥ nan.ty=6 | ekb=est. kbps; okb=actual uplink kbps |
| R12 | Weak network | nqs ≤ 🔴待定(prov.2) | nqs=networkQualityScore (1–5) |
| # | Cause | Trigger (🔴TBD) | Fields |
|---|---|---|---|
| R13 | Mic not working | arv=0 ∥ glc.ty=1 ∥ mpm=0 | arv=mic vol; mpm=mic permission |
| R14 | Camera not working | glc.ty=2 ∥ cpm=0 | cpm=camera permission |
| R15 | High CPU | vet/vdt > 🔴待定(prov.30) | vet/vdt=per-frame enc/dec time ms |
| R16 | Low memory | lme=1 OR amm < 🔴待定(prov.200) | lme=low-mem warn; amm=avail RAM MB |
| R17 | Thermal | tts ≥ 🔴待定(prov.2) ∥ stl.ty=4 v≥🔴待定(prov.2) | tts=thermalStatus (0..4); stl.ty=4=Thermal |
| R18/19 | Low battery / saver | btl < 🔴待定(prov.10) / bsm=1 | btl=battery %; bsm=power saver |
| R20 | Low-end device | ccr ≤ 🔴待定(prov.4) AND tmm < 🔴待定(prov.3000) | ccr=cores; tmm=total RAM MB |
| R21 | Bluetooth route | ble=1 ∥ stl.ty=5 flapping | ble=BT audio; stl.ty=5=AudioRoute |
| # | Cause | Trigger (🔴TBD) | Fields |
|---|---|---|---|
| R22/23 | Slow enc/dec | vet/vdt > 🔴待定(prov.30) | vet/vdt=per-frame enc/dec ms |
| R24 | Encoder downgrade | veq up ∥ stl.ty=10.r set | veq=QP; stl.ty=10.r=QualityLimitation |
| R25 | Poor FEC/PLC | apo low while alp/adp high | apo=original-packet % |
| R26 | Peer uplink loss | lma.pul > 🔴待定(prov.5) (inline) ∥ join peer sla | lma.pul=peer ul loss % (no join) |
| R27 | Peer device weak | join rid peer vet high | rid=roomId |
| R28 | Peer net down | tbr=0 AND tbs>0 ∥ lma.pdl very high | tbr/tbs=bytes recv/sent; lma.pdl=peer dl loss % |
| R29 | Peer app background | join peer stl.ty=7 v=1 | stl.ty=7=AppState (v=1 bg) |
| R30 | Bad server pick | tip region ≠ both AND tra > 🔴待定(prov.200) | tip=relay IP; tra=transport RTT |
| R31 | Slow ICE | ict > 🔴待定(prov.3000) | ict=media-setup ms |
| R32 | Server timeout drop | aer≠0 AND rsn = timeout code | aer=abnormalEnd; rsn=reason enum |
| R33 | Deliberate action (non-fault) | stl.ty=11/12 Mute ∥ ty=9 Hold | used to exclude false flags (see 00·5) |
| R34 | BWE crash | nan.ty=6 (BWECrash) | explains sudden freeze / downscale |
| R35 | Local app background | stl.ty=7 v=1 overlaps glc.ty=1/2 | explains background capture pre-emption |
rid — either leg bad → call bad. lma.pul/pdl inline the peer last-mile, so most peer attribution needs no join.vod; video uses vpd and only when vpd>0. glc.pem/nan.pem are ms — divide by 1000 to align with seconds.vod ≥ 🔴待定(prov.5s); vod < 🔴待定(prov.10s) abnormal-drop goes to C04.vod abnormally large (> 🔴待定prov.2h) and arv=0 AND apv=0 → bucketed separately, kept out of share stats.*Score are 1–5, higher = better, cutoffs 🔴TBD. Offsets ofm/of are relative to stm.rsn/aer enum table (hard blocker for S class) · join S class to signaling/push tables · per-codec QP calibration.