“The audio is choppy” is a symptom shared by packet loss, bad pacing, jitter-buffer behavior, transcoding pressure, browser scheduling, NAT and several application bugs.

That makes it tempting to change several things at once: codec preferences, jitter-buffer settings, WebRTC constraints, FreeSWITCH flags. Sometimes one of those changes appears to help. It also destroys the evidence needed to know why.

A better approach is to split the call into media legs and ask where the timing first becomes wrong.

One call contains several different truths

Consider a browser-based outbound call:

EXAMPLE

carrier <-> FreeSWITCH <-> WebRTC browser
                    |
                    +-> recording

The carrier RTP stream, browser-bound SRTP stream and recording are observations from different points in the pipeline. One can be healthy while another is broken.

We saw exactly that pattern with early media:

  • ringback and pre-answer audio in the browser sounded slow, intermittent and stretched;
  • the customer-leg recording was clean;
  • audio became normal after the remote party answered;
  • signalling completed successfully.

The clean recording initially made a codec or carrier problem look unlikely. It did not prove the browser path was healthy because the recording did not observe that path.

Write the symptom boundary before the hypothesis

Before capturing anything, describe where the defect appears:

EXAMPLE

direction:      customer -> browser
phase:          early media only
browser:        broken
recording:      clean
answered call:  clean
repeatability:  consistent

This small matrix prevents broad guesses. In our case it strongly suggested a difference between the early-media bridge path and the normal answered bridge.

It also ruled out one popular distraction. AVMD had been introduced later, while the defect existed before it. Without fresh evidence pointing back to AVMD, changing its settings would only add noise.

Collect evidence at each boundary

I want four sources for one correlated call.

1. Browser WebRTC statistics

chrome://webrtc-internals exposes inbound audio statistics from the browser’s point of view. Useful fields include:

  • packetsReceived and packetsLost;
  • jitter;
  • concealed samples;
  • inserted or removed samples for acceleration/deceleration;
  • jitter-buffer delay;
  • audio level;
  • codec and SSRC changes.

The browser view answers: what arrived and how did the browser compensate? It does not show what FreeSWITCH originally received from the carrier.

2. SIP signalling

The SIP trace establishes call phases and media addresses:

  • Did the carrier send 183 Session Progress with SDP?
  • Was the early-media codec the same as the answered codec?
  • Did the connection address or port change after 200 OK?
  • Was there a re-INVITE or UPDATE near the defect?

This anchors packet timing to signalling events. Without it, a packet capture is just a collection of streams.

3. Packet captures on both media legs

Capture near FreeSWITCH so the provider and browser directions share a clock. Then summarize RTP streams:

EXAMPLE

tshark -r call.pcapng -q -z rtp,streams

For WebRTC, the payload is encrypted, but packet timing, addresses, ports and sizes are still valuable. Compare:

  • packet count;
  • loss and sequence gaps;
  • mean and maximum inter-packet delta;
  • jitter;
  • whether timing changes at answer.

4. Media-server logs

FreeSWITCH logs reveal which code path and timer are active. Search for the channel UUIDs and markers around bridge setup, codec negotiation and RTP timers.

The goal is not to collect all logs. It is to connect one browser symptom to one provider stream, one browser stream and one pair of channel UUIDs.

The packet timing exposed the bug

On the provider/customer leg, early media was healthy:

EXAMPLE

723 packets over 14.4666 s
0% packet loss
mean delta 20.037 ms
mean jitter 0.342 ms

That is the expected shape for 20 ms PCMU packets.

The browser-bound stream was very different:

EXAMPLE

191 packets over 14.3426 s
mean delta 75.487 ms
most gaps between 70 and 130 ms

The carrier was not delivering choppy audio. FreeSWITCH received well-paced RTP and emitted under-paced SRTP toward the browser.

The decisive server-side clue was:

EXAMPLE

Not using a timer

on the internal WebRTC agent leg.

Why it affected only early media

The call used bridge_early_media=true so the browser agent could hear ringback and pre-answer announcements before the customer answered.

FreeSWITCH buffers this early media and emits it from the A-leg read loop. Without an RTP timer on the browser leg, sparse reads effectively stretched the outgoing packet cadence. The provider frames were clean in the buffer, and the customer-leg recording was clean, but delivery toward the browser was paced incorrectly.

Once the call answered, the normal media bridge followed a different timing path, so audio sounded fine. That phase boundary was not a coincidence; it was the architectural clue.

The fix belonged in the FreeSWITCH internal WebRTC Sofia profile:

EXAMPLE

<param name="rtp-timer-name" value="soft"/>

After rebuilding the runtime configuration, the log changed to:

EXAMPLE

Starting timer [soft] 160 bytes per 20ms

The browser-bound capture then showed:

EXAMPLE

290 packets over 5.7809 s
mean delta 20.003 ms
no gaps >= 70 ms

The audible result matched the measurements.

Why “the recording is fine” was useful but incomplete

A recording is evidence from the point where it is attached. It can prove that audio existed and was decodable there. It cannot prove that a later leg received it with correct timing.

This generalizes:

  • a clean carrier capture does not prove browser playback is clean;
  • a clean browser outbound track does not prove the carrier received it;
  • a successful local playback log does not prove a remote voicemail recorded it;
  • a healthy average jitter value does not explain a handful of large timing gaps.

Always name the observation point when making a conclusion.

A repeatable audio-debugging sequence

For one-way audio, dropouts or distorted timing, I use this order.

Step 1: Define direction and phase

Write down who cannot hear whom, and whether it happens during ringing, after answer, after transfer or only during AI playback.

Step 2: Correlate identifiers

Collect the application call ID, agent/customer channel UUIDs, SIP Call-ID, RTP endpoints and browser peer connection. Without correlation, evidence from simultaneous calls can be mixed accidentally.

Step 3: Verify the media topology

Confirm which legs actually exist. We once had successful customer signalling but no browser media because only the customer leg was originated. No browser setting can repair a missing media session.

Step 4: Compare both sides of the media server

Measure packet timing before and after FreeSWITCH. If input is wrong, inspect carrier, network and negotiated media. If input is clean and output is wrong, inspect media-server processing and pacing.

Step 5: Check the browser compensation signals

Use WebRTC stats to see whether the browser reports loss, concealment, jitter-buffer growth or timing adjustments. This distinguishes “packets never arrived” from “packets arrived irregularly.”

Step 6: Change one boundary

Make the narrowest change supported by the evidence, then repeat the same call and capture. A fix is much more convincing when the packet metric and audible symptom improve together.

What not to do first

Several actions are attractive because they are easy:

  • reorder codec preferences;
  • increase every jitter buffer;
  • blame NAT because the call uses WebRTC;
  • change browser autoplay handling;
  • disable unrelated media bugs;
  • compare only CPU averages;
  • accept a clean recording as end-to-end proof.

Any of these may be correct in another incident. None was supported by the first evidence in this one.

Audio debugging becomes much faster when the question changes from “what setting could cause choppy sound?” to “between which two observation points did 20 ms packets become 75 ms packets?”

That question has a measurable answer. Once we had it, the bug stopped being mysterious and became one missing timer parameter in the correct media leg.