Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Call quality

Beyond was there audio (Audio testing), ringo-flow can assert on how good the audio was — the RTP media metrics each agent reports for its call, grouped under agent.quality:

FieldMeaningUnit
agent.quality.mosEstimated Mean Opinion Score1.0 (bad) – 4.5 (excellent)
agent.quality.rttRound-trip timemilliseconds
agent.quality.jitterReceive-side inter-arrival jittermilliseconds
agent.quality.packetLossReceive-side packet losspercent

The MOS is an estimate from the simplified ITU-T G.107 E-model, derived from latency, jitter and loss — a single number to gate call quality on.

When the values are available

The metrics come from RTCP reports, which the peers exchange only about every ~5 seconds. So right after the call is established agent.quality is still undefined — let the call run a few seconds first:

await until(() => expect(caller.quality).toBeDefined(), "10s");

The whole object appears at once (the fields arrive together, so there are no per-field gaps), which is why the optional chain in caller.quality?.mos is only needed before the first report.

The values are snapshotted when the call closes, so they survive the hangup — you can read or assert on them after the call, not just during it.

Example

// @ts-check
/** @type {Agent} */ let caller;
/** @type {Agent} */ let callee;

setup(() => {
  caller = new Agent("caller", { username: env("A_USER"), domain: env("SIP_DOMAIN"), password: env("A_PASS") });
  callee = new Agent("callee", { username: env("B_USER"), domain: env("SIP_DOMAIN"), password: env("B_PASS") });
});

scenario("call quality", async () => {
  caller.dial(callee);
  await until(() => expect(callee.state).toBe(State.Ringing));
  callee.accept();
  await until(() => expect(caller.state).toBe(State.Established));

  // Let RTCP accumulate, then wait for the first report:
  const q = await until(() => expect(caller.quality).toBeDefined().value(), "10s");

  log(`caller → MOS ${q.mos} · RTT ${q.rtt}ms · jitter ${q.jitter}ms · loss ${q.packetLoss}%`);

  caller.hangup();
  await until(() => expect(caller.state).toBe(State.Idle));

  // The snapshot survives the hangup — assert on the final values:
  expect(caller.quality?.mos).toBeGreaterThanOrEqual(4.0);
  expect(caller.quality?.packetLoss).toBeLessThanOrEqual(1.0);
  expect(caller.quality?.rtt).toBeLessThanOrEqual(150);
});

The values are raw floats (e.g. MOS 4.236…). To shorten a log line, round them: log(`MOS ${q.mos.toFixed(2)}`).

Exporting metrics

To record these values without writing assertions — e.g. for trend monitoring — run with --metrics. ringo-flow then emits a per-agent metric event (MOS, jitter, loss, RTT + registered) at each scenario’s end, which a machine consumer can scrape from the --json stream.