Packet Loss Handling
Opus has two mechanisms for coping with lost packets in real-time streams:
packet loss concealment (PLC) via decode_lost(),
and in-band forward error correction (FEC) via
decode_fec(). Both keep the decoder state consistent
so the stream can recover cleanly after loss.
Packet Loss Concealment
When a packet is lost, call decode_lost() instead of
decode_packet(). You must tell the decoder how many
samples to conceal; this should match the expected frame size of the lost
packet.
import ruopus
dec = ruopus.OpusDecoder(2)
for pkt in stream:
if pkt is None:
# Conceal one lost 20 ms frame
concealed = dec.decode_lost(frame_size=960)
else:
pcm = dec.decode_packet(pkt)
CELT-mode concealment extrapolates the last pitch period, producing a short
fade-out. Frames following a SILK or hybrid packet fade to silence (full SILK
PLC is not yet ported). The final_range of a concealed decode is 0.
In-Band FEC (LBRR)
When in-band FEC is enabled on the encoder, SILK-mode packets carry a
low-bitrate redundant (LBRR) copy of the previous frame’s audio alongside
the current frame. If a packet is lost, its content can be recovered from the
next received packet using decode_fec():
Enable FEC on the Encoder
import ruopus
enc = ruopus.OpusEncoder(
1,
bitrate=24_000,
application=ruopus.Application.Voip,
signal=ruopus.Signal.Voice,
inband_fec=True,
packet_loss_perc=10, # hint to the encoder: expect 10% loss
)
packet_loss_perc biases the encoder toward loss-robust coding (more
redundancy, lower raw bitrate efficiency). Values are clamped to 0-100.
Recover a Lost Packet Using FEC
dec = ruopus.OpusDecoder(1)
FRAME = 960 # 20 ms at 48 kHz
prev_pkt = None
for pkt in stream:
if pkt is None:
if prev_pkt is not None:
# next_pkt not yet available; use concealment as fallback
recovered = dec.decode_lost(FRAME)
# ... wait for next packet
else:
# If the previous slot was empty and we have a successor,
# try FEC recovery first:
if prev_pkt is None and next_pkt is not None:
recovered = dec.decode_fec(next_pkt, frame_size=FRAME)
pcm = dec.decode_packet(pkt)
prev_pkt = pkt
The decoder automatically falls back to plain concealment when the packet carries no usable FEC data (CELT-only modes, or when the requested frame size is longer than the LBRR frame).
Realistic Loss Handling Loop
A production loss handler typically buffers packets and decides on decode vs conceal vs FEC at playback time:
import ruopus
import numpy as np
enc = ruopus.OpusEncoder(
1,
bitrate=16_000,
application=ruopus.Application.Voip,
signal=ruopus.Signal.Voice,
inband_fec=True,
packet_loss_perc=5,
)
dec = ruopus.OpusDecoder(1)
FRAME = 960 # 20 ms
LOSS_RATE = 0.05 # simulate 5% loss
# Simulate transmission
rng = np.random.default_rng(42)
pcm_in = np.random.randn(FRAME * 10, 1).astype(np.float32)
packets = []
for i in range(0, len(pcm_in), FRAME):
pkt = enc.encode_auto(pcm_in[i:i+FRAME])
packets.append(pkt if rng.random() > LOSS_RATE else None)
# Decode with FEC / PLC
output_blocks = []
for i, pkt in enumerate(packets):
if pkt is not None:
output_blocks.append(dec.decode_packet(pkt))
elif i + 1 < len(packets) and packets[i + 1] is not None:
# FEC: use the next received packet to recover this lost one
output_blocks.append(dec.decode_fec(packets[i + 1], FRAME))
else:
# No next packet yet: conceal
output_blocks.append(dec.decode_lost(FRAME))
recovered = np.concatenate(output_blocks, axis=0)
print(f"Recovered {len(recovered) / 48_000:.2f} s of audio")
Tuning for Loss-Prone Networks
Setting |
Recommendation |
|---|---|
|
Enable on the encoder when SILK modes will be used (Voip / Voice). |
|
Set to the expected loss rate. Higher values increase redundancy overhead but improve recovery quality. |
|
|
Frame size |
20 ms is the most common SILK frame size and gives FEC the most redundancy budget. |