Encode & decode

Packet loss

Packet loss

Realtime audio runs over lossy transports. Opus has two complementary tools for this, and libopus-wasm exposes both: packet-loss concealment (PLC) on the decoder, and in-band forward error correction (FEC) across encoder and decoder.

#Packet-loss concealment (PLC)

When a packet never arrives, ask the decoder to synthesize a replacement frame from its internal state. This keeps playback continuous instead of clicking or dropping to silence.

// A packet arrived: decode it normally.
const frame = decoder.decode(packet);

// The next packet was lost: conceal one frame.
const concealed = decoder.decodePacketLoss(); // defaults to a 20 ms frame

Pass an explicit frame size when your frames are not 20 ms:

decoder.decodePacketLoss(480);      // conceal a 10 ms frame at 48 kHz
decoder.decodePacketLossFloat(960); // Float32 variant

decodePacketLoss(frameSize) is exactly equivalent to decode(null, { frameSize }):

const concealed = decoder.decode(null, { frameSize: 960 });

That equivalence is why decodeFrames treats a null entry as a lost packet — a mixed array of packets and gaps decodes in one call:

const frames = decoder.decodeFrames([p0, null, p2]); // p1 was lost

PLC frame sizes must be a multiple of 2.5 ms (the Opus granularity), from 2.5 ms up to 120 ms. An invalid size throws a RangeError.

#Forward error correction (FEC)

FEC embeds a low-bitrate copy of the previous frame inside the current packet. If frame N is lost but frame N+1 arrives, the decoder can reconstruct N from N+1's FEC data — higher quality than PLC alone.

#Encode with FEC

Enable FEC and tell the encoder how lossy the channel is. The packet-loss percentage drives how much redundancy Opus spends:

const encoder = await createEncoder({
  fec: true,
  packetLossPercent: 15, // expected loss; tune to your transport
});

Both are also settable at runtime:

encoder.setFec(true);
encoder.setPacketLossPercent(15);

FEC only helps when the bitrate has room for the redundant copy. Pair it with a sensible bitrate and, for speech, Signal.Voice — see Encoder tuning.

#Decode the recovered frame

When you detect a gap and the next packet is in hand, decode that next packet with decodeFec to recover the lost frame first, then decode it again normally for its own audio:

// frame N was lost; we have packet N+1
const recovered = decoder.decode(nextPacket, {
  decodeFec: true,
  frameSize: 960, // size of the lost frame you are recovering
});

const current = decoder.decode(nextPacket); // now decode N+1 itself

decodeFec requires a real packet to read the redundant data from — combining decodeFec: true with a null packet throws a RangeError. When no next packet is available either, fall back to decodePacketLoss.

#A realtime receive loop

Putting it together for a jitter-buffer-style consumer:

function handlePacket(decoder, packet, lostFrameSize) {
  if (packet === null) {
    // Nothing arrived and no future packet to recover from.
    return decoder.decodePacketLoss(lostFrameSize);
  }
  return decoder.decode(packet);
}

For the FEC-recovery path, hold one packet of look-ahead so a lost frame can be rebuilt from the packet that follows it.

#Next