mirror of
https://github.com/aler9/gortsplib
synced 2025-10-04 14:52:46 +08:00
convert Tracks into Medias and Formats (#155)
* split tracks from medias * move tracks into dedicated package * move media into dedicated package * edit Medias.Marshal() in order to return SDP * add medias.Find() and simplify examples * improve coverage * fix rebase errors * replace TrackIDs with MediaIDs * implement media-specific and track-specific callbacks for reading RTCP and RTP packets * rename publish into record, read into play * add v2 tag * rename tracks into formats
This commit is contained in:
64
README.md
64
README.md
@@ -14,12 +14,12 @@ Go ≥ 1.17 is required.
|
|||||||
Features:
|
Features:
|
||||||
|
|
||||||
* Client
|
* Client
|
||||||
* Query servers about available streams and tracks
|
* Query servers about available streams and mediastreams
|
||||||
* Read
|
* 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)
|
* Read TLS-encrypted streams (TCP only)
|
||||||
* Switch transport protocol automatically
|
* Switch transport protocol automatically
|
||||||
* Read only selected tracks of a stream
|
* Read only selected media streams
|
||||||
* Pause or seek without disconnecting from the server
|
* Pause or seek without disconnecting from the server
|
||||||
* Generate RTCP receiver reports (UDP only)
|
* Generate RTCP receiver reports (UDP only)
|
||||||
* Reorder incoming RTP packets (UDP only)
|
* Reorder incoming RTP packets (UDP only)
|
||||||
@@ -33,28 +33,19 @@ Features:
|
|||||||
* Handle requests from clients
|
* Handle requests from clients
|
||||||
* Sessions and connections are independent
|
* Sessions and connections are independent
|
||||||
* Publish
|
* 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)
|
* Read TLS-encrypted streams (TCP only)
|
||||||
* Generate RTCP receiver reports (UDP only)
|
* Generate RTCP receiver reports (UDP only)
|
||||||
* Reorder incoming RTP packets (UDP only)
|
* Reorder incoming RTP packets (UDP only)
|
||||||
* Read
|
* 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
|
* Write TLS-encrypted streams
|
||||||
* Compute and provide SSRC, RTP-Info to clients
|
* Compute and provide SSRC, RTP-Info to clients
|
||||||
* Generate RTCP sender reports
|
* Generate RTCP sender reports
|
||||||
* Utilities
|
* Utilities
|
||||||
* Encode and decode codec-specific frames into/from RTP packets. The following codecs are supported:
|
* Encode and decode codec-specific frames into/from RTP packets. The following codecs are supported:
|
||||||
* Video
|
* Video: H264, H265, VP8, VP9
|
||||||
* H264
|
* Audio: G711 (PCMA, PCMU), G722, LPCM, MPEG4-audio (AAC), Opus
|
||||||
* H265
|
|
||||||
* VP8
|
|
||||||
* VP9
|
|
||||||
* Audio
|
|
||||||
* G711
|
|
||||||
* G722
|
|
||||||
* LPCM
|
|
||||||
* MPEG4-audio (AAC)
|
|
||||||
* Opus
|
|
||||||
* Parse RTSP elements: requests, responses, SDP
|
* Parse RTSP elements: requests, responses, SDP
|
||||||
* Parse H264 elements and formats: Annex-B, AVCC, anti-competition, DTS
|
* Parse H264 elements and formats: Annex-B, AVCC, anti-competition, DTS
|
||||||
* Parse MPEG4-audio (AAC) element and formats: ADTS, MPEG4-audio configurations
|
* Parse MPEG4-audio (AAC) element and formats: ADTS, MPEG4-audio configurations
|
||||||
@@ -70,31 +61,30 @@ Features:
|
|||||||
* [client-query](examples/client-query/main.go)
|
* [client-query](examples/client-query/main.go)
|
||||||
* [client-read](examples/client-read/main.go)
|
* [client-read](examples/client-read/main.go)
|
||||||
* [client-read-options](examples/client-read-options/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-pause](examples/client-read-pause/main.go)
|
||||||
* [client-read-republish](examples/client-read-republish/main.go)
|
* [client-read-republish](examples/client-read-republish/main.go)
|
||||||
* [client-read-track-g711](examples/client-read-track-g711/main.go)
|
* [client-read-format-g711](examples/client-read-format-g711/main.go)
|
||||||
* [client-read-track-g722](examples/client-read-track-g722/main.go)
|
* [client-read-format-g722](examples/client-read-format-g722/main.go)
|
||||||
* [client-read-track-h264](examples/client-read-track-h264/main.go)
|
* [client-read-format-h264](examples/client-read-format-h264/main.go)
|
||||||
* [client-read-track-h264-convert-to-jpeg](examples/client-read-track-h264-convert-to-jpeg/main.go)
|
* [client-read-format-h264-convert-to-jpeg](examples/client-read-format-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-format-h264-save-to-disk](examples/client-read-format-h264-save-to-disk/main.go)
|
||||||
* [client-read-track-h265](examples/client-read-track-h265/main.go)
|
* [client-read-format-h265](examples/client-read-format-h265/main.go)
|
||||||
* [client-read-track-lpcm](examples/client-read-track-lpcm/main.go)
|
* [client-read-format-lpcm](examples/client-read-format-lpcm/main.go)
|
||||||
* [client-read-track-mpeg4audio](examples/client-read-track-mpeg4audio/main.go)
|
* [client-read-format-mpeg4audio](examples/client-read-format-mpeg4audio/main.go)
|
||||||
* [client-read-track-opus](examples/client-read-track-opus/main.go)
|
* [client-read-format-opus](examples/client-read-format-opus/main.go)
|
||||||
* [client-read-track-vp8](examples/client-read-track-vp8/main.go)
|
* [client-read-format-vp8](examples/client-read-format-vp8/main.go)
|
||||||
* [client-read-track-vp9](examples/client-read-track-vp9/main.go)
|
* [client-read-format-vp9](examples/client-read-format-vp9/main.go)
|
||||||
* [client-publish-options](examples/client-publish-options/main.go)
|
* [client-publish-options](examples/client-publish-options/main.go)
|
||||||
* [client-publish-pause](examples/client-publish-pause/main.go)
|
* [client-publish-pause](examples/client-publish-pause/main.go)
|
||||||
* [client-publish-track-g711](examples/client-publish-track-g711/main.go)
|
* [client-publish-format-g711](examples/client-publish-format-g711/main.go)
|
||||||
* [client-publish-track-g722](examples/client-publish-track-g722/main.go)
|
* [client-publish-format-g722](examples/client-publish-format-g722/main.go)
|
||||||
* [client-publish-track-h264](examples/client-publish-track-h264/main.go)
|
* [client-publish-format-h264](examples/client-publish-format-h264/main.go)
|
||||||
* [client-publish-track-h265](examples/client-publish-track-h265/main.go)
|
* [client-publish-format-h265](examples/client-publish-format-h265/main.go)
|
||||||
* [client-publish-track-lpcm](examples/client-publish-track-lpcm/main.go)
|
* [client-publish-format-lpcm](examples/client-publish-format-lpcm/main.go)
|
||||||
* [client-publish-track-mpeg4audio](examples/client-publish-track-mpeg4audio/main.go)
|
* [client-publish-format-mpeg4audio](examples/client-publish-format-mpeg4audio/main.go)
|
||||||
* [client-publish-track-opus](examples/client-publish-track-opus/main.go)
|
* [client-publish-format-opus](examples/client-publish-format-opus/main.go)
|
||||||
* [client-publish-track-vp8](examples/client-publish-track-vp8/main.go)
|
* [client-publish-format-vp8](examples/client-publish-format-vp8/main.go)
|
||||||
* [client-publish-track-vp9](examples/client-publish-track-vp9/main.go)
|
* [client-publish-format-vp9](examples/client-publish-format-vp9/main.go)
|
||||||
* [server](examples/server/main.go)
|
* [server](examples/server/main.go)
|
||||||
* [server-tls](examples/server-tls/main.go)
|
* [server-tls](examples/server-tls/main.go)
|
||||||
* [server-h264-save-to-disk](examples/server-h264-save-to-disk/main.go)
|
* [server-h264-save-to-disk](examples/server-h264-save-to-disk/main.go)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -12,15 +12,28 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
"github.com/aler9/gortsplib/pkg/conn"
|
"github.com/aler9/gortsplib/v2/pkg/conn"
|
||||||
"github.com/aler9/gortsplib/pkg/headers"
|
"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{
|
var testRTPPacket = rtp.Packet{
|
||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Version: 2,
|
Version: 2,
|
||||||
PayloadType: 97,
|
PayloadType: 96,
|
||||||
CSRC: []uint32{},
|
CSRC: []uint32{},
|
||||||
SSRC: 0x38F27A2F,
|
SSRC: 0x38F27A2F,
|
||||||
},
|
},
|
||||||
@@ -51,7 +64,43 @@ var testRTCPPacketMarshaled = func() []byte {
|
|||||||
return byts
|
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{
|
for _, transport := range []string{
|
||||||
"udp",
|
"udp",
|
||||||
"tcp",
|
"tcp",
|
||||||
@@ -114,7 +163,7 @@ func TestClientPublishSerial(t *testing.T) {
|
|||||||
req, err = conn.ReadRequest()
|
req, err = conn.ReadRequest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, base.Setup, req.Method)
|
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
|
var inTH headers.Transport
|
||||||
err = inTH.Unmarshal(req.Header["Transport"])
|
err = inTH.Unmarshal(req.Header["Transport"])
|
||||||
@@ -224,22 +273,16 @@ func TestClientPublishSerial(t *testing.T) {
|
|||||||
v := TransportTCP
|
v := TransportTCP
|
||||||
return &v
|
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)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
@@ -248,20 +291,20 @@ func TestClientPublishSerial(t *testing.T) {
|
|||||||
c.Wait()
|
c.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = c.WritePacketRTP(0, &testRTPPacket)
|
err = c.WritePacketRTP(medi, &testRTPPacket)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
<-recvDone
|
<-recvDone
|
||||||
c.Close()
|
c.Close()
|
||||||
<-done
|
<-done
|
||||||
|
|
||||||
err = c.WritePacketRTP(0, &testRTPPacket)
|
err = c.WritePacketRTP(medi, &testRTPPacket)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientPublishParallel(t *testing.T) {
|
func TestClientRecordParallel(t *testing.T) {
|
||||||
for _, transport := range []string{
|
for _, transport := range []string{
|
||||||
"udp",
|
"udp",
|
||||||
"tcp",
|
"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{})
|
writerDone := make(chan struct{})
|
||||||
defer func() { <-writerDone }()
|
defer func() { <-writerDone }()
|
||||||
|
|
||||||
err = c.StartPublishing(scheme+"://localhost:8554/teststream",
|
medi := testH264Media.Clone()
|
||||||
Tracks{track})
|
err = record(&c, scheme+"://localhost:8554/teststream", media.Medias{medi}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
@@ -406,7 +442,7 @@ func TestClientPublishParallel(t *testing.T) {
|
|||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
for range t.C {
|
for range t.C {
|
||||||
err := c.WritePacketRTP(0, &testRTPPacket)
|
err := c.WritePacketRTP(medi, &testRTPPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -418,7 +454,7 @@ func TestClientPublishParallel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientPublishPauseSerial(t *testing.T) {
|
func TestClientRecordPauseSerial(t *testing.T) {
|
||||||
for _, transport := range []string{
|
for _, transport := range []string{
|
||||||
"udp",
|
"udp",
|
||||||
"tcp",
|
"tcp",
|
||||||
@@ -544,19 +580,12 @@ func TestClientPublishPauseSerial(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
}
|
}
|
||||||
|
|
||||||
track := &TrackH264{
|
medi := testH264Media.Clone()
|
||||||
PayloadType: 96,
|
err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil)
|
||||||
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)
|
require.NoError(t, err)
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
err = c.WritePacketRTP(0, &testRTPPacket)
|
err = c.WritePacketRTP(medi, &testRTPPacket)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = c.Pause()
|
_, err = c.Pause()
|
||||||
@@ -565,13 +594,13 @@ func TestClientPublishPauseSerial(t *testing.T) {
|
|||||||
_, err = c.Record()
|
_, err = c.Record()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = c.WritePacketRTP(0, &testRTPPacket)
|
err = c.WritePacketRTP(medi, &testRTPPacket)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientPublishPauseParallel(t *testing.T) {
|
func TestClientRecordPauseParallel(t *testing.T) {
|
||||||
for _, transport := range []string{
|
for _, transport := range []string{
|
||||||
"udp",
|
"udp",
|
||||||
"tcp",
|
"tcp",
|
||||||
@@ -679,15 +708,8 @@ func TestClientPublishPauseParallel(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
}
|
}
|
||||||
|
|
||||||
track := &TrackH264{
|
medi := testH264Media.Clone()
|
||||||
PayloadType: 96,
|
err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil)
|
||||||
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
writerDone := make(chan struct{})
|
writerDone := make(chan struct{})
|
||||||
@@ -698,7 +720,7 @@ func TestClientPublishPauseParallel(t *testing.T) {
|
|||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
for range t.C {
|
for range t.C {
|
||||||
err := c.WritePacketRTP(0, &testRTPPacket)
|
err := c.WritePacketRTP(medi, &testRTPPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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")
|
l, err := net.Listen("tcp", "localhost:8554")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
@@ -821,25 +843,18 @@ func TestClientPublishAutomaticProtocol(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
track := &TrackH264{
|
|
||||||
PayloadType: 96,
|
|
||||||
SPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PacketizationMode: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
c := Client{}
|
c := Client{}
|
||||||
|
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/teststream",
|
medi := testH264Media.Clone()
|
||||||
Tracks{track})
|
err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
err = c.WritePacketRTP(0, &testRTPPacket)
|
err = c.WritePacketRTP(medi, &testRTPPacket)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientPublishDecodeErrors(t *testing.T) {
|
func TestClientRecordDecodeErrors(t *testing.T) {
|
||||||
for _, ca := range []struct {
|
for _, ca := range []struct {
|
||||||
proto string
|
proto string
|
||||||
name string
|
name string
|
||||||
@@ -1010,15 +1025,7 @@ func TestClientPublishDecodeErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
track := &TrackH264{
|
err = record(&c, "rtsp://localhost:8554/stream", media.Medias{testH264Media.Clone()}, nil)
|
||||||
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})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer c.Close()
|
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"} {
|
for _, ca := range []string{"udp", "tcp"} {
|
||||||
t.Run(ca, func(t *testing.T) {
|
t.Run(ca, func(t *testing.T) {
|
||||||
reportReceived := make(chan struct{})
|
reportReceived := make(chan struct{})
|
||||||
@@ -1170,21 +1177,16 @@ func TestClientPublishRTCPReport(t *testing.T) {
|
|||||||
v := TransportTCP
|
v := TransportTCP
|
||||||
return &v
|
return &v
|
||||||
}(),
|
}(),
|
||||||
udpSenderReportPeriod: 500 * time.Millisecond,
|
senderReportPeriod: 500 * time.Millisecond,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/teststream",
|
medi := testH264Media.Clone()
|
||||||
Tracks{&TrackH264{
|
err = record(&c, "rtsp://localhost:8554/teststream", media.Medias{medi}, nil)
|
||||||
PayloadType: 96,
|
|
||||||
SPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PacketizationMode: 1,
|
|
||||||
}})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
err = c.WritePacketRTP(0, &rtp.Packet{
|
err = c.WritePacketRTP(medi, &rtp.Packet{
|
||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Version: 2,
|
Version: 2,
|
||||||
PayloadType: 96,
|
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")
|
l, err := net.Listen("tcp", "localhost:8554")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
@@ -1303,23 +1305,14 @@ func TestClientPublishIgnoreTCPRTPPackets(t *testing.T) {
|
|||||||
v := TransportTCP
|
v := TransportTCP
|
||||||
return &v
|
return &v
|
||||||
}(),
|
}(),
|
||||||
OnPacketRTP: func(ctx *ClientOnPacketRTPCtx) {
|
}
|
||||||
t.Errorf("should not happen")
|
|
||||||
},
|
medias := media.Medias{testH264Media.Clone()}
|
||||||
OnPacketRTCP: func(ctx *ClientOnPacketRTCPCtx) {
|
|
||||||
|
err = record(&c, "rtsp://localhost:8554/teststream", medias,
|
||||||
|
func(medi *media.Media, pkt rtcp.Packet) {
|
||||||
close(rtcpReceived)
|
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)
|
require.NoError(t, err)
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
@@ -8,10 +8,11 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/auth"
|
"github.com/aler9/gortsplib/v2/pkg/auth"
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
"github.com/aler9/gortsplib/pkg/conn"
|
"github.com/aler9/gortsplib/v2/pkg/conn"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/media"
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustParseURL(s string) *url.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"])
|
require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"])
|
||||||
|
|
||||||
track := &TrackH264{
|
medias := media.Medias{testH264Media.Clone()}
|
||||||
PayloadType: 96,
|
medias.SetControls()
|
||||||
SPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PacketizationMode: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks := Tracks{track}
|
|
||||||
tracks.setControls()
|
|
||||||
|
|
||||||
err = conn.WriteResponse(&base.Response{
|
err = conn.WriteResponse(&base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
@@ -121,7 +115,7 @@ func TestClientSession(t *testing.T) {
|
|||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Session": base.HeaderValue{"123456"},
|
"Session": base.HeaderValue{"123456"},
|
||||||
},
|
},
|
||||||
Body: tracks.Marshal(false),
|
Body: mustMarshalSDP(medias.Marshal(false)),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
@@ -189,22 +183,15 @@ func TestClientAuth(t *testing.T) {
|
|||||||
err = v.ValidateRequest(req)
|
err = v.ValidateRequest(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
track := &TrackH264{
|
medias := media.Medias{testH264Media.Clone()}
|
||||||
PayloadType: 96,
|
medias.SetControls()
|
||||||
SPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PacketizationMode: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks := Tracks{track}
|
|
||||||
tracks.setControls()
|
|
||||||
|
|
||||||
err = conn.WriteResponse(&base.Response{
|
err = conn.WriteResponse(&base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
},
|
},
|
||||||
Body: tracks.Marshal(false),
|
Body: mustMarshalSDP(medias.Marshal(false)),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
@@ -256,12 +243,7 @@ func TestClientDescribeCharset(t *testing.T) {
|
|||||||
require.Equal(t, base.Describe, req.Method)
|
require.Equal(t, base.Describe, req.Method)
|
||||||
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
||||||
|
|
||||||
track1 := &TrackH264{
|
medias := media.Medias{testH264Media.Clone()}
|
||||||
PayloadType: 96,
|
|
||||||
SPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PPS: []byte{0x01, 0x02, 0x03, 0x04},
|
|
||||||
PacketizationMode: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = conn.WriteResponse(&base.Response{
|
err = conn.WriteResponse(&base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
@@ -269,7 +251,7 @@ func TestClientDescribeCharset(t *testing.T) {
|
|||||||
"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
|
"Content-Type": base.HeaderValue{"application/sdp; charset=utf-8"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track1}.Marshal(false),
|
Body: mustMarshalSDP(medias.Marshal(false)),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
|
341
clientmedia.go
Normal file
341
clientmedia.go
Normal file
@@ -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
|
||||||
|
}
|
119
clienttrack.go
Normal file
119
clienttrack.go
Normal file
@@ -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)
|
||||||
|
}
|
109
clientudpl.go
109
clientudpl.go
@@ -2,13 +2,11 @@ package gortsplib
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/rtcp"
|
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +23,7 @@ func randIntn(n int) int {
|
|||||||
type clientUDPListener struct {
|
type clientUDPListener struct {
|
||||||
c *Client
|
c *Client
|
||||||
pc *net.UDPConn
|
pc *net.UDPConn
|
||||||
ct *clientTrack
|
cm *clientMedia
|
||||||
isRTP bool
|
isRTP bool
|
||||||
|
|
||||||
readIP net.IP
|
readIP net.IP
|
||||||
@@ -38,7 +36,7 @@ type clientUDPListener struct {
|
|||||||
readerDone chan 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
|
// choose two consecutive ports in range 65535-10000
|
||||||
// RTP port must be even and RTCP port odd
|
// RTP port must be even and RTCP port odd
|
||||||
for {
|
for {
|
||||||
@@ -47,7 +45,7 @@ func newClientUDPListenerPair(c *Client, ct *clientTrack) (*clientUDPListener, *
|
|||||||
c,
|
c,
|
||||||
false,
|
false,
|
||||||
":"+strconv.FormatInt(int64(rtpPort), 10),
|
":"+strconv.FormatInt(int64(rtpPort), 10),
|
||||||
ct,
|
cm,
|
||||||
true)
|
true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -58,7 +56,7 @@ func newClientUDPListenerPair(c *Client, ct *clientTrack) (*clientUDPListener, *
|
|||||||
c,
|
c,
|
||||||
false,
|
false,
|
||||||
":"+strconv.FormatInt(int64(rtcpPort), 10),
|
":"+strconv.FormatInt(int64(rtcpPort), 10),
|
||||||
ct,
|
cm,
|
||||||
false)
|
false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rtpListener.close()
|
rtpListener.close()
|
||||||
@@ -73,7 +71,7 @@ func newClientUDPListener(
|
|||||||
c *Client,
|
c *Client,
|
||||||
multicast bool,
|
multicast bool,
|
||||||
address string,
|
address string,
|
||||||
ct *clientTrack,
|
cm *clientMedia,
|
||||||
isRTP bool,
|
isRTP bool,
|
||||||
) (*clientUDPListener, error) {
|
) (*clientUDPListener, error) {
|
||||||
var pc *net.UDPConn
|
var pc *net.UDPConn
|
||||||
@@ -124,7 +122,7 @@ func newClientUDPListener(
|
|||||||
return &clientUDPListener{
|
return &clientUDPListener{
|
||||||
c: c,
|
c: c,
|
||||||
pc: pc,
|
pc: pc,
|
||||||
ct: ct,
|
cm: cm,
|
||||||
isRTP: isRTP,
|
isRTP: isRTP,
|
||||||
lastPacketTime: func() *int64 {
|
lastPacketTime: func() *int64 {
|
||||||
v := int64(0)
|
v := int64(0)
|
||||||
@@ -159,15 +157,11 @@ func (u *clientUDPListener) stop() {
|
|||||||
func (u *clientUDPListener) runReader(forPlay bool) {
|
func (u *clientUDPListener) runReader(forPlay bool) {
|
||||||
defer close(u.readerDone)
|
defer close(u.readerDone)
|
||||||
|
|
||||||
var processFunc func(time.Time, []byte)
|
var readFunc func([]byte) error
|
||||||
if forPlay {
|
|
||||||
if u.isRTP {
|
if u.isRTP {
|
||||||
processFunc = u.processPlayRTP
|
readFunc = u.cm.readRTP
|
||||||
} else {
|
} else {
|
||||||
processFunc = u.processPlayRTCP
|
readFunc = u.cm.readRTCP
|
||||||
}
|
|
||||||
} else {
|
|
||||||
processFunc = u.processRecordRTCP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -186,90 +180,7 @@ func (u *clientUDPListener) runReader(forPlay bool) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
atomic.StoreInt64(u.lastPacketTime, now.Unix())
|
atomic.StoreInt64(u.lastPacketTime, now.Unix())
|
||||||
|
|
||||||
processFunc(now, buf[:n])
|
readFunc(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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
60
clientwriter.go
Normal file
60
clientwriter.go
Normal file
@@ -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
|
||||||
|
}
|
@@ -4,24 +4,26 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/PCMA packets with GStreamer
|
// 1. generate RTP/G711 packets with GStreamer
|
||||||
// 2. connect to a RTSP server, announce a PCMA track
|
// 2. connect to a RTSP server, announce a G711 media
|
||||||
// 3. route the packets from GStreamer to the server
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
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")
|
pc, err := net.ListenPacket("udp", "localhost:9000")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer pc.Close()
|
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" +
|
"gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=8000" +
|
||||||
" ! alawenc ! rtppcmapay ! udpsink host=127.0.0.1 port=9000")
|
" ! alawenc ! rtppcmapay ! udpsink host=127.0.0.1 port=9000")
|
||||||
|
|
||||||
@@ -33,14 +35,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create a PCMA track
|
// create a media that contains a G711 format
|
||||||
track := &gortsplib.TrackG711{}
|
medi := &media.Media{
|
||||||
|
Type: media.TypeAudio,
|
||||||
|
Formats: []format.Format{&format.G711{}},
|
||||||
|
}
|
||||||
|
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -55,7 +60,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,13 +4,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/G722 packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -33,14 +35,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create a G722 track
|
// create a media that contains a G722 format
|
||||||
track := &gortsplib.TrackG722{}
|
medi := &media.Media{
|
||||||
|
Type: media.TypeAudio,
|
||||||
|
Formats: []format.Format{&format.G722{}},
|
||||||
|
}
|
||||||
|
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -55,7 +60,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,13 +4,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/H264 packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -34,16 +36,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create an H264 track
|
// create a media that contains a H264 format
|
||||||
track := &gortsplib.TrackH264{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
Type: media.TypeVideo,
|
||||||
|
Formats: []format.Format{&format.H264{
|
||||||
|
PayloadTyp: 96,
|
||||||
PacketizationMode: 1,
|
PacketizationMode: 1,
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -58,7 +63,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,13 +4,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/H265 packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -34,15 +36,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create an H265 track
|
// create a media that contains a H265 format
|
||||||
track := &gortsplib.TrackH265{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
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{}
|
c := gortsplib.Client{}
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,13 +4,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/LPCM packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -33,19 +35,22 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create an LPCM track
|
// create a media that contains a LPCM format
|
||||||
track := &gortsplib.TrackLPCM{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
Type: media.TypeAudio,
|
||||||
|
Formats: []format.Format{&format.LPCM{
|
||||||
|
PayloadTyp: 96,
|
||||||
BitDepth: 16,
|
BitDepth: 16,
|
||||||
SampleRate: 44100,
|
SampleRate: 44100,
|
||||||
ChannelCount: 1,
|
ChannelCount: 1,
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -60,7 +65,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,14 +4,16 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/mpeg4audio"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/MPEG4-audio packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -34,9 +36,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create an MPEG4-audio track
|
// create a media that contains a MPEG4-audio format
|
||||||
track := &gortsplib.TrackMPEG4Audio{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
Type: media.TypeAudio,
|
||||||
|
Formats: []format.Format{&format.MPEG4Audio{
|
||||||
|
PayloadTyp: 96,
|
||||||
Config: &mpeg4audio.Config{
|
Config: &mpeg4audio.Config{
|
||||||
Type: mpeg4audio.ObjectTypeAACLC,
|
Type: mpeg4audio.ObjectTypeAACLC,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
@@ -45,12 +49,13 @@ func main() {
|
|||||||
SizeLength: 13,
|
SizeLength: 13,
|
||||||
IndexLength: 3,
|
IndexLength: 3,
|
||||||
IndexDeltaLength: 3,
|
IndexDeltaLength: 3,
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -65,7 +70,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,13 +4,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/Opus packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -33,18 +35,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create an Opus track
|
// create a media that contains a Opus format
|
||||||
track := &gortsplib.TrackOpus{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
Type: media.TypeAudio,
|
||||||
|
Formats: []format.Format{&format.Opus{
|
||||||
|
PayloadTyp: 96,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
ChannelCount: 2,
|
ChannelCount: 2,
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -59,7 +64,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,13 +4,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/VP8 packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -34,15 +36,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create a VP8 track
|
// create a media that contains a VP8 format
|
||||||
track := &gortsplib.TrackVP8{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
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{}
|
c := gortsplib.Client{}
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -4,13 +4,15 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/VP9 packets with GStreamer
|
// 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
|
// 3. route the packets from GStreamer to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -34,15 +36,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create a VP9 track
|
// create a media that contains a VP9 format
|
||||||
track := &gortsplib.TrackVP9{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
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{}
|
c := gortsplib.Client{}
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
@@ -5,14 +5,16 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. set additional client options
|
// 1. set additional client options
|
||||||
// 2. generate RTP/H264 frames from a file with GStreamer
|
// 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
|
// 4. write the frames to the server
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -35,10 +37,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create an H264 track
|
// create a media that contains a H264 media
|
||||||
track := &gortsplib.TrackH264{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
Type: media.TypeVideo,
|
||||||
|
Formats: []format.Format{&format.H264{
|
||||||
|
PayloadTyp: 96,
|
||||||
PacketizationMode: 1,
|
PacketizationMode: 1,
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client allows to set additional client options
|
// Client allows to set additional client options
|
||||||
@@ -51,9 +56,9 @@ func main() {
|
|||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -68,7 +73,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
err = c.WritePacketRTP(0, &pkt)
|
err = c.WritePacketRTP(medi, &pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@@ -5,13 +5,15 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"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"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. generate RTP/H264 frames from a file with GStreamer
|
// 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
|
// 3. write the frames to the server for 5 seconds
|
||||||
// 4. pause for 5 seconds
|
// 4. pause for 5 seconds
|
||||||
// 5. repeat
|
// 5. repeat
|
||||||
@@ -36,16 +38,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("stream connected")
|
log.Println("stream connected")
|
||||||
|
|
||||||
// create an H264 track
|
// create a media that contains a H264 format
|
||||||
track := &gortsplib.TrackH264{
|
medi := &media.Media{
|
||||||
PayloadType: 96,
|
Type: media.TypeVideo,
|
||||||
|
Formats: []format.Format{&format.H264{
|
||||||
|
PayloadTyp: 96,
|
||||||
PacketizationMode: 1,
|
PacketizationMode: 1,
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to the server and start publishing the track
|
// connect to the server and start recording the media
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
err = c.StartPublishing("rtsp://localhost:8554/mystream",
|
err = c.StartRecording("rtsp://localhost:8554/mystream",
|
||||||
gortsplib.Tracks{track})
|
media.Medias{medi})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -62,7 +67,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route RTP packet to the server
|
// route RTP packet to the server
|
||||||
c.WritePacketRTP(0, &pkt)
|
c.WritePacketRTP(medi, &pkt)
|
||||||
|
|
||||||
// read another RTP packet from source
|
// read another RTP packet from source
|
||||||
n, _, err = pc.ReadFrom(buf)
|
n, _, err = pc.ReadFrom(buf)
|
||||||
|
@@ -3,13 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. connect to a RTSP server
|
// 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() {
|
func main() {
|
||||||
c := gortsplib.Client{}
|
c := gortsplib.Client{}
|
||||||
@@ -25,10 +25,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
tracks, _, _, err := c.Describe(u)
|
medias, _, _, err := c.Describe(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("available tracks: %v\n", tracks)
|
log.Printf("available medias: %v\n", medias)
|
||||||
}
|
}
|
||||||
|
75
examples/client-read-format-g711/main.go
Normal file
75
examples/client-read-format-g711/main.go
Normal file
@@ -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())
|
||||||
|
}
|
75
examples/client-read-format-g722/main.go
Normal file
75
examples/client-read-format-g722/main.go
Normal file
@@ -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())
|
||||||
|
}
|
@@ -8,13 +8,15 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/format"
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. connect to a RTSP server and read all tracks on a path
|
// 1. connect to a RTSP server
|
||||||
// 2. check if there's a H264 track
|
// 2. check if there's a H264 media
|
||||||
// 3. decode H264 into RGBA frames
|
// 3. decode H264 into RGBA frames
|
||||||
// 4. encode the frames into JPEG images and save them on disk
|
// 4. encode the frames into JPEG images and save them on disk
|
||||||
|
|
||||||
@@ -54,27 +56,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
// find published tracks
|
// find published medias
|
||||||
tracks, baseURL, _, err := c.Describe(u)
|
medias, baseURL, _, err := c.Describe(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the H264 track
|
// find the H264 media and format
|
||||||
track := func() *gortsplib.TrackH264 {
|
var trak *format.H264
|
||||||
for _, track := range tracks {
|
medi := medias.Find(&trak)
|
||||||
if track, ok := track.(*gortsplib.TrackH264); ok {
|
if medi == nil {
|
||||||
return track
|
panic("media not found")
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if track == nil {
|
|
||||||
panic("H264 track not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup RTP/H264->H264 decoder
|
// setup RTP/H264->H264 decoder
|
||||||
rtpDec := track.CreateDecoder()
|
rtpDec := trak.CreateDecoder()
|
||||||
|
|
||||||
// setup H264->raw frames decoder
|
// setup H264->raw frames decoder
|
||||||
h264RawDec, err := newH264Decoder()
|
h264RawDec, err := newH264Decoder()
|
||||||
@@ -84,20 +80,26 @@ func main() {
|
|||||||
defer h264RawDec.close()
|
defer h264RawDec.close()
|
||||||
|
|
||||||
// if SPS and PPS are present into the SDP, send them to the decoder
|
// if SPS and PPS are present into the SDP, send them to the decoder
|
||||||
sps := track.SafeSPS()
|
sps := trak.SafeSPS()
|
||||||
if sps != nil {
|
if sps != nil {
|
||||||
h264RawDec.decode(sps)
|
h264RawDec.decode(sps)
|
||||||
}
|
}
|
||||||
pps := track.SafePPS()
|
pps := trak.SafePPS()
|
||||||
if pps != nil {
|
if pps != nil {
|
||||||
h264RawDec.decode(pps)
|
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
|
// called when a RTP packet arrives
|
||||||
saveCount := 0
|
saveCount := 0
|
||||||
c.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) {
|
c.OnPacketRTP(medi, trak, func(pkt *rtp.Packet) {
|
||||||
// convert RTP packets into NALUs
|
// convert RTP packets into NALUs
|
||||||
nalus, _, err := rtpDec.Decode(ctx.Packet)
|
nalus, _, err := rtpDec.Decode(pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -126,10 +128,10 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// setup and read the H264 track only
|
// start playing
|
||||||
err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL)
|
_, err = c.Play(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
79
examples/client-read-format-h264-save-to-disk/main.go
Normal file
79
examples/client-read-format-h264-save-to-disk/main.go
Normal file
@@ -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())
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/h264"
|
"github.com/aler9/gortsplib/v2/pkg/h264"
|
||||||
"github.com/asticode/go-astits"
|
"github.com/asticode/go-astits"
|
||||||
)
|
)
|
||||||
|
|
@@ -3,13 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/format"
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// This example shows how to
|
||||||
// 1. connect to a RTSP server and read all tracks on a path
|
// 1. connect to a RTSP server
|
||||||
// 2. check if there's an H264 track
|
// 2. check if there's an H264 media
|
||||||
// 3. decode H264 into RGBA frames
|
// 3. decode H264 into RGBA frames
|
||||||
|
|
||||||
// This example requires the ffmpeg libraries, that can be installed in this way:
|
// This example requires the ffmpeg libraries, that can be installed in this way:
|
||||||
@@ -31,27 +33,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
// find published tracks
|
// find published medias
|
||||||
tracks, baseURL, _, err := c.Describe(u)
|
medias, baseURL, _, err := c.Describe(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the H264 track
|
// find the H264 media and format
|
||||||
track := func() *gortsplib.TrackH264 {
|
var trak *format.H264
|
||||||
for _, track := range tracks {
|
medi := medias.Find(&trak)
|
||||||
if track, ok := track.(*gortsplib.TrackH264); ok {
|
if medi == nil {
|
||||||
return track
|
panic("media not found")
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if track == nil {
|
|
||||||
panic("H264 track not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup RTP/H264->H264 decoder
|
// setup RTP/H264->H264 decoder
|
||||||
rtpDec := track.CreateDecoder()
|
rtpDec := trak.CreateDecoder()
|
||||||
|
|
||||||
// setup H264->raw frames decoder
|
// setup H264->raw frames decoder
|
||||||
h264RawDec, err := newH264Decoder()
|
h264RawDec, err := newH264Decoder()
|
||||||
@@ -61,19 +57,25 @@ func main() {
|
|||||||
defer h264RawDec.close()
|
defer h264RawDec.close()
|
||||||
|
|
||||||
// if SPS and PPS are present into the SDP, send them to the decoder
|
// if SPS and PPS are present into the SDP, send them to the decoder
|
||||||
sps := track.SafeSPS()
|
sps := trak.SafeSPS()
|
||||||
if sps != nil {
|
if sps != nil {
|
||||||
h264RawDec.decode(sps)
|
h264RawDec.decode(sps)
|
||||||
}
|
}
|
||||||
pps := track.SafePPS()
|
pps := trak.SafePPS()
|
||||||
if pps != nil {
|
if pps != nil {
|
||||||
h264RawDec.decode(pps)
|
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
|
// 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
|
// convert RTP packets into NALUs
|
||||||
nalus, _, err := rtpDec.Decode(ctx.Packet)
|
nalus, _, err := rtpDec.Decode(pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -92,10 +94,10 @@ func main() {
|
|||||||
|
|
||||||
log.Printf("decoded frame with size %v", img.Bounds().Max)
|
log.Printf("decoded frame with size %v", img.Bounds().Max)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// setup and read the H264 track only
|
// start playing
|
||||||
err = c.SetupAndPlay(gortsplib.Tracks{track}, baseURL)
|
_, err = c.Play(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
76
examples/client-read-format-h265/main.go
Normal file
76
examples/client-read-format-h265/main.go
Normal file
@@ -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())
|
||||||
|
}
|
75
examples/client-read-format-lpcm/main.go
Normal file
75
examples/client-read-format-lpcm/main.go
Normal file
@@ -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())
|
||||||
|
}
|
77
examples/client-read-format-mpeg4audio/main.go
Normal file
77
examples/client-read-format-mpeg4audio/main.go
Normal file
@@ -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())
|
||||||
|
}
|
75
examples/client-read-format-opus/main.go
Normal file
75
examples/client-read-format-opus/main.go
Normal file
@@ -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())
|
||||||
|
}
|
74
examples/client-read-format-vp8/main.go
Normal file
74
examples/client-read-format-vp8/main.go
Normal file
@@ -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())
|
||||||
|
}
|
74
examples/client-read-format-vp9/main.go
Normal file
74
examples/client-read-format-vp9/main.go
Normal file
@@ -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())
|
||||||
|
}
|
@@ -4,13 +4,17 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"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
|
// This example shows how to
|
||||||
// 1. set additional client options
|
// 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() {
|
func main() {
|
||||||
// Client allows to set additional client options
|
// Client allows to set additional client options
|
||||||
@@ -21,14 +25,6 @@ func main() {
|
|||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
// timeout of write operations
|
// timeout of write operations
|
||||||
WriteTimeout: 10 * time.Second,
|
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
|
// parse URL
|
||||||
@@ -44,14 +40,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
// find published tracks
|
// find published medias
|
||||||
tracks, baseURL, _, err := c.Describe(u)
|
medias, baseURL, _, err := c.Describe(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup and read all tracks
|
// setup all medias
|
||||||
err = c.SetupAndPlay(tracks, baseURL)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@@ -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())
|
|
||||||
}
|
|
@@ -4,27 +4,22 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"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
|
// 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
|
// 2. wait for 5 seconds
|
||||||
// 3. pause for 5 seconds
|
// 3. pause for 5 seconds
|
||||||
// 4. repeat
|
// 4. repeat
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := gortsplib.Client{
|
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse URL
|
// parse URL
|
||||||
u, err := url.Parse("rtsp://localhost:8554/mystream")
|
u, err := url.Parse("rtsp://localhost:8554/mystream")
|
||||||
@@ -39,14 +34,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
// find published tracks
|
// find published medias
|
||||||
tracks, baseURL, _, err := c.Describe(u)
|
medias, baseURL, _, err := c.Describe(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup and read all tracks
|
// setup all medias
|
||||||
err = c.SetupAndPlay(tracks, baseURL)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@@ -3,13 +3,16 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"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
|
// 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. re-publish all tracks on another path.
|
// 2. re-publish all medias on another path.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
reader := gortsplib.Client{}
|
reader := gortsplib.Client{}
|
||||||
@@ -27,30 +30,35 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
// find published tracks
|
// find published medias
|
||||||
tracks, baseURL, _, err := reader.Describe(sourceURL)
|
medias, baseURL, _, err := reader.Describe(sourceURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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{}
|
publisher := gortsplib.Client{}
|
||||||
|
err = publisher.StartRecording("rtsp://localhost:8554/mystream2", medias)
|
||||||
// connect to the server and start publishing
|
|
||||||
err = publisher.StartPublishing("rtsp://localhost:8554/mystream2", tracks)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer publisher.Close()
|
defer publisher.Close()
|
||||||
|
|
||||||
// called when a RTP packet arrives
|
// setup all medias
|
||||||
reader.OnPacketRTP = func(ctx *gortsplib.ClientOnPacketRTPCtx) {
|
err = reader.SetupAll(medias, baseURL)
|
||||||
publisher.WritePacketRTP(ctx.TrackID, ctx.Packet)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup and read all tracks
|
// read RTP packets from reader and write them to publisher
|
||||||
err = reader.SetupAndPlay(tracks, baseURL)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -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())
|
|
||||||
}
|
|
@@ -3,24 +3,19 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"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
|
// 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() {
|
func main() {
|
||||||
c := gortsplib.Client{
|
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse URL
|
// parse URL
|
||||||
u, err := url.Parse("rtsp://localhost:8554/mystream")
|
u, err := url.Parse("rtsp://localhost:8554/mystream")
|
||||||
@@ -35,14 +30,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
// find published tracks
|
// find published medias
|
||||||
tracks, baseURL, _, err := c.Describe(u)
|
medias, baseURL, _, err := c.Describe(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup and read all tracks
|
// setup all medias
|
||||||
err = c.SetupAndPlay(tracks, baseURL)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@@ -5,21 +5,25 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/pion/rtp"
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
|
||||||
"github.com/aler9/gortsplib/pkg/rtpcodecs/rtph264"
|
"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
|
// This example shows how to
|
||||||
// 1. create a RTSP server which accepts plain connections
|
// 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
|
// 2. allow a single client to publish a stream, containing a H264 media, with TCP or UDP
|
||||||
// 3. save the content of the H264 track into a file in MPEG-TS format
|
// 3. save the content of the H264 media into a file in MPEG-TS format
|
||||||
|
|
||||||
type serverHandler struct {
|
type serverHandler struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
publisher *gortsplib.ServerSession
|
publisher *gortsplib.ServerSession
|
||||||
h264TrackID int
|
media *media.Media
|
||||||
h264track *gortsplib.TrackH264
|
format *format.H264
|
||||||
rtpDec *rtph264.Decoder
|
rtpDec *rtph264.Decoder
|
||||||
mpegtsMuxer *mpegtsMuxer
|
mpegtsMuxer *mpegtsMuxer
|
||||||
}
|
}
|
||||||
@@ -50,7 +54,7 @@ func (sh *serverHandler) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionClo
|
|||||||
sh.mpegtsMuxer.close()
|
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) {
|
func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {
|
||||||
log.Printf("announce request")
|
log.Printf("announce request")
|
||||||
|
|
||||||
@@ -62,26 +66,20 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
|
|||||||
sh.mpegtsMuxer.close()
|
sh.mpegtsMuxer.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the H264 track
|
// find the H264 media and format
|
||||||
h264TrackID, h264track := func() (int, *gortsplib.TrackH264) {
|
var trak *format.H264
|
||||||
for i, track := range ctx.Tracks {
|
medi := ctx.Medias.Find(&trak)
|
||||||
if h264track, ok := track.(*gortsplib.TrackH264); ok {
|
if medi == nil {
|
||||||
return i, h264track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1, nil
|
|
||||||
}()
|
|
||||||
if h264TrackID < 0 {
|
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
StatusCode: base.StatusBadRequest,
|
StatusCode: base.StatusBadRequest,
|
||||||
}, fmt.Errorf("H264 track not found")
|
}, fmt.Errorf("H264 media not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup RTP/H264->H264 decoder
|
// setup RTP/H264->H264 decoder
|
||||||
rtpDec := h264track.CreateDecoder()
|
rtpDec := trak.CreateDecoder()
|
||||||
|
|
||||||
// setup H264->MPEGTS muxer
|
// setup H264->MPEGTS muxer
|
||||||
mpegtsMuxer, err := newMPEGTSMuxer(h264track.SafeSPS(), h264track.SafePPS())
|
mpegtsMuxer, err := newMPEGTSMuxer(trak.SafeSPS(), trak.SafePPS())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
StatusCode: base.StatusBadRequest,
|
StatusCode: base.StatusBadRequest,
|
||||||
@@ -89,7 +87,8 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
sh.publisher = ctx.Session
|
sh.publisher = ctx.Session
|
||||||
sh.h264TrackID = h264TrackID
|
sh.media = medi
|
||||||
|
sh.format = trak
|
||||||
sh.rtpDec = rtpDec
|
sh.rtpDec = rtpDec
|
||||||
sh.mpegtsMuxer = mpegtsMuxer
|
sh.mpegtsMuxer = mpegtsMuxer
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
|
|||||||
}, nil
|
}, 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) {
|
func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
log.Printf("setup request")
|
log.Printf("setup request")
|
||||||
|
|
||||||
@@ -107,31 +106,24 @@ func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.
|
|||||||
}, nil, nil
|
}, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// called after receiving a RECORD request.
|
// called when receiving a RECORD request.
|
||||||
func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
|
func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
|
||||||
log.Printf("record request")
|
log.Printf("record request")
|
||||||
|
|
||||||
return &base.Response{
|
// called when receiving a RTP packet
|
||||||
StatusCode: base.StatusOK,
|
ctx.Session.OnPacketRTP(sh.media, sh.format, func(pkt *rtp.Packet) {
|
||||||
}, nil
|
nalus, pts, err := sh.rtpDec.Decode(pkt)
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode H264 NALUs into MPEG-TS
|
// encode H264 NALUs into MPEG-TS
|
||||||
sh.mpegtsMuxer.encode(nalus, pts)
|
sh.mpegtsMuxer.encode(nalus, pts)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &base.Response{
|
||||||
|
StatusCode: base.StatusOK,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/h264"
|
"github.com/aler9/gortsplib/v2/pkg/h264"
|
||||||
"github.com/asticode/go-astits"
|
"github.com/asticode/go-astits"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -5,8 +5,12 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/pion/rtp"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// 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) {
|
func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
log.Printf("describe request")
|
log.Printf("describe request")
|
||||||
|
|
||||||
@@ -64,13 +68,13 @@ func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (
|
|||||||
}, nil, nil
|
}, 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{
|
return &base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
}, sh.stream, nil
|
}, 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) {
|
func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {
|
||||||
log.Printf("announce request")
|
log.Printf("announce request")
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create the stream and save the publisher
|
// create the stream and save the publisher
|
||||||
sh.stream = gortsplib.NewServerStream(ctx.Tracks)
|
sh.stream = gortsplib.NewServerStream(ctx.Medias)
|
||||||
sh.publisher = ctx.Session
|
sh.publisher = ctx.Session
|
||||||
|
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
@@ -92,7 +96,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
|
|||||||
}, nil
|
}, 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) {
|
func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
log.Printf("setup request")
|
log.Printf("setup request")
|
||||||
|
|
||||||
@@ -108,7 +112,7 @@ func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.
|
|||||||
}, sh.stream, nil
|
}, 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) {
|
func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
|
||||||
log.Printf("play request")
|
log.Printf("play request")
|
||||||
|
|
||||||
@@ -117,26 +121,21 @@ func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Re
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// called after receiving a RECORD request.
|
// called when receiving a RECORD request.
|
||||||
func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
|
func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
|
||||||
log.Printf("record request")
|
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{
|
return &base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
}, nil
|
}, 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() {
|
func main() {
|
||||||
// setup certificates - they can be generated with
|
// setup certificates - they can be generated with
|
||||||
// openssl genrsa -out server.key 2048
|
// openssl genrsa -out server.key 2048
|
||||||
|
@@ -4,8 +4,12 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/pion/rtp"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example shows how to
|
// 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) {
|
func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
log.Printf("describe request")
|
log.Printf("describe request")
|
||||||
|
|
||||||
@@ -63,13 +67,13 @@ func (sh *serverHandler) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx) (
|
|||||||
}, nil, nil
|
}, 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{
|
return &base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
}, sh.stream, nil
|
}, 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) {
|
func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {
|
||||||
log.Printf("announce request")
|
log.Printf("announce request")
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create the stream and save the publisher
|
// create the stream and save the publisher
|
||||||
sh.stream = gortsplib.NewServerStream(ctx.Tracks)
|
sh.stream = gortsplib.NewServerStream(ctx.Medias)
|
||||||
sh.publisher = ctx.Session
|
sh.publisher = ctx.Session
|
||||||
|
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
@@ -91,7 +95,7 @@ func (sh *serverHandler) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (
|
|||||||
}, nil
|
}, 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) {
|
func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
log.Printf("setup request")
|
log.Printf("setup request")
|
||||||
|
|
||||||
@@ -107,7 +111,7 @@ func (sh *serverHandler) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.
|
|||||||
}, sh.stream, nil
|
}, 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) {
|
func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
|
||||||
log.Printf("play request")
|
log.Printf("play request")
|
||||||
|
|
||||||
@@ -116,26 +120,21 @@ func (sh *serverHandler) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Re
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// called after receiving a RECORD request.
|
// called when receiving a RECORD request.
|
||||||
func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
|
func (sh *serverHandler) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
|
||||||
log.Printf("record request")
|
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{
|
return &base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
}, nil
|
}, 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() {
|
func main() {
|
||||||
// configure server
|
// configure server
|
||||||
s := &gortsplib.Server{
|
s := &gortsplib.Server{
|
||||||
|
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/aler9/gortsplib
|
module github.com/aler9/gortsplib/v2
|
||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
|
@@ -14,10 +14,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/rtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib"
|
"github.com/aler9/gortsplib/v2"
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"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-----
|
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
@@ -85,8 +88,6 @@ type testServerHandler struct {
|
|||||||
onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error)
|
onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error)
|
||||||
onRecord func(*gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error)
|
onRecord func(*gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error)
|
||||||
onPause func(*gortsplib.ServerHandlerOnPauseCtx) (*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)
|
onSetParameter func(*gortsplib.ServerHandlerOnSetParameterCtx) (*base.Response, error)
|
||||||
onGetParameter func(*gortsplib.ServerHandlerOnGetParameterCtx) (*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")
|
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) {
|
func (sh *testServerHandler) OnSetParameter(ctx *gortsplib.ServerHandlerOnSetParameterCtx) (*base.Response, error) {
|
||||||
if sh.onSetParameter != nil {
|
if sh.onSetParameter != nil {
|
||||||
return sh.onSetParameter(ctx)
|
return sh.onSetParameter(ctx)
|
||||||
@@ -238,7 +227,7 @@ func buildImage(image string) error {
|
|||||||
return ecmd.Run()
|
return ecmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerPublishRead(t *testing.T) {
|
func TestServerRecordRead(t *testing.T) {
|
||||||
files, err := os.ReadDir("images")
|
files, err := os.ReadDir("images")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -339,7 +328,7 @@ func TestServerPublishRead(t *testing.T) {
|
|||||||
}, fmt.Errorf("someone is already publishing")
|
}, fmt.Errorf("someone is already publishing")
|
||||||
}
|
}
|
||||||
|
|
||||||
stream = gortsplib.NewServerStream(ctx.Tracks)
|
stream = gortsplib.NewServerStream(ctx.Medias)
|
||||||
publisher = ctx.Session
|
publisher = ctx.Session
|
||||||
|
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
@@ -396,18 +385,14 @@ func TestServerPublishRead(t *testing.T) {
|
|||||||
}, fmt.Errorf("invalid query (%s)", ctx.Query)
|
}, 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{
|
return &base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
onPacketRTP: func(ctx *gortsplib.ServerHandlerOnPacketRTPCtx) {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
|
|
||||||
if ctx.Session == publisher {
|
|
||||||
stream.WritePacketRTP(ctx.TrackID, ctx.Packet)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
RTSPAddress: "localhost:8554",
|
RTSPAddress: "localhost:8554",
|
||||||
}
|
}
|
||||||
|
@@ -5,9 +5,9 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
"github.com/aler9/gortsplib/pkg/headers"
|
"github.com/aler9/gortsplib/v2/pkg/headers"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustParseURL(s string) *url.URL {
|
func mustParseURL(s string) *url.URL {
|
||||||
|
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
"github.com/aler9/gortsplib/pkg/headers"
|
"github.com/aler9/gortsplib/v2/pkg/headers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sender allows to generate credentials for a Validator.
|
// Sender allows to generate credentials for a Validator.
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSenderErrors(t *testing.T) {
|
func TestSenderErrors(t *testing.T) {
|
||||||
|
@@ -6,9 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
"github.com/aler9/gortsplib/pkg/headers"
|
"github.com/aler9/gortsplib/v2/pkg/headers"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func stringsReverseIndex(s, substr string) int {
|
func stringsReverseIndex(s, substr string) int {
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidatorErrors(t *testing.T) {
|
func TestValidatorErrors(t *testing.T) {
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustParseURL(s string) *url.URL {
|
func mustParseURL(s string) *url.URL {
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
"github.com/aler9/gortsplib/v2/pkg/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustParseURL(s string) *url.URL {
|
func mustParseURL(s string) *url.URL {
|
||||||
|
128
pkg/format/format.go
Normal file
128
pkg/format/format.go
Normal file
@@ -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
|
||||||
|
}
|
84
pkg/format/g711.go
Normal file
84
pkg/format/g711.go
Normal file
@@ -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
|
||||||
|
}
|
49
pkg/format/g711_test.go
Normal file
49
pkg/format/g711_test.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
71
pkg/format/g722.go
Normal file
71
pkg/format/g722.go
Normal file
@@ -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
|
||||||
|
}
|
30
pkg/format/g722_test.go
Normal file
30
pkg/format/g722_test.go
Normal file
@@ -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)
|
||||||
|
}
|
111
pkg/format/generic.go
Normal file
111
pkg/format/generic.go
Normal file
@@ -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:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
||||||
|
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
|
||||||
|
}
|
53
pkg/format/generic_test.go
Normal file
53
pkg/format/generic_test.go
Normal file
@@ -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)
|
||||||
|
}
|
240
pkg/format/h264.go
Normal file
240
pkg/format/h264.go
Normal file
@@ -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
|
||||||
|
}
|
72
pkg/format/h264_test.go
Normal file
72
pkg/format/h264_test.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
195
pkg/format/h265.go
Normal file
195
pkg/format/h265.go
Normal file
@@ -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
|
||||||
|
}
|
55
pkg/format/h265_test.go
Normal file
55
pkg/format/h265_test.go
Normal file
@@ -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)
|
||||||
|
}
|
42
pkg/format/jpeg.go
Normal file
42
pkg/format/jpeg.go
Normal file
@@ -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
|
||||||
|
}
|
30
pkg/format/jpeg_test.go
Normal file
30
pkg/format/jpeg_test.go
Normal file
@@ -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)
|
||||||
|
}
|
124
pkg/format/lpcm.go
Normal file
124
pkg/format/lpcm.go
Normal file
@@ -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
|
||||||
|
}
|
45
pkg/format/lpcm_test.go
Normal file
45
pkg/format/lpcm_test.go
Normal file
@@ -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)
|
||||||
|
}
|
42
pkg/format/mpeg2audio.go
Normal file
42
pkg/format/mpeg2audio.go
Normal file
@@ -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
|
||||||
|
}
|
30
pkg/format/mpeg2audio_test.go
Normal file
30
pkg/format/mpeg2audio_test.go
Normal file
@@ -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)
|
||||||
|
}
|
42
pkg/format/mpeg2video.go
Normal file
42
pkg/format/mpeg2video.go
Normal file
@@ -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
|
||||||
|
}
|
30
pkg/format/mpeg2video_test.go
Normal file
30
pkg/format/mpeg2video_test.go
Normal file
@@ -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)
|
||||||
|
}
|
182
pkg/format/mpeg4audio.go
Normal file
182
pkg/format/mpeg4audio.go
Normal file
@@ -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
|
||||||
|
}
|
63
pkg/format/mpeg4audio_test.go
Normal file
63
pkg/format/mpeg4audio_test.go
Normal file
@@ -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)
|
||||||
|
}
|
102
pkg/format/opus.go
Normal file
102
pkg/format/opus.go
Normal file
@@ -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
|
||||||
|
}
|
42
pkg/format/opus_test.go
Normal file
42
pkg/format/opus_test.go
Normal file
@@ -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)
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package gortsplib
|
package format
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -6,18 +6,17 @@ import (
|
|||||||
psdp "github.com/pion/sdp/v3"
|
psdp "github.com/pion/sdp/v3"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/mpeg4audio"
|
"github.com/aler9/gortsplib/v2/pkg/mpeg4audio"
|
||||||
"github.com/aler9/gortsplib/pkg/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTrackNewFromMediaDescription(t *testing.T) {
|
func TestNewFromMediaDescription(t *testing.T) {
|
||||||
for _, ca := range []struct {
|
for _, ca := range []struct {
|
||||||
name string
|
name string
|
||||||
md *psdp.MediaDescription
|
md *psdp.MediaDescription
|
||||||
track Track
|
format Format
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"pcma",
|
"audio g711 pcma",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -25,10 +24,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
Formats: []string{"8"},
|
Formats: []string{"8"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackG711{},
|
&G711{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pcmu",
|
"audio g711 pcmu",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -36,12 +35,12 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
Formats: []string{"0"},
|
Formats: []string{"0"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackG711{
|
&G711{
|
||||||
MULaw: true,
|
MULaw: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"g722",
|
"audio g722",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -49,10 +48,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
Formats: []string{"9"},
|
Formats: []string{"9"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackG722{},
|
&G722{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"lpcm 8",
|
"audio lpcm 8",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -66,15 +65,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackLPCM{
|
&LPCM{
|
||||||
PayloadType: 97,
|
PayloadTyp: 97,
|
||||||
BitDepth: 8,
|
BitDepth: 8,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
ChannelCount: 2,
|
ChannelCount: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"lpcm 16",
|
"audio lpcm 16",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -88,15 +87,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackLPCM{
|
&LPCM{
|
||||||
PayloadType: 97,
|
PayloadTyp: 97,
|
||||||
BitDepth: 16,
|
BitDepth: 16,
|
||||||
SampleRate: 96000,
|
SampleRate: 96000,
|
||||||
ChannelCount: 2,
|
ChannelCount: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"lpcm 24",
|
"audio lpcm 24",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -110,15 +109,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackLPCM{
|
&LPCM{
|
||||||
PayloadType: 98,
|
PayloadTyp: 98,
|
||||||
BitDepth: 24,
|
BitDepth: 24,
|
||||||
SampleRate: 44100,
|
SampleRate: 44100,
|
||||||
ChannelCount: 4,
|
ChannelCount: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"mpeg audio",
|
"audio mpeg2 audio",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -126,10 +125,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
Formats: []string{"14"},
|
Formats: []string{"14"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackMPEG2Audio{},
|
&MPEG2Audio{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aac",
|
"audio aac",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -151,8 +150,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackMPEG4Audio{
|
&MPEG4Audio{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
Config: &mpeg4audio.Config{
|
Config: &mpeg4audio.Config{
|
||||||
Type: mpeg4audio.ObjectTypeAACLC,
|
Type: mpeg4audio.ObjectTypeAACLC,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
@@ -164,7 +163,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aac vlc rtsp server",
|
"audio aac vlc rtsp server",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -182,8 +181,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackMPEG4Audio{
|
&MPEG4Audio{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
Config: &mpeg4audio.Config{
|
Config: &mpeg4audio.Config{
|
||||||
Type: mpeg4audio.ObjectTypeAACLC,
|
Type: mpeg4audio.ObjectTypeAACLC,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
@@ -195,7 +194,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aac without indexlength",
|
"audio aac without indexlength",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -217,8 +216,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackMPEG4Audio{
|
&MPEG4Audio{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
Config: &mpeg4audio.Config{
|
Config: &mpeg4audio.Config{
|
||||||
Type: mpeg4audio.ObjectTypeAACLC,
|
Type: mpeg4audio.ObjectTypeAACLC,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
@@ -230,7 +229,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"vorbis",
|
"audio vorbis",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -248,15 +247,15 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackVorbis{
|
&Vorbis{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
SampleRate: 44100,
|
SampleRate: 44100,
|
||||||
ChannelCount: 2,
|
ChannelCount: 2,
|
||||||
Configuration: []byte{0x01, 0x02, 0x03, 0x04},
|
Configuration: []byte{0x01, 0x02, 0x03, 0x04},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"opus",
|
"audio opus",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "audio",
|
Media: "audio",
|
||||||
@@ -274,14 +273,14 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackOpus{
|
&Opus{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
SampleRate: 48000,
|
SampleRate: 48000,
|
||||||
ChannelCount: 2,
|
ChannelCount: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"jpeg",
|
"video jpeg",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -289,10 +288,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
Formats: []string{"26"},
|
Formats: []string{"26"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackJPEG{},
|
&JPEG{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"mpeg video",
|
"video mpeg2 video",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -300,10 +299,10 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
Formats: []string{"32"},
|
Formats: []string{"32"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackMPEG2Video{},
|
&MPEG2Video{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"h264",
|
"video h264",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -322,8 +321,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackH264{
|
&H264{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
SPS: []byte{
|
SPS: []byte{
|
||||||
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
|
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
|
||||||
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
|
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{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -350,12 +349,12 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackH264{
|
&H264{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"h264 vlc rtsp server",
|
"video h264 vlc rtsp server",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -374,8 +373,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackH264{
|
&H264{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
SPS: []byte{
|
SPS: []byte{
|
||||||
0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50,
|
0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50,
|
||||||
0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03,
|
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{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -408,8 +407,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackH264{
|
&H264{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
SPS: []byte{
|
SPS: []byte{
|
||||||
0x67, 0x64, 0x00, 0x29, 0xac, 0x13, 0x31, 0x40,
|
0x67, 0x64, 0x00, 0x29, 0xac, 0x13, 0x31, 0x40,
|
||||||
0x78, 0x04, 0x47, 0xde, 0x03, 0xea, 0x02, 0x02,
|
0x78, 0x04, 0x47, 0xde, 0x03, 0xea, 0x02, 0x02,
|
||||||
@@ -423,7 +422,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"h265",
|
"video h265",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -443,8 +442,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackH265{
|
&H265{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
VPS: []byte{
|
VPS: []byte{
|
||||||
0x40, 0x1, 0xc, 0x1, 0xff, 0xff, 0x1, 0x60,
|
0x40, 0x1, 0xc, 0x1, 0xff, 0xff, 0x1, 0x60,
|
||||||
0x0, 0x0, 0x3, 0x0, 0x90, 0x0, 0x0, 0x3,
|
0x0, 0x0, 0x3, 0x0, 0x90, 0x0, 0x0, 0x3,
|
||||||
@@ -465,7 +464,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"vp8",
|
"video vp8",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -483,8 +482,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackVP8{
|
&VP8{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
MaxFR: func() *int {
|
MaxFR: func() *int {
|
||||||
v := 123
|
v := 123
|
||||||
return &v
|
return &v
|
||||||
@@ -496,7 +495,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"vp9",
|
"video vp9",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "video",
|
||||||
@@ -514,8 +513,8 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&TrackVP9{
|
&VP9{
|
||||||
PayloadType: 96,
|
PayloadTyp: 96,
|
||||||
MaxFR: func() *int {
|
MaxFR: func() *int {
|
||||||
v := 123
|
v := 123
|
||||||
return &v
|
return &v
|
||||||
@@ -531,74 +530,58 @@ func TestTrackNewFromMediaDescription(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"multiple formats",
|
"application",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: "video",
|
Media: "application",
|
||||||
Protos: []string{"RTP", "AVP"},
|
Protos: []string{"RTP", "AVP"},
|
||||||
Formats: []string{"96", "98"},
|
Formats: []string{"98"},
|
||||||
},
|
},
|
||||||
Attributes: []psdp.Attribute{
|
Attributes: []psdp.Attribute{
|
||||||
{
|
{
|
||||||
Key: "rtpmap",
|
Key: "rtpmap",
|
||||||
Value: "96 H264/90000",
|
Value: "98 MetaData/80000",
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "rtpmap",
|
|
||||||
Value: "98 MetaData",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Key: "rtcp-mux",
|
Key: "rtcp-mux",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Key: "fmtp",
|
|
||||||
Value: "96 packetization-mode=1;profile-level-id=4d002a;" +
|
|
||||||
"sprop-parameter-sets=Z00AKp2oHgCJ+WbgICAgQA==,aO48gA==",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&Generic{
|
||||||
|
PayloadTyp: 98,
|
||||||
|
RTPMap: "MetaData/80000",
|
||||||
|
ClockRat: 80000,
|
||||||
},
|
},
|
||||||
&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,
|
"application without clock rate",
|
||||||
RTPMap: "MetaData",
|
&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) {
|
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.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 {
|
for _, ca := range []struct {
|
||||||
name string
|
name string
|
||||||
md *psdp.MediaDescription
|
md *psdp.MediaDescription
|
||||||
err string
|
err string
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
"no formats",
|
|
||||||
&psdp.MediaDescription{
|
|
||||||
MediaName: psdp.MediaName{
|
|
||||||
Media: "audio",
|
|
||||||
Protos: []string{"RTP", "AVP"},
|
|
||||||
Formats: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no media formats found",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"aac missing fmtp",
|
"aac missing fmtp",
|
||||||
&psdp.MediaDescription{
|
&psdp.MediaDescription{
|
||||||
@@ -635,7 +618,7 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"invalid fmtp (96)",
|
"fmtp attribute is missing",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aac fmtp without key",
|
"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",
|
"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",
|
"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",
|
"opus invalid 1",
|
||||||
@@ -795,126 +778,8 @@ func TestTrackNewFromMediaDescriptionErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(ca.name, func(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)
|
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")
|
|
||||||
}
|
|
108
pkg/format/vorbis.go
Normal file
108
pkg/format/vorbis.go
Normal file
@@ -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
|
||||||
|
}
|
45
pkg/format/vorbis_test.go
Normal file
45
pkg/format/vorbis_test.go
Normal file
@@ -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)
|
||||||
|
}
|
110
pkg/format/vp8.go
Normal file
110
pkg/format/vp8.go
Normal file
@@ -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
|
||||||
|
}
|
44
pkg/format/vp8_test.go
Normal file
44
pkg/format/vp8_test.go
Normal file
@@ -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)
|
||||||
|
}
|
123
pkg/format/vp9.go
Normal file
123
pkg/format/vp9.go
Normal file
@@ -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
|
||||||
|
}
|
48
pkg/format/vp9_test.go
Normal file
48
pkg/format/vp9_test.go
Normal file
@@ -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)
|
||||||
|
}
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/bits"
|
"github.com/aler9/gortsplib/v2/pkg/bits"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPOC(buf []byte, sps *SPS) (uint32, error) {
|
func getPOC(buf []byte, sps *SPS) (uint32, error) {
|
||||||
|
@@ -3,7 +3,7 @@ package h264
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"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) {
|
func readScalingList(buf []byte, pos *int, size int) ([]int32, bool, error) {
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthMethod is an authentication method.
|
// AuthMethod is an authentication method.
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
var casesAuthenticate = []struct {
|
var casesAuthenticate = []struct {
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authorization is an Authorization header.
|
// Authorization is an Authorization header.
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/pkg/base"
|
"github.com/aler9/gortsplib/v2/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
var casesAuthorization = []struct {
|
var casesAuthorization = []struct {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user