From f98c9c59ca572714ec16a8b80096cc1098382489 Mon Sep 17 00:00:00 2001 From: eh Date: Tue, 17 Mar 2026 14:14:04 -0400 Subject: [PATCH] rtsp: support unwrapping MPEG-TS tracks (#5476) this allows to use MPEG-TS tracks with other protocols and with the recording system. --------- Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com> --- api/openapi.yaml | 2 + docs/2-publish/02-srt-clients.md | 2 +- docs/2-publish/06-rtsp-clients.md | 19 ++- docs/2-publish/07-rtsp-cameras-and-servers.md | 2 +- docs/2-publish/08-rtmp-clients.md | 2 +- docs/2-publish/15-ffmpeg.md | 2 +- docs/2-publish/16-gstreamer.md | 4 +- docs/3-read/02-srt.md | 2 +- docs/3-read/04-rtsp.md | 2 +- docs/3-read/05-rtmp.md | 2 +- docs/3-read/08-gstreamer.md | 2 +- docs/3-read/09-vlc.md | 2 +- docs/4-other/03-authentication.md | 2 +- docs/4-other/21-srt-specific-features.md | 2 +- docs/4-other/22-webrtc-specific-features.md | 2 +- docs/4-other/23-rtsp-specific-features.md | 6 +- docs/4-other/24-rtmp-specific-features.md | 2 +- internal/conf/path.go | 1 + internal/servers/rtsp/mpegts_demuxer.go | 124 ++++++++++++++ internal/servers/rtsp/server_test.go | 151 ++++++++++++++++++ internal/servers/rtsp/session.go | 60 ++++++- mediamtx.yml | 5 + 22 files changed, 380 insertions(+), 18 deletions(-) create mode 100644 internal/servers/rtsp/mpegts_demuxer.go diff --git a/api/openapi.yaml b/api/openapi.yaml index 2fa803e3..b00daafa 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -717,6 +717,8 @@ components: deprecated: true srtPublishPassphrase: type: string + rtspDemuxMpegts: + type: boolean # RTSP source rtspTransport: diff --git a/docs/2-publish/02-srt-clients.md b/docs/2-publish/02-srt-clients.md index 955d1c57..82e9a719 100644 --- a/docs/2-publish/02-srt-clients.md +++ b/docs/2-publish/02-srt-clients.md @@ -8,7 +8,7 @@ srt://localhost:8890?streamid=publish:mystream&pkt_size=1316 Replace `mystream` with any name you want. The resulting stream will be available on path `/mystream`. -If you need to use the standard stream ID syntax instead of the custom one in use by this server, see [Standard stream ID syntax](../4-other/21-srt-specific-features.md#standard-stream-id-syntax). +If you need to use the standard stream ID syntax instead of the custom one in use by this server, read [Standard stream ID syntax](../4-other/21-srt-specific-features.md#standard-stream-id-syntax). If you want to publish a stream by using a client in listening mode (i.e. with `mode=listener` appended to the URL), read the next section. diff --git a/docs/2-publish/06-rtsp-clients.md b/docs/2-publish/06-rtsp-clients.md index 3b41426b..9e806b40 100644 --- a/docs/2-publish/06-rtsp-clients.md +++ b/docs/2-publish/06-rtsp-clients.md @@ -1,6 +1,6 @@ # RTSP clients -RTSP is a protocol that allows to publish and read streams. It supports several underlying transport protocols and encryption (see [RTSP-specific features](../4-other/23-rtsp-specific-features.md)). In order to publish a stream to the server with the RTSP protocol, use this URL: +RTSP is a protocol that allows to publish and read streams. It supports several underlying transport protocols and encryption. In order to publish a stream to the server with the RTSP protocol, use this URL: ``` rtsp://localhost:8554/mystream @@ -9,3 +9,20 @@ rtsp://localhost:8554/mystream The resulting stream will be available on path `/mystream`. Some clients that can publish with RTSP are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md), [OBS Studio](17-obs-studio.md), [Python and OpenCV](18-python-opencv.md). + +Advanced RTSP features and settings are described in [RTSP-specific features](../4-other/23-rtsp-specific-features.md). + +## MPEG-TS inside RTSP + +Some RTSP clients encode tracks with MPEG-TS before sending them to the server, causing the server to see a single "MPEG-TS" track, and preventing track conversion from a protocol to another. + +It's possible to automatically demux these MPEG-TS-encoded streams, by toggling `rtspDemuxMpegts`: + +```yml +pathDefaults: + # Demux MPEG-TS over RTSP into elementary streams. + # When enabled, RTSP publishers sending MP2T/90000 will be demultiplexed + # and their elementary streams (H.264, H.265, AAC, etc.) exposed as native tracks. + # This allows HLS, WebRTC, and other outputs to work transparently with MPEG-TS sources. + rtspDemuxMpegts: true +``` diff --git a/docs/2-publish/07-rtsp-cameras-and-servers.md b/docs/2-publish/07-rtsp-cameras-and-servers.md index c2503273..245401f0 100644 --- a/docs/2-publish/07-rtsp-cameras-and-servers.md +++ b/docs/2-publish/07-rtsp-cameras-and-servers.md @@ -44,4 +44,4 @@ paths: All available parameters are listed in the [configuration file](../5-references/1-configuration-file.md). -Advanced RTSP features are described in [RTSP-specific features](../4-other/23-rtsp-specific-features.md). +Advanced RTSP features and settings are described in [RTSP-specific features](../4-other/23-rtsp-specific-features.md). diff --git a/docs/2-publish/08-rtmp-clients.md b/docs/2-publish/08-rtmp-clients.md index 90fb07e8..12fdf571 100644 --- a/docs/2-publish/08-rtmp-clients.md +++ b/docs/2-publish/08-rtmp-clients.md @@ -1,6 +1,6 @@ # RTMP clients -RTMP is a protocol that allows to read and publish streams. It supports encryption, see [RTMP-specific features](../4-other/24-rtmp-specific-features.md). Streams can be published to the server by using the URL: +RTMP is a protocol that allows to read and publish streams. It supports encryption, read [RTMP-specific features](../4-other/24-rtmp-specific-features.md). Streams can be published to the server by using the URL: ``` rtmp://localhost/mystream diff --git a/docs/2-publish/15-ffmpeg.md b/docs/2-publish/15-ffmpeg.md index 759b0d7c..0ceefc4e 100644 --- a/docs/2-publish/15-ffmpeg.md +++ b/docs/2-publish/15-ffmpeg.md @@ -36,7 +36,7 @@ ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \ ## FFmpeg and RTP over UDP -In _MediaMTX_ configuration, add a path with `source: udp+rtp://238.0.0.1:1234` and a valid `rtpSDP` (see [RTP](12-rtp.md)). Then: +In _MediaMTX_ configuration, add a path with `source: udp+rtp://238.0.0.1:1234` and a valid `rtpSDP` (read [RTP](12-rtp.md)). Then: ```sh ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \ diff --git a/docs/2-publish/16-gstreamer.md b/docs/2-publish/16-gstreamer.md index 18dc7927..344e8697 100644 --- a/docs/2-publish/16-gstreamer.md +++ b/docs/2-publish/16-gstreamer.md @@ -20,7 +20,7 @@ d.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream The resulting stream will be available on path `/mystream`. -For advanced options, see [RTSP-specific features](../4-other/23-rtsp-specific-features.md). +For advanced options, read [RTSP-specific features](../4-other/23-rtsp-specific-features.md). ## GStreamer and RTMP @@ -38,7 +38,7 @@ videotestsrc ! video/x-raw,width=1280,height=720,format=I420 ! x264enc speed-pre audiotestsrc ! audioconvert ! avenc_aac ! mux. ``` -For advanced options, see [RTSP-specific features](../4-other/23-rtsp-specific-features.md). +For advanced options, read [RTSP-specific features](../4-other/23-rtsp-specific-features.md). ## GStreamer and WebRTC diff --git a/docs/3-read/02-srt.md b/docs/3-read/02-srt.md index 2603b27b..088279eb 100644 --- a/docs/3-read/02-srt.md +++ b/docs/3-read/02-srt.md @@ -8,6 +8,6 @@ srt://localhost:8890?streamid=read:mystream Replace `mystream` with the path name. -If you need to use the standard stream ID syntax instead of the custom one in use by this server, see [Standard stream ID syntax](../4-other/21-srt-specific-features.md#standard-stream-id-syntax). +If you need to use the standard stream ID syntax instead of the custom one in use by this server, read [Standard stream ID syntax](../4-other/21-srt-specific-features.md#standard-stream-id-syntax). Some clients that can read with SRT are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md) and [VLC](09-vlc.md). diff --git a/docs/3-read/04-rtsp.md b/docs/3-read/04-rtsp.md index 7afb8c1d..9c862df7 100644 --- a/docs/3-read/04-rtsp.md +++ b/docs/3-read/04-rtsp.md @@ -1,6 +1,6 @@ # RTSP clients -RTSP is a protocol that allows to publish and read streams. It supports several underlying transport protocols and encryption (see [RTSP-specific features](../4-other/23-rtsp-specific-features.md)). In order to read a stream with the RTSP protocol, use this URL: +RTSP is a protocol that allows to publish and read streams. It supports several underlying transport protocols and encryption (read [RTSP-specific features](../4-other/23-rtsp-specific-features.md)). In order to read a stream with the RTSP protocol, use this URL: ``` rtsp://localhost:8554/mystream diff --git a/docs/3-read/05-rtmp.md b/docs/3-read/05-rtmp.md index fb957f27..3db3efb0 100644 --- a/docs/3-read/05-rtmp.md +++ b/docs/3-read/05-rtmp.md @@ -1,6 +1,6 @@ # RTMP clients -RTMP is a protocol that allows to read and publish streams. It supports encryption, see [RTMP-specific features](../4-other/24-rtmp-specific-features.md). Streams can be read from the server by using the URL: +RTMP is a protocol that allows to read and publish streams. It supports encryption, read [RTMP-specific features](../4-other/24-rtmp-specific-features.md). Streams can be read from the server by using the URL: ``` rtmp://localhost/mystream diff --git a/docs/3-read/08-gstreamer.md b/docs/3-read/08-gstreamer.md index 76c24502..c1ba0868 100644 --- a/docs/3-read/08-gstreamer.md +++ b/docs/3-read/08-gstreamer.md @@ -8,7 +8,7 @@ GStreamer can read a stream from the server in several ways. The recommended one gst-launch-1.0 rtspsrc location=rtsp://127.0.0.1:8554/mystream latency=0 ! decodebin ! autovideosink ``` -For advanced options, see [RTSP-specific features](../4-other/23-rtsp-specific-features.md). +For advanced options, read [RTSP-specific features](../4-other/23-rtsp-specific-features.md). ## GStreamer and WebRTC diff --git a/docs/3-read/09-vlc.md b/docs/3-read/09-vlc.md index a78563b5..9eac9add 100644 --- a/docs/3-read/09-vlc.md +++ b/docs/3-read/09-vlc.md @@ -8,7 +8,7 @@ vlc --network-caching=50 rtsp://localhost:8554/mystream ## RTSP and Ubuntu compatibility -The VLC shipped with Ubuntu 21.10 doesn't support playing RTSP due to a license issue (see [here](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=982299) and [here](https://stackoverflow.com/questions/69766748/cvlc-cannot-play-rtsp-omxplayer-instead-can)). To fix the issue, remove the default VLC instance and install the snap version: +The VLC shipped with Ubuntu 21.10 doesn't support playing RTSP due to a license issue (read [here](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=982299) and [here](https://stackoverflow.com/questions/69766748/cvlc-cannot-play-rtsp-omxplayer-instead-can)). To fix the issue, remove the default VLC instance and install the snap version: ```sh sudo apt purge -y vlc diff --git a/docs/4-other/03-authentication.md b/docs/4-other/03-authentication.md index 7e064d27..14ddc923 100644 --- a/docs/4-other/03-authentication.md +++ b/docs/4-other/03-authentication.md @@ -297,7 +297,7 @@ Username and password can be passed through the `Authorization: Basic` HTTP head Authorization: Basic base64(user:pass) ``` -When using a web browser, a dialog is first shown to users, asking for credentials, and then the header is automatically inserted into every request. If you need to automatically fill credentials from a parent web page, see [Embed streams in a website](14-embed-streams-in-a-website.md). +When using a web browser, a dialog is first shown to users, asking for credentials, and then the header is automatically inserted into every request. If you need to automatically fill credentials from a parent web page, read [Embed streams in a website](14-embed-streams-in-a-website.md). If the `Authorization: Basic` header cannot be used (for instance, in software like OBS Studio, which only allows to provide a "Bearer Token"), credentials can be passed through the `Authorization: Bearer` header (i.e. the "Bearer Token" in OBS), where the value is the concatenation of username and password, separated by a colon: diff --git a/docs/4-other/21-srt-specific-features.md b/docs/4-other/21-srt-specific-features.md index 6ea55eb2..74091cbe 100644 --- a/docs/4-other/21-srt-specific-features.md +++ b/docs/4-other/21-srt-specific-features.md @@ -1,6 +1,6 @@ # SRT-specific features -SRT is a protocol that can be used for publishing and reading streams. Regarding specific tasks, see [Publish](../2-publish/02-srt-clients.md) and [Read](../3-read/02-srt.md). Features in this page are shared among both tasks. +SRT is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/02-srt-clients.md) and [Read](../3-read/02-srt.md). Features in this page are shared among both tasks. ## Standard stream ID syntax diff --git a/docs/4-other/22-webrtc-specific-features.md b/docs/4-other/22-webrtc-specific-features.md index ef248580..36e5e16b 100644 --- a/docs/4-other/22-webrtc-specific-features.md +++ b/docs/4-other/22-webrtc-specific-features.md @@ -1,6 +1,6 @@ # WebRTC-specific features -WebRTC is a protocol that can be used for publishing and reading streams. Regarding specific tasks, see [Publish](../2-publish/04-webrtc-clients.md) and [Read](../3-read/03-webrtc.md). Features in this page are shared among both tasks. +WebRTC is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/04-webrtc-clients.md) and [Read](../3-read/03-webrtc.md). Features in this page are shared among both tasks. ## Codec support in browsers diff --git a/docs/4-other/23-rtsp-specific-features.md b/docs/4-other/23-rtsp-specific-features.md index 73ed973f..89366f84 100644 --- a/docs/4-other/23-rtsp-specific-features.md +++ b/docs/4-other/23-rtsp-specific-features.md @@ -1,6 +1,6 @@ # RTSP-specific features -RTSP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, see [Publish](../2-publish/06-rtsp-clients.md) and [Read](../3-read/04-rtsp.md). Features in this page are shared among both tasks. +RTSP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/06-rtsp-clients.md) and [Read](../3-read/04-rtsp.md). Features in this page are shared among both tasks. ## Transport protocols @@ -104,3 +104,7 @@ paths: ``` There are also the `rtsp+https`, `rtsp+ws`, `rtsp+wss` schemes to handle any combination. + +## MPEG-TS inside RTSP + +read [MPEG-TS inside RTSP](../2-publish/06-rtsp-clients.md#mpeg-ts-inside-rtsp) in the "Publish with RTSP clients" page. diff --git a/docs/4-other/24-rtmp-specific-features.md b/docs/4-other/24-rtmp-specific-features.md index eebd0e5b..f1cd1bf2 100644 --- a/docs/4-other/24-rtmp-specific-features.md +++ b/docs/4-other/24-rtmp-specific-features.md @@ -1,6 +1,6 @@ # RTMP-specific features -RTMP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, see [Publish](../2-publish/08-rtmp-clients.md) and [Read](../3-read/05-rtmp.md). Features in this page are shared among both tasks. +RTMP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/08-rtmp-clients.md) and [Read](../3-read/05-rtmp.md). Features in this page are shared among both tasks. ## Encryption diff --git a/internal/conf/path.go b/internal/conf/path.go index fb652c8d..5c4fbe32 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -210,6 +210,7 @@ type Path struct { OverridePublisher bool `json:"overridePublisher"` DisablePublisherOverride *bool `json:"disablePublisherOverride,omitempty" deprecated:"true"` SRTPublishPassphrase string `json:"srtPublishPassphrase"` + RTSPDemuxMpegts bool `json:"rtspDemuxMpegts"` // RTSP source RTSPTransport RTSPTransport `json:"rtspTransport"` diff --git a/internal/servers/rtsp/mpegts_demuxer.go b/internal/servers/rtsp/mpegts_demuxer.go new file mode 100644 index 00000000..a788047d --- /dev/null +++ b/internal/servers/rtsp/mpegts_demuxer.go @@ -0,0 +1,124 @@ +package rtsp + +import ( + "errors" + "fmt" + "io" + + "github.com/bluenviron/gortsplib/v5/pkg/description" + "github.com/bluenviron/gortsplib/v5/pkg/format" + "github.com/pion/rtp" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/defs" + "github.com/bluenviron/mediamtx/internal/errordumper" + "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/protocols/mpegts" + "github.com/bluenviron/mediamtx/internal/stream" +) + +// mpegtsDemuxer demuxes an MPEG-TS stream received via RTP into component streams. +type mpegtsDemuxer struct { + session *session + pathManager serverPathManager + pathConf *conf.Path + mpegtsMedia *description.Media + mpegtsFormat *format.MPEGTS + decodeErrors *errordumper.Dumper + pathName string + query string + + pipeWriter *io.PipeWriter +} + +func (d *mpegtsDemuxer) initialize() error { + decoder, err := d.mpegtsFormat.CreateDecoder() + if err != nil { + return fmt.Errorf("failed to create MPEG-TS decoder: %w", err) + } + + pr, pw := io.Pipe() + d.pipeWriter = pw + + d.session.rsession.OnPacketRTP(d.mpegtsMedia, d.mpegtsFormat, func(pkt *rtp.Packet) { + tsData, decErr := decoder.Decode(pkt) + if decErr != nil { + d.decodeErrors.Add(decErr) + return + } + + for _, data := range tsData { + _, err = pw.Write(data) + if err != nil { + d.session.Log(logger.Warn, "demuxer pipe write error: %v", err) + return + } + } + }) + + go d.run(pr) + + return nil +} + +func (d *mpegtsDemuxer) close() { + d.pipeWriter.CloseWithError(io.EOF) +} + +func (d *mpegtsDemuxer) run(pr *io.PipeReader) { + err := d.doRun(pr) + if err != nil { + d.session.Log(logger.Error, "MPEG-TS demuxer error: %v", err) + d.session.Close() + } +} + +func (d *mpegtsDemuxer) doRun(pr *io.PipeReader) error { + r := &mpegts.EnhancedReader{R: pr} + err := r.Initialize() + if err != nil { + return fmt.Errorf("failed to initialize MPEG-TS reader: %w", err) + } + + r.OnDecodeError(func(err error) { + d.decodeErrors.Add(err) + }) + + var subStream *stream.SubStream + + medias, err := mpegts.ToStream(r, &subStream, d.session) + if err != nil { + return fmt.Errorf("failed to map MPEG-TS to stream: %w", err) + } + + res, err := d.pathManager.AddPublisher(defs.PathAddPublisherReq{ + Author: d.session, + Desc: &description.Session{Medias: medias}, + UseRTPPackets: false, + ReplaceNTP: true, + ConfToCompare: d.pathConf, + AccessRequest: defs.PathAccessRequest{ + Name: d.pathName, + Query: d.query, + Publish: true, + SkipAuth: true, + }, + }) + if err != nil { + return fmt.Errorf("failed to add publisher: %w", err) + } + + defer res.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: d.session}) + + subStream = res.SubStream + + for { + err = r.Read() + if err != nil { + if !errors.Is(err, io.EOF) { + return err + } + return nil + } + } +} diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go index a524b0b9..9c84ebda 100644 --- a/internal/servers/rtsp/server_test.go +++ b/internal/servers/rtsp/server_test.go @@ -1,6 +1,8 @@ package rtsp import ( + "bufio" + "bytes" "fmt" "sync/atomic" "testing" @@ -11,6 +13,8 @@ import ( "github.com/bluenviron/gortsplib/v5/pkg/base" "github.com/bluenviron/gortsplib/v5/pkg/description" "github.com/bluenviron/gortsplib/v5/pkg/format" + mpegts "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts" + tscodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs" "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" @@ -206,6 +210,153 @@ func TestServerPublish(t *testing.T) { } } +func TestServerPublishMPEGTS(t *testing.T) { + var strm *stream.Stream + var reader *stream.Reader + defer func() { + if strm != nil && reader != nil { + strm.RemoveReader(reader) + } + }() + + dataReceived := make(chan struct{}) + + pathConf := &conf.Path{RTSPDemuxMpegts: true} + + pathManager := &test.PathManager{ + FindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) { + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "param=value", req.AccessRequest.Query) + return &defs.PathFindPathConfRes{Conf: pathConf}, nil + }, + AddPublisherImpl: func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) { + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "param=value", req.AccessRequest.Query) + require.True(t, req.AccessRequest.SkipAuth) + require.False(t, req.UseRTPPackets) + require.True(t, req.ReplaceNTP) + require.Same(t, pathConf, req.ConfToCompare) + require.Equal(t, &description.Session{Medias: []*description.Media{{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + }}, + }}}, req.Desc) + + strm = &stream.Stream{ + Desc: req.Desc, + WriteQueueSize: 512, + RTPMaxPayloadSize: 1450, + Parent: test.NilLogger, + } + err := strm.Initialize() + require.NoError(t, err) + + subStream := &stream.SubStream{ + Stream: strm, + UseRTPPackets: false, + } + err = subStream.Initialize() + require.NoError(t, err) + + reader = &stream.Reader{Parent: test.NilLogger} + n := 0 + + reader.OnData( + strm.Desc.Medias[0], + strm.Desc.Medias[0].Formats[0], + func(u *unit.Unit) error { + if n == 0 { + require.Equal(t, unit.PayloadH264{ + test.FormatH264.SPS, + test.FormatH264.PPS, + {5, 1}, + }, u.Payload) + close(dataReceived) + } + n++ + return nil + }) + + strm.AddReader(reader) + + return &defs.PathAddPublisherRes{Path: &dummyPath{}, SubStream: subStream}, nil + }, + } + + s := &Server{ + Address: "127.0.0.1:8557", + ReadTimeout: conf.Duration(10 * time.Second), + WriteTimeout: conf.Duration(10 * time.Second), + WriteQueueSize: 512, + Transports: conf.RTSPTransports{gortsplib.ProtocolTCP: {}}, + PathManager: pathManager, + Parent: test.NilLogger, + } + err := s.Initialize() + require.NoError(t, err) + defer s.Close() + + source := gortsplib.Client{} + + media0 := &description.Media{ + Type: description.MediaTypeApplication, + Formats: []format.Format{&format.MPEGTS{}}, + } + + err = source.StartRecording( + "rtsp://127.0.0.1:8557/teststream?param=value", + &description.Session{Medias: []*description.Media{media0}}) + require.NoError(t, err) + defer source.Close() + + track := &mpegts.Track{Codec: &tscodecs.H264{}} + + var buf bytes.Buffer + bw := bufio.NewWriter(&buf) + w := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}} + err = w.Initialize() + require.NoError(t, err) + + // the MPEG-TS muxer needs two PES packets in order to write the first one + err = w.WriteH264(track, 0, 0, [][]byte{ + test.FormatH264.SPS, + test.FormatH264.PPS, + {5, 1}, + }) + require.NoError(t, err) + + err = w.WriteH264(track, 0, 0, [][]byte{{5, 2}}) + require.NoError(t, err) + + err = bw.Flush() + require.NoError(t, err) + + raw := buf.Bytes() + require.NotEmpty(t, raw) + require.Zero(t, len(raw)%188) + + tsPackets := make([][]byte, 0, len(raw)/188) + for len(raw) > 0 { + tsPackets = append(tsPackets, raw[:188:188]) + raw = raw[188:] + } + + encoder, err := media0.Formats[0].(*format.MPEGTS).CreateEncoder() + require.NoError(t, err) + + rtpPackets, err := encoder.Encode(tsPackets) + require.NoError(t, err) + + for _, pkt := range rtpPackets { + err = source.WritePacketRTP(media0, pkt) + require.NoError(t, err) + } + + <-dataReceived +} + func TestServerRead(t *testing.T) { for _, ca := range []string{"basic", "digest", "basic+digest"} { t.Run(ca, func(t *testing.T) { diff --git a/internal/servers/rtsp/session.go b/internal/servers/rtsp/session.go index d799f005..8212e9ce 100644 --- a/internal/servers/rtsp/session.go +++ b/internal/servers/rtsp/session.go @@ -12,6 +12,8 @@ import ( "github.com/bluenviron/gortsplib/v5" rtspauth "github.com/bluenviron/gortsplib/v5/pkg/auth" "github.com/bluenviron/gortsplib/v5/pkg/base" + "github.com/bluenviron/gortsplib/v5/pkg/description" + "github.com/bluenviron/gortsplib/v5/pkg/format" "github.com/bluenviron/gortsplib/v5/pkg/headers" "github.com/google/uuid" @@ -37,6 +39,19 @@ func profileLabel(p headers.TransportProfile) string { return "unknown" } +func findSingleMPEGTSFormat(desc *description.Session) (*description.Media, *format.MPEGTS) { + if len(desc.Medias) != 1 || len(desc.Medias[0].Formats) != 1 { + return nil, nil + } + + forma := desc.Medias[0].Formats[0] + if forma, ok := forma.(*format.MPEGTS); ok { + return desc.Medias[0], forma + } + + return nil, nil +} + type sessionParent interface { logger.Writer getConnByRConnUnsafe(rconn *gortsplib.ServerConn) *conn @@ -64,6 +79,7 @@ type session struct { outboundRTPPacketsDiscarded *counterdumper.Dumper mutex sync.RWMutex user string + mpegtsDemuxer *mpegtsDemuxer } func (s *session) initialize() { @@ -135,12 +151,18 @@ func (s *session) onClose(err error) { s.onUnreadHook() } + if s.mpegtsDemuxer != nil { + s.mpegtsDemuxer.close() + } + switch s.rsession.State() { case gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay: s.path.RemoveReader(defs.PathRemoveReaderReq{Author: s}) case gortsplib.ServerSessionStateRecord: - s.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s}) + if s.path != nil { + s.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s}) + } } s.path = nil @@ -328,6 +350,34 @@ func (s *session) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, e // onRecord is called by rtspServer. func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) { + if s.pathConf.RTSPDemuxMpegts { + mpegtsMedia, mpegtsFormat := findSingleMPEGTSFormat(s.rsession.AnnouncedDescription()) + if mpegtsFormat != nil { + s.Log(logger.Info, "MPEG-TS demux mode enabled, starting demuxer...") + + s.mpegtsDemuxer = &mpegtsDemuxer{ + session: s, + pathManager: s.pathManager, + pathConf: s.pathConf, + mpegtsMedia: mpegtsMedia, + mpegtsFormat: mpegtsFormat, + decodeErrors: s.inboundRTPPacketsInError, + pathName: s.rsession.Path()[1:], + query: s.rsession.Query(), + } + err := s.mpegtsDemuxer.initialize() + if err != nil { + return &base.Response{ + StatusCode: base.StatusInternalServerError, + }, err + } + + return &base.Response{ + StatusCode: base.StatusOK, + }, nil + } + } + res, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{ Author: s, Desc: s.rsession.AnnouncedDescription(), @@ -364,6 +414,14 @@ func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Respons // onPause is called by rtspServer. func (s *session) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) { + // we can't close mpegtsDemuxer during pause because OnPacketRTP() is paused after onPause(), + // therefore a call to pipeWriter.CloseWithError() would cause a race condition. + if s.mpegtsDemuxer != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, fmt.Errorf("cannot pause in MPEG-TS demux mode") + } + switch s.rsession.State() { case gortsplib.ServerSessionStatePlay: s.onUnreadHook() diff --git a/mediamtx.yml b/mediamtx.yml index 8e27adf3..0aeaa442 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -548,6 +548,11 @@ pathDefaults: overridePublisher: true # SRT encryption passphrase required to publish to this path. srtPublishPassphrase: + # Demux MPEG-TS over RTSP into elementary streams. + # When enabled, RTSP publishers sending MP2T/90000 will be demultiplexed + # and their elementary streams (H.264, H.265, AAC, etc.) exposed as native tracks. + # This allows HLS, WebRTC, and other outputs to work transparently with MPEG-TS sources. + rtspDemuxMpegts: false ############################################### # Default path settings -> RTSP source (when source is a RTSP or a RTSPS URL)