diff --git a/README.md b/README.md index dc84c497..b57b610d 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,12 @@ Go ≥ 1.17 is required. Features: * Client - * Query servers about available streams and tracks + * Query servers about available streams and mediastreams * Read - * Read streams from servers with the UDP, UDP-multicast or TCP transport protocol + * Read media streams from servers with the UDP, UDP-multicast or TCP transport protocol * Read TLS-encrypted streams (TCP only) * Switch transport protocol automatically - * Read only selected tracks of a stream + * Read only selected media streams * Pause or seek without disconnecting from the server * Generate RTCP receiver reports (UDP only) * Reorder incoming RTP packets (UDP only) @@ -33,28 +33,19 @@ Features: * Handle requests from clients * Sessions and connections are independent * Publish - * Read streams from clients with the UDP or TCP transport protocol + * Read media streams from clients with the UDP or TCP transport protocol * Read TLS-encrypted streams (TCP only) * Generate RTCP receiver reports (UDP only) * Reorder incoming RTP packets (UDP only) * Read - * Write streams to clients with the UDP, UDP-multicast or TCP transport protocol + * Write media streams to clients with the UDP, UDP-multicast or TCP transport protocol * Write TLS-encrypted streams * Compute and provide SSRC, RTP-Info to clients * Generate RTCP sender reports * Utilities * Encode and decode codec-specific frames into/from RTP packets. The following codecs are supported: - * Video - * H264 - * H265 - * VP8 - * VP9 - * Audio - * G711 - * G722 - * LPCM - * MPEG4-audio (AAC) - * Opus + * Video: H264, H265, VP8, VP9 + * Audio: G711 (PCMA, PCMU), G722, LPCM, MPEG4-audio (AAC), Opus * Parse RTSP elements: requests, responses, SDP * Parse H264 elements and formats: Annex-B, AVCC, anti-competition, DTS * Parse MPEG4-audio (AAC) element and formats: ADTS, MPEG4-audio configurations @@ -70,31 +61,30 @@ Features: * [client-query](examples/client-query/main.go) * [client-read](examples/client-read/main.go) * [client-read-options](examples/client-read-options/main.go) -* [client-read-partial](examples/client-read-partial/main.go) * [client-read-pause](examples/client-read-pause/main.go) * [client-read-republish](examples/client-read-republish/main.go) -* [client-read-track-g711](examples/client-read-track-g711/main.go) -* [client-read-track-g722](examples/client-read-track-g722/main.go) -* [client-read-track-h264](examples/client-read-track-h264/main.go) -* [client-read-track-h264-convert-to-jpeg](examples/client-read-track-h264-convert-to-jpeg/main.go) -* [client-read-track-h264-save-to-disk](examples/client-read-track-h264-save-to-disk/main.go) -* [client-read-track-h265](examples/client-read-track-h265/main.go) -* [client-read-track-lpcm](examples/client-read-track-lpcm/main.go) -* [client-read-track-mpeg4audio](examples/client-read-track-mpeg4audio/main.go) -* [client-read-track-opus](examples/client-read-track-opus/main.go) -* [client-read-track-vp8](examples/client-read-track-vp8/main.go) -* [client-read-track-vp9](examples/client-read-track-vp9/main.go) +* [client-read-format-g711](examples/client-read-format-g711/main.go) +* [client-read-format-g722](examples/client-read-format-g722/main.go) +* [client-read-format-h264](examples/client-read-format-h264/main.go) +* [client-read-format-h264-convert-to-jpeg](examples/client-read-format-h264-convert-to-jpeg/main.go) +* [client-read-format-h264-save-to-disk](examples/client-read-format-h264-save-to-disk/main.go) +* [client-read-format-h265](examples/client-read-format-h265/main.go) +* [client-read-format-lpcm](examples/client-read-format-lpcm/main.go) +* [client-read-format-mpeg4audio](examples/client-read-format-mpeg4audio/main.go) +* [client-read-format-opus](examples/client-read-format-opus/main.go) +* [client-read-format-vp8](examples/client-read-format-vp8/main.go) +* [client-read-format-vp9](examples/client-read-format-vp9/main.go) * [client-publish-options](examples/client-publish-options/main.go) * [client-publish-pause](examples/client-publish-pause/main.go) -* [client-publish-track-g711](examples/client-publish-track-g711/main.go) -* [client-publish-track-g722](examples/client-publish-track-g722/main.go) -* [client-publish-track-h264](examples/client-publish-track-h264/main.go) -* [client-publish-track-h265](examples/client-publish-track-h265/main.go) -* [client-publish-track-lpcm](examples/client-publish-track-lpcm/main.go) -* [client-publish-track-mpeg4audio](examples/client-publish-track-mpeg4audio/main.go) -* [client-publish-track-opus](examples/client-publish-track-opus/main.go) -* [client-publish-track-vp8](examples/client-publish-track-vp8/main.go) -* [client-publish-track-vp9](examples/client-publish-track-vp9/main.go) +* [client-publish-format-g711](examples/client-publish-format-g711/main.go) +* [client-publish-format-g722](examples/client-publish-format-g722/main.go) +* [client-publish-format-h264](examples/client-publish-format-h264/main.go) +* [client-publish-format-h265](examples/client-publish-format-h265/main.go) +* [client-publish-format-lpcm](examples/client-publish-format-lpcm/main.go) +* [client-publish-format-mpeg4audio](examples/client-publish-format-mpeg4audio/main.go) +* [client-publish-format-opus](examples/client-publish-format-opus/main.go) +* [client-publish-format-vp8](examples/client-publish-format-vp8/main.go) +* [client-publish-format-vp9](examples/client-publish-format-vp9/main.go) * [server](examples/server/main.go) * [server-tls](examples/server-tls/main.go) * [server-h264-save-to-disk](examples/server-h264-save-to-disk/main.go) diff --git a/client.go b/client.go index 1b9514ca..2d7df98b 100644 --- a/client.go +++ b/client.go @@ -20,18 +20,16 @@ import ( "github.com/pion/rtcp" "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/auth" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/bytecounter" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/headers" - "github.com/aler9/gortsplib/pkg/liberrors" - "github.com/aler9/gortsplib/pkg/ringbuffer" - "github.com/aler9/gortsplib/pkg/rtcpreceiver" - "github.com/aler9/gortsplib/pkg/rtcpsender" - "github.com/aler9/gortsplib/pkg/rtpreorderer" - "github.com/aler9/gortsplib/pkg/sdp" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/auth" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/bytecounter" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/liberrors" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/sdp" + "github.com/aler9/gortsplib/v2/pkg/url" ) func isAnyPort(p int) bool { @@ -83,22 +81,6 @@ const ( clientStateRecord ) -type clientTrack struct { - id int - track Track - tcpChannel int - udpRTPListener *clientUDPListener - udpRTCPListener *clientUDPListener - - // play - udpRTPPacketBuffer *rtpPacketMultiBuffer - udpRTCPReceiver *rtcpreceiver.RTCPReceiver - reorderer *rtpreorderer.Reorderer - - // record - rtcpSender *rtcpsender.RTCPSender -} - func (s clientState) String() string { switch s { case clientStateInitial: @@ -127,12 +109,12 @@ type describeReq struct { type announceReq struct { url *url.URL - tracks Tracks + medias media.Medias res chan clientRes } type setupReq struct { - track Track + media *media.Media baseURL *url.URL rtpPort int rtcpPort int @@ -153,24 +135,12 @@ type pauseReq struct { } type clientRes struct { - tracks Tracks + medias media.Medias baseURL *url.URL res *base.Response err error } -// ClientOnPacketRTPCtx is the context of a RTP packet. -type ClientOnPacketRTPCtx struct { - TrackID int - Packet *rtp.Packet -} - -// ClientOnPacketRTCPCtx is the context of a RTCP packet. -type ClientOnPacketRTCPCtx struct { - TrackID int - Packet rtcp.Packet -} - // Client is a RTSP client. type Client struct { // @@ -237,10 +207,6 @@ type Client struct { OnRequest func(*base.Request) // called after every response. OnResponse func(*base.Response) - // called when receiving a RTP packet. - OnPacketRTP func(*ClientOnPacketRTPCtx) - // called when receiving a RTCP packet. - OnPacketRTCP func(*ClientOnPacketRTCPCtx) // called when there's a non-fatal decoding error of RTP or RTCP packets. OnDecodeError func(error) @@ -248,7 +214,7 @@ type Client struct { // private // - udpSenderReportPeriod time.Duration + senderReportPeriod time.Duration udpReceiverReportPeriod time.Duration checkStreamPeriod time.Duration keepalivePeriod time.Duration @@ -268,18 +234,17 @@ type Client struct { lastDescribeURL *url.URL baseURL *url.URL effectiveTransport *Transport - tracks []*clientTrack - tcpTracksByChannel map[int]*clientTrack + medias map[*media.Media]*clientMedia + tcpMediasByChannel map[int]*clientMedia lastRange *headers.Range - writeMutex sync.RWMutex // publish - writeFrameAllowed bool // publish + rtpPacketBuffer *rtpPacketMultiBuffer // play checkStreamTimer *time.Timer checkStreamInitial bool tcpLastFrameTime *int64 keepaliveTimer *time.Timer closeError error - writerRunning bool - writeBuffer *ringbuffer.RingBuffer + writer clientWriter + writeMutex sync.RWMutex // connCloser channels connCloserTerminate chan struct{} @@ -288,9 +253,6 @@ type Client struct { // reader channels readerErr chan error - // writer channels - writerDone chan struct{} - // in options chan optionsReq describe chan describeReq @@ -352,22 +314,14 @@ func (c *Client) Start(scheme string, host string) error { c.OnResponse = func(*base.Response) { } } - if c.OnPacketRTP == nil { - c.OnPacketRTP = func(*ClientOnPacketRTPCtx) { - } - } - if c.OnPacketRTCP == nil { - c.OnPacketRTCP = func(*ClientOnPacketRTCPCtx) { - } - } if c.OnDecodeError == nil { c.OnDecodeError = func(error) { } } // private - if c.udpSenderReportPeriod == 0 { - c.udpSenderReportPeriod = 10 * time.Second + if c.senderReportPeriod == 0 { + c.senderReportPeriod = 10 * time.Second } if c.udpReceiverReportPeriod == 0 { c.udpReceiverReportPeriod = 10 * time.Second @@ -401,8 +355,8 @@ func (c *Client) Start(scheme string, host string) error { return nil } -// StartPublishing connects to the address and starts publishing the tracks. -func (c *Client) StartPublishing(address string, tracks Tracks) error { +// StartRecording connects to the address and starts publishing given media. +func (c *Client) StartRecording(address string, medias media.Medias) error { u, err := url.Parse(address) if err != nil { return err @@ -413,17 +367,13 @@ func (c *Client) StartPublishing(address string, tracks Tracks) error { return err } - // control attribute of tracks is overridden by Announce(). - // use a copy in order not to mess the client-read-republish example. - tracks = tracks.clone() - - _, err = c.Announce(u, tracks) + _, err = c.Announce(u, medias) if err != nil { c.Close() return err } - err = c.SetupAll(tracks, u) + err = c.SetupAll(medias, u) if err != nil { c.Close() return err @@ -452,15 +402,6 @@ func (c *Client) Wait() error { return c.closeError } -// Tracks returns all the tracks that the client is reading or publishing. -func (c *Client) Tracks() Tracks { - ret := make(Tracks, len(c.tracks)) - for i, track := range c.tracks { - ret[i] = track.track - } - return ret -} - func (c *Client) run() { defer close(c.done) @@ -479,15 +420,15 @@ func (c *Client) runInner() error { req.res <- clientRes{res: res, err: err} case req := <-c.describe: - tracks, baseURL, res, err := c.doDescribe(req.url) - req.res <- clientRes{tracks: tracks, baseURL: baseURL, res: res, err: err} + medias, baseURL, res, err := c.doDescribe(req.url) + req.res <- clientRes{medias: medias, baseURL: baseURL, res: res, err: err} case req := <-c.announce: - res, err := c.doAnnounce(req.url, req.tracks) + res, err := c.doAnnounce(req.url, req.medias) req.res <- clientRes{res: res, err: err} case req := <-c.setup: - res, err := c.doSetup(req.track, req.baseURL, req.rtpPort, req.rtcpPort) + res, err := c.doSetup(req.media, req.baseURL, req.rtpPort, req.rtcpPort) req.res <- clientRes{res: res, err: err} case req := <-c.play: @@ -510,7 +451,7 @@ func (c *Client) runInner() error { // check that at least one packet has been received inTimeout := func() bool { - for _, ct := range c.tracks { + for _, ct := range c.medias { lft := atomic.LoadInt64(ct.udpRTPListener.lastPacketTime) if lft != 0 { return false @@ -532,7 +473,7 @@ func (c *Client) runInner() error { } else { inTimeout := func() bool { now := time.Now() - for _, ct := range c.tracks { + for _, ct := range c.medias { lft := time.Unix(atomic.LoadInt64(ct.udpRTPListener.lastPacketTime), 0) if now.Sub(lft) < c.ReadTimeout { return false @@ -609,11 +550,8 @@ func (c *Client) doClose() { c.conn = nil } - for _, track := range c.tracks { - if track.udpRTPListener != nil { - track.udpRTPListener.close() - track.udpRTCPListener.close() - } + for _, cm := range c.medias { + cm.close() } } @@ -628,8 +566,8 @@ func (c *Client) reset() { c.useGetParameter = false c.baseURL = nil c.effectiveTransport = nil - c.tracks = nil - c.tcpTracksByChannel = nil + c.medias = nil + c.tcpMediasByChannel = nil } func (c *Client) checkState(allowed map[clientState]struct{}) error { @@ -651,14 +589,14 @@ func (c *Client) trySwitchingProtocol() error { prevScheme := c.scheme prevHost := c.host prevBaseURL := c.baseURL - oldUseGetParameter := c.useGetParameter - prevTracks := c.tracks + prevUseGetParameter := c.useGetParameter + prevMedias := c.medias c.reset() v := TransportTCP c.effectiveTransport = &v - c.useGetParameter = oldUseGetParameter + c.useGetParameter = prevUseGetParameter c.scheme = prevScheme c.host = prevHost @@ -668,11 +606,16 @@ func (c *Client) trySwitchingProtocol() error { return err } - for _, track := range prevTracks { - _, err := c.doSetup(track.track, prevBaseURL, 0, 0) + for i, cm := range prevMedias { + _, err := c.doSetup(cm.media, prevBaseURL, 0, 0) if err != nil { return err } + + c.medias[i].onPacketRTCP = cm.onPacketRTCP + for j, tr := range cm.formats { + c.medias[i].formats[j].onPacketRTP = tr.onPacketRTP + } } _, err = c.doPlay(c.lastRange, true) @@ -687,93 +630,33 @@ func (c *Client) playRecordStart() { // stop connCloser c.connCloserStop() - // start writer if c.state == clientStatePlay { - // when reading, writeBuffer is only used to send RTCP receiver reports, - // that are much smaller than RTP packets and are sent at a fixed interval. - // decrease RAM consumption by allocating less buffers. - c.writeBuffer, _ = ringbuffer.New(8) - } else { - c.writeBuffer, _ = ringbuffer.New(uint64(c.WriteBufferCount)) - } - c.writerRunning = true - c.writerDone = make(chan struct{}) - go c.runWriter() - - // allow writing - c.writeMutex.Lock() - c.writeFrameAllowed = true - c.writeMutex.Unlock() - - if c.state == clientStatePlay { - for _, ct := range c.tracks { - if *c.effectiveTransport == TransportUDP || *c.effectiveTransport == TransportUDPMulticast { - ct.reorderer = rtpreorderer.New() - } - } - c.keepaliveTimer = time.NewTimer(c.keepalivePeriod) + c.rtpPacketBuffer = newRTPPacketMultiBuffer(uint64(c.ReadBufferCount)) switch *c.effectiveTransport { case TransportUDP: - for trackID, ct := range c.tracks { - ctrackID := trackID - ct.udpRTPPacketBuffer = newRTPPacketMultiBuffer(uint64(c.ReadBufferCount)) - ct.udpRTCPReceiver = rtcpreceiver.New(c.udpReceiverReportPeriod, nil, - ct.track.ClockRate(), func(pkt rtcp.Packet) { - c.WritePacketRTCP(ctrackID, pkt) - }) - } - c.checkStreamTimer = time.NewTimer(c.InitialUDPReadTimeout) c.checkStreamInitial = true - for _, ct := range c.tracks { - ct.udpRTPListener.start(true) - ct.udpRTCPListener.start(true) - } - case TransportUDPMulticast: - for trackID, ct := range c.tracks { - ctrackID := trackID - ct.udpRTPPacketBuffer = newRTPPacketMultiBuffer(uint64(c.ReadBufferCount)) - ct.udpRTCPReceiver = rtcpreceiver.New(c.udpReceiverReportPeriod, nil, - ct.track.ClockRate(), func(pkt rtcp.Packet) { - c.WritePacketRTCP(ctrackID, pkt) - }) - } - c.checkStreamTimer = time.NewTimer(c.checkStreamPeriod) - for _, ct := range c.tracks { - ct.udpRTPListener.start(true) - ct.udpRTCPListener.start(true) - } - default: // TCP c.checkStreamTimer = time.NewTimer(c.checkStreamPeriod) v := time.Now().Unix() c.tcpLastFrameTime = &v } - } else { - if !c.DisableRTCPSenderReports { - for trackID, ct := range c.tracks { - ctrackID := trackID - ct.rtcpSender = rtcpsender.New(c.udpSenderReportPeriod, - ct.track.ClockRate(), func(pkt rtcp.Packet) { - c.WritePacketRTCP(ctrackID, pkt) - }) - } - } - - if *c.effectiveTransport == TransportUDP { - for _, ct := range c.tracks { - ct.udpRTPListener.start(false) - ct.udpRTCPListener.start(false) - } - } } + for _, cm := range c.medias { + cm.start() + } + + c.writeMutex.Lock() + c.writer.start(c) + c.writeMutex.Unlock() + // for some reason, SetReadDeadline() must always be called in the same // goroutine, otherwise Read() freezes. // therefore, we disable the deadline and perform a check with a ticker. @@ -794,76 +677,6 @@ func (c *Client) runReader() { } } } else { - var processFunc func(*clientTrack, bool, []byte) error - - if c.state == clientStatePlay { - tcpRTPPacketBuffer := newRTPPacketMultiBuffer(uint64(c.ReadBufferCount)) - - processFunc = func(track *clientTrack, isRTP bool, payload []byte) error { - now := time.Now() - atomic.StoreInt64(c.tcpLastFrameTime, now.Unix()) - - if isRTP { - pkt := tcpRTPPacketBuffer.next() - err := pkt.Unmarshal(payload) - if err != nil { - return err - } - - c.OnPacketRTP(&ClientOnPacketRTPCtx{ - TrackID: track.id, - Packet: pkt, - }) - } else { - if len(payload) > maxPacketSize { - c.OnDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", - len(payload), maxPacketSize)) - return nil - } - - packets, err := rtcp.Unmarshal(payload) - if err != nil { - c.OnDecodeError(err) - return nil - } - - for _, pkt := range packets { - c.OnPacketRTCP(&ClientOnPacketRTCPCtx{ - TrackID: track.id, - Packet: pkt, - }) - } - } - - return nil - } - } else { - processFunc = func(track *clientTrack, isRTP bool, payload []byte) error { - if !isRTP { - if len(payload) > maxPacketSize { - c.OnDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", - len(payload), maxPacketSize)) - return nil - } - - packets, err := rtcp.Unmarshal(payload) - if err != nil { - c.OnDecodeError(err) - return nil - } - - for _, pkt := range packets { - c.OnPacketRTCP(&ClientOnPacketRTCPCtx{ - TrackID: track.id, - Packet: pkt, - }) - } - } - - return nil - } - } - for { what, err := c.conn.ReadInterleavedFrameOrResponse() if err != nil { @@ -878,12 +691,16 @@ func (c *Client) runReader() { isRTP = false } - track, ok := c.tcpTracksByChannel[channel] + media, ok := c.tcpMediasByChannel[channel] if !ok { continue } - err := processFunc(track, isRTP, fr.Payload) + if isRTP { + err = media.readRTP(fr.Payload) + } else { + err = media.readRTCP(fr.Payload) + } if err != nil { return err } @@ -900,44 +717,17 @@ func (c *Client) playRecordStop(isClosing bool) { <-c.readerErr } - // forbid writing - c.writeMutex.Lock() - c.writeFrameAllowed = false - c.writeMutex.Unlock() - - if *c.effectiveTransport == TransportUDP || - *c.effectiveTransport == TransportUDPMulticast { - for _, ct := range c.tracks { - ct.udpRTPListener.stop() - ct.udpRTCPListener.stop() - } - - if c.state == clientStatePlay { - for _, ct := range c.tracks { - ct.udpRTPPacketBuffer = nil - ct.udpRTCPReceiver.Close() - ct.udpRTCPReceiver = nil - } - } - } - - for _, ct := range c.tracks { - if ct.rtcpSender != nil { - ct.rtcpSender.Close() - ct.rtcpSender = nil - } - ct.reorderer = nil - } - // stop timers c.checkStreamTimer = emptyTimer() c.keepaliveTimer = emptyTimer() - // stop writer - c.writeBuffer.Close() - <-c.writerDone - c.writerRunning = false - c.writeBuffer = nil + c.writeMutex.Lock() + c.writer.stop() + c.writeMutex.Unlock() + + for _, cm := range c.medias { + cm.stop() + } // start connCloser if !isClosing { @@ -1166,7 +956,7 @@ func (c *Client) Options(u *url.URL) (*base.Response, error) { } } -func (c *Client) doDescribe(u *url.URL) (Tracks, *url.URL, *base.Response, error) { +func (c *Client) doDescribe(u *url.URL) (media.Medias, *url.URL, *base.Response, error) { err := c.checkState(map[clientState]struct{}{ clientStateInitial: {}, clientStatePrePlay: {}, @@ -1225,36 +1015,42 @@ func (c *Client) doDescribe(u *url.URL) (Tracks, *url.URL, *base.Response, error return nil, nil, nil, liberrors.ErrClientContentTypeUnsupported{CT: ct} } - var tracks Tracks - sd, err := tracks.Unmarshal(res.Body) + var sd sdp.SessionDescription + err = sd.Unmarshal(res.Body) if err != nil { return nil, nil, nil, err } - baseURL, err := findBaseURL(sd, res, u) + var medias media.Medias + err = medias.Unmarshal(sd.MediaDescriptions) + if err != nil { + return nil, nil, nil, err + } + + baseURL, err := findBaseURL(&sd, res, u) if err != nil { return nil, nil, nil, err } c.lastDescribeURL = u - return tracks, baseURL, res, nil + return medias, baseURL, res, nil } // Describe writes a DESCRIBE request and reads a Response. -func (c *Client) Describe(u *url.URL) (Tracks, *url.URL, *base.Response, error) { +func (c *Client) Describe(u *url.URL) (media.Medias, *url.URL, *base.Response, error) { cres := make(chan clientRes) select { case c.describe <- describeReq{url: u, res: cres}: res := <-cres - return res.tracks, res.baseURL, res.res, res.err + return res.medias, res.baseURL, res.res, res.err case <-c.ctx.Done(): return nil, nil, nil, liberrors.ErrClientTerminated{} } } -func (c *Client) doAnnounce(u *url.URL, tracks Tracks) (*base.Response, error) { +func (c *Client) doAnnounce(u *url.URL, medias media.Medias) (*base.Response, error) { err := c.checkState(map[clientState]struct{}{ clientStateInitial: {}, }) @@ -1262,7 +1058,12 @@ func (c *Client) doAnnounce(u *url.URL, tracks Tracks) (*base.Response, error) { return nil, err } - tracks.setControls() + medias.SetControls() + + byts, err := medias.Marshal(false).Marshal() + if err != nil { + return nil, err + } res, err := c.do(&base.Request{ Method: base.Announce, @@ -1270,7 +1071,7 @@ func (c *Client) doAnnounce(u *url.URL, tracks Tracks) (*base.Response, error) { Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: byts, }, false, false) if err != nil { return nil, err @@ -1289,10 +1090,10 @@ func (c *Client) doAnnounce(u *url.URL, tracks Tracks) (*base.Response, error) { } // Announce writes an ANNOUNCE request and reads a Response. -func (c *Client) Announce(u *url.URL, tracks Tracks) (*base.Response, error) { +func (c *Client) Announce(u *url.URL, medias media.Medias) (*base.Response, error) { cres := make(chan clientRes) select { - case c.announce <- announceReq{url: u, tracks: tracks, res: cres}: + case c.announce <- announceReq{url: u, medias: medias, res: cres}: res := <-cres return res.res, res.err @@ -1302,7 +1103,7 @@ func (c *Client) Announce(u *url.URL, tracks Tracks) (*base.Response, error) { } func (c *Client) doSetup( - track Track, + medi *media.Media, baseURL *url.URL, rtpPort int, rtcpPort int, @@ -1317,7 +1118,7 @@ func (c *Client) doSetup( } if c.baseURL != nil && *baseURL != *c.baseURL { - return nil, liberrors.ErrClientCannotSetupTracksDifferentURLs{} + return nil, liberrors.ErrClientCannotSetupMediasDifferentURLs{} } // always use TCP if encrypted @@ -1350,11 +1151,7 @@ func (c *Client) doSetup( Mode: &mode, } - trackID := len(c.tracks) - - ct := &clientTrack{ - track: track, - } + cm := newClientMedia(c) switch transport { case TransportUDP: @@ -1367,39 +1164,19 @@ func (c *Client) doSetup( return nil, liberrors.ErrClientUDPPortsNotConsecutive{} } - if rtpPort != 0 { - var err error - ct.udpRTPListener, err = newClientUDPListener( - c, - false, - ":"+strconv.FormatInt(int64(rtpPort), 10), - ct, - true) - if err != nil { - return nil, err - } - - ct.udpRTCPListener, err = newClientUDPListener( - c, - false, - ":"+strconv.FormatInt(int64(rtcpPort), 10), - ct, - true) - if err != nil { - ct.udpRTPListener.close() - return nil, err - } - } else { - ct.udpRTPListener, ct.udpRTCPListener = newClientUDPListenerPair(c, ct) + err := cm.allocateUDPListeners( + false, + ":"+strconv.FormatInt(int64(rtpPort), 10), + ":"+strconv.FormatInt(int64(rtcpPort), 10), + ) + if err != nil { + return nil, err } v1 := headers.TransportDeliveryUnicast th.Delivery = &v1 th.Protocol = headers.TransportProtocolUDP - th.ClientPorts = &[2]int{ - ct.udpRTPListener.port(), - ct.udpRTCPListener.port(), - } + th.ClientPorts = &[2]int{cm.udpRTPListener.port(), cm.udpRTCPListener.port()} case TransportUDPMulticast: v1 := headers.TransportDeliveryMulticast @@ -1410,38 +1187,30 @@ func (c *Client) doSetup( v1 := headers.TransportDeliveryUnicast th.Delivery = &v1 th.Protocol = headers.TransportProtocolTCP - th.InterleavedIDs = &[2]int{(trackID * 2), (trackID * 2) + 1} + mediaCount := len(c.medias) + th.InterleavedIDs = &[2]int{(mediaCount * 2), (mediaCount * 2) + 1} } - trackURL, err := track.url(baseURL) + mediaURL, err := medi.URL(baseURL) if err != nil { - if transport == TransportUDP { - ct.udpRTPListener.close() - ct.udpRTCPListener.close() - } + cm.close() return nil, err } res, err := c.do(&base.Request{ Method: base.Setup, - URL: trackURL, + URL: mediaURL, Header: base.Header{ "Transport": th.Marshal(), }, }, false, false) if err != nil { - if transport == TransportUDP { - ct.udpRTPListener.close() - ct.udpRTCPListener.close() - } + cm.close() return nil, err } if res.StatusCode != base.StatusOK { - if transport == TransportUDP { - ct.udpRTPListener.close() - ct.udpRTCPListener.close() - } + cm.close() // switch transport automatically if res.StatusCode == base.StatusUnsupportedTransport && @@ -1450,7 +1219,7 @@ func (c *Client) doSetup( v := TransportTCP c.effectiveTransport = &v - return c.doSetup(track, baseURL, 0, 0) + return c.doSetup(medi, baseURL, 0, 0) } return nil, liberrors.ErrClientBadStatusCode{Code: res.StatusCode, Message: res.StatusMessage} @@ -1459,51 +1228,48 @@ func (c *Client) doSetup( var thRes headers.Transport err = thRes.Unmarshal(res.Header["Transport"]) if err != nil { - if transport == TransportUDP { - ct.udpRTPListener.close() - ct.udpRTCPListener.close() - } + cm.close() return nil, liberrors.ErrClientTransportHeaderInvalid{Err: err} } switch transport { case TransportUDP: if thRes.Delivery != nil && *thRes.Delivery != headers.TransportDeliveryUnicast { + cm.close() return nil, liberrors.ErrClientTransportHeaderInvalidDelivery{} } if c.state == clientStatePreRecord || !c.AnyPortEnable { if thRes.ServerPorts == nil || isAnyPort(thRes.ServerPorts[0]) || isAnyPort(thRes.ServerPorts[1]) { - ct.udpRTPListener.close() - ct.udpRTCPListener.close() + cm.close() return nil, liberrors.ErrClientServerPortsNotProvided{} } } - ct.udpRTPListener.readIP = func() net.IP { + cm.udpRTPListener.readIP = func() net.IP { if thRes.Source != nil { return *thRes.Source } return c.nconn.RemoteAddr().(*net.TCPAddr).IP }() if thRes.ServerPorts != nil { - ct.udpRTPListener.readPort = thRes.ServerPorts[0] - ct.udpRTPListener.writeAddr = &net.UDPAddr{ + cm.udpRTPListener.readPort = thRes.ServerPorts[0] + cm.udpRTPListener.writeAddr = &net.UDPAddr{ IP: c.nconn.RemoteAddr().(*net.TCPAddr).IP, Zone: c.nconn.RemoteAddr().(*net.TCPAddr).Zone, Port: thRes.ServerPorts[0], } } - ct.udpRTCPListener.readIP = func() net.IP { + cm.udpRTCPListener.readIP = func() net.IP { if thRes.Source != nil { return *thRes.Source } return c.nconn.RemoteAddr().(*net.TCPAddr).IP }() if thRes.ServerPorts != nil { - ct.udpRTCPListener.readPort = thRes.ServerPorts[1] - ct.udpRTCPListener.writeAddr = &net.UDPAddr{ + cm.udpRTCPListener.readPort = thRes.ServerPorts[1] + cm.udpRTCPListener.writeAddr = &net.UDPAddr{ IP: c.nconn.RemoteAddr().(*net.TCPAddr).IP, Zone: c.nconn.RemoteAddr().(*net.TCPAddr).Zone, Port: thRes.ServerPorts[1], @@ -1523,37 +1289,25 @@ func (c *Client) doSetup( return nil, liberrors.ErrClientTransportHeaderNoDestination{} } - ct.udpRTPListener, err = newClientUDPListener( - c, + err := cm.allocateUDPListeners( true, thRes.Destination.String()+":"+strconv.FormatInt(int64(thRes.Ports[0]), 10), - ct, - true) - if err != nil { - return nil, err - } - - ct.udpRTCPListener, err = newClientUDPListener( - c, - true, thRes.Destination.String()+":"+strconv.FormatInt(int64(thRes.Ports[1]), 10), - ct, - false) + ) if err != nil { - ct.udpRTPListener.close() return nil, err } - ct.udpRTPListener.readIP = c.nconn.RemoteAddr().(*net.TCPAddr).IP - ct.udpRTPListener.readPort = thRes.Ports[0] - ct.udpRTPListener.writeAddr = &net.UDPAddr{ + cm.udpRTPListener.readIP = c.nconn.RemoteAddr().(*net.TCPAddr).IP + cm.udpRTPListener.readPort = thRes.Ports[0] + cm.udpRTPListener.writeAddr = &net.UDPAddr{ IP: *thRes.Destination, Port: thRes.Ports[0], } - ct.udpRTCPListener.readIP = c.nconn.RemoteAddr().(*net.TCPAddr).IP - ct.udpRTCPListener.readPort = thRes.Ports[1] - ct.udpRTCPListener.writeAddr = &net.UDPAddr{ + cm.udpRTCPListener.readIP = c.nconn.RemoteAddr().(*net.TCPAddr).IP + cm.udpRTCPListener.readPort = thRes.Ports[1] + cm.udpRTCPListener.writeAddr = &net.UDPAddr{ IP: *thRes.Destination, Port: thRes.Ports[1], } @@ -1572,22 +1326,26 @@ func (c *Client) doSetup( return nil, liberrors.ErrClientTransportHeaderInvalidInterleavedIDs{} } - if _, ok := c.tcpTracksByChannel[thRes.InterleavedIDs[0]]; ok { + if _, ok := c.tcpMediasByChannel[thRes.InterleavedIDs[0]]; ok { return &base.Response{ StatusCode: base.StatusBadRequest, }, liberrors.ErrClientTransportHeaderInterleavedIDsAlreadyUsed{} } - if c.tcpTracksByChannel == nil { - c.tcpTracksByChannel = make(map[int]*clientTrack) + if c.tcpMediasByChannel == nil { + c.tcpMediasByChannel = make(map[int]*clientMedia) } - c.tcpTracksByChannel[thRes.InterleavedIDs[0]] = ct - ct.tcpChannel = thRes.InterleavedIDs[0] + c.tcpMediasByChannel[thRes.InterleavedIDs[0]] = cm + cm.tcpChannel = thRes.InterleavedIDs[0] } - c.tracks = append(c.tracks, ct) - ct.id = trackID + if c.medias == nil { + c.medias = make(map[*media.Media]*clientMedia) + } + + c.medias[medi] = cm + cm.setMedia(medi) c.baseURL = baseURL c.effectiveTransport = &transport @@ -1605,7 +1363,7 @@ func (c *Client) doSetup( // rtpPort and rtcpPort are used only if transport is UDP. // if rtpPort and rtcpPort are zero, they are chosen automatically. func (c *Client) Setup( - track Track, + media *media.Media, baseURL *url.URL, rtpPort int, rtcpPort int, @@ -1613,7 +1371,7 @@ func (c *Client) Setup( cres := make(chan clientRes) select { case c.setup <- setupReq{ - track: track, + media: media, baseURL: baseURL, rtpPort: rtpPort, rtcpPort: rtcpPort, @@ -1627,10 +1385,10 @@ func (c *Client) Setup( } } -// SetupAll setups all the given tracks. -func (c *Client) SetupAll(tracks Tracks, baseURL *url.URL) error { - for _, t := range tracks { - _, err := c.Setup(t, baseURL, 0, 0) +// SetupAll setups all the given medias. +func (c *Client) SetupAll(medias media.Medias, baseURL *url.URL) error { + for _, m := range medias { + _, err := c.Setup(m, baseURL, 0, 0) if err != nil { return err } @@ -1651,7 +1409,7 @@ func (c *Client) doPlay(ra *headers.Range, isSwitchingProtocol bool) (*base.Resp // don't do this with multicast, otherwise the RTP packet is going to be broadcasted // to all listeners, including us, messing up the stream. if *c.effectiveTransport == TransportUDP { - for _, ct := range c.tracks { + for _, ct := range c.medias { byts, _ := (&rtp.Packet{Header: rtp.Header{Version: 2}}).Marshal() ct.udpRTPListener.write(byts) @@ -1707,17 +1465,6 @@ func (c *Client) Play(ra *headers.Range) (*base.Response, error) { } } -// SetupAndPlay setups and play the given tracks. -func (c *Client) SetupAndPlay(tracks Tracks, baseURL *url.URL) error { - err := c.SetupAll(tracks, baseURL) - if err != nil { - return err - } - - _, err = c.Play(nil) - return err -} - func (c *Client) doRecord() (*base.Response, error) { err := c.checkState(map[clientState]struct{}{ clientStatePreRecord: {}, @@ -1820,126 +1567,55 @@ func (c *Client) Seek(ra *headers.Range) (*base.Response, error) { return c.Play(ra) } -func (c *Client) runWriter() { - defer close(c.writerDone) - - var writeFunc func(int, bool, []byte) - - switch *c.effectiveTransport { - case TransportUDP, TransportUDPMulticast: - writeFunc = func(trackID int, isRTP bool, payload []byte) { - if isRTP { - c.tracks[trackID].udpRTPListener.write(payload) - } else { - c.tracks[trackID].udpRTCPListener.write(payload) - } +// OnPacketRTPAny sets the callback that is called when a RTP packet is read from any setupped media. +func (c *Client) OnPacketRTPAny(cb func(*media.Media, format.Format, *rtp.Packet)) { + for _, cm := range c.medias { + cmedia := cm.media + for _, trak := range cm.media.Formats { + c.OnPacketRTP(cm.media, trak, func(pkt *rtp.Packet) { + cb(cmedia, trak, pkt) + }) } - - default: // TCP - rtpFrames := make(map[int]*base.InterleavedFrame, len(c.tracks)) - rtcpFrames := make(map[int]*base.InterleavedFrame, len(c.tracks)) - - for trackID, ct := range c.tracks { - rtpFrames[trackID] = &base.InterleavedFrame{Channel: ct.tcpChannel} - rtcpFrames[trackID] = &base.InterleavedFrame{Channel: ct.tcpChannel + 1} - } - - buf := make([]byte, maxPacketSize+4) - - writeFunc = func(trackID int, isRTP bool, payload []byte) { - if isRTP { - fr := rtpFrames[trackID] - fr.Payload = payload - - c.nconn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) - c.conn.WriteInterleavedFrame(fr, buf) - } else { - fr := rtcpFrames[trackID] - fr.Payload = payload - - c.nconn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) - c.conn.WriteInterleavedFrame(fr, buf) - } - } - } - - for { - tmp, ok := c.writeBuffer.Pull() - if !ok { - return - } - data := tmp.(trackTypePayload) - - atomic.AddUint64(c.BytesSent, uint64(len(data.payload))) - - writeFunc(data.trackID, data.isRTP, data.payload) } } -// WritePacketRTP writes a RTP packet to all the readers of the stream. -func (c *Client) WritePacketRTP(trackID int, pkt *rtp.Packet) error { - return c.WritePacketRTPWithNTP(trackID, pkt, time.Now()) +// OnPacketRTCPAny sets the callback that is called when a RTCP packet is read from any setupped media. +func (c *Client) OnPacketRTCPAny(cb func(*media.Media, rtcp.Packet)) { + for _, cm := range c.medias { + cmedia := cm.media + c.OnPacketRTCP(cm.media, func(pkt rtcp.Packet) { + cb(cmedia, pkt) + }) + } } -// WritePacketRTPWithNTP writes a RTP packet. -// ntp is the absolute time of the packet, and is needed to generate RTCP sender reports -// that allows the receiver to reconstruct the absolute time of the packet. -func (c *Client) WritePacketRTPWithNTP(trackID int, pkt *rtp.Packet, ntp time.Time) error { - c.writeMutex.RLock() - defer c.writeMutex.RUnlock() - - if !c.writeFrameAllowed { - select { - case <-c.done: - return c.closeError - default: - return nil - } - } - - byts := make([]byte, maxPacketSize) - n, err := pkt.MarshalTo(byts) - if err != nil { - return err - } - byts = byts[:n] - - t := c.tracks[trackID] - if t.rtcpSender != nil { - t.rtcpSender.ProcessPacketRTP(ntp, pkt, ptsEqualsDTS(c.tracks[trackID].track, pkt)) - } - - c.writeBuffer.Push(trackTypePayload{ - trackID: trackID, - isRTP: true, - payload: byts, - }) - return nil +// OnPacketRTP sets the callback that is called when a RTP packet is read. +func (c *Client) OnPacketRTP(medi *media.Media, trak format.Format, cb func(*rtp.Packet)) { + cm := c.medias[medi] + ct := cm.formats[trak.PayloadType()] + ct.onPacketRTP = cb } -// WritePacketRTCP writes a RTCP packet. -func (c *Client) WritePacketRTCP(trackID int, pkt rtcp.Packet) error { - c.writeMutex.RLock() - defer c.writeMutex.RUnlock() - - if !c.writeFrameAllowed { - select { - case <-c.done: - return c.closeError - default: - return nil - } - } - - byts, err := pkt.Marshal() - if err != nil { - return err - } - - c.writeBuffer.Push(trackTypePayload{ - trackID: trackID, - isRTP: false, - payload: byts, - }) - return nil +// OnPacketRTCP sets the callback that is called when a RTCP packet is read. +func (c *Client) OnPacketRTCP(medi *media.Media, cb func(rtcp.Packet)) { + cm := c.medias[medi] + cm.onPacketRTCP = cb +} + +// WritePacketRTP writes a RTP packet to the media stream. +func (c *Client) WritePacketRTP(medi *media.Media, pkt *rtp.Packet) error { + return c.WritePacketRTPWithNTP(medi, pkt, time.Now()) +} + +// WritePacketRTPWithNTP writes a RTP packet to the media stream. +func (c *Client) WritePacketRTPWithNTP(medi *media.Media, pkt *rtp.Packet, ntp time.Time) error { + cm := c.medias[medi] + ct := cm.formats[pkt.PayloadType] + return ct.writePacketRTPWithNTP(pkt, ntp) +} + +// WritePacketRTCP writes a RTCP packet to the media stream. +func (c *Client) WritePacketRTCP(medi *media.Media, pkt rtcp.Packet) error { + cm := c.medias[medi] + return cm.writePacketRTCP(pkt) } diff --git a/client_read_test.go b/client_play_test.go similarity index 87% rename from client_read_test.go rename to client_play_test.go index 5bedd862..3aee5908 100644 --- a/client_read_test.go +++ b/client_play_test.go @@ -15,15 +15,26 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/net/ipv4" - "github.com/aler9/gortsplib/pkg/auth" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/headers" - "github.com/aler9/gortsplib/pkg/mpeg4audio" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/auth" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" + "github.com/aler9/gortsplib/v2/pkg/sdp" + "github.com/aler9/gortsplib/v2/pkg/url" ) -func startReading(c *Client, ur string) error { +func mustMarshalSDP(sdp *sdp.SessionDescription) []byte { + byts, err := sdp.Marshal() + if err != nil { + panic(err) + } + return byts +} + +func readAll(c *Client, ur string, cb func(*media.Media, format.Format, *rtp.Packet)) error { u, err := url.Parse(ur) if err != nil { return err @@ -34,13 +45,23 @@ func startReading(c *Client, ur string) error { return err } - tracks, baseURL, _, err := c.Describe(u) + medias, baseURL, _, err := c.Describe(u) if err != nil { c.Close() return err } - err = c.SetupAndPlay(tracks, baseURL) + err = c.SetupAll(medias, baseURL) + if err != nil { + c.Close() + return err + } + + if cb != nil { + c.OnPacketRTPAny(cb) + } + + _, err = c.Play(nil) if err != nil { c.Close() return err @@ -49,36 +70,37 @@ func startReading(c *Client, ur string) error { return nil } -func TestClientReadTracks(t *testing.T) { - track1 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, +func TestClientPlayFormats(t *testing.T) { + media1 := testH264Media.Clone() + + media2 := &media.Media{ + Type: media.TypeAudio, + Formats: []format.Format{&format.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }}, } - track2 := &TrackMPEG4Audio{ - PayloadType: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } - - track3 := &TrackMPEG4Audio{ - PayloadType: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 96000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, + media3 := &media.Media{ + Type: media.TypeAudio, + Formats: []format.Format{&format.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 96000, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }}, } l, err := net.Listen("tcp", "localhost:8554") @@ -116,8 +138,8 @@ func TestClientReadTracks(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - tracks := Tracks{track1, track2, track3} - tracks.setControls() + medias := media.Medias{media1, media2, media3} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -125,7 +147,7 @@ func TestClientReadTracks(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -133,7 +155,7 @@ func TestClientReadTracks(t *testing.T) { req, err := conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL(fmt.Sprintf("rtsp://localhost:8554/teststream/trackID=%d", i)), req.URL) + require.Equal(t, mustParseURL(fmt.Sprintf("rtsp://localhost:8554/teststream/mediaID=%d", i)), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -181,14 +203,12 @@ func TestClientReadTracks(t *testing.T) { c := Client{} - err = startReading(&c, "rtsp://localhost:8554/teststream") + err = readAll(&c, "rtsp://localhost:8554/teststream", nil) require.NoError(t, err) defer c.Close() - - require.Equal(t, Tracks{track1, track2, track3}, c.Tracks()) } -func TestClientRead(t *testing.T) { +func TestClientPlay(t *testing.T) { for _, transport := range []string{ "udp", "multicast", @@ -247,18 +267,18 @@ func TestClientRead(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL) - track := &TrackGeneric{ - Media: "application", - Payloads: []TrackGenericPayload{{ - Type: 97, - RTPMap: "private/90000", - }}, + trak := &format.Generic{ + PayloadTyp: 96, + RTPMap: "private/90000", } - err = track.Init() + err = trak.Init() require.NoError(t, err) - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{{ + Type: "application", + Formats: []format.Format{trak}, + }} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -266,14 +286,14 @@ func TestClientRead(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/test/stream?param=value/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value/trackID=0"), req.URL) + require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value/mediaID=0"), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -444,15 +464,13 @@ func TestClientRead(t *testing.T) { }(), } - c.OnPacketRTP = func(ctx *ClientOnPacketRTPCtx) { - require.Equal(t, 0, ctx.TrackID) - require.Equal(t, &testRTPPacket, ctx.Packet) - - err := c.WritePacketRTCP(0, &testRTCPPacket) - require.NoError(t, err) - } - - err = startReading(&c, scheme+"://"+listenIP+":8554/test/stream?param=value") + err = readAll(&c, + scheme+"://"+listenIP+":8554/test/stream?param=value", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + require.Equal(t, &testRTPPacket, pkt) + err := c.WritePacketRTCP(medi, &testRTCPPacket) + require.NoError(t, err) + }) require.NoError(t, err) defer c.Close() @@ -461,7 +479,7 @@ func TestClientRead(t *testing.T) { } } -func TestClientReadPartial(t *testing.T) { +func TestClientPlayPartial(t *testing.T) { listenIP := multicastCapableIP(t) l, err := net.Listen("tcp", listenIP+":8554") require.NoError(t, err) @@ -498,22 +516,11 @@ func TestClientReadPartial(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://"+listenIP+":8554/teststream"), req.URL) - track1 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, + medias := media.Medias{ + testH264Media.Clone(), + testH264Media.Clone(), } - - track2 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track1, track2} - tracks.setControls() + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -521,14 +528,14 @@ func TestClientReadPartial(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://" + listenIP + ":8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL("rtsp://"+listenIP+":8554/teststream/trackID=1"), req.URL) + require.Equal(t, mustParseURL("rtsp://"+listenIP+":8554/teststream/mediaID=1"), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -586,11 +593,6 @@ func TestClientReadPartial(t *testing.T) { v := TransportTCP return &v }(), - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { - require.Equal(t, 0, ctx.TrackID) - require.Equal(t, &testRTPPacket, ctx.Packet) - close(packetRecv) - }, } u, err := url.Parse("rtsp://" + listenIP + ":8554/teststream") @@ -600,19 +602,26 @@ func TestClientReadPartial(t *testing.T) { require.NoError(t, err) defer c.Close() - tracks, baseURL, _, err := c.Describe(u) + medias, baseURL, _, err := c.Describe(u) require.NoError(t, err) - _, err = c.Setup(tracks[1], baseURL, 0, 0) + _, err = c.Setup(medias[1], baseURL, 0, 0) require.NoError(t, err) + c.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + require.Equal(t, medias[1], medi) + require.Equal(t, medias[1].Formats[0], trak) + require.Equal(t, &testRTPPacket, pkt) + close(packetRecv) + }) + _, err = c.Play(nil) require.NoError(t, err) <-packetRecv } -func TestClientReadContentBase(t *testing.T) { +func TestClientPlayContentBase(t *testing.T) { for _, ca := range []string{ "absent", "inside control attribute", @@ -653,15 +662,8 @@ func TestClientReadContentBase(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() switch ca { case "absent": @@ -670,12 +672,12 @@ func TestClientReadContentBase(t *testing.T) { Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) case "inside control attribute": - body := string(tracks.Marshal(false)) + body := string(mustMarshalSDP(medias.Marshal(false))) body = strings.Replace(body, "t=0 0", "t=0 0\r\na=control:rtsp://localhost:8554/teststream", 1) err = conn.WriteResponse(&base.Response{ @@ -692,7 +694,7 @@ func TestClientReadContentBase(t *testing.T) { req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) + require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -739,14 +741,14 @@ func TestClientReadContentBase(t *testing.T) { c := Client{} - err = startReading(&c, "rtsp://localhost:8554/teststream") + err = readAll(&c, "rtsp://localhost:8554/teststream", nil) require.NoError(t, err) c.Close() }) } } -func TestClientReadAnyPort(t *testing.T) { +func TestClientPlayAnyPort(t *testing.T) { for _, ca := range []string{ "zero", "zero_one", @@ -790,15 +792,8 @@ func TestClientReadAnyPort(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -806,7 +801,7 @@ func TestClientReadAnyPort(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -893,27 +888,29 @@ func TestClientReadAnyPort(t *testing.T) { c := Client{ AnyPortEnable: true, - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { - require.Equal(t, &testRTPPacket, ctx.Packet) - close(packetRecv) - }, } - err = startReading(&c, "rtsp://localhost:8554/teststream") + var med *media.Media + err = readAll(&c, "rtsp://localhost:8554/teststream", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + require.Equal(t, &testRTPPacket, pkt) + med = medi + close(packetRecv) + }) require.NoError(t, err) defer c.Close() <-packetRecv if ca == "random" { - c.WritePacketRTCP(0, &testRTCPPacket) + c.WritePacketRTCP(med, &testRTCPPacket) <-serverRecv } }) } } -func TestClientReadAutomaticProtocol(t *testing.T) { +func TestClientPlayAutomaticProtocol(t *testing.T) { t.Run("switch after status code", func(t *testing.T) { l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) @@ -949,15 +946,8 @@ func TestClientReadAutomaticProtocol(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -965,7 +955,7 @@ func TestClientReadAutomaticProtocol(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -1020,13 +1010,11 @@ func TestClientReadAutomaticProtocol(t *testing.T) { packetRecv := make(chan struct{}) - c := Client{ - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { + c := Client{} + err = readAll(&c, "rtsp://localhost:8554/teststream", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { close(packetRecv) - }, - } - - err = startReading(&c, "rtsp://localhost:8554/teststream") + }) require.NoError(t, err) defer c.Close() @@ -1043,13 +1031,8 @@ func TestClientReadAutomaticProtocol(t *testing.T) { go func() { defer close(serverDone) - tracks := Tracks{&TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - }} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() func() { nconn, err := l.Accept() @@ -1100,14 +1083,14 @@ func TestClientReadAutomaticProtocol(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) + require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -1182,7 +1165,7 @@ func TestClientReadAutomaticProtocol(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -1203,7 +1186,7 @@ func TestClientReadAutomaticProtocol(t *testing.T) { req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) + require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), req.URL) err = v.ValidateRequest(req) require.NoError(t, err) @@ -1259,12 +1242,12 @@ func TestClientReadAutomaticProtocol(t *testing.T) { c := Client{ ReadTimeout: 1 * time.Second, - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { - close(packetRecv) - }, } - err = startReading(&c, "rtsp://myuser:mypass@localhost:8554/teststream") + err = readAll(&c, "rtsp://myuser:mypass@localhost:8554/teststream", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + close(packetRecv) + }) require.NoError(t, err) defer c.Close() @@ -1272,7 +1255,7 @@ func TestClientReadAutomaticProtocol(t *testing.T) { }) } -func TestClientReadDifferentInterleavedIDs(t *testing.T) { +func TestClientPlayDifferentInterleavedIDs(t *testing.T) { l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) defer l.Close() @@ -1308,15 +1291,8 @@ func TestClientReadDifferentInterleavedIDs(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - track1 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track1} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -1324,14 +1300,14 @@ func TestClientReadDifferentInterleavedIDs(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) + require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -1388,34 +1364,25 @@ func TestClientReadDifferentInterleavedIDs(t *testing.T) { v := TransportTCP return &v }(), - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { - require.Equal(t, 0, ctx.TrackID) - close(packetRecv) - }, } - err = startReading(&c, "rtsp://localhost:8554/teststream") + err = readAll(&c, "rtsp://localhost:8554/teststream", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + close(packetRecv) + }) require.NoError(t, err) defer c.Close() <-packetRecv } -func TestClientReadRedirect(t *testing.T) { +func TestClientPlayRedirect(t *testing.T) { for _, withCredentials := range []bool{false, true} { runName := "WithoutCredentials" if withCredentials { runName = "WithCredentials" } t.Run(runName, func(t *testing.T) { - packetRecv := make(chan struct{}) - - c := Client{ - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { - close(packetRecv) - }, - } - l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) defer l.Close() @@ -1519,15 +1486,8 @@ func TestClientReadRedirect(t *testing.T) { require.Equal(t, base.Describe, req.Method) } - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -1535,7 +1495,7 @@ func TestClientReadRedirect(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -1585,11 +1545,18 @@ func TestClientReadRedirect(t *testing.T) { }() }() + packetRecv := make(chan struct{}) + + c := Client{} + ru := "rtsp://localhost:8554/path1" if withCredentials { ru = "rtsp://testusr:testpwd@localhost:8554/path1" } - err = startReading(&c, ru) + err = readAll(&c, ru, + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + close(packetRecv) + }) require.NoError(t, err) defer c.Close() @@ -1598,7 +1565,7 @@ func TestClientReadRedirect(t *testing.T) { } } -func TestClientReadPause(t *testing.T) { +func TestClientPlayPause(t *testing.T) { writeFrames := func(inTH *headers.Transport, conn *conn.Conn) (chan struct{}, chan struct{}) { writerTerminate := make(chan struct{}) writerDone := make(chan struct{}) @@ -1680,15 +1647,8 @@ func TestClientReadPause(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -1696,7 +1656,7 @@ func TestClientReadPause(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -1791,14 +1751,14 @@ func TestClientReadPause(t *testing.T) { v := TransportTCP return &v }(), - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { + } + + err = readAll(&c, "rtsp://localhost:8554/teststream", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { if atomic.SwapInt32(&firstFrame, 1) == 0 { close(packetRecv) } - }, - } - - err = startReading(&c, "rtsp://localhost:8554/teststream") + }) require.NoError(t, err) defer c.Close() @@ -1818,7 +1778,7 @@ func TestClientReadPause(t *testing.T) { } } -func TestClientReadRTCPReport(t *testing.T) { +func TestClientPlayRTCPReport(t *testing.T) { reportReceived := make(chan struct{}) l, err := net.Listen("tcp", "localhost:8554") @@ -1855,15 +1815,8 @@ func TestClientReadRTCPReport(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -1871,7 +1824,7 @@ func TestClientReadRTCPReport(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -1939,6 +1892,9 @@ func TestClientReadRTCPReport(t *testing.T) { }) require.NoError(t, err) + // wait for the packet's SSRC to be saved + time.Sleep(500 * time.Millisecond) + sr := &rtcp.SenderReport{ SSRC: 753621, NTPTime: 0, @@ -1989,14 +1945,14 @@ func TestClientReadRTCPReport(t *testing.T) { udpReceiverReportPeriod: 1 * time.Second, } - err = startReading(&c, "rtsp://localhost:8554/teststream") + err = readAll(&c, "rtsp://localhost:8554/teststream", nil) require.NoError(t, err) defer c.Close() <-reportReceived } -func TestClientReadErrorTimeout(t *testing.T) { +func TestClientPlayErrorTimeout(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -2037,15 +1993,8 @@ func TestClientReadErrorTimeout(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -2053,7 +2002,7 @@ func TestClientReadErrorTimeout(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -2139,7 +2088,7 @@ func TestClientReadErrorTimeout(t *testing.T) { ReadTimeout: 1 * time.Second, } - err = startReading(&c, "rtsp://localhost:8554/teststream") + err = readAll(&c, "rtsp://localhost:8554/teststream", nil) require.NoError(t, err) err = c.Wait() @@ -2155,7 +2104,7 @@ func TestClientReadErrorTimeout(t *testing.T) { } } -func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) { +func TestClientPlayIgnoreTCPInvalidMedia(t *testing.T) { l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) defer l.Close() @@ -2190,15 +2139,8 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -2206,7 +2148,7 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -2273,19 +2215,19 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) { v := TransportTCP return &v }(), - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { - close(recv) - }, } - err = startReading(&c, "rtsp://localhost:8554/teststream") + err = readAll(&c, "rtsp://localhost:8554/teststream", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + close(recv) + }) require.NoError(t, err) defer c.Close() <-recv } -func TestClientReadSeek(t *testing.T) { +func TestClientPlaySeek(t *testing.T) { l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) defer l.Close() @@ -2320,15 +2262,8 @@ func TestClientReadSeek(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -2336,7 +2271,7 @@ func TestClientReadSeek(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -2433,10 +2368,10 @@ func TestClientReadSeek(t *testing.T) { require.NoError(t, err) defer c.Close() - tracks, baseURL, _, err := c.Describe(u) + medias, baseURL, _, err := c.Describe(u) require.NoError(t, err) - err = c.SetupAll(tracks, baseURL) + err = c.SetupAll(medias, baseURL) require.NoError(t, err) _, err = c.Play(&headers.Range{ @@ -2454,7 +2389,7 @@ func TestClientReadSeek(t *testing.T) { require.NoError(t, err) } -func TestClientReadKeepaliveFromSession(t *testing.T) { +func TestClientPlayKeepaliveFromSession(t *testing.T) { l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) defer l.Close() @@ -2491,15 +2426,8 @@ func TestClientReadKeepaliveFromSession(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -2507,7 +2435,7 @@ func TestClientReadKeepaliveFromSession(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -2575,14 +2503,14 @@ func TestClientReadKeepaliveFromSession(t *testing.T) { c := Client{} - err = startReading(&c, "rtsp://localhost:8554/teststream") + err = readAll(&c, "rtsp://localhost:8554/teststream", nil) require.NoError(t, err) defer c.Close() <-keepaliveOk } -func TestClientReadDifferentSource(t *testing.T) { +func TestClientPlayDifferentSource(t *testing.T) { packetRecv := make(chan struct{}) l, err := net.Listen("tcp", "localhost:8554") @@ -2621,15 +2549,8 @@ func TestClientReadDifferentSource(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/test/stream?param=value"), req.URL) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -2637,14 +2558,14 @@ func TestClientReadDifferentSource(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/test/stream?param=value/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL("rtsp://localhost:8554/test/stream?param=value/trackID=0"), req.URL) + require.Equal(t, mustParseURL("rtsp://localhost:8554/test/stream?param=value/mediaID=0"), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -2714,20 +2635,18 @@ func TestClientReadDifferentSource(t *testing.T) { }(), } - c.OnPacketRTP = func(ctx *ClientOnPacketRTPCtx) { - require.Equal(t, 0, ctx.TrackID) - require.Equal(t, &testRTPPacket, ctx.Packet) - close(packetRecv) - } - - err = startReading(&c, "rtsp://localhost:8554/test/stream?param=value") + err = readAll(&c, "rtsp://localhost:8554/test/stream?param=value", + func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + require.Equal(t, &testRTPPacket, pkt) + close(packetRecv) + }) require.NoError(t, err) defer c.Close() <-packetRecv } -func TestClientReadDecodeErrors(t *testing.T) { +func TestClientPlayDecodeErrors(t *testing.T) { for _, ca := range []struct { proto string name string @@ -2777,14 +2696,14 @@ func TestClientReadDecodeErrors(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - tracks := Tracks{&TrackGeneric{ - Media: "application", - Payloads: []TrackGenericPayload{{ - Type: 97, - RTPMap: "private/90000", + medias := media.Medias{{ + Type: media.TypeApplication, + Formats: []format.Format{&format.Generic{ + PayloadTyp: 97, + RTPMap: "private/90000", }}, }} - tracks.setControls() + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -2792,7 +2711,7 @@ func TestClientReadDecodeErrors(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/stream/"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) @@ -2866,6 +2785,7 @@ func TestClientReadDecodeErrors(t *testing.T) { case ca.proto == "udp" && ca.name == "rtp packets lost": byts, _ := rtp.Packet{ Header: rtp.Header{ + PayloadType: 97, SequenceNumber: 30, }, }.Marshal() @@ -2876,6 +2796,7 @@ func TestClientReadDecodeErrors(t *testing.T) { byts, _ = rtp.Packet{ Header: rtp.Header{ + PayloadType: 97, SequenceNumber: 100, }, }.Marshal() @@ -2957,7 +2878,7 @@ func TestClientReadDecodeErrors(t *testing.T) { }, } - err = startReading(&c, "rtsp://localhost:8554/stream") + err = readAll(&c, "rtsp://localhost:8554/stream", nil) require.NoError(t, err) defer c.Close() diff --git a/client_publish_test.go b/client_record_test.go similarity index 89% rename from client_publish_test.go rename to client_record_test.go index b81ebce6..4ad92d8c 100644 --- a/client_publish_test.go +++ b/client_record_test.go @@ -12,15 +12,28 @@ import ( "github.com/pion/rtp" "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/url" ) +var testH264Media = &media.Media{ + Type: media.TypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + SPS: []byte{0x01, 0x02, 0x03, 0x04}, + PPS: []byte{0x01, 0x02, 0x03, 0x04}, + PacketizationMode: 1, + }}, +} + var testRTPPacket = rtp.Packet{ Header: rtp.Header{ Version: 2, - PayloadType: 97, + PayloadType: 96, CSRC: []uint32{}, SSRC: 0x38F27A2F, }, @@ -51,7 +64,43 @@ var testRTCPPacketMarshaled = func() []byte { return byts }() -func TestClientPublishSerial(t *testing.T) { +func record(c *Client, ur string, medias media.Medias, cb func(*media.Media, rtcp.Packet)) error { + u, err := url.Parse(ur) + if err != nil { + return err + } + + err = c.Start(u.Scheme, u.Host) + if err != nil { + return err + } + + _, err = c.Announce(u, medias) + if err != nil { + c.Close() + return err + } + + err = c.SetupAll(medias, u) + if err != nil { + c.Close() + return err + } + + if cb != nil { + c.OnPacketRTCPAny(cb) + } + + _, err = c.Record() + if err != nil { + c.Close() + return err + } + + return nil +} + +func TestClientRecordSerial(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -114,7 +163,7 @@ func TestClientPublishSerial(t *testing.T) { req, err = conn.ReadRequest() require.NoError(t, err) require.Equal(t, base.Setup, req.Method) - require.Equal(t, mustParseURL(scheme+"://localhost:8554/teststream/trackID=0"), req.URL) + require.Equal(t, mustParseURL(scheme+"://localhost:8554/teststream/mediaID=0"), req.URL) var inTH headers.Transport err = inTH.Unmarshal(req.Header["Transport"]) @@ -224,22 +273,16 @@ func TestClientPublishSerial(t *testing.T) { v := TransportTCP return &v }(), - OnPacketRTCP: func(ctx *ClientOnPacketRTCPCtx) { - require.Equal(t, 0, ctx.TrackID) - require.Equal(t, &testRTCPPacket, ctx.Packet) + } + + medi := testH264Media.Clone() + medias := media.Medias{medi} + + err = record(&c, scheme+"://localhost:8554/teststream", medias, + func(medi *media.Media, pkt rtcp.Packet) { + require.Equal(t, &testRTCPPacket, pkt) close(recvDone) - }, - } - - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - err = c.StartPublishing(scheme+"://localhost:8554/teststream", - Tracks{track}) + }) require.NoError(t, err) done := make(chan struct{}) @@ -248,20 +291,20 @@ func TestClientPublishSerial(t *testing.T) { c.Wait() }() - err = c.WritePacketRTP(0, &testRTPPacket) + err = c.WritePacketRTP(medi, &testRTPPacket) require.NoError(t, err) <-recvDone c.Close() <-done - err = c.WritePacketRTP(0, &testRTPPacket) + err = c.WritePacketRTP(medi, &testRTPPacket) require.Error(t, err) }) } } -func TestClientPublishParallel(t *testing.T) { +func TestClientRecordParallel(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -384,18 +427,11 @@ func TestClientPublishParallel(t *testing.T) { }(), } - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - writerDone := make(chan struct{}) defer func() { <-writerDone }() - err = c.StartPublishing(scheme+"://localhost:8554/teststream", - Tracks{track}) + medi := testH264Media.Clone() + err = record(&c, scheme+"://localhost:8554/teststream", media.Medias{medi}, nil) require.NoError(t, err) defer c.Close() @@ -406,7 +442,7 @@ func TestClientPublishParallel(t *testing.T) { defer t.Stop() for range t.C { - err := c.WritePacketRTP(0, &testRTPPacket) + err := c.WritePacketRTP(medi, &testRTPPacket) if err != nil { return } @@ -418,7 +454,7 @@ func TestClientPublishParallel(t *testing.T) { } } -func TestClientPublishPauseSerial(t *testing.T) { +func TestClientRecordPauseSerial(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -544,19 +580,12 @@ func TestClientPublishPauseSerial(t *testing.T) { }(), } - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - err = c.StartPublishing("rtsp://localhost:8554/teststream", - Tracks{track}) + medi := testH264Media.Clone() + err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil) require.NoError(t, err) defer c.Close() - err = c.WritePacketRTP(0, &testRTPPacket) + err = c.WritePacketRTP(medi, &testRTPPacket) require.NoError(t, err) _, err = c.Pause() @@ -565,13 +594,13 @@ func TestClientPublishPauseSerial(t *testing.T) { _, err = c.Record() require.NoError(t, err) - err = c.WritePacketRTP(0, &testRTPPacket) + err = c.WritePacketRTP(medi, &testRTPPacket) require.NoError(t, err) }) } } -func TestClientPublishPauseParallel(t *testing.T) { +func TestClientRecordPauseParallel(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -679,15 +708,8 @@ func TestClientPublishPauseParallel(t *testing.T) { }(), } - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - err = c.StartPublishing("rtsp://localhost:8554/teststream", - Tracks{track}) + medi := testH264Media.Clone() + err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil) require.NoError(t, err) writerDone := make(chan struct{}) @@ -698,7 +720,7 @@ func TestClientPublishPauseParallel(t *testing.T) { defer t.Stop() for range t.C { - err := c.WritePacketRTP(0, &testRTPPacket) + err := c.WritePacketRTP(medi, &testRTPPacket) if err != nil { return } @@ -716,7 +738,7 @@ func TestClientPublishPauseParallel(t *testing.T) { } } -func TestClientPublishAutomaticProtocol(t *testing.T) { +func TestClientRecordAutomaticProtocol(t *testing.T) { l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) defer l.Close() @@ -821,25 +843,18 @@ func TestClientPublishAutomaticProtocol(t *testing.T) { require.NoError(t, err) }() - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - c := Client{} - err = c.StartPublishing("rtsp://localhost:8554/teststream", - Tracks{track}) + medi := testH264Media.Clone() + err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil) require.NoError(t, err) defer c.Close() - err = c.WritePacketRTP(0, &testRTPPacket) + err = c.WritePacketRTP(medi, &testRTPPacket) require.NoError(t, err) } -func TestClientPublishDecodeErrors(t *testing.T) { +func TestClientRecordDecodeErrors(t *testing.T) { for _, ca := range []struct { proto string name string @@ -1010,15 +1025,7 @@ func TestClientPublishDecodeErrors(t *testing.T) { }, } - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - err = c.StartPublishing("rtsp://localhost:8554/stream", - Tracks{track}) + err = record(&c, "rtsp://localhost:8554/stream", media.Medias{testH264Media.Clone()}, nil) require.NoError(t, err) defer c.Close() @@ -1027,7 +1034,7 @@ func TestClientPublishDecodeErrors(t *testing.T) { } } -func TestClientPublishRTCPReport(t *testing.T) { +func TestClientRecordRTCPReport(t *testing.T) { for _, ca := range []string{"udp", "tcp"} { t.Run(ca, func(t *testing.T) { reportReceived := make(chan struct{}) @@ -1170,21 +1177,16 @@ func TestClientPublishRTCPReport(t *testing.T) { v := TransportTCP return &v }(), - udpSenderReportPeriod: 500 * time.Millisecond, + senderReportPeriod: 500 * time.Millisecond, } - err = c.StartPublishing("rtsp://localhost:8554/teststream", - Tracks{&TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - }}) + medi := testH264Media.Clone() + err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil) require.NoError(t, err) defer c.Close() for i := 0; i < 2; i++ { - err = c.WritePacketRTP(0, &rtp.Packet{ + err = c.WritePacketRTP(medi, &rtp.Packet{ Header: rtp.Header{ Version: 2, PayloadType: 96, @@ -1200,7 +1202,7 @@ func TestClientPublishRTCPReport(t *testing.T) { } } -func TestClientPublishIgnoreTCPRTPPackets(t *testing.T) { +func TestClientRecordIgnoreTCPRTPPackets(t *testing.T) { l, err := net.Listen("tcp", "localhost:8554") require.NoError(t, err) defer l.Close() @@ -1303,23 +1305,14 @@ func TestClientPublishIgnoreTCPRTPPackets(t *testing.T) { v := TransportTCP return &v }(), - OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) { - t.Errorf("should not happen") - }, - OnPacketRTCP: func(ctx *ClientOnPacketRTCPCtx) { + } + + medias := media.Medias{testH264Media.Clone()} + + err = record(&c, "rtsp://localhost:8554/teststream", medias, + func(medi *media.Media, pkt rtcp.Packet) { close(rtcpReceived) - }, - } - - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - err = c.StartPublishing("rtsp://localhost:8554/teststream", - Tracks{track}) + }) require.NoError(t, err) defer c.Close() diff --git a/client_test.go b/client_test.go index 6790624a..bb3e8d43 100644 --- a/client_test.go +++ b/client_test.go @@ -8,10 +8,11 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/auth" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/auth" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/url" ) func mustParseURL(s string) *url.URL { @@ -105,15 +106,8 @@ func TestClientSession(t *testing.T) { require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"]) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -121,7 +115,7 @@ func TestClientSession(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Session": base.HeaderValue{"123456"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) }() @@ -189,22 +183,15 @@ func TestClientAuth(t *testing.T) { err = v.ValidateRequest(req) require.NoError(t, err) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) }() @@ -256,12 +243,7 @@ func TestClientDescribeCharset(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - track1 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } + medias := media.Medias{testH264Media.Clone()} err = conn.WriteResponse(&base.Response{ StatusCode: base.StatusOK, @@ -269,7 +251,7 @@ func TestClientDescribeCharset(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track1}.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) }() diff --git a/clientmedia.go b/clientmedia.go new file mode 100644 index 00000000..8cf04926 --- /dev/null +++ b/clientmedia.go @@ -0,0 +1,341 @@ +package gortsplib + +import ( + "fmt" + "sync/atomic" + "time" + + "github.com/pion/rtcp" + + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/media" +) + +type clientMedia struct { + c *Client + media *media.Media + formats map[uint8]*clientFormat + tcpChannel int + udpRTPListener *clientUDPListener + udpRTCPListener *clientUDPListener + tcpRTPFrame *base.InterleavedFrame + tcpRTCPFrame *base.InterleavedFrame + tcpBuffer []byte + writePacketRTPInQueue func([]byte) + writePacketRTCPInQueue func([]byte) + readRTP func([]byte) error + readRTCP func([]byte) error + onPacketRTCP func(rtcp.Packet) +} + +func newClientMedia(c *Client) *clientMedia { + return &clientMedia{ + c: c, + onPacketRTCP: func(rtcp.Packet) {}, + } +} + +func (cm *clientMedia) close() { + if cm.udpRTPListener != nil { + cm.udpRTPListener.close() + cm.udpRTCPListener.close() + } +} + +func (cm *clientMedia) allocateUDPListeners(multicast bool, rtpAddress string, rtcpAddress string) error { + if rtpAddress != ":0" { + l1, err := newClientUDPListener( + cm.c, multicast, rtpAddress, + cm, true) + if err != nil { + return err + } + + l2, err := newClientUDPListener( + cm.c, multicast, rtcpAddress, + cm, false) + if err != nil { + l1.close() + return err + } + + cm.udpRTPListener, cm.udpRTCPListener = l1, l2 + return nil + } + + cm.udpRTPListener, cm.udpRTCPListener = newClientUDPListenerPair(cm.c, cm) + return nil +} + +func (cm *clientMedia) setMedia(medi *media.Media) { + cm.media = medi + + cm.formats = make(map[uint8]*clientFormat) + for _, trak := range medi.Formats { + cm.formats[trak.PayloadType()] = newClientFormat(cm, trak) + } +} + +func (cm *clientMedia) start() { + if cm.udpRTPListener != nil { + cm.writePacketRTPInQueue = cm.writePacketRTPInQueueUDP + cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueUDP + + if cm.c.state == clientStatePlay { + cm.readRTP = cm.readRTPUDPPlay + cm.readRTCP = cm.readRTCPUDPPlay + } else { + cm.readRTP = cm.readRTPUDPRecord + cm.readRTCP = cm.readRTCPUDPRecord + } + } else { + cm.writePacketRTPInQueue = cm.writePacketRTPInQueueTCP + cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueTCP + + if cm.c.state == clientStatePlay { + cm.readRTP = cm.readRTPTCPPlay + cm.readRTCP = cm.readRTCPTCPPlay + } else { + cm.readRTP = cm.readRTPTCPRecord + cm.readRTCP = cm.readRTCPTCPRecord + } + + cm.tcpRTPFrame = &base.InterleavedFrame{Channel: cm.tcpChannel} + cm.tcpRTCPFrame = &base.InterleavedFrame{Channel: cm.tcpChannel + 1} + cm.tcpBuffer = make([]byte, maxPacketSize+4) + } + + for _, ct := range cm.formats { + ct.start(cm) + } + + if cm.udpRTPListener != nil { + cm.udpRTPListener.start(cm.c.state == clientStatePlay) + cm.udpRTCPListener.start(cm.c.state == clientStatePlay) + } + + for _, ct := range cm.formats { + ct.startWriting() + } +} + +func (cm *clientMedia) stop() { + if cm.udpRTPListener != nil { + cm.udpRTPListener.stop() + cm.udpRTCPListener.stop() + } + + for _, ct := range cm.formats { + ct.stop() + } +} + +func (cm *clientMedia) findFormatWithSSRC(ssrc uint32) *clientFormat { + for _, format := range cm.formats { + tssrc, ok := format.udpRTCPReceiver.LastSSRC() + if ok && tssrc == ssrc { + return format + } + } + return nil +} + +func (cm *clientMedia) writePacketRTPInQueueUDP(payload []byte) { + atomic.AddUint64(cm.c.BytesSent, uint64(len(payload))) + cm.udpRTPListener.write(payload) +} + +func (cm *clientMedia) writePacketRTCPInQueueUDP(payload []byte) { + atomic.AddUint64(cm.c.BytesSent, uint64(len(payload))) + cm.udpRTCPListener.write(payload) +} + +func (cm *clientMedia) writePacketRTPInQueueTCP(payload []byte) { + atomic.AddUint64(cm.c.BytesSent, uint64(len(payload))) + cm.tcpRTPFrame.Payload = payload + cm.c.nconn.SetWriteDeadline(time.Now().Add(cm.c.WriteTimeout)) + cm.c.conn.WriteInterleavedFrame(cm.tcpRTPFrame, cm.tcpBuffer) +} + +func (cm *clientMedia) writePacketRTCPInQueueTCP(payload []byte) { + atomic.AddUint64(cm.c.BytesSent, uint64(len(payload))) + cm.tcpRTCPFrame.Payload = payload + cm.c.nconn.SetWriteDeadline(time.Now().Add(cm.c.WriteTimeout)) + cm.c.conn.WriteInterleavedFrame(cm.tcpRTCPFrame, cm.tcpBuffer) +} + +func (cm *clientMedia) writePacketRTCP(pkt rtcp.Packet) error { + byts, err := pkt.Marshal() + if err != nil { + return err + } + + cm.c.writeMutex.RLock() + defer cm.c.writeMutex.RUnlock() + + ok := cm.c.writer.queue(func() { + cm.writePacketRTCPInQueue(byts) + }) + + if !ok { + select { + case <-cm.c.done: + return cm.c.closeError + default: + return nil + } + } + + return nil +} + +func (cm *clientMedia) readRTPTCPPlay(payload []byte) error { + now := time.Now() + atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix()) + + pkt := cm.c.rtpPacketBuffer.next() + err := pkt.Unmarshal(payload) + if err != nil { + return err + } + + trak, ok := cm.formats[pkt.PayloadType] + if !ok { + return nil + } + + trak.readRTPTCP(pkt) + return nil +} + +func (cm *clientMedia) readRTCPTCPPlay(payload []byte) error { + now := time.Now() + atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix()) + + if len(payload) > maxPacketSize { + cm.c.OnDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", + len(payload), maxPacketSize)) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.c.OnDecodeError(err) + return nil + } + + for _, pkt := range packets { + cm.onPacketRTCP(pkt) + } + + return nil +} + +func (cm *clientMedia) readRTPTCPRecord(payload []byte) error { + return nil +} + +func (cm *clientMedia) readRTCPTCPRecord(payload []byte) error { + if len(payload) > maxPacketSize { + cm.c.OnDecodeError(fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", + len(payload), maxPacketSize)) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.c.OnDecodeError(err) + return nil + } + + for _, pkt := range packets { + cm.onPacketRTCP(pkt) + } + + return nil +} + +func (cm *clientMedia) readRTPUDPPlay(payload []byte) error { + plen := len(payload) + + atomic.AddUint64(cm.c.BytesReceived, uint64(plen)) + + if plen == (maxPacketSize + 1) { + cm.c.OnDecodeError(fmt.Errorf("RTP packet is too big to be read with UDP")) + return nil + } + + pkt := cm.c.rtpPacketBuffer.next() + err := pkt.Unmarshal(payload) + if err != nil { + cm.c.OnDecodeError(err) + return nil + } + + trak, ok := cm.formats[pkt.PayloadType] + if !ok { + cm.c.OnDecodeError(fmt.Errorf("received RTP packet with unknown payload type (%d)", pkt.PayloadType)) + return nil + } + + trak.readRTPUDP(pkt) + return nil +} + +func (cm *clientMedia) readRTCPUDPPlay(payload []byte) error { + now := time.Now() + plen := len(payload) + + atomic.AddUint64(cm.c.BytesReceived, uint64(plen)) + + if plen == (maxPacketSize + 1) { + cm.c.OnDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP")) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.c.OnDecodeError(err) + return nil + } + + for _, pkt := range packets { + if sr, ok := pkt.(*rtcp.SenderReport); ok { + format := cm.findFormatWithSSRC(sr.SSRC) + if format != nil { + format.udpRTCPReceiver.ProcessSenderReport(sr, now) + } + } + + cm.onPacketRTCP(pkt) + } + + return nil +} + +func (cm *clientMedia) readRTPUDPRecord(payload []byte) error { + return nil +} + +func (cm *clientMedia) readRTCPUDPRecord(payload []byte) error { + plen := len(payload) + + atomic.AddUint64(cm.c.BytesReceived, uint64(plen)) + + if plen == (maxPacketSize + 1) { + cm.c.OnDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP")) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.c.OnDecodeError(err) + return nil + } + + for _, pkt := range packets { + cm.onPacketRTCP(pkt) + } + + return nil +} diff --git a/clienttrack.go b/clienttrack.go new file mode 100644 index 00000000..89f2ada8 --- /dev/null +++ b/clienttrack.go @@ -0,0 +1,119 @@ +package gortsplib + +import ( + "fmt" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/rtcpreceiver" + "github.com/aler9/gortsplib/v2/pkg/rtcpsender" + "github.com/aler9/gortsplib/v2/pkg/rtpreorderer" +) + +type clientFormat struct { + c *Client + cm *clientMedia + format format.Format + udpReorderer *rtpreorderer.Reorderer // play + udpRTCPReceiver *rtcpreceiver.RTCPReceiver // play + rtcpSender *rtcpsender.RTCPSender // record + onPacketRTP func(*rtp.Packet) +} + +func newClientFormat(cm *clientMedia, trak format.Format) *clientFormat { + return &clientFormat{ + c: cm.c, + cm: cm, + format: trak, + onPacketRTP: func(*rtp.Packet) {}, + } +} + +func (ct *clientFormat) start(cm *clientMedia) { + if cm.c.state == clientStatePlay { + if cm.udpRTPListener != nil { + ct.udpReorderer = rtpreorderer.New() + ct.udpRTCPReceiver = rtcpreceiver.New( + cm.c.udpReceiverReportPeriod, + nil, + ct.format.ClockRate(), func(pkt rtcp.Packet) { + cm.writePacketRTCP(pkt) + }) + } + } else { + ct.rtcpSender = rtcpsender.New( + ct.format.ClockRate(), + func(pkt rtcp.Packet) { + cm.writePacketRTCP(pkt) + }) + } +} + +// start RTCP senders after write() has been allocated in order to avoid a crash +func (ct *clientFormat) startWriting() { + if ct.c.state != clientStatePlay && !ct.c.DisableRTCPSenderReports { + ct.rtcpSender.Start(ct.c.senderReportPeriod) + } +} + +func (ct *clientFormat) stop() { + if ct.udpRTCPReceiver != nil { + ct.udpRTCPReceiver.Close() + ct.udpRTCPReceiver = nil + } + + if ct.rtcpSender != nil { + ct.rtcpSender.Close() + ct.rtcpSender = nil + } +} + +func (ct *clientFormat) writePacketRTPWithNTP(pkt *rtp.Packet, ntp time.Time) error { + byts := make([]byte, maxPacketSize) + n, err := pkt.MarshalTo(byts) + if err != nil { + return err + } + byts = byts[:n] + + ct.c.writeMutex.RLock() + defer ct.c.writeMutex.RUnlock() + + ok := ct.c.writer.queue(func() { + ct.cm.writePacketRTPInQueue(byts) + }) + + if !ok { + select { + case <-ct.c.done: + return ct.c.closeError + default: + return nil + } + } + + ct.rtcpSender.ProcessPacket(pkt, ntp, ct.format.PTSEqualsDTS(pkt)) + return nil +} + +func (ct *clientFormat) readRTPUDP(pkt *rtp.Packet) { + packets, missing := ct.udpReorderer.Process(pkt) + if missing != 0 { + ct.c.OnDecodeError(fmt.Errorf("%d RTP packet(s) lost", missing)) + // do not return + } + + now := time.Now() + + for _, pkt := range packets { + ct.udpRTCPReceiver.ProcessPacket(pkt, now, ct.format.PTSEqualsDTS(pkt)) + ct.onPacketRTP(pkt) + } +} + +func (ct *clientFormat) readRTPTCP(pkt *rtp.Packet) { + ct.onPacketRTP(pkt) +} diff --git a/clientudpl.go b/clientudpl.go index f2d9bb52..b4297a65 100644 --- a/clientudpl.go +++ b/clientudpl.go @@ -2,13 +2,11 @@ package gortsplib import ( "crypto/rand" - "fmt" "net" "strconv" "sync/atomic" "time" - "github.com/pion/rtcp" "golang.org/x/net/ipv4" ) @@ -25,7 +23,7 @@ func randIntn(n int) int { type clientUDPListener struct { c *Client pc *net.UDPConn - ct *clientTrack + cm *clientMedia isRTP bool readIP net.IP @@ -38,7 +36,7 @@ type clientUDPListener struct { readerDone chan struct{} } -func newClientUDPListenerPair(c *Client, ct *clientTrack) (*clientUDPListener, *clientUDPListener) { +func newClientUDPListenerPair(c *Client, cm *clientMedia) (*clientUDPListener, *clientUDPListener) { // choose two consecutive ports in range 65535-10000 // RTP port must be even and RTCP port odd for { @@ -47,7 +45,7 @@ func newClientUDPListenerPair(c *Client, ct *clientTrack) (*clientUDPListener, * c, false, ":"+strconv.FormatInt(int64(rtpPort), 10), - ct, + cm, true) if err != nil { continue @@ -58,7 +56,7 @@ func newClientUDPListenerPair(c *Client, ct *clientTrack) (*clientUDPListener, * c, false, ":"+strconv.FormatInt(int64(rtcpPort), 10), - ct, + cm, false) if err != nil { rtpListener.close() @@ -73,7 +71,7 @@ func newClientUDPListener( c *Client, multicast bool, address string, - ct *clientTrack, + cm *clientMedia, isRTP bool, ) (*clientUDPListener, error) { var pc *net.UDPConn @@ -124,7 +122,7 @@ func newClientUDPListener( return &clientUDPListener{ c: c, pc: pc, - ct: ct, + cm: cm, isRTP: isRTP, lastPacketTime: func() *int64 { v := int64(0) @@ -159,15 +157,11 @@ func (u *clientUDPListener) stop() { func (u *clientUDPListener) runReader(forPlay bool) { defer close(u.readerDone) - var processFunc func(time.Time, []byte) - if forPlay { - if u.isRTP { - processFunc = u.processPlayRTP - } else { - processFunc = u.processPlayRTCP - } + var readFunc func([]byte) error + if u.isRTP { + readFunc = u.cm.readRTP } else { - processFunc = u.processRecordRTCP + readFunc = u.cm.readRTCP } for { @@ -186,90 +180,7 @@ func (u *clientUDPListener) runReader(forPlay bool) { now := time.Now() atomic.StoreInt64(u.lastPacketTime, now.Unix()) - processFunc(now, buf[:n]) - } -} - -func (u *clientUDPListener) processPlayRTP(now time.Time, payload []byte) { - plen := len(payload) - - atomic.AddUint64(u.c.BytesReceived, uint64(plen)) - - if plen == (maxPacketSize + 1) { - u.c.OnDecodeError(fmt.Errorf("RTP packet is too big to be read with UDP")) - return - } - - pkt := u.ct.udpRTPPacketBuffer.next() - err := pkt.Unmarshal(payload) - if err != nil { - u.c.OnDecodeError(err) - return - } - - packets, missing := u.ct.reorderer.Process(pkt) - if missing != 0 { - u.c.OnDecodeError(fmt.Errorf("%d RTP packet(s) lost", missing)) - // do not return - } - - for _, pkt := range packets { - ptsEqualsDTS := ptsEqualsDTS(u.ct.track, pkt) - u.ct.udpRTCPReceiver.ProcessPacketRTP(time.Now(), pkt, ptsEqualsDTS) - - u.c.OnPacketRTP(&ClientOnPacketRTPCtx{ - TrackID: u.ct.id, - Packet: pkt, - }) - } -} - -func (u *clientUDPListener) processPlayRTCP(now time.Time, payload []byte) { - plen := len(payload) - - atomic.AddUint64(u.c.BytesReceived, uint64(plen)) - - if plen == (maxPacketSize + 1) { - u.c.OnDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP")) - return - } - - packets, err := rtcp.Unmarshal(payload) - if err != nil { - u.c.OnDecodeError(err) - return - } - - for _, pkt := range packets { - u.ct.udpRTCPReceiver.ProcessPacketRTCP(now, pkt) - u.c.OnPacketRTCP(&ClientOnPacketRTCPCtx{ - TrackID: u.ct.id, - Packet: pkt, - }) - } -} - -func (u *clientUDPListener) processRecordRTCP(now time.Time, payload []byte) { - plen := len(payload) - - atomic.AddUint64(u.c.BytesReceived, uint64(plen)) - - if plen == (maxPacketSize + 1) { - u.c.OnDecodeError(fmt.Errorf("RTCP packet is too big to be read with UDP")) - return - } - - packets, err := rtcp.Unmarshal(payload) - if err != nil { - u.c.OnDecodeError(err) - return - } - - for _, pkt := range packets { - u.c.OnPacketRTCP(&ClientOnPacketRTCPCtx{ - TrackID: u.ct.id, - Packet: pkt, - }) + readFunc(buf[:n]) } } diff --git a/clientwriter.go b/clientwriter.go new file mode 100644 index 00000000..a6723ade --- /dev/null +++ b/clientwriter.go @@ -0,0 +1,60 @@ +package gortsplib + +import ( + "github.com/aler9/gortsplib/v2/pkg/ringbuffer" +) + +// this struct contains a queue that allows to detach the routine that is reading a stream +// from the routine that is writing a stream. +type clientWriter struct { + allowWriting bool + buffer *ringbuffer.RingBuffer + + done chan struct{} +} + +func (cw *clientWriter) start(c *Client) { + if c.state == clientStatePlay { + // when reading, buffer is only used to send RTCP receiver reports, + // that are much smaller than RTP packets and are sent at a fixed interval. + // decrease RAM consumption by allocating less buffers. + cw.buffer, _ = ringbuffer.New(8) + } else { + cw.buffer, _ = ringbuffer.New(uint64(c.WriteBufferCount)) + } + + cw.done = make(chan struct{}) + go cw.run() + + cw.allowWriting = true +} + +func (cw *clientWriter) stop() { + cw.allowWriting = false + + cw.buffer.Close() + <-cw.done + cw.buffer = nil +} + +func (cw *clientWriter) run() { + defer close(cw.done) + + for { + tmp, ok := cw.buffer.Pull() + if !ok { + return + } + + tmp.(func())() + } +} + +func (cw *clientWriter) queue(cb func()) bool { + if !cw.allowWriting { + return false + } + + cw.buffer.Push(cb) + return true +} diff --git a/examples/client-publish-track-g711/main.go b/examples/client-publish-format-g711/main.go similarity index 60% rename from examples/client-publish-track-g711/main.go rename to examples/client-publish-format-g711/main.go index ddbbcba3..8e1426ac 100644 --- a/examples/client-publish-track-g711/main.go +++ b/examples/client-publish-format-g711/main.go @@ -4,24 +4,26 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to -// 1. generate RTP/PCMA packets with GStreamer -// 2. connect to a RTSP server, announce a PCMA track +// 1. generate RTP/G711 packets with GStreamer +// 2. connect to a RTSP server, announce a G711 media // 3. route the packets from GStreamer to the server func main() { - // open a listener to receive RTP/PCMA packets + // open a listener to receive RTP/G711 packets pc, err := net.ListenPacket("udp", "localhost:9000") if err != nil { panic(err) } defer pc.Close() - log.Println("Waiting for a RTP/PCMA stream on UDP port 9000 - you can send one with GStreamer:\n" + + log.Println("Waiting for a RTP/G711 stream on UDP port 9000 - you can send one with GStreamer:\n" + "gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=8000" + " ! alawenc ! rtppcmapay ! udpsink host=127.0.0.1 port=9000") @@ -33,14 +35,17 @@ func main() { } log.Println("stream connected") - // create a PCMA track - track := &gortsplib.TrackG711{} + // create a media that contains a G711 format + medi := &media.Media{ + Type: media.TypeAudio, + Formats: []format.Format{&format.G711{}}, + } c := gortsplib.Client{} - // connect to the server and start publishing the track - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + // connect to the server and start recording the media + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -55,7 +60,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-g722/main.go b/examples/client-publish-format-g722/main.go similarity index 70% rename from examples/client-publish-track-g722/main.go rename to examples/client-publish-format-g722/main.go index b17c0e72..d1643833 100644 --- a/examples/client-publish-track-g722/main.go +++ b/examples/client-publish-format-g722/main.go @@ -4,13 +4,15 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/G722 packets with GStreamer -// 2. connect to a RTSP server, announce a G722 track +// 2. connect to a RTSP server, announce a G722 media // 3. route the packets from GStreamer to the server func main() { @@ -33,14 +35,17 @@ func main() { } log.Println("stream connected") - // create a G722 track - track := &gortsplib.TrackG722{} + // create a media that contains a G722 format + medi := &media.Media{ + Type: media.TypeAudio, + Formats: []format.Format{&format.G722{}}, + } c := gortsplib.Client{} - // connect to the server and start publishing the track - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + // connect to the server and start recording the media + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -55,7 +60,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-h264/main.go b/examples/client-publish-format-h264/main.go similarity index 68% rename from examples/client-publish-track-h264/main.go rename to examples/client-publish-format-h264/main.go index cce1b3a8..9dd225d1 100644 --- a/examples/client-publish-track-h264/main.go +++ b/examples/client-publish-format-h264/main.go @@ -4,13 +4,15 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/H264 packets with GStreamer -// 2. connect to a RTSP server, announce an H264 track +// 2. connect to a RTSP server, announce an H264 media // 3. route the packets from GStreamer to the server func main() { @@ -34,16 +36,19 @@ func main() { } log.Println("stream connected") - // create an H264 track - track := &gortsplib.TrackH264{ - PayloadType: 96, - PacketizationMode: 1, + // create a media that contains a H264 format + medi := &media.Media{ + Type: media.TypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + }}, } - // connect to the server and start publishing the track + // connect to the server and start recording the media c := gortsplib.Client{} - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -58,7 +63,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-h265/main.go b/examples/client-publish-format-h265/main.go similarity index 70% rename from examples/client-publish-track-h265/main.go rename to examples/client-publish-format-h265/main.go index 5bc7307f..3fa9e113 100644 --- a/examples/client-publish-track-h265/main.go +++ b/examples/client-publish-format-h265/main.go @@ -4,13 +4,15 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/H265 packets with GStreamer -// 2. connect to a RTSP server, announce an H265 track +// 2. connect to a RTSP server, announce an H265 media // 3. route the packets from GStreamer to the server func main() { @@ -34,15 +36,18 @@ func main() { } log.Println("stream connected") - // create an H265 track - track := &gortsplib.TrackH265{ - PayloadType: 96, + // create a media that contains a H265 format + medi := &media.Media{ + Type: media.TypeVideo, + Formats: []format.Format{&format.H265{ + PayloadTyp: 96, + }}, } - // connect to the server and start publishing the track + // connect to the server and start recording the media c := gortsplib.Client{} - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -57,7 +62,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-lpcm/main.go b/examples/client-publish-format-lpcm/main.go similarity index 66% rename from examples/client-publish-track-lpcm/main.go rename to examples/client-publish-format-lpcm/main.go index 918f2279..6f71932d 100644 --- a/examples/client-publish-track-lpcm/main.go +++ b/examples/client-publish-format-lpcm/main.go @@ -4,13 +4,15 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/LPCM packets with GStreamer -// 2. connect to a RTSP server, announce an LPCM track +// 2. connect to a RTSP server, announce an LPCM media // 3. route the packets from GStreamer to the server func main() { @@ -33,19 +35,22 @@ func main() { } log.Println("stream connected") - // create an LPCM track - track := &gortsplib.TrackLPCM{ - PayloadType: 96, - BitDepth: 16, - SampleRate: 44100, - ChannelCount: 1, + // create a media that contains a LPCM format + medi := &media.Media{ + Type: media.TypeAudio, + Formats: []format.Format{&format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 44100, + ChannelCount: 1, + }}, } c := gortsplib.Client{} - // connect to the server and start publishing the track - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + // connect to the server and start recording the media + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -60,7 +65,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-mpeg4audio/main.go b/examples/client-publish-format-mpeg4audio/main.go similarity index 60% rename from examples/client-publish-track-mpeg4audio/main.go rename to examples/client-publish-format-mpeg4audio/main.go index 6435a912..fae9c508 100644 --- a/examples/client-publish-track-mpeg4audio/main.go +++ b/examples/client-publish-format-mpeg4audio/main.go @@ -4,14 +4,16 @@ import ( "log" "net" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/mpeg4audio" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/MPEG4-audio packets with GStreamer -// 2. connect to a RTSP server, announce an MPEG4-audio track +// 2. connect to a RTSP server, announce an MPEG4-audio media // 3. route the packets from GStreamer to the server func main() { @@ -34,23 +36,26 @@ func main() { } log.Println("stream connected") - // create an MPEG4-audio track - track := &gortsplib.TrackMPEG4Audio{ - PayloadType: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 48000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, + // create a media that contains a MPEG4-audio format + medi := &media.Media{ + Type: media.TypeAudio, + Formats: []format.Format{&format.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 48000, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }}, } - // connect to the server and start publishing the track + // connect to the server and start recording the media c := gortsplib.Client{} - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -65,7 +70,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-opus/main.go b/examples/client-publish-format-opus/main.go similarity index 67% rename from examples/client-publish-track-opus/main.go rename to examples/client-publish-format-opus/main.go index df771b4a..59887ce9 100644 --- a/examples/client-publish-track-opus/main.go +++ b/examples/client-publish-format-opus/main.go @@ -4,13 +4,15 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/Opus packets with GStreamer -// 2. connect to a RTSP server, announce an Opus track +// 2. connect to a RTSP server, announce an Opus media // 3. route the packets from GStreamer to the server func main() { @@ -33,18 +35,21 @@ func main() { } log.Println("stream connected") - // create an Opus track - track := &gortsplib.TrackOpus{ - PayloadType: 96, - SampleRate: 48000, - ChannelCount: 2, + // create a media that contains a Opus format + medi := &media.Media{ + Type: media.TypeAudio, + Formats: []format.Format{&format.Opus{ + PayloadTyp: 96, + SampleRate: 48000, + ChannelCount: 2, + }}, } c := gortsplib.Client{} - // connect to the server and start publishing the track - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + // connect to the server and start recording the media + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -59,7 +64,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-vp8/main.go b/examples/client-publish-format-vp8/main.go similarity index 69% rename from examples/client-publish-track-vp8/main.go rename to examples/client-publish-format-vp8/main.go index a3c78ee0..27a95fb1 100644 --- a/examples/client-publish-track-vp8/main.go +++ b/examples/client-publish-format-vp8/main.go @@ -4,13 +4,15 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/VP8 packets with GStreamer -// 2. connect to a RTSP server, announce an VP8 track +// 2. connect to a RTSP server, announce an VP8 media // 3. route the packets from GStreamer to the server func main() { @@ -34,15 +36,18 @@ func main() { } log.Println("stream connected") - // create a VP8 track - track := &gortsplib.TrackVP8{ - PayloadType: 96, + // create a media that contains a VP8 format + medi := &media.Media{ + Type: media.TypeVideo, + Formats: []format.Format{&format.VP8{ + PayloadTyp: 96, + }}, } - // connect to the server and start publishing the track + // connect to the server and start recording the media c := gortsplib.Client{} - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -57,7 +62,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-track-vp9/main.go b/examples/client-publish-format-vp9/main.go similarity index 69% rename from examples/client-publish-track-vp9/main.go rename to examples/client-publish-format-vp9/main.go index 24960415..5ad8798c 100644 --- a/examples/client-publish-track-vp9/main.go +++ b/examples/client-publish-format-vp9/main.go @@ -4,13 +4,15 @@ import ( "log" "net" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/VP9 packets with GStreamer -// 2. connect to a RTSP server, announce an VP9 track +// 2. connect to a RTSP server, announce an VP9 media // 3. route the packets from GStreamer to the server func main() { @@ -34,15 +36,18 @@ func main() { } log.Println("stream connected") - // create a VP9 track - track := &gortsplib.TrackVP9{ - PayloadType: 96, + // create a media that contains a VP9 format + medi := &media.Media{ + Type: media.TypeVideo, + Formats: []format.Format{&format.VP9{ + PayloadTyp: 96, + }}, } - // connect to the server and start publishing the track + // connect to the server and start recording the media c := gortsplib.Client{} - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -57,7 +62,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-options/main.go b/examples/client-publish-options/main.go index 8c29678c..9e71267b 100644 --- a/examples/client-publish-options/main.go +++ b/examples/client-publish-options/main.go @@ -5,14 +5,16 @@ import ( "net" "time" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. set additional client options // 2. generate RTP/H264 frames from a file with GStreamer -// 3. connect to a RTSP server, announce an H264 track +// 3. connect to a RTSP server, announce an H264 media // 4. write the frames to the server func main() { @@ -35,10 +37,13 @@ func main() { } log.Println("stream connected") - // create an H264 track - track := &gortsplib.TrackH264{ - PayloadType: 96, - PacketizationMode: 1, + // create a media that contains a H264 media + medi := &media.Media{ + Type: media.TypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + }}, } // Client allows to set additional client options @@ -51,9 +56,9 @@ func main() { WriteTimeout: 10 * time.Second, } - // connect to the server and start publishing the track - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + // connect to the server and start recording the media + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -68,7 +73,7 @@ func main() { } // route RTP packet to the server - err = c.WritePacketRTP(0, &pkt) + err = c.WritePacketRTP(medi, &pkt) if err != nil { panic(err) } diff --git a/examples/client-publish-pause/main.go b/examples/client-publish-pause/main.go index e98cfcb4..f7ca9c75 100644 --- a/examples/client-publish-pause/main.go +++ b/examples/client-publish-pause/main.go @@ -5,13 +5,15 @@ import ( "net" "time" - "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" "github.com/pion/rtp" ) // This example shows how to // 1. generate RTP/H264 frames from a file with GStreamer -// 2. connect to a RTSP server, announce an H264 track +// 2. connect to a RTSP server, announce an H264 media // 3. write the frames to the server for 5 seconds // 4. pause for 5 seconds // 5. repeat @@ -36,16 +38,19 @@ func main() { } log.Println("stream connected") - // create an H264 track - track := &gortsplib.TrackH264{ - PayloadType: 96, - PacketizationMode: 1, + // create a media that contains a H264 format + medi := &media.Media{ + Type: media.TypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + }}, } - // connect to the server and start publishing the track + // connect to the server and start recording the media c := gortsplib.Client{} - err = c.StartPublishing("rtsp://localhost:8554/mystream", - gortsplib.Tracks{track}) + err = c.StartRecording("rtsp://localhost:8554/mystream", + media.Medias{medi}) if err != nil { panic(err) } @@ -62,7 +67,7 @@ func main() { } // route RTP packet to the server - c.WritePacketRTP(0, &pkt) + c.WritePacketRTP(medi, &pkt) // read another RTP packet from source n, _, err = pc.ReadFrom(buf) diff --git a/examples/client-query/main.go b/examples/client-query/main.go index 7cd1ec00..951744af 100644 --- a/examples/client-query/main.go +++ b/examples/client-query/main.go @@ -3,13 +3,13 @@ package main import ( "log" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/url" ) // This example shows how to // 1. connect to a RTSP server -// 2. get and print informations about tracks published on a path. +// 2. get and print informations about medias published on a path. func main() { c := gortsplib.Client{} @@ -25,10 +25,10 @@ func main() { } defer c.Close() - tracks, _, _, err := c.Describe(u) + medias, _, _, err := c.Describe(u) if err != nil { panic(err) } - log.Printf("available tracks: %v\n", tracks) + log.Printf("available medias: %v\n", medias) } diff --git a/examples/client-read-format-g711/main.go b/examples/client-read-format-g711/main.go new file mode 100644 index 00000000..cb8cf0f0 --- /dev/null +++ b/examples/client-read-format-g711/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's a G711 media +// 3. get G711 frames of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the G711 media and format + var trak *format.G711 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // decode a G711 packet from the RTP packet + op, _, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + // print + log.Printf("received G711 frame of size %d\n", len(op)) + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-format-g722/main.go b/examples/client-read-format-g722/main.go new file mode 100644 index 00000000..1d9e32c4 --- /dev/null +++ b/examples/client-read-format-g722/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's a G722 media +// 3. get G722 frames of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the G722 media and format + var trak *format.G722 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // decode a G722 packet from the RTP packet + op, _, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + // print + log.Printf("received G722 frame of size %d\n", len(op)) + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-track-h264-convert-to-jpeg/h264decoder.go b/examples/client-read-format-h264-convert-to-jpeg/h264decoder.go similarity index 100% rename from examples/client-read-track-h264-convert-to-jpeg/h264decoder.go rename to examples/client-read-format-h264-convert-to-jpeg/h264decoder.go diff --git a/examples/client-read-track-h264-convert-to-jpeg/main.go b/examples/client-read-format-h264-convert-to-jpeg/main.go similarity index 71% rename from examples/client-read-track-h264-convert-to-jpeg/main.go rename to examples/client-read-format-h264-convert-to-jpeg/main.go index 971c8e4d..b341c97b 100644 --- a/examples/client-read-track-h264-convert-to-jpeg/main.go +++ b/examples/client-read-format-h264-convert-to-jpeg/main.go @@ -8,13 +8,15 @@ import ( "strconv" "time" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" ) // This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's a H264 track +// 1. connect to a RTSP server +// 2. check if there's a H264 media // 3. decode H264 into RGBA frames // 4. encode the frames into JPEG images and save them on disk @@ -54,27 +56,21 @@ func main() { } defer c.Close() - // find published tracks - tracks, baseURL, _, err := c.Describe(u) + // find published medias + medias, baseURL, _, err := c.Describe(u) if err != nil { panic(err) } - // find the H264 track - track := func() *gortsplib.TrackH264 { - for _, track := range tracks { - if track, ok := track.(*gortsplib.TrackH264); ok { - return track - } - } - return nil - }() - if track == nil { - panic("H264 track not found") + // find the H264 media and format + var trak *format.H264 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") } // setup RTP/H264->H264 decoder - rtpDec := track.CreateDecoder() + rtpDec := trak.CreateDecoder() // setup H264->raw frames decoder h264RawDec, err := newH264Decoder() @@ -84,20 +80,26 @@ func main() { defer h264RawDec.close() // if SPS and PPS are present into the SDP, send them to the decoder - sps := track.SafeSPS() + sps := trak.SafeSPS() if sps != nil { h264RawDec.decode(sps) } - pps := track.SafePPS() + pps := trak.SafePPS() if pps != nil { h264RawDec.decode(pps) } + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + // called when a RTP packet arrives saveCount := 0 - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { // convert RTP packets into NALUs - nalus, _, err := rtpDec.Decode(ctx.Packet) + nalus, _, err := rtpDec.Decode(pkt) if err != nil { return } @@ -126,10 +128,10 @@ func main() { os.Exit(1) } } - } + }) - // setup and read the H264 track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) + // start playing + _, err = c.Play(nil) if err != nil { panic(err) } diff --git a/examples/client-read-format-h264-save-to-disk/main.go b/examples/client-read-format-h264-save-to-disk/main.go new file mode 100644 index 00000000..e5f0648b --- /dev/null +++ b/examples/client-read-format-h264-save-to-disk/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's a H264 media +// 3. save the content of the H264 media into a file in MPEG-TS format + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the H264 media and format + var trak *format.H264 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup RTP/H264->H264 decoder + rtpDec := trak.CreateDecoder() + + // setup H264->MPEGTS muxer + mpegtsMuxer, err := newMPEGTSMuxer(trak.SafeSPS(), trak.SafePPS()) + if err != nil { + panic(err) + } + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // convert RTP packets into NALUs + nalus, pts, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + // encode H264 NALUs into MPEG-TS + mpegtsMuxer.encode(nalus, pts) + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-track-h264-save-to-disk/mpegtsmuxer.go b/examples/client-read-format-h264-save-to-disk/mpegtsmuxer.go similarity index 98% rename from examples/client-read-track-h264-save-to-disk/mpegtsmuxer.go rename to examples/client-read-format-h264-save-to-disk/mpegtsmuxer.go index 53b3d31c..db499377 100644 --- a/examples/client-read-track-h264-save-to-disk/mpegtsmuxer.go +++ b/examples/client-read-format-h264-save-to-disk/mpegtsmuxer.go @@ -7,7 +7,7 @@ import ( "os" "time" - "github.com/aler9/gortsplib/pkg/h264" + "github.com/aler9/gortsplib/v2/pkg/h264" "github.com/asticode/go-astits" ) diff --git a/examples/client-read-track-h264/h264decoder.go b/examples/client-read-format-h264/h264decoder.go similarity index 100% rename from examples/client-read-track-h264/h264decoder.go rename to examples/client-read-format-h264/h264decoder.go diff --git a/examples/client-read-track-h264/main.go b/examples/client-read-format-h264/main.go similarity index 63% rename from examples/client-read-track-h264/main.go rename to examples/client-read-format-h264/main.go index 73cc0434..0bfb0b18 100644 --- a/examples/client-read-track-h264/main.go +++ b/examples/client-read-format-h264/main.go @@ -3,13 +3,15 @@ package main import ( "log" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" ) // This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's an H264 track +// 1. connect to a RTSP server +// 2. check if there's an H264 media // 3. decode H264 into RGBA frames // This example requires the ffmpeg libraries, that can be installed in this way: @@ -31,27 +33,21 @@ func main() { } defer c.Close() - // find published tracks - tracks, baseURL, _, err := c.Describe(u) + // find published medias + medias, baseURL, _, err := c.Describe(u) if err != nil { panic(err) } - // find the H264 track - track := func() *gortsplib.TrackH264 { - for _, track := range tracks { - if track, ok := track.(*gortsplib.TrackH264); ok { - return track - } - } - return nil - }() - if track == nil { - panic("H264 track not found") + // find the H264 media and format + var trak *format.H264 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") } // setup RTP/H264->H264 decoder - rtpDec := track.CreateDecoder() + rtpDec := trak.CreateDecoder() // setup H264->raw frames decoder h264RawDec, err := newH264Decoder() @@ -61,19 +57,25 @@ func main() { defer h264RawDec.close() // if SPS and PPS are present into the SDP, send them to the decoder - sps := track.SafeSPS() + sps := trak.SafeSPS() if sps != nil { h264RawDec.decode(sps) } - pps := track.SafePPS() + pps := trak.SafePPS() if pps != nil { h264RawDec.decode(pps) } + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { // convert RTP packets into NALUs - nalus, _, err := rtpDec.Decode(ctx.Packet) + nalus, _, err := rtpDec.Decode(pkt) if err != nil { return } @@ -92,10 +94,10 @@ func main() { log.Printf("decoded frame with size %v", img.Bounds().Max) } - } + }) - // setup and read the H264 track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) + // start playing + _, err = c.Play(nil) if err != nil { panic(err) } diff --git a/examples/client-read-format-h265/main.go b/examples/client-read-format-h265/main.go new file mode 100644 index 00000000..d9ae0690 --- /dev/null +++ b/examples/client-read-format-h265/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's an H265 media +// 3. get access units of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the H265 media and format + var trak *format.H265 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup RTP/H265->H265 decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // convert RTP packets into NALUs + nalus, pts, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + for _, nalu := range nalus { + log.Printf("received NALU with PTS %v and size %d\n", pts, len(nalu)) + } + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-format-lpcm/main.go b/examples/client-read-format-lpcm/main.go new file mode 100644 index 00000000..fc0caf1c --- /dev/null +++ b/examples/client-read-format-lpcm/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's an LPCM media +// 3. get LPCM packets of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the LPCM media and format + var trak *format.LPCM + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // decode LPCM samples from the RTP packet + op, _, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + // print + log.Printf("received LPCM samples of size %d\n", len(op)) + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-format-mpeg4audio/main.go b/examples/client-read-format-mpeg4audio/main.go new file mode 100644 index 00000000..0b18f118 --- /dev/null +++ b/examples/client-read-format-mpeg4audio/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's an MPEG4-audio media +// 3. get access units of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the MPEG4-audio media and format + var trak *format.MPEG4Audio + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // decode MPEG4-audio AUs from the RTP packet + aus, _, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + // print AUs + for _, au := range aus { + log.Printf("received MPEG4-audio AU of size %d\n", len(au)) + } + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-format-opus/main.go b/examples/client-read-format-opus/main.go new file mode 100644 index 00000000..2d596e17 --- /dev/null +++ b/examples/client-read-format-opus/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's an Opus media +// 3. get Opus packets of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the Opus media and format + var trak *format.Opus + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // decode an Opus packet from the RTP packet + op, _, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + // print + log.Printf("received Opus packet of size %d\n", len(op)) + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-format-vp8/main.go b/examples/client-read-format-vp8/main.go new file mode 100644 index 00000000..8a2f0dd4 --- /dev/null +++ b/examples/client-read-format-vp8/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's an VP8 media +// 3. get access units of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the VP8 media and format + var trak *format.VP8 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // decode a VP8 frame from the RTP packet + vf, _, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + log.Printf("received frame of size %d\n", len(vf)) + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-format-vp9/main.go b/examples/client-read-format-vp9/main.go new file mode 100644 index 00000000..e19eebd4 --- /dev/null +++ b/examples/client-read-format-vp9/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "log" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. connect to a RTSP server +// 2. check if there's an VP9 media +// 3. get access units of that media + +func main() { + c := gortsplib.Client{} + + // parse URL + u, err := url.Parse("rtsp://localhost:8554/mystream") + if err != nil { + panic(err) + } + + // connect to the server + err = c.Start(u.Scheme, u.Host) + if err != nil { + panic(err) + } + defer c.Close() + + // find published medias + medias, baseURL, _, err := c.Describe(u) + if err != nil { + panic(err) + } + + // find the VP9 media and format + var trak *format.VP9 + medi := medias.Find(&trak) + if medi == nil { + panic("media not found") + } + + // setup decoder + rtpDec := trak.CreateDecoder() + + // setup the chosen media only + _, err = c.Setup(medi, baseURL, 0, 0) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) { + // decode a VP9 frame from the RTP packet + vf, _, err := rtpDec.Decode(pkt) + if err != nil { + return + } + + log.Printf("received frame of size %d\n", len(vf)) + }) + + // start playing + _, err = c.Play(nil) + if err != nil { + panic(err) + } + + // wait until a fatal error + panic(c.Wait()) +} diff --git a/examples/client-read-options/main.go b/examples/client-read-options/main.go index 5a582d5a..6a59f997 100644 --- a/examples/client-read-options/main.go +++ b/examples/client-read-options/main.go @@ -4,13 +4,17 @@ import ( "log" "time" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtcp" + "github.com/pion/rtp" ) // This example shows how to // 1. set additional client options -// 2. connect to a RTSP server and read all tracks on a path +// 2. connect to a RTSP server and read all medias on a path func main() { // Client allows to set additional client options @@ -21,14 +25,6 @@ func main() { ReadTimeout: 10 * time.Second, // timeout of write operations WriteTimeout: 10 * time.Second, - // called when a RTP packet arrives - OnPacketRTP: func(ctx *gortsplib.ClientOnPacketRTPCtx) { - log.Printf("RTP packet from track %d, payload type %d\n", ctx.TrackID, ctx.Packet.Header.PayloadType) - }, - // called when a RTCP packet arrives - OnPacketRTCP: func(ctx *gortsplib.ClientOnPacketRTCPCtx) { - log.Printf("RTCP packet from track %d, type %T\n", ctx.TrackID, ctx.Packet) - }, } // parse URL @@ -44,14 +40,30 @@ func main() { } defer c.Close() - // find published tracks - tracks, baseURL, _, err := c.Describe(u) + // find published medias + medias, baseURL, _, err := c.Describe(u) if err != nil { panic(err) } - // setup and read all tracks - err = c.SetupAndPlay(tracks, baseURL) + // setup all medias + err = c.SetupAll(medias, baseURL) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + log.Printf("RTP packet from media %v\n", medi) + }) + + // called when a RTCP packet arrives + c.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) { + log.Printf("RTCP packet from media %v, type %T\n", medi, pkt) + }) + + // start playing + _, err = c.Play(nil) if err != nil { panic(err) } diff --git a/examples/client-read-partial/main.go b/examples/client-read-partial/main.go deleted file mode 100644 index bff3f2d3..00000000 --- a/examples/client-read-partial/main.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server -// 2. get tracks published on a path -// 3. read only the H264 track - -func main() { - c := gortsplib.Client{ - // called when a RTP packet arrives - OnPacketRTP: func(ctx *gortsplib.ClientOnPacketRTPCtx) { - log.Printf("RTP packet from track %d, payload type %d\n", ctx.TrackID, ctx.Packet.Header.PayloadType) - }, - // called when a RTCP packet arrives - OnPacketRTCP: func(ctx *gortsplib.ClientOnPacketRTCPCtx) { - log.Printf("RTCP packet from track %d, type %T\n", ctx.TrackID, ctx.Packet) - }, - } - - u, err := url.Parse("rtsp://myserver/mypath") - if err != nil { - panic(err) - } - - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the H264 track - h264Track := func() gortsplib.Track { - for _, t := range tracks { - if _, ok := t.(*gortsplib.TrackH264); ok { - return t - } - } - return nil - }() - if h264Track == nil { - panic(fmt.Errorf("H264 track not found")) - } - - // setup and play the H264 track only - err = c.SetupAndPlay(gortsplib.Tracks{h264Track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-pause/main.go b/examples/client-read-pause/main.go index 8bb7bf15..d542aad1 100644 --- a/examples/client-read-pause/main.go +++ b/examples/client-read-pause/main.go @@ -4,27 +4,22 @@ import ( "log" "time" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtcp" + "github.com/pion/rtp" ) // This example shows how to -// 1. connect to a RTSP server and read all tracks on a path +// 1. connect to a RTSP server and read all medias on a path // 2. wait for 5 seconds // 3. pause for 5 seconds // 4. repeat func main() { - c := gortsplib.Client{ - // called when a RTP packet arrives - OnPacketRTP: func(ctx *gortsplib.ClientOnPacketRTPCtx) { - log.Printf("RTP packet from track %d, payload type %d\n", ctx.TrackID, ctx.Packet.Header.PayloadType) - }, - // called when a RTCP packet arrives - OnPacketRTCP: func(ctx *gortsplib.ClientOnPacketRTCPCtx) { - log.Printf("RTCP packet from track %d, type %T\n", ctx.TrackID, ctx.Packet) - }, - } + c := gortsplib.Client{} // parse URL u, err := url.Parse("rtsp://localhost:8554/mystream") @@ -39,14 +34,30 @@ func main() { } defer c.Close() - // find published tracks - tracks, baseURL, _, err := c.Describe(u) + // find published medias + medias, baseURL, _, err := c.Describe(u) if err != nil { panic(err) } - // setup and read all tracks - err = c.SetupAndPlay(tracks, baseURL) + // setup all medias + err = c.SetupAll(medias, baseURL) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + log.Printf("RTP packet from media %v\n", medi) + }) + + // called when a RTCP packet arrives + c.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) { + log.Printf("RTCP packet from media %v, type %T\n", medi, pkt) + }) + + // start playing + _, err = c.Play(nil) if err != nil { panic(err) } diff --git a/examples/client-read-republish/main.go b/examples/client-read-republish/main.go index cdd7f032..b37def8f 100644 --- a/examples/client-read-republish/main.go +++ b/examples/client-read-republish/main.go @@ -3,13 +3,16 @@ package main import ( "log" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtp" ) // This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. re-publish all tracks on another path. +// 1. connect to a RTSP server and read all medias on a path +// 2. re-publish all medias on another path. func main() { reader := gortsplib.Client{} @@ -27,30 +30,35 @@ func main() { } defer reader.Close() - // find published tracks - tracks, baseURL, _, err := reader.Describe(sourceURL) + // find published medias + medias, baseURL, _, err := reader.Describe(sourceURL) if err != nil { panic(err) } - log.Printf("republishing %d tracks", len(tracks)) + log.Printf("republishing %d medias", len(medias)) + // connect to the server and start recording the same medias publisher := gortsplib.Client{} - - // connect to the server and start publishing - err = publisher.StartPublishing("rtsp://localhost:8554/mystream2", tracks) + err = publisher.StartRecording("rtsp://localhost:8554/mystream2", medias) if err != nil { panic(err) } defer publisher.Close() - // called when a RTP packet arrives - reader.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - publisher.WritePacketRTP(ctx.TrackID, ctx.Packet) + // setup all medias + err = reader.SetupAll(medias, baseURL) + if err != nil { + panic(err) } - // setup and read all tracks - err = reader.SetupAndPlay(tracks, baseURL) + // read RTP packets from reader and write them to publisher + reader.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + publisher.WritePacketRTP(medi, pkt) + }) + + // start playing + _, err = reader.Play(nil) if err != nil { panic(err) } diff --git a/examples/client-read-track-g711/main.go b/examples/client-read-track-g711/main.go deleted file mode 100644 index 1f971fc5..00000000 --- a/examples/client-read-track-g711/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's a PCMA track -// 3. get PCMA frames of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the PCMA track - track := func() *gortsplib.TrackG711 { - for _, track := range tracks { - if tt, ok := track.(*gortsplib.TrackG711); ok { - return tt - } - } - return nil - }() - if track == nil { - panic("PCMA track not found") - } - - // setup decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode an PCMA packet from the RTP packet - op, _, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - // print - log.Printf("received PCMA frame of size %d\n", len(op)) - } - - // setup and read the PCMA track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-g722/main.go b/examples/client-read-track-g722/main.go deleted file mode 100644 index 315e28e4..00000000 --- a/examples/client-read-track-g722/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's a G722 track -// 3. get G722 frames of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the G722 track - track := func() *gortsplib.TrackG722 { - for _, track := range tracks { - if tt, ok := track.(*gortsplib.TrackG722); ok { - return tt - } - } - return nil - }() - if track == nil { - panic("G722 track not found") - } - - // setup decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode an G722 packet from the RTP packet - op, _, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - // print - log.Printf("received G722 frame of size %d\n", len(op)) - } - - // setup and read the G722 track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-h264-save-to-disk/main.go b/examples/client-read-track-h264-save-to-disk/main.go deleted file mode 100644 index b53ffdbb..00000000 --- a/examples/client-read-track-h264-save-to-disk/main.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's a H264 track -// 3. save the content of the H264 track into a file in MPEG-TS format - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the H264 track - track := func() *gortsplib.TrackH264 { - for _, track := range tracks { - if track, ok := track.(*gortsplib.TrackH264); ok { - return track - } - } - return nil - }() - if track == nil { - panic("H264 track not found") - } - - // setup RTP/H264->H264 decoder - rtpDec := track.CreateDecoder() - - // setup H264->MPEGTS muxer - mpegtsMuxer, err := newMPEGTSMuxer(track.SafeSPS(), track.SafePPS()) - if err != nil { - panic(err) - } - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // convert RTP packets into NALUs - nalus, pts, err := rtpDec.Decode(ctx.Packet) - if err != nil { - return - } - - // encode H264 NALUs into MPEG-TS - mpegtsMuxer.encode(nalus, pts) - } - - // setup and read the H264 track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-h265/main.go b/examples/client-read-track-h265/main.go deleted file mode 100644 index 40548cde..00000000 --- a/examples/client-read-track-h265/main.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's an H265 track -// 3. get access units of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the H265 track - track := func() *gortsplib.TrackH265 { - for _, track := range tracks { - if track, ok := track.(*gortsplib.TrackH265); ok { - return track - } - } - return nil - }() - if track == nil { - panic("H265 track not found") - } - - // setup RTP/H265->H265 decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // convert RTP packets into NALUs - nalus, pts, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - for _, nalu := range nalus { - log.Printf("received NALU with PTS %v and size %d\n", pts, len(nalu)) - } - } - - // setup and read the H265 track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-lpcm/main.go b/examples/client-read-track-lpcm/main.go deleted file mode 100644 index f0c10719..00000000 --- a/examples/client-read-track-lpcm/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's an LPCM track -// 3. get LPCM packets of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the LPCM track - track := func() *gortsplib.TrackLPCM { - for _, track := range tracks { - if tt, ok := track.(*gortsplib.TrackLPCM); ok { - return tt - } - } - return nil - }() - if track == nil { - panic("LPCM track not found") - } - - // setup decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode LPCM samples from the RTP packet - op, _, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - // print - log.Printf("received LPCM samples of size %d\n", len(op)) - } - - // setup and read the LPCM track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-mpeg4audio/main.go b/examples/client-read-track-mpeg4audio/main.go deleted file mode 100644 index d13f11ce..00000000 --- a/examples/client-read-track-mpeg4audio/main.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's an MPEG4-audio track -// 3. get access units of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the MPEG4-audio track - track := func() *gortsplib.TrackMPEG4Audio { - for _, track := range tracks { - if tt, ok := track.(*gortsplib.TrackMPEG4Audio); ok { - return tt - } - } - return nil - }() - if track == nil { - panic("MPEG4-audio track not found") - } - - // setup decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode MPEG4-audio AUs from the RTP packet - aus, _, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - // print AUs - for _, au := range aus { - log.Printf("received MPEG4-audio AU of size %d\n", len(au)) - } - } - - // setup and read the MPEG4-audio track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-opus/main.go b/examples/client-read-track-opus/main.go deleted file mode 100644 index bca1370f..00000000 --- a/examples/client-read-track-opus/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's an Opus track -// 3. get Opus packets of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the Opus track - track := func() *gortsplib.TrackOpus { - for _, track := range tracks { - if tt, ok := track.(*gortsplib.TrackOpus); ok { - return tt - } - } - return nil - }() - if track == nil { - panic("Opus track not found") - } - - // setup decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode an Opus packet from the RTP packet - op, _, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - // print - log.Printf("received Opus packet of size %d\n", len(op)) - } - - // setup and read the Opus track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-vp8/main.go b/examples/client-read-track-vp8/main.go deleted file mode 100644 index 98910437..00000000 --- a/examples/client-read-track-vp8/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's an VP8 track -// 3. get access units of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the VP8 track - track := func() *gortsplib.TrackVP8 { - for _, track := range tracks { - if tt, ok := track.(*gortsplib.TrackVP8); ok { - return tt - } - } - return nil - }() - if track == nil { - panic("VP8 track not found") - } - - // setup decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode a VP8 frame from the RTP packet - vf, _, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - log.Printf("received frame of size %d\n", len(vf)) - } - - // setup and read the VP8 track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read-track-vp9/main.go b/examples/client-read-track-vp9/main.go deleted file mode 100644 index 28c1b392..00000000 --- a/examples/client-read-track-vp9/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "log" - - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" -) - -// This example shows how to -// 1. connect to a RTSP server and read all tracks on a path -// 2. check if there's an VP9 track -// 3. get access units of that track - -func main() { - c := gortsplib.Client{} - - // parse URL - u, err := url.Parse("rtsp://localhost:8554/mystream") - if err != nil { - panic(err) - } - - // connect to the server - err = c.Start(u.Scheme, u.Host) - if err != nil { - panic(err) - } - defer c.Close() - - // find published tracks - tracks, baseURL, _, err := c.Describe(u) - if err != nil { - panic(err) - } - - // find the VP9 track - track := func() *gortsplib.TrackVP9 { - for _, track := range tracks { - if tt, ok := track.(*gortsplib.TrackVP9); ok { - return tt - } - } - return nil - }() - if track == nil { - panic("VP9 track not found") - } - - // setup decoder - dec := track.CreateDecoder() - - // called when a RTP packet arrives - c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) { - // decode a VP9 frame from the RTP packet - vf, _, err := dec.Decode(ctx.Packet) - if err != nil { - return - } - - log.Printf("received frame of size %d\n", len(vf)) - } - - // setup and read the VP9 track only - err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL) - if err != nil { - panic(err) - } - - // wait until a fatal error - panic(c.Wait()) -} diff --git a/examples/client-read/main.go b/examples/client-read/main.go index 823c336d..703fd782 100644 --- a/examples/client-read/main.go +++ b/examples/client-read/main.go @@ -3,24 +3,19 @@ package main import ( "log" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/url" + "github.com/pion/rtcp" + "github.com/pion/rtp" ) // This example shows how to connect to a RTSP server -// and read all tracks on a path. +// and read all medias on a path. func main() { - c := gortsplib.Client{ - // called when a RTP packet arrives - OnPacketRTP: func(ctx *gortsplib.ClientOnPacketRTPCtx) { - log.Printf("RTP packet from track %d, payload type %d\n", ctx.TrackID, ctx.Packet.Header.PayloadType) - }, - // called when a RTCP packet arrives - OnPacketRTCP: func(ctx *gortsplib.ClientOnPacketRTCPCtx) { - log.Printf("RTCP packet from track %d, type %T\n", ctx.TrackID, ctx.Packet) - }, - } + c := gortsplib.Client{} // parse URL u, err := url.Parse("rtsp://localhost:8554/mystream") @@ -35,14 +30,30 @@ func main() { } defer c.Close() - // find published tracks - tracks, baseURL, _, err := c.Describe(u) + // find published medias + medias, baseURL, _, err := c.Describe(u) if err != nil { panic(err) } - // setup and read all tracks - err = c.SetupAndPlay(tracks, baseURL) + // setup all medias + err = c.SetupAll(medias, baseURL) + if err != nil { + panic(err) + } + + // called when a RTP packet arrives + c.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + log.Printf("RTP packet from media %v\n", medi) + }) + + // called when a RTCP packet arrives + c.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) { + log.Printf("RTCP packet from media %v, type %T\n", medi, pkt) + }) + + // start playing + _, err = c.Play(nil) if err != nil { panic(err) } diff --git a/examples/server-h264-save-to-disk/main.go b/examples/server-h264-save-to-disk/main.go index d8c32394..a3acfed5 100644 --- a/examples/server-h264-save-to-disk/main.go +++ b/examples/server-h264-save-to-disk/main.go @@ -5,21 +5,25 @@ import ( "log" "sync" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtph264" + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtph264" ) // This example shows how to // 1. create a RTSP server which accepts plain connections -// 2. allow a single client to publish a stream, containing a H264 track, with TCP or UDP -// 3. save the content of the H264 track into a file in MPEG-TS format +// 2. allow a single client to publish a stream, containing a H264 media, with TCP or UDP +// 3. save the content of the H264 media into a file in MPEG-TS format type serverHandler struct { mutex sync.Mutex publisher *gortsplib.ServerSession - h264TrackID int - h264track *gortsplib.TrackH264 + media *media.Media + format *format.H264 rtpDec *rtph264.Decoder mpegtsMuxer *mpegtsMuxer } @@ -50,7 +54,7 @@ func (sh *serverHandler) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionClo sh.mpegtsMuxer.close() } -// called after receiving an ANNOUNCE request. +// called when receiving an ANNOUNCE request. func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { log.Printf("announce request") @@ -62,26 +66,20 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( sh.mpegtsMuxer.close() } - // find the H264 track - h264TrackID, h264track := func() (int, *gortsplib.TrackH264) { - for i, track := range ctx.Tracks { - if h264track, ok := track.(*gortsplib.TrackH264); ok { - return i, h264track - } - } - return -1, nil - }() - if h264TrackID < 0 { + // find the H264 media and format + var trak *format.H264 + medi := ctx.Medias.Find(&trak) + if medi == nil { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("H264 track not found") + }, fmt.Errorf("H264 media not found") } // setup RTP/H264->H264 decoder - rtpDec := h264track.CreateDecoder() + rtpDec := trak.CreateDecoder() // setup H264->MPEGTS muxer - mpegtsMuxer, err := newMPEGTSMuxer(h264track.SafeSPS(), h264track.SafePPS()) + mpegtsMuxer, err := newMPEGTSMuxer(trak.SafeSPS(), trak.SafePPS()) if err != nil { return &base.Response{ StatusCode: base.StatusBadRequest, @@ -89,7 +87,8 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( } sh.publisher = ctx.Session - sh.h264TrackID = h264TrackID + sh.media = medi + sh.format = trak sh.rtpDec = rtpDec sh.mpegtsMuxer = mpegtsMuxer @@ -98,7 +97,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( }, nil } -// called after receiving a SETUP request. +// called when receiving a SETUP request. func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { log.Printf("setup request") @@ -107,33 +106,26 @@ func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base. }, nil, nil } -// called after receiving a RECORD request. +// called when receiving a RECORD request. func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { log.Printf("record request") + // called when receiving a RTP packet + ctx.Session.OnPacketRTP(sh.media, sh.format, func(pkt *rtp.Packet) { + nalus, pts, err := sh.rtpDec.Decode(pkt) + if err != nil { + return + } + + // encode H264 NALUs into MPEG-TS + sh.mpegtsMuxer.encode(nalus, pts) + }) + return &base.Response{ StatusCode: base.StatusOK, }, nil } -// called after receiving a RTP packet. -func (sh *serverHandler) OnPacketRTP(ctx *gortsplib.ServerHandlerOnPacketRTPCtx) { - sh.mutex.Lock() - defer sh.mutex.Unlock() - - if ctx.TrackID != sh.h264TrackID { - return - } - - nalus, pts, err := sh.rtpDec.Decode(ctx.Packet) - if err != nil { - return - } - - // encode H264 NALUs into MPEG-TS - sh.mpegtsMuxer.encode(nalus, pts) -} - func main() { // configure server s := &gortsplib.Server{ diff --git a/examples/server-h264-save-to-disk/mpegtsmuxer.go b/examples/server-h264-save-to-disk/mpegtsmuxer.go index 53b3d31c..db499377 100644 --- a/examples/server-h264-save-to-disk/mpegtsmuxer.go +++ b/examples/server-h264-save-to-disk/mpegtsmuxer.go @@ -7,7 +7,7 @@ import ( "os" "time" - "github.com/aler9/gortsplib/pkg/h264" + "github.com/aler9/gortsplib/v2/pkg/h264" "github.com/asticode/go-astits" ) diff --git a/examples/server-tls/main.go b/examples/server-tls/main.go index 890a2a77..0d88dfd7 100644 --- a/examples/server-tls/main.go +++ b/examples/server-tls/main.go @@ -5,8 +5,12 @@ import ( "log" "sync" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/base" + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" ) // This example shows how to @@ -50,7 +54,7 @@ func (sh *serverHandler) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionClo } } -// called after receiving a DESCRIBE request. +// called when receiving a DESCRIBE request. func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { log.Printf("describe request") @@ -64,13 +68,13 @@ func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) ( }, nil, nil } - // send the track list that is being published to the client + // send medias that are being published to the client return &base.Response{ StatusCode: base.StatusOK, }, sh.stream, nil } -// called after receiving an ANNOUNCE request. +// called when receiving an ANNOUNCE request. func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { log.Printf("announce request") @@ -84,7 +88,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( } // create the stream and save the publisher - sh.stream = gortsplib.NewServerStream(ctx.Tracks) + sh.stream = gortsplib.NewServerStream(ctx.Medias) sh.publisher = ctx.Session return &base.Response{ @@ -92,7 +96,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( }, nil } -// called after receiving a SETUP request. +// called when receiving a SETUP request. func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { log.Printf("setup request") @@ -108,7 +112,7 @@ func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base. }, sh.stream, nil } -// called after receiving a PLAY request. +// called when receiving a PLAY request. func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { log.Printf("play request") @@ -117,26 +121,21 @@ func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Re }, nil } -// called after receiving a RECORD request. +// called when receiving a RECORD request. func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { log.Printf("record request") + // called when receiving a RTP packet + ctx.Session.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + // route the RTP packet to all readers + sh.stream.WritePacketRTP(medi, pkt) + }) + return &base.Response{ StatusCode: base.StatusOK, }, nil } -// called after receiving a RTP packet. -func (sh *serverHandler) OnPacketRTP(ctx *gortsplib.ServerHandlerOnPacketRTPCtx) { - sh.mutex.Lock() - defer sh.mutex.Unlock() - - // if we are the publisher, route the RTP packet to all readers - if ctx.Session == sh.publisher { - sh.stream.WritePacketRTP(ctx.TrackID, ctx.Packet) - } -} - func main() { // setup certificates - they can be generated with // openssl genrsa -out server.key 2048 diff --git a/examples/server/main.go b/examples/server/main.go index 2c3cbea4..f35ddbd9 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -4,8 +4,12 @@ import ( "log" "sync" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/base" + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" ) // This example shows how to @@ -49,7 +53,7 @@ func (sh *serverHandler) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionClo } } -// called after receiving a DESCRIBE request. +// called when receiving a DESCRIBE request. func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) { log.Printf("describe request") @@ -63,13 +67,13 @@ func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) ( }, nil, nil } - // send the track list that is being published to the client + // send medias that are being published to the client return &base.Response{ StatusCode: base.StatusOK, }, sh.stream, nil } -// called after receiving an ANNOUNCE request. +// called when receiving an ANNOUNCE request. func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) { log.Printf("announce request") @@ -83,7 +87,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( } // create the stream and save the publisher - sh.stream = gortsplib.NewServerStream(ctx.Tracks) + sh.stream = gortsplib.NewServerStream(ctx.Medias) sh.publisher = ctx.Session return &base.Response{ @@ -91,7 +95,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) ( }, nil } -// called after receiving a SETUP request. +// called when receiving a SETUP request. func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) { log.Printf("setup request") @@ -107,7 +111,7 @@ func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base. }, sh.stream, nil } -// called after receiving a PLAY request. +// called when receiving a PLAY request. func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) { log.Printf("play request") @@ -116,26 +120,21 @@ func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Re }, nil } -// called after receiving a RECORD request. +// called when receiving a RECORD request. func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { log.Printf("record request") + // called when receiving a RTP packet + ctx.Session.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + // route the RTP packet to all readers + sh.stream.WritePacketRTP(medi, pkt) + }) + return &base.Response{ StatusCode: base.StatusOK, }, nil } -// called after receiving a RTP packet. -func (sh *serverHandler) OnPacketRTP(ctx *gortsplib.ServerHandlerOnPacketRTPCtx) { - sh.mutex.Lock() - defer sh.mutex.Unlock() - - // if we are the publisher, route the RTP packet to all readers - if ctx.Session == sh.publisher { - sh.stream.WritePacketRTP(ctx.TrackID, ctx.Packet) - } -} - func main() { // configure server s := &gortsplib.Server{ diff --git a/go.mod b/go.mod index 59a134c7..3bd82795 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/aler9/gortsplib +module github.com/aler9/gortsplib/v2 go 1.17 diff --git a/internal/highleveltests/server_test.go b/internal/highleveltests/server_test.go index 576f3e88..a5856f61 100644 --- a/internal/highleveltests/server_test.go +++ b/internal/highleveltests/server_test.go @@ -14,10 +14,13 @@ import ( "testing" "time" + "github.com/pion/rtp" "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/media" ) var serverCert = []byte(`-----BEGIN CERTIFICATE----- @@ -85,8 +88,6 @@ type testServerHandler struct { onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) onRecord func(*gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) onPause func(*gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) - onPacketRTP func(*gortsplib.ServerHandlerOnPacketRTPCtx) - onPacketRTCP func(*gortsplib.ServerHandlerOnPacketRTCPCtx) onSetParameter func(*gortsplib.ServerHandlerOnSetParameterCtx) (*base.Response, error) onGetParameter func(*gortsplib.ServerHandlerOnGetParameterCtx) (*base.Response, error) } @@ -157,18 +158,6 @@ func (sh *testServerHandler) OnPause(ctx *gortsplib.ServerHandlerOnPauseCtx) (*b return nil, fmt.Errorf("unimplemented") } -func (sh *testServerHandler) OnPacketRTP(ctx *gortsplib.ServerHandlerOnPacketRTPCtx) { - if sh.onPacketRTP != nil { - sh.onPacketRTP(ctx) - } -} - -func (sh *testServerHandler) OnPacketRTCP(ctx *gortsplib.ServerHandlerOnPacketRTCPCtx) { - if sh.onPacketRTCP != nil { - sh.onPacketRTCP(ctx) - } -} - func (sh *testServerHandler) OnSetParameter(ctx *gortsplib.ServerHandlerOnSetParameterCtx) (*base.Response, error) { if sh.onSetParameter != nil { return sh.onSetParameter(ctx) @@ -238,7 +227,7 @@ func buildImage(image string) error { return ecmd.Run() } -func TestServerPublishRead(t *testing.T) { +func TestServerRecordRead(t *testing.T) { files, err := os.ReadDir("images") require.NoError(t, err) @@ -339,7 +328,7 @@ func TestServerPublishRead(t *testing.T) { }, fmt.Errorf("someone is already publishing") } - stream = gortsplib.NewServerStream(ctx.Tracks) + stream = gortsplib.NewServerStream(ctx.Medias) publisher = ctx.Session return &base.Response{ @@ -396,18 +385,14 @@ func TestServerPublishRead(t *testing.T) { }, fmt.Errorf("invalid query (%s)", ctx.Query) } + ctx.Session.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + stream.WritePacketRTP(medi, pkt) + }) + return &base.Response{ StatusCode: base.StatusOK, }, nil }, - onPacketRTP: func(ctx *gortsplib.ServerHandlerOnPacketRTPCtx) { - mutex.Lock() - defer mutex.Unlock() - - if ctx.Session == publisher { - stream.WritePacketRTP(ctx.TrackID, ctx.Packet) - } - }, }, RTSPAddress: "localhost:8554", } diff --git a/pkg/auth/package_test.go b/pkg/auth/package_test.go index ad724352..f513d2c0 100644 --- a/pkg/auth/package_test.go +++ b/pkg/auth/package_test.go @@ -5,9 +5,9 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/headers" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/url" ) func mustParseURL(s string) *url.URL { diff --git a/pkg/auth/sender.go b/pkg/auth/sender.go index 7e982374..11fc441c 100644 --- a/pkg/auth/sender.go +++ b/pkg/auth/sender.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/headers" ) // Sender allows to generate credentials for a Validator. diff --git a/pkg/auth/sender_test.go b/pkg/auth/sender_test.go index b3c5d331..c8454b2f 100644 --- a/pkg/auth/sender_test.go +++ b/pkg/auth/sender_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) func TestSenderErrors(t *testing.T) { diff --git a/pkg/auth/validator.go b/pkg/auth/validator.go index 1f33712c..3f04d27f 100644 --- a/pkg/auth/validator.go +++ b/pkg/auth/validator.go @@ -6,9 +6,9 @@ import ( "fmt" "strings" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/headers" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/url" ) func stringsReverseIndex(s, substr string) int { diff --git a/pkg/auth/validator_test.go b/pkg/auth/validator_test.go index 3a18e2f4..cb2a89e1 100644 --- a/pkg/auth/validator_test.go +++ b/pkg/auth/validator_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) func TestValidatorErrors(t *testing.T) { diff --git a/pkg/base/request.go b/pkg/base/request.go index f7ee2df6..bf9e391e 100644 --- a/pkg/base/request.go +++ b/pkg/base/request.go @@ -6,7 +6,7 @@ import ( "fmt" "strconv" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/url" ) const ( diff --git a/pkg/base/request_test.go b/pkg/base/request_test.go index 19801455..ca6b7f29 100644 --- a/pkg/base/request_test.go +++ b/pkg/base/request_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/url" ) func mustParseURL(s string) *url.URL { diff --git a/pkg/conn/conn.go b/pkg/conn/conn.go index fb538ed8..891737a3 100644 --- a/pkg/conn/conn.go +++ b/pkg/conn/conn.go @@ -5,7 +5,7 @@ import ( "bufio" "io" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) const ( diff --git a/pkg/conn/conn_test.go b/pkg/conn/conn_test.go index 60e9ee5f..523886ee 100644 --- a/pkg/conn/conn_test.go +++ b/pkg/conn/conn_test.go @@ -6,8 +6,8 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/url" ) func mustParseURL(s string) *url.URL { diff --git a/pkg/format/format.go b/pkg/format/format.go new file mode 100644 index 00000000..5a13c4d4 --- /dev/null +++ b/pkg/format/format.go @@ -0,0 +1,128 @@ +// Package format contains format definitions. +package format + +import ( + "strconv" + "strings" + + "github.com/pion/rtp" + psdp "github.com/pion/sdp/v3" +) + +func getFormatAttribute(attributes []psdp.Attribute, payloadType uint8, key string) string { + for _, attr := range attributes { + if attr.Key == key { + v := strings.TrimSpace(attr.Value) + if parts := strings.SplitN(v, " ", 2); len(parts) == 2 { + if tmp, err := strconv.ParseInt(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType { + return parts[1] + } + } + } + } + return "" +} + +func getCodecAndClock(rtpMap string) (string, string) { + parts2 := strings.SplitN(rtpMap, "/", 2) + if len(parts2) != 2 { + return "", "" + } + + return parts2[0], parts2[1] +} + +// Format is a RTSP format. +type Format interface { + // String returns a description of the format. + String() string + + // ClockRate returns the clock rate. + ClockRate() int + + // PayloadType returns the payload type. + PayloadType() uint8 + + unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error + + // Marshal encodes the format in SDP format. + Marshal() (string, string) + + // Clone clones the format. + Clone() Format + + // PTSEqualsDTS checks whether PTS is equal to DTS in the RTP packet. + PTSEqualsDTS(*rtp.Packet) bool +} + +// Unmarshal decodes a format from a media description. +func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error) { + tmp, err := strconv.ParseInt(payloadTypeStr, 10, 8) + if err != nil { + return nil, err + } + payloadType := uint8(tmp) + + rtpMap := getFormatAttribute(md.Attributes, payloadType, "rtpmap") + codec, clock := getCodecAndClock(rtpMap) + codec = strings.ToLower(codec) + fmtp := getFormatAttribute(md.Attributes, payloadType, "fmtp") + + format := func() Format { + switch { + case md.MediaName.Media == "video": + switch { + case payloadType == 26: + return &JPEG{} + + case payloadType == 32: + return &MPEG2Video{} + + case codec == "h264" && clock == "90000": + return &H264{} + + case codec == "h265" && clock == "90000": + return &H265{} + + case codec == "vp8" && clock == "90000": + return &VP8{} + + case codec == "vp9" && clock == "90000": + return &VP9{} + } + + case md.MediaName.Media == "audio": + switch { + case payloadType == 0, payloadType == 8: + return &G711{} + + case payloadType == 9: + return &G722{} + + case payloadType == 14: + return &MPEG2Audio{} + + case codec == "l8", codec == "l16", codec == "l24": + return &LPCM{} + + case codec == "mpeg4-generic": + return &MPEG4Audio{} + + case codec == "vorbis": + return &Vorbis{} + + case codec == "opus": + return &Opus{} + } + } + + return &Generic{} + }() + + err = format.unmarshal(payloadType, clock, codec, rtpMap, fmtp) + if err != nil { + return nil, err + } + + return format, nil +} diff --git a/pkg/format/g711.go b/pkg/format/g711.go new file mode 100644 index 00000000..6f5a8e55 --- /dev/null +++ b/pkg/format/g711.go @@ -0,0 +1,84 @@ +package format + +import ( + "fmt" + "strings" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtpsimpleaudio" +) + +// G711 is a G711 format, encoded with mu-law or A-law. +type G711 struct { + // whether to use mu-law. Otherwise, A-law is used. + MULaw bool +} + +// String implements Format. +func (t *G711) String() string { + return "G711" +} + +// ClockRate implements Format. +func (t *G711) ClockRate() int { + return 8000 +} + +// PayloadType implements Format. +func (t *G711) PayloadType() uint8 { + if t.MULaw { + return 0 + } + return 8 +} + +func (t *G711) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + tmp := strings.Split(clock, "/") + if len(tmp) == 2 && tmp[1] != "1" { + return fmt.Errorf("G711 formats can have only one channel") + } + + t.MULaw = (payloadType == 0) + + return nil +} + +// Marshal implements Format. +func (t *G711) Marshal() (string, string) { + if t.MULaw { + return "PCMU/8000", "" + } + return "PCMA/8000", "" +} + +// Clone implements Format. +func (t *G711) Clone() Format { + return &G711{ + MULaw: t.MULaw, + } +} + +// PTSEqualsDTS implements Format. +func (t *G711) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *G711) CreateDecoder() *rtpsimpleaudio.Decoder { + d := &rtpsimpleaudio.Decoder{ + SampleRate: 8000, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (t *G711) CreateEncoder() *rtpsimpleaudio.Encoder { + e := &rtpsimpleaudio.Encoder{ + PayloadType: t.PayloadType(), + SampleRate: 8000, + } + e.Init() + return e +} diff --git a/pkg/format/g711_test.go b/pkg/format/g711_test.go new file mode 100644 index 00000000..b3b977e9 --- /dev/null +++ b/pkg/format/g711_test.go @@ -0,0 +1,49 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestG711Attributes(t *testing.T) { + format := &G711{} + require.Equal(t, "G711", format.String()) + require.Equal(t, 8000, format.ClockRate()) + require.Equal(t, uint8(8), format.PayloadType()) + + format = &G711{ + MULaw: true, + } + require.Equal(t, "G711", format.String()) + require.Equal(t, 8000, format.ClockRate()) + require.Equal(t, uint8(0), format.PayloadType()) +} + +func TestG711Clone(t *testing.T) { + format := &G711{} + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestG711MediaDescription(t *testing.T) { + t.Run("pcma", func(t *testing.T) { + format := &G711{} + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "PCMA/8000", rtpmap) + require.Equal(t, "", fmtp) + }) + + t.Run("pcmu", func(t *testing.T) { + format := &G711{ + MULaw: true, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "PCMU/8000", rtpmap) + require.Equal(t, "", fmtp) + }) +} diff --git a/pkg/format/g722.go b/pkg/format/g722.go new file mode 100644 index 00000000..da50eff5 --- /dev/null +++ b/pkg/format/g722.go @@ -0,0 +1,71 @@ +package format + +import ( + "fmt" + "strings" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtpsimpleaudio" +) + +// G722 is a G722 format. +type G722 struct{} + +// String implements Format. +func (t *G722) String() string { + return "G722" +} + +// ClockRate implements Format. +func (t *G722) ClockRate() int { + return 8000 +} + +// PayloadType implements Format. +func (t *G722) PayloadType() uint8 { + return 9 +} + +func (t *G722) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + tmp := strings.Split(clock, "/") + if len(tmp) == 2 && tmp[1] != "1" { + return fmt.Errorf("G722 formats can have only one channel") + } + + return nil +} + +// Marshal implements Format. +func (t *G722) Marshal() (string, string) { + return "G722/8000", "" +} + +// Clone implements Format. +func (t *G722) Clone() Format { + return &G722{} +} + +// PTSEqualsDTS implements Format. +func (t *G722) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *G722) CreateDecoder() *rtpsimpleaudio.Decoder { + d := &rtpsimpleaudio.Decoder{ + SampleRate: 8000, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (t *G722) CreateEncoder() *rtpsimpleaudio.Encoder { + e := &rtpsimpleaudio.Encoder{ + PayloadType: 9, + SampleRate: 8000, + } + e.Init() + return e +} diff --git a/pkg/format/g722_test.go b/pkg/format/g722_test.go new file mode 100644 index 00000000..39838e08 --- /dev/null +++ b/pkg/format/g722_test.go @@ -0,0 +1,30 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestG722Attributes(t *testing.T) { + format := &G722{} + require.Equal(t, "G722", format.String()) + require.Equal(t, 8000, format.ClockRate()) + require.Equal(t, uint8(9), format.PayloadType()) +} + +func TestG722Clone(t *testing.T) { + format := &G722{} + + clone := format.Clone() + // require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestG722MediaDescription(t *testing.T) { + format := &G722{} + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "G722/8000", rtpmap) + require.Equal(t, "", fmtp) +} diff --git a/pkg/format/generic.go b/pkg/format/generic.go new file mode 100644 index 00000000..2cd3596a --- /dev/null +++ b/pkg/format/generic.go @@ -0,0 +1,111 @@ +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" +) + +func findClockRate(payloadType uint8, rtpMap string) (int, error) { + // get clock rate from payload type + // https://en.wikipedia.org/wiki/RTP_payload_formats + switch payloadType { + case 0, 1, 2, 3, 4, 5, 7, 8, 9, 12, 13, 15, 18: + return 8000, nil + + case 6: + return 16000, nil + + case 10, 11: + return 44100, nil + + case 14, 25, 26, 28, 31, 32, 33, 34: + return 90000, nil + + case 16: + return 11025, nil + + case 17: + return 22050, nil + } + + // get clock rate from rtpmap + // https://tools.ietf.org/html/rfc4566 + // a=rtpmap: / [/] + if rtpMap == "" { + return 0, fmt.Errorf("attribute 'rtpmap' not found") + } + + tmp := strings.Split(rtpMap, "/") + if len(tmp) != 2 && len(tmp) != 3 { + return 0, fmt.Errorf("invalid rtpmap (%v)", rtpMap) + } + + v, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return 0, err + } + + return int(v), nil +} + +// Generic is a generic format. +type Generic struct { + PayloadTyp uint8 + RTPMap string + FMTP string + + // clock rate of the format. Filled automatically. + ClockRat int +} + +// Init initializes a Generic. +func (t *Generic) Init() error { + t.ClockRat, _ = findClockRate(t.PayloadTyp, t.RTPMap) + return nil +} + +// String returns a description of the format. +func (t *Generic) String() string { + return "Generic" +} + +// ClockRate implements Format. +func (t *Generic) ClockRate() int { + return t.ClockRat +} + +// PayloadType implements Format. +func (t *Generic) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *Generic) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + t.RTPMap = rtpmap + t.FMTP = fmtp + + return t.Init() +} + +// Marshal implements Format. +func (t *Generic) Marshal() (string, string) { + return t.RTPMap, t.FMTP +} + +// Clone implements Format. +func (t *Generic) Clone() Format { + return &Generic{ + PayloadTyp: t.PayloadTyp, + RTPMap: t.RTPMap, + FMTP: t.FMTP, + ClockRat: t.ClockRat, + } +} + +// PTSEqualsDTS implements Format. +func (t *Generic) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/pkg/format/generic_test.go b/pkg/format/generic_test.go new file mode 100644 index 00000000..05fe86c2 --- /dev/null +++ b/pkg/format/generic_test.go @@ -0,0 +1,53 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenericAttributes(t *testing.T) { + format := &Generic{ + PayloadTyp: 98, + RTPMap: "H265/90000", + FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + } + err := format.Init() + require.NoError(t, err) + + require.Equal(t, "Generic", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(98), format.PayloadType()) +} + +func TestGenericClone(t *testing.T) { + format := &Generic{ + PayloadTyp: 98, + RTPMap: "H265/90000", + FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + } + err := format.Init() + require.NoError(t, err) + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestGenericMediaDescription(t *testing.T) { + format := &Generic{ + PayloadTyp: 98, + RTPMap: "H265/90000", + FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + } + err := format.Init() + require.NoError(t, err) + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "H265/90000", rtpmap) + require.Equal(t, "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; "+ + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", fmtp) +} diff --git a/pkg/format/h264.go b/pkg/format/h264.go new file mode 100644 index 00000000..12321120 --- /dev/null +++ b/pkg/format/h264.go @@ -0,0 +1,240 @@ +package format + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/h264" + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtph264" +) + +// check whether a RTP/H264 packet contains a IDR, without decoding the packet. +func rtpH264ContainsIDR(pkt *rtp.Packet) bool { + if len(pkt.Payload) == 0 { + return false + } + + typ := h264.NALUType(pkt.Payload[0] & 0x1F) + + switch typ { + case h264.NALUTypeIDR: + return true + + case 24: // STAP-A + payload := pkt.Payload[1:] + + for len(payload) > 0 { + if len(payload) < 2 { + return false + } + + size := uint16(payload[0])<<8 | uint16(payload[1]) + payload = payload[2:] + + if size == 0 || int(size) > len(payload) { + return false + } + + nalu := payload[:size] + payload = payload[size:] + + typ = h264.NALUType(nalu[0] & 0x1F) + if typ == h264.NALUTypeIDR { + return true + } + } + + return false + + case 28: // FU-A + if len(pkt.Payload) < 2 { + return false + } + + start := pkt.Payload[1] >> 7 + if start != 1 { + return false + } + + typ := h264.NALUType(pkt.Payload[1] & 0x1F) + return (typ == h264.NALUTypeIDR) + + default: + return false + } +} + +// H264 is a H264 format. +type H264 struct { + PayloadTyp uint8 + SPS []byte + PPS []byte + PacketizationMode int + + mutex sync.RWMutex +} + +// String implements Format. +func (t *H264) String() string { + return "H264" +} + +// ClockRate implements Format. +func (t *H264) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (t *H264) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *H264) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + if fmtp == "" { + return nil // do not return any error + } + + for _, kv := range strings.Split(fmtp, ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid fmtp attribute (%v)", fmtp) + } + + switch tmp[0] { + case "sprop-parameter-sets": + tmp := strings.Split(tmp[1], ",") + if len(tmp) < 2 { + return fmt.Errorf("invalid sprop-parameter-sets (%v)", fmtp) + } + + sps, err := base64.StdEncoding.DecodeString(tmp[0]) + if err != nil { + return fmt.Errorf("invalid sprop-parameter-sets (%v)", fmtp) + } + + pps, err := base64.StdEncoding.DecodeString(tmp[1]) + if err != nil { + return fmt.Errorf("invalid sprop-parameter-sets (%v)", fmtp) + } + + t.SPS = sps + t.PPS = pps + + case "packetization-mode": + tmp, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid packetization-mode (%v)", fmtp) + } + + t.PacketizationMode = int(tmp) + } + } + + return nil +} + +// Marshal implements Format. +func (t *H264) Marshal() (string, string) { + t.mutex.RLock() + defer t.mutex.RUnlock() + + var tmp []string + if t.PacketizationMode != 0 { + tmp = append(tmp, "packetization-mode="+strconv.FormatInt(int64(t.PacketizationMode), 10)) + } + var tmp2 []string + if t.SPS != nil { + tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.SPS)) + } + if t.PPS != nil { + tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.PPS)) + } + if tmp2 != nil { + tmp = append(tmp, "sprop-parameter-sets="+strings.Join(tmp2, ",")) + } + if len(t.SPS) >= 4 { + tmp = append(tmp, "profile-level-id="+strings.ToUpper(hex.EncodeToString(t.SPS[1:4]))) + } + var fmtp string + if tmp != nil { + fmtp = strings.Join(tmp, "; ") + } + + return "H264/90000", fmtp +} + +// Clone implements Format. +func (t *H264) Clone() Format { + return &H264{ + PayloadTyp: t.PayloadTyp, + SPS: t.SPS, + PPS: t.PPS, + PacketizationMode: t.PacketizationMode, + } +} + +// PTSEqualsDTS implements Format. +func (t *H264) PTSEqualsDTS(pkt *rtp.Packet) bool { + return rtpH264ContainsIDR(pkt) +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *H264) CreateDecoder() *rtph264.Decoder { + d := &rtph264.Decoder{ + PacketizationMode: t.PacketizationMode, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (t *H264) CreateEncoder() *rtph264.Encoder { + e := &rtph264.Encoder{ + PayloadType: t.PayloadTyp, + PacketizationMode: t.PacketizationMode, + } + e.Init() + return e +} + +// SafeSPS returns the format SPS. +func (t *H264) SafeSPS() []byte { + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.SPS +} + +// SafePPS returns the format PPS. +func (t *H264) SafePPS() []byte { + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.PPS +} + +// SafeSetSPS sets the format SPS. +func (t *H264) SafeSetSPS(v []byte) { + t.mutex.Lock() + defer t.mutex.Unlock() + t.SPS = v +} + +// SafeSetPPS sets the format PPS. +func (t *H264) SafeSetPPS(v []byte) { + t.mutex.Lock() + defer t.mutex.Unlock() + t.PPS = v +} diff --git a/pkg/format/h264_test.go b/pkg/format/h264_test.go new file mode 100644 index 00000000..29cb6ad9 --- /dev/null +++ b/pkg/format/h264_test.go @@ -0,0 +1,72 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestH264Attributes(t *testing.T) { + format := &H264{ + PayloadTyp: 96, + SPS: []byte{0x01, 0x02}, + PPS: []byte{0x03, 0x04}, + PacketizationMode: 1, + } + require.Equal(t, "H264", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(96), format.PayloadType()) + require.Equal(t, []byte{0x01, 0x02}, format.SafeSPS()) + require.Equal(t, []byte{0x03, 0x04}, format.SafePPS()) + + format.SafeSetSPS([]byte{0x07, 0x08}) + format.SafeSetPPS([]byte{0x09, 0x0A}) + require.Equal(t, []byte{0x07, 0x08}, format.SafeSPS()) + require.Equal(t, []byte{0x09, 0x0A}, format.SafePPS()) +} + +func TestH264Clone(t *testing.T) { + format := &H264{ + PayloadTyp: 96, + SPS: []byte{0x01, 0x02}, + PPS: []byte{0x03, 0x04}, + PacketizationMode: 1, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestH264MediaDescription(t *testing.T) { + t.Run("standard", func(t *testing.T) { + format := &H264{ + PayloadTyp: 96, + SPS: []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + PPS: []byte{ + 0x68, 0xee, 0x3c, 0x80, + }, + PacketizationMode: 1, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "H264/90000", rtpmap) + require.Equal(t, "packetization-mode=1; "+ + "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", fmtp) + }) + + t.Run("no sps/pps", func(t *testing.T) { + format := &H264{ + PayloadTyp: 96, + PacketizationMode: 1, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "H264/90000", rtpmap) + require.Equal(t, "packetization-mode=1", fmtp) + }) +} diff --git a/pkg/format/h265.go b/pkg/format/h265.go new file mode 100644 index 00000000..2f52e464 --- /dev/null +++ b/pkg/format/h265.go @@ -0,0 +1,195 @@ +package format + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtph265" +) + +// H265 is a H265 format. +type H265 struct { + PayloadTyp uint8 + VPS []byte + SPS []byte + PPS []byte + MaxDONDiff int + + mutex sync.RWMutex +} + +// String implements Format. +func (t *H265) String() string { + return "H265" +} + +// ClockRate implements Format. +func (t *H265) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (t *H265) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *H265) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + if fmtp == "" { + return nil // do not return any error + } + + for _, kv := range strings.Split(fmtp, ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid fmtp attribute (%v)", fmtp) + } + + switch tmp[0] { + case "sprop-vps": + var err error + t.VPS, err = base64.StdEncoding.DecodeString(tmp[1]) + if err != nil { + return fmt.Errorf("invalid sprop-vps (%v)", fmtp) + } + + case "sprop-sps": + var err error + t.SPS, err = base64.StdEncoding.DecodeString(tmp[1]) + if err != nil { + return fmt.Errorf("invalid sprop-sps (%v)", fmtp) + } + + case "sprop-pps": + var err error + t.PPS, err = base64.StdEncoding.DecodeString(tmp[1]) + if err != nil { + return fmt.Errorf("invalid sprop-pps (%v)", fmtp) + } + + case "sprop-max-don-diff": + tmp, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid sprop-max-don-diff (%v)", fmtp) + } + t.MaxDONDiff = int(tmp) + } + } + + return nil +} + +// Marshal implements Format. +func (t *H265) Marshal() (string, string) { + t.mutex.RLock() + defer t.mutex.RUnlock() + + var tmp []string + if t.VPS != nil { + tmp = append(tmp, "sprop-vps="+base64.StdEncoding.EncodeToString(t.VPS)) + } + if t.SPS != nil { + tmp = append(tmp, "sprop-sps="+base64.StdEncoding.EncodeToString(t.SPS)) + } + if t.PPS != nil { + tmp = append(tmp, "sprop-pps="+base64.StdEncoding.EncodeToString(t.PPS)) + } + if t.MaxDONDiff != 0 { + tmp = append(tmp, "sprop-max-don-diff="+strconv.FormatInt(int64(t.MaxDONDiff), 10)) + } + var fmtp string + if tmp != nil { + fmtp = strings.Join(tmp, "; ") + } + + return "H265/90000", fmtp +} + +// Clone implements Format. +func (t *H265) Clone() Format { + return &H265{ + PayloadTyp: t.PayloadTyp, + VPS: t.VPS, + SPS: t.SPS, + PPS: t.PPS, + MaxDONDiff: t.MaxDONDiff, + } +} + +// PTSEqualsDTS implements Format. +func (t *H265) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *H265) CreateDecoder() *rtph265.Decoder { + d := &rtph265.Decoder{ + MaxDONDiff: t.MaxDONDiff, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (t *H265) CreateEncoder() *rtph265.Encoder { + e := &rtph265.Encoder{ + PayloadType: t.PayloadTyp, + MaxDONDiff: t.MaxDONDiff, + } + e.Init() + return e +} + +// SafeVPS returns the format VPS. +func (t *H265) SafeVPS() []byte { + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.VPS +} + +// SafeSPS returns the format SPS. +func (t *H265) SafeSPS() []byte { + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.SPS +} + +// SafePPS returns the format PPS. +func (t *H265) SafePPS() []byte { + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.PPS +} + +// SafeSetVPS sets the format VPS. +func (t *H265) SafeSetVPS(v []byte) { + t.mutex.Lock() + defer t.mutex.Unlock() + t.VPS = v +} + +// SafeSetSPS sets the format SPS. +func (t *H265) SafeSetSPS(v []byte) { + t.mutex.Lock() + defer t.mutex.Unlock() + t.SPS = v +} + +// SafeSetPPS sets the format PPS. +func (t *H265) SafeSetPPS(v []byte) { + t.mutex.Lock() + defer t.mutex.Unlock() + t.PPS = v +} diff --git a/pkg/format/h265_test.go b/pkg/format/h265_test.go new file mode 100644 index 00000000..7c9c14da --- /dev/null +++ b/pkg/format/h265_test.go @@ -0,0 +1,55 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestH265Attributes(t *testing.T) { + format := &H265{ + PayloadTyp: 96, + VPS: []byte{0x01, 0x02}, + SPS: []byte{0x03, 0x04}, + PPS: []byte{0x05, 0x06}, + } + require.Equal(t, "H265", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(96), format.PayloadType()) + require.Equal(t, []byte{0x01, 0x02}, format.SafeVPS()) + require.Equal(t, []byte{0x03, 0x04}, format.SafeSPS()) + require.Equal(t, []byte{0x05, 0x06}, format.SafePPS()) + + format.SafeSetVPS([]byte{0x07, 0x08}) + format.SafeSetSPS([]byte{0x09, 0x0A}) + format.SafeSetPPS([]byte{0x0B, 0x0C}) + require.Equal(t, []byte{0x07, 0x08}, format.SafeVPS()) + require.Equal(t, []byte{0x09, 0x0A}, format.SafeSPS()) + require.Equal(t, []byte{0x0B, 0x0C}, format.SafePPS()) +} + +func TestH265Clone(t *testing.T) { + format := &H265{ + PayloadTyp: 96, + VPS: []byte{0x01, 0x02}, + SPS: []byte{0x03, 0x04}, + PPS: []byte{0x05, 0x06}, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestH265MediaDescription(t *testing.T) { + format := &H265{ + PayloadTyp: 96, + VPS: []byte{0x01, 0x02}, + SPS: []byte{0x03, 0x04}, + PPS: []byte{0x05, 0x06}, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "H265/90000", rtpmap) + require.Equal(t, "sprop-vps=AQI=; sprop-sps=AwQ=; sprop-pps=BQY=", fmtp) +} diff --git a/pkg/format/jpeg.go b/pkg/format/jpeg.go new file mode 100644 index 00000000..87efccb1 --- /dev/null +++ b/pkg/format/jpeg.go @@ -0,0 +1,42 @@ +package format + +import ( + "github.com/pion/rtp" +) + +// JPEG is a JPEG format. +type JPEG struct{} + +// String implements Format. +func (t *JPEG) String() string { + return "JPEG" +} + +// ClockRate implements Format. +func (t *JPEG) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (t *JPEG) PayloadType() uint8 { + return 26 +} + +func (t *JPEG) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + return nil +} + +// Marshal implements Format. +func (t *JPEG) Marshal() (string, string) { + return "JPEG/90000", "" +} + +// Clone implements Format. +func (t *JPEG) Clone() Format { + return &JPEG{} +} + +// PTSEqualsDTS implements Format. +func (t *JPEG) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/pkg/format/jpeg_test.go b/pkg/format/jpeg_test.go new file mode 100644 index 00000000..f78105da --- /dev/null +++ b/pkg/format/jpeg_test.go @@ -0,0 +1,30 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestJPEGAttributes(t *testing.T) { + format := &JPEG{} + require.Equal(t, "JPEG", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(26), format.PayloadType()) +} + +func TestJPEGClone(t *testing.T) { + format := &JPEG{} + + clone := format.Clone() + // require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestJPEGMediaDescription(t *testing.T) { + format := &JPEG{} + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "JPEG/90000", rtpmap) + require.Equal(t, "", fmtp) +} diff --git a/pkg/format/lpcm.go b/pkg/format/lpcm.go new file mode 100644 index 00000000..e49d65cc --- /dev/null +++ b/pkg/format/lpcm.go @@ -0,0 +1,124 @@ +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtplpcm" +) + +// LPCM is an uncompressed, Linear PCM format. +type LPCM struct { + PayloadTyp uint8 + BitDepth int + SampleRate int + ChannelCount int +} + +// String implements Format. +func (t *LPCM) String() string { + return "LPCM" +} + +// ClockRate implements Format. +func (t *LPCM) ClockRate() int { + return t.SampleRate +} + +// PayloadType implements Format. +func (t *LPCM) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *LPCM) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + switch codec { + case "l8": + t.BitDepth = 8 + + case "l16": + t.BitDepth = 16 + + case "l24": + t.BitDepth = 24 + } + + tmp := strings.SplitN(clock, "/", 32) + if len(tmp) != 2 { + return fmt.Errorf("invalid clock (%v)", clock) + } + + sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) + if err != nil { + return err + } + t.SampleRate = int(sampleRate) + + channelCount, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return err + } + t.ChannelCount = int(channelCount) + + return nil +} + +// Marshal implements Format. +func (t *LPCM) Marshal() (string, string) { + var codec string + switch t.BitDepth { + case 8: + codec = "L8" + + case 16: + codec = "L16" + + case 24: + codec = "L24" + } + + return codec + "/" + strconv.FormatInt(int64(t.SampleRate), 10) + + "/" + strconv.FormatInt(int64(t.ChannelCount), 10), "" +} + +// Clone implements Format. +func (t *LPCM) Clone() Format { + return &LPCM{ + PayloadTyp: t.PayloadTyp, + BitDepth: t.BitDepth, + SampleRate: t.SampleRate, + ChannelCount: t.ChannelCount, + } +} + +// PTSEqualsDTS implements Format. +func (t *LPCM) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *LPCM) CreateDecoder() *rtplpcm.Decoder { + d := &rtplpcm.Decoder{ + BitDepth: t.BitDepth, + SampleRate: t.SampleRate, + ChannelCount: t.ChannelCount, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (t *LPCM) CreateEncoder() *rtplpcm.Encoder { + e := &rtplpcm.Encoder{ + PayloadType: t.PayloadTyp, + BitDepth: t.BitDepth, + SampleRate: t.SampleRate, + ChannelCount: t.ChannelCount, + } + e.Init() + return e +} diff --git a/pkg/format/lpcm_test.go b/pkg/format/lpcm_test.go new file mode 100644 index 00000000..05bd39b4 --- /dev/null +++ b/pkg/format/lpcm_test.go @@ -0,0 +1,45 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLPCMAttributes(t *testing.T) { + format := &LPCM{ + PayloadTyp: 96, + BitDepth: 24, + SampleRate: 44100, + ChannelCount: 2, + } + require.Equal(t, "LPCM", format.String()) + require.Equal(t, 44100, format.ClockRate()) + require.Equal(t, uint8(96), format.PayloadType()) +} + +func TestTracLPCMClone(t *testing.T) { + format := &LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 48000, + ChannelCount: 2, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestLPCMMediaDescription(t *testing.T) { + format := &LPCM{ + PayloadTyp: 96, + BitDepth: 24, + SampleRate: 96000, + ChannelCount: 2, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "L24/96000/2", rtpmap) + require.Equal(t, "", fmtp) +} diff --git a/pkg/format/mpeg2audio.go b/pkg/format/mpeg2audio.go new file mode 100644 index 00000000..02fd1d1a --- /dev/null +++ b/pkg/format/mpeg2audio.go @@ -0,0 +1,42 @@ +package format + +import ( + "github.com/pion/rtp" +) + +// MPEG2Audio is a MPEG-1 or MPEG-2 audio format. +type MPEG2Audio struct{} + +// String implements Format. +func (t *MPEG2Audio) String() string { + return "MPEG2-audio" +} + +// ClockRate implements Format. +func (t *MPEG2Audio) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (t *MPEG2Audio) PayloadType() uint8 { + return 14 +} + +func (t *MPEG2Audio) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + return nil +} + +// Marshal implements Format. +func (t *MPEG2Audio) Marshal() (string, string) { + return "", "" +} + +// Clone implements Format. +func (t *MPEG2Audio) Clone() Format { + return &MPEG2Audio{} +} + +// PTSEqualsDTS implements Format. +func (t *MPEG2Audio) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/pkg/format/mpeg2audio_test.go b/pkg/format/mpeg2audio_test.go new file mode 100644 index 00000000..3c9f07ad --- /dev/null +++ b/pkg/format/mpeg2audio_test.go @@ -0,0 +1,30 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMPEG2AudioAttributes(t *testing.T) { + format := &MPEG2Audio{} + require.Equal(t, "MPEG2-audio", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(14), format.PayloadType()) +} + +func TestMPEG2AudioClone(t *testing.T) { + format := &MPEG2Audio{} + + clone := format.Clone() + // require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestMPEG2AudioMediaDescription(t *testing.T) { + format := &MPEG2Audio{} + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "", rtpmap) + require.Equal(t, "", fmtp) +} diff --git a/pkg/format/mpeg2video.go b/pkg/format/mpeg2video.go new file mode 100644 index 00000000..6d4872c0 --- /dev/null +++ b/pkg/format/mpeg2video.go @@ -0,0 +1,42 @@ +package format + +import ( + "github.com/pion/rtp" +) + +// MPEG2Video is a MPEG-1 or MPEG-2 video format. +type MPEG2Video struct{} + +// String implements Format. +func (t *MPEG2Video) String() string { + return "MPEG2-video" +} + +// ClockRate implements Format. +func (t *MPEG2Video) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (t *MPEG2Video) PayloadType() uint8 { + return 32 +} + +func (t *MPEG2Video) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + return nil +} + +// Marshal implements Format. +func (t *MPEG2Video) Marshal() (string, string) { + return "", "" +} + +// Clone implements Format. +func (t *MPEG2Video) Clone() Format { + return &MPEG2Video{} +} + +// PTSEqualsDTS implements Format. +func (t *MPEG2Video) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/pkg/format/mpeg2video_test.go b/pkg/format/mpeg2video_test.go new file mode 100644 index 00000000..a6be4934 --- /dev/null +++ b/pkg/format/mpeg2video_test.go @@ -0,0 +1,30 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMPEG2VideoAttributes(t *testing.T) { + format := &MPEG2Video{} + require.Equal(t, "MPEG2-video", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(32), format.PayloadType()) +} + +func TestMPEG2VideoClone(t *testing.T) { + format := &MPEG2Video{} + + clone := format.Clone() + // require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestMPEG2VideoMediaDescription(t *testing.T) { + format := &MPEG2Video{} + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "", rtpmap) + require.Equal(t, "", fmtp) +} diff --git a/pkg/format/mpeg4audio.go b/pkg/format/mpeg4audio.go new file mode 100644 index 00000000..29de6b2c --- /dev/null +++ b/pkg/format/mpeg4audio.go @@ -0,0 +1,182 @@ +package format + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtpmpeg4audio" +) + +// MPEG4Audio is a MPEG-4 audio format. +type MPEG4Audio struct { + PayloadTyp uint8 + Config *mpeg4audio.Config + SizeLength int + IndexLength int + IndexDeltaLength int +} + +// String implements Format. +func (t *MPEG4Audio) String() string { + return "MPEG4-audio" +} + +// ClockRate implements Format. +func (t *MPEG4Audio) ClockRate() int { + return t.Config.SampleRate +} + +// PayloadType implements Format. +func (t *MPEG4Audio) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *MPEG4Audio) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + if fmtp == "" { + return fmt.Errorf("fmtp attribute is missing") + } + + for _, kv := range strings.Split(fmtp, ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid fmtp (%v)", fmtp) + } + + switch strings.ToLower(tmp[0]) { + case "config": + enc, err := hex.DecodeString(tmp[1]) + if err != nil { + return fmt.Errorf("invalid AAC config (%v)", tmp[1]) + } + + t.Config = &mpeg4audio.Config{} + err = t.Config.Unmarshal(enc) + if err != nil { + return fmt.Errorf("invalid AAC config (%v)", tmp[1]) + } + + case "sizelength": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid AAC SizeLength (%v)", tmp[1]) + } + t.SizeLength = int(val) + + case "indexlength": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid AAC IndexLength (%v)", tmp[1]) + } + t.IndexLength = int(val) + + case "indexdeltalength": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid AAC IndexDeltaLength (%v)", tmp[1]) + } + t.IndexDeltaLength = int(val) + } + } + + if t.Config == nil { + return fmt.Errorf("config is missing (%v)", fmtp) + } + + if t.SizeLength == 0 { + return fmt.Errorf("sizelength is missing (%v)", fmtp) + } + + return nil +} + +// Marshal implements Format. +func (t *MPEG4Audio) Marshal() (string, string) { + enc, err := t.Config.Marshal() + if err != nil { + return "", "" + } + + sampleRate := t.Config.SampleRate + if t.Config.ExtensionSampleRate != 0 { + sampleRate = t.Config.ExtensionSampleRate + } + + fmtp := "profile-level-id=1; " + + "mode=AAC-hbr; " + + func() string { + if t.SizeLength > 0 { + return fmt.Sprintf("sizelength=%d; ", t.SizeLength) + } + return "" + }() + + func() string { + if t.IndexLength > 0 { + return fmt.Sprintf("indexlength=%d; ", t.IndexLength) + } + return "" + }() + + func() string { + if t.IndexDeltaLength > 0 { + return fmt.Sprintf("indexdeltalength=%d; ", t.IndexDeltaLength) + } + return "" + }() + + "config=" + hex.EncodeToString(enc) + + return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + + "/" + strconv.FormatInt(int64(t.Config.ChannelCount), 10), fmtp +} + +// Clone implements Format. +func (t *MPEG4Audio) Clone() Format { + return &MPEG4Audio{ + PayloadTyp: t.PayloadTyp, + Config: t.Config, + SizeLength: t.SizeLength, + IndexLength: t.IndexLength, + IndexDeltaLength: t.IndexDeltaLength, + } +} + +// PTSEqualsDTS implements Format. +func (t *MPEG4Audio) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *MPEG4Audio) CreateDecoder() *rtpmpeg4audio.Decoder { + d := &rtpmpeg4audio.Decoder{ + SampleRate: t.Config.SampleRate, + SizeLength: t.SizeLength, + IndexLength: t.IndexLength, + IndexDeltaLength: t.IndexDeltaLength, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (t *MPEG4Audio) CreateEncoder() *rtpmpeg4audio.Encoder { + e := &rtpmpeg4audio.Encoder{ + PayloadType: t.PayloadTyp, + SampleRate: t.Config.SampleRate, + SizeLength: t.SizeLength, + IndexLength: t.IndexLength, + IndexDeltaLength: t.IndexDeltaLength, + } + e.Init() + return e +} diff --git a/pkg/format/mpeg4audio_test.go b/pkg/format/mpeg4audio_test.go new file mode 100644 index 00000000..aa75e513 --- /dev/null +++ b/pkg/format/mpeg4audio_test.go @@ -0,0 +1,63 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" +) + +func TestMPEG4AudioAttributes(t *testing.T) { + format := &MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 48000, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + } + require.Equal(t, "MPEG4-audio", format.String()) + require.Equal(t, 48000, format.ClockRate()) + require.Equal(t, uint8(96), format.PayloadType()) +} + +func TestMPEG4AudioClone(t *testing.T) { + format := &MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 48000, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestMPEG4AudioMediaDescription(t *testing.T) { + format := &MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: mpeg4audio.ObjectTypeAACLC, + SampleRate: 48000, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "mpeg4-generic/48000/2", rtpmap) + require.Equal(t, "profile-level-id=1; mode=AAC-hbr; sizelength=13;"+ + " indexlength=3; indexdeltalength=3; config=1190", fmtp) +} diff --git a/pkg/format/opus.go b/pkg/format/opus.go new file mode 100644 index 00000000..ed930393 --- /dev/null +++ b/pkg/format/opus.go @@ -0,0 +1,102 @@ +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtpsimpleaudio" +) + +// Opus is a Opus format. +type Opus struct { + PayloadTyp uint8 + SampleRate int + ChannelCount int +} + +// String implements Format. +func (t *Opus) String() string { + return "Opus" +} + +// ClockRate implements Format. +func (t *Opus) ClockRate() int { + return t.SampleRate +} + +// PayloadType implements Format. +func (t *Opus) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *Opus) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + tmp := strings.SplitN(clock, "/", 32) + if len(tmp) != 2 { + return fmt.Errorf("invalid clock (%v)", clock) + } + + sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) + if err != nil { + return err + } + t.SampleRate = int(sampleRate) + + channelCount, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return err + } + t.ChannelCount = int(channelCount) + + return nil +} + +// Marshal implements Format. +func (t *Opus) Marshal() (string, string) { + fmtp := "sprop-stereo=" + func() string { + if t.ChannelCount == 2 { + return "1" + } + return "0" + }() + + return "opus/" + strconv.FormatInt(int64(t.SampleRate), 10) + + "/" + strconv.FormatInt(int64(t.ChannelCount), 10), fmtp +} + +// Clone implements Format. +func (t *Opus) Clone() Format { + return &Opus{ + PayloadTyp: t.PayloadTyp, + SampleRate: t.SampleRate, + ChannelCount: t.ChannelCount, + } +} + +// PTSEqualsDTS implements Format. +func (t *Opus) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *Opus) CreateDecoder() *rtpsimpleaudio.Decoder { + d := &rtpsimpleaudio.Decoder{ + SampleRate: t.SampleRate, + } + d.Init() + return d +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (t *Opus) CreateEncoder() *rtpsimpleaudio.Encoder { + e := &rtpsimpleaudio.Encoder{ + PayloadType: t.PayloadTyp, + SampleRate: 8000, + } + e.Init() + return e +} diff --git a/pkg/format/opus_test.go b/pkg/format/opus_test.go new file mode 100644 index 00000000..36639657 --- /dev/null +++ b/pkg/format/opus_test.go @@ -0,0 +1,42 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOpusAttributes(t *testing.T) { + format := &Opus{ + PayloadTyp: 96, + SampleRate: 48000, + ChannelCount: 2, + } + require.Equal(t, "Opus", format.String()) + require.Equal(t, 48000, format.ClockRate()) + require.Equal(t, uint8(96), format.PayloadType()) +} + +func TestTracOpusClone(t *testing.T) { + format := &Opus{ + PayloadTyp: 96, + SampleRate: 48000, + ChannelCount: 2, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestOpusMediaDescription(t *testing.T) { + format := &Opus{ + PayloadTyp: 96, + SampleRate: 48000, + ChannelCount: 2, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "opus/48000/2", rtpmap) + require.Equal(t, "sprop-stereo=1", fmtp) +} diff --git a/track_test.go b/pkg/format/track_test.go similarity index 69% rename from track_test.go rename to pkg/format/track_test.go index e6e5dade..45e0885b 100644 --- a/track_test.go +++ b/pkg/format/track_test.go @@ -1,4 +1,4 @@ -package gortsplib +package format import ( "testing" @@ -6,18 +6,17 @@ import ( psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/mpeg4audio" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" ) -func TestTrackNewFromMediaDescription(t *testing.T) { +func TestNewFromMediaDescription(t *testing.T) { for _, ca := range []struct { - name string - md *psdp.MediaDescription - track Track + name string + md *psdp.MediaDescription + format Format }{ { - "pcma", + "audio g711 pcma", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -25,10 +24,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) { Formats: []string{"8"}, }, }, - &TrackG711{}, + &G711{}, }, { - "pcmu", + "audio g711 pcmu", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -36,12 +35,12 @@ func TestTrackNewFromMediaDescription(t *testing.T) { Formats: []string{"0"}, }, }, - &TrackG711{ + &G711{ MULaw: true, }, }, { - "g722", + "audio g722", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -49,10 +48,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) { Formats: []string{"9"}, }, }, - &TrackG722{}, + &G722{}, }, { - "lpcm 8", + "audio lpcm 8", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -66,15 +65,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackLPCM{ - PayloadType: 97, + &LPCM{ + PayloadTyp: 97, BitDepth: 8, SampleRate: 48000, ChannelCount: 2, }, }, { - "lpcm 16", + "audio lpcm 16", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -88,15 +87,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackLPCM{ - PayloadType: 97, + &LPCM{ + PayloadTyp: 97, BitDepth: 16, SampleRate: 96000, ChannelCount: 2, }, }, { - "lpcm 24", + "audio lpcm 24", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -110,15 +109,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackLPCM{ - PayloadType: 98, + &LPCM{ + PayloadTyp: 98, BitDepth: 24, SampleRate: 44100, ChannelCount: 4, }, }, { - "mpeg audio", + "audio mpeg2 audio", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -126,10 +125,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) { Formats: []string{"14"}, }, }, - &TrackMPEG2Audio{}, + &MPEG2Audio{}, }, { - "aac", + "audio aac", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -151,8 +150,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackMPEG4Audio{ - PayloadType: 96, + &MPEG4Audio{ + PayloadTyp: 96, Config: &mpeg4audio.Config{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 48000, @@ -164,7 +163,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "aac vlc rtsp server", + "audio aac vlc rtsp server", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -182,8 +181,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackMPEG4Audio{ - PayloadType: 96, + &MPEG4Audio{ + PayloadTyp: 96, Config: &mpeg4audio.Config{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 48000, @@ -195,7 +194,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "aac without indexlength", + "audio aac without indexlength", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -217,8 +216,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackMPEG4Audio{ - PayloadType: 96, + &MPEG4Audio{ + PayloadTyp: 96, Config: &mpeg4audio.Config{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 48000, @@ -230,7 +229,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "vorbis", + "audio vorbis", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -248,15 +247,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackVorbis{ - PayloadType: 96, + &Vorbis{ + PayloadTyp: 96, SampleRate: 44100, ChannelCount: 2, Configuration: []byte{0x01, 0x02, 0x03, 0x04}, }, }, { - "opus", + "audio opus", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "audio", @@ -274,14 +273,14 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackOpus{ - PayloadType: 96, + &Opus{ + PayloadTyp: 96, SampleRate: 48000, ChannelCount: 2, }, }, { - "jpeg", + "video jpeg", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -289,10 +288,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) { Formats: []string{"26"}, }, }, - &TrackJPEG{}, + &JPEG{}, }, { - "mpeg video", + "video mpeg2 video", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -300,10 +299,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) { Formats: []string{"32"}, }, }, - &TrackMPEG2Video{}, + &MPEG2Video{}, }, { - "h264", + "video h264", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -322,8 +321,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackH264{ - PayloadType: 96, + &H264{ + PayloadTyp: 96, SPS: []byte{ 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, @@ -336,7 +335,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "h264 with a space at the end of rtpmap", + "video h264 with a space at the end of rtpmap", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -350,12 +349,12 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackH264{ - PayloadType: 96, + &H264{ + PayloadTyp: 96, }, }, { - "h264 vlc rtsp server", + "video h264 vlc rtsp server", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -374,8 +373,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackH264{ - PayloadType: 96, + &H264{ + PayloadTyp: 96, SPS: []byte{ 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50, 0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03, @@ -389,7 +388,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "h264 sprop-parameter-sets with extra data", + "video h264 sprop-parameter-sets with extra data", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -408,8 +407,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackH264{ - PayloadType: 96, + &H264{ + PayloadTyp: 96, SPS: []byte{ 0x67, 0x64, 0x00, 0x29, 0xac, 0x13, 0x31, 0x40, 0x78, 0x04, 0x47, 0xde, 0x03, 0xea, 0x02, 0x02, @@ -423,7 +422,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "h265", + "video h265", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -443,8 +442,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackH265{ - PayloadType: 96, + &H265{ + PayloadTyp: 96, VPS: []byte{ 0x40, 0x1, 0xc, 0x1, 0xff, 0xff, 0x1, 0x60, 0x0, 0x0, 0x3, 0x0, 0x90, 0x0, 0x0, 0x3, @@ -465,7 +464,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "vp8", + "video vp8", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -483,8 +482,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackVP8{ - PayloadType: 96, + &VP8{ + PayloadTyp: 96, MaxFR: func() *int { v := 123 return &v @@ -496,7 +495,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "vp9", + "video vp9", &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: "video", @@ -514,8 +513,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, }, - &TrackVP9{ - PayloadType: 96, + &VP9{ + PayloadTyp: 96, MaxFR: func() *int { v := 123 return &v @@ -531,74 +530,58 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, }, { - "multiple formats", + "application", &psdp.MediaDescription{ MediaName: psdp.MediaName{ - Media: "video", + Media: "application", Protos: []string{"RTP", "AVP"}, - Formats: []string{"96", "98"}, + Formats: []string{"98"}, }, Attributes: []psdp.Attribute{ { Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "rtpmap", - Value: "98 MetaData", + Value: "98 MetaData/80000", }, { Key: "rtcp-mux", }, - { - Key: "fmtp", - Value: "96 packetization-mode=1;profile-level-id=4d002a;" + - "sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==", - }, }, }, - &TrackGeneric{ - Media: "video", - Payloads: []TrackGenericPayload{ - { - Type: 96, - RTPMap: "H264/90000", - FMTP: "packetization-mode=1;profile-level-id=4d002a;sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==", - }, - { - Type: 98, - RTPMap: "MetaData", - }, + &Generic{ + PayloadTyp: 98, + RTPMap: "MetaData/80000", + ClockRat: 80000, + }, + }, + { + "application without clock rate", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "application", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"107"}, }, - clockRate: 90000, + }, + &Generic{ + PayloadTyp: 107, + ClockRat: 0, }, }, } { t.Run(ca.name, func(t *testing.T) { - track, err := newTrackFromMediaDescription(ca.md) + format, err := Unmarshal(ca.md, ca.md.MediaName.Formats[0]) require.NoError(t, err) - require.Equal(t, ca.track, track) + require.Equal(t, ca.format, format) }) } } -func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { +func TestNewFromMediaDescriptionErrors(t *testing.T) { for _, ca := range []struct { name string md *psdp.MediaDescription err string }{ - { - "no formats", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{}, - }, - }, - "no media formats found", - }, { "aac missing fmtp", &psdp.MediaDescription{ @@ -635,7 +618,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { }, }, }, - "invalid fmtp (96)", + "fmtp attribute is missing", }, { "aac fmtp without key", @@ -656,7 +639,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { }, }, }, - "invalid fmtp (96 profile-level-id)", + "invalid fmtp (profile-level-id)", }, { "aac missing config", @@ -677,7 +660,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { }, }, }, - "config is missing (96 profile-level-id=1)", + "config is missing (profile-level-id=1)", }, { "aac invalid config 1", @@ -740,7 +723,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { }, }, }, - "sizelength is missing (96 profile-level-id=1; config=1190)", + "sizelength is missing (profile-level-id=1; config=1190)", }, { "opus invalid 1", @@ -795,126 +778,8 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) { }, } { t.Run(ca.name, func(t *testing.T) { - _, err := newTrackFromMediaDescription(ca.md) + _, err := Unmarshal(ca.md, ca.md.MediaName.Formats[0]) require.EqualError(t, err, ca.err) }) } } - -func TestTrackURL(t *testing.T) { - for _, ca := range []struct { - name string - sdp []byte - baseURL *url.URL - ur *url.URL - }{ - { - "missing control", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), - }, - { - "absolute control", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:rtsp://localhost/path/trackID=7"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=7"), - }, - { - "relative control", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:trackID=5"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=5"), - }, - { - "relative control, subpath", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:trackID=5"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"), - }, - { - "relative control, url without slash", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:trackID=5"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"), - }, - { - "relative control, url with query", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:trackID=5"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + - "test?user=tmp&password=BagRep1&channel=1&stream=0.sdp"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + - "test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=5"), - }, - { - "relative control, url with special chars and query", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:trackID=5"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + - "te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + - "te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"), - }, - { - "relative control, url with query without question mark", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:trackID=5"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), - mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"), - }, - { - "relative control, control is query", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:?ctype=video"), - mustParseURL("rtsp://192.168.1.99:554/test"), - mustParseURL("rtsp://192.168.1.99:554/test?ctype=video"), - }, - { - "relative control, control is query and no path", - []byte("v=0\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000\r\n" + - "a=control:?ctype=video"), - mustParseURL("rtsp://192.168.1.99:554/"), - mustParseURL("rtsp://192.168.1.99:554/?ctype=video"), - }, - } { - t.Run(ca.name, func(t *testing.T) { - var tracks Tracks - _, err := tracks.Unmarshal(ca.sdp) - require.NoError(t, err) - ur, err := tracks[0].url(ca.baseURL) - require.NoError(t, err) - require.Equal(t, ca.ur, ur) - }) - } -} - -func TestTrackURLError(t *testing.T) { - track := &TrackH264{} - _, err := track.url(nil) - require.EqualError(t, err, "Content-Base header not provided") -} diff --git a/pkg/format/vorbis.go b/pkg/format/vorbis.go new file mode 100644 index 00000000..99cddd9f --- /dev/null +++ b/pkg/format/vorbis.go @@ -0,0 +1,108 @@ +package format + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" +) + +// Vorbis is a Vorbis format. +type Vorbis struct { + PayloadTyp uint8 + SampleRate int + ChannelCount int + Configuration []byte +} + +// String implements Format. +func (t *Vorbis) String() string { + return "Vorbis" +} + +// ClockRate implements Format. +func (t *Vorbis) ClockRate() int { + return t.SampleRate +} + +// PayloadType implements Format. +func (t *Vorbis) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *Vorbis) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + tmp := strings.SplitN(clock, "/", 32) + if len(tmp) != 2 { + return fmt.Errorf("invalid clock (%v)", clock) + } + + sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) + if err != nil { + return err + } + t.SampleRate = int(sampleRate) + + channelCount, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return err + } + t.ChannelCount = int(channelCount) + + if fmtp == "" { + return fmt.Errorf("fmtp attribute is missing") + } + + for _, kv := range strings.Split(fmtp, ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid fmtp (%v)", fmtp) + } + + if tmp[0] == "configuration" { + conf, err := base64.StdEncoding.DecodeString(tmp[1]) + if err != nil { + return fmt.Errorf("invalid AAC config (%v)", tmp[1]) + } + + t.Configuration = conf + } + } + + if t.Configuration == nil { + return fmt.Errorf("config is missing (%v)", fmtp) + } + + return nil +} + +// Marshal implements Format. +func (t *Vorbis) Marshal() (string, string) { + return "VORBIS/" + strconv.FormatInt(int64(t.SampleRate), 10) + + "/" + strconv.FormatInt(int64(t.ChannelCount), 10), + "configuration=" + base64.StdEncoding.EncodeToString(t.Configuration) +} + +// Clone implements Format. +func (t *Vorbis) Clone() Format { + return &Vorbis{ + PayloadTyp: t.PayloadTyp, + SampleRate: t.SampleRate, + ChannelCount: t.ChannelCount, + Configuration: t.Configuration, + } +} + +// PTSEqualsDTS implements Format. +func (t *Vorbis) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/pkg/format/vorbis_test.go b/pkg/format/vorbis_test.go new file mode 100644 index 00000000..473c74ca --- /dev/null +++ b/pkg/format/vorbis_test.go @@ -0,0 +1,45 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVorbisAttributes(t *testing.T) { + format := &Vorbis{ + PayloadTyp: 96, + SampleRate: 48000, + ChannelCount: 2, + Configuration: []byte{0x01, 0x02, 0x03, 0x04}, + } + require.Equal(t, "Vorbis", format.String()) + require.Equal(t, 48000, format.ClockRate()) + require.Equal(t, uint8(96), format.PayloadType()) +} + +func TestTracVorbisClone(t *testing.T) { + format := &Vorbis{ + PayloadTyp: 96, + SampleRate: 48000, + ChannelCount: 2, + Configuration: []byte{0x01, 0x02, 0x03, 0x04}, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestVorbisMediaDescription(t *testing.T) { + format := &Vorbis{ + PayloadTyp: 96, + SampleRate: 48000, + ChannelCount: 2, + Configuration: []byte{0x01, 0x02, 0x03, 0x04}, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "VORBIS/48000/2", rtpmap) + require.Equal(t, "configuration=AQIDBA==", fmtp) +} diff --git a/pkg/format/vp8.go b/pkg/format/vp8.go new file mode 100644 index 00000000..8fe7b4a3 --- /dev/null +++ b/pkg/format/vp8.go @@ -0,0 +1,110 @@ +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtpvp8" +) + +// VP8 is a VP8 format. +type VP8 struct { + PayloadTyp uint8 + MaxFR *int + MaxFS *int +} + +// String implements Format. +func (t *VP8) String() string { + return "VP8" +} + +// ClockRate implements Format. +func (t *VP8) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (t *VP8) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *VP8) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + if fmtp != "" { + for _, kv := range strings.Split(fmtp, ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid fmtp attribute (%v)", fmtp) + } + + switch tmp[0] { + case "max-fr": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fr (%v)", tmp[1]) + } + v2 := int(val) + t.MaxFR = &v2 + + case "max-fs": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fs (%v)", tmp[1]) + } + v2 := int(val) + t.MaxFS = &v2 + } + } + } + + return nil +} + +// Marshal implements Format. +func (t *VP8) Marshal() (string, string) { + var tmp []string + if t.MaxFR != nil { + tmp = append(tmp, "max-fr="+strconv.FormatInt(int64(*t.MaxFR), 10)) + } + if t.MaxFS != nil { + tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) + } + var fmtp string + if tmp != nil { + fmtp = strings.Join(tmp, ";") + } + + return "VP8/90000", fmtp +} + +// Clone implements Format. +func (t *VP8) Clone() Format { + return &VP8{ + PayloadTyp: t.PayloadTyp, + MaxFR: t.MaxFR, + MaxFS: t.MaxFS, + } +} + +// PTSEqualsDTS implements Format. +func (t *VP8) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *VP8) CreateDecoder() *rtpvp8.Decoder { + d := &rtpvp8.Decoder{} + d.Init() + return d +} diff --git a/pkg/format/vp8_test.go b/pkg/format/vp8_test.go new file mode 100644 index 00000000..154f4a57 --- /dev/null +++ b/pkg/format/vp8_test.go @@ -0,0 +1,44 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVP8ttributes(t *testing.T) { + format := &VP8{ + PayloadTyp: 99, + } + require.Equal(t, "VP8", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(99), format.PayloadType()) +} + +func TestVP8Clone(t *testing.T) { + maxFR := 123 + maxFS := 456 + format := &VP8{ + PayloadTyp: 96, + MaxFR: &maxFR, + MaxFS: &maxFS, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestVP8MediaDescription(t *testing.T) { + maxFR := 123 + maxFS := 456 + format := &VP8{ + PayloadTyp: 96, + MaxFR: &maxFR, + MaxFS: &maxFS, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "VP8/90000", rtpmap) + require.Equal(t, "max-fr=123;max-fs=456", fmtp) +} diff --git a/pkg/format/vp9.go b/pkg/format/vp9.go new file mode 100644 index 00000000..9d84b8fa --- /dev/null +++ b/pkg/format/vp9.go @@ -0,0 +1,123 @@ +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/rtpcodecs/rtpvp9" +) + +// VP9 is a VP9 format. +type VP9 struct { + PayloadTyp uint8 + MaxFR *int + MaxFS *int + ProfileID *int +} + +// String implements Format. +func (t *VP9) String() string { + return "VP9" +} + +// ClockRate implements Format. +func (t *VP9) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (t *VP9) PayloadType() uint8 { + return t.PayloadTyp +} + +func (t *VP9) unmarshal(payloadType uint8, clock string, codec string, rtpmap string, fmtp string) error { + t.PayloadTyp = payloadType + + if fmtp != "" { + for _, kv := range strings.Split(fmtp, ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid fmtp attribute (%v)", fmtp) + } + + switch tmp[0] { + case "max-fr": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fr (%v)", tmp[1]) + } + v2 := int(val) + t.MaxFR = &v2 + + case "max-fs": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid max-fs (%v)", tmp[1]) + } + v2 := int(val) + t.MaxFS = &v2 + + case "profile-id": + val, err := strconv.ParseUint(tmp[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid profile-id (%v)", tmp[1]) + } + v2 := int(val) + t.ProfileID = &v2 + } + } + } + + return nil +} + +// Marshal implements Format. +func (t *VP9) Marshal() (string, string) { + var tmp []string + if t.MaxFR != nil { + tmp = append(tmp, "max-fr="+strconv.FormatInt(int64(*t.MaxFR), 10)) + } + if t.MaxFS != nil { + tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) + } + if t.ProfileID != nil { + tmp = append(tmp, "profile-id="+strconv.FormatInt(int64(*t.ProfileID), 10)) + } + var fmtp string + if tmp != nil { + fmtp = strings.Join(tmp, ";") + } + + return "VP9/90000", fmtp +} + +// Clone implements Format. +func (t *VP9) Clone() Format { + return &VP9{ + PayloadTyp: t.PayloadTyp, + MaxFR: t.MaxFR, + MaxFS: t.MaxFS, + ProfileID: t.ProfileID, + } +} + +// PTSEqualsDTS implements Format. +func (t *VP9) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (t *VP9) CreateDecoder() *rtpvp9.Decoder { + d := &rtpvp9.Decoder{} + d.Init() + return d +} diff --git a/pkg/format/vp9_test.go b/pkg/format/vp9_test.go new file mode 100644 index 00000000..5cc56b8e --- /dev/null +++ b/pkg/format/vp9_test.go @@ -0,0 +1,48 @@ +package format + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVP9Attributes(t *testing.T) { + format := &VP9{ + PayloadTyp: 100, + } + require.Equal(t, "VP9", format.String()) + require.Equal(t, 90000, format.ClockRate()) + require.Equal(t, uint8(100), format.PayloadType()) +} + +func TestVP9Clone(t *testing.T) { + maxFR := 123 + maxFS := 456 + profileID := 789 + format := &VP9{ + PayloadTyp: 96, + MaxFR: &maxFR, + MaxFS: &maxFS, + ProfileID: &profileID, + } + + clone := format.Clone() + require.NotSame(t, format, clone) + require.Equal(t, format, clone) +} + +func TestVP9MediaDescription(t *testing.T) { + maxFR := 123 + maxFS := 456 + profileID := 789 + format := &VP9{ + PayloadTyp: 96, + MaxFR: &maxFR, + MaxFS: &maxFS, + ProfileID: &profileID, + } + + rtpmap, fmtp := format.Marshal() + require.Equal(t, "VP9/90000", rtpmap) + require.Equal(t, "max-fr=123;max-fs=456;profile-id=789", fmtp) +} diff --git a/pkg/h264/dtsextractor.go b/pkg/h264/dtsextractor.go index b09e5fe6..e476f54c 100644 --- a/pkg/h264/dtsextractor.go +++ b/pkg/h264/dtsextractor.go @@ -6,7 +6,7 @@ import ( "math" "time" - "github.com/aler9/gortsplib/pkg/bits" + "github.com/aler9/gortsplib/v2/pkg/bits" ) func getPOC(buf []byte, sps *SPS) (uint32, error) { diff --git a/pkg/h264/sps.go b/pkg/h264/sps.go index fb4fe3cb..3602ff5a 100644 --- a/pkg/h264/sps.go +++ b/pkg/h264/sps.go @@ -3,7 +3,7 @@ package h264 import ( "fmt" - "github.com/aler9/gortsplib/pkg/bits" + "github.com/aler9/gortsplib/v2/pkg/bits" ) func readScalingList(buf []byte, pos *int, size int) ([]int32, bool, error) { diff --git a/pkg/headers/authenticate.go b/pkg/headers/authenticate.go index 28713ba7..7554f91f 100644 --- a/pkg/headers/authenticate.go +++ b/pkg/headers/authenticate.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) // AuthMethod is an authentication method. diff --git a/pkg/headers/authenticate_test.go b/pkg/headers/authenticate_test.go index 7162de46..8ecddd0a 100644 --- a/pkg/headers/authenticate_test.go +++ b/pkg/headers/authenticate_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) var casesAuthenticate = []struct { diff --git a/pkg/headers/authorization.go b/pkg/headers/authorization.go index b7d27d5d..8379e72c 100644 --- a/pkg/headers/authorization.go +++ b/pkg/headers/authorization.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) // Authorization is an Authorization header. diff --git a/pkg/headers/authorization_test.go b/pkg/headers/authorization_test.go index fae24752..f0a64aba 100644 --- a/pkg/headers/authorization_test.go +++ b/pkg/headers/authorization_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) var casesAuthorization = []struct { diff --git a/pkg/headers/range.go b/pkg/headers/range.go index b4a69a0e..49965082 100644 --- a/pkg/headers/range.go +++ b/pkg/headers/range.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) func leadingZero(v uint) string { diff --git a/pkg/headers/range_test.go b/pkg/headers/range_test.go index ecd51c90..39851e94 100644 --- a/pkg/headers/range_test.go +++ b/pkg/headers/range_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) var casesRange = []struct { diff --git a/pkg/headers/rtpinfo.go b/pkg/headers/rtpinfo.go index 806b94f5..0a53090f 100644 --- a/pkg/headers/rtpinfo.go +++ b/pkg/headers/rtpinfo.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) // RTPInfoEntry is an entry of a RTP-Info header. diff --git a/pkg/headers/rtpinfo_test.go b/pkg/headers/rtpinfo_test.go index 416cf91a..5c940e87 100644 --- a/pkg/headers/rtpinfo_test.go +++ b/pkg/headers/rtpinfo_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) var casesRTPInfo = []struct { diff --git a/pkg/headers/session.go b/pkg/headers/session.go index d8b8fbaa..eb0dff66 100644 --- a/pkg/headers/session.go +++ b/pkg/headers/session.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) // Session is a Session header. diff --git a/pkg/headers/session_test.go b/pkg/headers/session_test.go index 14cde89b..bfff6a35 100644 --- a/pkg/headers/session_test.go +++ b/pkg/headers/session_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) var casesSession = []struct { diff --git a/pkg/headers/transport.go b/pkg/headers/transport.go index e95e53e0..d75fc393 100644 --- a/pkg/headers/transport.go +++ b/pkg/headers/transport.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) func parsePorts(val string) (*[2]int, error) { diff --git a/pkg/headers/transport_test.go b/pkg/headers/transport_test.go index f24af994..b823cd1d 100644 --- a/pkg/headers/transport_test.go +++ b/pkg/headers/transport_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) var casesTransport = []struct { diff --git a/pkg/liberrors/client.go b/pkg/liberrors/client.go index 05b105e0..edc2072f 100644 --- a/pkg/liberrors/client.go +++ b/pkg/liberrors/client.go @@ -3,7 +3,7 @@ package liberrors import ( "fmt" - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" ) // ErrClientTerminated is an error that can be returned by a client. @@ -65,12 +65,12 @@ func (e ErrClientContentTypeUnsupported) Error() string { return fmt.Sprintf("unsupported Content-Type header '%v'", e.CT) } -// ErrClientCannotSetupTracksDifferentURLs is an error that can be returned by a client. -type ErrClientCannotSetupTracksDifferentURLs struct{} +// ErrClientCannotSetupMediasDifferentURLs is an error that can be returned by a client. +type ErrClientCannotSetupMediasDifferentURLs struct{} // Error implements the error interface. -func (e ErrClientCannotSetupTracksDifferentURLs) Error() string { - return "cannot setup tracks with different base URLs" +func (e ErrClientCannotSetupMediasDifferentURLs) Error() string { + return "cannot setup medias with different base URLs" } // ErrClientUDPPortsZero is an error that can be returned by a client. diff --git a/pkg/liberrors/server.go b/pkg/liberrors/server.go index e6572a25..961d233b 100644 --- a/pkg/liberrors/server.go +++ b/pkg/liberrors/server.go @@ -4,8 +4,8 @@ import ( "fmt" "net" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/headers" ) // ErrServerTerminated is an error that can be returned by a server. @@ -106,14 +106,12 @@ func (e ErrServerTransportHeaderInvalid) Error() string { return fmt.Sprintf("invalid transport header: %v", e.Err) } -// ErrServerTrackAlreadySetup is an error that can be returned by a server. -type ErrServerTrackAlreadySetup struct { - TrackID int -} +// ErrServerMediaAlreadySetup is an error that can be returned by a server. +type ErrServerMediaAlreadySetup struct{} // Error implements the error interface. -func (e ErrServerTrackAlreadySetup) Error() string { - return fmt.Sprintf("track %d has already been setup", e.TrackID) +func (e ErrServerMediaAlreadySetup) Error() string { + return "media has already been setup" } // ErrServerTransportHeaderInvalidMode is an error that can be returned by a server. @@ -158,28 +156,36 @@ func (e ErrServerTransportHeaderInterleavedIDsAlreadyUsed) Error() string { return "interleaved IDs already used" } -// ErrServerTracksDifferentProtocols is an error that can be returned by a server. -type ErrServerTracksDifferentProtocols struct{} +// ErrServerMediasDifferentPaths is an error that can be returned by a server. +type ErrServerMediasDifferentPaths struct{} // Error implements the error interface. -func (e ErrServerTracksDifferentProtocols) Error() string { - return "can't setup tracks with different protocols" +func (e ErrServerMediasDifferentPaths) Error() string { + return "can't setup medias with different paths" } -// ErrServerNoTracksSetup is an error that can be returned by a server. -type ErrServerNoTracksSetup struct{} +// ErrServerMediasDifferentProtocols is an error that can be returned by a server. +type ErrServerMediasDifferentProtocols struct{} // Error implements the error interface. -func (e ErrServerNoTracksSetup) Error() string { - return "no tracks have been setup" +func (e ErrServerMediasDifferentProtocols) Error() string { + return "can't setup medias with different protocols" } -// ErrServerNotAllAnnouncedTracksSetup is an error that can be returned by a server. -type ErrServerNotAllAnnouncedTracksSetup struct{} +// ErrServerNoMediasSetup is an error that can be returned by a server. +type ErrServerNoMediasSetup struct{} // Error implements the error interface. -func (e ErrServerNotAllAnnouncedTracksSetup) Error() string { - return "not all announced tracks have been setup" +func (e ErrServerNoMediasSetup) Error() string { + return "no medias have been setup" +} + +// ErrServerNotAllAnnouncedMediasSetup is an error that can be returned by a server. +type ErrServerNotAllAnnouncedMediasSetup struct{} + +// Error implements the error interface. +func (e ErrServerNotAllAnnouncedMediasSetup) Error() string { + return "not all announced medias have been setup" } // ErrServerLinkedToOtherSession is an error that can be returned by a server. diff --git a/pkg/media/media.go b/pkg/media/media.go new file mode 100644 index 00000000..35d5e63c --- /dev/null +++ b/pkg/media/media.go @@ -0,0 +1,156 @@ +// Package media contains the media stream definition. +package media + +import ( + "fmt" + "strconv" + "strings" + + psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/url" +) + +func getControlAttribute(attributes []psdp.Attribute) string { + for _, attr := range attributes { + if attr.Key == "control" { + return attr.Value + } + } + return "" +} + +// Type is the type of a media stream. +type Type string + +// standard media stream types. +const ( + TypeVideo Type = "video" + TypeAudio Type = "audio" + TypeApplication Type = "application" +) + +// Media is a media stream. It contains one or more format. +type Media struct { + // Media type. + Type Type + + // Control attribute. + Control string + + // Formats contained into the media. + Formats []format.Format +} + +func (m *Media) unmarshal(md *psdp.MediaDescription) error { + m.Type = Type(md.MediaName.Media) + m.Control = getControlAttribute(md.Attributes) + m.Formats = nil + + for _, payloadType := range md.MediaName.Formats { + format, err := format.Unmarshal(md, payloadType) + if err != nil { + return err + } + + m.Formats = append(m.Formats, format) + } + + if m.Formats == nil { + return fmt.Errorf("no formats found") + } + + return nil +} + +// Marshal encodes the media in SDP format. +func (m *Media) Marshal() *psdp.MediaDescription { + md := &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: string(m.Type), + Protos: []string{"RTP", "AVP"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "control", + Value: m.Control, + }, + }, + } + + for _, trak := range m.Formats { + typ := strconv.FormatUint(uint64(trak.PayloadType()), 10) + md.MediaName.Formats = append(md.MediaName.Formats, typ) + + rtpmap, fmtp := trak.Marshal() + + if rtpmap != "" { + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "rtpmap", + Value: typ + " " + rtpmap, + }) + } + + if fmtp != "" { + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "fmtp", + Value: typ + " " + fmtp, + }) + } + } + + return md +} + +// Clone clones the media. +func (m Media) Clone() *Media { + ret := &Media{ + Type: m.Type, + Control: m.Control, + Formats: make([]format.Format, len(m.Formats)), + } + + for i, format := range m.Formats { + ret.Formats[i] = format.Clone() + } + + return ret +} + +// URL returns the media URL. +func (m Media) URL(contentBase *url.URL) (*url.URL, error) { + if contentBase == nil { + return nil, fmt.Errorf("Content-Base header not provided") + } + + // no control attribute, use base URL + if m.Control == "" { + return contentBase, nil + } + + // control attribute contains an absolute path + if strings.HasPrefix(m.Control, "rtsp://") { + ur, err := url.Parse(m.Control) + if err != nil { + return nil, err + } + + // copy host and credentials + ur.Host = contentBase.Host + ur.User = contentBase.User + return ur, nil + } + + // control attribute contains a relative control attribute + // insert the control attribute at the end of the URL + // if there's a query, insert it after the query + // otherwise insert it after the path + strURL := contentBase.String() + if m.Control[0] != '?' && !strings.HasSuffix(strURL, "/") { + strURL += "/" + } + + ur, _ := url.Parse(strURL + m.Control) + return ur, nil +} diff --git a/pkg/media/media_test.go b/pkg/media/media_test.go new file mode 100644 index 00000000..0436fe92 --- /dev/null +++ b/pkg/media/media_test.go @@ -0,0 +1,145 @@ +package media + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/sdp" + "github.com/aler9/gortsplib/v2/pkg/url" +) + +func mustParseURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + return u +} + +func TestMediaURL(t *testing.T) { + for _, ca := range []struct { + name string + sdp []byte + baseURL *url.URL + ur *url.URL + }{ + { + "missing control", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + }, + { + "absolute control", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:rtsp://localhost/path/trackID=7"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=7"), + }, + { + "relative control", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=5"), + }, + { + "relative control, subpath", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"), + }, + { + "relative control, url without slash", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"), + }, + { + "relative control, url with query", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + + "test?user=tmp&password=BagRep1&channel=1&stream=0.sdp"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + + "test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=5"), + }, + { + "relative control, url with special chars and query", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + + "te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/" + + "te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"), + }, + { + "relative control, url with query without question mark", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), + mustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"), + }, + { + "relative control, control is query", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:?ctype=video"), + mustParseURL("rtsp://192.168.1.99:554/test"), + mustParseURL("rtsp://192.168.1.99:554/test?ctype=video"), + }, + { + "relative control, control is query and no path", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:?ctype=video"), + mustParseURL("rtsp://192.168.1.99:554/"), + mustParseURL("rtsp://192.168.1.99:554/?ctype=video"), + }, + } { + t.Run(ca.name, func(t *testing.T) { + var sd sdp.SessionDescription + err := sd.Unmarshal(ca.sdp) + require.NoError(t, err) + + var medias Medias + err = medias.Unmarshal(sd.MediaDescriptions) + require.NoError(t, err) + + ur, err := medias[0].URL(ca.baseURL) + require.NoError(t, err) + require.Equal(t, ca.ur, ur) + }) + } +} + +func TestMediaURLError(t *testing.T) { + media := &Media{ + Type: "video", + Formats: []format.Format{&format.H264{}}, + } + _, err := media.URL(nil) + require.EqualError(t, err, "Content-Base header not provided") +} diff --git a/pkg/media/medias.go b/pkg/media/medias.go new file mode 100644 index 00000000..a9304edc --- /dev/null +++ b/pkg/media/medias.go @@ -0,0 +1,104 @@ +package media + +import ( + "fmt" + "reflect" + "strconv" + + psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/v2/pkg/sdp" +) + +// Medias is a list of media streams. +type Medias []*Media + +// Unmarshal decodes medias from the SDP format. +func (ms *Medias) Unmarshal(mds []*psdp.MediaDescription) error { + *ms = make(Medias, len(mds)) + + for i, md := range mds { + var m Media + err := m.unmarshal(md) + if err != nil { + return fmt.Errorf("media %d is invalid: %v", i+1, err) + } + (*ms)[i] = &m + } + + return nil +} + +// Marshal encodes the medias in SDP format. +func (ms Medias) Marshal(multicast bool) *sdp.SessionDescription { + var address string + if multicast { + address = "224.1.0.0" + } else { + address = "0.0.0.0" + } + + sout := &sdp.SessionDescription{ + SessionName: psdp.SessionName("Stream"), + Origin: psdp.Origin{ + Username: "-", + NetworkType: "IN", + AddressType: "IP4", + UnicastAddress: "127.0.0.1", + }, + // required by Darwin Streaming Server + ConnectionInformation: &psdp.ConnectionInformation{ + NetworkType: "IN", + AddressType: "IP4", + Address: &psdp.Address{Address: address}, + }, + TimeDescriptions: []psdp.TimeDescription{ + {Timing: psdp.Timing{StartTime: 0, StopTime: 0}}, + }, + MediaDescriptions: make([]*psdp.MediaDescription, len(ms)), + } + + for i, media := range ms { + sout.MediaDescriptions[i] = media.Marshal() + } + + return sout +} + +// Clone clones the media list. +func (ms Medias) Clone() Medias { + ret := make(Medias, len(ms)) + for i, media := range ms { + ret[i] = media.Clone() + } + return ret +} + +// CloneAndSetControls clones the media list and sets the control attribute +// of all medias in the list. +func (ms Medias) CloneAndSetControls() Medias { + ret := ms.Clone() + ret.SetControls() + return ret +} + +// SetControls sets the control attribute of all medias in the list. +func (ms Medias) SetControls() { + for i, media := range ms { + media.Control = "mediaID=" + strconv.FormatInt(int64(i), 10) + } +} + +// Find finds a certain format among all the formats in all the medias. +// If the format is found, it is inserted into trak, and format media is returned. +func (ms *Medias) Find(trak interface{}) *Media { + for _, media := range *ms { + for _, trakk := range media.Formats { + if reflect.TypeOf(trakk) == reflect.TypeOf(trak).Elem() { + reflect.ValueOf(trak).Elem().Set(reflect.ValueOf(trakk)) + return media + } + } + } + return nil +} diff --git a/pkg/media/medias_test.go b/pkg/media/medias_test.go new file mode 100644 index 00000000..2dc240dc --- /dev/null +++ b/pkg/media/medias_test.go @@ -0,0 +1,488 @@ +package media + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/sdp" +) + +var casesMedias = []struct { + name string + in string + out string + medias Medias +}{ + { + "one track for each media", + "v=0\r\n" + + "o=- 0 0 IN IP4 10.0.0.131\r\n" + + "s=Media Presentation\r\n" + + "i=samsung\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "b=AS:2632\r\n" + + "t=0 0\r\n" + + "a=control:rtsp://10.0.100.50/profile5/media.smp\r\n" + + "a=range:npt=now-\r\n" + + "m=video 42504 RTP/AVP 97\r\n" + + "b=AS:2560\r\n" + + "a=rtpmap:97 H264/90000\r\n" + + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\r\n" + + "a=cliprect:0,0,1080,1920\r\n" + + "a=framesize:97 1920-1080\r\n" + + "a=framerate:30.0\r\n" + + "a=fmtp:97 packetization-mode=1;profile-level-id=640028;sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" + + "m=audio 42506 RTP/AVP 0\r\n" + + "b=AS:64\r\n" + + "a=rtpmap:0 PCMU/8000\r\n" + + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" + + "a=recvonly\r\n" + + "m=application 42508 RTP/AVP 107\r\n" + + "b=AS:8\r\n", + "v=0\r\n" + + "o=- 0 0 IN IP4 127.0.0.1\r\n" + + "s=Stream\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "t=0 0\r\n" + + "m=video 0 RTP/AVP 97\r\n" + + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\r\n" + + "a=rtpmap:97 H264/90000\r\n" + + "a=fmtp:97 packetization-mode=1; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=; profile-level-id=640028\r\n" + + "m=audio 0 RTP/AVP 0\r\n" + + "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" + + "a=rtpmap:0 PCMU/8000\r\n" + + "m=application 0 RTP/AVP 107\r\n" + + "a=control\r\n", + Medias{ + { + Type: "video", + Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=v", + Formats: []format.Format{&format.H264{ + PayloadTyp: 97, + PacketizationMode: 1, + SPS: []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a}, + PPS: []byte{0x68, 0xee, 0x01, 0x9e, 0x2c}, + }}, + }, + { + Type: "audio", + Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=a", + Formats: []format.Format{&format.G711{ + MULaw: true, + }}, + }, + { + Type: "application", + Formats: []format.Format{&format.Generic{ + PayloadTyp: 107, + }}, + }, + }, + }, + { + "multiple tracks for each media", + "v=0\r\n" + + "o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" + + "s=-\r\n" + + "t=0 0\r\n" + + "a=group:BUNDLE audio video\r\n" + + "a=msid-semantic: WMS mediaStreamLocal\r\n" + + "m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "a=rtcp:9 IN IP4 0.0.0.0\r\n" + + "a=ice-ufrag:0D6Y\r\n" + + "a=ice-pwd:V3YEqLGAJJhUDUa13C/pKbWe\r\n" + + "a=ice-options:trickle renomination\r\n" + + "a=fingerprint:sha-256" + + " 5E:B5:97:8B:B4:D8:AE:2B:89:F6:82:44:47:69:77:83:05:29:C5:C8:EE:67:50:C3:77:6B:A7:BA:10:E3:08:B8\r\n" + + "a=setup:actpass\r\n" + + "a=mid:audio\r\n" + + "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" + + "a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" + + "a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" + + "a=sendonly\r\n" + + "a=rtcp-mux\r\n" + + "a=rtpmap:111 opus/48000/2\r\n" + + "a=rtcp-fb:111 transport-cc\r\n" + + "a=fmtp:111 minptime=10;useinbandfec=1\r\n" + + "a=rtpmap:103 ISAC/16000\r\n" + + "a=rtpmap:104 ISAC/32000\r\n" + + "a=rtpmap:9 G722/8000\r\n" + + "a=rtpmap:102 ILBC/8000\r\n" + + "a=rtpmap:0 PCMU/8000\r\n" + + "a=rtpmap:8 PCMA/8000\r\n" + + "a=rtpmap:106 CN/32000\r\n" + + "a=rtpmap:105 CN/16000\r\n" + + "a=rtpmap:13 CN/8000\r\n" + + "a=rtpmap:110 telephone-event/48000\r\n" + + "a=rtpmap:112 telephone-event/32000\r\n" + + "a=rtpmap:113 telephone-event/16000\r\n" + + "a=rtpmap:126 telephone-event/8000\r\n" + + "a=ssrc:3754810229 cname:CvU1TYqkVsjj5XOt\r\n" + + "a=ssrc:3754810229 msid:mediaStreamLocal 101\r\n" + + "a=ssrc:3754810229 mslabel:mediaStreamLocal\r\n" + + "a=ssrc:3754810229 label:101\r\n" + + "m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 125\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "a=rtcp:9 IN IP4 0.0.0.0\r\n" + + "a=ice-ufrag:0D6Y\r\n" + + "a=ice-pwd:V3YEqLGAJJhUDUa13C/pKbWe\r\n" + + "a=ice-options:trickle renomination\r\n" + + "a=fingerprint:sha-256" + + " 5E:B5:97:8B:B4:D8:AE:2B:89:F6:82:44:47:69:77:83:05:29:C5:C8:EE:67:50:C3:77:6B:A7:BA:10:E3:08:B8\r\n" + + "a=setup:actpass\r\n" + + "a=mid:video\r\n" + + "a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\n" + + "a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" + + "a=extmap:13 urn:3gpp:video-orientation\r\n" + + "a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" + + "a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n" + + "a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\n" + + "a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\n" + + "a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n" + + "a=sendonly\r\n" + + "a=rtcp-mux\r\n" + + "a=rtcp-rsize\r\n" + + "a=rtpmap:96 VP8/90000\r\n" + + "a=rtcp-fb:96 goog-remb\r\n" + + "a=rtcp-fb:96 transport-cc\r\n" + + "a=rtcp-fb:96 ccm fir\r\n" + + "a=rtcp-fb:96 nack\r\n" + + "a=rtcp-fb:96 nack pli\r\n" + + "a=rtpmap:97 rtx/90000\r\n" + + "a=fmtp:97 apt=96\r\n" + + "a=rtpmap:98 VP9/90000\r\n" + + "a=rtcp-fb:98 goog-remb\r\n" + + "a=rtcp-fb:98 transport-cc\r\n" + + "a=rtcp-fb:98 ccm fir\r\n" + + "a=rtcp-fb:98 nack\r\n" + + "a=rtcp-fb:98 nack pli\r\n" + + "a=rtpmap:99 rtx/90000\r\n" + + "a=fmtp:99 apt=98\r\n" + + "a=rtpmap:100 H264/90000\r\n" + + "a=rtcp-fb:100 goog-remb\r\n" + + "a=rtcp-fb:100 transport-cc\r\n" + + "a=rtcp-fb:100 ccm fir\r\n" + + "a=rtcp-fb:100 nack\r\n" + + "a=rtcp-fb:100 nack pli\r\n" + + "a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n" + + "a=rtpmap:101 rtx/90000\r\n" + + "a=fmtp:101 apt=100\r\n" + + "a=rtpmap:127 red/90000\r\n" + + "a=rtpmap:124 rtx/90000\r\n" + + "a=fmtp:124 apt=127\r\n" + + "a=rtpmap:125 ulpfec/90000\r\n" + + "a=ssrc-group:FID 2712436124 1733091158\r\n" + + "a=ssrc:2712436124 cname:CvU1TYqkVsjj5XOt\r\n" + + "a=ssrc:2712436124 msid:mediaStreamLocal 100\r\n" + + "a=ssrc:2712436124 mslabel:mediaStreamLocal\r\n" + + "a=ssrc:2712436124 label:100\r\n" + + "a=ssrc:1733091158 cname:CvU1TYqkVsjj5XOt\r\n" + + "a=ssrc:1733091158 msid:mediaStreamLocal 100\r\n" + + "a=ssrc:1733091158 mslabel:mediaStreamLocal\r\n" + + "a=ssrc:1733091158 label:100\r\n", + "v=0\r\n" + + "o=- 0 0 IN IP4 127.0.0.1\r\n" + + "s=Stream\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "t=0 0\r\n" + + "m=audio 0 RTP/AVP 111 103 104 9 102 0 8 106 105 13 110 112 113 126\r\n" + + "a=control\r\n" + + "a=rtpmap:111 opus/48000/2\r\n" + + "a=fmtp:111 sprop-stereo=1\r\n" + + "a=rtpmap:103 ISAC/16000\r\n" + + "a=rtpmap:104 ISAC/32000\r\n" + + "a=rtpmap:9 G722/8000\r\n" + + "a=rtpmap:102 ILBC/8000\r\n" + + "a=rtpmap:0 PCMU/8000\r\n" + + "a=rtpmap:8 PCMA/8000\r\n" + + "a=rtpmap:106 CN/32000\r\n" + + "a=rtpmap:105 CN/16000\r\n" + + "a=rtpmap:13 CN/8000\r\n" + + "a=rtpmap:110 telephone-event/48000\r\n" + + "a=rtpmap:112 telephone-event/32000\r\n" + + "a=rtpmap:113 telephone-event/16000\r\n" + + "a=rtpmap:126 telephone-event/8000\r\n" + + "m=video 0 RTP/AVP 96 97 98 99 100 101 127 124 125\r\n" + + "a=control\r\n" + + "a=rtpmap:96 VP8/90000\r\n" + + "a=rtpmap:97 rtx/90000\r\n" + + "a=fmtp:97 apt=96\r\n" + + "a=rtpmap:98 VP9/90000\r\n" + + "a=rtpmap:99 rtx/90000\r\n" + + "a=fmtp:99 apt=98\r\n" + + "a=rtpmap:100 H264/90000\r\n" + + "a=fmtp:100 packetization-mode=1\r\n" + + "a=rtpmap:101 rtx/90000\r\n" + + "a=fmtp:101 apt=100\r\n" + + "a=rtpmap:127 red/90000\r\n" + + "a=rtpmap:124 rtx/90000\r\n" + + "a=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec/90000\r\n", + Medias{ + { + Type: "audio", + Formats: []format.Format{ + &format.Opus{ + PayloadTyp: 111, + SampleRate: 48000, + ChannelCount: 2, + }, + &format.Generic{ + PayloadTyp: 103, + RTPMap: "ISAC/16000", + ClockRat: 16000, + }, + &format.Generic{ + PayloadTyp: 104, + RTPMap: "ISAC/32000", + ClockRat: 32000, + }, + &format.G722{}, + &format.Generic{ + PayloadTyp: 102, + RTPMap: "ILBC/8000", + ClockRat: 8000, + }, + &format.G711{ + MULaw: true, + }, + &format.G711{ + MULaw: false, + }, + &format.Generic{ + PayloadTyp: 106, + RTPMap: "CN/32000", + ClockRat: 32000, + }, + &format.Generic{ + PayloadTyp: 105, + RTPMap: "CN/16000", + ClockRat: 16000, + }, + &format.Generic{ + PayloadTyp: 13, + RTPMap: "CN/8000", + ClockRat: 8000, + }, + &format.Generic{ + PayloadTyp: 110, + RTPMap: "telephone-event/48000", + ClockRat: 48000, + }, + &format.Generic{ + PayloadTyp: 112, + RTPMap: "telephone-event/32000", + ClockRat: 32000, + }, + &format.Generic{ + PayloadTyp: 113, + RTPMap: "telephone-event/16000", + ClockRat: 16000, + }, + &format.Generic{ + PayloadTyp: 126, + RTPMap: "telephone-event/8000", + ClockRat: 8000, + }, + }, + }, + { + Type: "video", + Formats: []format.Format{ + &format.VP8{ + PayloadTyp: 96, + }, + &format.Generic{ + PayloadTyp: 97, + RTPMap: "rtx/90000", + FMTP: "apt=96", + ClockRat: 90000, + }, + &format.VP9{ + PayloadTyp: 98, + }, + &format.Generic{ + PayloadTyp: 99, + RTPMap: "rtx/90000", + FMTP: "apt=98", + ClockRat: 90000, + }, + &format.H264{ + PayloadTyp: 100, + PacketizationMode: 1, + }, + &format.Generic{ + PayloadTyp: 101, + RTPMap: "rtx/90000", + FMTP: "apt=100", + ClockRat: 90000, + }, + &format.Generic{ + PayloadTyp: 127, + RTPMap: "red/90000", + ClockRat: 90000, + }, + &format.Generic{ + PayloadTyp: 124, + RTPMap: "rtx/90000", + FMTP: "apt=127", + ClockRat: 90000, + }, + &format.Generic{ + PayloadTyp: 125, + RTPMap: "ulpfec/90000", + ClockRat: 90000, + }, + }, + }, + }, + }, + { + "multiple tracks for each media 2", + "v=0\r\n" + + "o=- 4158123474391860926 2 IN IP4 127.0.0.1\r\n" + + "s=-\r\n" + + "t=0 0\r\n" + + "m=video 42504 RTP/AVP 96 98\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=rtpmap:98 MetaData\r\n" + + "a=rtcp-mux\r\n" + + "a=fmtp:96 packetization-mode=1;profile-level-id=4d002a;" + + "sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==\r\n", + "v=0\r\n" + + "o=- 0 0 IN IP4 127.0.0.1\r\n" + + "s=Stream\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "t=0 0\r\n" + + "m=video 0 RTP/AVP 96 98\r\n" + + "a=control\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=fmtp:96 packetization-mode=1;" + + " sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==; profile-level-id=4D002A\r\n" + + "a=rtpmap:98 MetaData\r\n", + Medias{ + { + Type: "video", + Formats: []format.Format{ + &format.H264{ + PayloadTyp: 96, + SPS: []byte{ + 0x67, 0x4d, 0x00, 0x2a, 0x9d, 0xa8, 0x1e, 0x00, + 0x89, 0xf9, 0x66, 0xe0, 0x20, 0x20, 0x20, 0x40, + }, + PPS: []byte{0x68, 0xee, 0x3c, 0x80}, + PacketizationMode: 1, + }, + &format.Generic{ + PayloadTyp: 98, + RTPMap: "MetaData", + }, + }, + }, + }, + }, +} + +func TestMediasUnmarshal(t *testing.T) { + for _, ca := range casesMedias { + t.Run(ca.name, func(t *testing.T) { + var sdp sdp.SessionDescription + err := sdp.Unmarshal([]byte(ca.in)) + require.NoError(t, err) + + var medias Medias + err = medias.Unmarshal(sdp.MediaDescriptions) + require.NoError(t, err) + require.Equal(t, ca.medias, medias) + }) + } +} + +func TestMediasReadErrors(t *testing.T) { + for _, ca := range []struct { + name string + sdp string + err string + }{ + { + "invalid track", + "v=0\r\n" + + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + + "s=SDP Seminar\r\n" + + "m=video 0 RTP/AVP/TCP 96\r\n" + + "a=rtpmap:96 H265/90000\r\n" + + "a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwB4FwJA; " + + "sprop-sps=QgEBAWAAAAMAsAAAAwAAAwB4oAKggC8c1YgXuRZFL/y5/E/qbgQEBAE=; sprop-pps=RAHAcvBTJA==;\r\n" + + "a=control:streamid=0\r\n" + + "m=audio 0 RTP/AVP/TCP 97\r\n" + + "a=rtpmap:97 mpeg4-generic/44100/2\r\n" + + "a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" + + "a=control:streamid=1\r\n", + "media 2 is invalid: invalid AAC config (zzz1210)", + }, + } { + t.Run(ca.name, func(t *testing.T) { + var sd sdp.SessionDescription + err := sd.Unmarshal([]byte(ca.sdp)) + require.NoError(t, err) + + var medias Medias + err = medias.Unmarshal(sd.MediaDescriptions) + require.EqualError(t, err, ca.err) + }) + } +} + +func TestMediasMarshal(t *testing.T) { + for _, ca := range casesMedias { + t.Run(ca.name, func(t *testing.T) { + sdp := ca.medias.Marshal(false) + byts, err := sdp.Marshal() + require.NoError(t, err) + require.Equal(t, ca.out, string(byts)) + }) + } +} + +func TestMediasFind(t *testing.T) { + tr := &format.Generic{ + PayloadTyp: 97, + RTPMap: "rtx/90000", + FMTP: "apt=96", + ClockRat: 90000, + } + + md := &Media{ + Type: TypeVideo, + Formats: []format.Format{ + &format.VP8{ + PayloadTyp: 96, + }, + tr, + &format.VP9{ + PayloadTyp: 98, + }, + }, + } + + ms := Medias{ + { + Type: TypeAudio, + Formats: []format.Format{ + &format.Opus{ + PayloadTyp: 111, + SampleRate: 48000, + ChannelCount: 2, + }, + }, + }, + md, + } + + var trak *format.Generic + me := ms.Find(&trak) + require.Equal(t, md, me) + require.Equal(t, tr, trak) +} diff --git a/pkg/mpeg4audio/config.go b/pkg/mpeg4audio/config.go index 249ede53..9e86d3b7 100644 --- a/pkg/mpeg4audio/config.go +++ b/pkg/mpeg4audio/config.go @@ -3,7 +3,7 @@ package mpeg4audio import ( "fmt" - "github.com/aler9/gortsplib/pkg/bits" + "github.com/aler9/gortsplib/v2/pkg/bits" ) // Config is a MPEG-4 Audio configuration. diff --git a/pkg/rtcpreceiver/rtcpreceiver.go b/pkg/rtcpreceiver/rtcpreceiver.go index 9d0b9893..3a5123b7 100644 --- a/pkg/rtcpreceiver/rtcpreceiver.go +++ b/pkg/rtcpreceiver/rtcpreceiver.go @@ -27,19 +27,21 @@ type RTCPReceiver struct { mutex sync.Mutex // data from RTP packets - firstRTPReceived bool + initialized bool + timeInitialized bool sequenceNumberCycles uint16 - lastSequenceNumber *uint16 - lastRTPTimeRTP *uint32 - lastRTPTimeTime time.Time + lastSSRC uint32 + lastSequenceNumber uint16 + lastTimeRTP uint32 + lastTimeNTP time.Time totalLost uint32 totalLostSinceReport uint32 totalSinceReport uint32 jitter float64 - // data from rtcp packets - senderSSRC uint32 - lastSenderReportNTP *uint32 + // data from RTCP packets + senderInitialized bool + lastSenderReportNTP uint32 lastSenderReportTime time.Time terminate chan struct{} @@ -47,7 +49,10 @@ type RTCPReceiver struct { } // New allocates a RTCPReceiver. -func New(period time.Duration, receiverSSRC *uint32, clockRate int, +func New( + period time.Duration, + receiverSSRC *uint32, + clockRate int, writePacketRTCP func(rtcp.Packet), ) *RTCPReceiver { rr := &RTCPReceiver{ @@ -99,11 +104,7 @@ func (rr *RTCPReceiver) report(ts time.Time) rtcp.Packet { rr.mutex.Lock() defer rr.mutex.Unlock() - if rr.lastSenderReportNTP == nil || rr.lastSequenceNumber == nil { - return nil - } - - if rr.clockRate == 0 { + if !rr.senderInitialized || !rr.initialized || rr.clockRate == 0 { return nil } @@ -111,10 +112,10 @@ func (rr *RTCPReceiver) report(ts time.Time) rtcp.Packet { SSRC: rr.receiverSSRC, Reports: []rtcp.ReceptionReport{ { - SSRC: rr.senderSSRC, - LastSequenceNumber: uint32(rr.sequenceNumberCycles)<<16 | uint32(*rr.lastSequenceNumber), + SSRC: rr.lastSSRC, + LastSequenceNumber: uint32(rr.sequenceNumberCycles)<<16 | uint32(rr.lastSequenceNumber), // middle 32 bits out of 64 in the NTP timestamp of last sender report - LastSenderReport: *rr.lastSenderReportNTP, + LastSenderReport: rr.lastSenderReportNTP, // equivalent to taking the integer part after multiplying the // loss fraction by 256 FractionLost: uint8(float64(rr.totalLostSinceReport*256) / float64(rr.totalSinceReport)), @@ -134,27 +135,27 @@ func (rr *RTCPReceiver) report(ts time.Time) rtcp.Packet { return report } -// ProcessPacketRTP extracts the needed data from RTP packets. -func (rr *RTCPReceiver) ProcessPacketRTP(ts time.Time, pkt *rtp.Packet, ptsEqualsDTS bool) { +// ProcessPacket extracts the needed data from RTP packets. +func (rr *RTCPReceiver) ProcessPacket(pkt *rtp.Packet, ntp time.Time, ptsEqualsDTS bool) { rr.mutex.Lock() defer rr.mutex.Unlock() // first packet - if rr.lastSequenceNumber == nil { - rr.firstRTPReceived = true + if !rr.initialized { + rr.initialized = true rr.totalSinceReport = 1 - v := pkt.Header.SequenceNumber - rr.lastSequenceNumber = &v + rr.lastSSRC = pkt.SSRC + rr.lastSequenceNumber = pkt.SequenceNumber if ptsEqualsDTS { - v := pkt.Header.Timestamp - rr.lastRTPTimeRTP = &v - rr.lastRTPTimeTime = ts + rr.timeInitialized = true + rr.lastTimeRTP = pkt.Timestamp + rr.lastTimeNTP = ntp } // subsequent packets } else { - diff := int32(pkt.Header.SequenceNumber) - int32(*rr.lastSequenceNumber) + diff := int32(pkt.SequenceNumber) - int32(rr.lastSequenceNumber) // overflow if diff < -0x0FFF { @@ -162,7 +163,7 @@ func (rr *RTCPReceiver) ProcessPacketRTP(ts time.Time, pkt *rtp.Packet, ptsEqual } // detect lost packets - if pkt.Header.SequenceNumber != (*rr.lastSequenceNumber + 1) { + if pkt.SequenceNumber != (rr.lastSequenceNumber + 1) { rr.totalLost += uint32(uint16(diff) - 1) rr.totalLostSinceReport += uint32(uint16(diff) - 1) @@ -176,37 +177,42 @@ func (rr *RTCPReceiver) ProcessPacketRTP(ts time.Time, pkt *rtp.Packet, ptsEqual } rr.totalSinceReport += uint32(uint16(diff)) - v := pkt.Header.SequenceNumber - rr.lastSequenceNumber = &v + rr.lastSSRC = pkt.SSRC + rr.lastSequenceNumber = pkt.SequenceNumber if ptsEqualsDTS { - if rr.lastRTPTimeRTP != nil { + if rr.timeInitialized { // update jitter // https://tools.ietf.org/html/rfc3550#page-39 - D := ts.Sub(rr.lastRTPTimeTime).Seconds()*rr.clockRate - - (float64(pkt.Header.Timestamp) - float64(*rr.lastRTPTimeRTP)) + D := ntp.Sub(rr.lastTimeNTP).Seconds()*rr.clockRate - + (float64(pkt.Timestamp) - float64(rr.lastTimeRTP)) if D < 0 { D = -D } rr.jitter += (D - rr.jitter) / 16 } - v := pkt.Header.Timestamp - rr.lastRTPTimeRTP = &v - rr.lastRTPTimeTime = ts + rr.timeInitialized = true + rr.lastTimeRTP = pkt.Timestamp + rr.lastTimeNTP = ntp + rr.lastSSRC = pkt.SSRC } } } -// ProcessPacketRTCP extracts the needed data from RTCP packets. -func (rr *RTCPReceiver) ProcessPacketRTCP(ts time.Time, pkt rtcp.Packet) { - if sr, ok := (pkt).(*rtcp.SenderReport); ok { - rr.mutex.Lock() - defer rr.mutex.Unlock() +// ProcessSenderReport extracts the needed data from RTCP sender reports. +func (rr *RTCPReceiver) ProcessSenderReport(sr *rtcp.SenderReport, ts time.Time) { + rr.mutex.Lock() + defer rr.mutex.Unlock() - rr.senderSSRC = sr.SSRC - v := uint32(sr.NTPTime >> 16) - rr.lastSenderReportNTP = &v - rr.lastSenderReportTime = ts - } + rr.senderInitialized = true + rr.lastSenderReportNTP = uint32(sr.NTPTime >> 16) + rr.lastSenderReportTime = ts +} + +// LastSSRC returns the SSRC of the last RTP packet. +func (rr *RTCPReceiver) LastSSRC() (uint32, bool) { + rr.mutex.Lock() + defer rr.mutex.Unlock() + return rr.lastSSRC, rr.initialized } diff --git a/pkg/rtcpreceiver/rtcpreceiver_test.go b/pkg/rtcpreceiver/rtcpreceiver_test.go index 1ae1a73b..a06d8f8e 100644 --- a/pkg/rtcpreceiver/rtcpreceiver_test.go +++ b/pkg/rtcpreceiver/rtcpreceiver_test.go @@ -41,7 +41,7 @@ func TestRTCPReceiverBase(t *testing.T) { OctetCount: 859127, } ts := time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTCP(ts, &srPkt) + rr.ProcessSenderReport(&srPkt, ts) rtpPkt := rtp.Packet{ Header: rtp.Header{ @@ -55,7 +55,7 @@ func TestRTCPReceiverBase(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -69,7 +69,7 @@ func TestRTCPReceiverBase(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 21, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) <-done } @@ -107,7 +107,7 @@ func TestRTCPReceiverOverflow(t *testing.T) { OctetCount: 859127, } ts := time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTCP(ts, &srPkt) + rr.ProcessSenderReport(&srPkt, ts) rtpPkt := rtp.Packet{ Header: rtp.Header{ @@ -121,7 +121,7 @@ func TestRTCPReceiverOverflow(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -135,7 +135,7 @@ func TestRTCPReceiverOverflow(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) <-done } @@ -176,7 +176,7 @@ func TestRTCPReceiverPacketLost(t *testing.T) { OctetCount: 859127, } ts := time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTCP(ts, &srPkt) + rr.ProcessSenderReport(&srPkt, ts) rtpPkt := rtp.Packet{ Header: rtp.Header{ @@ -190,7 +190,7 @@ func TestRTCPReceiverPacketLost(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -204,7 +204,7 @@ func TestRTCPReceiverPacketLost(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) <-done } @@ -245,7 +245,7 @@ func TestRTCPReceiverOverflowPacketLost(t *testing.T) { OctetCount: 859127, } ts := time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTCP(ts, &srPkt) + rr.ProcessSenderReport(&srPkt, ts) rtpPkt := rtp.Packet{ Header: rtp.Header{ @@ -259,7 +259,7 @@ func TestRTCPReceiverOverflowPacketLost(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -273,7 +273,7 @@ func TestRTCPReceiverOverflowPacketLost(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) <-done } @@ -310,7 +310,7 @@ func TestRTCPReceiverJitter(t *testing.T) { OctetCount: 859127, } ts := time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTCP(ts, &srPkt) + rr.ProcessSenderReport(&srPkt, ts) rtpPkt := rtp.Packet{ Header: rtp.Header{ @@ -324,7 +324,7 @@ func TestRTCPReceiverJitter(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -338,7 +338,7 @@ func TestRTCPReceiverJitter(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 21, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, true) + rr.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -352,7 +352,7 @@ func TestRTCPReceiverJitter(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 22, 0, time.UTC) - rr.ProcessPacketRTP(ts, &rtpPkt, false) + rr.ProcessPacket(&rtpPkt, ts, false) <-done } diff --git a/pkg/rtcpsender/rtcpsender.go b/pkg/rtcpsender/rtcpsender.go index f98e3bb2..cab08b2c 100644 --- a/pkg/rtcpsender/rtcpsender.go +++ b/pkg/rtcpsender/rtcpsender.go @@ -13,43 +13,53 @@ var now = time.Now // RTCPSender is a utility to generate RTCP sender reports. type RTCPSender struct { - period time.Duration clockRate float64 writePacketRTCP func(rtcp.Packet) mutex sync.Mutex + started bool + period time.Duration // data from RTP packets - senderSSRC *uint32 - lastRTPTimeRTP *uint32 - lastRTPTimeTime time.Time - packetCount uint32 - octetCount uint32 + initialized bool + lastTimeRTP uint32 + lastTimeNTP time.Time + lastSSRC uint32 + lastSequenceNumber uint16 + packetCount uint32 + octetCount uint32 terminate chan struct{} done chan struct{} } // New allocates a RTCPSender. -func New(period time.Duration, clockRate int, +func New( + clockRate int, writePacketRTCP func(rtcp.Packet), ) *RTCPSender { rs := &RTCPSender{ - period: period, clockRate: float64(clockRate), writePacketRTCP: writePacketRTCP, terminate: make(chan struct{}), done: make(chan struct{}), } - go rs.run() - return rs } // Close closes the RTCPSender. func (rs *RTCPSender) Close() { - close(rs.terminate) - <-rs.done + if rs.started { + close(rs.terminate) + <-rs.done + } +} + +// Start starts the periodic generation of RTCP sender reports. +func (rs *RTCPSender) Start(period time.Duration) { + rs.started = true + rs.period = period + go rs.run() } func (rs *RTCPSender) run() { @@ -76,44 +86,52 @@ func (rs *RTCPSender) report(ts time.Time) rtcp.Packet { rs.mutex.Lock() defer rs.mutex.Unlock() - if rs.senderSSRC == nil || rs.lastRTPTimeRTP == nil { - return nil - } - - if rs.clockRate == 0 { + if !rs.initialized || rs.clockRate == 0 { return nil } return &rtcp.SenderReport{ - SSRC: *rs.senderSSRC, + SSRC: rs.lastSSRC, NTPTime: func() uint64 { // seconds since 1st January 1900 // higher 32 bits are the integer part, lower 32 bits are the fractional part s := uint64(ts.UnixNano()) + 2208988800*1000000000 return (s/1000000000)<<32 | (s % 1000000000) }(), - RTPTime: *rs.lastRTPTimeRTP + uint32((ts.Sub(rs.lastRTPTimeTime)).Seconds()*rs.clockRate), + RTPTime: rs.lastTimeRTP + uint32((ts.Sub(rs.lastTimeNTP)).Seconds()*rs.clockRate), PacketCount: rs.packetCount, OctetCount: rs.octetCount, } } -// ProcessPacketRTP extracts the needed data from RTP packets. -func (rs *RTCPSender) ProcessPacketRTP(ts time.Time, pkt *rtp.Packet, ptsEqualsDTS bool) { +// ProcessPacket extracts the needed data from RTP packets. +func (rs *RTCPSender) ProcessPacket(pkt *rtp.Packet, ntp time.Time, ptsEqualsDTS bool) { rs.mutex.Lock() defer rs.mutex.Unlock() - if rs.senderSSRC == nil { - v := pkt.SSRC - rs.senderSSRC = &v + if ptsEqualsDTS { + rs.initialized = true + rs.lastTimeRTP = pkt.Timestamp + rs.lastTimeNTP = ntp } - if ptsEqualsDTS { - v := pkt.Timestamp - rs.lastRTPTimeRTP = &v - rs.lastRTPTimeTime = ts - } + rs.lastSSRC = pkt.SSRC + rs.lastSequenceNumber = pkt.SequenceNumber rs.packetCount++ rs.octetCount += uint32(len(pkt.Payload)) } + +// LastSSRC returns the SSRC of the last RTP packet. +func (rs *RTCPSender) LastSSRC() (uint32, bool) { + rs.mutex.Lock() + defer rs.mutex.Unlock() + return rs.lastSSRC, rs.initialized +} + +// LastPacketData returns metadata of the last RTP packet. +func (rs *RTCPSender) LastPacketData() (uint16, uint32, time.Time, bool) { + rs.mutex.Lock() + defer rs.mutex.Unlock() + return rs.lastSequenceNumber, rs.lastTimeRTP, rs.lastTimeNTP, rs.initialized +} diff --git a/pkg/rtcpsender/rtcpsender_test.go b/pkg/rtcpsender/rtcpsender_test.go index 48b3dcc2..d9304a3b 100644 --- a/pkg/rtcpsender/rtcpsender_test.go +++ b/pkg/rtcpsender/rtcpsender_test.go @@ -15,7 +15,7 @@ func TestRTCPSender(t *testing.T) { } done := make(chan struct{}) - rs := New(250*time.Millisecond, 90000, func(pkt rtcp.Packet) { + rs := New(90000, func(pkt rtcp.Packet) { require.Equal(t, &rtcp.SenderReport{ SSRC: 0xba9da416, NTPTime: 14690122083862791680, @@ -27,6 +27,7 @@ func TestRTCPSender(t *testing.T) { }) defer rs.Close() + rs.Start(250 * time.Millisecond) time.Sleep(400 * time.Millisecond) rtpPkt := rtp.Packet{ @@ -41,7 +42,7 @@ func TestRTCPSender(t *testing.T) { Payload: []byte("\x00\x00"), } ts := time.Date(2008, 0o5, 20, 22, 15, 20, 0, time.UTC) - rs.ProcessPacketRTP(ts, &rtpPkt, true) + rs.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -55,7 +56,7 @@ func TestRTCPSender(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 500000000, time.UTC) - rs.ProcessPacketRTP(ts, &rtpPkt, true) + rs.ProcessPacket(&rtpPkt, ts, true) rtpPkt = rtp.Packet{ Header: rtp.Header{ @@ -69,7 +70,7 @@ func TestRTCPSender(t *testing.T) { Payload: []byte("\x00\x00"), } ts = time.Date(2008, 0o5, 20, 22, 15, 20, 500000000, time.UTC) - rs.ProcessPacketRTP(ts, &rtpPkt, false) + rs.ProcessPacket(&rtpPkt, ts, false) <-done } diff --git a/pkg/rtpcodecs/rtpcodecs.go b/pkg/rtpcodecs/rtpcodecs.go index 7f5e3a35..6dc43baf 100644 --- a/pkg/rtpcodecs/rtpcodecs.go +++ b/pkg/rtpcodecs/rtpcodecs.go @@ -1,2 +1,2 @@ -// Package rtpcodecs contains utilities to convert codec-specific elements from/to RTP packets. +// Package rtpcodecs contains utilities that convert RTP packets into codec-specific elements and vice versa. package rtpcodecs diff --git a/pkg/rtpcodecs/rtph264/decoder.go b/pkg/rtpcodecs/rtph264/decoder.go index 4490fd12..696030f3 100644 --- a/pkg/rtpcodecs/rtph264/decoder.go +++ b/pkg/rtpcodecs/rtph264/decoder.go @@ -8,8 +8,8 @@ import ( "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/h264" - "github.com/aler9/gortsplib/pkg/rtptimedec" + "github.com/aler9/gortsplib/v2/pkg/h264" + "github.com/aler9/gortsplib/v2/pkg/rtptimedec" ) // ErrMorePacketsNeeded is returned when more packets are needed. diff --git a/pkg/rtpcodecs/rtph264/nalutype.go b/pkg/rtpcodecs/rtph264/nalutype.go index 9779d478..0aee8e2b 100644 --- a/pkg/rtpcodecs/rtph264/nalutype.go +++ b/pkg/rtpcodecs/rtph264/nalutype.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/aler9/gortsplib/pkg/h264" + "github.com/aler9/gortsplib/v2/pkg/h264" ) type naluType h264.NALUType diff --git a/pkg/rtpcodecs/rtph265/decoder.go b/pkg/rtpcodecs/rtph265/decoder.go index 57058627..711d193f 100644 --- a/pkg/rtpcodecs/rtph265/decoder.go +++ b/pkg/rtpcodecs/rtph265/decoder.go @@ -7,7 +7,7 @@ import ( "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/rtptimedec" + "github.com/aler9/gortsplib/v2/pkg/rtptimedec" ) const ( diff --git a/pkg/rtpcodecs/rtplpcm/decoder.go b/pkg/rtpcodecs/rtplpcm/decoder.go index 3cd6d38e..4e48ea01 100644 --- a/pkg/rtpcodecs/rtplpcm/decoder.go +++ b/pkg/rtpcodecs/rtplpcm/decoder.go @@ -6,7 +6,7 @@ import ( "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/rtptimedec" + "github.com/aler9/gortsplib/v2/pkg/rtptimedec" ) // Decoder is a RTP/LPCM decoder. diff --git a/pkg/rtpcodecs/rtpmpeg4audio/decoder.go b/pkg/rtpcodecs/rtpmpeg4audio/decoder.go index 604cf909..064fd9ee 100644 --- a/pkg/rtpcodecs/rtpmpeg4audio/decoder.go +++ b/pkg/rtpcodecs/rtpmpeg4audio/decoder.go @@ -7,9 +7,9 @@ import ( "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/bits" - "github.com/aler9/gortsplib/pkg/mpeg4audio" - "github.com/aler9/gortsplib/pkg/rtptimedec" + "github.com/aler9/gortsplib/v2/pkg/bits" + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" + "github.com/aler9/gortsplib/v2/pkg/rtptimedec" ) // ErrMorePacketsNeeded is returned when more packets are needed. diff --git a/pkg/rtpcodecs/rtpmpeg4audio/decoder_test.go b/pkg/rtpcodecs/rtpmpeg4audio/decoder_test.go index 2e4d31b8..6bb42a0b 100644 --- a/pkg/rtpcodecs/rtpmpeg4audio/decoder_test.go +++ b/pkg/rtpcodecs/rtpmpeg4audio/decoder_test.go @@ -8,7 +8,7 @@ import ( "github.com/pion/rtp" "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/mpeg4audio" + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" ) func mergeBytes(vals ...[]byte) []byte { diff --git a/pkg/rtpcodecs/rtpmpeg4audio/encoder.go b/pkg/rtpcodecs/rtpmpeg4audio/encoder.go index 65b54ec3..62dbbd2a 100644 --- a/pkg/rtpcodecs/rtpmpeg4audio/encoder.go +++ b/pkg/rtpcodecs/rtpmpeg4audio/encoder.go @@ -6,8 +6,8 @@ import ( "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/bits" - "github.com/aler9/gortsplib/pkg/mpeg4audio" + "github.com/aler9/gortsplib/v2/pkg/bits" + "github.com/aler9/gortsplib/v2/pkg/mpeg4audio" ) const ( diff --git a/pkg/rtpcodecs/rtpsimpleaudio/decoder.go b/pkg/rtpcodecs/rtpsimpleaudio/decoder.go index 08fe6d86..67379183 100644 --- a/pkg/rtpcodecs/rtpsimpleaudio/decoder.go +++ b/pkg/rtpcodecs/rtpsimpleaudio/decoder.go @@ -5,7 +5,7 @@ import ( "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/rtptimedec" + "github.com/aler9/gortsplib/v2/pkg/rtptimedec" ) // Decoder is a RTP/simple audio decoder. diff --git a/pkg/rtpcodecs/rtpvp8/decoder.go b/pkg/rtpcodecs/rtpvp8/decoder.go index 190be7c4..3030da5b 100644 --- a/pkg/rtpcodecs/rtpvp8/decoder.go +++ b/pkg/rtpcodecs/rtpvp8/decoder.go @@ -8,7 +8,7 @@ import ( "github.com/pion/rtp" "github.com/pion/rtp/codecs" - "github.com/aler9/gortsplib/pkg/rtptimedec" + "github.com/aler9/gortsplib/v2/pkg/rtptimedec" ) // ErrMorePacketsNeeded is returned when more packets are needed. diff --git a/pkg/rtpcodecs/rtpvp9/decoder.go b/pkg/rtpcodecs/rtpvp9/decoder.go index 17b1adae..10345a9b 100644 --- a/pkg/rtpcodecs/rtpvp9/decoder.go +++ b/pkg/rtpcodecs/rtpvp9/decoder.go @@ -8,7 +8,7 @@ import ( "github.com/pion/rtp" "github.com/pion/rtp/codecs" - "github.com/aler9/gortsplib/pkg/rtptimedec" + "github.com/aler9/gortsplib/v2/pkg/rtptimedec" ) // ErrMorePacketsNeeded is returned when more packets are needed. diff --git a/ptsequalsdts.go b/ptsequalsdts.go deleted file mode 100644 index f3d71d7a..00000000 --- a/ptsequalsdts.go +++ /dev/null @@ -1,71 +0,0 @@ -package gortsplib - -import ( - "github.com/pion/rtp" - - "github.com/aler9/gortsplib/pkg/h264" -) - -// find IDR NALUs without decoding RTP -func rtpH264ContainsIDR(pkt *rtp.Packet) bool { - if len(pkt.Payload) == 0 { - return false - } - - typ := h264.NALUType(pkt.Payload[0] & 0x1F) - - switch typ { - case h264.NALUTypeIDR: - return true - - case 24: // STAP-A - payload := pkt.Payload[1:] - - for len(payload) > 0 { - if len(payload) < 2 { - return false - } - - size := uint16(payload[0])<<8 | uint16(payload[1]) - payload = payload[2:] - - if size == 0 || int(size) > len(payload) { - return false - } - - nalu := payload[:size] - payload = payload[size:] - - typ = h264.NALUType(nalu[0] & 0x1F) - if typ == h264.NALUTypeIDR { - return true - } - } - - return false - - case 28: // FU-A - if len(pkt.Payload) < 2 { - return false - } - - start := pkt.Payload[1] >> 7 - if start != 1 { - return false - } - - typ := h264.NALUType(pkt.Payload[1] & 0x1F) - return (typ == h264.NALUTypeIDR) - - default: - return false - } -} - -func ptsEqualsDTS(track Track, pkt *rtp.Packet) bool { - if _, ok := track.(*TrackH264); ok { - return rtpH264ContainsIDR(pkt) - } - - return true -} diff --git a/server.go b/server.go index 2b31df35..1aa2f26b 100644 --- a/server.go +++ b/server.go @@ -10,8 +10,8 @@ import ( "sync" "time" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/liberrors" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/liberrors" ) func extractPort(address string) (int, error) { @@ -132,22 +132,21 @@ type Server struct { // udpReceiverReportPeriod time.Duration - udpSenderReportPeriod time.Duration + senderReportPeriod time.Duration sessionTimeout time.Duration checkStreamPeriod time.Duration - ctx context.Context - ctxCancel func() - wg sync.WaitGroup - multicastNet *net.IPNet - multicastNextIP net.IP - tcpListener net.Listener - udpRTPListener *serverUDPListener - udpRTCPListener *serverUDPListener - udpRTPPacketBuffer *rtpPacketMultiBuffer - sessions map[string]*ServerSession - conns map[*ServerConn]struct{} - closeError error + ctx context.Context + ctxCancel func() + wg sync.WaitGroup + multicastNet *net.IPNet + multicastNextIP net.IP + tcpListener net.Listener + udpRTPListener *serverUDPListener + udpRTCPListener *serverUDPListener + sessions map[string]*ServerSession + conns map[*ServerConn]struct{} + closeError error // in connClose chan *ServerConn @@ -187,8 +186,8 @@ func (s *Server) Start() error { if s.udpReceiverReportPeriod == 0 { s.udpReceiverReportPeriod = 10 * time.Second } - if s.udpSenderReportPeriod == 0 { - s.udpSenderReportPeriod = 10 * time.Second + if s.senderReportPeriod == 0 { + s.senderReportPeriod = 10 * time.Second } if s.sessionTimeout == 0 { s.sessionTimeout = 1 * 60 * time.Second @@ -243,8 +242,6 @@ func (s *Server) Start() error { s.udpRTPListener.close() return err } - - s.udpRTPPacketBuffer = newRTPPacketMultiBuffer(uint64(s.ReadBufferCount)) } if s.MulticastIPRange != "" && (s.MulticastRTPPort == 0 || s.MulticastRTCPPort == 0) || @@ -293,7 +290,6 @@ func (s *Server) Start() error { } s.multicastNextIP = s.multicastNet.IP - s.udpRTPPacketBuffer = newRTPPacketMultiBuffer(uint64(s.ReadBufferCount)) } var err error diff --git a/server_read_test.go b/server_play_test.go similarity index 86% rename from server_read_test.go rename to server_play_test.go index 4d185944..997b8704 100644 --- a/server_read_test.go +++ b/server_play_test.go @@ -11,14 +11,16 @@ import ( "github.com/pion/rtcp" "github.com/pion/rtp" - psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" "golang.org/x/net/ipv4" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/headers" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/sdp" + "github.com/aler9/gortsplib/v2/pkg/url" ) func multicastCapableIP(t *testing.T) string { @@ -47,67 +49,53 @@ func multicastCapableIP(t *testing.T) string { return "" } -func TestServerReadSetupPath(t *testing.T) { +func TestServerPlaySetupPath(t *testing.T) { for _, ca := range []struct { - name string - url string - path string - trackID int + name string + url string + path string }{ { "normal", - "rtsp://localhost:8554/teststream/trackID=2", + "rtsp://localhost:8554/teststream/mediaID=2", "teststream", - 2, }, { "with query", - "rtsp://localhost:8554/teststream?testing=123/trackID=4", + "rtsp://localhost:8554/teststream?testing=123/mediaID=4", "teststream", - 4, }, { // this is needed to support reading mpegts with ffmpeg - "without track id", + "without media id", "rtsp://localhost:8554/teststream/", "teststream", - 0, }, { "subpath", - "rtsp://localhost:8554/test/stream/trackID=0", + "rtsp://localhost:8554/test/stream/mediaID=0", "test/stream", - 0, }, { - "subpath without track id", + "subpath without media id", "rtsp://localhost:8554/test/stream/", "test/stream", - 0, }, { "subpath with query", - "rtsp://localhost:8554/test/stream?testing=123/trackID=4", + "rtsp://localhost:8554/test/stream?testing=123/mediaID=4", "test/stream", - 4, }, } { t.Run(ca.name, func(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track, track, track, track, track}) + medi := testH264Media.Clone() + stream := NewServerStream(media.Medias{medi, medi, medi, medi, medi}) defer stream.Close() s := &Server{ Handler: &testServerHandler{ onSetup: func(ctx *ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) { require.Equal(t, ca.path, ctx.Path) - require.Equal(t, ca.trackID, ctx.TrackID) return &base.Response{ StatusCode: base.StatusOK, }, stream, nil @@ -135,7 +123,7 @@ func TestServerReadSetupPath(t *testing.T) { v := headers.TransportModePlay return &v }(), - InterleavedIDs: &[2]int{ca.trackID * 2, (ca.trackID * 2) + 1}, + InterleavedIDs: &[2]int{0, 1}, } res, err := writeReqReadRes(conn, base.Request{ @@ -152,7 +140,7 @@ func TestServerReadSetupPath(t *testing.T) { } } -func TestServerReadSetupErrors(t *testing.T) { +func TestServerPlaySetupErrors(t *testing.T) { for _, ca := range []string{ "different paths", "double setup", @@ -161,14 +149,7 @@ func TestServerReadSetupErrors(t *testing.T) { t.Run(ca, func(t *testing.T) { nconnClosed := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) if ca == "closed stream" { stream.Close() } else { @@ -180,10 +161,10 @@ func TestServerReadSetupErrors(t *testing.T) { onConnClose: func(ctx *ServerHandlerOnConnCloseCtx) { switch ca { case "different paths": - require.EqualError(t, ctx.Error, "can't setup tracks with different paths") + require.EqualError(t, ctx.Error, "can't setup medias with different paths") case "double setup": - require.EqualError(t, ctx.Error, "track 0 has already been setup") + require.EqualError(t, ctx.Error, "media has already been setup") case "closed stream": require.EqualError(t, ctx.Error, "stream is closed") @@ -223,7 +204,7 @@ func TestServerReadSetupErrors(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": th.Marshal(), @@ -242,7 +223,7 @@ func TestServerReadSetupErrors(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/test12stream/trackID=1"), + URL: mustParseURL("rtsp://localhost:8554/test12stream/mediaID=1"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": th.Marshal(), @@ -263,7 +244,7 @@ func TestServerReadSetupErrors(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": th.Marshal(), @@ -283,15 +264,8 @@ func TestServerReadSetupErrors(t *testing.T) { } } -func TestServerReadSetupErrorSameUDPPortsAndIP(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) +func TestServerPlaySetupErrorSameUDPPortsAndIP(t *testing.T) { + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() first := int32(1) errorRecv := make(chan struct{}) @@ -346,7 +320,7 @@ func TestServerReadSetupErrorSameUDPPortsAndIP(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -364,7 +338,7 @@ func TestServerReadSetupErrorSameUDPPortsAndIP(t *testing.T) { <-errorRecv } -func TestServerRead(t *testing.T) { +func TestServerPlay(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -378,14 +352,7 @@ func TestServerRead(t *testing.T) { sessionClosed := make(chan struct{}) framesReceived := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() counter := uint64(0) @@ -415,32 +382,33 @@ func TestServerRead(t *testing.T) { // send RTCP packets directly to the session. // these are sent after the response, only if onPlay returns StatusOK. if transport != "multicast" { - ctx.Session.WritePacketRTCP(0, &testRTCPPacket) + ctx.Session.WritePacketRTCP(stream.Medias()[0], &testRTCPPacket) } + ctx.Session.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) { + // ignore multicast loopback + if transport == "multicast" && atomic.AddUint64(&counter, 1) <= 1 { + return + } + + require.Equal(t, stream.Medias()[0], medi) + require.Equal(t, &testRTCPPacket, pkt) + close(framesReceived) + }) + // the session is added to the stream only after onPlay returns // with StatusOK; therefore we must wait before calling // ServerStream.WritePacket*() go func() { time.Sleep(1 * time.Second) - stream.WritePacketRTCP(0, &testRTCPPacket) - stream.WritePacketRTP(0, &testRTPPacket) + stream.WritePacketRTCP(stream.Medias()[0], &testRTCPPacket) + stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket) }() return &base.Response{ StatusCode: base.StatusOK, }, nil }, - onPacketRTCP: func(ctx *ServerHandlerOnPacketRTCPCtx) { - // ignore multicast loopback - if transport == "multicast" && atomic.AddUint64(&counter, 1) <= 1 { - return - } - - require.Equal(t, 0, ctx.TrackID) - require.Equal(t, &testRTCPPacket, ctx.Packet) - close(framesReceived) - }, onGetParameter: func(ctx *ServerHandlerOnGetParameterCtx) (*base.Response, error) { return &base.Response{ StatusCode: base.StatusOK, @@ -511,7 +479,7 @@ func TestServerRead(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://" + listenIP + ":8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://" + listenIP + ":8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -717,7 +685,7 @@ func TestServerRead(t *testing.T) { } } -func TestServerReadDecodeErrors(t *testing.T) { +func TestServerPlayDecodeErrors(t *testing.T) { for _, ca := range []struct { proto string name string @@ -730,14 +698,7 @@ func TestServerReadDecodeErrors(t *testing.T) { t.Run(ca.proto+" "+ca.name, func(t *testing.T) { errorRecv := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -804,7 +765,7 @@ func TestServerReadDecodeErrors(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -878,15 +839,10 @@ func TestServerReadDecodeErrors(t *testing.T) { } } -func TestServerReadRTCPReport(t *testing.T) { +func TestServerPlayRTCPReport(t *testing.T) { for _, ca := range []string{"udp", "tcp"} { t.Run(ca, func(t *testing.T) { - stream := NewServerStream(Tracks{&TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - }}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -902,10 +858,10 @@ func TestServerReadRTCPReport(t *testing.T) { }, nil }, }, - udpSenderReportPeriod: 500 * time.Millisecond, - RTSPAddress: "localhost:8554", - UDPRTPAddress: "127.0.0.1:8000", - UDPRTCPAddress: "127.0.0.1:8001", + senderReportPeriod: 1 * time.Second, + RTSPAddress: "localhost:8554", + UDPRTPAddress: "127.0.0.1:8000", + UDPRTCPAddress: "127.0.0.1:8001", } err := s.Start() @@ -938,7 +894,7 @@ func TestServerReadRTCPReport(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -975,7 +931,7 @@ func TestServerReadRTCPReport(t *testing.T) { require.Equal(t, base.StatusOK, res.StatusCode) for i := 0; i < 2; i++ { - stream.WritePacketRTP(0, &rtp.Packet{ + stream.WritePacketRTP(stream.Medias()[0], &rtp.Packet{ Header: rtp.Header{ Version: 2, PayloadType: 96, @@ -1029,15 +985,8 @@ func TestServerReadRTCPReport(t *testing.T) { } } -func TestServerReadVLCMulticast(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) +func TestServerPlayVLCMulticast(t *testing.T) { + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() listenIP := multicastCapableIP(t) @@ -1075,25 +1024,18 @@ func TestServerReadVLCMulticast(t *testing.T) { require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) - var desc psdp.SessionDescription + var desc sdp.SessionDescription err = desc.Unmarshal(res.Body) require.NoError(t, err) require.Equal(t, "224.1.0.0", desc.ConnectionInformation.Address.Address) } -func TestServerReadTCPResponseBeforeFrames(t *testing.T) { +func TestServerPlayTCPResponseBeforeFrames(t *testing.T) { writerDone := make(chan struct{}) writerTerminate := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1112,7 +1054,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) { go func() { defer close(writerDone) - stream.WritePacketRTP(0, &testRTPPacket) + stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket) t := time.NewTicker(50 * time.Millisecond) defer t.Stop() @@ -1120,7 +1062,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) { for { select { case <-t.C: - stream.WritePacketRTP(0, &testRTPPacket) + stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket) case <-writerTerminate: return } @@ -1147,7 +1089,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -1186,15 +1128,8 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) { require.NoError(t, err) } -func TestServerReadPlayPlay(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) +func TestServerPlayPlayPlay(t *testing.T) { + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1226,7 +1161,7 @@ func TestServerReadPlayPlay(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -1273,19 +1208,12 @@ func TestServerReadPlayPlay(t *testing.T) { require.Equal(t, base.StatusOK, res.StatusCode) } -func TestServerReadPlayPausePlay(t *testing.T) { +func TestServerPlayPlayPausePlay(t *testing.T) { writerStarted := false writerDone := make(chan struct{}) writerTerminate := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1311,7 +1239,7 @@ func TestServerReadPlayPausePlay(t *testing.T) { for { select { case <-t.C: - stream.WritePacketRTP(0, &testRTPPacket) + stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket) case <-writerTerminate: return } @@ -1343,7 +1271,7 @@ func TestServerReadPlayPausePlay(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -1401,18 +1329,11 @@ func TestServerReadPlayPausePlay(t *testing.T) { require.Equal(t, base.StatusOK, res.StatusCode) } -func TestServerReadPlayPausePause(t *testing.T) { +func TestServerPlayPlayPausePause(t *testing.T) { writerDone := make(chan struct{}) writerTerminate := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1436,7 +1357,7 @@ func TestServerReadPlayPausePause(t *testing.T) { for { select { case <-t.C: - stream.WritePacketRTP(0, &testRTPPacket) + stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket) case <-writerTerminate: return } @@ -1467,7 +1388,7 @@ func TestServerReadPlayPausePause(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -1531,7 +1452,7 @@ func TestServerReadPlayPausePause(t *testing.T) { require.Equal(t, base.StatusOK, res.StatusCode) } -func TestServerReadTimeout(t *testing.T) { +func TestServerPlayTimeout(t *testing.T) { for _, transport := range []string{ "udp", "multicast", @@ -1540,14 +1461,7 @@ func TestServerReadTimeout(t *testing.T) { t.Run(transport, func(t *testing.T) { sessionClosed := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1618,7 +1532,7 @@ func TestServerReadTimeout(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -1647,7 +1561,7 @@ func TestServerReadTimeout(t *testing.T) { } } -func TestServerReadWithoutTeardown(t *testing.T) { +func TestServerPlayWithoutTeardown(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -1656,14 +1570,7 @@ func TestServerReadWithoutTeardown(t *testing.T) { nconnClosed := make(chan struct{}) sessionClosed := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1730,7 +1637,7 @@ func TestServerReadWithoutTeardown(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -1762,15 +1669,8 @@ func TestServerReadWithoutTeardown(t *testing.T) { } } -func TestServerReadUDPChangeConn(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) +func TestServerPlayUDPChangeConn(t *testing.T) { + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1823,7 +1723,7 @@ func TestServerReadUDPChangeConn(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -1869,22 +1769,8 @@ func TestServerReadUDPChangeConn(t *testing.T) { }() } -func TestServerReadPartialTracks(t *testing.T) { - track1 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - track2 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track1, track2}) +func TestServerPlayPartialMedias(t *testing.T) { + stream := NewServerStream(media.Medias{testH264Media.Clone(), testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1897,8 +1783,8 @@ func TestServerReadPartialTracks(t *testing.T) { onPlay: func(ctx *ServerHandlerOnPlayCtx) (*base.Response, error) { go func() { time.Sleep(1 * time.Second) - stream.WritePacketRTP(0, &testRTPPacket) - stream.WritePacketRTP(1, &testRTPPacket) + stream.WritePacketRTP(stream.Medias()[0], &testRTPPacket) + stream.WritePacketRTP(stream.Medias()[1], &testRTPPacket) }() return &base.Response{ @@ -1933,7 +1819,7 @@ func TestServerReadPartialTracks(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=1"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=1"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -1963,7 +1849,7 @@ func TestServerReadPartialTracks(t *testing.T) { require.Equal(t, testRTPPacketMarshaled, f.Payload) } -func TestServerReadAdditionalInfos(t *testing.T) { +func TestServerPlayAdditionalInfos(t *testing.T) { getInfos := func() (*headers.RTPInfo, []*uint32) { nconn, err := net.Dial("tcp", "localhost:8554") require.NoError(t, err) @@ -1987,7 +1873,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTH.Marshal(), @@ -2020,7 +1906,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=1"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=1"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": inTH.Marshal(), @@ -2053,17 +1939,19 @@ func TestServerReadAdditionalInfos(t *testing.T) { return &ri, ssrcs } - track := &TrackGeneric{ - Media: "application", - Payloads: []TrackGenericPayload{{ - Type: 96, - RTPMap: "private/90000", - }}, + trak := &format.Generic{ + PayloadTyp: 96, + RTPMap: "private/90000", } - err := track.Init() + err := trak.Init() require.NoError(t, err) - stream := NewServerStream(Tracks{track, track}) + medi := &media.Media{ + Type: "application", + Formats: []format.Format{trak}, + } + + stream := NewServerStream(media.Medias{medi.Clone(), medi.Clone()}) defer stream.Close() s := &Server{ @@ -2086,7 +1974,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { require.NoError(t, err) defer s.Close() - stream.WritePacketRTP(0, &rtp.Packet{ + stream.WritePacketRTP(stream.Medias()[0], &rtp.Packet{ Header: rtp.Header{ Version: 2, PayloadType: 96, @@ -2103,7 +1991,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { URL: (&url.URL{ Scheme: "rtsp", Host: "localhost:8554", - Path: "/teststream/trackID=0", + Path: "/teststream/mediaID=0", }).String(), SequenceNumber: func() *uint16 { v := uint16(557) @@ -2120,7 +2008,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { nil, }, ssrcs) - stream.WritePacketRTP(1, &rtp.Packet{ + stream.WritePacketRTP(stream.Medias()[1], &rtp.Packet{ Header: rtp.Header{ Version: 2, PayloadType: 96, @@ -2137,7 +2025,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { URL: (&url.URL{ Scheme: "rtsp", Host: "localhost:8554", - Path: "/teststream/trackID=0", + Path: "/teststream/mediaID=0", }).String(), SequenceNumber: func() *uint16 { v := uint16(557) @@ -2149,7 +2037,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { URL: (&url.URL{ Scheme: "rtsp", Host: "localhost:8554", - Path: "/teststream/trackID=1", + Path: "/teststream/mediaID=1", }).String(), SequenceNumber: func() *uint16 { v := uint16(88) diff --git a/server_publish_test.go b/server_record_test.go similarity index 80% rename from server_publish_test.go rename to server_record_test.go index eb1c61ab..4662c2cf 100644 --- a/server_publish_test.go +++ b/server_record_test.go @@ -12,9 +12,12 @@ import ( psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/sdp" ) func invalidURLAnnounceReq(t *testing.T, control string) base.Request { @@ -26,15 +29,10 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request { "Content-Type": base.HeaderValue{"application/sdp"}, }, Body: func() []byte { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - track.SetControl(control) + medi := testH264Media.Clone() + medi.Control = control - sout := &psdp.SessionDescription{ + sout := &sdp.SessionDescription{ SessionName: psdp.SessionName("Stream"), Origin: psdp.Origin{ Username: "-", @@ -45,9 +43,7 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request { TimeDescriptions: []psdp.TimeDescription{ {Timing: psdp.Timing{0, 0}}, //nolint:govet }, - MediaDescriptions: []*psdp.MediaDescription{ - track.MediaDescription(), - }, + MediaDescriptions: []*psdp.MediaDescription{medi.Marshal()}, } byts, _ := sout.Marshal() @@ -56,7 +52,7 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request { } } -func TestServerPublishErrorAnnounce(t *testing.T) { +func TestServerRecordErrorAnnounce(t *testing.T) { for _, ca := range []struct { name string req base.Request @@ -86,7 +82,7 @@ func TestServerPublishErrorAnnounce(t *testing.T) { "unsupported Content-Type header '[aa]'", }, { - "invalid tracks", + "invalid medias", base.Request{ Method: base.Announce, URL: mustParseURL("rtsp://localhost:8554/teststream"), @@ -101,17 +97,17 @@ func TestServerPublishErrorAnnounce(t *testing.T) { { "invalid URL 1", invalidURLAnnounceReq(t, "rtsp:// aaaaa"), - "unable to generate track URL", + "unable to generate media URL", }, { "invalid URL 2", invalidURLAnnounceReq(t, "rtsp://host"), - "invalid track URL (rtsp://localhost:8554)", + "invalid media URL (rtsp://localhost:8554)", }, { "invalid URL 3", invalidURLAnnounceReq(t, "rtsp://host/otherpath"), - "invalid track path: must begin with 'teststream', but is 'otherpath'", + "invalid media path: must begin with 'teststream', but is 'otherpath'", }, } { t.Run(ca.name, func(t *testing.T) { @@ -149,63 +145,38 @@ func TestServerPublishErrorAnnounce(t *testing.T) { } } -func TestServerPublishSetupPath(t *testing.T) { +func TestServerRecordSetupPath(t *testing.T) { for _, ca := range []struct { name string control string url string path string - trackID int }{ { "normal", - "trackID=0", - "rtsp://localhost:8554/teststream/trackID=0", + "bbb=ccc", + "rtsp://localhost:8554/teststream/bbb=ccc", "teststream", - 0, - }, - { - "unordered id", - "trackID=2", - "rtsp://localhost:8554/teststream/trackID=2", - "teststream", - 0, - }, - { - "custom param name", - "testing=0", - "rtsp://localhost:8554/teststream/testing=0", - "teststream", - 0, - }, - { - "query", - "?testing=0", - "rtsp://localhost:8554/teststream?testing=0", - "teststream", - 0, }, { "subpath", - "trackID=0", - "rtsp://localhost:8554/test/stream/trackID=0", + "ddd=eee", + "rtsp://localhost:8554/test/stream/ddd=eee", "test/stream", - 0, }, { "subpath and query", - "?testing=0", - "rtsp://localhost:8554/test/stream?testing=0", - "test/stream", - 0, + "fff=ggg", + "rtsp://localhost:8554/test/stream?testing=0/fff=ggg", + "test/stream?testing=0", }, } { t.Run(ca.name, func(t *testing.T) { s := &Server{ Handler: &testServerHandler{ onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) { - // make sure that track URLs are not overridden by NewServerStream() - stream := NewServerStream(ctx.Tracks) + // make sure that media URLs are not overridden by NewServerStream() + stream := NewServerStream(ctx.Medias) defer stream.Close() return &base.Response{ @@ -213,8 +184,11 @@ func TestServerPublishSetupPath(t *testing.T) { }, nil }, onSetup: func(ctx *ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) { - require.Equal(t, ca.path, ctx.Path) - require.Equal(t, ca.trackID, ctx.TrackID) + p := ctx.Path + if ctx.Query != "" { + p += "?" + ctx.Query + } + require.Equal(t, ca.path, p) return &base.Response{ StatusCode: base.StatusOK, }, nil, nil @@ -232,15 +206,10 @@ func TestServerPublishSetupPath(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - track.SetControl(ca.control) + media := testH264Media.Clone() + media.Control = ca.control - sout := &psdp.SessionDescription{ + sout := &sdp.SessionDescription{ SessionName: psdp.SessionName("Stream"), Origin: psdp.Origin{ Username: "-", @@ -252,7 +221,7 @@ func TestServerPublishSetupPath(t *testing.T) { {Timing: psdp.Timing{0, 0}}, //nolint:govet }, MediaDescriptions: []*psdp.MediaDescription{ - track.MediaDescription(), + media.Marshal(), }, } @@ -297,7 +266,7 @@ func TestServerPublishSetupPath(t *testing.T) { } } -func TestServerPublishErrorSetupDifferentPaths(t *testing.T) { +func TestServerRecordErrorSetupMediaTwice(t *testing.T) { serverErr := make(chan error) s := &Server{ @@ -328,15 +297,8 @@ func TestServerPublishErrorSetupDifferentPaths(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -345,111 +307,28 @@ func TestServerPublishErrorSetupDifferentPaths(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) - th := &headers.Transport{ - Protocol: headers.TransportProtocolTCP, - Delivery: func() *headers.TransportDelivery { - v := headers.TransportDeliveryUnicast - return &v - }(), - Mode: func() *headers.TransportMode { - v := headers.TransportModeRecord - return &v - }(), - InterleavedIDs: &[2]int{0, 1}, - } - res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/test2stream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ - "CSeq": base.HeaderValue{"2"}, - "Transport": th.Marshal(), - }, - }) - require.NoError(t, err) - require.Equal(t, base.StatusBadRequest, res.StatusCode) - - err = <-serverErr - require.EqualError(t, err, "invalid track path (test2stream/trackID=0)") -} - -func TestServerPublishErrorSetupTrackTwice(t *testing.T) { - serverErr := make(chan error) - - s := &Server{ - Handler: &testServerHandler{ - onConnClose: func(ctx *ServerHandlerOnConnCloseCtx) { - serverErr <- ctx.Error - }, - onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, nil - }, - onSetup: func(ctx *ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) { - return &base.Response{ - StatusCode: base.StatusOK, - }, nil, nil - }, - }, - RTSPAddress: "localhost:8554", - } - - err := s.Start() - require.NoError(t, err) - defer s.Close() - - nconn, err := net.Dial("tcp", "localhost:8554") - require.NoError(t, err) - defer nconn.Close() - conn := conn.NewConn(nconn) - - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() - - res, err := writeReqReadRes(conn, base.Request{ - Method: base.Announce, - URL: mustParseURL("rtsp://localhost:8554/teststream"), - Header: base.Header{ - "CSeq": base.HeaderValue{"1"}, - "Content-Type": base.HeaderValue{"application/sdp"}, - }, - Body: tracks.Marshal(false), - }) - require.NoError(t, err) - require.Equal(t, base.StatusOK, res.StatusCode) - - th := &headers.Transport{ - Protocol: headers.TransportProtocolTCP, - Delivery: func() *headers.TransportDelivery { - v := headers.TransportDeliveryUnicast - return &v - }(), - Mode: func() *headers.TransportMode { - v := headers.TransportModeRecord - return &v - }(), - InterleavedIDs: &[2]int{0, 1}, - } - - res, err = writeReqReadRes(conn, base.Request{ - Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), - Header: base.Header{ - "CSeq": base.HeaderValue{"2"}, - "Transport": th.Marshal(), + "CSeq": base.HeaderValue{"2"}, + "Transport": headers.Transport{ + Protocol: headers.TransportProtocolTCP, + Delivery: func() *headers.TransportDelivery { + v := headers.TransportDeliveryUnicast + return &v + }(), + Mode: func() *headers.TransportMode { + v := headers.TransportModeRecord + return &v + }(), + InterleavedIDs: &[2]int{0, 1}, + }.Marshal(), }, }) require.NoError(t, err) @@ -461,21 +340,32 @@ func TestServerPublishErrorSetupTrackTwice(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ - "CSeq": base.HeaderValue{"3"}, - "Transport": th.Marshal(), - "Session": base.HeaderValue{sx.Session}, + "CSeq": base.HeaderValue{"3"}, + "Transport": headers.Transport{ + Protocol: headers.TransportProtocolTCP, + Delivery: func() *headers.TransportDelivery { + v := headers.TransportDeliveryUnicast + return &v + }(), + Mode: func() *headers.TransportMode { + v := headers.TransportModeRecord + return &v + }(), + InterleavedIDs: &[2]int{2, 3}, + }.Marshal(), + "Session": base.HeaderValue{sx.Session}, }, }) require.NoError(t, err) require.Equal(t, base.StatusBadRequest, res.StatusCode) err = <-serverErr - require.EqualError(t, err, "track 0 has already been setup") + require.EqualError(t, err, "media has already been setup") } -func TestServerPublishErrorRecordPartialTracks(t *testing.T) { +func TestServerRecordErrorRecordPartialMedias(t *testing.T) { serverErr := make(chan error) s := &Server{ @@ -511,22 +401,8 @@ func TestServerPublishErrorRecordPartialTracks(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track1 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - track2 := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track1, track2} - tracks.setControls() + medias := media.Medias{testH264Media.Clone(), testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -535,7 +411,7 @@ func TestServerPublishErrorRecordPartialTracks(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -555,7 +431,7 @@ func TestServerPublishErrorRecordPartialTracks(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": th.Marshal(), @@ -580,10 +456,10 @@ func TestServerPublishErrorRecordPartialTracks(t *testing.T) { require.Equal(t, base.StatusBadRequest, res.StatusCode) err = <-serverErr - require.EqualError(t, err, "not all announced tracks have been setup") + require.EqualError(t, err, "not all announced medias have been setup") } -func TestServerPublish(t *testing.T) { +func TestServerRecord(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -622,21 +498,24 @@ func TestServerPublish(t *testing.T) { onRecord: func(ctx *ServerHandlerOnRecordCtx) (*base.Response, error) { // send RTCP packets directly to the session. // these are sent after the response, only if onRecord returns StatusOK. - ctx.Session.WritePacketRTCP(0, &testRTCPPacket) + ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedMedias()[0], &testRTCPPacket) + + ctx.Session.OnPacketRTPAny(func(medi *media.Media, trak format.Format, pkt *rtp.Packet) { + require.Equal(t, ctx.Session.AnnouncedMedias()[0], medi) + require.Equal(t, ctx.Session.AnnouncedMedias()[0].Formats[0], trak) + require.Equal(t, &testRTPPacket, pkt) + }) + + ctx.Session.OnPacketRTCPAny(func(medi *media.Media, pkt rtcp.Packet) { + require.Equal(t, ctx.Session.AnnouncedMedias()[0], medi) + require.Equal(t, &testRTCPPacket, pkt) + ctx.Session.WritePacketRTCP(ctx.Session.AnnouncedMedias()[0], &testRTCPPacket) + }) return &base.Response{ StatusCode: base.StatusOK, }, nil }, - onPacketRTP: func(ctx *ServerHandlerOnPacketRTPCtx) { - require.Equal(t, 0, ctx.TrackID) - require.Equal(t, &testRTPPacket, ctx.Packet) - }, - onPacketRTCP: func(ctx *ServerHandlerOnPacketRTCPCtx) { - require.Equal(t, 0, ctx.TrackID) - require.Equal(t, &testRTCPPacket, ctx.Packet) - ctx.Session.WritePacketRTCP(0, &testRTCPPacket) - }, }, RTSPAddress: "localhost:8554", } @@ -670,15 +549,8 @@ func TestServerPublish(t *testing.T) { <-nconnOpened - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -687,7 +559,7 @@ func TestServerPublish(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -726,7 +598,7 @@ func TestServerPublish(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": inTH.Marshal(), @@ -831,7 +703,7 @@ func TestServerPublish(t *testing.T) { } } -func TestServerPublishErrorInvalidProtocol(t *testing.T) { +func TestServerRecordErrorInvalidProtocol(t *testing.T) { errorRecv := make(chan struct{}) s := &Server{ @@ -855,9 +727,6 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) { StatusCode: base.StatusOK, }, nil }, - onPacketRTP: func(ctx *ServerHandlerOnPacketRTPCtx) { - t.Error("should not happen") - }, }, UDPRTPAddress: "127.0.0.1:8000", UDPRTCPAddress: "127.0.0.1:8001", @@ -873,15 +742,8 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -890,7 +752,7 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -910,7 +772,7 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": inTH.Marshal(), @@ -947,7 +809,7 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) { <-errorRecv } -func TestServerPublishRTCPReport(t *testing.T) { +func TestServerRecordRTCPReport(t *testing.T) { s := &Server{ Handler: &testServerHandler{ onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) { @@ -981,15 +843,8 @@ func TestServerPublishRTCPReport(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -998,7 +853,7 @@ func TestServerPublishRTCPReport(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -1013,7 +868,7 @@ func TestServerPublishRTCPReport(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": headers.Transport{ @@ -1069,6 +924,9 @@ func TestServerPublishRTCPReport(t *testing.T) { }) require.NoError(t, err) + // wait for the packet's SSRC to be saved + time.Sleep(500 * time.Millisecond) + byts, _ = (&rtcp.SenderReport{ SSRC: 753621, NTPTime: 0xcbddcc34999997ff, @@ -1108,7 +966,7 @@ func TestServerPublishRTCPReport(t *testing.T) { }, rr) } -func TestServerPublishTimeout(t *testing.T) { +func TestServerRecordTimeout(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -1159,15 +1017,8 @@ func TestServerPublishTimeout(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -1176,7 +1027,7 @@ func TestServerPublishTimeout(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -1202,7 +1053,7 @@ func TestServerPublishTimeout(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": inTH.Marshal(), @@ -1239,7 +1090,7 @@ func TestServerPublishTimeout(t *testing.T) { } } -func TestServerPublishWithoutTeardown(t *testing.T) { +func TestServerRecordWithoutTeardown(t *testing.T) { for _, transport := range []string{ "udp", "tcp", @@ -1289,15 +1140,8 @@ func TestServerPublishWithoutTeardown(t *testing.T) { require.NoError(t, err) conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -1306,7 +1150,7 @@ func TestServerPublishWithoutTeardown(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -1332,7 +1176,7 @@ func TestServerPublishWithoutTeardown(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": inTH.Marshal(), @@ -1368,7 +1212,7 @@ func TestServerPublishWithoutTeardown(t *testing.T) { } } -func TestServerPublishUDPChangeConn(t *testing.T) { +func TestServerRecordUDPChangeConn(t *testing.T) { s := &Server{ Handler: &testServerHandler{ onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) { @@ -1409,15 +1253,8 @@ func TestServerPublishUDPChangeConn(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - tracks := Tracks{track} - tracks.setControls() + medias := media.Medias{testH264Media.Clone()} + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -1426,7 +1263,7 @@ func TestServerPublishUDPChangeConn(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -1446,7 +1283,7 @@ func TestServerPublishUDPChangeConn(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": inTH.Marshal(), @@ -1492,7 +1329,7 @@ func TestServerPublishUDPChangeConn(t *testing.T) { }() } -func TestServerPublishDecodeErrors(t *testing.T) { +func TestServerRecordDecodeErrors(t *testing.T) { for _, ca := range []struct { proto string name string @@ -1565,14 +1402,14 @@ func TestServerPublishDecodeErrors(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - tracks := Tracks{&TrackGeneric{ - Media: "application", - Payloads: []TrackGenericPayload{{ - Type: 97, - RTPMap: "private/90000", + medias := media.Medias{{ + Type: media.TypeApplication, + Formats: []format.Format{&format.Generic{ + PayloadTyp: 97, + RTPMap: "private/90000", }}, }} - tracks.setControls() + medias.SetControls() res, err := writeReqReadRes(conn, base.Request{ Method: base.Announce, @@ -1581,7 +1418,7 @@ func TestServerPublishDecodeErrors(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: tracks.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -1620,7 +1457,7 @@ func TestServerPublishDecodeErrors(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"2"}, "Transport": inTH.Marshal(), @@ -1664,6 +1501,7 @@ func TestServerPublishDecodeErrors(t *testing.T) { case ca.proto == "udp" && ca.name == "rtp packets lost": byts, _ := rtp.Packet{ Header: rtp.Header{ + PayloadType: 97, SequenceNumber: 30, }, }.Marshal() @@ -1674,6 +1512,7 @@ func TestServerPublishDecodeErrors(t *testing.T) { byts, _ = rtp.Packet{ Header: rtp.Header{ + PayloadType: 97, SequenceNumber: 100, }, }.Marshal() diff --git a/server_test.go b/server_test.go index c4c6e6c6..8d3fa8a2 100644 --- a/server_test.go +++ b/server_test.go @@ -7,10 +7,11 @@ import ( "github.com/stretchr/testify/require" - "github.com/aler9/gortsplib/pkg/auth" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/auth" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/media" ) var serverCert = []byte(`-----BEGIN CERTIFICATE----- @@ -92,8 +93,6 @@ type testServerHandler struct { onPause func(*ServerHandlerOnPauseCtx) (*base.Response, error) onSetParameter func(*ServerHandlerOnSetParameterCtx) (*base.Response, error) onGetParameter func(*ServerHandlerOnGetParameterCtx) (*base.Response, error) - onPacketRTP func(*ServerHandlerOnPacketRTPCtx) - onPacketRTCP func(*ServerHandlerOnPacketRTCPCtx) onDecodeError func(*ServerHandlerOnDecodeErrorCtx) } @@ -177,18 +176,6 @@ func (sh *testServerHandler) OnGetParameter(ctx *ServerHandlerOnGetParameterCtx) return nil, fmt.Errorf("unimplemented") } -func (sh *testServerHandler) OnPacketRTP(ctx *ServerHandlerOnPacketRTPCtx) { - if sh.onPacketRTP != nil { - sh.onPacketRTP(ctx) - } -} - -func (sh *testServerHandler) OnPacketRTCP(ctx *ServerHandlerOnPacketRTCPCtx) { - if sh.onPacketRTCP != nil { - sh.onPacketRTCP(ctx) - } -} - func (sh *testServerHandler) OnDecodeError(ctx *ServerHandlerOnDecodeErrorCtx) { if sh.onDecodeError != nil { sh.onDecodeError(ctx) @@ -339,14 +326,7 @@ func (s *testServerErrMethodNotImplemented) OnSetup( func TestServerErrorMethodNotImplemented(t *testing.T) { for _, ca := range []string{"outside session", "inside session"} { t.Run(ca, func(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -368,7 +348,7 @@ func TestServerErrorMethodNotImplemented(t *testing.T) { if ca == "inside session" { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -400,7 +380,7 @@ func TestServerErrorMethodNotImplemented(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.SetParameter, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: headers, }) require.NoError(t, err) @@ -415,7 +395,7 @@ func TestServerErrorMethodNotImplemented(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Options, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: headers, }) require.NoError(t, err) @@ -425,14 +405,7 @@ func TestServerErrorMethodNotImplemented(t *testing.T) { } func TestServerErrorTCPTwoConnOneSession(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -467,7 +440,7 @@ func TestServerErrorTCPTwoConnOneSession(t *testing.T) { res, err := writeReqReadRes(conn1, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -509,7 +482,7 @@ func TestServerErrorTCPTwoConnOneSession(t *testing.T) { res, err = writeReqReadRes(conn2, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -532,14 +505,7 @@ func TestServerErrorTCPTwoConnOneSession(t *testing.T) { } func TestServerErrorTCPOneConnTwoSessions(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -574,7 +540,7 @@ func TestServerErrorTCPOneConnTwoSessions(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -611,7 +577,7 @@ func TestServerErrorTCPOneConnTwoSessions(t *testing.T) { res, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"3"}, "Transport": headers.Transport{ @@ -633,12 +599,7 @@ func TestServerErrorTCPOneConnTwoSessions(t *testing.T) { } func TestServerSetupMultipleTransports(t *testing.T) { - stream := NewServerStream(Tracks{&TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - }}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -690,7 +651,7 @@ func TestServerSetupMultipleTransports(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": inTHS.Marshal(), @@ -715,14 +676,7 @@ func TestServerSetupMultipleTransports(t *testing.T) { func TestServerGetSetParameter(t *testing.T) { for _, ca := range []string{"inside session", "outside session"} { t.Run(ca, func(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() var params []byte @@ -776,7 +730,7 @@ func TestServerGetSetParameter(t *testing.T) { if ca == "inside session" { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -889,12 +843,7 @@ func TestServerErrorInvalidSession(t *testing.T) { } func TestServerSessionClose(t *testing.T) { - stream := NewServerStream(Tracks{&TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - }}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() var session *ServerSession @@ -924,7 +873,7 @@ func TestServerSessionClose(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -964,12 +913,7 @@ func TestServerSessionAutoClose(t *testing.T) { t.Run(ca, func(t *testing.T) { sessionClosed := make(chan struct{}) - stream := NewServerStream(Tracks{&TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - }}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1002,7 +946,7 @@ func TestServerSessionAutoClose(t *testing.T) { _, err = writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -1029,12 +973,7 @@ func TestServerSessionAutoClose(t *testing.T) { } func TestServerSessionTeardown(t *testing.T) { - stream := NewServerStream(Tracks{&TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - }}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1059,7 +998,7 @@ func TestServerSessionTeardown(t *testing.T) { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -1110,14 +1049,7 @@ func TestServerErrorInvalidPath(t *testing.T) { t.Run(ca, func(t *testing.T) { nconnClosed := make(chan struct{}) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } - - stream := NewServerStream(Tracks{track}) + stream := NewServerStream(media.Medias{testH264Media.Clone()}) defer stream.Close() s := &Server{ @@ -1147,7 +1079,7 @@ func TestServerErrorInvalidPath(t *testing.T) { if ca == "inside session" { res, err := writeReqReadRes(conn, base.Request{ Method: base.Setup, - URL: mustParseURL("rtsp://localhost:8554/teststream/trackID=0"), + URL: mustParseURL("rtsp://localhost:8554/teststream/mediaID=0"), Header: base.Header{ "CSeq": base.HeaderValue{"1"}, "Transport": headers.Transport{ @@ -1231,12 +1163,7 @@ func TestServerAuth(t *testing.T) { defer nconn.Close() conn := conn.NewConn(nconn) - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02, 0x03, 0x04}, - PPS: []byte{0x01, 0x02, 0x03, 0x04}, - PacketizationMode: 1, - } + medias := media.Medias{testH264Media.Clone()} req := base.Request{ Method: base.Announce, @@ -1245,7 +1172,7 @@ func TestServerAuth(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: Tracks{track}.Marshal(false), + Body: mustMarshalSDP(medias.Marshal(false)), } res, err := writeReqReadRes(conn, req) diff --git a/serverconn.go b/serverconn.go index 1f410f75..bf8b6f2d 100644 --- a/serverconn.go +++ b/serverconn.go @@ -4,20 +4,17 @@ import ( "context" "crypto/tls" "errors" - "fmt" "net" gourl "net/url" "strings" "sync/atomic" "time" - "github.com/pion/rtcp" - - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/bytecounter" - "github.com/aler9/gortsplib/pkg/conn" - "github.com/aler9/gortsplib/pkg/liberrors" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/bytecounter" + "github.com/aler9/gortsplib/v2/pkg/conn" + "github.com/aler9/gortsplib/v2/pkg/liberrors" + "github.com/aler9/gortsplib/v2/pkg/url" ) func getSessionID(header base.Header) string { @@ -244,76 +241,6 @@ func (sc *ServerConn) readFuncTCP(readRequest chan readReq) error { case <-sc.session.ctx.Done(): } - var processFunc func(*ServerSessionSetuppedTrack, bool, []byte) error - - if sc.session.state == ServerSessionStatePlay { - processFunc = func(track *ServerSessionSetuppedTrack, isRTP bool, payload []byte) error { - if !isRTP { - if len(payload) > maxPacketSize { - onDecodeError(sc.session, fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", - len(payload), maxPacketSize)) - return nil - } - - packets, err := rtcp.Unmarshal(payload) - if err != nil { - onDecodeError(sc.session, err) - return nil - } - - if h, ok := sc.s.Handler.(ServerHandlerOnPacketRTCP); ok { - for _, pkt := range packets { - h.OnPacketRTCP(&ServerHandlerOnPacketRTCPCtx{ - Session: sc.session, - TrackID: track.id, - Packet: pkt, - }) - } - } - } - - return nil - } - } else { - tcpRTPPacketBuffer := newRTPPacketMultiBuffer(uint64(sc.s.ReadBufferCount)) - - processFunc = func(track *ServerSessionSetuppedTrack, isRTP bool, payload []byte) error { - if isRTP { - pkt := tcpRTPPacketBuffer.next() - err := pkt.Unmarshal(payload) - if err != nil { - return err - } - - if h, ok := sc.s.Handler.(ServerHandlerOnPacketRTP); ok { - h.OnPacketRTP(&ServerHandlerOnPacketRTPCtx{ - Session: sc.session, - TrackID: track.id, - Packet: pkt, - }) - } - } else { - if len(payload) > maxPacketSize { - onDecodeError(sc.session, fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", - len(payload), maxPacketSize)) - return nil - } - - packets, err := rtcp.Unmarshal(payload) - if err != nil { - onDecodeError(sc.session, err) - return nil - } - - for _, pkt := range packets { - sc.session.onPacketRTCP(track.id, pkt) - } - } - - return nil - } - } - for { if sc.session.state == ServerSessionStateRecord { sc.nconn.SetReadDeadline(time.Now().Add(sc.s.ReadTimeout)) @@ -335,11 +262,11 @@ func (sc *ServerConn) readFuncTCP(readRequest chan readReq) error { atomic.AddUint64(sc.session.bytesReceived, uint64(len(twhat.Payload))) - // forward frame only if it has been set up - if track, ok := sc.session.tcpTracksByChannel[channel]; ok { - err := processFunc(track, isRTP, twhat.Payload) - if err != nil { - return err + if sm, ok := sc.session.tcpMediasByChannel[channel]; ok { + if isRTP { + sm.readRTP(twhat.Payload) + } else { + sm.readRTCP(twhat.Payload) } } @@ -451,7 +378,8 @@ func (sc *ServerConn) handleRequest(req *base.Request) (*base.Response, error) { } if stream != nil { - res.Body = stream.Tracks().Marshal(multicast) + byts, _ := stream.Medias().CloneAndSetControls().Marshal(multicast).Marshal() + res.Body = byts } } diff --git a/serverhandler.go b/serverhandler.go index 62421266..9cfe1afb 100644 --- a/serverhandler.go +++ b/serverhandler.go @@ -1,10 +1,8 @@ package gortsplib import ( - "github.com/pion/rtcp" - "github.com/pion/rtp" - - "github.com/aler9/gortsplib/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/media" ) // ServerHandler is the interface implemented by all the server handlers. @@ -79,7 +77,7 @@ type ServerHandlerOnDescribeCtx struct { // ServerHandlerOnDescribe can be implemented by a ServerHandler. type ServerHandlerOnDescribe interface { - // called after receiving a DESCRIBE request. + // called when receiving a DESCRIBE request. OnDescribe(*ServerHandlerOnDescribeCtx) (*base.Response, *ServerStream, error) } @@ -91,12 +89,12 @@ type ServerHandlerOnAnnounceCtx struct { Request *base.Request Path string Query string - Tracks Tracks + Medias media.Medias } // ServerHandlerOnAnnounce can be implemented by a ServerHandler. type ServerHandlerOnAnnounce interface { - // called after receiving an ANNOUNCE request. + // called when receiving an ANNOUNCE request. OnAnnounce(*ServerHandlerOnAnnounceCtx) (*base.Response, error) } @@ -108,13 +106,12 @@ type ServerHandlerOnSetupCtx struct { Request *base.Request Path string Query string - TrackID int Transport Transport } // ServerHandlerOnSetup can be implemented by a ServerHandler. type ServerHandlerOnSetup interface { - // called after receiving a SETUP request. + // called when receiving a SETUP request. // must return a Response and a stream. // the stream is needed to // - add the session the the stream's readers @@ -133,7 +130,7 @@ type ServerHandlerOnPlayCtx struct { // ServerHandlerOnPlay can be implemented by a ServerHandler. type ServerHandlerOnPlay interface { - // called after receiving a PLAY request. + // called when receiving a PLAY request. OnPlay(*ServerHandlerOnPlayCtx) (*base.Response, error) } @@ -148,7 +145,7 @@ type ServerHandlerOnRecordCtx struct { // ServerHandlerOnRecord can be implemented by a ServerHandler. type ServerHandlerOnRecord interface { - // called after receiving a RECORD request. + // called when receiving a RECORD request. OnRecord(*ServerHandlerOnRecordCtx) (*base.Response, error) } @@ -163,7 +160,7 @@ type ServerHandlerOnPauseCtx struct { // ServerHandlerOnPause can be implemented by a ServerHandler. type ServerHandlerOnPause interface { - // called after receiving a PAUSE request. + // called when receiving a PAUSE request. OnPause(*ServerHandlerOnPauseCtx) (*base.Response, error) } @@ -178,7 +175,7 @@ type ServerHandlerOnGetParameterCtx struct { // ServerHandlerOnGetParameter can be implemented by a ServerHandler. type ServerHandlerOnGetParameter interface { - // called after receiving a GET_PARAMETER request. + // called when receiving a GET_PARAMETER request. OnGetParameter(*ServerHandlerOnGetParameterCtx) (*base.Response, error) } @@ -193,36 +190,10 @@ type ServerHandlerOnSetParameterCtx struct { // ServerHandlerOnSetParameter can be implemented by a ServerHandler. type ServerHandlerOnSetParameter interface { - // called after receiving a SET_PARAMETER request. + // called when receiving a SET_PARAMETER request. OnSetParameter(*ServerHandlerOnSetParameterCtx) (*base.Response, error) } -// ServerHandlerOnPacketRTPCtx is the context of OnPacketRTP. -type ServerHandlerOnPacketRTPCtx struct { - Session *ServerSession - TrackID int - Packet *rtp.Packet -} - -// ServerHandlerOnPacketRTP can be implemented by a ServerHandler. -type ServerHandlerOnPacketRTP interface { - // called when receiving a RTP packet. - OnPacketRTP(*ServerHandlerOnPacketRTPCtx) -} - -// ServerHandlerOnPacketRTCPCtx is the context of OnPacketRTCP. -type ServerHandlerOnPacketRTCPCtx struct { - Session *ServerSession - TrackID int - Packet rtcp.Packet -} - -// ServerHandlerOnPacketRTCP can be implemented by a ServerHandler. -type ServerHandlerOnPacketRTCP interface { - // called when receiving a RTCP packet. - OnPacketRTCP(*ServerHandlerOnPacketRTCPCtx) -} - // ServerHandlerOnDecodeErrorCtx is the context of OnDecodeError. type ServerHandlerOnDecodeErrorCtx struct { Session *ServerSession diff --git a/servermulticasthandler.go b/servermulticasthandler.go index 7e010c15..0d505284 100644 --- a/servermulticasthandler.go +++ b/servermulticasthandler.go @@ -3,11 +3,10 @@ package gortsplib import ( "net" - "github.com/aler9/gortsplib/pkg/ringbuffer" + "github.com/aler9/gortsplib/v2/pkg/ringbuffer" ) -type trackTypePayload struct { - trackID int +type typeAndPayload struct { isRTP bool payload []byte } @@ -69,7 +68,7 @@ func (h *serverMulticastHandler) runWriter() { if !ok { return } - data := tmp.(trackTypePayload) + data := tmp.(typeAndPayload) if data.isRTP { h.rtpl.write(data.payload, rtpAddr) @@ -80,14 +79,14 @@ func (h *serverMulticastHandler) runWriter() { } func (h *serverMulticastHandler) writePacketRTP(payload []byte) { - h.writeBuffer.Push(trackTypePayload{ + h.writeBuffer.Push(typeAndPayload{ isRTP: true, payload: payload, }) } func (h *serverMulticastHandler) writePacketRTCP(payload []byte) { - h.writeBuffer.Push(trackTypePayload{ + h.writeBuffer.Push(typeAndPayload{ isRTP: false, payload: payload, }) diff --git a/serversession.go b/serversession.go index d8af91c8..ba08be14 100644 --- a/serversession.go +++ b/serversession.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net" - "sort" "strconv" "strings" "sync/atomic" @@ -13,13 +12,14 @@ import ( "github.com/pion/rtcp" "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/headers" - "github.com/aler9/gortsplib/pkg/liberrors" - "github.com/aler9/gortsplib/pkg/ringbuffer" - "github.com/aler9/gortsplib/pkg/rtcpreceiver" - "github.com/aler9/gortsplib/pkg/rtpreorderer" - "github.com/aler9/gortsplib/pkg/url" + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/liberrors" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/ringbuffer" + "github.com/aler9/gortsplib/v2/pkg/sdp" + "github.com/aler9/gortsplib/v2/pkg/url" ) func stringsReverseIndex(s, substr string) int { @@ -31,61 +31,47 @@ func stringsReverseIndex(s, substr string) int { return -1 } -func setupGetTrackIDPathQuery( - u *url.URL, - thMode *headers.TransportMode, - announcedTracks Tracks, - setuppedPath *string, - setuppedQuery *string, - setuppedBaseURL *url.URL, -) (int, string, string, error) { +func serverParseURLForPlay(u *url.URL) (string, string, int, error) { pathAndQuery, ok := u.RTSPPathAndQuery() if !ok { - return 0, "", "", liberrors.ErrServerInvalidPath{} + return "", "", -1, liberrors.ErrServerInvalidPath{} } - if thMode == nil || *thMode == headers.TransportModePlay { - i := stringsReverseIndex(pathAndQuery, "/trackID=") - - // URL doesn't contain trackID - it's track zero - if i < 0 { - if !strings.HasSuffix(pathAndQuery, "/") { - return 0, "", "", fmt.Errorf("path of a SETUP request must end with a slash. " + - "This typically happens when VLC fails a request, and then switches to an " + - "unsupported RTSP dialect") - } - pathAndQuery = pathAndQuery[:len(pathAndQuery)-1] - - path, query := url.PathSplitQuery(pathAndQuery) - - // we assume it's track 0 - return 0, path, query, nil + i := stringsReverseIndex(pathAndQuery, "/mediaID=") + if i < 0 { + if !strings.HasSuffix(pathAndQuery, "/") { + return "", "", -1, fmt.Errorf("path of a SETUP request must end with a slash. " + + "This typically happens when VLC fails a request, and then switches to an " + + "unsupported RTSP dialect") } - tmp, err := strconv.ParseInt(pathAndQuery[i+len("/trackID="):], 10, 64) - if err != nil || tmp < 0 { - return 0, "", "", fmt.Errorf("unable to parse track ID (%v)", pathAndQuery) - } - trackID := int(tmp) - pathAndQuery = pathAndQuery[:i] - - path, query := url.PathSplitQuery(pathAndQuery) - - if setuppedPath != nil && (path != *setuppedPath || query != *setuppedQuery) { - return 0, "", "", fmt.Errorf("can't setup tracks with different paths") - } - - return trackID, path, query, nil + path, query := url.PathSplitQuery(pathAndQuery[:len(pathAndQuery)-1]) + return path, query, 0, nil } - for trackID, track := range announcedTracks { - u2, _ := track.url(setuppedBaseURL) - if u2.String() == u.String() { - return trackID, *setuppedPath, *setuppedQuery, nil + var t string + pathAndQuery, t = pathAndQuery[:i], pathAndQuery[i+len("/mediaID="):] + path, query := url.PathSplitQuery(pathAndQuery) + tmp, _ := strconv.ParseInt(t, 10, 64) + return path, query, int(tmp), nil +} + +func findMediaByURL(medias media.Medias, baseURL *url.URL, u *url.URL) (*media.Media, bool) { + for _, media := range medias { + u1, err := media.URL(baseURL) + if err == nil && u1.String() == u.String() { + return media, true } } - return 0, "", "", fmt.Errorf("invalid track path (%s)", pathAndQuery) + return nil, false +} + +func findMediaByID(medias media.Medias, id int) (*media.Media, bool) { + if len(medias) <= id { + return nil, false + } + return medias[id], true } func findFirstSupportedTransportHeader(s *Server, tsh headers.Transports) *headers.Transport { @@ -103,6 +89,36 @@ func findFirstSupportedTransportHeader(s *Server, tsh headers.Transports) *heade return nil } +func findAndValidateTransport(inTH *headers.Transport, + tcpMediasByChannel map[int]*serverSessionMedia, +) (Transport, error) { + if inTH.Protocol == headers.TransportProtocolUDP { + if inTH.Delivery != nil && *inTH.Delivery == headers.TransportDeliveryMulticast { + return TransportUDPMulticast, nil + } + + if inTH.ClientPorts == nil { + return 0, liberrors.ErrServerTransportHeaderNoClientPorts{} + } + return TransportUDP, nil + } + + if inTH.InterleavedIDs == nil { + return 0, liberrors.ErrServerTransportHeaderNoInterleavedIDs{} + } + + if (inTH.InterleavedIDs[0]%2) != 0 || + (inTH.InterleavedIDs[0]+1) != inTH.InterleavedIDs[1] { + return 0, liberrors.ErrServerTransportHeaderInvalidInterleavedIDs{} + } + + if _, ok := tcpMediasByChannel[inTH.InterleavedIDs[0]]; ok { + return 0, liberrors.ErrServerTransportHeaderInterleavedIDsAlreadyUsed{} + } + + return TransportTCP, nil +} + // ServerSessionState is a state of a ServerSession. type ServerSessionState int @@ -132,51 +148,33 @@ func (s ServerSessionState) String() string { return "unknown" } -// ServerSessionSetuppedTrack is a setupped track of a ServerSession. -type ServerSessionSetuppedTrack struct { - id int - track Track // filled only when publishing - tcpChannel int - udpRTPReadPort int - udpRTPWriteAddr *net.UDPAddr - udpRTCPReadPort int - udpRTCPWriteAddr *net.UDPAddr - - // publish - udpRTCPReceiver *rtcpreceiver.RTCPReceiver - reorderer *rtpreorderer.Reorderer -} - // ServerSession is a server-side RTSP session. type ServerSession struct { s *Server secretID string // must not be shared, allows to take ownership of the session author *ServerConn - ctx context.Context - ctxCancel func() - bytesReceived *uint64 - bytesSent *uint64 - userData interface{} - conns map[*ServerConn]struct{} - state ServerSessionState - setuppedTracks map[int]*ServerSessionSetuppedTrack - tcpTracksByChannel map[int]*ServerSessionSetuppedTrack - setuppedTransport *Transport - setuppedBaseURL *url.URL // publish - setuppedStream *ServerStream // read - setuppedPath *string - setuppedQuery *string - lastRequestTime time.Time - tcpConn *ServerConn - announcedTracks Tracks // publish - udpLastPacketTime *int64 // publish - udpCheckStreamTimer *time.Timer - writerRunning bool - writeBuffer *ringbuffer.RingBuffer - - // writer channels - writerDone chan struct{} + ctx context.Context + ctxCancel func() + bytesReceived *uint64 + bytesSent *uint64 + userData interface{} + conns map[*ServerConn]struct{} + state ServerSessionState + setuppedMedias map[*media.Media]*serverSessionMedia + setuppedMediasOrdered []*serverSessionMedia + tcpMediasByChannel map[int]*serverSessionMedia + setuppedTransport *Transport + setuppedStream *ServerStream // read + setuppedPath *string + setuppedQuery string + lastRequestTime time.Time + tcpConn *ServerConn + announcedMedias media.Medias // publish + udpLastPacketTime *int64 // publish + udpCheckStreamTimer *time.Timer + writer serverWriter + rtpPacketBuffer *rtpPacketMultiBuffer // in request chan sessionRequestReq @@ -234,19 +232,14 @@ func (ss *ServerSession) State() ServerSessionState { return ss.state } -// SetuppedTracks returns the setupped tracks. -func (ss *ServerSession) SetuppedTracks() map[int]*ServerSessionSetuppedTrack { - return ss.setuppedTracks -} - -// SetuppedTransport returns the transport of the setupped tracks. +// SetuppedTransport returns the transport negotiated during SETUP. func (ss *ServerSession) SetuppedTransport() *Transport { return ss.setuppedTransport } -// AnnouncedTracks returns the announced tracks. -func (ss *ServerSession) AnnouncedTracks() Tracks { - return ss.announcedTracks +// AnnouncedMedias returns the announced media. +func (ss *ServerSession) AnnouncedMedias() media.Medias { + return ss.announcedMedias } // SetUserData sets some user data associated to the session. @@ -287,35 +280,17 @@ func (ss *ServerSession) run() { ss.ctxCancel() - switch ss.state { - case ServerSessionStatePlay: - ss.setuppedStream.readerSetInactive(ss) - - if *ss.setuppedTransport == TransportUDP { - ss.s.udpRTCPListener.removeClient(ss) - } - - case ServerSessionStateRecord: - if *ss.setuppedTransport == TransportUDP { - ss.s.udpRTPListener.removeClient(ss) - ss.s.udpRTCPListener.removeClient(ss) - - for _, at := range ss.setuppedTracks { - at.udpRTCPReceiver.Close() - at.udpRTCPReceiver = nil - } - } - } - if ss.setuppedStream != nil { + ss.setuppedStream.readerSetInactive(ss) ss.setuppedStream.readerRemove(ss) } - if ss.writerRunning { - ss.writeBuffer.Close() - <-ss.writerDone + for _, sm := range ss.setuppedMedias { + sm.stop() } + ss.writer.stop() + // close all associated connections, both UDP and TCP // except for the ones that called TEARDOWN // (that are detached from the session just after the request) @@ -417,12 +392,10 @@ func (ss *ServerSession) runInner() error { } case <-ss.startWriter: - if !ss.writerRunning && (ss.state == ServerSessionStateRecord || + if (ss.state == ServerSessionStateRecord || ss.state == ServerSessionStatePlay) && *ss.setuppedTransport == TransportTCP { - ss.writerRunning = true - ss.writerDone = make(chan struct{}) - go ss.runWriter() + ss.writer.start() } case <-ss.udpCheckStreamTimer.C: @@ -531,34 +504,42 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base }, liberrors.ErrServerContentTypeUnsupported{CT: ct} } - var tracks Tracks - _, err = tracks.Unmarshal(req.Body) + var sd sdp.SessionDescription + err = sd.Unmarshal(req.Body) if err != nil { return &base.Response{ StatusCode: base.StatusBadRequest, }, liberrors.ErrServerSDPInvalid{Err: err} } - for _, track := range tracks { - trackURL, err := track.url(req.URL) + var medias media.Medias + err = medias.Unmarshal(sd.MediaDescriptions) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerSDPInvalid{Err: err} + } + + for _, medi := range medias { + mediURL, err := medi.URL(req.URL) if err != nil { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("unable to generate track URL") + }, fmt.Errorf("unable to generate media URL") } - trackPath, ok := trackURL.RTSPPathAndQuery() + mediPath, ok := mediURL.RTSPPathAndQuery() if !ok { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid track URL (%v)", trackURL) + }, fmt.Errorf("invalid media URL (%v)", mediURL) } - if !strings.HasPrefix(trackPath, path) { + if !strings.HasPrefix(mediPath, path) { return &base.Response{ StatusCode: base.StatusBadRequest, - }, fmt.Errorf("invalid track path: must begin with '%s', but is '%s'", - path, trackPath) + }, fmt.Errorf("invalid media path: must begin with '%s', but is '%s'", + path, mediPath) } } @@ -569,7 +550,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base Request: req, Path: path, Query: query, - Tracks: tracks, + Medias: medias, }) if res.StatusCode != base.StatusOK { @@ -578,9 +559,8 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base ss.state = ServerSessionStatePreRecord ss.setuppedPath = &path - ss.setuppedQuery = &query - ss.setuppedBaseURL = req.URL - ss.announcedTracks = tracks + ss.setuppedQuery = query + ss.announcedMedias = medias v := time.Now().Unix() ss.udpLastPacketTime = &v @@ -613,61 +593,41 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base }, nil } - trackID, path, query, err := setupGetTrackIDPathQuery(req.URL, inTH.Mode, - ss.announcedTracks, ss.setuppedPath, ss.setuppedQuery, ss.setuppedBaseURL) + var path string + var query string + var mediaID int + switch ss.state { + case ServerSessionStateInitial, ServerSessionStatePrePlay: // play + var err error + path, query, mediaID, err = serverParseURLForPlay(req.URL) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + if ss.setuppedPath != nil && path != *ss.setuppedPath { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerMediasDifferentPaths{} + } + + default: // record + path = *ss.setuppedPath + query = ss.setuppedQuery + } + + transport, err := findAndValidateTransport(inTH, ss.tcpMediasByChannel) if err != nil { return &base.Response{ StatusCode: base.StatusBadRequest, }, err } - if _, ok := ss.setuppedTracks[trackID]; ok { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, liberrors.ErrServerTrackAlreadySetup{TrackID: trackID} - } - - var transport Transport - - if inTH.Protocol == headers.TransportProtocolUDP { - if inTH.Delivery != nil && *inTH.Delivery == headers.TransportDeliveryMulticast { - transport = TransportUDPMulticast - } else { - if inTH.ClientPorts == nil { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, liberrors.ErrServerTransportHeaderNoClientPorts{} - } - - transport = TransportUDP - } - } else { - if inTH.InterleavedIDs == nil { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, liberrors.ErrServerTransportHeaderNoInterleavedIDs{} - } - - if (inTH.InterleavedIDs[0]%2) != 0 || - (inTH.InterleavedIDs[0]+1) != inTH.InterleavedIDs[1] { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, liberrors.ErrServerTransportHeaderInvalidInterleavedIDs{} - } - - if _, ok := ss.tcpTracksByChannel[inTH.InterleavedIDs[0]]; ok { - return &base.Response{ - StatusCode: base.StatusBadRequest, - }, liberrors.ErrServerTransportHeaderInterleavedIDsAlreadyUsed{} - } - - transport = TransportTCP - } - if ss.setuppedTransport != nil && *ss.setuppedTransport != transport { return &base.Response{ StatusCode: base.StatusBadRequest, - }, liberrors.ErrServerTracksDifferentProtocols{} + }, liberrors.ErrServerMediasDifferentProtocols{} } switch ss.state { @@ -699,7 +659,6 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base Request: req, Path: path, Query: query, - TrackID: trackID, Transport: transport, }) @@ -719,6 +678,32 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base return res, err } + var medi *media.Media + var ok bool + switch ss.state { + case ServerSessionStateInitial, ServerSessionStatePrePlay: // play + medi, ok = findMediaByID(stream.medias, mediaID) + default: // record + medi, ok = findMediaByURL(ss.announcedMedias, &url.URL{ + Scheme: req.URL.Scheme, + Host: req.URL.Host, + Path: path, + RawQuery: query, + }, req.URL) + } + + if !ok { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("media not found") + } + + if _, ok := ss.setuppedMedias[medi]; ok { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerMediaAlreadySetup{} + } + if ss.state == ServerSessionStateInitial { err := stream.readerAdd(ss, transport, @@ -732,15 +717,14 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base ss.state = ServerSessionStatePrePlay ss.setuppedPath = &path - ss.setuppedQuery = &query ss.setuppedStream = stream } th := headers.Transport{} if ss.state == ServerSessionStatePrePlay { - ssrc := stream.ssrc(trackID) - if ssrc != 0 { + ssrc, ok := stream.lastSSRC(medi) + if ok { th.SSRC = &ssrc } } @@ -751,29 +735,30 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base res.Header = make(base.Header) } - sst := &ServerSessionSetuppedTrack{ - id: trackID, - } + sm := newServerSessionMedia(ss, medi) if ss.state == ServerSessionStatePreRecord { - sst.track = ss.announcedTracks[trackID] + sm.formats = make(map[uint8]*serverSessionFormat) + for _, trak := range sm.media.Formats { + sm.formats[trak.PayloadType()] = newServerSessionFormat(sm, trak) + } } switch transport { case TransportUDP: - sst.udpRTPReadPort = inTH.ClientPorts[0] - sst.udpRTCPReadPort = inTH.ClientPorts[1] + sm.udpRTPReadPort = inTH.ClientPorts[0] + sm.udpRTCPReadPort = inTH.ClientPorts[1] - sst.udpRTPWriteAddr = &net.UDPAddr{ + sm.udpRTPWriteAddr = &net.UDPAddr{ IP: ss.author.ip(), Zone: ss.author.zone(), - Port: sst.udpRTPReadPort, + Port: sm.udpRTPReadPort, } - sst.udpRTCPWriteAddr = &net.UDPAddr{ + sm.udpRTCPWriteAddr = &net.UDPAddr{ IP: ss.author.ip(), Zone: ss.author.zone(), - Port: sst.udpRTCPReadPort, + Port: sm.udpRTCPReadPort, } th.Protocol = headers.TransportProtocolUDP @@ -788,18 +773,18 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base th.Delivery = &de v := uint(127) th.TTL = &v - d := stream.streamTracks[trackID].multicastHandler.ip() + d := stream.streamMedias[medi].multicastHandler.ip() th.Destination = &d th.Ports = &[2]int{ss.s.MulticastRTPPort, ss.s.MulticastRTCPPort} default: // TCP - sst.tcpChannel = inTH.InterleavedIDs[0] + sm.tcpChannel = inTH.InterleavedIDs[0] - if ss.tcpTracksByChannel == nil { - ss.tcpTracksByChannel = make(map[int]*ServerSessionSetuppedTrack) + if ss.tcpMediasByChannel == nil { + ss.tcpMediasByChannel = make(map[int]*serverSessionMedia) } - ss.tcpTracksByChannel[inTH.InterleavedIDs[0]] = sst + ss.tcpMediasByChannel[inTH.InterleavedIDs[0]] = sm th.Protocol = headers.TransportProtocolTCP de := headers.TransportDeliveryUnicast @@ -807,10 +792,11 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base th.InterleavedIDs = inTH.InterleavedIDs } - if ss.setuppedTracks == nil { - ss.setuppedTracks = make(map[int]*ServerSessionSetuppedTrack) + if ss.setuppedMedias == nil { + ss.setuppedMedias = make(map[*media.Media]*serverSessionMedia) } - ss.setuppedTracks[trackID] = sst + ss.setuppedMedias[medi] = sm + ss.setuppedMediasOrdered = append(ss.setuppedMediasOrdered, sm) res.Header["Transport"] = th.Marshal() @@ -840,7 +826,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base // inside the callback. if ss.state != ServerSessionStatePlay && *ss.setuppedTransport != TransportUDPMulticast { - ss.writeBuffer, _ = ringbuffer.New(uint64(ss.s.WriteBufferCount)) + ss.writer.buffer, _ = ringbuffer.New(uint64(ss.s.WriteBufferCount)) } res, err := sc.s.Handler.(ServerHandlerOnPlay).OnPlay(&ServerHandlerOnPlayCtx{ @@ -853,7 +839,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base if res.StatusCode != base.StatusOK { if ss.state != ServerSessionStatePlay { - ss.writeBuffer = nil + ss.writer.buffer = nil } return res, err } @@ -864,20 +850,16 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base ss.state = ServerSessionStatePlay + for _, sm := range ss.setuppedMedias { + sm.start() + } + + ss.setuppedStream.readerSetActive(ss) + switch *ss.setuppedTransport { case TransportUDP: ss.udpCheckStreamTimer = time.NewTimer(ss.s.checkStreamPeriod) - - ss.writerRunning = true - ss.writerDone = make(chan struct{}) - go ss.runWriter() - - for _, track := range ss.setuppedTracks { - // readers can send RTCP packets only - sc.s.udpRTCPListener.addClient(ss.author.ip(), track.udpRTCPReadPort, ss, track, false) - - // firewall opening is performed by RTCP sender reports generated by ServerStream - } + ss.writer.start() case TransportUDPMulticast: ss.udpCheckStreamTimer = time.NewTimer(ss.s.checkStreamPeriod) @@ -886,42 +868,23 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base ss.tcpConn = sc ss.tcpConn.readFunc = ss.tcpConn.readFuncTCP err = errSwitchReadFunc - - // runWriter() is called by ServerConn after the response has been sent + // writer.start() is called by ServerConn after the response has been sent } - ss.setuppedStream.readerSetActive(ss) - - var trackIDs []int - for trackID := range ss.setuppedTracks { - trackIDs = append(trackIDs, trackID) - } - - sort.Slice(trackIDs, func(a, b int) bool { - return trackIDs[a] < trackIDs[b] - }) - var ri headers.RTPInfo now := time.Now() - for _, trackID := range trackIDs { - seqNum, ts, ok := ss.setuppedStream.rtpInfo(trackID, now) - if !ok { - continue - } + for i, sm := range ss.setuppedMediasOrdered { + entry := ss.setuppedStream.rtpInfoEntry(sm.media, now) + if entry != nil { + entry.URL = (&url.URL{ + Scheme: req.URL.Scheme, + Host: req.URL.Host, + Path: "/" + *ss.setuppedPath + "/mediaID=" + strconv.FormatInt(int64(i), 10), + }).String() - u := &url.URL{ - Scheme: req.URL.Scheme, - User: req.URL.User, - Host: req.URL.Host, - Path: "/" + *ss.setuppedPath + "/trackID=" + strconv.FormatInt(int64(trackID), 10), + ri = append(ri, entry) } - - ri = append(ri, &headers.RTPInfoEntry{ - URL: u.String(), - SequenceNumber: &seqNum, - Timestamp: &ts, - }) } if len(ri) > 0 { if res.Header == nil { @@ -942,10 +905,10 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base }, err } - if len(ss.setuppedTracks) != len(ss.announcedTracks) { + if len(ss.setuppedMedias) != len(ss.announcedMedias) { return &base.Response{ StatusCode: base.StatusBadRequest, - }, liberrors.ErrServerNotAllAnnouncedTracksSetup{} + }, liberrors.ErrServerNotAllAnnouncedMediasSetup{} } if path != *ss.setuppedPath { @@ -960,7 +923,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base // when recording, writeBuffer is only used to send RTCP receiver reports, // that are much smaller than RTP packets and are sent at a fixed interval. // decrease RAM consumption by allocating less buffers. - ss.writeBuffer, _ = ringbuffer.New(uint64(8)) + ss.writer.buffer, _ = ringbuffer.New(uint64(8)) res, err := ss.s.Handler.(ServerHandlerOnRecord).OnRecord(&ServerHandlerOnRecordCtx{ Session: ss, @@ -971,50 +934,27 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base }) if res.StatusCode != base.StatusOK { - ss.writeBuffer = nil + ss.writer.buffer = nil return res, err } ss.state = ServerSessionStateRecord - for _, st := range ss.setuppedTracks { - if *ss.setuppedTransport == TransportUDP { - st.reorderer = rtpreorderer.New() - } + ss.rtpPacketBuffer = newRTPPacketMultiBuffer(uint64(ss.s.ReadBufferCount)) + + for _, sm := range ss.setuppedMedias { + sm.start() } switch *ss.setuppedTransport { case TransportUDP: ss.udpCheckStreamTimer = time.NewTimer(ss.s.checkStreamPeriod) - - ss.writerRunning = true - ss.writerDone = make(chan struct{}) - go ss.runWriter() - - for trackID, st := range ss.setuppedTracks { - // open the firewall by sending test packets to the counterpart. - ss.WritePacketRTP(trackID, &rtp.Packet{Header: rtp.Header{Version: 2}}) - ss.WritePacketRTCP(trackID, &rtcp.ReceiverReport{}) - - ctrackID := trackID - - st.udpRTCPReceiver = rtcpreceiver.New( - ss.s.udpReceiverReportPeriod, - nil, - st.track.ClockRate(), - func(pkt rtcp.Packet) { - ss.WritePacketRTCP(ctrackID, pkt) - }) - - ss.s.udpRTPListener.addClient(ss.author.ip(), st.udpRTPReadPort, ss, st, true) - ss.s.udpRTCPListener.addClient(ss.author.ip(), st.udpRTCPReadPort, ss, st, true) - } + ss.writer.start() default: // TCP ss.tcpConn = sc ss.tcpConn.readFunc = ss.tcpConn.readFuncTCP err = errSwitchReadFunc - // runWriter() is called by conn after sending the response } @@ -1045,24 +985,24 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base return res, err } - if ss.writerRunning { - ss.writeBuffer.Close() - <-ss.writerDone - ss.writerRunning = false + ss.writer.stop() + + if ss.setuppedStream != nil { + ss.setuppedStream.readerSetInactive(ss) + } + + for _, sm := range ss.setuppedMedias { + sm.stop() } switch ss.state { case ServerSessionStatePlay: - ss.setuppedStream.readerSetInactive(ss) - ss.state = ServerSessionStatePrePlay switch *ss.setuppedTransport { case TransportUDP: ss.udpCheckStreamTimer = emptyTimer() - ss.s.udpRTCPListener.removeClient(ss) - case TransportUDPMulticast: ss.udpCheckStreamTimer = emptyTimer() @@ -1077,24 +1017,12 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base case TransportUDP: ss.udpCheckStreamTimer = emptyTimer() - ss.s.udpRTPListener.removeClient(ss) - ss.s.udpRTCPListener.removeClient(ss) - - for _, st := range ss.setuppedTracks { - st.udpRTCPReceiver.Close() - st.udpRTCPReceiver = nil - } - default: // TCP ss.tcpConn.readFunc = ss.tcpConn.readFuncStandard err = errSwitchReadFunc ss.tcpConn = nil } - for _, st := range ss.setuppedTracks { - st.reorderer = nil - } - ss.state = ServerSessionStatePreRecord } @@ -1150,110 +1078,67 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base }, nil } -func (ss *ServerSession) runWriter() { - defer close(ss.writerDone) - - var writeFunc func(int, bool, []byte) - - if *ss.setuppedTransport == TransportUDP { - writeFunc = func(trackID int, isRTP bool, payload []byte) { - if isRTP { - ss.s.udpRTPListener.write(payload, ss.setuppedTracks[trackID].udpRTPWriteAddr) - } else { - ss.s.udpRTCPListener.write(payload, ss.setuppedTracks[trackID].udpRTCPWriteAddr) - } +// OnPacketRTPAny sets the callback that is called when a RTP packet is read from any setupped media. +func (ss *ServerSession) OnPacketRTPAny(cb func(*media.Media, format.Format, *rtp.Packet)) { + for _, sm := range ss.setuppedMedias { + cmedia := sm.media + for _, trak := range sm.media.Formats { + ss.OnPacketRTP(sm.media, trak, func(pkt *rtp.Packet) { + cb(cmedia, trak, pkt) + }) } - } else { // TCP - rtpFrames := make(map[int]*base.InterleavedFrame, len(ss.setuppedTracks)) - rtcpFrames := make(map[int]*base.InterleavedFrame, len(ss.setuppedTracks)) - - for trackID, sst := range ss.setuppedTracks { - rtpFrames[trackID] = &base.InterleavedFrame{Channel: sst.tcpChannel} - rtcpFrames[trackID] = &base.InterleavedFrame{Channel: sst.tcpChannel + 1} - } - - buf := make([]byte, maxPacketSize+4) - - writeFunc = func(trackID int, isRTP bool, payload []byte) { - if isRTP { - fr := rtpFrames[trackID] - fr.Payload = payload - - ss.tcpConn.nconn.SetWriteDeadline(time.Now().Add(ss.s.WriteTimeout)) - ss.tcpConn.conn.WriteInterleavedFrame(fr, buf) - } else { - fr := rtcpFrames[trackID] - fr.Payload = payload - - ss.tcpConn.nconn.SetWriteDeadline(time.Now().Add(ss.s.WriteTimeout)) - ss.tcpConn.conn.WriteInterleavedFrame(fr, buf) - } - } - } - - for { - tmp, ok := ss.writeBuffer.Pull() - if !ok { - return - } - data := tmp.(trackTypePayload) - - atomic.AddUint64(ss.bytesSent, uint64(len(data.payload))) - - writeFunc(data.trackID, data.isRTP, data.payload) } } -func (ss *ServerSession) onPacketRTCP(trackID int, pkt rtcp.Packet) { - if h, ok := ss.s.Handler.(ServerHandlerOnPacketRTCP); ok { - h.OnPacketRTCP(&ServerHandlerOnPacketRTCPCtx{ - Session: ss, - TrackID: trackID, - Packet: pkt, +// OnPacketRTCPAny sets the callback that is called when a RTCP packet is read from any setupped media. +func (ss *ServerSession) OnPacketRTCPAny(cb func(*media.Media, rtcp.Packet)) { + for _, sm := range ss.setuppedMedias { + cmedia := sm.media + ss.OnPacketRTCP(sm.media, func(pkt rtcp.Packet) { + cb(cmedia, pkt) }) } } -func (ss *ServerSession) writePacketRTP(trackID int, byts []byte) { - if _, ok := ss.setuppedTracks[trackID]; !ok { - return - } +// OnPacketRTP sets the callback that is called when a RTP packet is read. +func (ss *ServerSession) OnPacketRTP(medi *media.Media, trak format.Format, cb func(*rtp.Packet)) { + sm := ss.setuppedMedias[medi] + st := sm.formats[trak.PayloadType()] + st.onPacketRTP = cb +} - ss.writeBuffer.Push(trackTypePayload{ - trackID: trackID, - isRTP: true, - payload: byts, - }) +// OnPacketRTCP sets the callback that is called when a RTCP packet is read. +func (ss *ServerSession) OnPacketRTCP(medi *media.Media, cb func(rtcp.Packet)) { + sm := ss.setuppedMedias[medi] + sm.onPacketRTCP = cb +} + +func (ss *ServerSession) writePacketRTP(medi *media.Media, byts []byte) { + sm := ss.setuppedMedias[medi] + sm.writePacketRTP(byts) } // WritePacketRTP writes a RTP packet to the session. -func (ss *ServerSession) WritePacketRTP(trackID int, pkt *rtp.Packet) { +func (ss *ServerSession) WritePacketRTP(medi *media.Media, pkt *rtp.Packet) { byts, err := pkt.Marshal() if err != nil { return } - ss.writePacketRTP(trackID, byts) + ss.writePacketRTP(medi, byts) } -func (ss *ServerSession) writePacketRTCP(trackID int, byts []byte) { - if _, ok := ss.setuppedTracks[trackID]; !ok { - return - } - - ss.writeBuffer.Push(trackTypePayload{ - trackID: trackID, - isRTP: false, - payload: byts, - }) +func (ss *ServerSession) writePacketRTCP(medi *media.Media, byts []byte) { + sm := ss.setuppedMedias[medi] + sm.writePacketRTCP(byts) } // WritePacketRTCP writes a RTCP packet to the session. -func (ss *ServerSession) WritePacketRTCP(trackID int, pkt rtcp.Packet) { +func (ss *ServerSession) WritePacketRTCP(medi *media.Media, pkt rtcp.Packet) { byts, err := pkt.Marshal() if err != nil { return } - ss.writePacketRTCP(trackID, byts) + ss.writePacketRTCP(medi, byts) } diff --git a/serversessionmedia.go b/serversessionmedia.go new file mode 100644 index 00000000..0a1d57ff --- /dev/null +++ b/serversessionmedia.go @@ -0,0 +1,302 @@ +package gortsplib + +import ( + "fmt" + "net" + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/base" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/rtcpreceiver" + "github.com/aler9/gortsplib/v2/pkg/rtpreorderer" +) + +type serverSessionMedia struct { + ss *ServerSession + media *media.Media + tcpChannel int + udpRTPReadPort int + udpRTPWriteAddr *net.UDPAddr + udpRTCPReadPort int + udpRTCPWriteAddr *net.UDPAddr + tcpRTPFrame *base.InterleavedFrame + tcpRTCPFrame *base.InterleavedFrame + tcpBuffer []byte + formats map[uint8]*serverSessionFormat // record + writePacketRTPInQueue func([]byte) + writePacketRTCPInQueue func([]byte) + readRTP func([]byte) error + readRTCP func([]byte) error + onPacketRTCP func(rtcp.Packet) +} + +func newServerSessionMedia(ss *ServerSession, medi *media.Media) *serverSessionMedia { + return &serverSessionMedia{ + ss: ss, + media: medi, + onPacketRTCP: func(rtcp.Packet) {}, + } +} + +func (sm *serverSessionMedia) start() { + switch *sm.ss.setuppedTransport { + case TransportUDP, TransportUDPMulticast: + sm.writePacketRTPInQueue = sm.writePacketRTPInQueueUDP + sm.writePacketRTCPInQueue = sm.writePacketRTCPInQueueUDP + + if sm.ss.state == ServerSessionStatePlay { + sm.readRTCP = sm.readRTCPUDPPlay + } else { + sm.readRTP = sm.readRTPUDPRecord + sm.readRTCP = sm.readRTCPUDPRecord + + for _, tr := range sm.formats { + tr.udpReorderer = rtpreorderer.New() + + cmedia := sm.media + tr.udpRTCPReceiver = rtcpreceiver.New( + sm.ss.s.udpReceiverReportPeriod, + nil, + tr.format.ClockRate(), + func(pkt rtcp.Packet) { + sm.ss.WritePacketRTCP(cmedia, pkt) + }) + } + } + + case TransportTCP: + sm.writePacketRTPInQueue = sm.writePacketRTPInQueueTCP + sm.writePacketRTCPInQueue = sm.writePacketRTCPInQueueTCP + + if sm.ss.state == ServerSessionStatePlay { + sm.readRTP = sm.readRTPTCPPlay + sm.readRTCP = sm.readRTCPTCPPlay + } else { + sm.readRTP = sm.readRTPTCPRecord + sm.readRTCP = sm.readRTCPTCPRecord + } + + sm.tcpRTPFrame = &base.InterleavedFrame{Channel: sm.tcpChannel} + sm.tcpRTCPFrame = &base.InterleavedFrame{Channel: sm.tcpChannel + 1} + sm.tcpBuffer = make([]byte, maxPacketSize+4) + } + + if *sm.ss.setuppedTransport == TransportUDP { + if sm.ss.state == ServerSessionStatePlay { + // firewall opening is performed by RTCP sender reports generated by ServerStream + + // readers can send RTCP packets only + sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm) + } else { + // open the firewall by sending test packets to the counterpart. + sm.ss.WritePacketRTP(sm.media, &rtp.Packet{Header: rtp.Header{Version: 2}}) + sm.ss.WritePacketRTCP(sm.media, &rtcp.ReceiverReport{}) + + sm.ss.s.udpRTPListener.addClient(sm.ss.author.ip(), sm.udpRTPReadPort, sm) + sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm) + } + } +} + +func (sm *serverSessionMedia) stop() { + if *sm.ss.setuppedTransport == TransportUDP { + sm.ss.s.udpRTPListener.removeClient(sm) + sm.ss.s.udpRTCPListener.removeClient(sm) + } + + for _, tr := range sm.formats { + if tr.udpRTCPReceiver != nil { + tr.udpRTCPReceiver.Close() + tr.udpRTCPReceiver = nil + } + } +} + +func (sm *serverSessionMedia) writePacketRTPInQueueUDP(payload []byte) { + atomic.AddUint64(sm.ss.bytesSent, uint64(len(payload))) + sm.ss.s.udpRTPListener.write(payload, sm.udpRTPWriteAddr) +} + +func (sm *serverSessionMedia) writePacketRTCPInQueueUDP(payload []byte) { + atomic.AddUint64(sm.ss.bytesSent, uint64(len(payload))) + sm.ss.s.udpRTCPListener.write(payload, sm.udpRTCPWriteAddr) +} + +func (sm *serverSessionMedia) writePacketRTPInQueueTCP(payload []byte) { + atomic.AddUint64(sm.ss.bytesSent, uint64(len(payload))) + sm.tcpRTPFrame.Payload = payload + sm.ss.tcpConn.nconn.SetWriteDeadline(time.Now().Add(sm.ss.s.WriteTimeout)) + sm.ss.tcpConn.conn.WriteInterleavedFrame(sm.tcpRTPFrame, sm.tcpBuffer) +} + +func (sm *serverSessionMedia) writePacketRTCPInQueueTCP(payload []byte) { + atomic.AddUint64(sm.ss.bytesSent, uint64(len(payload))) + sm.tcpRTCPFrame.Payload = payload + sm.ss.tcpConn.nconn.SetWriteDeadline(time.Now().Add(sm.ss.s.WriteTimeout)) + sm.ss.tcpConn.conn.WriteInterleavedFrame(sm.tcpRTCPFrame, sm.tcpBuffer) +} + +func (sm *serverSessionMedia) writePacketRTP(payload []byte) { + sm.ss.writer.queue(func() { + sm.writePacketRTPInQueue(payload) + }) +} + +func (sm *serverSessionMedia) writePacketRTCP(payload []byte) { + sm.ss.writer.queue(func() { + sm.writePacketRTCPInQueue(payload) + }) +} + +func (sm *serverSessionMedia) readRTCPUDPPlay(payload []byte) error { + plen := len(payload) + + atomic.AddUint64(sm.ss.bytesReceived, uint64(plen)) + + if plen == (maxPacketSize + 1) { + onDecodeError(sm.ss, fmt.Errorf("RTCP packet is too big to be read with UDP")) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + onDecodeError(sm.ss, err) + return nil + } + + for _, pkt := range packets { + sm.onPacketRTCP(pkt) + } + + return nil +} + +func (sm *serverSessionMedia) readRTPUDPRecord(payload []byte) error { + plen := len(payload) + + atomic.AddUint64(sm.ss.bytesReceived, uint64(plen)) + + if plen == (maxPacketSize + 1) { + onDecodeError(sm.ss, fmt.Errorf("RTP packet is too big to be read with UDP")) + return nil + } + + pkt := sm.ss.rtpPacketBuffer.next() + err := pkt.Unmarshal(payload) + if err != nil { + onDecodeError(sm.ss, err) + return nil + } + + trak, ok := sm.formats[pkt.PayloadType] + if !ok { + onDecodeError(sm.ss, fmt.Errorf("received RTP packet with unknown payload type (%d)", pkt.PayloadType)) + return nil + } + + now := time.Now() + atomic.StoreInt64(sm.ss.udpLastPacketTime, now.Unix()) + + trak.readRTPUDP(pkt, now) + return nil +} + +func (sm *serverSessionMedia) readRTCPUDPRecord(payload []byte) error { + plen := len(payload) + + atomic.AddUint64(sm.ss.bytesReceived, uint64(plen)) + + if plen == (maxPacketSize + 1) { + onDecodeError(sm.ss, fmt.Errorf("RTCP packet is too big to be read with UDP")) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + onDecodeError(sm.ss, err) + return nil + } + + now := time.Now() + atomic.StoreInt64(sm.ss.udpLastPacketTime, now.Unix()) + + for _, pkt := range packets { + if sr, ok := pkt.(*rtcp.SenderReport); ok { + format := serverFindFormatWithSSRC(sm.formats, sr.SSRC) + if format != nil { + format.udpRTCPReceiver.ProcessSenderReport(sr, now) + } + } + } + + for _, pkt := range packets { + sm.onPacketRTCP(pkt) + } + + return nil +} + +func (sm *serverSessionMedia) readRTPTCPPlay(payload []byte) error { + return nil +} + +func (sm *serverSessionMedia) readRTCPTCPPlay(payload []byte) error { + if len(payload) > maxPacketSize { + onDecodeError(sm.ss, fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", + len(payload), maxPacketSize)) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + onDecodeError(sm.ss, err) + return nil + } + + for _, pkt := range packets { + sm.onPacketRTCP(pkt) + } + + return nil +} + +func (sm *serverSessionMedia) readRTPTCPRecord(payload []byte) error { + pkt := sm.ss.rtpPacketBuffer.next() + err := pkt.Unmarshal(payload) + if err != nil { + return err + } + + trak, ok := sm.formats[pkt.PayloadType] + if !ok { + onDecodeError(sm.ss, fmt.Errorf("received RTP packet with unknown payload type (%d)", pkt.PayloadType)) + return nil + } + + trak.readRTPTCP(pkt) + return nil +} + +func (sm *serverSessionMedia) readRTCPTCPRecord(payload []byte) error { + if len(payload) > maxPacketSize { + onDecodeError(sm.ss, fmt.Errorf("RTCP packet size (%d) is greater than maximum allowed (%d)", + len(payload), maxPacketSize)) + return nil + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + onDecodeError(sm.ss, err) + return nil + } + + for _, pkt := range packets { + sm.onPacketRTCP(pkt) + } + + return nil +} diff --git a/serversessiontrack.go b/serversessiontrack.go new file mode 100644 index 00000000..3c04563b --- /dev/null +++ b/serversessiontrack.go @@ -0,0 +1,45 @@ +package gortsplib + +import ( + "fmt" + "time" + + "github.com/pion/rtp" + + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/rtcpreceiver" + "github.com/aler9/gortsplib/v2/pkg/rtpreorderer" +) + +type serverSessionFormat struct { + sm *serverSessionMedia + format format.Format + udpReorderer *rtpreorderer.Reorderer + udpRTCPReceiver *rtcpreceiver.RTCPReceiver + onPacketRTP func(*rtp.Packet) +} + +func newServerSessionFormat(sm *serverSessionMedia, trak format.Format) *serverSessionFormat { + return &serverSessionFormat{ + sm: sm, + format: trak, + onPacketRTP: func(*rtp.Packet) {}, + } +} + +func (st *serverSessionFormat) readRTPUDP(pkt *rtp.Packet, now time.Time) { + packets, missing := st.udpReorderer.Process(pkt) + if missing != 0 { + onDecodeError(st.sm.ss, fmt.Errorf("%d RTP packet(s) lost", missing)) + // do not return + } + + for _, pkt := range packets { + st.udpRTCPReceiver.ProcessPacket(pkt, now, st.format.PTSEqualsDTS(pkt)) + st.onPacketRTP(pkt) + } +} + +func (st *serverSessionFormat) readRTPTCP(pkt *rtp.Packet) { + st.onPacketRTP(pkt) +} diff --git a/serverstream.go b/serverstream.go index a5e64d8d..620b82c2 100644 --- a/serverstream.go +++ b/serverstream.go @@ -8,55 +8,73 @@ import ( "github.com/pion/rtcp" "github.com/pion/rtp" - "github.com/aler9/gortsplib/pkg/liberrors" - "github.com/aler9/gortsplib/pkg/rtcpsender" + "github.com/aler9/gortsplib/v2/pkg/headers" + "github.com/aler9/gortsplib/v2/pkg/liberrors" + "github.com/aler9/gortsplib/v2/pkg/media" + "github.com/aler9/gortsplib/v2/pkg/rtcpsender" ) -type serverStreamTrack struct { - lastSequenceNumber uint16 - lastSSRC uint32 - lastTimeFilled bool - lastTimeRTP uint32 - lastTimeNTP time.Time - rtcpSender *rtcpsender.RTCPSender - multicastHandler *serverMulticastHandler -} - // ServerStream represents a data stream. // This is in charge of // - distributing the stream to each reader // - allocating multicast listeners // - gathering infos about the stream in order to generate SSRC and RTP-Info type ServerStream struct { - tracks Tracks + medias media.Medias mutex sync.RWMutex s *Server activeUnicastReaders map[*ServerSession]struct{} readers map[*ServerSession]struct{} - streamTracks []*serverStreamTrack + streamMedias map[*media.Media]*serverStreamMedia closed bool } // NewServerStream allocates a ServerStream. -func NewServerStream(tracks Tracks) *ServerStream { - tracks = tracks.clone() - tracks.setControls() - +func NewServerStream(medias media.Medias) *ServerStream { st := &ServerStream{ - tracks: tracks, + medias: medias, activeUnicastReaders: make(map[*ServerSession]struct{}), readers: make(map[*ServerSession]struct{}), } - st.streamTracks = make([]*serverStreamTrack, len(tracks)) - for i := range st.streamTracks { - st.streamTracks[i] = &serverStreamTrack{} + st.streamMedias = make(map[*media.Media]*serverStreamMedia, len(medias)) + for _, media := range medias { + ssm := &serverStreamMedia{} + + ssm.formats = make(map[uint8]*serverStreamFormat) + for _, trak := range media.Formats { + tr := &serverStreamFormat{ + format: trak, + } + + cmedia := media + tr.rtcpSender = rtcpsender.New( + trak.ClockRate(), + func(pkt rtcp.Packet) { + st.WritePacketRTCP(cmedia, pkt) + }, + ) + + ssm.formats[trak.PayloadType()] = tr + } + + st.streamMedias[media] = ssm } return st } +func (st *ServerStream) initializeServerDependentPart() { + if !st.s.DisableRTCPSenderReports { + for _, ssm := range st.streamMedias { + for _, tr := range ssm.formats { + tr.rtcpSender.Start(st.s.senderReportPeriod) + } + } + } +} + // Close closes a ServerStream. func (st *ServerStream) Close() error { st.mutex.Lock() @@ -67,55 +85,85 @@ func (st *ServerStream) Close() error { ss.Close() } - for _, track := range st.streamTracks { - if track.rtcpSender != nil { - track.rtcpSender.Close() - } - if track.multicastHandler != nil { - track.multicastHandler.close() - } + for _, sm := range st.streamMedias { + sm.close() } return nil } -// Tracks returns the tracks of the stream. -func (st *ServerStream) Tracks() Tracks { - return st.tracks +// Medias returns the medias of the stream. +func (st *ServerStream) Medias() media.Medias { + return st.medias } -func (st *ServerStream) ssrc(trackID int) uint32 { - st.mutex.Lock() - defer st.mutex.Unlock() - return st.streamTracks[trackID].lastSSRC -} - -func (st *ServerStream) rtpInfo(trackID int, now time.Time) (uint16, uint32, bool) { +func (st *ServerStream) lastSSRC(medi *media.Media) (uint32, bool) { st.mutex.Lock() defer st.mutex.Unlock() - track := st.streamTracks[trackID] + sm := st.streamMedias[medi] - if !track.lastTimeFilled { - return 0, 0, false + // since lastSSRC() is used to fill SSRC inside the Transport header, + // if there are multiple formats inside a single media stream, + // do not return anything, since Transport headers don't support multiple SSRCs. + if len(sm.formats) > 1 { + return 0, false } - clockRate := st.tracks[trackID].ClockRate() + var firstKey uint8 + for key := range sm.formats { + firstKey = key + break + } + + return sm.formats[firstKey].rtcpSender.LastSSRC() +} + +func (st *ServerStream) rtpInfoEntry(medi *media.Media, now time.Time) *headers.RTPInfoEntry { + st.mutex.Lock() + defer st.mutex.Unlock() + + sm := st.streamMedias[medi] + + // if there are multiple formats inside a single media stream, + // do not generate a RTP-Info entry, since RTP-Info doesn't support + // multiple sequence numbers / timestamps. + if len(sm.formats) > 1 { + return nil + } + + var firstKey uint8 + for key := range sm.formats { + firstKey = key + break + } + + format := sm.formats[firstKey] + + lastSeqNum, lastTimeRTP, lastTimeNTP, ok := format.rtcpSender.LastPacketData() + if !ok { + return nil + } + + clockRate := format.format.ClockRate() if clockRate == 0 { - return 0, 0, false + return nil } // sequence number of the first packet of the stream - seq := track.lastSequenceNumber + 1 + seqNum := lastSeqNum + 1 // RTP timestamp corresponding to the time value in // the Range response header. // remove a small quantity in order to avoid DTS > PTS - ts := uint32(uint64(track.lastTimeRTP) + - uint64(now.Sub(track.lastTimeNTP).Seconds()*float64(clockRate)) - + ts := uint32(uint64(lastTimeRTP) + + uint64(now.Sub(lastTimeNTP).Seconds()*float64(clockRate)) - uint64(clockRate)/10) - return seq, ts, true + return &headers.RTPInfoEntry{ + SequenceNumber: &seqNum, + Timestamp: &ts, + } } func (st *ServerStream) readerAdd( @@ -132,19 +180,7 @@ func (st *ServerStream) readerAdd( if st.s == nil { st.s = ss.s - - if !st.s.DisableRTCPSenderReports { - for trackID, track := range st.streamTracks { - cTrackID := trackID - track.rtcpSender = rtcpsender.New( - st.s.udpSenderReportPeriod, - st.tracks[trackID].ClockRate(), - func(pkt rtcp.Packet) { - st.WritePacketRTCP(cTrackID, pkt) - }, - ) - } - } + st.initializeServerDependentPart() } switch transport { @@ -154,7 +190,7 @@ func (st *ServerStream) readerAdd( if *r.setuppedTransport == TransportUDP && r.author.ip().Equal(ss.author.ip()) && r.author.zone() == ss.author.zone() { - for _, rt := range r.setuppedTracks { + for _, rt := range r.setuppedMedias { if rt.udpRTPReadPort == clientPorts[0] { return liberrors.ErrServerUDPPortsAlreadyInUse{Port: rt.udpRTPReadPort} } @@ -164,13 +200,10 @@ func (st *ServerStream) readerAdd( case TransportUDPMulticast: // allocate multicast listeners - for _, track := range st.streamTracks { - if track.multicastHandler == nil { - mh, err := newServerMulticastHandler(st.s) - if err != nil { - return err - } - track.multicastHandler = mh + for _, media := range st.streamMedias { + err := media.allocateMulticastHandler(st.s) + if err != nil { + return err } } } @@ -191,10 +224,10 @@ func (st *ServerStream) readerRemove(ss *ServerSession) { delete(st.readers, ss) if len(st.readers) == 0 { - for _, track := range st.streamTracks { - if track.multicastHandler != nil { - track.multicastHandler.close() - track.multicastHandler = nil + for _, media := range st.streamMedias { + if media.multicastHandler != nil { + media.multicastHandler.close() + media.multicastHandler = nil } } } @@ -209,9 +242,10 @@ func (st *ServerStream) readerSetActive(ss *ServerSession) { } if *ss.setuppedTransport == TransportUDPMulticast { - for trackID, track := range ss.setuppedTracks { - st.streamTracks[trackID].multicastHandler.rtcpl.addClient( - ss.author.ip(), st.streamTracks[trackID].multicastHandler.rtcpl.port(), ss, track, false) + for mediaID, sm := range ss.setuppedMedias { + streamMedia := st.streamMedias[mediaID] + streamMedia.multicastHandler.rtcpl.addClient( + ss.author.ip(), streamMedia.multicastHandler.rtcpl.port(), sm) } } else { st.activeUnicastReaders[ss] = struct{}{} @@ -227,8 +261,9 @@ func (st *ServerStream) readerSetInactive(ss *ServerSession) { } if *ss.setuppedTransport == TransportUDPMulticast { - for trackID := range ss.setuppedTracks { - st.streamTracks[trackID].multicastHandler.rtcpl.removeClient(ss) + for mediaID, sm := range ss.setuppedMedias { + streamMedia := st.streamMedias[mediaID] + streamMedia.multicastHandler.rtcpl.removeClient(sm) } } else { delete(st.activeUnicastReaders, ss) @@ -236,14 +271,14 @@ func (st *ServerStream) readerSetInactive(ss *ServerSession) { } // WritePacketRTP writes a RTP packet to all the readers of the stream. -func (st *ServerStream) WritePacketRTP(trackID int, pkt *rtp.Packet) { - st.WritePacketRTPWithNTP(trackID, pkt, time.Now()) +func (st *ServerStream) WritePacketRTP(medi *media.Media, pkt *rtp.Packet) { + st.WritePacketRTPWithNTP(medi, pkt, time.Now()) } // WritePacketRTPWithNTP writes a RTP packet to all the readers of the stream. // ntp is the absolute time of the packet, and is needed to generate RTCP sender reports // that allows the receiver to reconstruct the absolute time of the packet. -func (st *ServerStream) WritePacketRTPWithNTP(trackID int, pkt *rtp.Packet, ntp time.Time) { +func (st *ServerStream) WritePacketRTPWithNTP(medi *media.Media, pkt *rtp.Packet, ntp time.Time) { byts := make([]byte, maxPacketSize) n, err := pkt.MarshalTo(byts) if err != nil { @@ -258,35 +293,28 @@ func (st *ServerStream) WritePacketRTPWithNTP(trackID int, pkt *rtp.Packet, ntp return } - track := st.streamTracks[trackID] - ptsEqualsDTS := ptsEqualsDTS(st.tracks[trackID], pkt) + sm := st.streamMedias[medi] - if ptsEqualsDTS { - track.lastTimeFilled = true - track.lastTimeRTP = pkt.Header.Timestamp - track.lastTimeNTP = ntp - } + trak := sm.formats[pkt.PayloadType] - track.lastSequenceNumber = pkt.Header.SequenceNumber - track.lastSSRC = pkt.Header.SSRC - - if track.rtcpSender != nil { - track.rtcpSender.ProcessPacketRTP(ntp, pkt, ptsEqualsDTS) - } + trak.rtcpSender.ProcessPacket(pkt, ntp, trak.format.PTSEqualsDTS(pkt)) // send unicast for r := range st.activeUnicastReaders { - r.writePacketRTP(trackID, byts) + sm, ok := r.setuppedMedias[medi] + if ok { + sm.writePacketRTP(byts) + } } // send multicast - if track.multicastHandler != nil { - track.multicastHandler.writePacketRTP(byts) + if sm.multicastHandler != nil { + sm.multicastHandler.writePacketRTP(byts) } } // WritePacketRTCP writes a RTCP packet to all the readers of the stream. -func (st *ServerStream) WritePacketRTCP(trackID int, pkt rtcp.Packet) { +func (st *ServerStream) WritePacketRTCP(medi *media.Media, pkt rtcp.Packet) { byts, err := pkt.Marshal() if err != nil { return @@ -299,14 +327,18 @@ func (st *ServerStream) WritePacketRTCP(trackID int, pkt rtcp.Packet) { return } + sm := st.streamMedias[medi] + // send unicast for r := range st.activeUnicastReaders { - r.writePacketRTCP(trackID, byts) + sm, ok := r.setuppedMedias[medi] + if ok { + sm.writePacketRTCP(byts) + } } // send multicast - track := st.streamTracks[trackID] - if track.multicastHandler != nil { - track.multicastHandler.writePacketRTCP(byts) + if sm.multicastHandler != nil { + sm.multicastHandler.writePacketRTCP(byts) } } diff --git a/serverstreammedia.go b/serverstreammedia.go new file mode 100644 index 00000000..19deec6c --- /dev/null +++ b/serverstreammedia.go @@ -0,0 +1,30 @@ +package gortsplib + +type serverStreamMedia struct { + formats map[uint8]*serverStreamFormat + multicastHandler *serverMulticastHandler +} + +func (sm *serverStreamMedia) close() { + for _, tr := range sm.formats { + if tr.rtcpSender != nil { + tr.rtcpSender.Close() + } + } + + if sm.multicastHandler != nil { + sm.multicastHandler.close() + } +} + +func (sm *serverStreamMedia) allocateMulticastHandler(s *Server) error { + if sm.multicastHandler == nil { + mh, err := newServerMulticastHandler(s) + if err != nil { + return err + } + + sm.multicastHandler = mh + } + return nil +} diff --git a/serverstreamtrack.go b/serverstreamtrack.go new file mode 100644 index 00000000..db7b1cf9 --- /dev/null +++ b/serverstreamtrack.go @@ -0,0 +1,11 @@ +package gortsplib + +import ( + "github.com/aler9/gortsplib/v2/pkg/format" + "github.com/aler9/gortsplib/v2/pkg/rtcpsender" +) + +type serverStreamFormat struct { + format format.Format + rtcpSender *rtcpsender.RTCPSender +} diff --git a/serverudpl.go b/serverudpl.go index f5fb741d..4e1e111e 100644 --- a/serverudpl.go +++ b/serverudpl.go @@ -5,17 +5,22 @@ import ( "net" "strconv" "sync" - "sync/atomic" "time" - "github.com/pion/rtcp" "golang.org/x/net/ipv4" ) -type clientData struct { - session *ServerSession - track *ServerSessionSetuppedTrack - isPublishing bool +func serverFindFormatWithSSRC( + formats map[uint8]*serverSessionFormat, + ssrc uint32, +) *serverSessionFormat { + for _, format := range formats { + tssrc, ok := format.udpRTCPReceiver.LastSSRC() + if ok && tssrc == ssrc { + return format + } + } + return nil } type clientAddr struct { @@ -51,7 +56,7 @@ type serverUDPListener struct { isRTP bool writeTimeout time.Duration clientsMutex sync.RWMutex - clients map[clientAddr]*clientData + clients map[clientAddr]*serverSessionMedia readerDone chan struct{} } @@ -143,7 +148,7 @@ func newServerUDPListener( s: s, pc: pc, listenIP: listenIP, - clients: make(map[clientAddr]*clientData), + clients: make(map[clientAddr]*serverSessionMedia), isRTP: isRTP, writeTimeout: s.WriteTimeout, readerDone: make(chan struct{}), @@ -170,11 +175,15 @@ func (u *serverUDPListener) port() int { func (u *serverUDPListener) runReader() { defer close(u.readerDone) - var processFunc func(*clientData, []byte) + var readFunc func(*serverSessionMedia, []byte) if u.isRTP { - processFunc = u.processRTP + readFunc = func(sm *serverSessionMedia, payload []byte) { + sm.readRTP(payload) + } } else { - processFunc = u.processRTCP + readFunc = func(sm *serverSessionMedia, payload []byte) { + sm.readRTCP(payload) + } } for { @@ -190,88 +199,16 @@ func (u *serverUDPListener) runReader() { var clientAddr clientAddr clientAddr.fill(addr.IP, addr.Port) - clientData, ok := u.clients[clientAddr] + sm, ok := u.clients[clientAddr] if !ok { return } - processFunc(clientData, buf[:n]) + readFunc(sm, buf[:n]) }() } } -func (u *serverUDPListener) processRTP(clientData *clientData, payload []byte) { - plen := len(payload) - - atomic.AddUint64(clientData.session.bytesReceived, uint64(plen)) - - if plen == (maxPacketSize + 1) { - onDecodeError(clientData.session, fmt.Errorf("RTP packet is too big to be read with UDP")) - return - } - - pkt := u.s.udpRTPPacketBuffer.next() - err := pkt.Unmarshal(payload) - if err != nil { - onDecodeError(clientData.session, err) - return - } - - now := time.Now() - atomic.StoreInt64(clientData.session.udpLastPacketTime, now.Unix()) - - packets, missing := clientData.track.reorderer.Process(pkt) - if missing != 0 { - onDecodeError(clientData.session, fmt.Errorf("%d RTP packet(s) lost", missing)) - // do not return - } - - track := clientData.track.track - - for _, pkt := range packets { - ptsEqualsDTS := ptsEqualsDTS(track, pkt) - clientData.track.udpRTCPReceiver.ProcessPacketRTP(now, pkt, ptsEqualsDTS) - - if h, ok := clientData.session.s.Handler.(ServerHandlerOnPacketRTP); ok { - h.OnPacketRTP(&ServerHandlerOnPacketRTPCtx{ - Session: clientData.session, - TrackID: clientData.track.id, - Packet: pkt, - }) - } - } -} - -func (u *serverUDPListener) processRTCP(clientData *clientData, payload []byte) { - plen := len(payload) - - atomic.AddUint64(clientData.session.bytesReceived, uint64(plen)) - - if plen == (maxPacketSize + 1) { - onDecodeError(clientData.session, fmt.Errorf("RTCP packet is too big to be read with UDP")) - return - } - - packets, err := rtcp.Unmarshal(payload) - if err != nil { - onDecodeError(clientData.session, err) - return - } - - if clientData.isPublishing { - now := time.Now() - atomic.StoreInt64(clientData.session.udpLastPacketTime, now.Unix()) - - for _, pkt := range packets { - clientData.track.udpRTCPReceiver.ProcessPacketRTCP(now, pkt) - } - } - - for _, pkt := range packets { - clientData.session.onPacketRTCP(clientData.track.id, pkt) - } -} - func (u *serverUDPListener) write(buf []byte, addr *net.UDPAddr) error { // no mutex is needed here since Write() has an internal lock. // https://github.com/golang/go/issues/27203#issuecomment-534386117 @@ -281,28 +218,22 @@ func (u *serverUDPListener) write(buf []byte, addr *net.UDPAddr) error { return err } -func (u *serverUDPListener) addClient(ip net.IP, port int, ss *ServerSession, - track *ServerSessionSetuppedTrack, isPublishing bool, -) { +func (u *serverUDPListener) addClient(ip net.IP, port int, sm *serverSessionMedia) { u.clientsMutex.Lock() defer u.clientsMutex.Unlock() var addr clientAddr addr.fill(ip, port) - u.clients[addr] = &clientData{ - session: ss, - track: track, - isPublishing: isPublishing, - } + u.clients[addr] = sm } -func (u *serverUDPListener) removeClient(ss *ServerSession) { +func (u *serverUDPListener) removeClient(sm *serverSessionMedia) { u.clientsMutex.Lock() defer u.clientsMutex.Unlock() - for addr, data := range u.clients { - if data.session == ss { + for addr, sm1 := range u.clients { + if sm1 == sm { delete(u.clients, addr) } } diff --git a/serverwriter.go b/serverwriter.go new file mode 100644 index 00000000..8417280d --- /dev/null +++ b/serverwriter.go @@ -0,0 +1,45 @@ +package gortsplib + +import ( + "github.com/aler9/gortsplib/v2/pkg/ringbuffer" +) + +type serverWriter struct { + running bool + buffer *ringbuffer.RingBuffer + + done chan struct{} +} + +func (sw *serverWriter) start() { + if !sw.running { + sw.running = true + sw.done = make(chan struct{}) + go sw.run() + } +} + +func (sw *serverWriter) stop() { + if sw.running { + sw.buffer.Close() + <-sw.done + sw.running = false + } +} + +func (sw *serverWriter) run() { + defer close(sw.done) + + for { + tmp, ok := sw.buffer.Pull() + if !ok { + return + } + + tmp.(func())() + } +} + +func (sw *serverWriter) queue(cb func()) { + sw.buffer.Push(cb) +} diff --git a/track.go b/track.go deleted file mode 100644 index 3a2830dc..00000000 --- a/track.go +++ /dev/null @@ -1,202 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/url" -) - -// Track is a RTSP track. -type Track interface { - // String returns the track codec. - String() string - - // ClockRate returns the track clock rate. - ClockRate() int - - // GetControl returns the track control attribute. - GetControl() string - - // SetControl sets the track control attribute. - SetControl(string) - - // MediaDescription returns the track media description in SDP format. - MediaDescription() *psdp.MediaDescription - - clone() Track - url(*url.URL) (*url.URL, error) -} - -func getControlAttribute(attributes []psdp.Attribute) string { - for _, attr := range attributes { - if attr.Key == "control" { - return attr.Value - } - } - return "" -} - -func getRtpmapAttribute(attributes []psdp.Attribute, payloadType uint8) string { - for _, attr := range attributes { - if attr.Key == "rtpmap" { - v := strings.TrimSpace(attr.Value) - if parts := strings.SplitN(v, " ", 2); len(parts) == 2 { - if tmp, err := strconv.ParseInt(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType { - return parts[1] - } - } - } - } - return "" -} - -func getFmtpAttribute(attributes []psdp.Attribute, payloadType uint8) string { - for _, attr := range attributes { - if attr.Key == "fmtp" { - if parts := strings.SplitN(attr.Value, " ", 2); len(parts) == 2 { - if tmp, err := strconv.ParseInt(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType { - return parts[1] - } - } - } - } - return "" -} - -func getCodecAndClock(attributes []psdp.Attribute, payloadType uint8) (string, string) { - rtpmap := getRtpmapAttribute(attributes, payloadType) - if rtpmap == "" { - return "", "" - } - - parts2 := strings.SplitN(rtpmap, "/", 2) - if len(parts2) != 2 { - return "", "" - } - - return parts2[0], parts2[1] -} - -func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) { - if len(md.MediaName.Formats) == 0 { - return nil, fmt.Errorf("no media formats found") - } - - control := getControlAttribute(md.Attributes) - - if len(md.MediaName.Formats) == 1 { - tmp, err := strconv.ParseInt(md.MediaName.Formats[0], 10, 8) - if err != nil { - return nil, err - } - payloadType := uint8(tmp) - - codec, clock := getCodecAndClock(md.Attributes, payloadType) - codec = strings.ToLower(codec) - - switch { - case md.MediaName.Media == "video": - switch { - case payloadType == 26: - return newTrackJPEGFromMediaDescription(control) - - case payloadType == 32: - return newTrackMPEG2VideoFromMediaDescription(control) - - case codec == "h264" && clock == "90000": - return newTrackH264FromMediaDescription(control, payloadType, md) - - case codec == "h265" && clock == "90000": - return newTrackH265FromMediaDescription(control, payloadType, md) - - case codec == "vp8" && clock == "90000": - return newTrackVP8FromMediaDescription(control, payloadType, md) - - case codec == "vp9" && clock == "90000": - return newTrackVP9FromMediaDescription(control, payloadType, md) - } - - case md.MediaName.Media == "audio": - switch { - case payloadType == 0, payloadType == 8: - return newTrackG711FromMediaDescription(control, payloadType, clock) - - case payloadType == 9: - return newTrackG722FromMediaDescription(control, clock) - - case payloadType == 14: - return newTrackMPEG2AudioFromMediaDescription(control) - - case codec == "l8", codec == "l16", codec == "l24": - return newTrackLPCMFromMediaDescription(control, payloadType, codec, clock) - - case codec == "mpeg4-generic": - return newTrackMPEG4AudioFromMediaDescription(control, payloadType, md) - - case codec == "vorbis": - return newTrackVorbisFromMediaDescription(control, payloadType, clock, md) - - case codec == "opus": - return newTrackOpusFromMediaDescription(control, payloadType, clock) - } - } - } - - return newTrackGenericFromMediaDescription(control, md) -} - -type trackBase struct { - control string -} - -// GetControl gets the track control attribute. -func (t *trackBase) GetControl() string { - return t.control -} - -// SetControl sets the track control attribute. -func (t *trackBase) SetControl(c string) { - t.control = c -} - -func (t *trackBase) url(contentBase *url.URL) (*url.URL, error) { - if contentBase == nil { - return nil, fmt.Errorf("Content-Base header not provided") - } - - control := t.GetControl() - - // no control attribute, use base URL - if control == "" { - return contentBase, nil - } - - // control attribute contains an absolute path - if strings.HasPrefix(control, "rtsp://") { - ur, err := url.Parse(control) - if err != nil { - return nil, err - } - - // copy host and credentials - ur.Host = contentBase.Host - ur.User = contentBase.User - return ur, nil - } - - // control attribute contains a relative control attribute - // insert the control attribute at the end of the URL - // if there's a query, insert it after the query - // otherwise insert it after the path - strURL := contentBase.String() - if control[0] != '?' && !strings.HasSuffix(strURL, "/") { - strURL += "/" - } - - ur, _ := url.Parse(strURL + control) - return ur, nil -} diff --git a/track_g711.go b/track_g711.go deleted file mode 100644 index 0b104baa..00000000 --- a/track_g711.go +++ /dev/null @@ -1,111 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtpsimpleaudio" -) - -// TrackG711 is a PCMA track. -type TrackG711 struct { - // whether to use mu-law. Otherwise, A-law is used. - MULaw bool - - trackBase -} - -func newTrackG711FromMediaDescription( - control string, - payloadType uint8, - clock string, -) (*TrackG711, error, -) { - tmp := strings.Split(clock, "/") - if len(tmp) == 2 && tmp[1] != "1" { - return nil, fmt.Errorf("G711 tracks can have only one channel") - } - - return &TrackG711{ - MULaw: (payloadType == 0), - trackBase: trackBase{ - control: control, - }, - }, nil -} - -// String returns the track codec. -func (t *TrackG711) String() string { - return "G711" -} - -// ClockRate returns the track clock rate. -func (t *TrackG711) ClockRate() int { - return 8000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackG711) MediaDescription() *psdp.MediaDescription { - var formats []string - var rtpmap string - if t.MULaw { - formats = []string{"0"} - rtpmap = "0 PCMU/8000" - } else { - formats = []string{"8"} - rtpmap = "8 PCMA/8000" - } - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: formats, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: rtpmap, - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackG711) clone() Track { - return &TrackG711{ - MULaw: t.MULaw, - trackBase: t.trackBase, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackG711) CreateDecoder() *rtpsimpleaudio.Decoder { - d := &rtpsimpleaudio.Decoder{ - SampleRate: 8000, - } - d.Init() - return d -} - -// CreateEncoder creates an encoder able to encode the content of the track. -func (t *TrackG711) CreateEncoder() *rtpsimpleaudio.Encoder { - var payloadType uint8 - if t.MULaw { - payloadType = 0 - } else { - payloadType = 8 - } - - e := &rtpsimpleaudio.Encoder{ - PayloadType: payloadType, - SampleRate: 8000, - } - e.Init() - return e -} diff --git a/track_g711_test.go b/track_g711_test.go deleted file mode 100644 index 0402b08d..00000000 --- a/track_g711_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackG711Attributes(t *testing.T) { - track := &TrackG711{} - require.Equal(t, "G711", track.String()) - require.Equal(t, 8000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackG711Clone(t *testing.T) { - track := &TrackG711{} - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackG711MediaDescription(t *testing.T) { - t.Run("pcma", func(t *testing.T) { - track := &TrackG711{} - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"8"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "8 PCMA/8000", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) - }) - - t.Run("pcmu", func(t *testing.T) { - track := &TrackG711{ - MULaw: true, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"0"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "0 PCMU/8000", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) - }) -} diff --git a/track_g722.go b/track_g722.go deleted file mode 100644 index f4cb61dc..00000000 --- a/track_g722.go +++ /dev/null @@ -1,88 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - "fmt" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtpsimpleaudio" -) - -// TrackG722 is a G722 track. -type TrackG722 struct { - trackBase -} - -func newTrackG722FromMediaDescription( - control string, - clock string, -) (*TrackG722, error, -) { - tmp := strings.Split(clock, "/") - if len(tmp) == 2 && tmp[1] != "1" { - return nil, fmt.Errorf("G722 tracks can have only one channel") - } - - return &TrackG722{ - trackBase: trackBase{ - control: control, - }, - }, nil -} - -// String returns the track codec. -func (t *TrackG722) String() string { - return "G722" -} - -// ClockRate returns the track clock rate. -func (t *TrackG722) ClockRate() int { - return 8000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackG722) MediaDescription() *psdp.MediaDescription { - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"9"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "9 G722/8000", - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackG722) clone() Track { - return &TrackG722{ - trackBase: t.trackBase, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackG722) CreateDecoder() *rtpsimpleaudio.Decoder { - d := &rtpsimpleaudio.Decoder{ - SampleRate: 8000, - } - d.Init() - return d -} - -// CreateEncoder creates an encoder able to encode the content of the track. -func (t *TrackG722) CreateEncoder() *rtpsimpleaudio.Encoder { - e := &rtpsimpleaudio.Encoder{ - PayloadType: 9, - SampleRate: 8000, - } - e.Init() - return e -} diff --git a/track_g722_test.go b/track_g722_test.go deleted file mode 100644 index cd2bcfd0..00000000 --- a/track_g722_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackG722Attributes(t *testing.T) { - track := &TrackG722{} - require.Equal(t, "G722", track.String()) - require.Equal(t, 8000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackG722Clone(t *testing.T) { - track := &TrackG722{} - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackG722MediaDescription(t *testing.T) { - track := &TrackG722{} - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"9"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "9 G722/8000", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_generic.go b/track_generic.go deleted file mode 100644 index d77d41d5..00000000 --- a/track_generic.go +++ /dev/null @@ -1,173 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" -) - -func findClockRate(track *TrackGeneric) (int, error) { - // RFC 4566 - // When a list of - // payload type numbers is given, this implies that all of these - // payload formats MAY be used in the session, but the first of these - // formats SHOULD be used as the default format for the session - payload := track.Payloads[0] - - // get clock rate from payload type - // https://en.wikipedia.org/wiki/RTP_payload_formats - switch payload.Type { - case 0, 1, 2, 3, 4, 5, 7, 8, 9, 12, 13, 15, 18: - return 8000, nil - - case 6: - return 16000, nil - - case 10, 11: - return 44100, nil - - case 14, 25, 26, 28, 31, 32, 33, 34: - return 90000, nil - - case 16: - return 11025, nil - - case 17: - return 22050, nil - } - - // get clock rate from rtpmap - // https://tools.ietf.org/html/rfc4566 - // a=rtpmap: / [/] - if payload.RTPMap == "" { - return 0, fmt.Errorf("attribute 'rtpmap' not found") - } - - tmp := strings.Split(payload.RTPMap, "/") - if len(tmp) != 2 && len(tmp) != 3 { - return 0, fmt.Errorf("invalid rtpmap (%v)", payload.RTPMap) - } - - v, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return 0, err - } - - return int(v), nil -} - -// TrackGenericPayload is a payload of a TrackGeneric. -type TrackGenericPayload struct { - Type uint8 - RTPMap string - FMTP string -} - -// TrackGeneric is a generic track. -type TrackGeneric struct { - Media string - Payloads []TrackGenericPayload - - trackBase - - clockRate int -} - -func newTrackGenericFromMediaDescription( - control string, - md *psdp.MediaDescription, -) (*TrackGeneric, error) { - t := &TrackGeneric{ - Media: md.MediaName.Media, - trackBase: trackBase{ - control: control, - }, - } - - for _, format := range md.MediaName.Formats { - tmp, err := strconv.ParseInt(format, 10, 8) - if err != nil { - return nil, err - } - payloadType := uint8(tmp) - - t.Payloads = append(t.Payloads, TrackGenericPayload{ - Type: payloadType, - RTPMap: getRtpmapAttribute(md.Attributes, payloadType), - FMTP: getFmtpAttribute(md.Attributes, payloadType), - }) - } - - err := t.Init() - if err != nil { - return nil, err - } - - return t, nil -} - -// Init initializes a TrackGeneric -func (t *TrackGeneric) Init() error { - t.clockRate, _ = findClockRate(t) - return nil -} - -// String returns the track codec. -func (t *TrackGeneric) String() string { - return "Generic" -} - -// ClockRate returns the track clock rate. -func (t *TrackGeneric) ClockRate() int { - return t.clockRate -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackGeneric) MediaDescription() *psdp.MediaDescription { - formats := make([]string, len(t.Payloads)) - for i, pl := range t.Payloads { - formats[i] = strconv.FormatInt(int64(pl.Type), 10) - } - - var attributes []psdp.Attribute - - for _, pl := range t.Payloads { - if pl.RTPMap != "" { - attributes = append(attributes, psdp.Attribute{ - Key: "rtpmap", - Value: strconv.FormatInt(int64(pl.Type), 10) + " " + pl.RTPMap, - }) - } - if pl.FMTP != "" { - attributes = append(attributes, psdp.Attribute{ - Key: "fmtp", - Value: strconv.FormatInt(int64(pl.Type), 10) + " " + pl.FMTP, - }) - } - } - - attributes = append(attributes, psdp.Attribute{ - Key: "control", - Value: t.control, - }) - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: t.Media, - Protos: []string{"RTP", "AVP"}, - Formats: formats, - }, - Attributes: attributes, - } -} - -func (t *TrackGeneric) clone() Track { - return &TrackGeneric{ - Media: t.Media, - Payloads: append([]TrackGenericPayload(nil), t.Payloads...), - trackBase: t.trackBase, - clockRate: t.clockRate, - } -} diff --git a/track_generic_test.go b/track_generic_test.go deleted file mode 100644 index 05815484..00000000 --- a/track_generic_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackGenericAttributes(t *testing.T) { - track := &TrackGeneric{ - Media: "video", - Payloads: []TrackGenericPayload{ - { - Type: 98, - RTPMap: "H265/90000", - FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", - }, - { - Type: 100, - }, - }, - } - err := track.Init() - require.NoError(t, err) - - require.Equal(t, "Generic", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackGenericClone(t *testing.T) { - track := &TrackGeneric{ - Media: "video", - Payloads: []TrackGenericPayload{ - { - Type: 98, - RTPMap: "H265/90000", - FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", - }, - { - Type: 100, - }, - }, - } - err := track.Init() - require.NoError(t, err) - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackGenericMediaDescription(t *testing.T) { - track := &TrackGeneric{ - Media: "video", - Payloads: []TrackGenericPayload{ - { - Type: 98, - RTPMap: "H265/90000", - FMTP: "profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", - }, - { - Type: 100, - }, - }, - } - err := track.Init() - require.NoError(t, err) - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"98", "100"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "98 H265/90000", - }, - { - Key: "fmtp", - Value: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_h264.go b/track_h264.go deleted file mode 100644 index c3d3bf8d..00000000 --- a/track_h264.go +++ /dev/null @@ -1,218 +0,0 @@ -package gortsplib - -import ( - "encoding/base64" - "encoding/hex" - "fmt" - "strconv" - "strings" - "sync" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtph264" -) - -// TrackH264 is a H264 track. -type TrackH264 struct { - PayloadType uint8 - SPS []byte - PPS []byte - PacketizationMode int - - trackBase - mutex sync.RWMutex -} - -func newTrackH264FromMediaDescription( - control string, - payloadType uint8, - md *psdp.MediaDescription, -) (*TrackH264, error) { - t := &TrackH264{ - PayloadType: payloadType, - trackBase: trackBase{ - control: control, - }, - } - - t.fillParamsFromMediaDescription(md) - - return t, nil -} - -func (t *TrackH264) fillParamsFromMediaDescription(md *psdp.MediaDescription) error { - v, ok := md.Attribute("fmtp") - if !ok { - return fmt.Errorf("fmtp attribute is missing") - } - - tmp := strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - switch tmp[0] { - case "sprop-parameter-sets": - tmp := strings.Split(tmp[1], ",") - if len(tmp) < 2 { - return fmt.Errorf("invalid sprop-parameter-sets (%v)", v) - } - - sps, err := base64.StdEncoding.DecodeString(tmp[0]) - if err != nil { - return fmt.Errorf("invalid sprop-parameter-sets (%v)", v) - } - - pps, err := base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid sprop-parameter-sets (%v)", v) - } - - t.SPS = sps - t.PPS = pps - - case "packetization-mode": - tmp, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid packetization-mode (%v)", v) - } - - t.PacketizationMode = int(tmp) - } - } - - return fmt.Errorf("sprop-parameter-sets is missing (%v)", v) -} - -// String returns the track codec. -func (t *TrackH264) String() string { - return "H264" -} - -// ClockRate returns the track clock rate. -func (t *TrackH264) ClockRate() int { - return 90000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackH264) MediaDescription() *psdp.MediaDescription { - t.mutex.RLock() - defer t.mutex.RUnlock() - - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - fmtp := typ - - var tmp []string - if t.PacketizationMode != 0 { - tmp = append(tmp, "packetization-mode="+strconv.FormatInt(int64(t.PacketizationMode), 10)) - } - var tmp2 []string - if t.SPS != nil { - tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.SPS)) - } - if t.PPS != nil { - tmp2 = append(tmp2, base64.StdEncoding.EncodeToString(t.PPS)) - } - if tmp2 != nil { - tmp = append(tmp, "sprop-parameter-sets="+strings.Join(tmp2, ",")) - } - if len(t.SPS) >= 4 { - tmp = append(tmp, "profile-level-id="+strings.ToUpper(hex.EncodeToString(t.SPS[1:4]))) - } - if tmp != nil { - fmtp += " " + strings.Join(tmp, "; ") - } - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " H264/90000", - }, - { - Key: "fmtp", - Value: fmtp, - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackH264) clone() Track { - return &TrackH264{ - PayloadType: t.PayloadType, - SPS: t.SPS, - PPS: t.PPS, - PacketizationMode: t.PacketizationMode, - trackBase: t.trackBase, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackH264) CreateDecoder() *rtph264.Decoder { - d := &rtph264.Decoder{ - PacketizationMode: t.PacketizationMode, - } - d.Init() - return d -} - -// CreateEncoder creates an encoder able to encode the content of the track. -func (t *TrackH264) CreateEncoder() *rtph264.Encoder { - e := &rtph264.Encoder{ - PayloadType: t.PayloadType, - PacketizationMode: t.PacketizationMode, - } - e.Init() - return e -} - -// SafeSPS returns the track SPS. -func (t *TrackH264) SafeSPS() []byte { - t.mutex.RLock() - defer t.mutex.RUnlock() - return t.SPS -} - -// SafePPS returns the track PPS. -func (t *TrackH264) SafePPS() []byte { - t.mutex.RLock() - defer t.mutex.RUnlock() - return t.PPS -} - -// SafeSetSPS sets the track SPS. -func (t *TrackH264) SafeSetSPS(v []byte) { - t.mutex.Lock() - defer t.mutex.Unlock() - t.SPS = v -} - -// SafeSetPPS sets the track PPS. -func (t *TrackH264) SafeSetPPS(v []byte) { - t.mutex.Lock() - defer t.mutex.Unlock() - t.PPS = v -} diff --git a/track_h264_test.go b/track_h264_test.go deleted file mode 100644 index 595480f8..00000000 --- a/track_h264_test.go +++ /dev/null @@ -1,267 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackH264Attributes(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02}, - PPS: []byte{0x03, 0x04}, - PacketizationMode: 1, - } - require.Equal(t, "H264", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) - require.Equal(t, []byte{0x01, 0x02}, track.SafeSPS()) - require.Equal(t, []byte{0x03, 0x04}, track.SafePPS()) - - track.SafeSetSPS([]byte{0x07, 0x08}) - track.SafeSetPPS([]byte{0x09, 0x0A}) - require.Equal(t, []byte{0x07, 0x08}, track.SafeSPS()) - require.Equal(t, []byte{0x09, 0x0A}, track.SafePPS()) -} - -func TestTrackH264GetSPSPPSErrors(t *testing.T) { - for _, ca := range []struct { - name string - md *psdp.MediaDescription - err string - }{ - { - "missing fmtp", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - }, - }, - "fmtp attribute is missing", - }, - { - "invalid fmtp", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96", - }, - }, - }, - "invalid fmtp attribute (96)", - }, - { - "fmtp without key", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode", - }, - }, - }, - "invalid fmtp attribute (96 packetization-mode)", - }, - { - "missing sprop-parameter-set", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1", - }, - }, - }, - "sprop-parameter-sets is missing (96 packetization-mode=1)", - }, - { - "invalid sprop-parameter-set 1", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=aaaaaa", - }, - }, - }, - "invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa)", - }, - { - "invalid sprop-parameter-set 2", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=aaaaaa,bbb", - }, - }, - }, - "invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa,bbb)", - }, - { - "invalid sprop-parameter-set 3", - &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb", - }, - }, - }, - "invalid sprop-parameter-sets (96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb)", - }, - } { - t.Run(ca.name, func(t *testing.T) { - var tr TrackH264 - err := tr.fillParamsFromMediaDescription(ca.md) - require.EqualError(t, err, ca.err) - }) - } -} - -func TestTrackH264Clone(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{0x01, 0x02}, - PPS: []byte{0x03, 0x04}, - PacketizationMode: 1, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackH264MediaDescription(t *testing.T) { - t.Run("standard", func(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - SPS: []byte{ - 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, - 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, - 0x00, 0x03, 0x00, 0x3d, 0x08, - }, - PPS: []byte{ - 0x68, 0xee, 0x3c, 0x80, - }, - PacketizationMode: 1, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1; " + - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) - }) - - t.Run("no sps/pps", func(t *testing.T) { - track := &TrackH264{ - PayloadType: 96, - PacketizationMode: 1, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) - }) -} diff --git a/track_h265.go b/track_h265.go deleted file mode 100644 index 8afbdd48..00000000 --- a/track_h265.go +++ /dev/null @@ -1,230 +0,0 @@ -package gortsplib - -import ( - "encoding/base64" - "fmt" - "strconv" - "strings" - "sync" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtph265" -) - -// TrackH265 is a H265 track. -type TrackH265 struct { - PayloadType uint8 - VPS []byte - SPS []byte - PPS []byte - MaxDONDiff int - - trackBase - mutex sync.RWMutex -} - -func newTrackH265FromMediaDescription( - control string, - payloadType uint8, - md *psdp.MediaDescription, -) (*TrackH265, error) { - t := &TrackH265{ - PayloadType: payloadType, - trackBase: trackBase{ - control: control, - }, - } - - t.fillParamsFromMediaDescription(md) - - return t, nil -} - -func (t *TrackH265) fillParamsFromMediaDescription(md *psdp.MediaDescription) error { - v, ok := md.Attribute("fmtp") - if !ok { - return fmt.Errorf("fmtp attribute is missing") - } - - tmp := strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - switch tmp[0] { - case "sprop-vps": - var err error - t.VPS, err = base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid sprop-vps (%v)", v) - } - - case "sprop-sps": - var err error - t.SPS, err = base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid sprop-sps (%v)", v) - } - - case "sprop-pps": - var err error - t.PPS, err = base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return fmt.Errorf("invalid sprop-pps (%v)", v) - } - - case "sprop-max-don-diff": - tmp, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid sprop-max-don-diff (%v)", v) - } - t.MaxDONDiff = int(tmp) - } - } - - return nil -} - -// String returns the track codec. -func (t *TrackH265) String() string { - return "H265" -} - -// ClockRate returns the track clock rate. -func (t *TrackH265) ClockRate() int { - return 90000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackH265) MediaDescription() *psdp.MediaDescription { - t.mutex.RLock() - defer t.mutex.RUnlock() - - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - fmtp := typ - - var tmp []string - if t.VPS != nil { - tmp = append(tmp, "sprop-vps="+base64.StdEncoding.EncodeToString(t.VPS)) - } - if t.SPS != nil { - tmp = append(tmp, "sprop-sps="+base64.StdEncoding.EncodeToString(t.SPS)) - } - if t.PPS != nil { - tmp = append(tmp, "sprop-pps="+base64.StdEncoding.EncodeToString(t.PPS)) - } - if t.MaxDONDiff != 0 { - tmp = append(tmp, "sprop-max-don-diff="+strconv.FormatInt(int64(t.MaxDONDiff), 10)) - } - if tmp != nil { - fmtp += " " + strings.Join(tmp, "; ") - } - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " H265/90000", - }, - { - Key: "fmtp", - Value: fmtp, - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackH265) clone() Track { - return &TrackH265{ - PayloadType: t.PayloadType, - VPS: t.VPS, - SPS: t.SPS, - PPS: t.PPS, - MaxDONDiff: t.MaxDONDiff, - trackBase: t.trackBase, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackH265) CreateDecoder() *rtph265.Decoder { - d := &rtph265.Decoder{ - MaxDONDiff: t.MaxDONDiff, - } - d.Init() - return d -} - -// CreateEncoder creates an encoder able to encode the content of the track. -func (t *TrackH265) CreateEncoder() *rtph265.Encoder { - e := &rtph265.Encoder{ - PayloadType: t.PayloadType, - MaxDONDiff: t.MaxDONDiff, - } - e.Init() - return e -} - -// SafeVPS returns the track VPS. -func (t *TrackH265) SafeVPS() []byte { - t.mutex.RLock() - defer t.mutex.RUnlock() - return t.VPS -} - -// SafeSPS returns the track SPS. -func (t *TrackH265) SafeSPS() []byte { - t.mutex.RLock() - defer t.mutex.RUnlock() - return t.SPS -} - -// SafePPS returns the track PPS. -func (t *TrackH265) SafePPS() []byte { - t.mutex.RLock() - defer t.mutex.RUnlock() - return t.PPS -} - -// SafeSetVPS sets the track VPS. -func (t *TrackH265) SafeSetVPS(v []byte) { - t.mutex.Lock() - defer t.mutex.Unlock() - t.VPS = v -} - -// SafeSetSPS sets the track SPS. -func (t *TrackH265) SafeSetSPS(v []byte) { - t.mutex.Lock() - defer t.mutex.Unlock() - t.SPS = v -} - -// SafeSetPPS sets the track PPS. -func (t *TrackH265) SafeSetPPS(v []byte) { - t.mutex.Lock() - defer t.mutex.Unlock() - t.PPS = v -} diff --git a/track_h265_test.go b/track_h265_test.go deleted file mode 100644 index fda021a2..00000000 --- a/track_h265_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackH265Attributes(t *testing.T) { - track := &TrackH265{ - PayloadType: 96, - VPS: []byte{0x01, 0x02}, - SPS: []byte{0x03, 0x04}, - PPS: []byte{0x05, 0x06}, - } - require.Equal(t, "H265", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) - require.Equal(t, []byte{0x01, 0x02}, track.SafeVPS()) - require.Equal(t, []byte{0x03, 0x04}, track.SafeSPS()) - require.Equal(t, []byte{0x05, 0x06}, track.SafePPS()) - - track.SafeSetVPS([]byte{0x07, 0x08}) - track.SafeSetSPS([]byte{0x09, 0x0A}) - track.SafeSetPPS([]byte{0x0B, 0x0C}) - require.Equal(t, []byte{0x07, 0x08}, track.SafeVPS()) - require.Equal(t, []byte{0x09, 0x0A}, track.SafeSPS()) - require.Equal(t, []byte{0x0B, 0x0C}, track.SafePPS()) -} - -func TestTrackH265Clone(t *testing.T) { - track := &TrackH265{ - PayloadType: 96, - VPS: []byte{0x01, 0x02}, - SPS: []byte{0x03, 0x04}, - PPS: []byte{0x05, 0x06}, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackH265MediaDescription(t *testing.T) { - track := &TrackH265{ - PayloadType: 96, - VPS: []byte{0x01, 0x02}, - SPS: []byte{0x03, 0x04}, - PPS: []byte{0x05, 0x06}, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H265/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-vps=AQI=; sprop-sps=AwQ=; sprop-pps=BQY=", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_jpeg.go b/track_jpeg.go deleted file mode 100644 index f9b72bf9..00000000 --- a/track_jpeg.go +++ /dev/null @@ -1,58 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - psdp "github.com/pion/sdp/v3" -) - -// TrackJPEG is a JPEG track. -type TrackJPEG struct { - trackBase -} - -func newTrackJPEGFromMediaDescription( - control string, -) (*TrackJPEG, error, -) { - return &TrackJPEG{ - trackBase: trackBase{ - control: control, - }, - }, nil -} - -// String returns the track codec. -func (t *TrackJPEG) String() string { - return "JPEG" -} - -// ClockRate returns the track clock rate. -func (t *TrackJPEG) ClockRate() int { - return 90000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackJPEG) MediaDescription() *psdp.MediaDescription { - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"26"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "26 JPEG/90000", - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackJPEG) clone() Track { - return &TrackJPEG{ - trackBase: t.trackBase, - } -} diff --git a/track_jpeg_test.go b/track_jpeg_test.go deleted file mode 100644 index ba9257d1..00000000 --- a/track_jpeg_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackJPEGAttributes(t *testing.T) { - track := &TrackJPEG{} - require.Equal(t, "JPEG", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackJPEGClone(t *testing.T) { - track := &TrackJPEG{} - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackJPEGMediaDescription(t *testing.T) { - track := &TrackJPEG{} - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"26"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "26 JPEG/90000", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_lpcm.go b/track_lpcm.go deleted file mode 100644 index f75732f8..00000000 --- a/track_lpcm.go +++ /dev/null @@ -1,145 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtplpcm" -) - -// TrackLPCM is an uncompressed, Linear PCM track. -type TrackLPCM struct { - PayloadType uint8 - BitDepth int - SampleRate int - ChannelCount int - - trackBase -} - -func newTrackLPCMFromMediaDescription( - control string, - payloadType uint8, - codec string, - clock string, -) (*TrackLPCM, error, -) { - var bitDepth int - switch codec { - case "l8": - bitDepth = 8 - - case "l16": - bitDepth = 16 - - case "l24": - bitDepth = 24 - } - - tmp := strings.SplitN(clock, "/", 32) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid clock (%v)", clock) - } - - sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) - if err != nil { - return nil, err - } - - channelCount, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return nil, err - } - - return &TrackLPCM{ - PayloadType: payloadType, - BitDepth: bitDepth, - SampleRate: int(sampleRate), - ChannelCount: int(channelCount), - trackBase: trackBase{ - control: control, - }, - }, nil -} - -// String returns the track codec. -func (t *TrackLPCM) String() string { - return "LPCM" -} - -// ClockRate returns the track clock rate. -func (t *TrackLPCM) ClockRate() int { - return t.SampleRate -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackLPCM) MediaDescription() *psdp.MediaDescription { - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - var codec string - switch t.BitDepth { - case 8: - codec = "L8" - - case 16: - codec = "L16" - - case 24: - codec = "L24" - } - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " " + codec + "/" + strconv.FormatInt(int64(t.SampleRate), 10) + - "/" + strconv.FormatInt(int64(t.ChannelCount), 10), - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackLPCM) clone() Track { - return &TrackLPCM{ - PayloadType: t.PayloadType, - BitDepth: t.BitDepth, - SampleRate: t.SampleRate, - ChannelCount: t.ChannelCount, - trackBase: t.trackBase, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackLPCM) CreateDecoder() *rtplpcm.Decoder { - d := &rtplpcm.Decoder{ - BitDepth: t.BitDepth, - SampleRate: t.SampleRate, - ChannelCount: t.ChannelCount, - } - d.Init() - return d -} - -// CreateEncoder creates an encoder able to encode the content of the track. -func (t *TrackLPCM) CreateEncoder() *rtplpcm.Encoder { - e := &rtplpcm.Encoder{ - PayloadType: t.PayloadType, - BitDepth: t.BitDepth, - SampleRate: t.SampleRate, - ChannelCount: t.ChannelCount, - } - e.Init() - return e -} diff --git a/track_lpcm_test.go b/track_lpcm_test.go deleted file mode 100644 index 097d4928..00000000 --- a/track_lpcm_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackLPCMAttributes(t *testing.T) { - track := &TrackLPCM{ - PayloadType: 96, - BitDepth: 24, - SampleRate: 44100, - ChannelCount: 2, - } - require.Equal(t, "LPCM", track.String()) - require.Equal(t, 44100, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTracLPCMClone(t *testing.T) { - track := &TrackLPCM{ - PayloadType: 96, - BitDepth: 16, - SampleRate: 48000, - ChannelCount: 2, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackLPCMMediaDescription(t *testing.T) { - track := &TrackLPCM{ - PayloadType: 96, - BitDepth: 24, - SampleRate: 96000, - ChannelCount: 2, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 L24/96000/2", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_mpeg2audio.go b/track_mpeg2audio.go deleted file mode 100644 index 62394297..00000000 --- a/track_mpeg2audio.go +++ /dev/null @@ -1,54 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - psdp "github.com/pion/sdp/v3" -) - -// TrackMPEG2Audio is a MPEG-1 or MPEG-2 audio track. -type TrackMPEG2Audio struct { - trackBase -} - -func newTrackMPEG2AudioFromMediaDescription( - control string, -) (*TrackMPEG2Audio, error, -) { - return &TrackMPEG2Audio{ - trackBase: trackBase{ - control: control, - }, - }, nil -} - -// String returns the track codec. -func (t *TrackMPEG2Audio) String() string { - return "MPEG2-audio" -} - -// ClockRate returns the track clock rate. -func (t *TrackMPEG2Audio) ClockRate() int { - return 90000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackMPEG2Audio) MediaDescription() *psdp.MediaDescription { - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"14"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackMPEG2Audio) clone() Track { - return &TrackMPEG2Audio{ - trackBase: t.trackBase, - } -} diff --git a/track_mpeg2audio_test.go b/track_mpeg2audio_test.go deleted file mode 100644 index 29bf35bb..00000000 --- a/track_mpeg2audio_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackMPEG2AudioAttributes(t *testing.T) { - track := &TrackMPEG2Audio{} - require.Equal(t, "MPEG2-audio", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackMPEG2AudioClone(t *testing.T) { - track := &TrackMPEG2Audio{} - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackMPEG2AudioMediaDescription(t *testing.T) { - track := &TrackMPEG2Audio{} - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"14"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_mpeg2video.go b/track_mpeg2video.go deleted file mode 100644 index e627e7bd..00000000 --- a/track_mpeg2video.go +++ /dev/null @@ -1,54 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - psdp "github.com/pion/sdp/v3" -) - -// TrackMPEG2Video is a MPEG-1 or MPEG-2 video track. -type TrackMPEG2Video struct { - trackBase -} - -func newTrackMPEG2VideoFromMediaDescription( - control string, -) (*TrackMPEG2Video, error, -) { - return &TrackMPEG2Video{ - trackBase: trackBase{ - control: control, - }, - }, nil -} - -// String returns the track codec. -func (t *TrackMPEG2Video) String() string { - return "MPEG2-video" -} - -// ClockRate returns the track clock rate. -func (t *TrackMPEG2Video) ClockRate() int { - return 90000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackMPEG2Video) MediaDescription() *psdp.MediaDescription { - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"32"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackMPEG2Video) clone() Track { - return &TrackMPEG2Video{ - trackBase: t.trackBase, - } -} diff --git a/track_mpeg2video_test.go b/track_mpeg2video_test.go deleted file mode 100644 index 39acf896..00000000 --- a/track_mpeg2video_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package gortsplib //nolint:dupl - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackMPEG2VideoAttributes(t *testing.T) { - track := &TrackMPEG2Video{} - require.Equal(t, "MPEG2-video", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackMPEG2VideoClone(t *testing.T) { - track := &TrackMPEG2Video{} - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackMPEG2VideoMediaDescription(t *testing.T) { - track := &TrackMPEG2Video{} - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"32"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_mpeg4audio.go b/track_mpeg4audio.go deleted file mode 100644 index e4a45e54..00000000 --- a/track_mpeg4audio.go +++ /dev/null @@ -1,209 +0,0 @@ -package gortsplib - -import ( - "encoding/hex" - "fmt" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/mpeg4audio" - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtpmpeg4audio" -) - -// TrackMPEG4Audio is a MPEG-4 audio track. -type TrackMPEG4Audio struct { - PayloadType uint8 - Config *mpeg4audio.Config - SizeLength int - IndexLength int - IndexDeltaLength int - - trackBase -} - -func newTrackMPEG4AudioFromMediaDescription( - control string, - payloadType uint8, - md *psdp.MediaDescription, -) (*TrackMPEG4Audio, error) { - t := &TrackMPEG4Audio{ - PayloadType: payloadType, - trackBase: trackBase{ - control: control, - }, - } - - v, ok := md.Attribute("fmtp") - if !ok { - return nil, fmt.Errorf("fmtp attribute is missing") - } - - tmp := strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp (%v)", v) - } - - switch strings.ToLower(tmp[0]) { - case "config": - enc, err := hex.DecodeString(tmp[1]) - if err != nil { - return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - t.Config = &mpeg4audio.Config{} - err = t.Config.Unmarshal(enc) - if err != nil { - return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - case "sizelength": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid AAC SizeLength (%v)", tmp[1]) - } - t.SizeLength = int(val) - - case "indexlength": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid AAC IndexLength (%v)", tmp[1]) - } - t.IndexLength = int(val) - - case "indexdeltalength": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid AAC IndexDeltaLength (%v)", tmp[1]) - } - t.IndexDeltaLength = int(val) - } - } - - if t.Config == nil { - return nil, fmt.Errorf("config is missing (%v)", v) - } - - if t.SizeLength == 0 { - return nil, fmt.Errorf("sizelength is missing (%v)", v) - } - - return t, nil -} - -// String returns the track codec. -func (t *TrackMPEG4Audio) String() string { - return "MPEG4-audio" -} - -// ClockRate returns the track clock rate. -func (t *TrackMPEG4Audio) ClockRate() int { - return t.Config.SampleRate -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackMPEG4Audio) MediaDescription() *psdp.MediaDescription { - enc, err := t.Config.Marshal() - if err != nil { - return nil - } - - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - sampleRate := t.Config.SampleRate - if t.Config.ExtensionSampleRate != 0 { - sampleRate = t.Config.ExtensionSampleRate - } - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + - "/" + strconv.FormatInt(int64(t.Config.ChannelCount), 10), - }, - { - Key: "fmtp", - Value: typ + " profile-level-id=1; " + - "mode=AAC-hbr; " + - func() string { - if t.SizeLength > 0 { - return fmt.Sprintf("sizelength=%d; ", t.SizeLength) - } - return "" - }() + - func() string { - if t.IndexLength > 0 { - return fmt.Sprintf("indexlength=%d; ", t.IndexLength) - } - return "" - }() + - func() string { - if t.IndexDeltaLength > 0 { - return fmt.Sprintf("indexdeltalength=%d; ", t.IndexDeltaLength) - } - return "" - }() + - "config=" + hex.EncodeToString(enc), - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackMPEG4Audio) clone() Track { - return &TrackMPEG4Audio{ - PayloadType: t.PayloadType, - Config: t.Config, - SizeLength: t.SizeLength, - IndexLength: t.IndexLength, - IndexDeltaLength: t.IndexDeltaLength, - trackBase: t.trackBase, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackMPEG4Audio) CreateDecoder() *rtpmpeg4audio.Decoder { - d := &rtpmpeg4audio.Decoder{ - SampleRate: t.Config.SampleRate, - SizeLength: t.SizeLength, - IndexLength: t.IndexLength, - IndexDeltaLength: t.IndexDeltaLength, - } - d.Init() - return d -} - -// CreateEncoder creates an encoder able to encode the content of the track. -func (t *TrackMPEG4Audio) CreateEncoder() *rtpmpeg4audio.Encoder { - e := &rtpmpeg4audio.Encoder{ - PayloadType: t.PayloadType, - SampleRate: t.Config.SampleRate, - SizeLength: t.SizeLength, - IndexLength: t.IndexLength, - IndexDeltaLength: t.IndexDeltaLength, - } - e.Init() - return e -} diff --git a/track_mpeg4audio_test.go b/track_mpeg4audio_test.go deleted file mode 100644 index a30b8ea4..00000000 --- a/track_mpeg4audio_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" - - "github.com/aler9/gortsplib/pkg/mpeg4audio" -) - -func TestTrackMPEG4AudioAttributes(t *testing.T) { - track := &TrackMPEG4Audio{ - PayloadType: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 48000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } - require.Equal(t, "MPEG4-audio", track.String()) - require.Equal(t, 48000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackMPEG4AudioClone(t *testing.T) { - track := &TrackMPEG4Audio{ - PayloadType: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 48000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackMPEG4AudioMediaDescription(t *testing.T) { - track := &TrackMPEG4Audio{ - PayloadType: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 48000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_opus.go b/track_opus.go deleted file mode 100644 index 250cdbb2..00000000 --- a/track_opus.go +++ /dev/null @@ -1,121 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtpsimpleaudio" -) - -// TrackOpus is a Opus track. -type TrackOpus struct { - PayloadType uint8 - SampleRate int - ChannelCount int - - trackBase -} - -func newTrackOpusFromMediaDescription( - control string, - payloadType uint8, - clock string, -) (*TrackOpus, error) { - tmp := strings.SplitN(clock, "/", 32) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid clock (%v)", clock) - } - - sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) - if err != nil { - return nil, err - } - - channelCount, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return nil, err - } - - return &TrackOpus{ - PayloadType: payloadType, - SampleRate: int(sampleRate), - ChannelCount: int(channelCount), - trackBase: trackBase{ - control: control, - }, - }, nil -} - -// String returns the track codec. -func (t *TrackOpus) String() string { - return "Opus" -} - -// ClockRate returns the track clock rate. -func (t *TrackOpus) ClockRate() int { - return t.SampleRate -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackOpus) MediaDescription() *psdp.MediaDescription { - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " opus/" + strconv.FormatInt(int64(t.SampleRate), 10) + - "/" + strconv.FormatInt(int64(t.ChannelCount), 10), - }, - { - Key: "fmtp", - Value: typ + " sprop-stereo=" + func() string { - if t.ChannelCount == 2 { - return "1" - } - return "0" - }(), - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackOpus) clone() Track { - return &TrackOpus{ - PayloadType: t.PayloadType, - SampleRate: t.SampleRate, - ChannelCount: t.ChannelCount, - trackBase: t.trackBase, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackOpus) CreateDecoder() *rtpsimpleaudio.Decoder { - d := &rtpsimpleaudio.Decoder{ - SampleRate: t.SampleRate, - } - d.Init() - return d -} - -// CreateEncoder creates an encoder able to encode the content of the track. -func (t *TrackOpus) CreateEncoder() *rtpsimpleaudio.Encoder { - e := &rtpsimpleaudio.Encoder{ - PayloadType: t.PayloadType, - SampleRate: 8000, - } - e.Init() - return e -} diff --git a/track_opus_test.go b/track_opus_test.go deleted file mode 100644 index 00dc4867..00000000 --- a/track_opus_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackOpusAttributes(t *testing.T) { - track := &TrackOpus{ - PayloadType: 96, - SampleRate: 48000, - ChannelCount: 2, - } - require.Equal(t, "Opus", track.String()) - require.Equal(t, 48000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTracOpusClone(t *testing.T) { - track := &TrackOpus{ - PayloadType: 96, - SampleRate: 48000, - ChannelCount: 2, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackOpusMediaDescription(t *testing.T) { - track := &TrackOpus{ - PayloadType: 96, - SampleRate: 48000, - ChannelCount: 2, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 opus/48000/2", - }, - { - Key: "fmtp", - Value: "96 sprop-stereo=1", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_vorbis.go b/track_vorbis.go deleted file mode 100644 index b494b37d..00000000 --- a/track_vorbis.go +++ /dev/null @@ -1,144 +0,0 @@ -package gortsplib - -import ( - "encoding/base64" - "fmt" - "strconv" - "strings" - "sync" - - psdp "github.com/pion/sdp/v3" -) - -// TrackVorbis is a Vorbis track. -type TrackVorbis struct { - PayloadType uint8 - SampleRate int - ChannelCount int - Configuration []byte - - trackBase - mutex sync.RWMutex -} - -func newTrackVorbisFromMediaDescription( - control string, - payloadType uint8, - clock string, - md *psdp.MediaDescription, -) (*TrackVorbis, error) { - tmp := strings.SplitN(clock, "/", 32) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid clock (%v)", clock) - } - - sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) - if err != nil { - return nil, err - } - - channelCount, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return nil, err - } - - t := &TrackVorbis{ - PayloadType: payloadType, - SampleRate: int(sampleRate), - ChannelCount: int(channelCount), - trackBase: trackBase{ - control: control, - }, - } - - v, ok := md.Attribute("fmtp") - if !ok { - return nil, fmt.Errorf("fmtp attribute is missing") - } - - tmp = strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp (%v)", v) - } - - if tmp[0] == "configuration" { - conf, err := base64.StdEncoding.DecodeString(tmp[1]) - if err != nil { - return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) - } - - t.Configuration = conf - } - } - - if t.Configuration == nil { - return nil, fmt.Errorf("config is missing (%v)", v) - } - - return t, nil -} - -// String returns the track codec. -func (t *TrackVorbis) String() string { - return "Vorbis" -} - -// ClockRate returns the track clock rate. -func (t *TrackVorbis) ClockRate() int { - return t.SampleRate -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackVorbis) MediaDescription() *psdp.MediaDescription { - t.mutex.RLock() - defer t.mutex.RUnlock() - - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - fmtp := typ + " configuration=" + base64.StdEncoding.EncodeToString(t.Configuration) - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " VORBIS/" + strconv.FormatInt(int64(t.SampleRate), 10) + - "/" + strconv.FormatInt(int64(t.ChannelCount), 10), - }, - { - Key: "fmtp", - Value: fmtp, - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackVorbis) clone() Track { - return &TrackVorbis{ - PayloadType: t.PayloadType, - SampleRate: t.SampleRate, - ChannelCount: t.ChannelCount, - Configuration: t.Configuration, - trackBase: t.trackBase, - } -} diff --git a/track_vorbis_test.go b/track_vorbis_test.go deleted file mode 100644 index c3813eee..00000000 --- a/track_vorbis_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackVorbisAttributes(t *testing.T) { - track := &TrackVorbis{ - PayloadType: 96, - SampleRate: 48000, - ChannelCount: 2, - Configuration: []byte{0x01, 0x02, 0x03, 0x04}, - } - require.Equal(t, "Vorbis", track.String()) - require.Equal(t, 48000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTracVorbisClone(t *testing.T) { - track := &TrackVorbis{ - PayloadType: 96, - SampleRate: 48000, - ChannelCount: 2, - Configuration: []byte{0x01, 0x02, 0x03, 0x04}, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackVorbisMediaDescription(t *testing.T) { - track := &TrackVorbis{ - PayloadType: 96, - SampleRate: 48000, - ChannelCount: 2, - Configuration: []byte{0x01, 0x02, 0x03, 0x04}, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 VORBIS/48000/2", - }, - { - Key: "fmtp", - Value: "96 configuration=AQIDBA==", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_vp8.go b/track_vp8.go deleted file mode 100644 index 48bcc731..00000000 --- a/track_vp8.go +++ /dev/null @@ -1,147 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtpvp8" -) - -// TrackVP8 is a VP8 track. -type TrackVP8 struct { - trackBase - PayloadType uint8 - MaxFR *int - MaxFS *int -} - -func newTrackVP8FromMediaDescription( - control string, - payloadType uint8, - md *psdp.MediaDescription, -) (*TrackVP8, error) { - t := &TrackVP8{ - PayloadType: payloadType, - trackBase: trackBase{ - control: control, - }, - } - - t.fillParamsFromMediaDescription(md) - - return t, nil -} - -func (t *TrackVP8) fillParamsFromMediaDescription(md *psdp.MediaDescription) error { - v, ok := md.Attribute("fmtp") - if !ok { - return fmt.Errorf("fmtp attribute is missing") - } - - tmp := strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - switch tmp[0] { - case "max-fr": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fr (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFR = &v2 - - case "max-fs": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fs (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFS = &v2 - } - } - - return nil -} - -// String returns the track codec. -func (t *TrackVP8) String() string { - return "VP8" -} - -// ClockRate returns the track clock rate. -func (t *TrackVP8) ClockRate() int { - return 90000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackVP8) MediaDescription() *psdp.MediaDescription { - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - fmtp := typ - - var tmp []string - if t.MaxFR != nil { - tmp = append(tmp, "max-fr="+strconv.FormatInt(int64(*t.MaxFR), 10)) - } - if t.MaxFS != nil { - tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) - } - if tmp != nil { - fmtp += " " + strings.Join(tmp, ";") - } - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " VP8/90000", - }, - { - Key: "fmtp", - Value: fmtp, - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackVP8) clone() Track { - return &TrackVP8{ - trackBase: t.trackBase, - PayloadType: t.PayloadType, - MaxFR: t.MaxFR, - MaxFS: t.MaxFS, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackVP8) CreateDecoder() *rtpvp8.Decoder { - d := &rtpvp8.Decoder{} - d.Init() - return d -} diff --git a/track_vp8_test.go b/track_vp8_test.go deleted file mode 100644 index 4ff2c66c..00000000 --- a/track_vp8_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackVP8ttributes(t *testing.T) { - track := &TrackVP8{} - require.Equal(t, "VP8", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackVP8Clone(t *testing.T) { - maxFR := 123 - maxFS := 456 - track := &TrackVP8{ - PayloadType: 96, - MaxFR: &maxFR, - MaxFS: &maxFS, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackVP8MediaDescription(t *testing.T) { - maxFR := 123 - maxFS := 456 - track := &TrackVP8{ - PayloadType: 96, - MaxFR: &maxFR, - MaxFS: &maxFS, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 VP8/90000", - }, - { - Key: "fmtp", - Value: "96 max-fr=123;max-fs=456", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/track_vp9.go b/track_vp9.go deleted file mode 100644 index afc08f9b..00000000 --- a/track_vp9.go +++ /dev/null @@ -1,160 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strconv" - "strings" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/rtpcodecs/rtpvp9" -) - -// TrackVP9 is a VP9 track. -type TrackVP9 struct { - trackBase - PayloadType uint8 - MaxFR *int - MaxFS *int - ProfileID *int -} - -func newTrackVP9FromMediaDescription( - control string, - payloadType uint8, - md *psdp.MediaDescription, -) (*TrackVP9, error) { - t := &TrackVP9{ - PayloadType: payloadType, - trackBase: trackBase{ - control: control, - }, - } - - t.fillParamsFromMediaDescription(md) - - return t, nil -} - -func (t *TrackVP9) fillParamsFromMediaDescription(md *psdp.MediaDescription) error { - v, ok := md.Attribute("fmtp") - if !ok { - return fmt.Errorf("fmtp attribute is missing") - } - - tmp := strings.SplitN(v, " ", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - for _, kv := range strings.Split(tmp[1], ";") { - kv = strings.Trim(kv, " ") - - if len(kv) == 0 { - continue - } - - tmp := strings.SplitN(kv, "=", 2) - if len(tmp) != 2 { - return fmt.Errorf("invalid fmtp attribute (%v)", v) - } - - switch tmp[0] { - case "max-fr": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fr (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFR = &v2 - - case "max-fs": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid max-fs (%v)", tmp[1]) - } - v2 := int(val) - t.MaxFS = &v2 - - case "profile-id": - val, err := strconv.ParseUint(tmp[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid profile-id (%v)", tmp[1]) - } - v2 := int(val) - t.ProfileID = &v2 - } - } - - return nil -} - -// String returns the track codec. -func (t *TrackVP9) String() string { - return "VP9" -} - -// ClockRate returns the track clock rate. -func (t *TrackVP9) ClockRate() int { - return 90000 -} - -// MediaDescription returns the track media description in SDP format. -func (t *TrackVP9) MediaDescription() *psdp.MediaDescription { - typ := strconv.FormatInt(int64(t.PayloadType), 10) - - fmtp := typ - - var tmp []string - if t.MaxFR != nil { - tmp = append(tmp, "max-fr="+strconv.FormatInt(int64(*t.MaxFR), 10)) - } - if t.MaxFS != nil { - tmp = append(tmp, "max-fs="+strconv.FormatInt(int64(*t.MaxFS), 10)) - } - if t.ProfileID != nil { - tmp = append(tmp, "profile-id="+strconv.FormatInt(int64(*t.ProfileID), 10)) - } - if tmp != nil { - fmtp += " " + strings.Join(tmp, ";") - } - - return &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " VP9/90000", - }, - { - Key: "fmtp", - Value: fmtp, - }, - { - Key: "control", - Value: t.control, - }, - }, - } -} - -func (t *TrackVP9) clone() Track { - return &TrackVP9{ - trackBase: t.trackBase, - PayloadType: t.PayloadType, - MaxFR: t.MaxFR, - MaxFS: t.MaxFS, - ProfileID: t.ProfileID, - } -} - -// CreateDecoder creates a decoder able to decode the content of the track. -func (t *TrackVP9) CreateDecoder() *rtpvp9.Decoder { - d := &rtpvp9.Decoder{} - d.Init() - return d -} diff --git a/track_vp9_test.go b/track_vp9_test.go deleted file mode 100644 index 5fdd1a3b..00000000 --- a/track_vp9_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package gortsplib - -import ( - "testing" - - psdp "github.com/pion/sdp/v3" - "github.com/stretchr/testify/require" -) - -func TestTrackVP9Attributes(t *testing.T) { - track := &TrackVP9{} - require.Equal(t, "VP9", track.String()) - require.Equal(t, 90000, track.ClockRate()) - require.Equal(t, "", track.GetControl()) -} - -func TestTrackVP9Clone(t *testing.T) { - maxFR := 123 - maxFS := 456 - profileID := 789 - track := &TrackVP9{ - PayloadType: 96, - MaxFR: &maxFR, - MaxFS: &maxFS, - ProfileID: &profileID, - } - - clone := track.clone() - require.NotSame(t, track, clone) - require.Equal(t, track, clone) -} - -func TestTrackVP9MediaDescription(t *testing.T) { - maxFR := 123 - maxFS := 456 - profileID := 789 - track := &TrackVP9{ - PayloadType: 96, - MaxFR: &maxFR, - MaxFS: &maxFS, - ProfileID: &profileID, - } - - require.Equal(t, &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 VP9/90000", - }, - { - Key: "fmtp", - Value: "96 max-fr=123;max-fs=456;profile-id=789", - }, - { - Key: "control", - Value: "", - }, - }, - }, track.MediaDescription()) -} diff --git a/tracks.go b/tracks.go deleted file mode 100644 index ef62c187..00000000 --- a/tracks.go +++ /dev/null @@ -1,87 +0,0 @@ -package gortsplib - -import ( - "fmt" - "strconv" - - psdp "github.com/pion/sdp/v3" - - "github.com/aler9/gortsplib/pkg/sdp" -) - -// Tracks is a list of tracks. -type Tracks []Track - -// Unmarshal decodes tracks from the SDP format. It returns the decoded SDP. -func (ts *Tracks) Unmarshal(byts []byte) (*sdp.SessionDescription, error) { - var sd sdp.SessionDescription - err := sd.Unmarshal(byts) - if err != nil { - return nil, err - } - - *ts = nil - - for i, md := range sd.MediaDescriptions { - t, err := newTrackFromMediaDescription(md) - if err != nil { - return nil, fmt.Errorf("unable to parse track %d: %s", i+1, err) - } - - *ts = append(*ts, t) - } - - if *ts == nil { - return nil, fmt.Errorf("no valid tracks found") - } - - return &sd, nil -} - -func (ts Tracks) clone() Tracks { - ret := make(Tracks, len(ts)) - for i, track := range ts { - ret[i] = track.clone() - } - return ret -} - -func (ts Tracks) setControls() { - for i, t := range ts { - t.SetControl("trackID=" + strconv.FormatInt(int64(i), 10)) - } -} - -// Marshal encodes tracks in the SDP format. -func (ts Tracks) Marshal(multicast bool) []byte { - address := "0.0.0.0" - if multicast { - address = "224.1.0.0" - } - - sout := &sdp.SessionDescription{ - SessionName: psdp.SessionName("Stream"), - Origin: psdp.Origin{ - Username: "-", - NetworkType: "IN", - AddressType: "IP4", - UnicastAddress: "127.0.0.1", - }, - // required by Darwin Streaming Server - ConnectionInformation: &psdp.ConnectionInformation{ - NetworkType: "IN", - AddressType: "IP4", - Address: &psdp.Address{Address: address}, - }, - TimeDescriptions: []psdp.TimeDescription{ - {Timing: psdp.Timing{0, 0}}, //nolint:govet - }, - } - - for _, track := range ts { - sout.MediaDescriptions = append(sout.MediaDescriptions, track.MediaDescription()) - } - - byts, _ := sout.Marshal() - return byts -} diff --git a/tracks_test.go b/tracks_test.go deleted file mode 100644 index e87a2d45..00000000 --- a/tracks_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package gortsplib - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestTracksRead(t *testing.T) { - sdp := []byte("v=0\r\n" + - "o=- 0 0 IN IP4 10.0.0.131\r\n" + - "s=Media Presentation\r\n" + - "i=samsung\r\n" + - "c=IN IP4 0.0.0.0\r\n" + - "b=AS:2632\r\n" + - "t=0 0\r\n" + - "a=control:rtsp://10.0.100.50/profile5/media.smp\r\n" + - "a=range:npt=now-\r\n" + - "m=video 42504 RTP/AVP 97\r\n" + - "b=AS:2560\r\n" + - "a=rtpmap:97 H264/90000\r\n" + - "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=v\r\n" + - "a=cliprect:0,0,1080,1920\r\n" + - "a=framesize:97 1920-1080\r\n" + - "a=framerate:30.0\r\n" + - "a=fmtp:97 packetization-mode=1;profile-level-id=640028;sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" + - "m=audio 42506 RTP/AVP 0\r\n" + - "b=AS:64\r\n" + - "a=rtpmap:0 PCMU/8000\r\n" + - "a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" + - "a=recvonly\r\n" + - "m=application 42508 RTP/AVP 107\r\n" + - "b=AS:8\r\n") - - var tracks Tracks - _, err := tracks.Unmarshal(sdp) - require.NoError(t, err) - require.Equal(t, Tracks{ - &TrackH264{ - trackBase: trackBase{ - control: "rtsp://10.0.100.50/profile5/media.smp/trackID=v", - }, - PayloadType: 97, - SPS: []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a}, - PPS: []byte{0x68, 0xee, 0x01, 0x9e, 0x2c}, - PacketizationMode: 1, - }, - &TrackG711{ - MULaw: true, - trackBase: trackBase{ - control: "rtsp://10.0.100.50/profile5/media.smp/trackID=a", - }, - }, - &TrackGeneric{ - Media: "application", - Payloads: []TrackGenericPayload{{ - Type: 107, - }}, - }, - }, tracks) -} - -func TestTracksReadErrors(t *testing.T) { - for _, ca := range []struct { - name string - sdp []byte - err string - }{ - { - "invalid SDP", - []byte{0x00, 0x01}, - "invalid line: (\x00\x01)", - }, - { - "invalid track", - []byte("v=0\r\n" + - "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + - "s=SDP Seminar\r\n" + - "m=video 0 RTP/AVP/TCP 96\r\n" + - "a=rtpmap:96 H265/90000\r\n" + - "a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwB4FwJA; " + - "sprop-sps=QgEBAWAAAAMAsAAAAwAAAwB4oAKggC8c1YgXuRZFL/y5/E/qbgQEBAE=; sprop-pps=RAHAcvBTJA==;\r\n" + - "a=control:streamid=0\r\n" + - "m=audio 0 RTP/AVP/TCP 97\r\n" + - "a=rtpmap:97 mpeg4-generic/44100/2\r\n" + - "a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" + - "a=control:streamid=1\r\n"), - "unable to parse track 2: invalid AAC config (zzz1210)", - }, - } { - t.Run(ca.name, func(t *testing.T) { - var tracks Tracks - _, err := tracks.Unmarshal(ca.sdp) - require.EqualError(t, err, ca.err) - }) - } -}