// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package webrtc import ( "strings" "sync" "github.com/pion/rtp" "github.com/pion/webrtc/v3/internal/util" "github.com/pion/webrtc/v3/pkg/media" ) // trackBinding is a single bind for a Track // Bind can be called multiple times, this stores the // result for a single bind call so that it can be used when writing type trackBinding struct { id string ssrc SSRC payloadType PayloadType writeStream TrackLocalWriter } // TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets. // If you wish to send a media.Sample use TrackLocalStaticSample type TrackLocalStaticRTP struct { mu sync.RWMutex bindings []trackBinding codec RTPCodecCapability id, rid, streamID string } // NewTrackLocalStaticRTP returns a TrackLocalStaticRTP. func NewTrackLocalStaticRTP(c RTPCodecCapability, id, streamID string, options ...func(*TrackLocalStaticRTP)) (*TrackLocalStaticRTP, error) { t := &TrackLocalStaticRTP{ codec: c, bindings: []trackBinding{}, id: id, streamID: streamID, } for _, option := range options { option(t) } return t, nil } // WithRTPStreamID sets the RTP stream ID for this TrackLocalStaticRTP. func WithRTPStreamID(rid string) func(*TrackLocalStaticRTP) { return func(t *TrackLocalStaticRTP) { t.rid = rid } } // Bind is called by the PeerConnection after negotiation is complete // This asserts that the code requested is supported by the remote peer. // If so it setups all the state (SSRC and PayloadType) to have a call func (s *TrackLocalStaticRTP) Bind(t TrackLocalContext) (RTPCodecParameters, error) { s.mu.Lock() defer s.mu.Unlock() parameters := RTPCodecParameters{RTPCodecCapability: s.codec} if codec, matchType := codecParametersFuzzySearch(parameters, t.CodecParameters()); matchType != codecMatchNone { s.bindings = append(s.bindings, trackBinding{ ssrc: t.SSRC(), payloadType: codec.PayloadType, writeStream: t.WriteStream(), id: t.ID(), }) return codec, nil } return RTPCodecParameters{}, ErrUnsupportedCodec } // Unbind implements the teardown logic when the track is no longer needed. This happens // because a track has been stopped. func (s *TrackLocalStaticRTP) Unbind(t TrackLocalContext) error { s.mu.Lock() defer s.mu.Unlock() for i := range s.bindings { if s.bindings[i].id == t.ID() { s.bindings[i] = s.bindings[len(s.bindings)-1] s.bindings = s.bindings[:len(s.bindings)-1] return nil } } return ErrUnbindFailed } // ID is the unique identifier for this Track. This should be unique for the // stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' // and StreamID would be 'desktop' or 'webcam' func (s *TrackLocalStaticRTP) ID() string { return s.id } // StreamID is the group this track belongs too. This must be unique func (s *TrackLocalStaticRTP) StreamID() string { return s.streamID } // RID is the RTP stream identifier. func (s *TrackLocalStaticRTP) RID() string { return s.rid } // Kind controls if this TrackLocal is audio or video func (s *TrackLocalStaticRTP) Kind() RTPCodecType { switch { case strings.HasPrefix(s.codec.MimeType, "audio/"): return RTPCodecTypeAudio case strings.HasPrefix(s.codec.MimeType, "video/"): return RTPCodecTypeVideo default: return RTPCodecType(0) } } // Codec gets the Codec of the track func (s *TrackLocalStaticRTP) Codec() RTPCodecCapability { return s.codec } // packetPool is a pool of packets used by WriteRTP and Write below // nolint:gochecknoglobals var rtpPacketPool = sync.Pool{ New: func() interface{} { return &rtp.Packet{} }, } func resetPacketPoolAllocation(localPacket *rtp.Packet) { *localPacket = rtp.Packet{} rtpPacketPool.Put(localPacket) } func getPacketAllocationFromPool() *rtp.Packet { ipacket := rtpPacketPool.Get() return ipacket.(*rtp.Packet) //nolint:forcetypeassert } // WriteRTP writes a RTP Packet to the TrackLocalStaticRTP // If one PeerConnection fails the packets will still be sent to // all PeerConnections. The error message will contain the ID of the failed // PeerConnections so you can remove them func (s *TrackLocalStaticRTP) WriteRTP(p *rtp.Packet) error { packet := getPacketAllocationFromPool() defer resetPacketPoolAllocation(packet) *packet = *p return s.writeRTP(packet) } // writeRTP is like WriteRTP, except that it may modify the packet p func (s *TrackLocalStaticRTP) writeRTP(p *rtp.Packet) error { s.mu.RLock() defer s.mu.RUnlock() writeErrs := []error{} for _, b := range s.bindings { p.Header.SSRC = uint32(b.ssrc) p.Header.PayloadType = uint8(b.payloadType) if _, err := b.writeStream.WriteRTP(&p.Header, p.Payload); err != nil { writeErrs = append(writeErrs, err) } } return util.FlattenErrs(writeErrs) } // Write writes a RTP Packet as a buffer to the TrackLocalStaticRTP // If one PeerConnection fails the packets will still be sent to // all PeerConnections. The error message will contain the ID of the failed // PeerConnections so you can remove them func (s *TrackLocalStaticRTP) Write(b []byte) (n int, err error) { packet := getPacketAllocationFromPool() defer resetPacketPoolAllocation(packet) if err = packet.Unmarshal(b); err != nil { return 0, err } return len(b), s.writeRTP(packet) } // TrackLocalStaticSample is a TrackLocal that has a pre-set codec and accepts Samples. // If you wish to send a RTP Packet use TrackLocalStaticRTP type TrackLocalStaticSample struct { packetizer rtp.Packetizer sequencer rtp.Sequencer rtpTrack *TrackLocalStaticRTP clockRate float64 } // NewTrackLocalStaticSample returns a TrackLocalStaticSample func NewTrackLocalStaticSample(c RTPCodecCapability, id, streamID string, options ...func(*TrackLocalStaticRTP)) (*TrackLocalStaticSample, error) { rtpTrack, err := NewTrackLocalStaticRTP(c, id, streamID, options...) if err != nil { return nil, err } return &TrackLocalStaticSample{ rtpTrack: rtpTrack, }, nil } // ID is the unique identifier for this Track. This should be unique for the // stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' // and StreamID would be 'desktop' or 'webcam' func (s *TrackLocalStaticSample) ID() string { return s.rtpTrack.ID() } // StreamID is the group this track belongs too. This must be unique func (s *TrackLocalStaticSample) StreamID() string { return s.rtpTrack.StreamID() } // RID is the RTP stream identifier. func (s *TrackLocalStaticSample) RID() string { return s.rtpTrack.RID() } // Kind controls if this TrackLocal is audio or video func (s *TrackLocalStaticSample) Kind() RTPCodecType { return s.rtpTrack.Kind() } // Codec gets the Codec of the track func (s *TrackLocalStaticSample) Codec() RTPCodecCapability { return s.rtpTrack.Codec() } // Bind is called by the PeerConnection after negotiation is complete // This asserts that the code requested is supported by the remote peer. // If so it setups all the state (SSRC and PayloadType) to have a call func (s *TrackLocalStaticSample) Bind(t TrackLocalContext) (RTPCodecParameters, error) { codec, err := s.rtpTrack.Bind(t) if err != nil { return codec, err } s.rtpTrack.mu.Lock() defer s.rtpTrack.mu.Unlock() // We only need one packetizer if s.packetizer != nil { return codec, nil } payloader, err := payloaderForCodec(codec.RTPCodecCapability) if err != nil { return codec, err } s.sequencer = rtp.NewRandomSequencer() s.packetizer = rtp.NewPacketizer( rtpOutboundMTU, 0, // Value is handled when writing 0, // Value is handled when writing payloader, s.sequencer, codec.ClockRate, ) s.clockRate = float64(codec.RTPCodecCapability.ClockRate) return codec, nil } // Unbind implements the teardown logic when the track is no longer needed. This happens // because a track has been stopped. func (s *TrackLocalStaticSample) Unbind(t TrackLocalContext) error { return s.rtpTrack.Unbind(t) } // WriteSample writes a Sample to the TrackLocalStaticSample // If one PeerConnection fails the packets will still be sent to // all PeerConnections. The error message will contain the ID of the failed // PeerConnections so you can remove them func (s *TrackLocalStaticSample) WriteSample(sample media.Sample) error { s.rtpTrack.mu.RLock() p := s.packetizer clockRate := s.clockRate s.rtpTrack.mu.RUnlock() if p == nil { return nil } // skip packets by the number of previously dropped packets for i := uint16(0); i < sample.PrevDroppedPackets; i++ { s.sequencer.NextSequenceNumber() } samples := uint32(sample.Duration.Seconds() * clockRate) if sample.PrevDroppedPackets > 0 { p.SkipSamples(samples * uint32(sample.PrevDroppedPackets)) } packets := p.Packetize(sample.Data, samples) writeErrs := []error{} for _, p := range packets { if err := s.rtpTrack.WriteRTP(p); err != nil { writeErrs = append(writeErrs, err) } } return util.FlattenErrs(writeErrs) } // GeneratePadding writes padding-only samples to the TrackLocalStaticSample // If one PeerConnection fails the packets will still be sent to // all PeerConnections. The error message will contain the ID of the failed // PeerConnections so you can remove them func (s *TrackLocalStaticSample) GeneratePadding(samples uint32) error { s.rtpTrack.mu.RLock() p := s.packetizer s.rtpTrack.mu.RUnlock() if p == nil { return nil } packets := p.GeneratePadding(samples) writeErrs := []error{} for _, p := range packets { if err := s.rtpTrack.WriteRTP(p); err != nil { writeErrs = append(writeErrs, err) } } return util.FlattenErrs(writeErrs) }