Rename the active product, Go module, binaries, configuration, protocols and documentation to Piik while retaining the TV mascot and existing media/room behavior. Use one current private contract without old-name aliases. Preserve SQLite authority and copy compatible local settings once; other settings start fresh. Git-message cleanup and the infrastructure name cutover are coordinated with this phase, with original history and deployment data archived externally. Validated locally with Web and Go checks, Windows capture compilation, matched Client/Server builds, packaged Client control handshake and history-rewrite integrity checks. Skip duplicate hosted CI for this owner-authorized local release. [skip ci]
309 lines
11 KiB
Go
309 lines
11 KiB
Go
package forwarding
|
|
|
|
import (
|
|
"cmp"
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/TNTcraftHIM/Piik/internal/diagnostics"
|
|
"github.com/TNTcraftHIM/Piik/internal/media/encoded"
|
|
"github.com/livekit/livekit-server/pkg/sfu"
|
|
"github.com/livekit/livekit-server/pkg/sfu/buffer"
|
|
"github.com/livekit/mediatransportutil"
|
|
"github.com/livekit/protocol/livekit"
|
|
"github.com/livekit/protocol/utils"
|
|
"github.com/livekit/protocol/utils/mono"
|
|
"github.com/pion/rtcp"
|
|
"github.com/pion/rtp"
|
|
"github.com/pion/webrtc/v4"
|
|
)
|
|
|
|
type LayerFormat struct {
|
|
Width, Height, Bitrate uint32
|
|
}
|
|
|
|
type SourceOptions struct {
|
|
ID, StreamID string
|
|
Codec webrtc.RTPCodecParameters
|
|
Formats []LayerFormat
|
|
HeaderExtensions []webrtc.RTPHeaderExtensionParameter
|
|
MaxPackets int
|
|
OnRTCP func(layer int, packets []rtcp.Packet)
|
|
}
|
|
|
|
// Source owns finite RTP layers and their mature forwarding buffers. The caller
|
|
// owns input generations, codec clocks and PeerConnections. Feedback callbacks
|
|
// run without Source's mutex and may retire the source.
|
|
type Source struct {
|
|
*sfu.ReceiverBase
|
|
mu sync.Mutex
|
|
buffers []*buffer.Buffer
|
|
maxPackets int
|
|
onRTCP func(int, []rtcp.Packet)
|
|
closed bool
|
|
currentTrackInfo atomic.Pointer[livekit.TrackInfo]
|
|
lowestRateControlled atomic.Bool
|
|
}
|
|
|
|
// The live codec owner sets this after startup and clears it at retirement.
|
|
func (source *Source) SetLowestLayerRateControlled(controlled bool) {
|
|
source.lowestRateControlled.Store(controlled)
|
|
}
|
|
|
|
func (source *Source) RateControlled(layer int32) bool {
|
|
return layer == 0 && !source.IsClosed() && source.lowestRateControlled.Load()
|
|
}
|
|
|
|
func NewSource(options SourceOptions) (*Source, error) {
|
|
if options.ID == "" || options.StreamID == "" ||
|
|
options.Codec.ClockRate != 90_000 ||
|
|
(!strings.EqualFold(options.Codec.MimeType, webrtc.MimeTypeH264) &&
|
|
!strings.EqualFold(options.Codec.MimeType, webrtc.MimeTypeVP8)) {
|
|
return nil, errors.New("forwarded video source is invalid")
|
|
}
|
|
if options.MaxPackets == 0 {
|
|
options.MaxPackets = encoded.MaxPacketWindow
|
|
}
|
|
if options.MaxPackets < buffer.InitPacketBufferSizeVideo || options.MaxPackets > 1<<16 {
|
|
return nil, errors.New("forwarded video packet bound is invalid")
|
|
}
|
|
info := &livekit.TrackInfo{
|
|
Sid: options.ID, Type: livekit.TrackType_VIDEO, Source: livekit.TrackSource_SCREEN_SHARE,
|
|
MimeType: options.Codec.MimeType,
|
|
}
|
|
if err := setLayerFormats(info, options.Formats); err != nil {
|
|
return nil, err
|
|
}
|
|
source := &Source{
|
|
ReceiverBase: sfu.NewReceiverBase(sfu.ReceiverBaseParams{
|
|
TrackID: livekit.TrackID(options.ID), StreamID: options.StreamID,
|
|
Kind: webrtc.RTPCodecTypeVideo, Codec: options.Codec,
|
|
HeaderExtensions: canonicalHeaderExtensions(options.HeaderExtensions), Logger: diagnostics.MediaLogger("source").WithValues("trackId", diagnostics.ID(options.ID)),
|
|
StreamTrackerManagerConfig: sfu.DefaultStreamTrackerManagerConfig,
|
|
}, info, sfu.ReceiverCodecStateNormal),
|
|
buffers: make([]*buffer.Buffer, len(options.Formats)),
|
|
maxPackets: options.MaxPackets, onRTCP: options.OnRTCP,
|
|
}
|
|
source.currentTrackInfo.Store(info)
|
|
return source, nil
|
|
}
|
|
|
|
func (source *Source) TrackInfo() *livekit.TrackInfo {
|
|
return utils.CloneProto(source.currentTrackInfo.Load())
|
|
}
|
|
|
|
func canonicalHeaderExtensions(extensions []webrtc.RTPHeaderExtensionParameter) []webrtc.RTPHeaderExtensionParameter {
|
|
result := slices.Clone(extensions)
|
|
slices.SortFunc(result, func(left, right webrtc.RTPHeaderExtensionParameter) int {
|
|
if order := cmp.Compare(left.ID, right.ID); order != 0 {
|
|
return order
|
|
}
|
|
return cmp.Compare(left.URI, right.URI)
|
|
})
|
|
return result
|
|
}
|
|
|
|
func setLayerFormats(info *livekit.TrackInfo, formats []LayerFormat) error {
|
|
if len(formats) < 1 || len(formats) > int(buffer.DefaultMaxLayerSpatial)+1 {
|
|
return errors.New("forwarded video layer count is invalid")
|
|
}
|
|
layers := make([]*livekit.VideoLayer, len(formats))
|
|
for index, format := range formats {
|
|
if (format.Width == 0) != (format.Height == 0) {
|
|
return errors.New("forwarded video layer format is invalid")
|
|
}
|
|
quality := livekit.VideoQuality(index)
|
|
if index == len(formats)-1 {
|
|
quality = livekit.VideoQuality_HIGH
|
|
}
|
|
layers[index] = &livekit.VideoLayer{
|
|
Quality: quality, SpatialLayer: int32(index), Rid: buffer.DefaultVideoLayersRid[index],
|
|
Width: format.Width, Height: format.Height, Bitrate: format.Bitrate,
|
|
}
|
|
}
|
|
highest := formats[len(formats)-1]
|
|
info.Width, info.Height = highest.Width, highest.Height
|
|
info.Simulcast, info.Layers = len(layers) > 1, layers
|
|
info.Codecs = []*livekit.SimulcastCodecInfo{{
|
|
MimeType: info.MimeType, VideoLayerMode: livekit.VideoLayer_ONE_SPATIAL_LAYER_PER_STREAM, Layers: layers,
|
|
}}
|
|
return nil
|
|
}
|
|
|
|
// UpdateFormats changes one source owner's declared layer metadata without
|
|
// replacing its buffers or packet identities. Zero values remain unknown.
|
|
// The caller serializes updates with its capture/source generation changes.
|
|
func (source *Source) UpdateFormats(formats []LayerFormat) error {
|
|
source.mu.Lock()
|
|
closed, count := source.closed, len(source.buffers)
|
|
source.mu.Unlock()
|
|
if closed {
|
|
return sfu.ErrReceiverClosed
|
|
}
|
|
if len(formats) != count {
|
|
return errors.New("forwarded video layer count changed")
|
|
}
|
|
info := source.TrackInfo()
|
|
if err := setLayerFormats(info, formats); err != nil {
|
|
return err
|
|
}
|
|
source.currentTrackInfo.Store(info)
|
|
// The library can notify downtracks while applying metadata.
|
|
source.UpdateTrackInfo(info)
|
|
return nil
|
|
}
|
|
|
|
// UpdateDimensions publishes actual encoder dimensions without UpdateTrackInfo's
|
|
// mute propagation, which resets every active layer's tracker and bitrate.
|
|
func (source *Source) UpdateDimensions(layer int, width, height uint32) error {
|
|
source.mu.Lock()
|
|
defer source.mu.Unlock()
|
|
if source.closed {
|
|
return sfu.ErrReceiverClosed
|
|
}
|
|
if layer < 0 || layer >= len(source.buffers) || width == 0 || height == 0 {
|
|
return errors.New("forwarded video layer dimensions are invalid")
|
|
}
|
|
current := source.currentTrackInfo.Load()
|
|
if current.Layers[layer].Width == width && current.Layers[layer].Height == height {
|
|
return nil
|
|
}
|
|
info := utils.CloneProto(current)
|
|
info.Layers[layer].Width, info.Layers[layer].Height = width, height
|
|
info.Codecs[0].Layers[layer].Width, info.Codecs[0].Layers[layer].Height = width, height
|
|
if layer == len(source.buffers)-1 {
|
|
info.Width, info.Height = width, height
|
|
}
|
|
source.currentTrackInfo.Store(info)
|
|
return nil
|
|
}
|
|
|
|
// BindLayer installs a single source identity. A different SSRC requires the
|
|
// caller's source-generation replacement, not silent buffer reuse.
|
|
func (source *Source) BindLayer(layer int, ssrc uint32, parameters webrtc.RTPParameters) error {
|
|
source.mu.Lock()
|
|
defer source.mu.Unlock()
|
|
if source.closed {
|
|
return sfu.ErrReceiverClosed
|
|
}
|
|
if layer < 0 || layer >= len(source.buffers) || ssrc == 0 {
|
|
return sfu.ErrInvalidLayer
|
|
}
|
|
if source.buffers[layer] != nil {
|
|
return sfu.ErrDuplicateLayer
|
|
}
|
|
codec := source.Codec()
|
|
if !slices.ContainsFunc(parameters.Codecs, func(candidate webrtc.RTPCodecParameters) bool {
|
|
return candidate.PayloadType == codec.PayloadType && candidate.ClockRate == codec.ClockRate &&
|
|
strings.EqualFold(candidate.MimeType, codec.MimeType) && candidate.SDPFmtpLine == codec.SDPFmtpLine
|
|
}) || !slices.Equal(canonicalHeaderExtensions(parameters.HeaderExtensions), source.HeaderExtensions()) {
|
|
return errors.New("forwarded video layer codec does not match the source")
|
|
}
|
|
for _, existing := range source.buffers {
|
|
if existing != nil && existing.SSRC() == ssrc {
|
|
return errors.New("forwarded video layers cannot share an SSRC")
|
|
}
|
|
}
|
|
input := buffer.NewBuffer(ssrc, source.maxPackets, buffer.InitPacketBufferSizeAudio)
|
|
input.SetLogger(diagnostics.MediaLogger("rtp-buffer").WithValues("trackId", diagnostics.ID(string(source.TrackID()))))
|
|
if err := input.Bind(parameters, codec.RTPCodecCapability, int(source.TrackInfo().Layers[layer].Bitrate)); err != nil {
|
|
_ = input.Close()
|
|
return err
|
|
}
|
|
if source.onRTCP != nil {
|
|
input.OnRtcpFeedback(func(packets []rtcp.Packet) { source.onRTCP(layer, packets) })
|
|
}
|
|
source.AddBuffer(input, int32(layer))
|
|
source.buffers[layer] = input
|
|
source.StartBuffer(input, int32(layer))
|
|
return nil
|
|
}
|
|
|
|
func (source *Source) layerBuffer(layer int) (*buffer.Buffer, error) {
|
|
source.mu.Lock()
|
|
defer source.mu.Unlock()
|
|
if source.closed {
|
|
return nil, sfu.ErrReceiverClosed
|
|
}
|
|
if layer < 0 || layer >= len(source.buffers) || source.buffers[layer] == nil {
|
|
return nil, sfu.ErrInvalidLayer
|
|
}
|
|
return source.buffers[layer], nil
|
|
}
|
|
|
|
func (source *Source) WriteRTP(layer int, packet *rtp.Packet) error {
|
|
input, err := source.layerBuffer(layer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if packet == nil || packet.Version != 2 || packet.SSRC != input.SSRC() ||
|
|
packet.PayloadType != uint8(source.Codec().PayloadType) {
|
|
return errors.New("forwarded RTP packet does not match its layer")
|
|
}
|
|
data, err := packet.Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = input.Write(data)
|
|
return err
|
|
}
|
|
|
|
func (source *Source) SenderReport(layer int, report *rtcp.SenderReport) error {
|
|
input, err := source.layerBuffer(layer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if report == nil || report.SSRC != input.SSRC() {
|
|
return errors.New("forwarded sender report does not match its layer")
|
|
}
|
|
input.SetSenderReportData(&livekit.RTCPSenderReportState{
|
|
RtpTimestamp: report.RTPTime, NtpTimestamp: report.NTPTime,
|
|
Packets: report.PacketCount, Octets: uint64(report.OctetCount), At: mono.UnixNano(),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// ReferenceTime keeps derived outputs on the received source's RTP/NTP clock.
|
|
// The bounded input history makes the nearest 32-bit timestamp delta unambiguous.
|
|
func (source *Source) ReferenceTime(layer int, timestamp uint32) (time.Time, bool) {
|
|
input, err := source.layerBuffer(layer)
|
|
if err != nil {
|
|
return time.Time{}, false
|
|
}
|
|
report := input.GetSenderReportData()
|
|
if report == nil || report.NtpTimestamp == 0 {
|
|
return time.Time{}, false
|
|
}
|
|
delta := time.Duration(int32(timestamp-report.RtpTimestamp)) * time.Second / time.Duration(source.Codec().ClockRate)
|
|
return mediatransportutil.NtpTime(report.NtpTimestamp).Time().Add(delta), true
|
|
}
|
|
|
|
// SetCorrelation receives the local source owner's RTP/NTP mapping unchanged.
|
|
// The layer must already have received RTP; the media core owns report ordering.
|
|
func (source *Source) SetCorrelation(layer int, correlation *livekit.RTCPSenderReportState) error {
|
|
input, err := source.layerBuffer(layer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if correlation == nil {
|
|
return errors.New("forwarded source correlation is absent")
|
|
}
|
|
input.SetSenderReportData(correlation)
|
|
return nil
|
|
}
|
|
|
|
func (source *Source) Close() {
|
|
source.mu.Lock()
|
|
if source.closed {
|
|
source.mu.Unlock()
|
|
return
|
|
}
|
|
source.closed = true
|
|
source.mu.Unlock()
|
|
source.ReceiverBase.Close("source closed", true)
|
|
}
|