diff --git a/rtpcodec.go b/rtpcodec.go index d4733dbd07e..6190724fae6 100644 --- a/rtpcodec.go +++ b/rtpcodec.go @@ -143,16 +143,26 @@ func codecParametersFuzzySearch( return RTPCodecParameters{}, codecMatchNone } -// Given a CodecParameters find the RTX CodecParameters if one exists. +// Given a CodecParameters find the RTX PayloadType. func findRTXPayloadType(needle PayloadType, haystack []RTPCodecParameters) PayloadType { + rtxParam := findRTXCodecParameters(needle, haystack) + if rtxParam != nil { + return rtxParam.PayloadType + } + + return PayloadType(0) +} + +// Given a CodecParameters find the RTX CodecParameters if one exists. +func findRTXCodecParameters(needle PayloadType, haystack []RTPCodecParameters) *RTPCodecParameters { aptStr := fmt.Sprintf("apt=%d", needle) - for _, c := range haystack { + for i, c := range haystack { if aptStr == c.SDPFmtpLine { - return c.PayloadType + return &haystack[i] } } - return PayloadType(0) + return nil } // For now, only FlexFEC is supported. diff --git a/rtpreceiver.go b/rtpreceiver.go index 0e85b2d958c..ae72d8e804c 100644 --- a/rtpreceiver.go +++ b/rtpreceiver.go @@ -19,25 +19,84 @@ import ( "github.com/pion/webrtc/v4/internal/util" ) +type trackStream struct { + streamInfo *interceptor.StreamInfo + rtpReadStream *srtp.ReadStreamSRTP + rtpInterceptor interceptor.RTPReader + rtcpReadStream *srtp.ReadStreamSRTCP + rtcpInterceptor interceptor.RTCPReader +} + // trackStreams maintains a mapping of RTP/RTCP streams to a specific track // a RTPReceiver may contain multiple streams if we are dealing with Simulcast. type trackStreams struct { track *TrackRemote - streamInfo, repairStreamInfo *interceptor.StreamInfo + mediaStream trackStream + rtxStream trackStream - rtpReadStream *srtp.ReadStreamSRTP - rtpInterceptor interceptor.RTPReader + repairStreamChannel chan rtxPacketWithAttributes +} - rtcpReadStream *srtp.ReadStreamSRTCP - rtcpInterceptor interceptor.RTCPReader +func (s *trackStreams) configureStreams( + decodingParams RTPDecodingParameters, + codec RTPCodecCapability, + globalParams RTPParameters, + streamsForSSRC func(SSRC, interceptor.StreamInfo) ( + *srtp.ReadStreamSRTP, + interceptor.RTPReader, + *srtp.ReadStreamSRTCP, + interceptor.RTCPReader, + error, + ), +) (bool, error) { + rtxPayloadType := findRTXPayloadType(globalParams.Codecs[0].PayloadType, globalParams.Codecs) + + s.mediaStream.streamInfo = createStreamInfo( + "", + decodingParams.SSRC, + decodingParams.RTX.SSRC, + decodingParams.FEC.SSRC, + globalParams.Codecs[0].PayloadType, + rtxPayloadType, + findFECPayloadType(globalParams.Codecs), + codec, + globalParams.HeaderExtensions, + ) + var err error + s.mediaStream.rtpReadStream, + s.mediaStream.rtpInterceptor, + s.mediaStream.rtcpReadStream, + s.mediaStream.rtcpInterceptor, err = streamsForSSRC(decodingParams.SSRC, *s.mediaStream.streamInfo) + if err != nil { + return false, err + } - repairReadStream *srtp.ReadStreamSRTP - repairInterceptor interceptor.RTPReader - repairStreamChannel chan rtxPacketWithAttributes + if decodingParams.RTX.SSRC == 0 { + return false, nil + } - repairRtcpReadStream *srtp.ReadStreamSRTCP - repairRtcpInterceptor interceptor.RTCPReader + s.rtxStream.streamInfo = createStreamInfo( + "", + decodingParams.RTX.SSRC, + 0, + 0, + rtxPayloadType, + 0, + 0, + codec, + globalParams.HeaderExtensions, + ) + s.rtxStream.rtpReadStream, + s.rtxStream.rtpInterceptor, + s.rtxStream.rtcpReadStream, + s.rtxStream.rtcpInterceptor, err = streamsForSSRC(decodingParams.RTX.SSRC, *s.rtxStream.streamInfo) + if err != nil { + return true, err + } + s.repairStreamChannel = make(chan rtxPacketWithAttributes, 50) + + return true, nil } type rtxPacketWithAttributes struct { @@ -218,41 +277,18 @@ func (r *RTPReceiver) startReceive(parameters RTPReceiveParameters) error { //no return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, parameters.Encodings[i].SSRC) } - streams.streamInfo = createStreamInfo( - "", - parameters.Encodings[i].SSRC, - 0, 0, 0, 0, 0, + haveRTXStream, err := streams.configureStreams( + parameters.Encodings[i], codec, - globalParams.HeaderExtensions, + globalParams, + r.transport.streamsForSSRC, ) - var err error - - //nolint:lll // # TODO refactor - if streams.rtpReadStream, streams.rtpInterceptor, streams.rtcpReadStream, streams.rtcpInterceptor, err = r.transport.streamsForSSRC(parameters.Encodings[i].SSRC, *streams.streamInfo); err != nil { + if err != nil { return err } - if rtxSsrc := parameters.Encodings[i].RTX.SSRC; rtxSsrc != 0 { - streamInfo := createStreamInfo("", rtxSsrc, 0, 0, 0, 0, 0, codec, globalParams.HeaderExtensions) - rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor, err := r.transport.streamsForSSRC( - rtxSsrc, - *streamInfo, - ) - if err != nil { - return err - } - - if err = r.receiveForRtx( - rtxSsrc, - "", - streamInfo, - rtpReadStream, - rtpInterceptor, - rtcpReadStream, - rtcpInterceptor, - ); err != nil { - return err - } + if haveRTXStream { + go r.handleReceivedRTXPackets(streams) } } @@ -272,7 +308,7 @@ func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error { func (r *RTPReceiver) Read(b []byte) (n int, a interceptor.Attributes, err error) { select { case <-r.received: - return r.tracks[0].rtcpInterceptor.Read(b, a) + return r.tracks[0].mediaStream.rtcpInterceptor.Read(b, a) case <-r.closed: return 0, nil, io.ErrClosedPipe } @@ -287,7 +323,7 @@ func (r *RTPReceiver) ReadSimulcast(b []byte, rid string) (n int, a interceptor. r.mu.Lock() for _, t := range r.tracks { if t.track != nil && t.track.rid == rid { - rtcpInterceptor = t.rtcpInterceptor + rtcpInterceptor = t.mediaStream.rtcpInterceptor } } r.mu.Unlock() @@ -359,28 +395,24 @@ func (r *RTPReceiver) Stop() error { //nolint:cyclop for i := range r.tracks { errs := []error{} - if r.tracks[i].rtcpReadStream != nil { - errs = append(errs, r.tracks[i].rtcpReadStream.Close()) + if r.tracks[i].mediaStream.streamInfo != nil { + r.api.interceptor.UnbindRemoteStream(r.tracks[i].mediaStream.streamInfo) } - - if r.tracks[i].rtpReadStream != nil { - errs = append(errs, r.tracks[i].rtpReadStream.Close()) + if r.tracks[i].mediaStream.rtcpReadStream != nil { + errs = append(errs, r.tracks[i].mediaStream.rtcpReadStream.Close()) } - - if r.tracks[i].repairReadStream != nil { - errs = append(errs, r.tracks[i].repairReadStream.Close()) + if r.tracks[i].mediaStream.rtpReadStream != nil { + errs = append(errs, r.tracks[i].mediaStream.rtpReadStream.Close()) } - if r.tracks[i].repairRtcpReadStream != nil { - errs = append(errs, r.tracks[i].repairRtcpReadStream.Close()) + if r.tracks[i].rtxStream.streamInfo != nil { + r.api.interceptor.UnbindRemoteStream(r.tracks[i].rtxStream.streamInfo) } - - if r.tracks[i].streamInfo != nil { - r.api.interceptor.UnbindRemoteStream(r.tracks[i].streamInfo) + if r.tracks[i].rtxStream.rtcpReadStream != nil { + errs = append(errs, r.tracks[i].rtxStream.rtcpReadStream.Close()) } - - if r.tracks[i].repairStreamInfo != nil { - r.api.interceptor.UnbindRemoteStream(r.tracks[i].repairStreamInfo) + if r.tracks[i].rtxStream.rtpReadStream != nil { + errs = append(errs, r.tracks[i].rtxStream.rtpReadStream.Close()) } err = util.FlattenErrs(errs) @@ -412,7 +444,7 @@ func (r *RTPReceiver) readRTP(b []byte, reader *TrackRemote) (n int, a intercept } if t := r.streamsForTrack(reader); t != nil { - return t.rtpInterceptor.Read(b, a) + return t.mediaStream.rtpInterceptor.Read(b, a) } return 0, nil, fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, reader.SSRC()) @@ -441,11 +473,11 @@ func (r *RTPReceiver) receiveForRid( r.tracks[i].track.ssrc = SSRC(streamInfo.SSRC) r.tracks[i].track.mu.Unlock() - r.tracks[i].streamInfo = streamInfo - r.tracks[i].rtpReadStream = rtpReadStream - r.tracks[i].rtpInterceptor = rtpInterceptor - r.tracks[i].rtcpReadStream = rtcpReadStream - r.tracks[i].rtcpInterceptor = rtcpInterceptor + r.tracks[i].mediaStream.streamInfo = streamInfo + r.tracks[i].mediaStream.rtpReadStream = rtpReadStream + r.tracks[i].mediaStream.rtpInterceptor = rtpInterceptor + r.tracks[i].mediaStream.rtcpReadStream = rtcpReadStream + r.tracks[i].mediaStream.rtcpInterceptor = rtcpInterceptor return r.tracks[i].track, nil } @@ -454,6 +486,63 @@ func (r *RTPReceiver) receiveForRid( return nil, fmt.Errorf("%w: %s", errRTPReceiverForRIDTrackStreamNotFound, rid) } +func (r *RTPReceiver) handleReceivedRTXPackets(stream *trackStreams) { + for { + b := r.rtxPool.Get().([]byte) // nolint:forcetypeassert + i, attributes, err := stream.rtxStream.rtpInterceptor.Read(b, nil) + if err != nil { + r.rtxPool.Put(b) // nolint:staticcheck + + return + } + + // RTX packets have a different payload format. Move the OSN in the payload to the RTP header and rewrite the + // payload type and SSRC, so that we can return RTX packets to the caller 'transparently' i.e. in the same format + // as non-RTX RTP packets + hasExtension := b[0]&0b10000 > 0 + hasPadding := b[0]&0b100000 > 0 + csrcCount := b[0] & 0b1111 + headerLength := uint16(12 + (4 * csrcCount)) + paddingLength := 0 + if hasExtension { + headerLength += 4 * (1 + binary.BigEndian.Uint16(b[headerLength+2:headerLength+4])) + } + if hasPadding { + paddingLength = int(b[i-1]) + } + + if i-int(headerLength)-paddingLength < 2 { + // BWE probe packet, ignore + r.rtxPool.Put(b) // nolint:staticcheck + + continue + } + + if attributes == nil { + attributes = make(interceptor.Attributes) + } + attributes.Set(AttributeRtxPayloadType, b[1]&0x7F) + attributes.Set(AttributeRtxSequenceNumber, binary.BigEndian.Uint16(b[2:4])) + attributes.Set(AttributeRtxSsrc, binary.BigEndian.Uint32(b[8:12])) + + b[1] = (b[1] & 0x80) | uint8(stream.track.PayloadType()) + b[2] = b[headerLength] + b[3] = b[headerLength+1] + binary.BigEndian.PutUint32(b[8:12], uint32(stream.track.SSRC())) + copy(b[headerLength:i-2], b[headerLength+2:i]) + + select { + case <-r.closed: + r.rtxPool.Put(b) // nolint:staticcheck + + return + case stream.repairStreamChannel <- rtxPacketWithAttributes{pkt: b[:i-2], attributes: attributes, pool: &r.rtxPool}: + default: + // skip the RTX packet if the repair stream channel is full, could be blocked in the application's read loop + } + } +} + // receiveForRtx starts a routine that processes the repair stream. // //nolint:cyclop @@ -486,69 +575,14 @@ func (r *RTPReceiver) receiveForRtx( return fmt.Errorf("%w: ssrc(%d) rsid(%s)", errRTPReceiverForRIDTrackStreamNotFound, ssrc, rsid) } - track.repairStreamInfo = streamInfo - track.repairReadStream = rtpReadStream - track.repairInterceptor = rtpInterceptor - track.repairRtcpReadStream = rtcpReadStream - track.repairRtcpInterceptor = rtcpInterceptor + track.rtxStream.streamInfo = streamInfo + track.rtxStream.rtpReadStream = rtpReadStream + track.rtxStream.rtpInterceptor = rtpInterceptor + track.rtxStream.rtcpReadStream = rtcpReadStream + track.rtxStream.rtcpInterceptor = rtcpInterceptor track.repairStreamChannel = make(chan rtxPacketWithAttributes, 50) - go func() { - for { - b := r.rtxPool.Get().([]byte) // nolint:forcetypeassert - i, attributes, err := track.repairInterceptor.Read(b, nil) - if err != nil { - r.rtxPool.Put(b) // nolint:staticcheck - - return - } - - // RTX packets have a different payload format. Move the OSN in the payload to the RTP header and rewrite the - // payload type and SSRC, so that we can return RTX packets to the caller 'transparently' i.e. in the same format - // as non-RTX RTP packets - hasExtension := b[0]&0b10000 > 0 - hasPadding := b[0]&0b100000 > 0 - csrcCount := b[0] & 0b1111 - headerLength := uint16(12 + (4 * csrcCount)) - paddingLength := 0 - if hasExtension { - headerLength += 4 * (1 + binary.BigEndian.Uint16(b[headerLength+2:headerLength+4])) - } - if hasPadding { - paddingLength = int(b[i-1]) - } - - if i-int(headerLength)-paddingLength < 2 { - // BWE probe packet, ignore - r.rtxPool.Put(b) // nolint:staticcheck - - continue - } - - if attributes == nil { - attributes = make(interceptor.Attributes) - } - attributes.Set(AttributeRtxPayloadType, b[1]&0x7F) - attributes.Set(AttributeRtxSequenceNumber, binary.BigEndian.Uint16(b[2:4])) - attributes.Set(AttributeRtxSsrc, binary.BigEndian.Uint32(b[8:12])) - - b[1] = (b[1] & 0x80) | uint8(track.track.PayloadType()) - b[2] = b[headerLength] - b[3] = b[headerLength+1] - binary.BigEndian.PutUint32(b[8:12], uint32(track.track.SSRC())) - copy(b[headerLength:i-2], b[headerLength+2:i]) - - select { - case <-r.closed: - r.rtxPool.Put(b) // nolint:staticcheck - - return - case track.repairStreamChannel <- rtxPacketWithAttributes{pkt: b[:i-2], attributes: attributes, pool: &r.rtxPool}: - default: - // skip the RTX packet if the repair stream channel is full, could be blocked in the application's read loop - } - } - }() + go r.handleReceivedRTXPackets(track) return nil } @@ -558,7 +592,7 @@ func (r *RTPReceiver) SetReadDeadline(t time.Time) error { r.mu.RLock() defer r.mu.RUnlock() - return r.tracks[0].rtcpReadStream.SetReadDeadline(t) + return r.tracks[0].mediaStream.rtcpReadStream.SetReadDeadline(t) } // SetReadDeadlineSimulcast sets the max amount of time the RTCP stream for a given rid will block before returning. @@ -569,7 +603,7 @@ func (r *RTPReceiver) SetReadDeadlineSimulcast(deadline time.Time, rid string) e for _, t := range r.tracks { if t.track != nil && t.track.rid == rid { - return t.rtcpReadStream.SetReadDeadline(deadline) + return t.mediaStream.rtcpReadStream.SetReadDeadline(deadline) } } @@ -583,7 +617,7 @@ func (r *RTPReceiver) setRTPReadDeadline(deadline time.Time, reader *TrackRemote defer r.mu.RUnlock() if t := r.streamsForTrack(reader); t != nil { - return t.rtpReadStream.SetReadDeadline(deadline) + return t.mediaStream.rtpReadStream.SetReadDeadline(deadline) } return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, reader.SSRC()) diff --git a/rtpreceiver_go.go b/rtpreceiver_go.go index 4d150f0f31e..b7eb9b6aa56 100644 --- a/rtpreceiver_go.go +++ b/rtpreceiver_go.go @@ -26,7 +26,7 @@ func (r *RTPReceiver) SetRTPParameters(params RTPParameters) { for ndx, codec := range params.Codecs { currentTrack := r.tracks[ndx].track - r.tracks[ndx].streamInfo.RTPHeaderExtensions = headerExtensions + r.tracks[ndx].mediaStream.streamInfo.RTPHeaderExtensions = headerExtensions currentTrack.mu.Lock() currentTrack.codec = codec diff --git a/rtpreceiver_test.go b/rtpreceiver_test.go index 0c09ca92f2f..3b53a73ff36 100644 --- a/rtpreceiver_test.go +++ b/rtpreceiver_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/pion/interceptor" + "github.com/pion/srtp/v3" "github.com/pion/transport/v3/test" "github.com/pion/webrtc/v4/pkg/media" "github.com/stretchr/testify/assert" @@ -64,3 +65,238 @@ func Test_RTPReceiver_SetReadDeadline(t *testing.T) { assert.NoError(t, wan.Stop()) closePairNow(t, sender, receiver) } + +func Test_trackStreams_configureStreams(t *testing.T) { + t.Run("with RTX", func(t *testing.T) { + trackStreams := &trackStreams{} + decodingParams := RTPDecodingParameters{ + RTPCodingParameters: RTPCodingParameters{ + RID: "", + SSRC: 0x89b82af4, + PayloadType: 0x0, + RTX: RTPRtxParameters{SSRC: 0x35c9eefc}, + FEC: RTPFecParameters{SSRC: 0x0}, + }, + } + codec := RTPCodecCapability{ + MimeType: "video/AV1", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "level-idx=5;profile=0;tier=0", + RTCPFeedback: []RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + {Type: "transport-cc", Parameter: ""}, + }, + } + globalParams := RTPParameters{ + HeaderExtensions: []RTPHeaderExtensionParameter{ + { + URI: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + ID: 4, + }, + }, + Codecs: []RTPCodecParameters{ + { + RTPCodecCapability: RTPCodecCapability{ + MimeType: "video/AV1", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "level-idx=5;profile=0;tier=0", + RTCPFeedback: []RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + {Type: "transport-cc", Parameter: ""}, + }, + }, + PayloadType: 0x2d, + statsID: "", + }, + { + RTPCodecCapability: RTPCodecCapability{ + MimeType: "video/rtx", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "apt=45", + RTCPFeedback: nil, + }, + PayloadType: 0x2e, + statsID: "", + }, + }, + } + expectedMediaStreamInfo := &interceptor.StreamInfo{ + ID: "", + Attributes: interceptor.Attributes{}, + SSRC: 0x89b82af4, + SSRCRetransmission: 0x35c9eefc, + SSRCForwardErrorCorrection: 0x0, + PayloadType: 0x2d, + PayloadTypeRetransmission: 0x2e, + PayloadTypeForwardErrorCorrection: 0x0, + RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ + {URI: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ID: 4}, + }, + MimeType: "video/AV1", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "level-idx=5;profile=0;tier=0", + RTCPFeedback: []interceptor.RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + {Type: "transport-cc", Parameter: ""}, + }, + } + expectedRTXStreamInfo := &interceptor.StreamInfo{ + ID: "", + Attributes: interceptor.Attributes{}, + SSRC: 0x35c9eefc, + SSRCRetransmission: 0x0, + SSRCForwardErrorCorrection: 0x0, + PayloadType: 0x2e, + PayloadTypeRetransmission: 0x0, + PayloadTypeForwardErrorCorrection: 0x0, + RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ + {URI: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ID: 4}, + }, + MimeType: "video/AV1", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "level-idx=5;profile=0;tier=0", + RTCPFeedback: []interceptor.RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + {Type: "transport-cc", Parameter: ""}, + }, + } + callCount := 0 + haveRTX, err := trackStreams.configureStreams( + decodingParams, + codec, + globalParams, + // Mock DTLSTransport.streamsForSSRC here, DTLSTransport.streamsForSSRC calls interceptor.BindRemoteStream, + // so the interceptor.StreamInfo here is exactly the StreamInfo that will be passed into interceptors. + func(s SSRC, si interceptor.StreamInfo) ( + *srtp.ReadStreamSRTP, + interceptor.RTPReader, + *srtp.ReadStreamSRTCP, + interceptor.RTCPReader, + error, + ) { + switch callCount { + case 0: + assert.Equal(t, s, SSRC(0x89b82af4)) + assert.Equal(t, si, *expectedMediaStreamInfo) + case 1: + assert.Equal(t, s, SSRC(0x35c9eefc)) + assert.Equal(t, si, *expectedRTXStreamInfo) + default: + assert.Fail(t, "streamsForSSRC called more than twice when only video track and rtx track existed") + } + callCount++ + + return nil, nil, nil, nil, nil + }, + ) + assert.NoError(t, err) + assert.True(t, haveRTX) + assert.Equal(t, trackStreams.mediaStream.streamInfo, expectedMediaStreamInfo) + assert.Equal(t, trackStreams.rtxStream.streamInfo, expectedRTXStreamInfo) + }) + + t.Run("no RTX", func(t *testing.T) { + trackStreams := &trackStreams{} + decodingParams := RTPDecodingParameters{ + RTPCodingParameters: RTPCodingParameters{ + RID: "", + SSRC: 0x89b82af4, + PayloadType: 0x0, + RTX: RTPRtxParameters{SSRC: 0x0}, + FEC: RTPFecParameters{SSRC: 0x0}, + }, + } + codec := RTPCodecCapability{ + MimeType: "video/AV1", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "level-idx=5;profile=0;tier=0", + RTCPFeedback: []RTCPFeedback{ + {Type: "transport-cc", Parameter: ""}, + }, + } + globalParams := RTPParameters{ + HeaderExtensions: []RTPHeaderExtensionParameter{ + { + URI: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + ID: 4, + }, + }, + Codecs: []RTPCodecParameters{ + { + RTPCodecCapability: RTPCodecCapability{ + MimeType: "video/AV1", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "level-idx=5;profile=0;tier=0", + RTCPFeedback: []RTCPFeedback{ + {Type: "transport-cc", Parameter: ""}, + }, + }, + PayloadType: 0x2d, + statsID: "", + }, + }, + } + expectedMediaStreamInfo := &interceptor.StreamInfo{ + ID: "", + Attributes: interceptor.Attributes{}, + SSRC: 0x89b82af4, + SSRCRetransmission: 0x0, + SSRCForwardErrorCorrection: 0x0, + PayloadType: 0x2d, + PayloadTypeRetransmission: 0x0, + PayloadTypeForwardErrorCorrection: 0x0, + RTPHeaderExtensions: []interceptor.RTPHeaderExtension{ + {URI: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ID: 4}, + }, + MimeType: "video/AV1", + ClockRate: 0x15f90, + Channels: 0x0, + SDPFmtpLine: "level-idx=5;profile=0;tier=0", + RTCPFeedback: []interceptor.RTCPFeedback{ + {Type: "transport-cc", Parameter: ""}, + }, + } + var expectedRTXStreamInfo *interceptor.StreamInfo + callCount := 0 + haveRTX, err := trackStreams.configureStreams( + decodingParams, + codec, + globalParams, + // Mock DTLSTransport.streamsForSSRC here, DTLSTransport.streamsForSSRC calls interceptor.BindRemoteStream, + // so the interceptor.StreamInfo here is exactly the StreamInfo that will be passed into interceptors. + func(s SSRC, si interceptor.StreamInfo) ( + *srtp.ReadStreamSRTP, + interceptor.RTPReader, + *srtp.ReadStreamSRTCP, + interceptor.RTCPReader, + error, + ) { + switch callCount { + case 0: + assert.Equal(t, s, SSRC(0x89b82af4)) + assert.Equal(t, si, *expectedMediaStreamInfo) + default: + assert.Fail(t, "streamsForSSRC called more than once when only video track existed") + } + callCount++ + + return nil, nil, nil, nil, nil + }, + ) + assert.NoError(t, err) + assert.False(t, haveRTX) + assert.Equal(t, trackStreams.mediaStream.streamInfo, expectedMediaStreamInfo) + assert.Equal(t, trackStreams.rtxStream.streamInfo, expectedRTXStreamInfo) + }) +}