From b660c3b58211e3e58b5b76129e0f839d1c58f2da Mon Sep 17 00:00:00 2001 From: cnderrauber Date: Mon, 18 Aug 2025 09:06:57 +0800 Subject: [PATCH 1/5] Extract video size from media stream (#3856) * Extract video size from media stream * fix test --- pkg/rtc/mediatrack.go | 65 ++- pkg/rtc/mediatrack_test.go | 21 +- pkg/rtc/mediatrackreceiver.go | 54 +- pkg/rtc/participant.go | 2 + pkg/rtc/wrappedreceiver.go | 8 + pkg/sfu/buffer/buffer.go | 84 ++- pkg/sfu/buffer/dependencydescriptorparser.go | 13 + pkg/sfu/buffer/h26xhelper.go | 527 +++++++++++++++++++ pkg/sfu/buffer/h26xhelper_test.go | 40 ++ pkg/sfu/buffer/helpers.go | 33 +- pkg/sfu/receiver.go | 49 +- 11 files changed, 838 insertions(+), 58 deletions(-) create mode 100644 pkg/sfu/buffer/h26xhelper.go create mode 100644 pkg/sfu/buffer/h26xhelper_test.go diff --git a/pkg/rtc/mediatrack.go b/pkg/rtc/mediatrack.go index 9da1d3d6e..29f222a7e 100644 --- a/pkg/rtc/mediatrack.go +++ b/pkg/rtc/mediatrack.go @@ -64,24 +64,25 @@ type MediaTrack struct { } type MediaTrackParams struct { - ParticipantID func() livekit.ParticipantID - ParticipantIdentity livekit.ParticipantIdentity - ParticipantVersion uint32 - ParticipantCountry string - BufferFactory *buffer.Factory - ReceiverConfig ReceiverConfig - SubscriberConfig DirectionConfig - PLIThrottleConfig sfu.PLIThrottleConfig - AudioConfig sfu.AudioConfig - VideoConfig config.VideoConfig - Telemetry telemetry.TelemetryService - Logger logger.Logger - Reporter roomobs.TrackReporter - SimTracks map[uint32]SimulcastTrackInfo - OnRTCP func([]rtcp.Packet) - ForwardStats *sfu.ForwardStats - OnTrackEverSubscribed func(livekit.TrackID) - ShouldRegressCodec func() bool + ParticipantID func() livekit.ParticipantID + ParticipantIdentity livekit.ParticipantIdentity + ParticipantVersion uint32 + ParticipantCountry string + BufferFactory *buffer.Factory + ReceiverConfig ReceiverConfig + SubscriberConfig DirectionConfig + PLIThrottleConfig sfu.PLIThrottleConfig + AudioConfig sfu.AudioConfig + VideoConfig config.VideoConfig + Telemetry telemetry.TelemetryService + Logger logger.Logger + Reporter roomobs.TrackReporter + SimTracks map[uint32]SimulcastTrackInfo + OnRTCP func([]rtcp.Packet) + ForwardStats *sfu.ForwardStats + OnTrackEverSubscribed func(livekit.TrackID) + ShouldRegressCodec func() bool + PreferVideoSizeFromMedia bool } func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack { @@ -96,17 +97,18 @@ func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack { } t.MediaTrackReceiver = NewMediaTrackReceiver(MediaTrackReceiverParams{ - MediaTrack: t, - IsRelayed: false, - ParticipantID: params.ParticipantID, - ParticipantIdentity: params.ParticipantIdentity, - ParticipantVersion: params.ParticipantVersion, - ReceiverConfig: params.ReceiverConfig, - SubscriberConfig: params.SubscriberConfig, - AudioConfig: params.AudioConfig, - Telemetry: params.Telemetry, - Logger: params.Logger, - RegressionTargetCodec: t.regressionTargetCodec, + MediaTrack: t, + IsRelayed: false, + ParticipantID: params.ParticipantID, + ParticipantIdentity: params.ParticipantIdentity, + ParticipantVersion: params.ParticipantVersion, + ReceiverConfig: params.ReceiverConfig, + SubscriberConfig: params.SubscriberConfig, + AudioConfig: params.AudioConfig, + Telemetry: params.Telemetry, + Logger: params.Logger, + RegressionTargetCodec: t.regressionTargetCodec, + PreferVideoSizeFromMedia: params.PreferVideoSizeFromMedia, }, ti) if ti.Type == livekit.TrackType_AUDIO { @@ -446,6 +448,11 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe newWR.AddOnCodecStateChange(func(codec webrtc.RTPCodecParameters, state sfu.ReceiverCodecState) { t.MediaTrackReceiver.HandleReceiverCodecChange(newWR, codec, state) }) + + // update subscriber video layers when video size changes + newWR.OnVideoSizeChanged(func() { + t.MediaTrackSubscriptions.UpdateVideoLayers() + }) } if newCodec && t.enableRegression() { diff --git a/pkg/rtc/mediatrack_test.go b/pkg/rtc/mediatrack_test.go index f9d5c03e6..7839a943d 100644 --- a/pkg/rtc/mediatrack_test.go +++ b/pkg/rtc/mediatrack_test.go @@ -21,6 +21,7 @@ import ( "github.com/livekit/livekit-server/pkg/sfu/mime" "github.com/livekit/protocol/livekit" + "github.com/livekit/protocol/logger" ) func TestTrackInfo(t *testing.T) { @@ -52,7 +53,9 @@ func TestTrackInfo(t *testing.T) { func TestGetQualityForDimension(t *testing.T) { t.Run("landscape source", func(t *testing.T) { - mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{ + mt := NewMediaTrack(MediaTrackParams{ + Logger: logger.GetLogger(), + }, &livekit.TrackInfo{ Type: livekit.TrackType_VIDEO, Width: 1080, Height: 720, @@ -66,7 +69,9 @@ func TestGetQualityForDimension(t *testing.T) { }) t.Run("portrait source", func(t *testing.T) { - mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{ + mt := NewMediaTrack(MediaTrackParams{ + Logger: logger.GetLogger(), + }, &livekit.TrackInfo{ Type: livekit.TrackType_VIDEO, Width: 540, Height: 960, @@ -79,7 +84,9 @@ func TestGetQualityForDimension(t *testing.T) { }) t.Run("layers provided", func(t *testing.T) { - mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{ + mt := NewMediaTrack(MediaTrackParams{ + Logger: logger.GetLogger(), + }, &livekit.TrackInfo{ Type: livekit.TrackType_VIDEO, Width: 1080, Height: 720, @@ -114,7 +121,9 @@ func TestGetQualityForDimension(t *testing.T) { }) t.Run("highest layer with smallest dimensions", func(t *testing.T) { - mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{ + mt := NewMediaTrack(MediaTrackParams{ + Logger: logger.GetLogger(), + }, &livekit.TrackInfo{ Type: livekit.TrackType_VIDEO, Width: 1080, Height: 720, @@ -148,7 +157,9 @@ func TestGetQualityForDimension(t *testing.T) { require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700)) require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1200, 800)) - mt = NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{ + mt = NewMediaTrack(MediaTrackParams{ + Logger: logger.GetLogger(), + }, &livekit.TrackInfo{ Type: livekit.TrackType_VIDEO, Width: 1080, Height: 720, diff --git a/pkg/rtc/mediatrackreceiver.go b/pkg/rtc/mediatrackreceiver.go index b0ba9c3be..a31c40a2f 100644 --- a/pkg/rtc/mediatrackreceiver.go +++ b/pkg/rtc/mediatrackreceiver.go @@ -119,17 +119,18 @@ func (r *simulcastReceiver) IsRegressed() bool { // ----------------------------------------------------- type MediaTrackReceiverParams struct { - MediaTrack types.MediaTrack - IsRelayed bool - ParticipantID func() livekit.ParticipantID - ParticipantIdentity livekit.ParticipantIdentity - ParticipantVersion uint32 - ReceiverConfig ReceiverConfig - SubscriberConfig DirectionConfig - AudioConfig sfu.AudioConfig - Telemetry telemetry.TelemetryService - Logger logger.Logger - RegressionTargetCodec mime.MimeType + MediaTrack types.MediaTrack + IsRelayed bool + ParticipantID func() livekit.ParticipantID + ParticipantIdentity livekit.ParticipantIdentity + ParticipantVersion uint32 + ReceiverConfig ReceiverConfig + SubscriberConfig DirectionConfig + AudioConfig sfu.AudioConfig + Telemetry telemetry.TelemetryService + Logger logger.Logger + RegressionTargetCodec mime.MimeType + PreferVideoSizeFromMedia bool } type MediaTrackReceiver struct { @@ -910,7 +911,12 @@ func (t *MediaTrackReceiver) GetQualityForDimension(mimeType mime.MimeType, widt trackInfo := t.TrackInfo() - if trackInfo.Height == 0 { + var mediaSizes []buffer.VideoSize + if receiver := t.Receiver(mimeType); receiver != nil { + mediaSizes = receiver.VideoSizes() + } + + if trackInfo.Height == 0 && len(mediaSizes) == 0 { return quality } origSize := trackInfo.Height @@ -921,12 +927,36 @@ func (t *MediaTrackReceiver) GetQualityForDimension(mimeType mime.MimeType, widt requestedSize = width } + if origSize == 0 { + for i := len(mediaSizes) - 1; i >= 0; i-- { + if mediaSizes[i].Height > 0 { + origSize = mediaSizes[i].Height + if mediaSizes[i].Width < mediaSizes[i].Height { + origSize = mediaSizes[i].Width + } + break + } + } + } + // default sizes representing qualities low - high layerSizes := []uint32{180, 360, origSize} var providedSizes []uint32 for _, layer := range buffer.GetVideoLayersForMimeType(mimeType, trackInfo) { providedSizes = append(providedSizes, layer.Height) } + + if len(providedSizes) == 0 || providedSizes[0] == 0 || t.params.PreferVideoSizeFromMedia { + if len(mediaSizes) > 0 { + providedSizes = providedSizes[:0] + for _, size := range mediaSizes { + providedSizes = append(providedSizes, size.Height) + } + } else { + t.params.Logger.Debugw("no video sizes provided by receiver, using track info sizes") + } + } + if len(providedSizes) > 0 { layerSizes = providedSizes // comparing height always diff --git a/pkg/rtc/participant.go b/pkg/rtc/participant.go index a37411396..5fd0f4171 100644 --- a/pkg/rtc/participant.go +++ b/pkg/rtc/participant.go @@ -208,6 +208,7 @@ type ParticipantParams struct { DisableCodecRegression bool LastPubReliableSeq uint32 Country string + PreferVideoSizeFromMedia bool } type ParticipantImpl struct { @@ -3138,6 +3139,7 @@ func (p *ParticipantImpl) addMediaTrack(signalCid string, ti *livekit.TrackInfo) ShouldRegressCodec: func() bool { return p.helper().ShouldRegressCodec() }, + PreferVideoSizeFromMedia: p.params.PreferVideoSizeFromMedia, }, ti) mt.OnSubscribedMaxQualityChange(p.onSubscribedMaxQualityChange) diff --git a/pkg/rtc/wrappedreceiver.go b/pkg/rtc/wrappedreceiver.go index 54e01be3a..5195908d2 100644 --- a/pkg/rtc/wrappedreceiver.go +++ b/pkg/rtc/wrappedreceiver.go @@ -477,6 +477,14 @@ func (d *DummyReceiver) CodecState() sfu.ReceiverCodecState { return sfu.ReceiverCodecStateNormal } +func (d *DummyReceiver) VideoSizes() []buffer.VideoSize { + if r, ok := d.receiver.Load().(sfu.TrackReceiver); ok { + return r.VideoSizes() + } + + return nil +} + // -------------------------------------------- type DummyRedReceiver struct { diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index 5deff6beb..feb95ee68 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -72,6 +72,12 @@ type ExtPacket struct { IsOutOfOrder bool } +// VideoSize represents video resolution +type VideoSize struct { + Width uint32 + Height uint32 +} + // Buffer contains all packets type Buffer struct { sync.RWMutex @@ -125,6 +131,10 @@ type Buffer struct { onFpsChanged func() onFinalRtpStats func(*livekit.RTPStats) onCodecChange func(webrtc.RTPCodecParameters) + onVideoSizeChanged func([]VideoSize) + + // video size tracking for multiple spatial layers + currentVideoSize [DefaultMaxLayerSpatial + 1]VideoSize // logger logger logger.Logger @@ -886,6 +896,7 @@ func (b *Buffer) getExtPacket(rtpPacket *rtp.Packet, arrivalTime int64, flowStat } ep.Temporal = 0 + var videoSize []VideoSize if b.ddParser != nil { ddVal, videoLayer, err := b.ddParser.Parse(ep.Packet) if err != nil { @@ -901,6 +912,7 @@ func (b *Buffer) getExtPacket(rtpPacket *rtp.Packet, arrivalTime int64, flowStat } else if ddVal != nil { ep.DependencyDescriptor = ddVal ep.VideoLayer = videoLayer + videoSize = ExtractDependencyDescriptorVideoSize(ddVal.Descriptor) // DD-TODO : notify active decode target change if changed. } } @@ -915,6 +927,12 @@ func (b *Buffer) getExtPacket(rtpPacket *rtp.Packet, arrivalTime int64, flowStat ep.KeyFrame = vp8Packet.IsKeyFrame if ep.DependencyDescriptor == nil { ep.Temporal = int32(vp8Packet.TID) + + if ep.KeyFrame { + if sz := ExtractVP8VideoSize(&vp8Packet, rtpPacket.Payload); sz.Width > 0 && sz.Height > 0 { + videoSize = append(videoSize, sz) + } + } } else { // vp8 with DependencyDescriptor enabled, use the TID from the descriptor vp8Packet.TID = uint8(ep.Temporal) @@ -935,17 +953,36 @@ func (b *Buffer) getExtPacket(rtpPacket *rtp.Packet, arrivalTime int64, flowStat Temporal: int32(vp9Packet.TID), } ep.Payload = vp9Packet + ep.KeyFrame = IsVP9KeyFrame(&vp9Packet, rtpPacket.Payload) + + if ep.KeyFrame { + for i := 0; i < len(vp9Packet.Width); i++ { + videoSize = append(videoSize, VideoSize{ + Width: uint32(vp9Packet.Width[i]), + Height: uint32(vp9Packet.Height[i]), + }) + } + } + } else { + ep.KeyFrame = IsVP9KeyFrame(nil, rtpPacket.Payload) } - ep.KeyFrame = IsVP9KeyFrame(rtpPacket.Payload) case mime.MimeTypeH264: ep.KeyFrame = IsH264KeyFrame(rtpPacket.Payload) ep.Spatial = InvalidLayerSpatial // h.264 don't have spatial scalability, reset to invalid + // Check H264 key frame video size + if ep.KeyFrame { + if sz := ExtractH264VideoSize(rtpPacket.Payload); sz.Width > 0 && sz.Height > 0 { + videoSize = append(videoSize, sz) + } + } + case mime.MimeTypeAV1: ep.KeyFrame = IsAV1KeyFrame(rtpPacket.Payload) case mime.MimeTypeH265: + ep.KeyFrame = IsH265KeyFrame(rtpPacket.Payload) if ep.DependencyDescriptor == nil { if len(rtpPacket.Payload) < 2 { b.logger.Warnw("invalid H265 packet", nil) @@ -955,8 +992,13 @@ func (b *Buffer) getExtPacket(rtpPacket *rtp.Packet, arrivalTime int64, flowStat Temporal: int32(rtpPacket.Payload[1]&0x07) - 1, } ep.Spatial = InvalidLayerSpatial + + if ep.KeyFrame { + if sz := ExtractH265VideoSize(rtpPacket.Payload); sz.Width > 0 && sz.Height > 0 { + videoSize = append(videoSize, sz) + } + } } - ep.KeyFrame = IsH265KeyFrame(rtpPacket.Payload) } if ep.KeyFrame { @@ -974,6 +1016,10 @@ func (b *Buffer) getExtPacket(rtpPacket *rtp.Packet, arrivalTime int64, flowStat } } + if len(videoSize) > 0 { + b.checkVideoSizeChange(videoSize) + } + return ep } @@ -1237,6 +1283,40 @@ func (b *Buffer) OnFpsChanged(f func()) { b.Unlock() } +func (b *Buffer) OnVideoSizeChanged(fn func([]VideoSize)) { + b.Lock() + b.onVideoSizeChanged = fn + b.Unlock() +} + +// checkVideoSizeChange checks if video size has changed for a specific spatial layer and fires callback +func (b *Buffer) checkVideoSizeChange(videoSizes []VideoSize) { + if len(videoSizes) > len(b.currentVideoSize) { + b.logger.Warnw("video size index out of range", nil, "newSize", videoSizes, "currentVideoSize", b.currentVideoSize) + return + } + + if len(videoSizes) < len(b.currentVideoSize) { + videoSizes = append(videoSizes, make([]VideoSize, len(b.currentVideoSize)-len(videoSizes))...) + } + + changed := false + for i, sz := range videoSizes { + if b.currentVideoSize[i].Width != sz.Width || b.currentVideoSize[i].Height != sz.Height { + changed = true + break + } + } + + if changed { + b.logger.Debugw("video size changed", "from", b.currentVideoSize, "to", videoSizes) + copy(b.currentVideoSize[:], videoSizes[:]) + if b.onVideoSizeChanged != nil { + go b.onVideoSizeChanged(videoSizes) + } + } +} + func (b *Buffer) GetTemporalLayerFpsForSpatial(layer int32) []float32 { if int(layer) >= len(b.frameRateCalculator) { return nil diff --git a/pkg/sfu/buffer/dependencydescriptorparser.go b/pkg/sfu/buffer/dependencydescriptorparser.go index 8941f8004..3fbfaf9cc 100644 --- a/pkg/sfu/buffer/dependencydescriptorparser.go +++ b/pkg/sfu/buffer/dependencydescriptorparser.go @@ -217,3 +217,16 @@ func GetActiveDecodeTargetBitmask(layer VideoLayer, decodeTargets []DependencyDe } // ------------------------------------------------------------------------------ + +func ExtractDependencyDescriptorVideoSize(dd *dd.DependencyDescriptor) []VideoSize { + if dd.AttachedStructure == nil { + return nil + } + + videoSizes := make([]VideoSize, 0, len(dd.AttachedStructure.Resolutions)) + for _, res := range dd.AttachedStructure.Resolutions { + videoSizes = append(videoSizes, VideoSize{Width: uint32(res.Width), Height: uint32(res.Height)}) + } + + return videoSizes +} diff --git a/pkg/sfu/buffer/h26xhelper.go b/pkg/sfu/buffer/h26xhelper.go new file mode 100644 index 000000000..a483a4050 --- /dev/null +++ b/pkg/sfu/buffer/h26xhelper.go @@ -0,0 +1,527 @@ +package buffer + +import ( + "errors" + "fmt" +) + +// SPSInfo holds parsed SPS parameters +type SPSInfo struct { + ChromaFormatIDC uint + PicWidthInLumaSamples uint + PicHeightInLumaSamples uint + ConformanceWindowFlag bool + ConfWinLeftOffset uint + ConfWinRightOffset uint + ConfWinTopOffset uint + ConfWinBottomOffset uint + CodedWidth, CodedHeight uint // Raw coded resolution + DisplayWidth, DisplayHeight uint // Resolution after conformance window cropping +} + +// -------- BitReader -------- +type BitReader struct { + data []byte + pos int // bit position +} + +func NewBitReader(data []byte) *BitReader { + return &BitReader{data: data} +} + +func (br *BitReader) left() int { + return len(br.data)*8 - br.pos +} + +func (br *BitReader) ReadBits(n int) (uint, error) { + if n < 0 || br.left() < n { + return 0, errors.New("not enough bits") + } + var v uint + for i := 0; i < n; i++ { + bytePos := br.pos / 8 + bitPos := 7 - (br.pos % 8) + bit := (br.data[bytePos] >> bitPos) & 1 + v = (v << 1) | uint(bit) + br.pos++ + } + return v, nil +} + +func (br *BitReader) ReadFlag() (bool, error) { + b, err := br.ReadBits(1) + return b == 1, err +} + +func (br *BitReader) ReadUE() (uint, error) { + // Unsigned Exp-Golomb + zeros := 0 + for { + bit, err := br.ReadBits(1) + if err != nil { + return 0, err + } + if bit == 0 { + zeros++ + continue + } + break // hit the stop bit '1' + } + if zeros == 0 { + return 0, nil + } + info, err := br.ReadBits(zeros) + if err != nil { + return 0, err + } + return (1<= 4 && b[0] == 0x00 && b[1] == 0x00 && b[2] == 0x00 && b[3] == 0x01 { + return b[4:] + } + if len(b) >= 3 && b[0] == 0x00 && b[1] == 0x00 && b[2] == 0x01 { + return b[3:] + } + return b +} + +// removeEmulationPreventionBytes removes 0x03 after 0x0000 +func removeEmulationPreventionBytes(data []byte) []byte { + out := make([]byte, 0, len(data)) + for i := 0; i < len(data); i++ { + if i > 1 && data[i] == 0x03 && data[i-1] == 0x00 && data[i-2] == 0x00 { + continue + } + out = append(out, data[i]) + } + return out +} + +// parseH265SPS parses a full H.265 SPS NAL unit +func parseH265SPS(nal []byte) (*SPSInfo, error) { + // Optional start code + nal = stripStartCode(nal) + + // Remove emulation prevention bytes across the NAL + rbsp := removeEmulationPreventionBytes(nal) + + br := NewBitReader(rbsp) + + // ---- NAL header (16 bits): forbidden_zero_bit(1), nal_unit_type(6), nuh_layer_id(6), nuh_temporal_id_plus1(3) + if _, err := br.ReadBits(1); err != nil { // forbidden_zero_bit + return nil, err + } + nalUnitType, err := br.ReadBits(6) + if err != nil { + return nil, err + } + if _, err = br.ReadBits(6); err != nil { // nuh_layer_id + return nil, err + } + if _, err = br.ReadBits(3); err != nil { // nuh_temporal_id_plus1 + return nil, err + } + // 33 = SPS + if nalUnitType != 33 { + return nil, fmt.Errorf("not an HEVC SPS NAL (type=%d)", nalUnitType) + } + + // ---- sps_video_parameter_set_id u(4), sps_max_sub_layers_minus1 u(3), sps_temporal_id_nesting_flag u(1) + if _, err = br.ReadBits(4); err != nil { + return nil, err + } + maxSubLayersMinus1, err := br.ReadBits(3) + if err != nil { + return nil, err + } + if _, err = br.ReadBits(1); err != nil { + return nil, err + } + + // ---- profile_tier_level(1, max_sub_layers_minus1) + // general_profile_space u(2), general_tier_flag u(1), general_profile_idc u(5) + if _, err = br.ReadBits(2 + 1 + 5); err != nil { + return nil, err + } + // general_profile_compatibility_flags u(32) + if _, err = br.ReadBits(32); err != nil { + return nil, err + } + // general_constraint_indicator_flags u(48) + if _, err = br.ReadBits(16); err != nil { + return nil, err + } + if _, err = br.ReadBits(32); err != nil { + return nil, err + } + // general_level_idc u(8) + if _, err = br.ReadBits(8); err != nil { + return nil, err + } + + subLayerProfilePresentFlag := make([]bool, maxSubLayersMinus1) + subLayerLevelPresentFlag := make([]bool, maxSubLayersMinus1) + for i := uint(0); i < maxSubLayersMinus1; i++ { + f1, err := br.ReadFlag() + if err != nil { + return nil, err + } + f2, err := br.ReadFlag() + if err != nil { + return nil, err + } + subLayerProfilePresentFlag[i] = f1 + subLayerLevelPresentFlag[i] = f2 + } + if maxSubLayersMinus1 > 0 { + // reserved_zero_2bits for i = maxSubLayersMinus1 .. 7 + for i := maxSubLayersMinus1; i < 8; i++ { + if _, err := br.ReadBits(2); err != nil { + return nil, err + } + } + } + for i := uint(0); i < maxSubLayersMinus1; i++ { + if subLayerProfilePresentFlag[i] { + if _, err = br.ReadBits(2 + 1 + 5); err != nil { + return nil, err + } + if _, err = br.ReadBits(32); err != nil { + return nil, err + } + if _, err = br.ReadBits(48); err != nil { + return nil, err + } + } + if subLayerLevelPresentFlag[i] { + if _, err = br.ReadBits(8); err != nil { + return nil, err + } + } + } + + // ---- Now the core SPS fields we need + _, err = br.ReadUE() // sps_seq_parameter_set_id + if err != nil { + return nil, err + } + + chromaFormatIDC, err := br.ReadUE() + if err != nil { + return nil, err + } + if chromaFormatIDC == 3 { + // separate_colour_plane_flag u(1) + if _, err := br.ReadFlag(); err != nil { + return nil, err + } + } + + picW, err := br.ReadUE() // pic_width_in_luma_samples + if err != nil { + return nil, err + } + picH, err := br.ReadUE() // pic_height_in_luma_samples + if err != nil { + return nil, err + } + + confFlag, err := br.ReadFlag() + if err != nil { + return nil, err + } + var l, r, t, b uint + if confFlag { + if l, err = br.ReadUE(); err != nil { + return nil, err + } + if r, err = br.ReadUE(); err != nil { + return nil, err + } + if t, err = br.ReadUE(); err != nil { + return nil, err + } + if b, err = br.ReadUE(); err != nil { + return nil, err + } + } + + // crop unit size depends on chroma_format_idc + subWidthC, subHeightC := getSubWidthC(chromaFormatIDC), getSubHeightC(chromaFormatIDC) + + info := &SPSInfo{ + ChromaFormatIDC: chromaFormatIDC, + PicWidthInLumaSamples: picW, + PicHeightInLumaSamples: picH, + ConformanceWindowFlag: confFlag, + ConfWinLeftOffset: l, + ConfWinRightOffset: r, + ConfWinTopOffset: t, + ConfWinBottomOffset: b, + CodedWidth: picW, + CodedHeight: picH, + } + + if confFlag { + w := int(picW) - int(l+r)*int(subWidthC) + h := int(picH) - int(t+b)*int(subHeightC) + if w < 0 { + w = 0 + } + if h < 0 { + h = 0 + } + info.DisplayWidth = uint(w) + info.DisplayHeight = uint(h) + } else { + info.DisplayWidth = picW + info.DisplayHeight = picH + } + + return info, nil +} + +func getSubWidthC(chromaFormatIDC uint) uint { + if chromaFormatIDC == 1 || chromaFormatIDC == 2 { + return 2 + } + return 1 +} + +func getSubHeightC(chromaFormatIDC uint) uint { + if chromaFormatIDC == 1 { + return 2 + } + return 1 +} + +func ExtractH265VideoSize(payload []byte) VideoSize { + if len(payload) < 2 { + return VideoSize{} + } + nalType := (payload[0] >> 1) & 0x3F + + var spsNalu []byte + switch nalType { + case 33: // SPS + spsNalu = payload + case 48: // Aggregation Packet (AP) + // skip 2-byte header + i := 2 + for i+2 <= len(payload) { + nalSize := int(payload[i])<<8 | int(payload[i+1]) + i += 2 + if i+nalSize > len(payload) { + break + } + nalUnit := payload[i : i+nalSize] + nt := (nalUnit[0] >> 1) & 0x3F + if nt == 33 { + spsNalu = nalUnit + break + } + i += nalSize + } + } + + if len(spsNalu) > 0 { + info, err := parseH265SPS(spsNalu) + if err != nil { + return VideoSize{} + } + return VideoSize{Width: uint32(info.DisplayWidth), Height: uint32(info.DisplayHeight)} + } + + return VideoSize{} +} + +// ------------------------- H264 ------------------------- + +// parseH264SPS parses a full H.264 SPS NAL unit into SPSInfo +func parseH264SPS(nal []byte) (*SPSInfo, error) { + if len(nal) < 1 { + return nil, errors.New("empty SPS NAL") + } + nal = stripStartCode(nal) + nalType := nal[0] & 0x1F + if nalType != 7 { + return nil, fmt.Errorf("not an SPS NAL (type=%d)", nalType) + } + + rbsp := removeEmulationPreventionBytes(nal[1:]) // skip NAL header + br := NewBitReader(rbsp) + + profileIDC, _ := br.ReadBits(8) + _, _ = br.ReadBits(8) // constraint flags + _, _ = br.ReadBits(8) // level_idc + _, _ = br.ReadUE() // seq_parameter_set_id + + chromaFormatIDC := uint(1) + if profileIDC == 100 || profileIDC == 110 || profileIDC == 122 || profileIDC == 244 || + profileIDC == 44 || profileIDC == 83 || profileIDC == 86 || profileIDC == 118 || profileIDC == 128 { + chromaFormatIDC, _ = br.ReadUE() + if chromaFormatIDC == 3 { + br.ReadFlag() // separate_colour_plane_flag + } + br.ReadUE() // bit_depth_luma_minus8 + br.ReadUE() // bit_depth_chroma_minus8 + br.ReadFlag() // qpprime_y_zero_transform_bypass_flag + if v, _ := br.ReadFlag(); v { // seq_scaling_matrix_present_flag + for i := 0; i < 8; i++ { + br.ReadFlag() + } + } + } + + br.ReadUE() // log2_max_frame_num_minus4 + pocType, _ := br.ReadUE() + if pocType == 0 { + br.ReadUE() + } else if pocType == 1 { + br.ReadFlag() + br.ReadSE() + br.ReadSE() + cnt, _ := br.ReadUE() + for i := uint(0); i < cnt; i++ { + br.ReadSE() + } + } + + br.ReadUE() // max_num_ref_frames + br.ReadFlag() // gaps_in_frame_num_value_allowed_flag + + wMbs, _ := br.ReadUE() + hMapUnits, _ := br.ReadUE() + frameMbsOnly, _ := br.ReadFlag() + if !frameMbsOnly { + br.ReadFlag() // mb_adaptive_frame_field_flag + } + br.ReadFlag() // direct_8x8_inference_flag + + var cropLeft, cropRight, cropTop, cropBottom uint + if frameCropping, _ := br.ReadFlag(); frameCropping { + cropLeft, _ = br.ReadUE() + cropRight, _ = br.ReadUE() + cropTop, _ = br.ReadUE() + cropBottom, _ = br.ReadUE() + } + + width := (wMbs + 1) * 16 + height := (hMapUnits + 1) * 16 + if !frameMbsOnly { + height *= 2 + } + + subWidthC := getSubWidthC(chromaFormatIDC) + subHeightC := getSubHeightC(chromaFormatIDC) + cropUnitX := subWidthC + cropUnitY := subHeightC + if chromaFormatIDC == 0 { + cropUnitX = 1 + if !frameMbsOnly { + cropUnitY = 2 + } else { + cropUnitY = 1 + } + } else if !frameMbsOnly { + cropUnitY *= 2 + } + + info := &SPSInfo{ + ChromaFormatIDC: chromaFormatIDC, + PicWidthInLumaSamples: width, + PicHeightInLumaSamples: height, + ConformanceWindowFlag: cropLeft+cropRight+cropTop+cropBottom > 0, + ConfWinLeftOffset: cropLeft, + ConfWinRightOffset: cropRight, + ConfWinTopOffset: cropTop, + ConfWinBottomOffset: cropBottom, + CodedWidth: width, + CodedHeight: height, + DisplayWidth: width - (cropLeft+cropRight)*cropUnitX, + DisplayHeight: height - (cropTop+cropBottom)*cropUnitY, + } + + return info, nil +} + +// ExtractH264VideoSize extracts resolution from H.264 RTP payload +func ExtractH264VideoSize(payload []byte) VideoSize { + if len(payload) < 1 { + return VideoSize{} + } + + parseNAL := func(nal []byte) VideoSize { + info, err := parseH264SPS(nal) + if err != nil { + return VideoSize{} + } + return VideoSize{Width: uint32(info.DisplayWidth), Height: uint32(info.DisplayHeight)} + } + + nalType := payload[0] & 0x1F + + switch nalType { + case 7: // SPS NAL + return parseNAL(payload) + + case 28: // FU-A + if len(payload) < 2 { + return VideoSize{} + } + start := (payload[1] & 0x80) != 0 + if !start { + return VideoSize{} + } + nalHeader := (payload[0] & 0xE0) | (payload[1] & 0x1F) + sps := append([]byte{nalHeader}, payload[2:]...) + return parseNAL(sps) + + case 24, 25, 26, 27: // STAP-A/B, MTAP16, MTAP24 + offset := 1 + if nalType == 25 { // STAP-B has 16-bit DON + offset += 2 + } else if nalType == 26 { // MTAP16 + offset += 3 + } else if nalType == 27 { // MTAP24 + offset += 4 + } + + for offset+2 <= len(payload) { + naluSize := int(payload[offset])<<8 | int(payload[offset+1]) + offset += 2 + if offset+naluSize > len(payload) { + break + } + nalu := payload[offset : offset+naluSize] + if nalu[0]&0x1F == 7 { // SPS + return parseNAL(nalu) + } + offset += naluSize + } + return VideoSize{} + + default: + return VideoSize{} + } +} diff --git a/pkg/sfu/buffer/h26xhelper_test.go b/pkg/sfu/buffer/h26xhelper_test.go new file mode 100644 index 000000000..424d6bb8b --- /dev/null +++ b/pkg/sfu/buffer/h26xhelper_test.go @@ -0,0 +1,40 @@ +package buffer + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExtractH26xVideoSize(t *testing.T) { + type testcase struct { + payload string + width uint32 + height uint32 + isH264 bool + } + + testcases := []testcase{ + {"eAAOZ0LAH4xoBQBboB4RCNQABGjOPIA=", 1280, 720, true}, + {"eAAPZ0LAFoxoCgL3lgHhEI1AAARozjyA", 640, 360, true}, + {"eAAOZ0LADIxoFBl54B4RCNQABGjOPIA=", 320, 180, true}, + {"YAEAGkABDAP//wFgAAADALAAAAMAAAMAXQAAGwJAAC9CAQMBYAAAAwCwAAADAAADAF0AAKACgIAtFiBu5FIy5+E9C+ob1SmoCAgIH8IBBAAHRAHAcvBbJA==", 1280, 720, false}, + {"YAEAGkABDAP//wFgAAADALAAAAMAAAMAPwAAGwJAADBCAQMBYAAAAwCwAAADAAADAD8AAKAFAgFx8uIG7kUjLn4T0L6hvVKagICAgfwgEEAAB0QBwHLwWyQ=", 640, 360, false}, + {"QgEDAWAAAAMAsAAAAwAAAwA8AACgCggMHz4gM7kUhi5+E9C+ob1Q/qoI9VQT6qoK9VVBfqqqDPVVVKagICAgfwgEEA==", 320, 180, false}, + } + + for _, tc := range testcases { + payload, err := base64.StdEncoding.DecodeString(tc.payload) + require.NoError(t, err) + + var sz VideoSize + if tc.isH264 { + sz = ExtractH264VideoSize(payload) + } else { + sz = ExtractH265VideoSize(payload) + } + require.Equal(t, tc.width, sz.Width) + require.Equal(t, tc.height, sz.Height) + } +} diff --git a/pkg/sfu/buffer/helpers.go b/pkg/sfu/buffer/helpers.go index d8c862ddd..ccd0c076c 100644 --- a/pkg/sfu/buffer/helpers.go +++ b/pkg/sfu/buffer/helpers.go @@ -318,12 +318,15 @@ func IsH264KeyFrame(payload []byte) bool { // IsVP9KeyFrame detects if vp9 payload is a keyframe // taken from https://github.com/jech/galene/blob/master/codecs/codecs.go // all credits belongs to Juliusz Chroboczek @jech and the awesome Galene SFU -func IsVP9KeyFrame(payload []byte) bool { - var vp9 codecs.VP9Packet - _, err := vp9.Unmarshal(payload) - if err != nil || len(vp9.Payload) < 1 { - return false +func IsVP9KeyFrame(vp9 *codecs.VP9Packet, payload []byte) bool { + if vp9 == nil { + vp9 = &codecs.VP9Packet{} + _, err := vp9.Unmarshal(payload) + if err != nil || len(vp9.Payload) < 1 { + return false + } } + if !vp9.B { return false } @@ -452,4 +455,22 @@ func IsH265KeyFrame(payload []byte) (kf bool) { } } -// ------------------------------------- +// ExtractVP8VideoSize extracts video resolution from VP8 key frame +func ExtractVP8VideoSize(vp8Packet *VP8, payload []byte) VideoSize { + if !vp8Packet.IsKeyFrame || len(payload) < vp8Packet.HeaderSize+10 { + return VideoSize{} + } + + vp8Payload := payload[vp8Packet.HeaderSize:] + + // Check for VP8 start code + if len(vp8Payload) < 10 || vp8Payload[3] != 0x9D || vp8Payload[4] != 0x01 || vp8Payload[5] != 0x2A { + return VideoSize{} + } + + // Read width and height from bytes 6-9 + width := uint32(vp8Payload[6]) | (uint32(vp8Payload[7]) << 8) + height := uint32(vp8Payload[8]) | (uint32(vp8Payload[9]) << 8) + + return VideoSize{width & 0x3FFF, height & 0x3FFF} +} diff --git a/pkg/sfu/receiver.go b/pkg/sfu/receiver.go index 4c1773610..e51a1971b 100644 --- a/pkg/sfu/receiver.go +++ b/pkg/sfu/receiver.go @@ -141,6 +141,9 @@ type TrackReceiver interface { AddOnCodecStateChange(func(webrtc.RTPCodecParameters, ReceiverCodecState)) CodecState() ReceiverCodecState + + // VideoSizes returns the video size parsed from rtp packet for each spatial layer. + VideoSizes() []buffer.VideoSize } type REDTransformer interface { @@ -181,10 +184,13 @@ type WebRTCReceiver struct { onRTCP func([]rtcp.Packet) - bufferMu sync.RWMutex - buffers [buffer.DefaultMaxLayerSpatial + 1]*buffer.Buffer - upTracks [buffer.DefaultMaxLayerSpatial + 1]TrackRemote - rtt uint32 + bufferMu sync.RWMutex + buffers [buffer.DefaultMaxLayerSpatial + 1]*buffer.Buffer + upTracks [buffer.DefaultMaxLayerSpatial + 1]TrackRemote + videoSizeMu sync.RWMutex + videoSizes [buffer.DefaultMaxLayerSpatial + 1]buffer.VideoSize + onVideoSizeChanged func() + rtt uint32 lbThreshold int @@ -415,6 +421,21 @@ func (w *WebRTCReceiver) AddUpTrack(track TrackRemote, buff *buffer.Buffer) erro rt.(REDTransformer).ForwardRTCPSenderReport(w.codec.PayloadType, layer, srData) } }) + buff.OnVideoSizeChanged(func(videoSize []buffer.VideoSize) { + w.videoSizeMu.Lock() + if w.videoLayerMode == livekit.VideoLayer_MULTIPLE_SPATIAL_LAYERS_PER_STREAM { + copy(w.videoSizes[:], videoSize) + } else { + w.videoSizes[layer] = videoSize[0] + } + w.logger.Debugw("video size changed", "size", w.videoSizes) + cb := w.onVideoSizeChanged + w.videoSizeMu.Unlock() + + if cb != nil { + cb() + } + }) if w.Kind() == webrtc.RTPCodecTypeVideo && layer == 0 { buff.OnCodecChange(w.handleCodecChange) @@ -974,6 +995,26 @@ func (w *WebRTCReceiver) SetCodecState(state ReceiverCodecState) { } } +func (w *WebRTCReceiver) VideoSizes() []buffer.VideoSize { + var sizes []buffer.VideoSize + w.videoSizeMu.RLock() + defer w.videoSizeMu.RUnlock() + for _, v := range w.videoSizes { + if v.Width == 0 || v.Height == 0 { + break + } + sizes = append(sizes, v) + } + + return sizes +} + +func (w *WebRTCReceiver) OnVideoSizeChanged(f func()) { + w.videoSizeMu.Lock() + w.onVideoSizeChanged = f + w.videoSizeMu.Unlock() +} + // ----------------------------------------------------------- // closes all track senders in parallel, returns when all are closed From afbf541ee2adda8a77173e5e1fed26bfe3179731 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:34:44 -0700 Subject: [PATCH 2/5] Update pion deps (#3863) Generated by renovateBot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c4ff89c1c..119e6e86c 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/ory/dockertest/v3 v3.12.0 github.com/pion/datachannel v1.5.10 - github.com/pion/dtls/v3 v3.0.6 + github.com/pion/dtls/v3 v3.0.7 github.com/pion/ice/v4 v4.0.10 github.com/pion/interceptor v0.1.40 github.com/pion/rtcp v1.2.15 @@ -40,7 +40,7 @@ require ( github.com/pion/sctp v1.8.39 github.com/pion/sdp/v3 v3.0.15 github.com/pion/transport/v3 v3.0.7 - github.com/pion/turn/v4 v4.1.0 + github.com/pion/turn/v4 v4.1.1 github.com/pion/webrtc/v4 v4.1.3 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.0 diff --git a/go.sum b/go.sum index 4cef0cd2e..05706c498 100644 --- a/go.sum +++ b/go.sum @@ -235,8 +235,8 @@ github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCy github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= -github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= +github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= @@ -261,8 +261,8 @@ github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.1.0 h1:+J56+aS8Bi6B4zij3ah6VvJpRuy8W8FtExR0OJPiTdM= -github.com/pion/turn/v4 v4.1.0/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= +github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= +github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c= github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= From 98d577eed94880ea5e29d08ceb98bf2db905e6c4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:41:35 -0700 Subject: [PATCH 3/5] Update module github.com/livekit/protocol to v1.40.0 (#3865) Generated by renovateBot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 119e6e86c..464b3e3ef 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded - github.com/livekit/protocol v1.39.4-0.20250813045730-236ffc7e5e12 + github.com/livekit/protocol v1.40.0 github.com/livekit/psrpc v0.6.1-0.20250726180611-3915e005e741 github.com/mackerelio/go-osstat v0.2.6 github.com/magefile/mage v1.15.0 diff --git a/go.sum b/go.sum index 05706c498..ba52efbde 100644 --- a/go.sum +++ b/go.sum @@ -170,8 +170,8 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded h1:ylZPdnlX1RW9Z15SD4mp87vT2D2shsk0hpLJwSPcq3g= github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded/go.mod h1:mSNtYzSf6iY9xM3UX42VEI+STHvMgHmrYzEHPcdhB8A= -github.com/livekit/protocol v1.39.4-0.20250813045730-236ffc7e5e12 h1:Ya6XP1yroaBk6SfYJa+wzkkUWHFZ00RrV0mam+wwAfI= -github.com/livekit/protocol v1.39.4-0.20250813045730-236ffc7e5e12/go.mod h1:YlgUxAegtU8jZ0tVXoIV/4fHeHqqLvS+6JnPKDbpFPU= +github.com/livekit/protocol v1.40.0 h1:FzCv17ivkxI4ySE0uFXSPPVPBVepf736Cgnmi054z2o= +github.com/livekit/protocol v1.40.0/go.mod h1:YlgUxAegtU8jZ0tVXoIV/4fHeHqqLvS+6JnPKDbpFPU= github.com/livekit/psrpc v0.6.1-0.20250726180611-3915e005e741 h1:KKL1u94l6dF9u4cBwnnfozk27GH1txWy2SlvkfgmzoY= github.com/livekit/psrpc v0.6.1-0.20250726180611-3915e005e741/go.mod h1:AuDC5uOoEjQJEc69v4Li3t77Ocz0e0NdjQEuFfO+vfk= github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0= From c58e5d23c56a9717da0b0d231539af38d5b51c47 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:49:02 -0700 Subject: [PATCH 4/5] Update golang Docker tag to v1.25 (#3864) Generated by renovateBot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4d8e23170..d2ad082df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM golang:1.24-alpine AS builder +FROM golang:1.25-alpine AS builder ARG TARGETPLATFORM ARG TARGETARCH From d62336e1a608b034e49bd9b99f8bd72d9a14e1e1 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Wed, 20 Aug 2025 09:24:13 +0530 Subject: [PATCH 5/5] Remove unnecessary check (#3870) --- pkg/rtc/mediatracksubscriptions.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/rtc/mediatracksubscriptions.go b/pkg/rtc/mediatracksubscriptions.go index 9026a5d84..133573f43 100644 --- a/pkg/rtc/mediatracksubscriptions.go +++ b/pkg/rtc/mediatracksubscriptions.go @@ -168,9 +168,7 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr * if !sub.Hidden() { downTrack.OnBindAndConnected(func() { - if err == nil { - t.params.MediaTrack.OnTrackSubscribed() - } + t.params.MediaTrack.OnTrackSubscribed() }) }