Files
AAA 1332473513 feat: rename product and runtime to Piik
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]
2026-09-10 11:10:12 +08:00

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)
}