Browser display capture could remain low-resolution after a temporary bandwidth reduction because sender adaptation also reduced the shared capture input. The encoded-output carrier could separately reduce frame rate after the real producer had already adapted, causing repeated queue recovery. Keep display capture and sender feedback separate through bounded native frame streams, retain capture-owned constraints and metadata, and preserve carrier cadence. Existing WebRTC picture adaptation and transport ownership remain in place. Unsupported frame APIs retain ordinary sender tracks. Real display acceptance also exposed an existing VP8 forwarding defect: a 15 fps L1T3 stream reached the SFU subscriber at about 4 fps because both shared forwarding owners overrode LiveKit's temporal limit with T0. Removing those overrides restores the library's layer selection for SFU and Native forwarding. ## Validation - Web type checking, 1,186 tests and production build passed. - Go tests, formatting, vet, Windows/Linux builds and Windows native capture checks passed; macOS packaging runs in the existing native CI job. - H.264/VP8 real-tab first-frame, live/paused settings, source replacement, failure retirement and two-Viewer shaped-network checks passed. - Real display through embedded SFU passed H.264 and VP8: hidden Host, two decoded spatial layers, resumed delivery, track replacement, profile updates, subscription recovery and full cleanup. VP8 decoded cadence recovered from about 4 to 15 fps. Track replacement uses the same visual source, and these checks do not measure A/V synchronization or physical mobile backgrounding. - Shared RTP tests cover all three VP8 temporal layers through Transport and Publication; restoring the old T0 caps makes both tests fail. - Documentation and repository hygiene checks passed. ## Release notes ### 中文 这是一次浏览器分享稳定性修复更新,重点改善网络波动后的画质恢复和编码复用时的画面流畅度。 - 修复部分 Chromium 浏览器屏幕共享降画质后,采集输入持续停留在低分辨率的问题。 - 修复编码复用路径重复降低帧率,导致画面卡顿和反复请求恢复帧的问题。 - 修复部分 VP8 画面经过媒体服务器或 App 转发时,帧率被错误限制在最低时间层的问题。 - 网页用户请在站点更新后刷新页面并重新开始分享;App 用户请更新并重启 App。 复杂画面的恢复速度和不同设备、网络下的短时清晰度波动仍在继续验证,本次更新不代表所有画质波动都已解决。 ### English This maintenance update improves browser sharing stability, focusing on picture recovery after network fluctuations and smoother delivery when encoded frames are reused. - Fixed an issue in some Chromium browsers where display capture could remain at a low resolution after the sender reduced picture quality. - Fixed repeated frame-rate reduction in the encoding-reuse path that could cause stuttering and repeated recovery-frame requests. - Fixed an unintended frame-rate limit when VP8 video with temporal layers passed through the media server or Piik App's forwarding path. - Web users should refresh the page and restart sharing after their site is updated. App users should update and restart Piik App. Recovery with complex content and brief quality fluctuations across different devices and networks remain under investigation. This update does not claim to eliminate every quality fluctuation.
647 lines
22 KiB
Go
647 lines
22 KiB
Go
package forwarding
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/TNTcraftHIM/Piik/internal/diagnostics"
|
|
"github.com/livekit/livekit-server/pkg/sfu"
|
|
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
|
"github.com/livekit/livekit-server/pkg/sfu/bwe"
|
|
"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
|
|
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
|
|
"github.com/livekit/livekit-server/pkg/sfu/pacer"
|
|
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
|
|
"github.com/livekit/protocol/livekit"
|
|
"github.com/pion/interceptor"
|
|
"github.com/pion/rtcp"
|
|
"github.com/pion/rtp"
|
|
"github.com/pion/transport/v4/packetio"
|
|
"github.com/pion/webrtc/v4"
|
|
)
|
|
|
|
const publicationRIDExtension = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
|
|
|
|
// Publication sends existing representations over one simulcast connection.
|
|
// LiveKit's allocator owns all track allocations and probes on the shared BWE.
|
|
type Publication struct {
|
|
PC *webrtc.PeerConnection
|
|
source *Source
|
|
tracks []*sfu.DownTrack
|
|
counters []publicationCounters
|
|
audioBytes atomic.Uint64
|
|
allocator *streamallocator.StreamAllocator
|
|
bandwidth *publicationBWE
|
|
pacer *publicationPacer
|
|
mu sync.Mutex
|
|
writeMu sync.Mutex
|
|
attached bool
|
|
closed bool
|
|
activeCount int
|
|
readers sync.WaitGroup
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
}
|
|
|
|
func NewPublication(options TransportOptions) (_ *Publication, err error) {
|
|
if options.Source == nil || options.ConnectionID == "" || options.InitialBitrate <= 0 {
|
|
return nil, errors.New("publication options are invalid")
|
|
}
|
|
publication := &Publication{source: options.Source, activeCount: len(options.Source.TrackInfo().Layers)}
|
|
publication.counters = make([]publicationCounters, publication.activeCount)
|
|
defer func() {
|
|
if err != nil {
|
|
_ = publication.Close()
|
|
}
|
|
}()
|
|
log := diagnostics.MediaLogger("publication").WithValues("connectionId", diagnostics.ID(options.ConnectionID))
|
|
publication.bandwidth = &publicationBWE{SendSideBWE: sendsidebwe.NewSendSideBWE(sendsidebwe.SendSideBWEParams{
|
|
Config: sendsidebwe.DefaultSendSideBWEConfig, Logger: log,
|
|
})}
|
|
publication.bandwidth.target.Store(int64(options.InitialBitrate))
|
|
publication.pacer = &publicationPacer{headers: make(map[uint32]publicationHeaders), boundedPacer: newBoundedPacer(
|
|
pacer.NewLeakyBucket(log, nil, 5*time.Millisecond, options.InitialBitrate), log, options.Source.maxPackets,
|
|
), bandwidth: publication.bandwidth}
|
|
publication.pacer.sender = pacer.NewBase(log, publication.bandwidth)
|
|
publication.bandwidth.pacer = publication.pacer
|
|
publication.allocator = streamallocator.NewStreamAllocator(streamallocator.StreamAllocatorParams{
|
|
Config: streamallocator.DefaultStreamAllocatorConfig, BWE: publication.bandwidth,
|
|
Pacer: publication.pacer, Logger: log,
|
|
}, true, true)
|
|
publication.allocator.Start()
|
|
publication.pacer.SetBitrate(options.InitialBitrate)
|
|
media := &webrtc.MediaEngine{}
|
|
codec := options.Source.Codec()
|
|
if err = media.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, uri := range []string{
|
|
"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
|
|
"urn:ietf:params:rtp-hdrext:sdes:mid", publicationRIDExtension,
|
|
} {
|
|
if err = media.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: uri}, webrtc.RTPCodecTypeVideo); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if options.Audio != nil {
|
|
audio, ok := options.Audio.(interface {
|
|
Codec() webrtc.RTPCodecCapability
|
|
})
|
|
if !ok {
|
|
return nil, errors.New("publication audio codec is unavailable")
|
|
}
|
|
if err = media.RegisterCodec(webrtc.RTPCodecParameters{RTPCodecCapability: audio.Codec(), PayloadType: 111}, webrtc.RTPCodecTypeAudio); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
factory := buffer.NewFactoryOfBufferFactory(options.Source.maxPackets, buffer.InitPacketBufferSizeAudio).CreateBufferFactory()
|
|
settings := options.Settings
|
|
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
|
|
settings.LoggerFactory = diagnostics.PionLoggerFactory("connectionId", diagnostics.ID(options.ConnectionID))
|
|
}
|
|
settings.BufferFactory = func(kind packetio.BufferPacketType, ssrc uint32) io.ReadWriteCloser {
|
|
if kind == packetio.RTCPBufferPacket {
|
|
if reader := factory.GetRTCPReader(ssrc); reader != nil {
|
|
return reader
|
|
}
|
|
}
|
|
input := packetio.NewBuffer()
|
|
limit := 1_000_000
|
|
if kind == packetio.RTCPBufferPacket {
|
|
limit = 100_000
|
|
}
|
|
input.SetLimitSize(limit)
|
|
return input
|
|
}
|
|
registry := &interceptor.Registry{}
|
|
if err = webrtc.ConfigureRTCPReports(registry); err != nil {
|
|
return nil, err
|
|
}
|
|
publication.PC, err = webrtc.NewAPI(webrtc.WithMediaEngine(media), webrtc.WithInterceptorRegistry(registry),
|
|
webrtc.WithSettingEngine(settings)).NewPeerConnection(webrtc.Configuration{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var transceiver *webrtc.RTPTransceiver
|
|
// Pion's diagnostic PC.ID is clock-based and can repeat. Source membership
|
|
// needs a unique physical owner even when signaling reuses a connection ID.
|
|
subscriberID := rand.Text()
|
|
for index := range options.Source.TrackInfo().Layers {
|
|
receiver := &publicationLayer{Source: options.Source, layer: int32(index), publication: publication}
|
|
track, trackErr := sfu.NewDownTrack(sfu.DownTrackParams{
|
|
Codecs: []webrtc.RTPCodecParameters{codec}, Source: livekit.TrackSource_SCREEN_SHARE,
|
|
Receiver: receiver, BufferFactory: factory,
|
|
SubID: livekit.ParticipantID(subscriberID + "_" + strconv.Itoa(index)),
|
|
StreamID: options.Source.StreamID(), MaxTrack: options.Source.maxPackets,
|
|
Pacer: publication.pacer, Logger: log, Listener: publication,
|
|
RTCPWriter: publication.PC.WriteRTCP, DisableSenderReportPassThrough: true,
|
|
EnableStartAtDesiredQuality: true,
|
|
})
|
|
if trackErr != nil {
|
|
return nil, trackErr
|
|
}
|
|
publication.tracks = append(publication.tracks, track)
|
|
track.SetMaxSpatialLayer(int32(index))
|
|
local := &publicationTrack{DownTrack: track, id: options.Source.TrackID(), rid: PublicationRID(index, publication.activeCount), pacer: publication.pacer, counters: &publication.counters[index]}
|
|
if transceiver == nil {
|
|
transceiver, err = publication.PC.AddTransceiverFromTrack(local, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
|
|
} else {
|
|
err = transceiver.Sender().AddEncoding(local)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
local.transceiver = transceiver
|
|
track.SetTransceiver(transceiver)
|
|
publication.allocator.AddTrack(track, streamallocator.AddTrackParams{
|
|
Source: livekit.TrackSource_SCREEN_SHARE, IsMultiLayered: true,
|
|
PublisherID: livekit.ParticipantID(options.Source.StreamID()),
|
|
})
|
|
}
|
|
if options.Audio != nil {
|
|
sender, audioErr := publication.PC.AddTrack(&publicationAudio{TrackLocal: options.Audio, bytes: &publication.audioBytes})
|
|
if audioErr != nil {
|
|
return nil, audioErr
|
|
}
|
|
publication.readers.Add(1)
|
|
go func() {
|
|
defer publication.readers.Done()
|
|
for {
|
|
if _, _, readErr := sender.ReadRTCP(); readErr != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
return publication, nil
|
|
}
|
|
|
|
func PublicationRID(layer, count int) string {
|
|
if count == 1 {
|
|
return ""
|
|
}
|
|
return []string{"q", "h", "f"}[layer]
|
|
}
|
|
|
|
func (publication *Publication) SetConnected() error {
|
|
publication.mu.Lock()
|
|
defer publication.mu.Unlock()
|
|
if publication.closed {
|
|
return io.ErrClosedPipe
|
|
}
|
|
if publication.attached {
|
|
return nil
|
|
}
|
|
for _, track := range publication.tracks {
|
|
receiver := track.Receiver().(*publicationLayer)
|
|
receiver.attached = true
|
|
if err := receiver.AddDownTrack(track); err != nil {
|
|
receiver.attached = false
|
|
return err
|
|
}
|
|
track.SetConnected()
|
|
}
|
|
publication.attached = true
|
|
return nil
|
|
}
|
|
|
|
func (publication *Publication) CurrentSource() *Source {
|
|
publication.mu.Lock()
|
|
defer publication.mu.Unlock()
|
|
return publication.source
|
|
}
|
|
|
|
// ReplaceSource keeps every RID, allocator, counter and sender on this PC.
|
|
func (publication *Publication) ReplaceSource(next *Source) error {
|
|
recovery := false
|
|
// Source feedback may retire this publication; release all locks first.
|
|
defer func() {
|
|
if recovery {
|
|
for layer := range next.TrackInfo().Layers {
|
|
next.SendPLI(int32(layer), true)
|
|
}
|
|
}
|
|
}()
|
|
publication.mu.Lock()
|
|
defer publication.mu.Unlock()
|
|
if publication.closed {
|
|
return io.ErrClosedPipe
|
|
}
|
|
if next == nil {
|
|
return errors.New("replacement publication source is absent")
|
|
}
|
|
next.mu.Lock()
|
|
defer next.mu.Unlock()
|
|
if err := validateReplacement(publication.source, next); err != nil {
|
|
return err
|
|
}
|
|
if next == publication.source {
|
|
return nil
|
|
}
|
|
publication.writeMu.Lock()
|
|
defer publication.writeMu.Unlock()
|
|
for _, track := range publication.tracks {
|
|
if track.IsClosed() {
|
|
return io.ErrClosedPipe
|
|
}
|
|
}
|
|
for index, track := range publication.tracks {
|
|
receiver := &publicationLayer{Source: next, layer: int32(index), publication: publication, attached: publication.attached}
|
|
track.ReceiverRestart(track.Receiver())
|
|
track.SetReceiver(receiver)
|
|
// The subscribed max layer did not change, so SetReceiver's layer
|
|
// notification alone does not wake the allocator for this fresh input.
|
|
publication.allocator.OnSubscriptionChanged(track)
|
|
}
|
|
publication.source = next
|
|
recovery = publication.attached
|
|
return nil
|
|
}
|
|
|
|
func (publication *Publication) SetActiveCount(count int) error {
|
|
publication.mu.Lock()
|
|
defer publication.mu.Unlock()
|
|
if publication.closed {
|
|
return io.ErrClosedPipe
|
|
}
|
|
if count < 0 || count > len(publication.tracks) {
|
|
return errors.New("publication layer demand is invalid")
|
|
}
|
|
publication.activeCount = count
|
|
for index, track := range publication.tracks {
|
|
// Publisher demand is authoritative even while allocation is paused;
|
|
// subscription visibility mute may be ignored by the framework then.
|
|
track.PubMute(index >= count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (publication *Publication) RequiredActiveCount() int {
|
|
publication.mu.Lock()
|
|
defer publication.mu.Unlock()
|
|
if publication.closed || !publication.attached {
|
|
return 0
|
|
}
|
|
// The allocator still needs actual input for requested encodings when it
|
|
// probes recovery. This is codec demand, not affordable or delivered upload.
|
|
return publication.activeCount
|
|
}
|
|
|
|
func (publication *Publication) SetAudioBitrate(bitrate uint32) {
|
|
publication.bandwidth.audio.Store(int64(bitrate))
|
|
}
|
|
func (publication *Publication) VideoBudget() int64 {
|
|
return max(publication.bandwidth.target.Load()-publication.bandwidth.audio.Load(), 0)
|
|
}
|
|
func (publication *Publication) LowestLayerBudget() int64 {
|
|
remaining := publication.VideoBudget()
|
|
for _, track := range publication.tracks[1:] {
|
|
remaining -= track.BandwidthRequested()
|
|
}
|
|
return max(remaining, 0)
|
|
}
|
|
|
|
type PublicationCounters struct {
|
|
Frames []uint64
|
|
VideoBytes, AudioBytes uint64
|
|
TargetBitrate int64
|
|
Observed, Limited bool
|
|
}
|
|
|
|
func (publication *Publication) Counters() PublicationCounters {
|
|
publication.mu.Lock()
|
|
defer publication.mu.Unlock()
|
|
stats := PublicationCounters{Frames: make([]uint64, len(publication.counters)),
|
|
AudioBytes: publication.audioBytes.Load(), TargetBitrate: publication.bandwidth.target.Load(),
|
|
Observed: publication.bandwidth.observed.Load(), Limited: publication.bandwidth.CongestionState() == bwe.CongestionStateCongested}
|
|
for index := range publication.counters {
|
|
stats.Frames[index] = publication.counters[index].frames.Load()
|
|
stats.VideoBytes += publication.counters[index].bytes.Load()
|
|
if index < publication.activeCount && publication.tracks[index].IsDeficient() {
|
|
stats.Limited = true
|
|
}
|
|
}
|
|
return stats
|
|
}
|
|
|
|
func (publication *Publication) Close() error {
|
|
publication.closeOnce.Do(func() {
|
|
publication.mu.Lock()
|
|
publication.closed = true
|
|
publication.mu.Unlock()
|
|
if publication.PC != nil {
|
|
publication.closeErr = publication.PC.Close()
|
|
}
|
|
if publication.allocator != nil {
|
|
publication.allocator.Stop()
|
|
}
|
|
for _, track := range publication.tracks {
|
|
publication.source.DeleteDownTrack(track.SubscriberID())
|
|
track.Close()
|
|
}
|
|
if publication.pacer != nil {
|
|
publication.pacer.Stop()
|
|
}
|
|
publication.readers.Wait()
|
|
})
|
|
return publication.closeErr
|
|
}
|
|
|
|
func (*Publication) OnBindAndConnected() {}
|
|
func (publication *Publication) OnStatsUpdate(stats *livekit.AnalyticsStat) {
|
|
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
|
|
slog.Debug("media-stats", "scope", "publication", "rtcPeerId", diagnostics.ID(publication.PC.ID()),
|
|
"rtp", stats, "egress", publication.pacer.stats(), "targetBitrate", publication.bandwidth.target.Load(),
|
|
"pendingPackets", publication.pacer.pendingCount(), "packetCapacity", publication.pacer.limit)
|
|
}
|
|
}
|
|
func (*Publication) OnMaxSubscribedLayerChanged(int32) {}
|
|
func (publication *Publication) OnRttUpdate(rtt uint32) {
|
|
publication.bandwidth.UpdateRTT(float64(rtt) / 1000)
|
|
}
|
|
func (*Publication) OnCodecNegotiated(webrtc.RTPCodecCapability) {}
|
|
func (*Publication) OnDownTrackClose(bool) {}
|
|
func (*Publication) OnStreamStarted(time.Duration) {}
|
|
|
|
// Each RID exposes precisely one existing layer to the allocator; another RID
|
|
// can never substitute the same lower output and charge it twice.
|
|
type publicationLayer struct {
|
|
*Source
|
|
layer int32
|
|
publication *Publication
|
|
attached bool
|
|
}
|
|
|
|
func (layer *publicationLayer) AddDownTrack(sender sfu.TrackSender) error {
|
|
if !layer.attached {
|
|
return nil
|
|
}
|
|
filtered := &publicationLayerSender{
|
|
DownTrack: sender.(*sfu.DownTrack), layer: layer.layer, receiver: layer,
|
|
}
|
|
filtered.registering.Store(true)
|
|
err := layer.Source.AddDownTrack(filtered)
|
|
filtered.registering.Store(false)
|
|
return err
|
|
}
|
|
|
|
func (layer *publicationLayer) TrackID() livekit.TrackID {
|
|
return livekit.TrackID(string(layer.Source.TrackID()) + "_" + strconv.Itoa(int(layer.layer)))
|
|
}
|
|
func (layer *publicationLayer) GetLayeredBitrate() ([]int32, sfu.Bitrates) {
|
|
available, rates := layer.Source.GetLayeredBitrate()
|
|
return onlyPublicationLayer(layer.layer, available, rates)
|
|
}
|
|
func (*publicationLayer) SetMaxExpectedSpatialLayer(int32) {}
|
|
|
|
func onlyPublicationLayer(layer int32, available []int32, rates sfu.Bitrates) ([]int32, sfu.Bitrates) {
|
|
var selected sfu.Bitrates
|
|
selected[layer] = rates[layer]
|
|
for _, index := range available {
|
|
if index == layer {
|
|
return []int32{layer}, selected
|
|
}
|
|
}
|
|
return nil, selected
|
|
}
|
|
|
|
type publicationLayerSender struct {
|
|
*sfu.DownTrack
|
|
layer int32
|
|
receiver *publicationLayer
|
|
registering atomic.Bool
|
|
}
|
|
|
|
func (sender *publicationLayerSender) UpTrackBitrateReport(available []int32, rates sfu.Bitrates) {
|
|
sender.withReceiver(func() {
|
|
available, rates = onlyPublicationLayer(sender.layer, available, rates)
|
|
sender.DownTrack.UpTrackBitrateReport(available, rates)
|
|
})
|
|
}
|
|
func (sender *publicationLayerSender) WriteRTP(packet *buffer.ExtPacket, layer int32) int32 {
|
|
sender.receiver.publication.writeMu.Lock()
|
|
defer sender.receiver.publication.writeMu.Unlock()
|
|
if sender.Receiver() != sender.receiver {
|
|
return 0
|
|
}
|
|
if layer != sender.layer {
|
|
return 0
|
|
}
|
|
return sender.DownTrack.WriteRTP(packet, layer)
|
|
}
|
|
|
|
func (sender *publicationLayerSender) Close() {
|
|
sender.withReceiver(sender.DownTrack.Close)
|
|
}
|
|
|
|
func (sender *publicationLayerSender) withReceiver(fn func()) {
|
|
if !sender.registering.Load() {
|
|
sender.receiver.publication.writeMu.Lock()
|
|
defer sender.receiver.publication.writeMu.Unlock()
|
|
}
|
|
if sender.Receiver() == sender.receiver {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
func (sender *publicationLayerSender) ReceiverRestart(receiver sfu.TrackReceiver) {
|
|
sender.withReceiver(func() { sender.DownTrack.ReceiverRestart(receiver) })
|
|
}
|
|
|
|
func (sender *publicationLayerSender) UpTrackLayersChange() {
|
|
sender.withReceiver(sender.DownTrack.UpTrackLayersChange)
|
|
}
|
|
|
|
func (sender *publicationLayerSender) UpTrackBitrateAvailabilityChange() {
|
|
sender.withReceiver(sender.DownTrack.UpTrackBitrateAvailabilityChange)
|
|
}
|
|
|
|
func (sender *publicationLayerSender) UpTrackMaxPublishedLayerChange(layer int32) {
|
|
sender.withReceiver(func() { sender.DownTrack.UpTrackMaxPublishedLayerChange(layer) })
|
|
}
|
|
|
|
func (sender *publicationLayerSender) UpTrackMaxTemporalLayerSeenChange(layer int32) {
|
|
sender.withReceiver(func() { sender.DownTrack.UpTrackMaxTemporalLayerSeenChange(layer) })
|
|
}
|
|
|
|
func (sender *publicationLayerSender) HandleRTCPSenderReportData(payloadType webrtc.PayloadType, layer int32, report *livekit.RTCPSenderReportState) error {
|
|
sender.receiver.publication.writeMu.Lock()
|
|
defer sender.receiver.publication.writeMu.Unlock()
|
|
if sender.Receiver() != sender.receiver {
|
|
return nil
|
|
}
|
|
return sender.DownTrack.HandleRTCPSenderReportData(payloadType, layer, report)
|
|
}
|
|
|
|
type publicationTrack struct {
|
|
*sfu.DownTrack
|
|
id livekit.TrackID
|
|
rid string
|
|
transceiver *webrtc.RTPTransceiver
|
|
pacer *publicationPacer
|
|
counters *publicationCounters
|
|
}
|
|
|
|
func (track *publicationTrack) ID() string { return string(track.id) }
|
|
func (track *publicationTrack) RID() string { return track.rid }
|
|
func (track *publicationTrack) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) {
|
|
var extension, midExtension uint8
|
|
for _, header := range context.HeaderExtensions() {
|
|
if header.URI == publicationRIDExtension {
|
|
extension = uint8(header.ID)
|
|
}
|
|
if header.URI == "urn:ietf:params:rtp-hdrext:sdes:mid" {
|
|
midExtension = uint8(header.ID)
|
|
}
|
|
}
|
|
if track.rid != "" && extension == 0 {
|
|
return webrtc.RTPCodecParameters{}, errors.New("publication RID was not negotiated")
|
|
}
|
|
track.pacer.headersMu.Lock()
|
|
track.pacer.headers[uint32(context.SSRC())] = publicationHeaders{rid: track.rid, extension: extension,
|
|
mid: track.transceiver.Mid(), midExtension: midExtension, counters: track.counters}
|
|
track.pacer.headersMu.Unlock()
|
|
return track.DownTrack.Bind(context)
|
|
}
|
|
|
|
type publicationHeaders struct {
|
|
rid, mid string
|
|
extension, midExtension uint8
|
|
counters *publicationCounters
|
|
}
|
|
|
|
type publicationCounters struct{ frames, bytes atomic.Uint64 }
|
|
|
|
type publicationWriter struct {
|
|
webrtc.TrackLocalWriter
|
|
bytes *atomic.Uint64
|
|
frames *atomic.Uint64
|
|
primary bool
|
|
}
|
|
|
|
func (writer *publicationWriter) WriteRTP(header *rtp.Header, payload []byte) (int, error) {
|
|
written, err := writer.TrackLocalWriter.WriteRTP(header, payload)
|
|
if err == nil && written > 0 {
|
|
writer.bytes.Add(uint64(header.MarshalSize() + len(payload)))
|
|
if writer.frames != nil && writer.primary && header.Marker && len(payload) > 0 {
|
|
writer.frames.Add(1)
|
|
}
|
|
}
|
|
return written, err
|
|
}
|
|
|
|
type publicationAudio struct {
|
|
webrtc.TrackLocal
|
|
bytes *atomic.Uint64
|
|
}
|
|
|
|
func (audio *publicationAudio) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) {
|
|
return audio.TrackLocal.Bind(publicationAudioContext{TrackLocalContext: context,
|
|
writer: &publicationWriter{TrackLocalWriter: context.WriteStream(), bytes: audio.bytes}})
|
|
}
|
|
|
|
type publicationAudioContext struct {
|
|
webrtc.TrackLocalContext
|
|
writer webrtc.TrackLocalWriter
|
|
}
|
|
|
|
func (context publicationAudioContext) WriteStream() webrtc.TrackLocalWriter { return context.writer }
|
|
|
|
// Opus uses its one existing encoder at an explicit fixed ceiling. Reserve its
|
|
// bytes from allocator capacities. TWCC observes video; reserving unobserved
|
|
// audio is conservative and does not claim that the estimator acknowledged it.
|
|
type publicationBWE struct {
|
|
*sendsidebwe.SendSideBWE
|
|
listener bwe.BWEListener
|
|
target atomic.Int64
|
|
audio atomic.Int64
|
|
observed atomic.Bool
|
|
pacer *publicationPacer
|
|
probeMu sync.Mutex
|
|
completedProbe ccutils.ProbeClusterInfo
|
|
}
|
|
|
|
func (bandwidth *publicationBWE) HandleTWCCFeedback(report *rtcp.TransportLayerCC) {
|
|
bandwidth.observed.Store(true)
|
|
bandwidth.SendSideBWE.HandleTWCCFeedback(report)
|
|
}
|
|
|
|
func (bandwidth *publicationBWE) SetBWEListener(listener bwe.BWEListener) {
|
|
bandwidth.listener = listener
|
|
bandwidth.SendSideBWE.SetBWEListener(bandwidth)
|
|
}
|
|
func (bandwidth *publicationBWE) OnCongestionStateChange(from, to bwe.CongestionState, capacity int64) {
|
|
if capacity > 0 {
|
|
bandwidth.target.Store(capacity)
|
|
bandwidth.pacer.SetBitrate(int(capacity))
|
|
}
|
|
bandwidth.listener.OnCongestionStateChange(from, to, max(capacity-bandwidth.audio.Load(), 0))
|
|
}
|
|
func (bandwidth *publicationBWE) ProbeClusterDone(info ccutils.ProbeClusterInfo) {
|
|
bandwidth.probeMu.Lock()
|
|
defer bandwidth.probeMu.Unlock()
|
|
bandwidth.completedProbe = info
|
|
bandwidth.SendSideBWE.ProbeClusterDone(info)
|
|
}
|
|
func (bandwidth *publicationBWE) ProbeClusterFinalize() (ccutils.ProbeSignal, int64, bool) {
|
|
bandwidth.probeMu.Lock()
|
|
defer bandwidth.probeMu.Unlock()
|
|
signal, capacity, done := bandwidth.SendSideBWE.ProbeClusterFinalize()
|
|
if done {
|
|
valid := sendsidebwe.DefaultSendSideBWEConfig.CongestionDetector.ProbeSignal.IsValid(bandwidth.completedProbe)
|
|
bandwidth.completedProbe = ccutils.ProbeClusterInfoInvalid
|
|
if !valid {
|
|
// StreamAllocator may upgrade on Inconclusive too. Its capacity must
|
|
// never promote the estimator's initial sentinel after an empty probe.
|
|
signal = ccutils.ProbeSignalInconclusive
|
|
capacity = min(capacity, bandwidth.target.Load())
|
|
} else if signal == ccutils.ProbeSignalNotCongesting && capacity > bandwidth.target.Load() {
|
|
bandwidth.target.Store(capacity)
|
|
bandwidth.pacer.SetBitrate(int(capacity))
|
|
}
|
|
}
|
|
return signal, max(capacity-bandwidth.audio.Load(), 0), done
|
|
}
|
|
|
|
type publicationPacer struct {
|
|
*boundedPacer
|
|
bandwidth *publicationBWE
|
|
headersMu sync.Mutex
|
|
headers map[uint32]publicationHeaders
|
|
}
|
|
|
|
func (queue *publicationPacer) Enqueue(packet *pacer.Packet) {
|
|
queue.headersMu.Lock()
|
|
headers := queue.headers[packet.Header.SSRC]
|
|
queue.headersMu.Unlock()
|
|
if headers.extension != 0 {
|
|
_ = packet.Header.SetExtension(headers.extension, []byte(headers.rid))
|
|
}
|
|
if headers.midExtension != 0 {
|
|
_ = packet.Header.SetExtension(headers.midExtension, []byte(headers.mid))
|
|
}
|
|
packet.HeaderSize = packet.Header.MarshalSize()
|
|
if headers.counters != nil {
|
|
packet.WriteStream = &publicationWriter{TrackLocalWriter: packet.WriteStream,
|
|
bytes: &headers.counters.bytes, frames: &headers.counters.frames, primary: !packet.IsRTX && !packet.IsProbe}
|
|
}
|
|
queue.boundedPacer.Enqueue(packet)
|
|
}
|
|
func (queue *publicationPacer) StartProbeCluster(info ccutils.ProbeClusterInfo) {
|
|
queue.boundedPacer.StartProbeCluster(info)
|
|
queue.SetBitrate(info.Goal.DesiredBps + int(queue.bandwidth.audio.Load()))
|
|
}
|
|
func (queue *publicationPacer) EndProbeCluster(id ccutils.ProbeClusterId) ccutils.ProbeClusterInfo {
|
|
info := queue.boundedPacer.EndProbeCluster(id)
|
|
queue.cancelProbes(id)
|
|
queue.SetBitrate(int(queue.bandwidth.target.Load()))
|
|
return info
|
|
}
|