mirror of
https://github.com/livekit/livekit.git
synced 2026-09-17 10:15:25 +00:00
Generate repairs at each frame marker, splitting large frames into blocks of at most 48 packets. Match upstream Q8 repair-count rounding and minimum-one protection, and lazily reuse packet buffers. Reserve measured RTP repair overhead during layer allocation so small frames have enough headroom. Cover sparse frames, large-frame tails, buffer reuse, real Pion wire recovery, and live preset changes.
249 lines
8.2 KiB
Go
249 lines
8.2 KiB
Go
// Copyright 2026 LiveKit, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package flexfec
|
|
|
|
import (
|
|
"math/rand/v2"
|
|
"sync"
|
|
"time"
|
|
|
|
pionflexfec "github.com/pion/interceptor/pkg/flexfec"
|
|
"github.com/pion/rtp"
|
|
"go.uber.org/atomic"
|
|
"go.uber.org/zap/zapcore"
|
|
|
|
"github.com/livekit/protocol/utils/mono"
|
|
)
|
|
|
|
const (
|
|
// DefaultProtectionPercent is the repair-to-media packet ratio when unspecified.
|
|
DefaultProtectionPercent uint32 = 0
|
|
MaxProtectionPercent uint32 = 100
|
|
// MaxMediaPacketsPerGroup bounds memory and work for large frames. Like
|
|
// libwebrtc, use at most 48 media packets in a protection block.
|
|
MaxMediaPacketsPerGroup = 48
|
|
maxEncoderGroupAge = 200 * time.Millisecond
|
|
overheadUpdateInterval = time.Second
|
|
// Reserve all three FEC masks and up to 16 bytes of outer RTP extensions
|
|
// (AST/TWCC). A 48-packet group can require the third mask.
|
|
maxEncoderMediaPacketSize = maxMediaPacketSize - pionflexfec.BaseFec03HeaderSize - 12 - 16
|
|
)
|
|
|
|
// EncoderState preserves the repair sequence when a sender SSRC is reused.
|
|
// Partial media groups are never carried over to a new binding.
|
|
type EncoderState struct {
|
|
SSRC uint32
|
|
NextSequenceNumber uint16
|
|
}
|
|
|
|
func (s EncoderState) MarshalLogObject(e zapcore.ObjectEncoder) error {
|
|
e.AddUint32("SSRC", s.SSRC)
|
|
e.AddUint16("NextSequenceNumber", s.NextSequenceNumber)
|
|
return nil
|
|
}
|
|
|
|
type encoderMediaGroup struct {
|
|
packets [MaxMediaPacketsPerGroup]rtp.Packet
|
|
storage [MaxMediaPacketsPerGroup]*[maxEncoderMediaPacketSize]byte
|
|
}
|
|
|
|
// Encoder batches final, outgoing video packets for Pion's FlexFEC-03 encoder.
|
|
// It owns reusable wire storage: the caller may recycle headers, extensions and
|
|
// payloads as soon as Encode returns. It does not delay media or start workers.
|
|
// Methods are safe to call from concurrent pass-through pacer writes.
|
|
type Encoder struct {
|
|
mu sync.Mutex
|
|
encoder *pionflexfec.FlexEncoder03
|
|
media *encoderMediaGroup
|
|
count int
|
|
startedAt int64
|
|
sequenceNumber uint16
|
|
ssrc uint32
|
|
closed bool
|
|
onSent func(packets int, bytes int)
|
|
protectionPercent uint32
|
|
protectionFactor uint32
|
|
|
|
overheadPercent atomic.Uint32
|
|
overheadUpdatedAt int64
|
|
sentMediaBytes uint64
|
|
sentRepairBytes uint64
|
|
}
|
|
|
|
func NewEncoder(payloadType uint8, ssrc uint32, onSent func(packets int, bytes int)) *Encoder {
|
|
return &Encoder{
|
|
encoder: pionflexfec.NewFlexEncoder03(payloadType, ssrc),
|
|
sequenceNumber: uint16(rand.Uint32()),
|
|
ssrc: ssrc,
|
|
onSent: onSent,
|
|
protectionPercent: DefaultProtectionPercent,
|
|
}
|
|
}
|
|
|
|
func (e *Encoder) GetState() EncoderState {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
return EncoderState{SSRC: e.ssrc, NextSequenceNumber: e.sequenceNumber}
|
|
}
|
|
|
|
// SeedState must be called before forwarding starts. Only reuse sequencing for
|
|
// the same repair SSRC; a new SSRC keeps its randomized initial sequence number.
|
|
func (e *Encoder) SeedState(state EncoderState) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if e.closed || state.SSRC == 0 || state.SSRC != e.ssrc {
|
|
return
|
|
}
|
|
e.sequenceNumber = state.NextSequenceNumber
|
|
e.count = 0
|
|
}
|
|
|
|
// Encode returns independently owned repairs at the end of a video frame (the
|
|
// RTP marker), splitting large frames at MaxMediaPacketsPerGroup. Timestamp
|
|
// changes discard an incomplete previous frame. Gaps, codec switches and stale
|
|
// groups also reset the block; Pion requires consecutive sequence numbers.
|
|
// Media is never delayed. No timer or worker is needed for sparse frames.
|
|
// Call after the media write, including any interceptor header modifications.
|
|
func (e *Encoder) Encode(header *rtp.Header, payload []byte) []rtp.Packet {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if e.closed || e.protectionPercent == 0 {
|
|
return nil
|
|
}
|
|
|
|
now := mono.UnixNano()
|
|
size := header.MarshalSize() + len(payload)
|
|
e.sentMediaBytes += uint64(size)
|
|
if header.Padding || len(payload) == 0 || size > maxEncoderMediaPacketSize {
|
|
e.count = 0
|
|
return nil
|
|
}
|
|
if e.media == nil {
|
|
// Like the upstream recovery buffer, allocate packet storage only once
|
|
// it is needed. Negotiated tracks with the default "none" stay small.
|
|
e.media = &encoderMediaGroup{}
|
|
}
|
|
if e.count != 0 {
|
|
previous := &e.media.packets[e.count-1]
|
|
if header.SequenceNumber != previous.SequenceNumber+1 ||
|
|
header.SSRC != previous.SSRC || header.PayloadType != previous.PayloadType ||
|
|
header.Timestamp != previous.Timestamp ||
|
|
now-e.startedAt > int64(maxEncoderGroupAge) {
|
|
e.count = 0
|
|
}
|
|
}
|
|
if e.count == 0 {
|
|
e.startedAt = now
|
|
}
|
|
|
|
if e.media.storage[e.count] == nil {
|
|
// Retain only the packet buffers this stream has actually needed.
|
|
e.media.storage[e.count] = new([maxEncoderMediaPacketSize]byte)
|
|
}
|
|
raw := e.media.storage[e.count][:size]
|
|
n, err := header.MarshalTo(raw)
|
|
if err != nil {
|
|
e.count = 0
|
|
return nil
|
|
}
|
|
copy(raw[n:], payload)
|
|
if err = e.media.packets[e.count].Unmarshal(raw); err != nil {
|
|
e.count = 0
|
|
return nil
|
|
}
|
|
e.count++
|
|
if !header.Marker && e.count < MaxMediaPacketsPerGroup {
|
|
return nil
|
|
}
|
|
|
|
count := e.count
|
|
e.count = 0
|
|
// Match libwebrtc's ForwardErrorCorrection::NumFecPackets: round the Q8
|
|
// protection factor, with at least one repair for every protected block.
|
|
// In particular, a one-packet frame is protected immediately at every preset.
|
|
numRepair := max(uint32(1), (uint32(count)*e.protectionFactor+128)>>8)
|
|
repair := e.encoder.EncodeFec(e.media.packets[:count], numRepair)
|
|
for i := range repair {
|
|
// Pion's encoder uses a constant timestamp and a fixed initial sequence number.
|
|
// Use this stream's media clock and a randomized, continuous repair sequence.
|
|
repair[i].Timestamp = header.Timestamp
|
|
repair[i].SequenceNumber = e.sequenceNumber
|
|
e.sequenceNumber++
|
|
}
|
|
return repair
|
|
}
|
|
|
|
// SetProtectionPercent updates the packet ratio without changing the repair
|
|
// SSRC or sequence number. A new value discards partial groups and measurements.
|
|
// Values above 100 are capped; zero disables encoding before any packet copies.
|
|
func (e *Encoder) SetProtectionPercent(percent uint32) {
|
|
percent = min(percent, MaxProtectionPercent)
|
|
e.mu.Lock()
|
|
if e.protectionPercent != percent {
|
|
e.protectionPercent = percent
|
|
// Publish-track presets use the same conversion to libwebrtc's Q8 rate.
|
|
e.protectionFactor = percent * 255 / 100
|
|
e.count = 0
|
|
e.overheadPercent.Store(percent)
|
|
e.overheadUpdatedAt = 0
|
|
e.sentMediaBytes = 0
|
|
e.sentRepairBytes = 0
|
|
}
|
|
e.mu.Unlock()
|
|
}
|
|
|
|
// OverheadPercent reserves at least the configured rate, using measured RTP
|
|
// bytes when frame rounding or repair headers increase the actual overhead.
|
|
// Reads by the allocator do not contend with encoding.
|
|
func (e *Encoder) OverheadPercent() uint32 {
|
|
return e.overheadPercent.Load()
|
|
}
|
|
|
|
// RecordSent accounts successful repairs, including headers for the allocator
|
|
// and payload bytes for the upstream-compatible telemetry callback. Refresh the
|
|
// estimate on the first repair and at most once per second thereafter.
|
|
// Callbacks run outside the encoder lock.
|
|
func (e *Encoder) RecordSent(packets int, payloadBytes int, rtpBytes int) {
|
|
if packets == 0 {
|
|
return
|
|
}
|
|
e.mu.Lock()
|
|
if !e.closed && e.protectionPercent != 0 {
|
|
e.sentRepairBytes += uint64(rtpBytes)
|
|
now := mono.UnixNano()
|
|
if e.sentMediaBytes != 0 && (e.overheadUpdatedAt == 0 || now-e.overheadUpdatedAt >= int64(overheadUpdateInterval)) {
|
|
percent := uint32((e.sentRepairBytes*100 + e.sentMediaBytes - 1) / e.sentMediaBytes)
|
|
e.overheadPercent.Store(max(e.protectionPercent, percent))
|
|
e.overheadUpdatedAt = now
|
|
e.sentMediaBytes = 0
|
|
e.sentRepairBytes = 0
|
|
}
|
|
}
|
|
e.mu.Unlock()
|
|
if e.onSent != nil {
|
|
e.onSent(packets, payloadBytes)
|
|
}
|
|
}
|
|
|
|
// Close prevents queued packets from generating repairs after unbind/close.
|
|
func (e *Encoder) Close() {
|
|
e.mu.Lock()
|
|
e.closed = true
|
|
e.count = 0
|
|
e.media = nil
|
|
e.encoder = nil
|
|
e.mu.Unlock()
|
|
}
|