mirror of
https://github.com/livekit/livekit.git
synced 2026-09-17 10:15:25 +00:00
162 lines
5.2 KiB
Go
162 lines
5.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"
|
|
|
|
"github.com/livekit/protocol/utils/mono"
|
|
)
|
|
|
|
const (
|
|
// DefaultProtectionPercent is the repair-to-media packet ratio when unspecified.
|
|
DefaultProtectionPercent uint32 = 0
|
|
MaxProtectionPercent uint32 = 100
|
|
// MediaPacketsPerGroup bounds both retained packet memory and encoding work.
|
|
MediaPacketsPerGroup = 5
|
|
maxEncoderGroupAge = 200 * time.Millisecond
|
|
// Reserve the FEC header and up to 16 bytes of outer RTP extensions (AST/TWCC).
|
|
// Groups of at most 15 packets use only the first FlexFEC packet mask.
|
|
maxEncoderMediaPacketSize = maxMediaPacketSize - pionflexfec.BaseFec03HeaderSize - 16
|
|
)
|
|
|
|
// 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
|
|
packets [MediaPacketsPerGroup]rtp.Packet
|
|
storage [MediaPacketsPerGroup][maxEncoderMediaPacketSize]byte
|
|
count int
|
|
startedAt int64
|
|
sequenceNumber uint16
|
|
closed bool
|
|
onSent func(packets int, bytes int)
|
|
protectionPercent uint32
|
|
repairCredit uint32
|
|
}
|
|
|
|
func NewEncoder(payloadType uint8, ssrc uint32, onSent func(packets int, bytes int)) *Encoder {
|
|
return &Encoder{
|
|
encoder: pionflexfec.NewFlexEncoder03(payloadType, ssrc),
|
|
sequenceNumber: uint16(rand.Uint32()),
|
|
onSent: onSent,
|
|
protectionPercent: DefaultProtectionPercent,
|
|
}
|
|
}
|
|
|
|
// Encode returns independently owned repair packets for complete groups only.
|
|
// Gaps (including skipped padding), codec switches and stale groups start a new
|
|
// group, since Pion requires consecutive sequence numbers. RTP wrap is valid.
|
|
// 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)
|
|
if header.Padding || len(payload) == 0 || size > maxEncoderMediaPacketSize {
|
|
e.count = 0
|
|
return nil
|
|
}
|
|
if e.count != 0 {
|
|
previous := &e.packets[e.count-1]
|
|
if header.SequenceNumber != previous.SequenceNumber+1 ||
|
|
header.SSRC != previous.SSRC || header.PayloadType != previous.PayloadType ||
|
|
now-e.startedAt > int64(maxEncoderGroupAge) {
|
|
e.count = 0
|
|
}
|
|
}
|
|
if e.count == 0 {
|
|
e.startedAt = now
|
|
}
|
|
|
|
raw := e.storage[e.count][:size]
|
|
n, err := header.MarshalTo(raw)
|
|
if err != nil {
|
|
e.count = 0
|
|
return nil
|
|
}
|
|
copy(raw[n:], payload)
|
|
if err = e.packets[e.count].Unmarshal(raw); err != nil {
|
|
e.count = 0
|
|
return nil
|
|
}
|
|
e.count++
|
|
if e.count < MediaPacketsPerGroup {
|
|
return nil
|
|
}
|
|
|
|
e.count = 0
|
|
// Carry fractional packets between groups rather than rounding each group
|
|
// up (which would turn e.g. 30% into 40%). Storage and latency stay bounded
|
|
// at five media packets, independent of the configured percentage.
|
|
e.repairCredit += e.protectionPercent * MediaPacketsPerGroup
|
|
numRepair := e.repairCredit / 100
|
|
e.repairCredit %= 100
|
|
if numRepair == 0 {
|
|
return nil
|
|
}
|
|
repair := e.encoder.EncodeFec(e.packets[:], numRepair)
|
|
for i := range repair {
|
|
// Pion's encoder uses a constant timestamp and 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 old credit.
|
|
// 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
|
|
e.count = 0
|
|
e.repairCredit = 0
|
|
}
|
|
e.mu.Unlock()
|
|
}
|
|
|
|
// RecordSent reports successful repair writes, with RTP payload bytes like the
|
|
// upstream counters. Callbacks run outside the encoder lock.
|
|
func (e *Encoder) RecordSent(packets int, bytes int) {
|
|
if e.onSent != nil && packets != 0 {
|
|
e.onSent(packets, bytes)
|
|
}
|
|
}
|
|
|
|
// Close prevents queued packets from generating repairs after unbind/close.
|
|
func (e *Encoder) Close() {
|
|
e.mu.Lock()
|
|
e.closed = true
|
|
e.count = 0
|
|
e.mu.Unlock()
|
|
}
|