support reading and publishing with Media-over-QUIC (#5815)

Media-over-QUIC is a streaming protocol built upon cutting edge
protocols (QUIC, HTTP3) and browser APIs (WebTransport, WebCodecs).
It's slightly faster than WebRTC, has an advanced data recovery
mechanism (placed at the frame level and not at the packet level), it
supports additional codecs (FLAC) and is less complicated to route.
This commit is contained in:
Alessandro Ros
2026-06-02 23:04:24 +02:00
committed by GitHub
parent 1dcdd09538
commit b5b63d02fc
117 changed files with 8515 additions and 188 deletions
+4
View File
@@ -6,3 +6,7 @@
/internal/core/VERSION
/internal/servers/hls/hls.min.js
/internal/staticsources/rpicamera/mtxrpicam_*/
/auto.crt
/auto.key
/internal/core/auto.crt
/internal/core/auto.key
+4
View File
@@ -5,3 +5,7 @@
/internal/core/VERSION
/internal/servers/hls/hls.min.js
/internal/staticsources/rpicamera/mtxrpicam_*/
/auto.crt
/auto.key
/internal/core/auto.crt
/internal/core/auto.key
+2 -2
View File
@@ -28,8 +28,8 @@ _MediaMTX_ is a ready-to-use and zero-dependency real-time media server and medi
<h3>Features</h3>
- [Publish](https://mediamtx.org/docs/features/publish) live streams to the server with SRT, WebRTC, RTSP, RTMP, HLS, MPEG-TS, RTP, using FFmpeg, GStreamer, OBS Studio, Python , Golang, Unity, web browsers, Raspberry Pi Cameras and more.
- [Read](https://mediamtx.org/docs/features/read) live streams from the server with SRT, WebRTC, RTSP, RTMP, HLS, using FFmpeg, GStreamer, VLC, OBS Studio, Python , Golang, Unity, web browsers and more.
- [Publish](https://mediamtx.org/docs/features/publish) live streams to the server with Media-over-QUIC, SRT, WebRTC, RTSP, RTMP, HLS, MPEG-TS, RTP, using FFmpeg, GStreamer, OBS Studio, Python , Golang, Unity, web browsers, Raspberry Pi Cameras and more.
- [Read](https://mediamtx.org/docs/features/read) live streams from the server with Media-over-QUIC, SRT, WebRTC, RTSP, RTMP, HLS, using FFmpeg, GStreamer, VLC, OBS Studio, Python , Golang, Unity, web browsers and more.
- Streams are automatically converted from a protocol to another
- Serve several streams at once in separate paths
- Reload the configuration without disconnecting existing clients (hot reloading)
+174
View File
@@ -155,6 +155,10 @@ components:
type: string
enum: [read, publish]
MoQSessionState:
type: string
enum: [idle, read, publish]
OK:
type: object
properties:
@@ -594,6 +598,26 @@ components:
srtAddress:
type: string
# MoQ server
moq:
type: boolean
moqHTTPS2Address:
type: string
moqHTTPS3Address:
type: string
moqServerKey:
type: string
moqServerCert:
type: string
moqAllowOrigins:
type: array
items:
type: string
moqTrustedProxies:
type: array
items:
type: string
# Record (deprecated)
record:
type: boolean
@@ -1856,6 +1880,39 @@ components:
items:
$ref: "#/components/schemas/WebRTCSession"
MoQSession:
type: object
properties:
id:
type: string
format: uuid
created:
type: string
remoteAddr:
type: string
state:
$ref: "#/components/schemas/MoQSessionState"
path:
type: string
query:
type: string
userAgent:
type: string
MoQSessionList:
type: object
properties:
pageCount:
type: integer
format: int64
itemCount:
type: integer
format: int64
items:
type: array
items:
$ref: "#/components/schemas/MoQSession"
paths:
/v3/info:
get:
@@ -3388,6 +3445,123 @@ paths:
schema:
$ref: "#/components/schemas/Error"
/v3/moqsessions/list:
get:
operationId: moqSessionsList
tags: [MoQ]
summary: returns all MoQ sessions.
description: ""
parameters:
- name: page
in: query
description: page number.
schema:
type: integer
default: 0
- name: itemsPerPage
in: query
description: items per page.
schema:
type: integer
default: 100
responses:
"200":
description: the request was successful.
content:
application/json:
schema:
$ref: "#/components/schemas/MoQSessionList"
"400":
description: invalid request.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
description: server error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v3/moqsessions/get/{id}:
get:
operationId: moqSessionsGet
tags: [MoQ]
summary: returns a MoQ session.
description: ""
parameters:
- name: id
in: path
required: true
description: ID of the session.
schema:
type: string
responses:
"200":
description: the request was successful.
content:
application/json:
schema:
$ref: "#/components/schemas/MoQSession"
"400":
description: invalid request.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: session not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
description: server error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v3/moqsessions/kick/{id}:
post:
operationId: moqSessionsKick
tags: [MoQ]
summary: kicks out a MoQ session from the server.
description: ""
parameters:
- name: id
in: path
required: true
description: ID of the session.
schema:
type: string
responses:
"200":
description: the request was successful.
content:
application/json:
schema:
$ref: "#/components/schemas/OK"
"400":
description: invalid request.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: session not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
description: server error.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v3/recordings/list:
get:
operationId: recordingsList
+2 -2
View File
@@ -6,8 +6,8 @@ _MediaMTX_ is a ready-to-use and zero-dependency live media server and media pro
Main features:
- [Publish](../2-features/03-publish.md) live streams to the server with SRT, WebRTC, RTSP, RTMP, HLS, MPEG-TS, RTP, using FFmpeg, GStreamer, OBS Studio, Python , Golang, Unity, Web browsers, Raspberry Pi Cameras and more.
- [Read](../2-features/04-read.md) live streams from the server with SRT, WebRTC, RTSP, RTMP, HLS, using FFmpeg, GStreamer, VLC, OBS Studio, Python , Golang, Unity, Web browsers and more.
- [Publish](../2-features/03-publish.md) live streams to the server with Media-over-QUIC, SRT, WebRTC, RTSP, RTMP, HLS, MPEG-TS, RTP, using FFmpeg, GStreamer, OBS Studio, Python , Golang, Unity, Web browsers, Raspberry Pi Cameras and more.
- [Read](../2-features/04-read.md) live streams from the server with Media-over-QUIC, SRT, WebRTC, RTSP, RTMP, HLS, using FFmpeg, GStreamer, VLC, OBS Studio, Python , Golang, Unity, Web browsers and more.
- Streams are automatically converted from a protocol to another
- Serve several streams at once in separate paths
- Reload the configuration without disconnecting existing clients (hot reloading)
+21 -20
View File
@@ -2,29 +2,30 @@
Live streams can be published to the server with the following protocols:
- [SRT clients](../3-publish/01-srt-clients.md)
- [SRT cameras and servers](../3-publish/02-srt-cameras-and-servers.md)
- [WebRTC clients](../3-publish/03-webrtc-clients.md)
- [WebRTC servers](../3-publish/04-webrtc-servers.md)
- [RTSP clients](../3-publish/05-rtsp-clients.md)
- [RTSP cameras and servers](../3-publish/06-rtsp-cameras-and-servers.md)
- [RTMP clients](../3-publish/07-rtmp-clients.md)
- [RTMP cameras and servers](../3-publish/08-rtmp-cameras-and-servers.md)
- [HLS cameras and servers](../3-publish/09-hls-cameras-and-servers.md)
- [MPEG-TS](../3-publish/10-mpeg-ts.md)
- [RTP](../3-publish/11-rtp.md)
- [Media-over-QUIC clients](../3-publish/01-moq-clients.md)
- [SRT clients](../3-publish/02-srt-clients.md)
- [SRT cameras and servers](../3-publish/04-srt-cameras-and-servers.md)
- [WebRTC clients](../3-publish/05-webrtc-clients.md)
- [WebRTC servers](../3-publish/06-webrtc-servers.md)
- [RTSP clients](../3-publish/07-rtsp-clients.md)
- [RTSP cameras and servers](../3-publish/08-rtsp-cameras-and-servers.md)
- [RTMP clients](../3-publish/09-rtmp-clients.md)
- [RTMP cameras and servers](../3-publish/10-rtmp-cameras-and-servers.md)
- [HLS cameras and servers](../3-publish/11-hls-cameras-and-servers.md)
- [MPEG-TS](../3-publish/12-mpeg-ts.md)
- [RTP](../3-publish/13-rtp.md)
We provide instructions for publishing with the following devices:
- [Raspberry Pi Cameras](../3-publish/12-raspberry-pi-cameras.md)
- [Generic webcams](../3-publish/13-generic-webcams.md)
- [Raspberry Pi Cameras](../3-publish/14-raspberry-pi-cameras.md)
- [Generic webcams](../3-publish/15-generic-webcams.md)
We provide instructions for publishing with the following software:
- [Web browsers](../3-publish/14-web-browsers.md)
- [FFmpeg](../3-publish/15-ffmpeg.md)
- [GStreamer](../3-publish/16-gstreamer.md)
- [OBS Studio](../3-publish/17-obs-studio.md)
- [Python and OpenCV](../3-publish/18-python-opencv.md)
- [Golang](../3-publish/19-golang.md)
- [Unity](../3-publish/20-unity.md)
- [Web browsers](../3-publish/16-web-browsers.md)
- [FFmpeg](../3-publish/17-ffmpeg.md)
- [GStreamer](../3-publish/18-gstreamer.md)
- [OBS Studio](../3-publish/19-obs-studio.md)
- [Python and OpenCV](../3-publish/20-python-opencv.md)
- [Golang](../3-publish/21-golang.md)
- [Unity](../3-publish/22-unity.md)
+14 -13
View File
@@ -2,19 +2,20 @@
Live streams can be read from the server with the following protocols:
- [SRT clients](../4-read/01-srt.md)
- [WebRTC clients](../4-read/02-webrtc.md)
- [RTSP clients](../4-read/03-rtsp.md)
- [RTMP clients](../4-read/04-rtmp.md)
- [HLS](../4-read/05-hls.md)
- [Media-over-QUIC clients](../4-read/01-moq.md)
- [SRT clients](../4-read/02-srt.md)
- [WebRTC clients](../4-read/03-webrtc.md)
- [RTSP clients](../4-read/04-rtsp.md)
- [RTMP clients](../4-read/05-rtmp.md)
- [HLS](../4-read/06-hls.md)
We provide instructions for reading with the following software:
- [Web browsers](../4-read/06-web-browsers.md)
- [FFmpeg](../4-read/07-ffmpeg.md)
- [GStreamer](../4-read/08-gstreamer.md)
- [VLC](../4-read/09-vlc.md)
- [OBS Studio](../4-read/10-obs-studio.md)
- [Python and OpenCV](../4-read/11-python-opencv.md)
- [Golang](../4-read/12-golang.md)
- [Unity](../4-read/13-unity.md)
- [Web browsers](../4-read/07-web-browsers.md)
- [FFmpeg](../4-read/08-ffmpeg.md)
- [GStreamer](../4-read/09-gstreamer.md)
- [VLC](../4-read/10-vlc.md)
- [OBS Studio](../4-read/11-obs-studio.md)
- [Python and OpenCV](../4-read/12-python-opencv.md)
- [Golang](../4-read/13-golang.md)
- [Unity](../4-read/14-unity.md)
+2 -2
View File
@@ -299,7 +299,7 @@ Username and password can be passed through the `Authorization: Basic` HTTP head
Authorization: Basic base64(myuser:mypass)
```
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 in a website](../4-read/06-web-browsers.md#embed-in-a-website).
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 in a website](../4-read/07-web-browsers.md#embed-in-a-website).
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:
@@ -351,4 +351,4 @@ In OBS Studio, this is the "Bearer Token" field.
If the `Authorization: Bearer` token cannot be directly provided (for instance, with web browsers that directly access _MediaMTX_ and show a credential dialog), you can pass the token as password, using an arbitrary user.
In web browsers, if you need to automatically fill credentials from a parent web page, read [Embed in a website](../4-read/06-web-browsers.md#embed-in-a-website).
In web browsers, if you need to automatically fill credentials from a parent web page, read [Embed in a website](../4-read/07-web-browsers.md#embed-in-a-website).
@@ -1,12 +1,14 @@
# Expose the server in a subfolder
HTTP-based services (WebRTC, HLS, Control API, Playback Server, Metrics, pprof) can be exposed in a subfolder of an existing HTTP server or reverse proxy. The reverse proxy must be able to intercept HTTP requests addressed to _MediaMTX_ and corresponding responses, and perform the following changes:
HTTP-based services (Media-over-QUIC, WebRTC, HLS, Control API, Playback Server, Metrics, pprof) can be exposed in a subfolder of an external HTTP server or reverse proxy. The reverse proxy must be able to intercept HTTP requests addressed to _MediaMTX_ and corresponding responses, and must perform the following changes:
- The subfolder path must be stripped from request paths. For instance, if the server is exposed behind `/subpath` and the reverse proxy receives a request with path `/subpath/mystream/index.m3u8`, this has to be changed into `/mystream/index.m3u8`.
- Any `Location` header in responses must be prefixed with the subfolder path. For instance, if the server is exposed behind `/subpath` and the server sends a response with `Location: /mystream/index.m3u8`, this has to be changed into `Location: /subfolder/mystream/index.m3u8`.
If _nginx_ is the reverse proxy, this can be achieved with the following configuration:
## Nginx
When _Nginx_ is the reverse proxy, rules described above can be implemented with the following configuration:
```
location /subpath/ {
@@ -15,7 +17,9 @@ location /subpath/ {
}
```
If _Apache HTTP Server_ is the reverse proxy, this can be achieved with the following configuration:
## Apache HTTP Server
When _Apache HTTP Server_ is the reverse proxy, rules described above can be implemented with the following configuration:
```
<Location /subpath>
@@ -25,7 +29,9 @@ If _Apache HTTP Server_ is the reverse proxy, this can be achieved with the foll
</Location>
```
If _Caddy_ is the reverse proxy, this can be achieved with the following configuration:
## Caddy
When _Caddy_ is the reverse proxy, rules described above can be implemented with the following configuration:
```
:80 {
+7 -1
View File
@@ -149,13 +149,18 @@ webrtc_sessions_outbound_bytes{id="[id]",path="[path]",remoteAddr="[remoteAddr]"
webrtc_sessions_outbound_rtp_packets{id="[id]",path="[path]",remoteAddr="[remoteAddr]",state="[state]"} 123
webrtc_sessions_outbound_rtcp_packets{id="[id]",path="[path]",remoteAddr="[remoteAddr]",state="[state]"} 123
webrtc_sessions_outbound_frames_discarded{id="[id]",path="[path]",remoteAddr="[remoteAddr]",state="[state]"} 12
# MoQ sessions
moq_sessions{id="[id]",path="[path]",remoteAddr="[remoteAddr]",state="[state]"} 1
moq_sessions_inbound_bytes{id="[id]",path="[path]",remoteAddr="[remoteAddr]",state="[state]"} 1234
moq_sessions_outbound_bytes{id="[id]",path="[path]",remoteAddr="[remoteAddr]",state="[state]"} 187
```
Bitrates are not provided directly as metrics because they can be computed from received and sent bytes by any metrics analyzer (i.e. Grafana).
Metrics can be filtered by using HTTP query parameters:
- `type=[TYPE]`: show metrics of a certain type only. TYPE can be `paths`, `hls_sessions`, `hls_muxers`, `rtsp_conns`, `rtsp_sessions`, `rtsps_conns`, `rtsps_sessions`, `rtmp_conns`, `rtmps_conns`, `srt_conns`, `webrtc_sessions`.
- `type=[TYPE]`: show metrics of a certain type only. TYPE can be `paths`, `hls_sessions`, `hls_muxers`, `rtsp_conns`, `rtsp_sessions`, `rtsps_conns`, `rtsps_sessions`, `rtmp_conns`, `rtmps_conns`, `srt_conns`, `webrtc_sessions`, `moq_sessions`.
- `path=[PATH]`: show metrics belonging to a specific path only
- `hls_muxer=[PATH]`: show metrics belonging to a specific HLS muxer only
- `hls_session=[ID]`: show metrics belonging to a specific HLS session only
@@ -167,3 +172,4 @@ Metrics can be filtered by using HTTP query parameters:
- `rtmps_conn=[ID]` show metrics belonging to a specific RTMPS connection only
- `srt_conn=[ID]` show metrics belonging to a specific SRT connection only
- `webrtc_session=[ID]` show metrics belonging to a specific WebRTC session only
- `moq_session=[ID]` show metrics belonging to a specific MoQ session only
+1 -1
View File
@@ -1,6 +1,6 @@
# SRT-specific features
SRT is a protocol that can be used for publishing and reading streams. Regarding specific tasks, check out [Publish with SRT clients](../3-publish/01-srt-clients.md) and [Read with SRT clients](../4-read/01-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, check out [Publish with SRT clients](../3-publish/02-srt-clients.md) and [Read with SRT clients](../4-read/02-srt.md). Features in this page are shared among both tasks.
## Standard stream ID syntax
@@ -1,6 +1,6 @@
# WebRTC-specific features
WebRTC is a protocol that can be used for publishing and reading streams. Regarding specific tasks, check out [Publish with WebRTC clients](../3-publish/03-webrtc-clients.md) and [Read with WebRTC clients](../4-read/02-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, check out [Publish with WebRTC clients](../3-publish/05-webrtc-clients.md) and [Read with WebRTC clients](../4-read/03-webrtc.md). Features in this page are shared among both tasks.
## Codec support in browsers
+2 -2
View File
@@ -1,6 +1,6 @@
# RTSP-specific features
RTSP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, check out [Publish with RTSP clients](../3-publish/05-rtsp-clients.md) and [Read with RTSP clients](../4-read/03-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, check out [Publish with RTSP clients](../3-publish/07-rtsp-clients.md) and [Read with RTSP clients](../4-read/04-rtsp.md). Features in this page are shared among both tasks.
## Transport protocols
@@ -107,4 +107,4 @@ There are also the `rtsps+http`, `rtsp+ws`, `rtsps+ws` schemas to handle any var
## MPEG-TS inside RTSP
read [MPEG-TS inside RTSP](../3-publish/05-rtsp-clients.md#mpeg-ts-inside-rtsp) in the "Publish with RTSP clients" page.
read [MPEG-TS inside RTSP](../3-publish/07-rtsp-clients.md#mpeg-ts-inside-rtsp) in the "Publish with RTSP clients" page.
+1 -1
View File
@@ -1,6 +1,6 @@
# RTMP-specific features
RTMP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, check out [Publish with RTMP clients](../3-publish/07-rtmp-clients.md) and [Read with RTMP clients](../4-read/04-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, check out [Publish with RTMP clients](../3-publish/09-rtmp-clients.md) and [Read with RTMP clients](../4-read/05-rtmp.md). Features in this page are shared among both tasks.
## Encryption
+36
View File
@@ -0,0 +1,36 @@
# Media-over-QUIC clients
| | supported codecs |
| --------- | ------------------------- |
| **video** | AV1, VP9, VP8, H265, H264 |
| **audio** | Opus, MPEG-4 Audio (AAC) |
Media-over-QUIC is a streaming protocol built upon cutting edge protocols (QUIC, HTTP3) and browser APIs (WebTransport, WebCodecs) that can be used to publish and read live media streams. It's slightly faster than WebRTC, has an advanced data recovery mechanism, it supports additional codecs (FLAC and future ones) and is less complicated to route.
Media-over-QUIC has a wide range of features and variants, most of them in active development. We currently support the following:
- We support `draft-18` of the [main specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport-18).
- We only support using Media-over-QUIC through browsers and in particular through the WebTransport API. We do not support using QUIC directly.
- We support the `PUBLISH` and `SUBSCRIBE` messages only, which are the ones meant to be used by a routing solution like _MediaMTX_.
- We use the MOQT Streaming Format (MSF) to advertise tracks, described in [this specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-msf-00).
- We use the Low Overhead Media Container (LOC) to ship frames, described in [this specification](https://datatracker.ietf.org/doc/draft-ietf-moq-loc/).
- We host web pages through a HTTP/2 listener and host the WebTransport endpoint through a HTTP/3 listener. This hybrid setup allows to use self-signed certificates, that are normally forbidden in pure HTTP/3.
There are some server requirements:
- HTTPS is mandatory.
- Clients must be able to access both the HTTP/2 listener (`:8892`) and the HTTP/3 listener (`:8892`), the latter of which runs over UDP.
And there are some client (browser) requirements:
- If the server certificate is self-signed, browser must support the [serverCertificatesHashes option](https://caniuse.com/mdn-api_webtransport_webtransport_options_servercertificatehashes_parameter) (all except iOS safari do).
- Browser must support [WebTransport](https://caniuse.com/webtransport) and [WebCodecs](https://caniuse.com/webcodecs) (all modern browsers do)
- When publishing tracks, the browser to support [MediaStreamTrackProcessor](https://caniuse.com/mdn-api_mediastreamtrackprocessor) (only Chrome does).
You can publish a stream with Media-over-QUIC and a web browser by visiting:
```
https://localhost:8892/mystream/publish
```
The only clients that can currently publish with Media-over-QUIC are [Web browsers](16-web-browsers.md).
@@ -18,4 +18,4 @@ If you need to use the standard stream ID syntax instead of the custom one in us
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.
Some clients that can publish with SRT are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md), [OBS Studio](17-obs-studio.md).
Some clients that can publish with SRT are [FFmpeg](17-ffmpeg.md), [GStreamer](18-gstreamer.md), [OBS Studio](19-obs-studio.md).
@@ -23,4 +23,4 @@ Be aware that not all browsers can read any codec, check [Codec support in brows
Depending on the network it might be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](../2-features/26-webrtc-specific-features.md#solving-webrtc-connectivity-issues).
Some clients that can publish with WebRTC and WHIP are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md), [OBS Studio](17-obs-studio.md), [Unity](20-unity.md) and [Web browsers](14-web-browsers.md).
Some clients that can publish with WebRTC and WHIP are [FFmpeg](17-ffmpeg.md), [GStreamer](18-gstreamer.md), [OBS Studio](19-obs-studio.md), [Unity](22-unity.md) and [Web browsers](16-web-browsers.md).
@@ -14,7 +14,7 @@ paths:
source: wheps://host:port/path
```
If the remote server is a _MediaMTX_ instance, remember to add a `/whep` suffix after the stream name, since in _MediaMTX_ [it's part of the WHEP URL](../4-read/02-webrtc.md):
If the remote server is a _MediaMTX_ instance, remember to add a `/whep` suffix after the stream name, since in _MediaMTX_ [it's part of the WHEP URL](../4-read/03-webrtc.md):
```yml
paths:
@@ -14,7 +14,7 @@ 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).
Some clients that can publish with RTSP are [FFmpeg](17-ffmpeg.md), [GStreamer](18-gstreamer.md), [OBS Studio](19-obs-studio.md), [Python and OpenCV](20-python-opencv.md).
Advanced RTSP features and settings are described in [RTSP-specific features](../2-features/27-rtsp-specific-features.md).
@@ -13,4 +13,4 @@ rtmp://localhost/mystream
The resulting stream will be available on path `/mystream`.
Some clients that can publish with RTMP are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md) and [OBS Studio](17-obs-studio.md).
Some clients that can publish with RTMP are [FFmpeg](17-ffmpeg.md), [GStreamer](18-gstreamer.md) and [OBS Studio](19-obs-studio.md).
@@ -34,7 +34,7 @@ paths:
source: udp+mpegts://0.0.0.0:1234?source=192.168.3.5
```
Some clients that can publish with UDP and MPEG-TS are [FFmpeg](15-ffmpeg.md) and [GStreamer](16-gstreamer.md).
Some clients that can publish with UDP and MPEG-TS are [FFmpeg](17-ffmpeg.md) and [GStreamer](18-gstreamer.md).
Unix sockets are more efficient than UDP packets and can be used as transport by specifying the `unix+mpegts` scheme:
@@ -27,4 +27,4 @@ paths:
`rtpSDP` must contain a valid SDP, that is a description of the RTP session.
Some clients that can publish with UDP and MPEG-TS are [FFmpeg](15-ffmpeg.md) and [GStreamer](16-gstreamer.md).
Some clients that can publish with UDP and MPEG-TS are [FFmpeg](17-ffmpeg.md) and [GStreamer](18-gstreamer.md).
@@ -1,6 +1,26 @@
# Web browsers
Web browsers can publish a stream to the server by using the [WebRTC protocol](03-webrtc-clients.md). Start the server and open the web page:
Web browsers can publish a stream to the server by acting as a [Media-over-QUIC client](01-moq-clients.md) or a [WebRTC client](05-webrtc-clients.md).
## Media-over-QUIC
Start the server and open the web page:
```
https://localhost:8892/mystream/publish
```
The resulting stream will be available on path `/mystream`.
This web page can be embedded into another web page by using an iframe:
```html
<iframe src="https://mediamtx-ip:8892/mystream/publish" scrolling="no"></iframe>
```
## WebRTC
Start the server and open the web page:
```
http://localhost:8889/mystream/publish
@@ -1,8 +1,8 @@
# FFmpeg
FFmpeg can publish a stream to the server by using the [RTSP](05-rtsp-clients.md), [RTMP](07-rtmp-clients.md), [MPEG-TS](10-mpeg-ts.md), [RTP](11-rtp.md), [SRT](01-srt-clients.md) and [WebRTC](03-webrtc-clients.md) protocols. The recommended one is RTSP.
FFmpeg can publish a stream to the server by acting as a [RTSP client](07-rtsp-clients.md), [RTMP client](09-rtmp-clients.md), [SRT client](02-srt-clients.md), [WebRTC client](05-webrtc-clients.md) or by sending [MPEG-TS packets](12-mpeg-ts.md) or [RTP packets](13-rtp.md). The recommended way is acting as a RTSP client.
## FFmpeg and RTSP
## FFmpeg as a RTSP client
```sh
ffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f rtsp rtsp://localhost:8554/mystream
@@ -10,7 +10,7 @@ ffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f rtsp rtsp://localhost:8554/mys
The resulting stream will be available on path `/mystream`.
## FFmpeg and RTMP
## FFmpeg as a RTMP client
```sh
ffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f flv rtmp://localhost:1935/mystream
@@ -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` (read [RTP](11-rtp.md)). Then:
In _MediaMTX_ configuration, add a path with `source: udp+rtp://238.0.0.1:1234` and a valid `rtpSDP` (read [RTP](13-rtp.md)). Then:
```sh
ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
@@ -44,13 +44,13 @@ ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
-f rtp udp://238.0.0.1:1234?pkt_size=1316
```
## FFmpeg and SRT
## FFmpeg as a SRT client
```sh
ffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f mpegts 'srt://localhost:8890?streamid=publish:stream&pkt_size=1316'
```
## FFmpeg and WebRTC
## FFmpeg as a WebRTC client
```sh
ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
@@ -1,8 +1,8 @@
# GStreamer
GStreamer can publish a stream to the server by using the [RTSP](05-rtsp-clients.md), [RTMP](07-rtmp-clients.md), [SRT](01-srt-clients.md), [MPEG-TS](10-mpeg-ts.md) and [WebRTC](03-webrtc-clients.md) protocols. The recommended one is RTSP.
GStreamer can publish a stream to the server by acting as a [RTSP client](07-rtsp-clients.md), [RTMP client](09-rtmp-clients.md), [SRT client](02-srt-clients.md), [WebRTC client](05-webrtc-clients.md) or by sending [MPEG-TS packets](12-mpeg-ts.md). The recommended way is acting as a RTSP client.
## GStreamer and RTSP
## GStreamer as a RTSP client
```sh
gst-launch-1.0 rtspclientsink name=s location=rtsp://localhost:8554/mystream \
@@ -20,7 +20,7 @@ d.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream
The resulting stream will be available on path `/mystream`.
## GStreamer and RTMP
## GStreamer as a RTMP client
```sh
gst-launch-1.0 -v flvmux name=mux ! rtmpsink location=rtmp://localhost/stream \
@@ -28,7 +28,7 @@ videotestsrc ! video/x-raw,width=1280,height=720,format=I420 ! x264enc speed-pre
audiotestsrc ! audioconvert ! avenc_aac ! mux.
```
## GStreamer and SRT
## GStreamer as a SRT client
```sh
gst-launch-1.0 -v mpegtsmux name=mux ! srtsink uri="srt://localhost:8890?streamid=publish:mystream&pkt_size=1316" \
@@ -46,7 +46,7 @@ videotestsrc ! video/x-raw,width=1280,height=720,format=I420 ! x264enc speed-pre
audiotestsrc ! audioconvert ! avenc_aac ! mux.
```
## GStreamer and WebRTC
## GStreamer as a WebRTC client
Make sure that GStreamer version is at least 1.22, and that if the codec is H264, the profile is baseline. Use the `whipclientsink` element:
@@ -1,8 +1,8 @@
# OBS Studio
OBS Studio can publish streams to the server by using the [RTMP](07-rtmp-clients.md) and [WebRTC](03-webrtc-clients.md) protocols. The recommended one is RTMP.
OBS Studio can publish streams to the server by acting as a [RTMP client](09-rtmp-clients.md) or a [WebRTC client](05-webrtc-clients.md) protocols. The recommended way is acting as a RTMP client.
## OBS Studio and RTMP
## OBS Studio as a RTMP client
### Standard
@@ -173,11 +173,11 @@ OBS Studio requires _MediaMTX_ to use a TLS certificate signed by a public certi
certutil -addstore "Root" myca.crt
```
## OBS Studio and WebRTC
## OBS Studio as a WebRTC client
### Standard
Recent versions of OBS Studio can also publish streams to the server with the [WebRTC / WHIP protocol](03-webrtc-clients.md) Use the following parameters:
Recent versions of OBS Studio can also publish streams to the server with the [WebRTC / WHIP protocol](05-webrtc-clients.md) Use the following parameters:
- Service: `WHIP`
- Server: `http://localhost:8889/mystream/whip`
@@ -1,6 +1,6 @@
# Python and OpenCV
Python-based software can publish streams to the server with the OpenCV library and its GStreamer plugin, acting as a [RTSP client](05-rtsp-clients.md). OpenCV must be compiled with support for GStreamer, by following this procedure:
Python-based software can publish streams to the server with the OpenCV library and its GStreamer plugin, acting as a [RTSP client](07-rtsp-clients.md). OpenCV must be compiled with support for GStreamer, by following this procedure:
```sh
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-ugly gstreamer1.0-rtsp python3-dev python3-numpy
@@ -1,6 +1,6 @@
# Unity
Software written with the Unity Engine can publish a stream to the server by using the [WebRTC protocol](03-webrtc-clients.md).
Software written with the Unity Engine can publish a stream to the server by acting as a [WebRTC client](05-webrtc-clients.md).
Create a new Unity project or open an existing one.
+18
View File
@@ -0,0 +1,18 @@
# Media-over-QUIC clients
| | supported codecs |
| --------- | ------------------------------------------------------- |
| **video** | AV1, VP9, VP8, H265, H264 |
| **audio** | Opus, FLAC, MPEG-4 Audio (AAC), G711 (PCMA, PCMU), LPCM |
Media-over-QUIC is a streaming protocol built upon cutting edge protocols (QUIC, HTTP3) and browser APIs (WebTransport, WebCodecs) that can be used to publish and read live media streams. It's slightly faster than WebRTC, has an advanced data recovery mechanism, it supports additional codecs (FLAC and future ones) and is less complicated to route.
There are some limitations and requirements that are listed in [Publish with Media-over-QUIC clients](../3-publish/01-moq-clients.md).
You can read a stream with Media-over-QUIC and a web browser by visiting:
```
https://localhost:8892/mystream/publish
```
The only clients that can currently read with Media-over-QUIC are [Web browsers](07-web-browsers.md).
@@ -16,4 +16,4 @@ 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, read [Standard stream ID syntax](../2-features/25-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).
Some clients that can read with SRT are [FFmpeg](08-ffmpeg.md), [GStreamer](09-gstreamer.md) and [VLC](10-vlc.md).
@@ -22,4 +22,4 @@ Be aware that not all browsers can read any codec, check [Codec support in brows
Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](../2-features/26-webrtc-specific-features.md#solving-webrtc-connectivity-issues).
Some clients that can read with WebRTC and WHEP are [GStreamer](08-gstreamer.md), [Unity](13-unity.md) and [web browsers](06-web-browsers.md).
Some clients that can read with WebRTC and WHEP are [GStreamer](09-gstreamer.md), [Unity](14-unity.md) and [web browsers](07-web-browsers.md).
@@ -12,4 +12,4 @@ RTSP is a protocol that allows to publish and read streams. It supports several
rtsp://localhost:8554/mystream
```
Some clients that can read with RTSP are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md) and [VLC](09-vlc.md).
Some clients that can read with RTSP are [FFmpeg](08-ffmpeg.md), [GStreamer](09-gstreamer.md) and [VLC](10-vlc.md).
@@ -11,4 +11,4 @@ RTMP is a protocol that allows to read and publish streams. It has been expanded
rtmp://localhost/mystream
```
Some clients that can read with RTMP are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md) and [VLC](09-vlc.md).
Some clients that can read with RTMP are [FFmpeg](08-ffmpeg.md), [GStreamer](09-gstreamer.md) and [VLC](10-vlc.md).
@@ -18,7 +18,7 @@ and can also be accessed without using the browsers, by software that supports t
http://localhost:8888/mystream/index.m3u8
```
Some clients that can read with HLS are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md), [VLC](09-vlc.md) and [web browsers](06-web-browsers.md).
Some clients that can read with HLS are [FFmpeg](08-ffmpeg.md), [GStreamer](09-gstreamer.md), [VLC](10-vlc.md) and [web browsers](07-web-browsers.md).
_MediaMTX_ supports generating HLS in several variants (including Low-Latency mode), and provides various parameters to tune HLS generation. These are listed in the [configuration file](../5-references/1-configuration-file.md):
@@ -1,9 +1,19 @@
# Web browsers
Web browsers can read a stream from the server with the [WebRTC](02-webrtc.md) and [HLS](05-hls.md) protocols, by accessing web pages hosted by the server. It is also possible to embed streams into an external website.
Web browsers can read a stream from the server with the [Media-over-QUIC](01-moq.md), [WebRTC](03-webrtc.md) and [HLS](06-hls.md) protocols, by accessing web pages hosted by the server. It is also possible to embed streams into an external website.
## Direct read
### Media-over-QUIC
Visit the web page:
```
https://localhost:8892/mystream
```
Replace `mystream` with the path name.
### WebRTC
Visit the web page:
@@ -28,6 +38,14 @@ Replace `mystream` with the path name.
Live streams can be embedded into an external website by using the WebRTC or HLS protocol. Before embedding, check that the stream is ready and can be accessed with intended protocol by visiting web pages mentioned in the previous section.
### Media-over-QUIC in iframe
The simplest way to embed a live stream in a web page, using the Media-over-QUIC protocol, consists in adding an `<iframe>` tag to the body section of the HTML:
```html
<iframe src="https://mediamtx-ip:8892/mystream" scrolling="no"></iframe>
```
### WebRTC in iframe
The simplest way to embed a live stream in a web page, using the WebRTC protocol, consists in adding an `<iframe>` tag to the body section of the HTML:
@@ -1,6 +1,6 @@
# FFmpeg
FFmpeg can read a stream from the server the [RTSP](03-rtsp.md), [RTMP](04-rtmp.md), [HLS](05-hls.md) and [SRT](01-srt.md) protocols. The recommended one is RTSP.
FFmpeg can read a stream from the server the [RTSP](04-rtsp.md), [RTMP](05-rtmp.md), [HLS](06-hls.md) and [SRT](02-srt.md) protocols. The recommended one is RTSP.
## FFmpeg and RTSP
@@ -1,6 +1,6 @@
# GStreamer
GStreamer can read a stream from the server by using the [RTSP](03-rtsp.md), [RTMP](04-rtmp.md), [HLS](05-hls.md), [SRT](01-srt.md) and [WebRTC](02-webrtc.md) protocols. The recommended one is RTSP.
GStreamer can read a stream from the server by using the [RTSP](04-rtsp.md), [RTMP](05-rtmp.md), [HLS](06-hls.md), [SRT](02-srt.md) and [WebRTC](03-webrtc.md) protocols. The recommended one is RTSP.
## GStreamer and RTSP
@@ -1,6 +1,6 @@
# VLC
VLC can read a stream from the server by using the [RTSP](03-rtsp.md), [RTMP](04-rtmp.md), [HLS](05-hls.md) and [SRT](01-srt.md) protocols. The recommended one is RTSP.
VLC can read a stream from the server by using the [RTSP](04-rtsp.md), [RTMP](05-rtmp.md), [HLS](06-hls.md) and [SRT](02-srt.md) protocols. The recommended one is RTSP.
In order to minimize latency, it is also suggested to change the `Network caching` parameter of VLC:
@@ -1,6 +1,6 @@
# OBS Studio
OBS Studio can read streams from the server by using the [RTSP protocol](03-rtsp.md).
OBS Studio can read streams from the server by using the [RTSP protocol](04-rtsp.md).
Open OBS, click on _Add Source_, _Media source_, _OK_, uncheck _Local file_, insert in _Input_:
@@ -1,6 +1,6 @@
# Python and OpenCV
Python-based software can read streams from the server with the OpenCV library, acting as a [RTSP client](03-rtsp.md).
Python-based software can read streams from the server with the OpenCV library, acting as a [RTSP client](04-rtsp.md).
```python
import cv2
@@ -1,6 +1,6 @@
# Unity
Software written with the Unity Engine can read a stream from the server by using the [WebRTC protocol](02-webrtc.md).
Software written with the Unity Engine can read a stream from the server by using the [WebRTC protocol](03-webrtc.md).
Create a new Unity project or open an existing one.
+6 -1
View File
@@ -1,7 +1,7 @@
# Specifications
| name | area |
| ------------------------------------------------------------------------------------------------------------- | -------------- |
| ------------------------------------------------------------------------------------------------------------- | --------------- |
| [RTSP / RTP / RTCP specifications](https://github.com/bluenviron/gortsplib#specifications) | RTSP |
| [HLS specifications](https://github.com/bluenviron/gohlslib#specifications) | HLS |
| [RTMP specifications](https://github.com/bluenviron/gortmplib#specifications) | RTMP |
@@ -13,6 +13,11 @@
| [H.265 Profile for WebRTC](https://datatracker.ietf.org/doc/html/draft-ietf-avtcore-hevc-webrtc-08) | WebRTC |
| [RFC9725, WebRTC-HTTP Ingestion Protocol (WHIP)](https://datatracker.ietf.org/doc/html/rfc9725) | WebRTC |
| [WebRTC-HTTP Egress Protocol (WHEP)](https://datatracker.ietf.org/doc/html/draft-ietf-wish-whep) | WebRTC |
| [RFC9000, QUIC: A UDP-Based Multiplexed and Secure Transport](https://datatracker.ietf.org/doc/html/rfc9000) | Media-over-QUIC |
| [RFC9114, HTTP/3](https://datatracker.ietf.org/doc/html/rfc9114) | Media-over-QUIC |
| [Media over QUIC Transport](https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport) | Media-over-QUIC |
| [MOQT Streaming Format (MSF)](https://datatracker.ietf.org/doc/html/draft-ietf-moq-msf-00) | Media-over-QUIC |
| [Low Overhead Media Container (LOC)](https://datatracker.ietf.org/doc/draft-ietf-moq-loc/) | Media-over-QUIC |
| [The SRT Protocol](https://haivision.github.io/srt-rfc/draft-sharabayko-srt.html) | SRT |
| [Codec specifications](https://github.com/bluenviron/mediacommon#specifications) | codecs |
| [Golang project layout](https://github.com/golang-standards/project-layout) | project layout |
+2
View File
@@ -10,6 +10,8 @@
- [pion/sdp (SDP library used internally)](https://github.com/pion/sdp)
- [pion/rtp (RTP library used internally)](https://github.com/pion/rtp)
- [pion/rtcp (RTCP library used internally)](https://github.com/pion/rtcp)
- [quic-go/quic-go (QUIC library used internally)](https://github.com/quic-go/quic-go)
- [quic-go/webtransport-go (WebTransport library used internally)](https://github.com/quic-go/webtransport-go)
- [go-astits (MPEG-TS library used internally)](https://github.com/asticode/go-astits)
- [go-mp4 (MP4 library used internally)](https://github.com/abema/go-mp4)
- [hls.js (browser-side HLS library used internally)](https://github.com/video-dev/hls.js)
+4 -1
View File
@@ -37,10 +37,13 @@ require (
github.com/pion/sdp/v3 v3.0.18
github.com/pion/transport/v4 v4.0.2
github.com/pion/webrtc/v4 v4.2.14
github.com/quic-go/quic-go v0.59.0
github.com/quic-go/webtransport-go v0.10.0
github.com/stretchr/testify v1.11.1
github.com/wlynxg/anet v0.0.5
golang.org/x/crypto v0.52.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
)
@@ -59,6 +62,7 @@ require (
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
@@ -88,7 +92,6 @@ require (
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+4
View File
@@ -61,6 +61,8 @@ github.com/datarhei/gosrt v0.11.0/go.mod h1:F5B25N3CFf68K4igNLQ1iARcKDbkv8riymjT
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -206,6 +208,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI=
github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+9 -3
View File
@@ -81,6 +81,7 @@ type API struct {
HLSServer defs.APIHLSServer
WebRTCServer defs.APIWebRTCServer
SRTServer defs.APISRTServer
MoQServer defs.APIMoQServer
Parent apiParent
httpServer *httpp.Server
@@ -91,7 +92,6 @@ type API struct {
func (a *API) Initialize() error {
router := gin.New()
router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.Use(a.middlewarePreflightRequests)
router.Use(a.middlewareAuth)
@@ -165,6 +165,12 @@ func (a *API) Initialize() error {
group.POST("/srtconns/kick/:id", a.onSRTConnsKick)
}
if !interfaceIsEmpty(a.MoQServer) {
group.GET("/moqsessions/list", a.onMoQSessionsList)
group.GET("/moqsessions/get/:id", a.onMoQSessionsGet)
group.POST("/moqsessions/kick/:id", a.onMoQSessionsKick)
}
group.GET("/recordings/list", a.onRecordingsList)
group.GET("/recordings/get/*name", a.onRecordingsGet)
group.DELETE("/recordings/deletesegment", a.onRecordingDeleteSegment)
@@ -187,7 +193,7 @@ func (a *API) Initialize() error {
return err
}
str := "listener opened on " + a.Address
str := "started with listener on " + a.Address
if !a.Encryption {
str += " (TCP/HTTP)"
} else {
@@ -200,7 +206,7 @@ func (a *API) Initialize() error {
// Close closes the API.
func (a *API) Close() {
a.Log(logger.Info, "listener is closing")
a.Log(logger.Info, "closing")
a.httpServer.Close()
}
+68
View File
@@ -0,0 +1,68 @@
package api //nolint:revive,dupl
import (
"errors"
"net/http"
"github.com/bluenviron/mediamtx/internal/servers/moq"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func (a *API) onMoQSessionsList(ctx *gin.Context) {
data, err := a.MoQServer.APISessionsList()
if err != nil {
a.writeError(ctx, http.StatusInternalServerError, err)
return
}
data.ItemCount = len(data.Items)
pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
data.PageCount = pageCount
ctx.JSON(http.StatusOK, data)
}
func (a *API) onMoQSessionsGet(ctx *gin.Context) {
id, err := uuid.Parse(ctx.Param("id"))
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
data, err := a.MoQServer.APISessionsGet(id)
if err != nil {
if errors.Is(err, moq.ErrSessionNotFound) {
a.writeError(ctx, http.StatusNotFound, err)
} else {
a.writeError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.JSON(http.StatusOK, data)
}
func (a *API) onMoQSessionsKick(ctx *gin.Context) {
id, err := uuid.Parse(ctx.Param("id"))
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
err = a.MoQServer.APISessionsKick(id)
if err != nil {
if errors.Is(err, moq.ErrSessionNotFound) {
a.writeError(ctx, http.StatusNotFound, err)
} else {
a.writeError(ctx, http.StatusInternalServerError, err)
}
return
}
a.writeOK(ctx)
}
+181
View File
@@ -0,0 +1,181 @@
package api //nolint:revive
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/servers/moq"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
type testMoQServer struct {
sessions map[uuid.UUID]*defs.APIMoQSession
}
func (s *testMoQServer) APISessionsList() (*defs.APIMoQSessionList, error) {
items := make([]defs.APIMoQSession, 0, len(s.sessions))
for _, session := range s.sessions {
items = append(items, *session)
}
return &defs.APIMoQSessionList{Items: items}, nil
}
func (s *testMoQServer) APISessionsGet(id uuid.UUID) (*defs.APIMoQSession, error) {
session, ok := s.sessions[id]
if !ok {
return nil, moq.ErrSessionNotFound
}
return session, nil
}
func (s *testMoQServer) APISessionsKick(id uuid.UUID) error {
_, ok := s.sessions[id]
if !ok {
return moq.ErrSessionNotFound
}
return nil
}
func TestMoQSessionsList(t *testing.T) {
id1 := uuid.New()
id2 := uuid.New()
now := time.Now()
moqServer := &testMoQServer{
sessions: map[uuid.UUID]*defs.APIMoQSession{
id1: {
ID: id1,
Created: now,
RemoteAddr: "192.168.1.1:5000",
State: defs.APIMoQSessionStatePublish,
Path: "stream1",
Query: "token=abc",
InboundBytes: 1000,
OutboundBytes: 2000,
},
id2: {
ID: id2,
Created: now.Add(time.Minute),
RemoteAddr: "192.168.1.2:5001",
State: defs.APIMoQSessionStateRead,
Path: "stream2",
Query: "",
InboundBytes: 500,
OutboundBytes: 1500,
},
},
}
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
MoQServer: moqServer,
Parent: &testParent{},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
var out defs.APIMoQSessionList
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/moqsessions/list", nil, &out)
require.Equal(t, 2, out.ItemCount)
require.Equal(t, 1, out.PageCount)
require.Len(t, out.Items, 2)
}
func TestMoQSessionsGet(t *testing.T) {
id := uuid.New()
now := time.Now()
moqServer := &testMoQServer{
sessions: map[uuid.UUID]*defs.APIMoQSession{
id: {
ID: id,
Created: now,
RemoteAddr: "192.168.1.100:5000",
State: defs.APIMoQSessionStatePublish,
Path: "mystream",
Query: "key=value",
InboundBytes: 999999,
OutboundBytes: 888888,
},
},
}
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
MoQServer: moqServer,
Parent: &testParent{},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
var out defs.APIMoQSession
httpRequest(t, hc, http.MethodGet, fmt.Sprintf("http://localhost:9997/v3/moqsessions/get/%s", id), nil, &out)
require.Equal(t, id, out.ID)
require.Equal(t, "192.168.1.100:5000", out.RemoteAddr)
require.Equal(t, defs.APIMoQSessionStatePublish, out.State)
require.Equal(t, "mystream", out.Path)
require.Equal(t, uint64(999999), out.InboundBytes)
require.Equal(t, uint64(888888), out.OutboundBytes)
}
func TestMoQSessionsKick(t *testing.T) {
id := uuid.New()
now := time.Now()
moqServer := &testMoQServer{
sessions: map[uuid.UUID]*defs.APIMoQSession{
id: {
ID: id,
Created: now,
RemoteAddr: "192.168.1.100:5000",
State: defs.APIMoQSessionStatePublish,
Path: "mystream",
Query: "",
InboundBytes: 1000,
OutboundBytes: 2000,
},
},
}
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
MoQServer: moqServer,
Parent: &testParent{},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
httpRequest(t, hc, http.MethodPost, fmt.Sprintf("http://localhost:9997/v3/moqsessions/kick/%s", id), nil, nil)
}
+1
View File
@@ -17,6 +17,7 @@ const (
ProtocolHLS Protocol = "hls"
ProtocolWebRTC Protocol = "webrtc"
ProtocolSRT Protocol = "srt"
ProtocolMoQ Protocol = "moq"
)
// Request is an authentication request.
+108 -10
View File
@@ -2,17 +2,63 @@
package certloader
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"sync"
"time"
"github.com/bluenviron/mediamtx/internal/confwatcher"
"github.com/bluenviron/mediamtx/internal/logger"
)
const (
autoKeyName = "auto.key"
autoCertName = "auto.crt"
)
func generateTLSCert() (certPEM, keyPEM []byte, err error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("generate key: %w", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "mediamtx"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return nil, nil, fmt.Errorf("create certificate: %w", err)
}
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
return nil, nil, fmt.Errorf("marshal key: %w", err)
}
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
return certPEM, keyPEM, nil
}
// CertLoader is a certificate loader. It watches for changes to the certificate and key files.
type CertLoader struct {
CertPath string
KeyPath string
AllowAuto bool
Parent logger.Writer
certWatcher, keyWatcher *confwatcher.ConfWatcher
@@ -26,38 +72,88 @@ type CertLoader struct {
func (cl *CertLoader) Initialize() error {
cl.done = make(chan struct{})
cl.certWatcher = &confwatcher.ConfWatcher{FilePath: cl.CertPath}
err := cl.certWatcher.Initialize()
if cl.AllowAuto && cl.KeyPath == autoKeyName && cl.CertPath == autoCertName {
watch, err := cl.initializeAuto()
if err != nil {
return err
}
cl.keyWatcher = &confwatcher.ConfWatcher{FilePath: cl.KeyPath}
err = cl.keyWatcher.Initialize()
if err != nil {
cl.certWatcher.Close() //nolint:errcheck
return err
if !watch {
return nil
}
} else {
cert, err := tls.LoadX509KeyPair(cl.CertPath, cl.KeyPath)
if err != nil {
return err
}
cl.certMu.Lock()
cl.cert = &cert
cl.certMu.Unlock()
}
cl.certWatcher = &confwatcher.ConfWatcher{FilePath: cl.CertPath}
if err := cl.certWatcher.Initialize(); err != nil {
return err
}
cl.keyWatcher = &confwatcher.ConfWatcher{FilePath: cl.KeyPath}
if err := cl.keyWatcher.Initialize(); err != nil {
cl.certWatcher.Close() //nolint:errcheck
return err
}
go cl.watch()
return nil
}
func (cl *CertLoader) initializeAuto() (bool, error) {
_, keyErr := os.Stat(cl.KeyPath)
_, certErr := os.Stat(cl.CertPath)
if keyErr == nil && certErr == nil {
cert, err := tls.LoadX509KeyPair(cl.CertPath, cl.KeyPath)
if err != nil {
return false, err
}
cl.certMu.Lock()
cl.cert = &cert
cl.certMu.Unlock()
return true, nil
}
cl.Parent.Log(logger.Warn, "certificate %s not found, generating it from scratch", cl.KeyPath)
certPEM, keyPEM, err := generateTLSCert()
if err != nil {
return false, err
}
keyWriteErr := os.WriteFile(cl.KeyPath, keyPEM, 0o600)
if keyWriteErr != nil {
cl.Parent.Log(logger.Warn, "failed to save TLS key to %s: %v", cl.KeyPath, keyWriteErr)
}
certWriteErr := os.WriteFile(cl.CertPath, certPEM, 0o600)
if certWriteErr != nil {
cl.Parent.Log(logger.Warn, "failed to save TLS cert to %s: %v", cl.CertPath, certWriteErr)
}
cert, _ := tls.X509KeyPair(certPEM, keyPEM)
cl.certMu.Lock()
cl.cert = &cert
cl.certMu.Unlock()
return keyWriteErr == nil && certWriteErr == nil, nil
}
// Close closes a CertLoader and releases any underlying resources.
func (cl *CertLoader) Close() {
close(cl.done)
if cl.certWatcher != nil {
cl.certWatcher.Close() //nolint:errcheck
}
if cl.keyWatcher != nil {
cl.keyWatcher.Close() //nolint:errcheck
}
cl.certMu.Lock()
defer cl.certMu.Unlock()
cl.cert = nil
@@ -87,6 +183,7 @@ func (cl *CertLoader) watch() {
cl.certMu.Unlock()
cl.Parent.Log(logger.Info, "certificate reloaded after change to %s", cl.CertPath)
case <-cl.keyWatcher.Watch():
cert, err := tls.LoadX509KeyPair(cl.CertPath, cl.KeyPath)
if err != nil {
@@ -99,6 +196,7 @@ func (cl *CertLoader) watch() {
cl.certMu.Unlock()
cl.Parent.Log(logger.Info, "certificate reloaded after change to %s", cl.KeyPath)
case <-cl.done:
return
}
+17
View File
@@ -394,6 +394,15 @@ type Conf struct {
SRT bool `json:"srt"`
SRTAddress string `json:"srtAddress"`
// MoQ server
MoQ bool `json:"moq"`
MoQHTTPS2Address string `json:"moqHTTPS2Address"`
MoQHTTPS3Address string `json:"moqHTTPS3Address"`
MoQServerKey string `json:"moqServerKey"`
MoQServerCert string `json:"moqServerCert"`
MoQAllowOrigins []string `json:"moqAllowOrigins"`
MoQTrustedProxies IPNetworks `json:"moqTrustedProxies"`
// Record (deprecated)
Record *bool `json:"record,omitempty" deprecated:"true"`
RecordPath *string `json:"recordPath,omitempty" deprecated:"true"`
@@ -522,6 +531,14 @@ func (conf *Conf) setDefaults() {
conf.SRT = true
conf.SRTAddress = ":8890"
// MoQ server
conf.MoQ = true
conf.MoQHTTPS2Address = ":8892"
conf.MoQHTTPS3Address = ":8892"
conf.MoQServerKey = "auto.key"
conf.MoQServerCert = "auto.crt"
conf.MoQAllowOrigins = []string{"*"}
conf.PathDefaults.setDefaults()
}
+9 -2
View File
@@ -875,14 +875,14 @@ func TestAPIProtocolListGet(t *testing.T) {
"bytesAvailReceiveBuf": float64(0),
"bytesAvailSendBuf": float64(0),
"bytesReceiveBuf": float64(0),
"bytesReceived": float64(628),
"bytesReceived": out1.(map[string]any)["items"].([]any)[0].(map[string]any)["bytesReceived"],
"bytesReceivedBelated": float64(0),
"bytesReceivedDrop": float64(0),
"bytesReceivedLoss": float64(0),
"bytesReceivedRetrans": float64(0),
"bytesReceivedUndecrypt": float64(0),
"outboundFramesDiscarded": out1.(map[string]any)["items"].([]any)[0].(map[string]any)["outboundFramesDiscarded"],
"bytesReceivedUnique": float64(628),
"bytesReceivedUnique": out1.(map[string]any)["items"].([]any)[0].(map[string]any)["bytesReceivedUnique"],
"bytesRetrans": float64(0),
"bytesSendBuf": float64(0),
"bytesSendDrop": float64(0),
@@ -1001,6 +1001,13 @@ func TestAPIProtocolListGet(t *testing.T) {
case "hls muxers":
out2.(map[string]any)["lastRequest"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["lastRequest"]
case "srt":
out2.(map[string]any)["bytesReceived"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["bytesReceived"]
out2.(map[string]any)["bytesReceivedUnique"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["bytesReceivedUnique"]
out2.(map[string]any)["packetsReceivedACK"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["packetsReceivedACK"]
out2.(map[string]any)["packetsSentACK"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["packetsSentACK"]
out2.(map[string]any)["msRTT"] = out1.(map[string]any)["items"].([]any)[0].(map[string]any)["msRTT"]
}
require.Equal(t, out1.(map[string]any)["items"].([]any)[0], out2)
+47
View File
@@ -31,6 +31,7 @@ import (
"github.com/bluenviron/mediamtx/internal/recordcleaner"
"github.com/bluenviron/mediamtx/internal/rlimit"
"github.com/bluenviron/mediamtx/internal/servers/hls"
"github.com/bluenviron/mediamtx/internal/servers/moq"
"github.com/bluenviron/mediamtx/internal/servers/rtmp"
"github.com/bluenviron/mediamtx/internal/servers/rtsp"
"github.com/bluenviron/mediamtx/internal/servers/srt"
@@ -125,6 +126,7 @@ type Core struct {
hlsServer *hls.Server
webRTCServer *webrtc.Server
srtServer *srt.Server
moqServer *moq.Server
api *api.API
confWatcher *confwatcher.ConfWatcher
@@ -694,6 +696,29 @@ func (p *Core) createResources(initial bool) error {
p.srtServer = i
}
if p.conf.MoQ &&
p.moqServer == nil {
i := &moq.Server{
HTTPS2Address: p.conf.MoQHTTPS2Address,
HTTPS3Address: p.conf.MoQHTTPS3Address,
ServerKey: p.conf.MoQServerKey,
ServerCert: p.conf.MoQServerCert,
AllowOrigins: p.conf.MoQAllowOrigins,
TrustedProxies: p.conf.MoQTrustedProxies,
UDPReadBufferSize: p.conf.UDPReadBufferSize,
ReadTimeout: p.conf.ReadTimeout,
WriteTimeout: p.conf.WriteTimeout,
PathManager: p.pathManager,
Metrics: p.metrics,
Parent: p,
}
err = i.Initialize()
if err != nil {
return err
}
p.moqServer = i
}
if p.conf.API &&
p.api == nil {
i := &api.API{
@@ -718,6 +743,7 @@ func (p *Core) createResources(initial bool) error {
HLSServer: p.hlsServer,
WebRTCServer: p.webRTCServer,
SRTServer: p.srtServer,
MoQServer: p.moqServer,
Parent: p,
}
err = i.Initialize()
@@ -974,6 +1000,21 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closePathManager ||
closeLogger
closeMoQServer := newConf == nil ||
newConf.MoQ != p.conf.MoQ ||
newConf.MoQHTTPS2Address != p.conf.MoQHTTPS2Address ||
newConf.MoQHTTPS3Address != p.conf.MoQHTTPS3Address ||
newConf.MoQServerKey != p.conf.MoQServerKey ||
newConf.MoQServerCert != p.conf.MoQServerCert ||
!slices.Equal(newConf.MoQAllowOrigins, p.conf.MoQAllowOrigins) ||
!reflect.DeepEqual(newConf.MoQTrustedProxies, p.conf.MoQTrustedProxies) ||
newConf.UDPReadBufferSize != p.conf.UDPReadBufferSize ||
newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteTimeout != p.conf.WriteTimeout ||
closeMetrics ||
closePathManager ||
closeLogger
closeAPI := newConf == nil ||
newConf.API != p.conf.API ||
newConf.APIAddress != p.conf.APIAddress ||
@@ -994,6 +1035,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeHLSServer ||
closeWebRTCServer ||
closeSRTServer ||
closeMoQServer ||
closeLogger
if newConf == nil && p.confWatcher != nil {
@@ -1015,6 +1057,11 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
p.srtServer = nil
}
if closeMoQServer && p.moqServer != nil {
p.moqServer.Close()
p.moqServer = nil
}
if closeWebRTCServer && p.webRTCServer != nil {
p.webRTCServer.Close()
p.webRTCServer = nil
+10
View File
@@ -263,6 +263,11 @@ webrtc_sessions_rtp_packets_jitter 0
webrtc_sessions_rtcp_packets_received 0
webrtc_sessions_rtcp_packets_sent 0
# MoQ sessions
moq_sessions 0
moq_sessions_inbound_bytes 0
moq_sessions_outbound_bytes 0
`, string(bo))
})
@@ -497,6 +502,11 @@ webrtc_sessions_rtcp_packets_sent 0
"paths_bytes_received 0\n"+
"paths_bytes_sent 0\n"+
"paths_readers 0\n"+
"\n"+
"# MoQ sessions\n"+
"moq_sessions 0\n"+
"moq_sessions_inbound_bytes 0\n"+
"moq_sessions_outbound_bytes 0\n"+
"\n",
string(bo))
})
+2
View File
@@ -345,11 +345,13 @@ func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
return
}
if !req.AccessRequest.SkipAuth {
_, err2 := pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())
if err2 != nil {
req.Res <- defs.PathDescribeRes{Err: err2}
return
}
}
// create path if it doesn't exist
if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
+44
View File
@@ -0,0 +1,44 @@
package defs
import (
"time"
"github.com/google/uuid"
)
// APIMoQServer contains methods used by the API server.
type APIMoQServer interface {
APISessionsList() (*APIMoQSessionList, error)
APISessionsGet(uuid.UUID) (*APIMoQSession, error)
APISessionsKick(uuid.UUID) error
}
// APIMoQSessionState is the state of a MoQ session.
type APIMoQSessionState string
// states.
const (
APIMoQSessionStateIdle APIMoQSessionState = "idle"
APIMoQSessionStateRead APIMoQSessionState = "read"
APIMoQSessionStatePublish APIMoQSessionState = "publish"
)
// APIMoQSessionList is a list of MoQ sessions.
type APIMoQSessionList struct {
ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"`
Items []APIMoQSession `json:"items"`
}
// APIMoQSession is a MoQ session.
type APIMoQSession struct {
ID uuid.UUID `json:"id"`
Created time.Time `json:"created"`
RemoteAddr string `json:"remoteAddr"`
State APIMoQSessionState `json:"state"`
Path string `json:"path"`
Query string `json:"query"`
UserAgent string `json:"userAgent"`
InboundBytes uint64 `json:"inboundBytes"`
OutboundBytes uint64 `json:"outboundBytes"`
}
+2
View File
@@ -30,6 +30,7 @@ const (
APIPathSourceTypeRTPSource APIPathSourceType = "rtpSource"
APIPathSourceTypeWebRTCSession APIPathSourceType = "webRTCSession"
APIPathSourceTypeWebRTCSource APIPathSourceType = "webRTCSource"
APIPathSourceTypeMoQSession APIPathSourceType = "moqSession"
)
// APIPathSource is a source.
@@ -52,6 +53,7 @@ const (
APIPathReaderTypeRTSPSSession APIPathReaderType = "rtspsSession"
APIPathReaderTypeSRTConn APIPathReaderType = "srtConn"
APIPathReaderTypeWebRTCSession APIPathReaderType = "webRTCSession"
APIPathReaderTypeMoQSession APIPathReaderType = "moqSession"
APIPathReaderTypeHidden APIPathReaderType = "hidden"
)
+1 -1
View File
@@ -12,9 +12,9 @@ import (
type PathAccessRequest struct {
Name string
Query string
UserAgent string
Publish bool
SkipAuth bool
UserAgent string
// only if skipAuth = false
Proto auth.Protocol
+46 -4
View File
@@ -84,6 +84,7 @@ const (
metricsTypeRTMPSConns metricsType = "rtmps_conns"
metricsTypeSRTConns metricsType = "srt_conns"
metricsTypeWebRTCSessions metricsType = "webrtc_sessions"
metricsTypeMoQSessions metricsType = "moq_sessions"
)
type metricsAuthManager interface {
@@ -118,13 +119,13 @@ type Metrics struct {
rtmpsServer defs.APIRTMPServer
srtServer defs.APISRTServer
webRTCServer defs.APIWebRTCServer
moqServer defs.APIMoQServer
}
// Initialize initializes metrics.
func (m *Metrics) Initialize() error {
router := gin.New()
router.SetTrustedProxies(m.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.Use(m.middlewarePreflightRequests)
router.Use(m.middlewareAuth)
@@ -148,7 +149,7 @@ func (m *Metrics) Initialize() error {
return err
}
str := "listener opened on " + m.Address
str := "started with listener on " + m.Address
if !m.Encryption {
str += " (TCP/HTTP)"
} else {
@@ -161,7 +162,7 @@ func (m *Metrics) Initialize() error {
// Close closes Metrics.
func (m *Metrics) Close() {
m.Log(logger.Info, "listener is closing")
m.Log(logger.Info, "closing")
m.httpServer.Close()
}
@@ -223,6 +224,7 @@ func (m *Metrics) onMetrics(ctx *gin.Context) {
rtmpsServer := m.rtmpsServer
srtServer := m.srtServer
webRTCServer := m.webRTCServer
moqServer := m.moqServer
m.mutex.RUnlock()
typ := metricsType(ctx.Query("type"))
@@ -237,6 +239,7 @@ func (m *Metrics) onMetrics(ctx *gin.Context) {
rtmpsConnFilter := ctx.Query("rtmps_conn")
srtConnFilter := ctx.Query("srt_conn")
webrtcSessionFilter := ctx.Query("webrtc_session")
moqSessionFilter := ctx.Query("moq_session")
anyFilterActive := pathFilter != "" ||
hlsMuxerFilter != "" ||
@@ -248,7 +251,8 @@ func (m *Metrics) onMetrics(ctx *gin.Context) {
rtmpConnFilter != "" ||
rtmpsConnFilter != "" ||
srtConnFilter != "" ||
webrtcSessionFilter != ""
webrtcSessionFilter != "" ||
moqSessionFilter != ""
var out strings.Builder
@@ -993,6 +997,37 @@ func (m *Metrics) onMetrics(ctx *gin.Context) {
}
}
if !interfaceIsEmpty(moqServer) &&
(typ == "" || typ == metricsTypeMoQSessions) &&
(!anyFilterActive || moqSessionFilter != "") {
var data *defs.APIMoQSessionList
data, err := moqServer.APISessionsList()
if err == nil && len(data.Items) != 0 {
out.WriteString("# MoQ sessions\n")
for _, i := range data.Items {
if moqSessionFilter == "" || moqSessionFilter == i.ID.String() {
ta := tags(map[string]string{
"id": i.ID.String(),
"state": string(i.State),
"path": i.Path,
"remoteAddr": i.RemoteAddr,
})
metric(&out, "moq_sessions", ta, 1)
metric(&out, "moq_sessions_inbound_bytes", ta, int64(i.InboundBytes))
metric(&out, "moq_sessions_outbound_bytes", ta, int64(i.OutboundBytes))
}
}
out.WriteString("\n")
} else if moqSessionFilter == "" {
out.WriteString("# MoQ sessions\n")
metric(&out, "moq_sessions", "", 0)
metric(&out, "moq_sessions_inbound_bytes", "", 0)
metric(&out, "moq_sessions_outbound_bytes", "", 0)
out.WriteString("\n")
}
}
ctx.Writer.WriteHeader(http.StatusOK)
ctx.Writer.WriteString(out.String()) //nolint:errcheck
}
@@ -1052,3 +1087,10 @@ func (m *Metrics) SetWebRTCServer(s defs.APIWebRTCServer) {
defer m.mutex.Unlock()
m.webRTCServer = s
}
// SetMoQServer is called by core.
func (m *Metrics) SetMoQServer(s defs.APIMoQServer) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.moqServer = s
}
+75
View File
@@ -328,6 +328,32 @@ func (dummyWebRTCServer) APISessionsKick(uuid.UUID) error {
panic("unused")
}
type dummyMoQServer struct{}
func (dummyMoQServer) APISessionsList() (*defs.APIMoQSessionList, error) {
return &defs.APIMoQSessionList{
ItemCount: 1,
PageCount: 1,
Items: []defs.APIMoQSession{{
ID: uuid.MustParse("b47ac10b-58cc-4372-a567-0e02b2c3d479"),
Created: time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),
RemoteAddr: "127.0.0.2:3456",
State: defs.APIMoQSessionStatePublish,
Path: "mypath",
InboundBytes: 321,
OutboundBytes: 654,
}},
}, nil
}
func (dummyMoQServer) APISessionsGet(uuid.UUID) (*defs.APIMoQSession, error) {
panic("unused")
}
func (dummyMoQServer) APISessionsKick(uuid.UUID) error {
panic("unused")
}
type emptyPathManager struct{}
func (emptyPathManager) APIPathsList() (*defs.APIPathList, error) {
@@ -410,6 +436,20 @@ func (emptyWebRTCServer) APISessionsKick(uuid.UUID) error {
panic("unused")
}
type emptyMoQServer struct{}
func (emptyMoQServer) APISessionsList() (*defs.APIMoQSessionList, error) {
return &defs.APIMoQSessionList{}, nil
}
func (emptyMoQServer) APISessionsGet(uuid.UUID) (*defs.APIMoQSession, error) {
panic("unused")
}
func (emptyMoQServer) APISessionsKick(uuid.UUID) error {
panic("unused")
}
func TestPreflightRequest(t *testing.T) {
m := Metrics{
Address: "localhost:9998",
@@ -479,6 +519,7 @@ func TestMetrics(t *testing.T) {
m.SetRTMPSServer(&dummyRTMPServer{})
m.SetSRTServer(&dummySRTServer{})
m.SetWebRTCServer(&dummyWebRTCServer{})
m.SetMoQServer(&dummyMoQServer{})
tr := &http.Transport{}
defer tr.CloseIdleConnections()
@@ -821,6 +862,14 @@ func TestMetrics(t *testing.T) {
"remoteAddr=\"127.0.0.1:3455\",state=\"read\"} 123\n"+
"webrtc_sessions_rtcp_packets_sent{id=\"f47ac10b-58cc-4372-a567-0e02b2c3d479\",path=\"mypath\","+
"remoteAddr=\"127.0.0.1:3455\",state=\"read\"} 456\n"+
"\n"+
"# MoQ sessions\n"+
"moq_sessions{id=\"b47ac10b-58cc-4372-a567-0e02b2c3d479\",path=\"mypath\","+
"remoteAddr=\"127.0.0.2:3456\",state=\"publish\"} 1\n"+
"moq_sessions_inbound_bytes{id=\"b47ac10b-58cc-4372-a567-0e02b2c3d479\",path=\"mypath\","+
"remoteAddr=\"127.0.0.2:3456\",state=\"publish\"} 321\n"+
"moq_sessions_outbound_bytes{id=\"b47ac10b-58cc-4372-a567-0e02b2c3d479\",path=\"mypath\","+
"remoteAddr=\"127.0.0.2:3456\",state=\"publish\"} 654\n"+
"\n",
string(byts))
@@ -845,6 +894,7 @@ func TestZeroMetricsFallback(t *testing.T) {
m.SetRTSPServer(&emptyRTSPServer{})
m.SetSRTServer(&emptySRTServer{})
m.SetWebRTCServer(&emptyWebRTCServer{})
m.SetMoQServer(&emptyMoQServer{})
tr := &http.Transport{}
defer tr.CloseIdleConnections()
@@ -995,6 +1045,11 @@ func TestZeroMetricsFallback(t *testing.T) {
"webrtc_sessions_rtp_packets_jitter 0\n"+
"webrtc_sessions_rtcp_packets_received 0\n"+
"webrtc_sessions_rtcp_packets_sent 0\n"+
"\n"+
"# MoQ sessions\n"+
"moq_sessions 0\n"+
"moq_sessions_inbound_bytes 0\n"+
"moq_sessions_outbound_bytes 0\n"+
"\n",
string(byts))
}
@@ -1012,6 +1067,7 @@ func TestFilter(t *testing.T) {
"rtmps_conn",
"srt_conn",
"webrtc_session",
"moq_session",
} {
t.Run(ca, func(t *testing.T) {
m := Metrics{
@@ -1034,6 +1090,7 @@ func TestFilter(t *testing.T) {
m.SetRTMPSServer(&dummyRTMPServer{})
m.SetSRTServer(&dummySRTServer{})
m.SetWebRTCServer(&dummyWebRTCServer{})
m.SetMoQServer(&dummyMoQServer{})
tr := &http.Transport{}
defer tr.CloseIdleConnections()
@@ -1064,6 +1121,8 @@ func TestFilter(t *testing.T) {
u += "?srt_conn=a0b1c2d3-e4f5-6789-abcd-ef0123456789"
case "webrtc_session":
u += "?webrtc_session=f47ac10b-58cc-4372-a567-0e02b2c3d479"
case "moq_session":
u += "?moq_session=b47ac10b-58cc-4372-a567-0e02b2c3d479"
}
res, err := hc.Get(u)
@@ -1435,6 +1494,17 @@ func TestFilter(t *testing.T) {
`webrtc_sessions_rtcp_packets_sent{id="f47ac10b-58cc-4372-a567-0e02b2c3d479",`+
`path="mypath",remoteAddr="127.0.0.1:3455",state="read"} 456`+"\n\n",
string(byts))
case "moq_session":
require.Equal(t,
"# MoQ sessions\n"+
`moq_sessions{id="b47ac10b-58cc-4372-a567-0e02b2c3d479",`+
`path="mypath",remoteAddr="127.0.0.2:3456",state="publish"} 1`+"\n"+
`moq_sessions_inbound_bytes{id="b47ac10b-58cc-4372-a567-0e02b2c3d479",`+
`path="mypath",remoteAddr="127.0.0.2:3456",state="publish"} 321`+"\n"+
`moq_sessions_outbound_bytes{id="b47ac10b-58cc-4372-a567-0e02b2c3d479",`+
`path="mypath",remoteAddr="127.0.0.2:3456",state="publish"} 654`+"\n\n",
string(byts))
}
})
}
@@ -1463,6 +1533,7 @@ func TestFilterByType(t *testing.T) {
m.SetRTMPSServer(&dummyRTMPServer{})
m.SetSRTServer(&dummySRTServer{})
m.SetWebRTCServer(&dummyWebRTCServer{})
m.SetMoQServer(&dummyMoQServer{})
tr := &http.Transport{}
defer tr.CloseIdleConnections()
@@ -1661,6 +1732,7 @@ func TestMetricsConcurrentSettersAndReads(t *testing.T) {
m.SetRTMPSServer(&dummyRTMPServer{})
m.SetSRTServer(&dummySRTServer{})
m.SetWebRTCServer(&dummyWebRTCServer{})
m.SetMoQServer(&dummyMoQServer{})
tr := &http.Transport{}
defer tr.CloseIdleConnections()
@@ -1688,6 +1760,7 @@ func TestMetricsConcurrentSettersAndReads(t *testing.T) {
m.SetRTMPSServer(&dummyRTMPServer{})
m.SetSRTServer(&dummySRTServer{})
m.SetWebRTCServer(&dummyWebRTCServer{})
m.SetMoQServer(&dummyMoQServer{})
m.SetHLSServer(nil)
m.SetRTSPServer(nil)
@@ -1696,6 +1769,7 @@ func TestMetricsConcurrentSettersAndReads(t *testing.T) {
m.SetRTMPSServer(nil)
m.SetSRTServer(nil)
m.SetWebRTCServer(nil)
m.SetMoQServer(nil)
}
readers.Wait()
@@ -1752,6 +1826,7 @@ func BenchmarkFullMetricsHandler(b *testing.B) {
m.SetRTMPSServer(&dummyRTMPServer{})
m.SetSRTServer(&dummySRTServer{})
m.SetWebRTCServer(&dummyWebRTCServer{})
m.SetMoQServer(&dummyMoQServer{})
tr := &http.Transport{}
defer tr.CloseIdleConnections()
+2 -3
View File
@@ -43,7 +43,6 @@ type Server struct {
func (s *Server) Initialize() error {
router := gin.New()
router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.Use(s.middlewarePreflightRequests)
router.GET("/list", s.onList)
@@ -67,7 +66,7 @@ func (s *Server) Initialize() error {
return err
}
str := "listener opened on " + s.Address
str := "started with listener on " + s.Address
if !s.Encryption {
str += " (TCP/HTTP)"
} else {
@@ -80,7 +79,7 @@ func (s *Server) Initialize() error {
// Close closes Server.
func (s *Server) Close() {
s.Log(logger.Info, "listener is closing")
s.Log(logger.Info, "closing")
s.httpServer.Close()
}
+2 -3
View File
@@ -46,7 +46,6 @@ type PPROF struct {
func (pp *PPROF) Initialize() error {
router := gin.New()
router.SetTrustedProxies(pp.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.Use(pp.middlewarePreflightRequests)
router.Use(pp.middlewareAuth)
@@ -70,7 +69,7 @@ func (pp *PPROF) Initialize() error {
return err
}
str := "listener opened on " + pp.Address
str := "started with listener on " + pp.Address
if !pp.Encryption {
str += " (TCP/HTTP)"
} else {
@@ -83,7 +82,7 @@ func (pp *PPROF) Initialize() error {
// Close closes PPROF.
func (pp *PPROF) Close() {
pp.Log(logger.Info, "listener is closing")
pp.Log(logger.Info, "closing")
pp.httpServer.Close()
}
+13 -2
View File
@@ -25,8 +25,8 @@ func (nilWriter) Write(p []byte) (int, error) {
}
// Server is a wrapper around http.Server that provides:
// - net.Listener allocation and closure
// - TLS allocation
// - net.Listener creation and destruction
// - TLS initialization and hot reload
// - exit on panic
// - logging
// - server header
@@ -41,6 +41,7 @@ type Server struct {
Encryption bool
ServerCert string
ServerKey string
AllowAutoCert bool
Handler http.Handler
Parent logger.Writer
@@ -69,6 +70,7 @@ func (s *Server) Initialize() error {
s.loader = &certloader.CertLoader{
CertPath: s.ServerCert,
KeyPath: s.ServerKey,
AllowAuto: s.AllowAutoCert,
Parent: s.Parent,
}
err := s.loader.Initialize()
@@ -121,6 +123,9 @@ func (s *Server) Initialize() error {
if tlsConfig != nil {
err := http2.ConfigureServer(s.inner, &http2.Server{})
if err != nil {
if s.loader != nil {
s.loader.Close()
}
return err
}
}
@@ -145,12 +150,18 @@ func (s *Server) Initialize() error {
var err error
s.ln, err = tlsListen(network, address, tlsConfig)
if err != nil {
if s.loader != nil {
s.loader.Close()
}
return err
}
} else {
var err error
s.ln, err = listen(network, address)
if err != nil {
if s.loader != nil {
s.loader.Close()
}
return err
}
}
+197
View File
@@ -0,0 +1,197 @@
// Package httpp3 contains HTTP/3 utilities.
package httpp3
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/http"
"os"
"sync"
"time"
"github.com/bluenviron/gortsplib/v5/pkg/readbuffer"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/webtransport-go"
)
const (
// WebTransport requires certificates valid for at most 14 days.
certValidity = 14 * 24 * time.Hour
certRotation = 13 * 24 * time.Hour
)
func generateWebTransportCert() (*tls.Certificate, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate key: %w", err)
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, fmt.Errorf("generate serial: %w", err)
}
now := time.Now()
template := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: "mediamtx"},
NotBefore: now,
NotAfter: now.Add(certValidity),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return nil, fmt.Errorf("create certificate: %w", err)
}
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
return nil, fmt.Errorf("marshal key: %w", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, fmt.Errorf("parse certificate: %w", err)
}
return &cert, nil
}
// Server is a wrapper around http3.Server that provides:
// - net.PacketConn creation and destruction
// - JIT TLS certificate generation and rotation (WebTransport-compatible: ECDSA P-256, 14-day validity)
type Server struct {
Address string
UDPReadBufferSize uint
EnableWebTransport bool
Handler http.Handler
Parent logger.Writer
ln net.PacketConn
inner *http3.Server
wtServer *webtransport.Server
cert *tls.Certificate
certMu sync.RWMutex
terminate chan struct{}
}
// Initialize initializes a Server.
func (s *Server) Initialize() error {
os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true") //nolint:errcheck
s.terminate = make(chan struct{})
var err error
s.ln, err = net.ListenPacket("udp", s.Address)
if err != nil {
return err
}
if s.UDPReadBufferSize != 0 {
err = readbuffer.SetReadBuffer(s.ln.(*net.UDPConn), int(s.UDPReadBufferSize))
if err != nil {
s.ln.Close()
return err
}
}
cert, err := generateWebTransportCert()
if err != nil {
return err
}
s.cert = cert
tlsConfig := &tls.Config{
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
s.certMu.RLock()
defer s.certMu.RUnlock()
return s.cert, nil
},
}
s.inner = &http3.Server{
Handler: s.Handler,
TLSConfig: tlsConfig,
}
if s.EnableWebTransport {
// webtransport.Server.Serve calls quic.ListenEarly directly with s.H3.TLSConfig,
// bypassing the http3.ConfigureTLSConfig call that http3.Server.Serve does internally.
// Pre-configure to set NextProtos = ["h3"] so QUIC ALPN negotiation succeeds.
s.inner.TLSConfig = http3.ConfigureTLSConfig(tlsConfig)
webtransport.ConfigureHTTP3Server(s.inner)
s.wtServer = &webtransport.Server{
H3: s.inner,
CheckOrigin: func(_ *http.Request) bool { return true },
}
go s.wtServer.Serve(s.ln) //nolint:errcheck
} else {
go s.inner.Serve(s.ln) //nolint:errcheck
}
go s.rotateCert()
return nil
}
func (s *Server) rotateCert() {
t := time.NewTimer(certRotation)
defer t.Stop()
for {
select {
case <-t.C:
cert, err := generateWebTransportCert()
if err != nil {
s.Parent.Log(logger.Error, "failed to rotate WebTransport certificate: %v", err)
} else {
s.certMu.Lock()
s.cert = cert
s.certMu.Unlock()
s.Parent.Log(logger.Info, "WebTransport certificate rotated")
}
t.Reset(certRotation)
case <-s.terminate:
return
}
}
}
// Close closes all resources and waits for all routines to return.
func (s *Server) Close() {
close(s.terminate)
s.ln.Close()
if s.wtServer != nil {
s.wtServer.Close() //nolint:errcheck
} else {
s.inner.Close() //nolint:errcheck
}
}
// Certificate returns the current TLS certificate.
func (s *Server) Certificate() *tls.Certificate {
s.certMu.RLock()
defer s.certMu.RUnlock()
return s.cert
}
// Upgrade upgrades an HTTP/3 request to a WebTransport session.
// Only valid when EnableWebTransport is true.
func (s *Server) Upgrade(w http.ResponseWriter, r *http.Request) (*webtransport.Session, error) {
return s.wtServer.Upgrade(w, r)
}
+43
View File
@@ -0,0 +1,43 @@
package httpp3
import (
"crypto/tls"
"io"
"net/http"
"testing"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/quic-go/quic-go/http3"
"github.com/stretchr/testify/require"
)
func TestServer(t *testing.T) {
// test that Server can be initialized and reached by a simple GET request
s := &Server{
Address: "127.0.0.1:18443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}),
Parent: test.NilLogger,
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http3.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
},
}
defer tr.Close() //nolint:errcheck
hc := &http.Client{Transport: tr}
res, err := hc.Get("https://127.0.0.1:18443/")
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
_, err = io.ReadAll(res.Body)
require.NoError(t, err)
}
+26
View File
@@ -0,0 +1,26 @@
// Package catalog contains a media catalog.
package catalog
// Catalog is a media catalog.
// spec: draft-ietf-moq-msf-00
type Catalog struct {
Version int `json:"version"`
Tracks []Track `json:"tracks"`
}
// Track is a track in a media catalog.
type Track struct {
Name string `json:"name"`
Packaging string `json:"packaging"`
IsLive bool `json:"isLive"`
Namespace string `json:"namespace,omitempty"`
Codec string `json:"codec,omitempty"`
Bitrate int `json:"bitrate,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Framerate float64 `json:"framerate,omitempty"`
Samplerate int `json:"samplerate,omitempty"`
Channels int `json:"channels,omitempty"`
ClockRate int `json:"clockrate,omitempty"`
InitData string `json:"initData,omitempty"`
}
@@ -0,0 +1,65 @@
// Package controlmessage contains MoQ control messages.
package controlmessage
import (
"fmt"
"io"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
// Message is a control message.
type Message interface {
// Marshal encodes the message to bytes.
Marshal() []byte
isMessage()
unmarshal(buf []byte) error
}
// Read reads a Message from a Reader.
func Read(r io.Reader) (Message, error) {
var t varint.Varint
if err := t.Read(r); err != nil {
return nil, err
}
var lenBuf [2]byte
_, err := io.ReadFull(r, lenBuf[:])
if err != nil {
return nil, err
}
length := uint16(lenBuf[0])<<8 | uint16(lenBuf[1])
payload := make([]byte, length)
_, err = io.ReadFull(r, payload)
if err != nil {
return nil, err
}
var m Message
switch t {
case typeSetup:
m = &Setup{}
case typeSubscribe:
m = &Subscribe{}
case typeSubscribeOk:
m = &SubscribeOk{}
case typeRequestError:
m = &RequestError{}
case typePublish:
m = &Publish{}
case typeRequestOk:
m = &RequestOk{}
default:
return nil, fmt.Errorf("unknown message type: 0x%x", t)
}
err = m.unmarshal(payload)
if err != nil {
return nil, err
}
return m, nil
}
@@ -0,0 +1,174 @@
package controlmessage
import (
"bytes"
"testing"
"github.com/bluenviron/mediamtx/internal/protocols/moq/parameter"
"github.com/stretchr/testify/require"
)
var cases = []struct {
name string
enc []byte
dec Message
}{
{
name: "setup",
enc: []byte{
0xAF, 0x00, // type 0x2F00 (2-byte varint)
0x00, 0x00, // length = 0
},
dec: &Setup{},
},
{
name: "subscribe",
enc: []byte{
0x03, // type 0x03
0x00, 0x0B, // length = 11
0x01, // RequestID = 1
0x01, // namespace count = 1
0x03, 0x66, 0x6F, 0x6F, // namespace[0] = "foo"
0x03, 0x62, 0x61, 0x72, // track name = "bar"
0x00, // parameters count = 0
},
dec: &Subscribe{
RequestID: 1,
Namespace: []string{"foo"},
TrackName: "bar",
},
},
{
name: "subscribe with params",
enc: []byte{
0x03, // type 0x03
0x00, 0x15, // length = 21
0x01, // RequestID = 1
0x01, // namespace count = 1
0x03, 0x66, 0x6F, 0x6F, // namespace[0] = "foo"
0x03, 0x62, 0x61, 0x72, // track name = "bar"
0x01, // parameters count = 1
0x03, // type delta = 3 (AuthorizationToken)
0x08, // inner length = 8
0x03, // alias type = UseValue
0x01, // token type = 1
0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // token value = "secret"
},
dec: &Subscribe{
RequestID: 1,
Namespace: []string{"foo"},
TrackName: "bar",
Parameters: []parameter.Parameter{
&parameter.AuthorizationToken{
AliasType: parameter.AuthorizationTokenAliasTypeUseValue,
TokenType: 1,
TokenValue: []byte("secret"),
},
},
},
},
{
name: "subscribe_ok",
enc: []byte{
0x04, // type 0x04
0x00, 0x02, // length = 2
0x01, // TrackAlias = 1
0x00, // Number of Parameters = 0
},
dec: &SubscribeOk{
TrackAlias: 1,
},
},
{
name: "request_error",
enc: []byte{
0x05, // type 0x05
0x00, 0x06, // length = 6
0x01, // Code = 1
0x00, // retryInterval = 0 (ignored)
0x03, 0x66, 0x6F, 0x6F, // Reason = "foo"
},
dec: &RequestError{
Code: 1,
Reason: "foo",
},
},
{
name: "request_ok",
enc: []byte{
0x07, // type 0x07
0x00, 0x01, // length = 1
0x00, // Number of Parameters = 0
},
dec: &RequestOk{},
},
{
name: "publish",
enc: []byte{
0x1D, // type 0x1D
0x00, 0x0C, // length = 12
0x01, // RequestID = 1
0x01, // namespace count = 1
0x03, 0x66, 0x6F, 0x6F, // namespace[0] = "foo"
0x03, 0x62, 0x61, 0x72, // track name = "bar"
0x02, // TrackAlias = 2
0x00, // parameters count = 0
},
dec: &Publish{
RequestID: 1,
Namespace: []string{"foo"},
TrackName: "bar",
TrackAlias: 2,
},
},
{
name: "publish with parameters",
enc: []byte{
0x1D, // type 0x1D
0x00, 0x16, // length = 22
0x01, // RequestID = 1
0x01, // namespace count = 1
0x03, 0x66, 0x6F, 0x6F, // namespace[0] = "foo"
0x03, 0x62, 0x61, 0x72, // track name = "bar"
0x02, // TrackAlias = 2
0x01, // parameters count = 1
0x03, // type delta = 3 (AuthorizationToken)
0x08, // inner length = 8
0x03, // alias type = UseValue
0x01, // token type = 1
0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // token value = "secret"
},
dec: &Publish{
RequestID: 1,
Namespace: []string{"foo"},
TrackName: "bar",
TrackAlias: 2,
Parameters: []parameter.Parameter{
&parameter.AuthorizationToken{
AliasType: parameter.AuthorizationTokenAliasTypeUseValue,
TokenType: 1,
TokenValue: []byte("secret"),
},
},
},
},
}
func TestUnmarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
m, err := Read(bytes.NewReader(ca.enc))
require.NoError(t, err)
require.Equal(t, ca.dec, m)
})
}
}
func TestMarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
buf := ca.dec.Marshal()
require.Equal(t, ca.enc, buf)
})
}
}
@@ -0,0 +1,145 @@
package controlmessage
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/moq/parameter"
"github.com/bluenviron/mediamtx/internal/protocols/moq/property"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
const typePublish varint.Varint = 0x1d
// Publish is the PUBLISH control message.
// spec: draft-18, section 10.10
type Publish struct {
RequestID uint64
Namespace []string
TrackName string
TrackAlias uint64
Parameters parameter.Parameters
TrackProperties property.Properties
}
func (*Publish) isMessage() {}
func (m *Publish) unmarshal(buf []byte) error {
var requestID varint.Varint
n, err := requestID.Unmarshal(buf)
if err != nil {
return err
}
m.RequestID = uint64(requestID)
buf = buf[n:]
var nsCount varint.Varint
n, err = nsCount.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
m.Namespace = make([]string, nsCount)
for i := range m.Namespace {
var l varint.Varint
n, err = l.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
if len(buf) < int(l) {
return fmt.Errorf("not enough bytes for namespace part")
}
m.Namespace[i] = string(buf[:l])
buf = buf[int(l):]
}
var tnLen varint.Varint
n, err = tnLen.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
if len(buf) < int(tnLen) {
return fmt.Errorf("not enough bytes for track name")
}
m.TrackName = string(buf[:tnLen])
buf = buf[int(tnLen):]
var trackAlias varint.Varint
n, err = trackAlias.Unmarshal(buf)
if err != nil {
return err
}
m.TrackAlias = uint64(trackAlias)
buf = buf[n:]
var paramCount varint.Varint
n, err = paramCount.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
consumed, err := m.Parameters.Unmarshal(int(paramCount), buf)
if err != nil {
return err
}
buf = buf[consumed:]
return m.TrackProperties.Unmarshal(buf)
}
func (m Publish) marshalSize() int {
n := varint.Varint(m.RequestID).MarshalSize() +
varint.Varint(len(m.Namespace)).MarshalSize()
for _, part := range m.Namespace {
n += varint.Varint(len(part)).MarshalSize() + len(part)
}
n += varint.Varint(len(m.TrackName)).MarshalSize() + len(m.TrackName)
n += varint.Varint(m.TrackAlias).MarshalSize()
n += varint.Varint(len(m.Parameters)).MarshalSize()
n += m.Parameters.MarshalSize()
n += m.TrackProperties.MarshalSize()
return typePublish.MarshalSize() + 2 + n
}
func (m Publish) marshalTo(buf []byte) int {
payloadSize := varint.Varint(m.RequestID).MarshalSize() +
varint.Varint(len(m.Namespace)).MarshalSize()
for _, part := range m.Namespace {
payloadSize += varint.Varint(len(part)).MarshalSize() + len(part)
}
payloadSize += varint.Varint(len(m.TrackName)).MarshalSize() + len(m.TrackName)
payloadSize += varint.Varint(m.TrackAlias).MarshalSize()
payloadSize += varint.Varint(len(m.Parameters)).MarshalSize()
payloadSize += m.Parameters.MarshalSize()
payloadSize += m.TrackProperties.MarshalSize()
n := typePublish.MarshalTo(buf)
buf[n] = byte(payloadSize >> 8)
buf[n+1] = byte(payloadSize)
n += 2
n += varint.Varint(m.RequestID).MarshalTo(buf[n:])
n += varint.Varint(len(m.Namespace)).MarshalTo(buf[n:])
for _, part := range m.Namespace {
n += varint.Varint(len(part)).MarshalTo(buf[n:])
n += copy(buf[n:], part)
}
n += varint.Varint(len(m.TrackName)).MarshalTo(buf[n:])
n += copy(buf[n:], m.TrackName)
n += varint.Varint(m.TrackAlias).MarshalTo(buf[n:])
n += varint.Varint(len(m.Parameters)).MarshalTo(buf[n:])
n += m.Parameters.MarshalTo(buf[n:])
n += m.TrackProperties.MarshalTo(buf[n:])
return n
}
// Marshal implements Message.
func (m Publish) Marshal() []byte {
buf := make([]byte, m.marshalSize())
m.marshalTo(buf)
return buf
}
@@ -0,0 +1,87 @@
package controlmessage
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
const typeRequestError varint.Varint = 0x05
// RequestErrorCode is a code of REQUEST_ERROR.
type RequestErrorCode uint64
// spec: draft-18, section 15.10.2
const (
RequestErrorCodeUnauthorized RequestErrorCode = 0x01
RequestErrorCodeNotSupported RequestErrorCode = 0x03
RequestErrorCodeDoesNotExist RequestErrorCode = 0x10
RequestErrorCodeUninterested RequestErrorCode = 0x20
)
// RequestError is the REQUEST_ERROR control message.
// spec: draft-18, section 10.6.2
type RequestError struct {
Code RequestErrorCode
Reason string
}
func (*RequestError) isMessage() {}
func (m *RequestError) unmarshal(buf []byte) error {
var code varint.Varint
n, err := code.Unmarshal(buf)
if err != nil {
return err
}
m.Code = RequestErrorCode(code)
buf = buf[n:]
var retry varint.Varint
n, err = retry.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
var l varint.Varint
n, err = l.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
if len(buf) < int(l) {
return fmt.Errorf("not enough bytes")
}
m.Reason = string(buf[:l])
return nil
}
func (m RequestError) marshalSize() int {
payloadSize := varint.Varint(m.Code).MarshalSize() + 1 +
varint.Varint(len(m.Reason)).MarshalSize() + len(m.Reason)
return typeRequestError.MarshalSize() + 2 + payloadSize
}
func (m RequestError) marshalTo(buf []byte) int {
payloadSize := varint.Varint(m.Code).MarshalSize() + 1 +
varint.Varint(len(m.Reason)).MarshalSize() + len(m.Reason)
n := typeRequestError.MarshalTo(buf)
buf[n] = byte(payloadSize >> 8)
buf[n+1] = byte(payloadSize)
n += 2
n += varint.Varint(m.Code).MarshalTo(buf[n:])
buf[n] = 0x00
n++
n += varint.Varint(len(m.Reason)).MarshalTo(buf[n:])
n += copy(buf[n:], m.Reason)
return n
}
// Marshal implements Message.
func (m RequestError) Marshal() []byte {
buf := make([]byte, m.marshalSize())
m.marshalTo(buf)
return buf
}
@@ -0,0 +1,63 @@
package controlmessage
import (
"github.com/bluenviron/mediamtx/internal/protocols/moq/parameter"
"github.com/bluenviron/mediamtx/internal/protocols/moq/property"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
const typeRequestOk varint.Varint = 0x07
// RequestOk is the REQUEST_OK control message.
// spec: draft-18, section 10.5
type RequestOk struct {
Parameters parameter.Parameters
TrackProperties property.Properties
}
func (*RequestOk) isMessage() {}
func (m *RequestOk) unmarshal(buf []byte) error {
var numParams varint.Varint
n, err := numParams.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
consumed, err := m.Parameters.Unmarshal(int(numParams), buf)
if err != nil {
return err
}
buf = buf[consumed:]
return m.TrackProperties.Unmarshal(buf)
}
func (m RequestOk) marshalSize() int {
payloadSize := varint.Varint(len(m.Parameters)).MarshalSize() +
m.Parameters.MarshalSize() +
m.TrackProperties.MarshalSize()
return typeRequestOk.MarshalSize() + 2 + payloadSize
}
func (m RequestOk) marshalTo(buf []byte) int {
payloadSize := varint.Varint(len(m.Parameters)).MarshalSize() +
m.Parameters.MarshalSize() +
m.TrackProperties.MarshalSize()
n := typeRequestOk.MarshalTo(buf)
buf[n] = byte(payloadSize >> 8)
buf[n+1] = byte(payloadSize)
n += 2
n += varint.Varint(len(m.Parameters)).MarshalTo(buf[n:])
n += m.Parameters.MarshalTo(buf[n:])
n += m.TrackProperties.MarshalTo(buf[n:])
return n
}
// Marshal implements Message.
func (m RequestOk) Marshal() []byte {
buf := make([]byte, m.marshalSize())
m.marshalTo(buf)
return buf
}
@@ -0,0 +1,31 @@
package controlmessage
import "github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
const typeSetup varint.Varint = 0x2F00
// Setup is the SETUP control message.
// spec: draft-18, section 10.3
type Setup struct{}
func (*Setup) isMessage() {}
func (*Setup) unmarshal(_ []byte) error { return nil }
func (Setup) marshalSize() int {
return typeSetup.MarshalSize() + 2
}
func (Setup) marshalTo(buf []byte) int {
pos := typeSetup.MarshalTo(buf)
buf[pos] = 0x00
buf[pos+1] = 0x00
return pos + 2
}
// Marshal implements Message.
func (Setup) Marshal() []byte {
buf := make([]byte, Setup{}.marshalSize())
Setup{}.marshalTo(buf)
return buf
}
@@ -0,0 +1,128 @@
package controlmessage
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/moq/parameter"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
const typeSubscribe varint.Varint = 0x03
// Subscribe is the SUBSCRIBE control message.
// spec: draft-18, section 10.7
type Subscribe struct {
RequestID uint64
Namespace []string
TrackName string
Parameters parameter.Parameters
}
func (*Subscribe) isMessage() {}
func (m *Subscribe) unmarshal(buf []byte) error {
var requestID varint.Varint
n, err := requestID.Unmarshal(buf)
if err != nil {
return err
}
m.RequestID = uint64(requestID)
buf = buf[n:]
var nsCount varint.Varint
n, err = nsCount.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
m.Namespace = make([]string, nsCount)
for i := range m.Namespace {
var l varint.Varint
n, err = l.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
if len(buf) < int(l) {
return fmt.Errorf("not enough bytes for namespace part")
}
m.Namespace[i] = string(buf[:l])
buf = buf[int(l):]
}
var tnLen varint.Varint
n, err = tnLen.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
if len(buf) < int(tnLen) {
return fmt.Errorf("not enough bytes for track name")
}
m.TrackName = string(buf[:tnLen])
buf = buf[int(tnLen):]
var paramCount varint.Varint
n, err = paramCount.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
_, err = m.Parameters.Unmarshal(int(paramCount), buf)
if err != nil {
return err
}
return nil
}
func (m Subscribe) marshalSize() int {
n := varint.Varint(m.RequestID).MarshalSize() +
varint.Varint(len(m.Namespace)).MarshalSize()
for _, part := range m.Namespace {
n += varint.Varint(len(part)).MarshalSize() + len(part)
}
n += varint.Varint(len(m.TrackName)).MarshalSize() + len(m.TrackName)
n += varint.Varint(len(m.Parameters)).MarshalSize()
n += m.Parameters.MarshalSize()
return typeSubscribe.MarshalSize() + 2 + n
}
func (m Subscribe) marshalTo(buf []byte) int {
payloadSize := varint.Varint(m.RequestID).MarshalSize() +
varint.Varint(len(m.Namespace)).MarshalSize()
for _, part := range m.Namespace {
payloadSize += varint.Varint(len(part)).MarshalSize() + len(part)
}
payloadSize += varint.Varint(len(m.TrackName)).MarshalSize() + len(m.TrackName)
payloadSize += varint.Varint(len(m.Parameters)).MarshalSize()
payloadSize += m.Parameters.MarshalSize()
n := typeSubscribe.MarshalTo(buf)
buf[n] = byte(payloadSize >> 8)
buf[n+1] = byte(payloadSize)
n += 2
n += varint.Varint(m.RequestID).MarshalTo(buf[n:])
n += varint.Varint(len(m.Namespace)).MarshalTo(buf[n:])
for _, part := range m.Namespace {
n += varint.Varint(len(part)).MarshalTo(buf[n:])
n += copy(buf[n:], part)
}
n += varint.Varint(len(m.TrackName)).MarshalTo(buf[n:])
n += copy(buf[n:], m.TrackName)
n += varint.Varint(len(m.Parameters)).MarshalTo(buf[n:])
n += m.Parameters.MarshalTo(buf[n:])
return n
}
// Marshal implements Message.
func (m Subscribe) Marshal() []byte {
buf := make([]byte, m.marshalSize())
m.marshalTo(buf)
return buf
}
@@ -0,0 +1,75 @@
package controlmessage
import (
"github.com/bluenviron/mediamtx/internal/protocols/moq/parameter"
"github.com/bluenviron/mediamtx/internal/protocols/moq/property"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
const typeSubscribeOk varint.Varint = 0x04
// SubscribeOk is the SUBSCRIBE_OK control message.
// spec: draft-18, section 10.8
type SubscribeOk struct {
TrackAlias uint64
Parameters parameter.Parameters
TrackProperties property.Properties
}
func (*SubscribeOk) isMessage() {}
func (m *SubscribeOk) unmarshal(buf []byte) error {
var v varint.Varint
n, err := v.Unmarshal(buf)
if err != nil {
return err
}
m.TrackAlias = uint64(v)
buf = buf[n:]
var numParams varint.Varint
n, err = numParams.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
consumed, err := m.Parameters.Unmarshal(int(numParams), buf)
if err != nil {
return err
}
buf = buf[consumed:]
return m.TrackProperties.Unmarshal(buf)
}
func (m SubscribeOk) marshalSize() int {
payloadSize := varint.Varint(m.TrackAlias).MarshalSize() +
varint.Varint(len(m.Parameters)).MarshalSize() +
m.Parameters.MarshalSize() +
m.TrackProperties.MarshalSize()
return typeSubscribeOk.MarshalSize() + 2 + payloadSize
}
func (m SubscribeOk) marshalTo(buf []byte) int {
payloadSize := varint.Varint(m.TrackAlias).MarshalSize() +
varint.Varint(len(m.Parameters)).MarshalSize() +
m.Parameters.MarshalSize() +
m.TrackProperties.MarshalSize()
n := typeSubscribeOk.MarshalTo(buf)
buf[n] = byte(payloadSize >> 8)
buf[n+1] = byte(payloadSize)
n += 2
n += varint.Varint(m.TrackAlias).MarshalTo(buf[n:])
n += varint.Varint(len(m.Parameters)).MarshalTo(buf[n:])
n += m.Parameters.MarshalTo(buf[n:])
n += m.TrackProperties.MarshalTo(buf[n:])
return n
}
// Marshal implements Message.
func (m SubscribeOk) Marshal() []byte {
buf := make([]byte, m.marshalSize())
m.marshalTo(buf)
return buf
}
+318
View File
@@ -0,0 +1,318 @@
// Package moq contains Media-over-QUIC utilities.
package moq
import (
"encoding/base64"
"encoding/hex"
"strings"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/flac"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/g711"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9"
"github.com/bluenviron/mediamtx/internal/unit"
)
// FromStream maps a MediaMTX stream to a Media-over-QUIC stream.
func FromStream(desc *description.Session) ([]*Track, error) {
var tracks []*Track
for _, media := range desc.Medias {
for _, forma := range media.Formats {
switch forma := forma.(type) {
case *format.AV1:
firstRandomAccess := false
track := &Track{
Codec: "av01.0.04M.08",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !av1.IsRandomAccess2(u.Payload.(unit.PayloadAV1)) {
return nil
}
firstRandomAccess = true
payload, err := av1.Bitstream([][]byte(u.Payload.(unit.PayloadAV1))).Marshal()
if err != nil {
return err
}
return wrapped(payload, u.PTS)
},
}
tracks = append(tracks, track)
case *format.VP9:
firstRandomAccess := false
track := &Track{
Codec: "vp09.00.10.08",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !vp9.IsRandomAccess(u.Payload.(unit.PayloadVP9)) {
return nil
}
firstRandomAccess = true
return wrapped(u.Payload.(unit.PayloadVP9), u.PTS)
},
}
tracks = append(tracks, track)
case *format.VP8:
firstRandomAccess := false
track := &Track{
Codec: "vp8",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !vp8.IsRandomAccess(u.Payload.(unit.PayloadVP8)) {
return nil
}
firstRandomAccess = true
return wrapped(u.Payload.(unit.PayloadVP8), u.PTS)
},
}
tracks = append(tracks, track)
case *format.H265:
firstRandomAccess := false
track := &Track{
Codec: "hev1.1.6.L93.B0",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !h265.IsRandomAccess(u.Payload.(unit.PayloadH265)) {
return nil
}
firstRandomAccess = true
payload, err := h264.AVCC(u.Payload.(unit.PayloadH265)).Marshal()
if err != nil {
return err
}
return wrapped(payload, u.PTS)
},
}
tracks = append(tracks, track)
case *format.H264:
firstRandomAccess := false
track := &Track{
Codec: "avc3.640028",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !h264.IsRandomAccess(u.Payload.(unit.PayloadH264)) {
return nil
}
firstRandomAccess = true
payload, err := h264.AVCC(u.Payload.(unit.PayloadH264)).Marshal()
if err != nil {
return err
}
return wrapped(payload, u.PTS)
},
}
tracks = append(tracks, track)
case *format.Opus:
track := &Track{
Codec: "opus",
Samplerate: 48000,
Channels: forma.ChannelCount,
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
for _, pkt := range u.Payload.(unit.PayloadOpus) {
err := wrapped(pkt, u.PTS)
if err != nil {
return err
}
}
return nil
},
}
tracks = append(tracks, track)
case *format.Generic:
if strings.HasPrefix(strings.ToLower(forma.RTPMap()), "flac/") {
enc, err := hex.DecodeString(forma.FMT["streaminfo"])
if err != nil {
return nil, err
}
var streamInfo flac.StreamInfo
err = streamInfo.Unmarshal(enc)
if err != nil {
return nil, err
}
track := &Track{
Codec: "flac",
Samplerate: int(streamInfo.SampleRate),
Channels: int(streamInfo.ChannelCount),
InitData: base64.StdEncoding.EncodeToString(enc),
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
return wrapped(u.Payload.(unit.PayloadFLAC), u.PTS)
},
}
tracks = append(tracks, track)
}
case *format.MPEG4Audio:
if forma.Config != nil {
enc, err := forma.Config.Marshal()
if err != nil {
return nil, err
}
track := &Track{
Codec: "mp4a.40.2",
Samplerate: forma.Config.SampleRate,
Channels: int(forma.Config.ChannelConfig),
InitData: base64.StdEncoding.EncodeToString(enc),
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
pts := u.PTS
for _, au := range u.Payload.(unit.PayloadMPEG4Audio) {
err2 := wrapped(au, pts)
if err2 != nil {
return err2
}
pts += mpeg4audio.SamplesPerAccessUnit
}
return nil
},
}
tracks = append(tracks, track)
}
case *format.G711:
track := &Track{
Codec: "pcm-s16",
Samplerate: forma.SampleRate,
Channels: forma.ChannelCount,
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
var bigEndian []byte
if forma.MULaw {
var mu g711.Mulaw
mu.Unmarshal(u.Payload.(unit.PayloadG711))
bigEndian = mu
} else {
var al g711.Alaw
al.Unmarshal(u.Payload.(unit.PayloadG711))
bigEndian = al
}
swapped := make([]byte, len(bigEndian))
for i := 0; i+2 <= len(bigEndian); i += 2 {
swapped[i], swapped[i+1] = bigEndian[i+1], bigEndian[i]
}
return wrapped(swapped, u.PTS)
},
}
tracks = append(tracks, track)
case *format.LPCM:
var codec string
switch forma.BitDepth {
case 8:
codec = "pcm-u8"
case 16:
codec = "pcm-s16"
case 24:
codec = "pcm-s24"
default: // 32
codec = "pcm-s32"
}
track := &Track{
Codec: codec,
Samplerate: forma.SampleRate,
Channels: forma.ChannelCount,
Media: media,
Format: forma,
OnData: func(u *unit.Unit, onData func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
src := []byte(u.Payload.(unit.PayloadLPCM))
byteDepth := forma.BitDepth / 8
swapped := make([]byte, len(src))
for i := 0; i+byteDepth <= len(src); i += byteDepth {
for j := range byteDepth {
swapped[i+j] = src[i+byteDepth-1-j]
}
}
return onData(swapped, u.PTS)
},
}
tracks = append(tracks, track)
}
}
}
return tracks, nil
}
+111
View File
@@ -0,0 +1,111 @@
package moq
import (
"encoding/base64"
"encoding/hex"
"testing"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/flac"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/stretchr/testify/require"
)
func TestFromStream(t *testing.T) {
flacEnc, err := (&flac.StreamInfo{
SampleRate: 44100,
ChannelCount: 2,
BitDepth: 16,
}).Marshal()
require.NoError(t, err)
mpeg4Config := &mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 44100,
ChannelConfig: 2,
}
mpeg4Enc, err := mpeg4Config.Marshal()
require.NoError(t, err)
desc := &description.Session{
Medias: []*description.Media{
{Formats: []format.Format{&format.AV1{}}},
{Formats: []format.Format{&format.VP9{}}},
{Formats: []format.Format{&format.VP8{}}},
{Formats: []format.Format{&format.H265{}}},
{Formats: []format.Format{&format.H264{}}},
{Formats: []format.Format{&format.Opus{ChannelCount: 2}}},
{Formats: []format.Format{&format.Generic{
RTPMa: "FLAC/44100/2",
ClockRat: 44100,
FMT: map[string]string{"streaminfo": hex.EncodeToString(flacEnc)},
}}},
{Formats: []format.Format{&format.MPEG4Audio{
Config: mpeg4Config,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}}},
{Formats: []format.Format{&format.G711{
SampleRate: 8000,
ChannelCount: 1,
}}},
{Formats: []format.Format{&format.LPCM{
BitDepth: 16,
SampleRate: 44100,
ChannelCount: 2,
}}},
},
}
tracks, err := FromStream(desc)
require.NoError(t, err)
for _, track := range tracks {
track.OnData = nil
}
require.Equal(t, []*Track{
{
Codec: "av01.0.04M.08", Media: desc.Medias[0],
Format: desc.Medias[0].Formats[0],
},
{
Codec: "vp09.00.10.08", Media: desc.Medias[1],
Format: desc.Medias[1].Formats[0],
},
{
Codec: "vp8", Media: desc.Medias[2],
Format: desc.Medias[2].Formats[0],
},
{
Codec: "hev1.1.6.L93.B0", Media: desc.Medias[3],
Format: desc.Medias[3].Formats[0],
},
{
Codec: "avc3.640028", Media: desc.Medias[4],
Format: desc.Medias[4].Formats[0],
},
{
Codec: "opus", Samplerate: 48000, Channels: 2,
Media: desc.Medias[5], Format: desc.Medias[5].Formats[0],
},
{
Codec: "flac", Samplerate: 44100, Channels: 2,
InitData: base64.StdEncoding.EncodeToString(flacEnc), Media: desc.Medias[6], Format: desc.Medias[6].Formats[0],
},
{
Codec: "mp4a.40.2", Samplerate: 44100, Channels: 2,
InitData: base64.StdEncoding.EncodeToString(mpeg4Enc), Media: desc.Medias[7], Format: desc.Medias[7].Formats[0],
},
{
Codec: "pcm-s16", Samplerate: 8000, Channels: 1,
Media: desc.Medias[8], Format: desc.Medias[8].Formats[0],
},
{
Codec: "pcm-s16", Samplerate: 44100, Channels: 2,
Media: desc.Medias[9], Format: desc.Medias[9].Formats[0],
},
}, tracks)
}
@@ -0,0 +1,84 @@
package parameter
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
const typeAuthorizationToken = 0x03
// AuthorizationTokenAliasType is a value of Alias Type.
// spec: draft-18, section 10.2.2
type AuthorizationTokenAliasType uint64
// spec: draft-18, section 10.2.2
const (
AuthorizationTokenAliasTypeUseValue AuthorizationTokenAliasType = 0x03
)
// AuthorizationToken is the AUTHORIZATION_TOKEN parameter.
// spec: draft-18, section 10.2.2
type AuthorizationToken struct {
AliasType AuthorizationTokenAliasType
TokenType uint64
TokenValue []byte
}
func (*AuthorizationToken) isParameter() {}
func (*AuthorizationToken) paramType() uint64 {
return typeAuthorizationToken
}
func (t *AuthorizationToken) unmarshal(buf []byte) (int, error) {
var le varint.Varint
llen, err := le.Unmarshal(buf)
if err != nil {
return 0, err
}
if len(buf)-llen < int(le) {
return 0, fmt.Errorf("not enough bytes for parameter value")
}
buf = buf[llen : llen+int(le)]
var aliasType varint.Varint
n, err := aliasType.Unmarshal(buf)
if err != nil {
return 0, err
}
t.AliasType = AuthorizationTokenAliasType(aliasType)
if t.AliasType != AuthorizationTokenAliasTypeUseValue {
return 0, fmt.Errorf("unsupported token alias type: %d", aliasType)
}
buf = buf[n:]
var tokenType varint.Varint
n, err = tokenType.Unmarshal(buf)
if err != nil {
return 0, err
}
t.TokenType = uint64(tokenType)
t.TokenValue = buf[n:]
return llen + int(le), nil
}
func (t AuthorizationToken) marshalSize() int {
n := varint.Varint(t.AliasType).MarshalSize() +
varint.Varint(t.TokenType).MarshalSize() +
len(t.TokenValue)
return varint.Varint(n).MarshalSize() + n
}
func (t AuthorizationToken) marshalTo(buf []byte) int {
innerSize := varint.Varint(t.AliasType).MarshalSize() +
varint.Varint(t.TokenType).MarshalSize() +
len(t.TokenValue)
n := varint.Varint(innerSize).MarshalTo(buf)
n += varint.Varint(t.AliasType).MarshalTo(buf[n:])
n += varint.Varint(t.TokenType).MarshalTo(buf[n:])
n += copy(buf[n:], t.TokenValue)
return n
}
@@ -0,0 +1,91 @@
// Package parameter contains MoQ parameters.
package parameter
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
// Parameter is a parameter of a control message.
// spec: draft-18, section 10.2
type Parameter interface {
isParameter()
paramType() uint64
unmarshal(data []byte) (int, error)
marshalSize() int
marshalTo(buf []byte) int
}
// Parameters is a list of parameters.
type Parameters []Parameter
// Unmarshal decodes parameters. Returns the number of bytes consumed.
func (p *Parameters) Unmarshal(count int, buf []byte) (int, error) {
var currentType uint64
total := 0
for range uint64(count) {
var typeDelta varint.Varint
n, err := typeDelta.Unmarshal(buf)
if err != nil {
return 0, err
}
buf = buf[n:]
total += n
currentType += uint64(typeDelta)
var param Parameter
switch currentType {
case typeAuthorizationToken:
param = &AuthorizationToken{}
default:
return 0, fmt.Errorf("unsupported parameter type: %d", currentType)
}
n, err = param.unmarshal(buf)
if err != nil {
return 0, fmt.Errorf("failed to unmarshal authorization token: %w", err)
}
*p = append(*p, param)
buf = buf[n:]
total += n
}
return total, nil
}
// MarshalSize returns the size of marshaled parameters.
func (p Parameters) MarshalSize() int {
n := 0
var prevType uint64
for _, param := range p {
t := param.paramType()
delta := t - prevType
n += varint.Varint(delta).MarshalSize() + param.marshalSize()
prevType = t
}
return n
}
// MarshalTo encodes parameters.
func (p Parameters) MarshalTo(buf []byte) int {
n := 0
var prevType uint64
for _, param := range p {
t := param.paramType()
delta := t - prevType
n += varint.Varint(delta).MarshalTo(buf[n:])
n += param.marshalTo(buf[n:])
prevType = t
}
return n
}
@@ -0,0 +1,60 @@
package parameter
import (
"testing"
"github.com/stretchr/testify/require"
)
var cases = []struct {
name string
count int
enc []byte
dec Parameters
}{
{
name: "no parameters",
count: 0,
enc: []byte{},
dec: nil,
},
{
name: "authorization token",
count: 1,
enc: []byte{
0x03, // type delta = 3 (AuthorizationToken)
0x08, // inner length = 8
0x03, // alias type = UseValue
0x01, // token type = 1
0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // token value = "secret"
},
dec: Parameters{
&AuthorizationToken{
AliasType: AuthorizationTokenAliasTypeUseValue,
TokenType: 1,
TokenValue: []byte("secret"),
},
},
},
}
func TestUnmarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
var params Parameters
_, err := params.Unmarshal(ca.count, ca.enc)
require.NoError(t, err)
require.Equal(t, ca.dec, params)
})
}
}
func TestMarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
buf := make([]byte, ca.dec.MarshalSize())
ca.dec.MarshalTo(buf)
require.Equal(t, ca.enc, buf)
})
}
}
+105
View File
@@ -0,0 +1,105 @@
// Package property contains Media over QUIC object properties.
package property
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
// Property is an object property.
// spec: draft-18, section 11.2.1.2
type Property interface {
isProperty()
propType() varint.Varint
unmarshal(buf []byte) (int, error)
marshalSize() int
marshalTo(buf []byte) int
}
// Properties are object properties.
// spec: draft-18, section 11.2.1.2
type Properties []Property
// Unmarshal decodes properties.
func (p *Properties) Unmarshal(buf []byte) error {
var currentType uint64
for len(buf) != 0 {
var delta varint.Varint
n, err := delta.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n:]
currentType += uint64(delta)
switch currentType {
case timestampPropertyType:
var ts Timestamp
var n2 int
n2, err = ts.unmarshal(buf)
if err != nil {
return err
}
*p = append(*p, &ts)
buf = buf[n2:]
default:
if currentType%2 == 1 {
var length varint.Varint
var n2 int
n2, err = length.Unmarshal(buf)
if err != nil {
return err
}
if len(buf)-n2 < int(length) {
return fmt.Errorf("not enough bytes for unknown property")
}
buf = buf[n2+int(length):]
} else {
var skip varint.Varint
var n2 int
n2, err = skip.Unmarshal(buf)
if err != nil {
return err
}
buf = buf[n2:]
}
}
}
return nil
}
// MarshalSize returns the size of marshaled properties.
func (p Properties) MarshalSize() int {
n := 0
var prevType varint.Varint
for _, prop := range p {
t := prop.propType()
delta := t - prevType
n += delta.MarshalSize() + prop.marshalSize()
prevType = t
}
return n
}
// MarshalTo encodes properties.
func (p Properties) MarshalTo(buf []byte) int {
n := 0
var prevType varint.Varint
for _, prop := range p {
t := prop.propType()
delta := t - prevType
n += delta.MarshalTo(buf[n:])
n += prop.marshalTo(buf[n:])
prevType = t
}
return n
}
@@ -0,0 +1,50 @@
package property
import (
"testing"
"github.com/stretchr/testify/require"
)
var cases = []struct {
name string
enc []byte
dec Properties
}{
{
name: "no properties",
enc: []byte{},
dec: nil,
},
{
name: "timestamp",
enc: []byte{
0x06, // type delta = 6 (Timestamp)
0x83, 0xe8, // value = 1000
},
dec: Properties{
new(Timestamp(1000)),
},
},
}
func TestUnmarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
var props Properties
err := props.Unmarshal(ca.enc)
require.NoError(t, err)
require.Equal(t, ca.dec, props)
})
}
}
func TestMarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
buf := make([]byte, ca.dec.MarshalSize())
ca.dec.MarshalTo(buf)
require.Equal(t, ca.enc, buf)
})
}
}
@@ -0,0 +1,33 @@
package property
import "github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
const timestampPropertyType = 0x06
// Timestamp is the timestamp property.
// spec: draft-ietf-moq-loc, section 2.3.1.1
type Timestamp int64
func (t *Timestamp) isProperty() {}
func (Timestamp) propType() varint.Varint {
return timestampPropertyType
}
func (t *Timestamp) unmarshal(buf []byte) (int, error) {
var val varint.Varint
n, err := val.Unmarshal(buf)
if err != nil {
return 0, err
}
*t = Timestamp(val)
return n, nil
}
func (t Timestamp) marshalSize() int {
return varint.Varint(t).MarshalSize()
}
func (t Timestamp) marshalTo(buf []byte) int {
return varint.Varint(t).MarshalTo(buf)
}
@@ -0,0 +1,88 @@
// Package reorderer contains a subgroup reorderer.
package reorderer
import (
"sync"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/moq/subgroup"
)
// Reorderer is a subgroup reorderer.
type Reorderer struct {
MaxReordered int
Parent logger.Writer
initialized bool
mu sync.Mutex
curGroupID uint64
pending map[uint64]*subgroup.SubGroup
}
// Initialize initializes the reorderer.
func (r *Reorderer) Initialize() {
r.pending = make(map[uint64]*subgroup.SubGroup)
}
// Push pushes a subgroup and returns a list of subgroups that are now in order.
func (r *Reorderer) Push(sg *subgroup.SubGroup) ([]*subgroup.SubGroup, error) {
r.mu.Lock()
defer r.mu.Unlock()
if !r.initialized {
r.initialized = true
r.curGroupID = sg.Header.GroupID
return []*subgroup.SubGroup{sg}, nil
}
switch {
case sg.Header.GroupID <= r.curGroupID:
r.Parent.Log(logger.Warn, "skipping out-of-order subgroup")
case sg.Header.GroupID == r.curGroupID+1 && len(r.pending) == 0:
r.curGroupID = sg.Header.GroupID
return []*subgroup.SubGroup{sg}, nil
default:
r.pending[sg.Header.GroupID] = sg
diff := sg.Header.GroupID - r.curGroupID
var countInRange uint64
for id := range r.pending {
if id <= sg.Header.GroupID {
countInRange++
}
}
if countInRange == diff {
return r.flushUpTo(sg.Header.GroupID), nil
} else if len(r.pending) > r.MaxReordered {
r.Parent.Log(logger.Warn, "too many reordered subgroups, flushing")
return r.flushUpTo(sg.Header.GroupID), nil
}
}
return nil, nil
}
func (r *Reorderer) flushUpTo(maxGroupID uint64) []*subgroup.SubGroup {
out := make([]*subgroup.SubGroup, 0, len(r.pending))
for i := r.curGroupID + 1; i <= maxGroupID; i++ {
if e, ok := r.pending[i]; ok {
out = append(out, e)
delete(r.pending, i)
}
}
r.curGroupID = maxGroupID
for {
next, ok := r.pending[r.curGroupID+1]
if !ok {
break
}
out = append(out, next)
delete(r.pending, r.curGroupID+1)
r.curGroupID++
}
return out
}
@@ -0,0 +1,163 @@
package reorderer
import (
"testing"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/moq/subgroup"
"github.com/stretchr/testify/require"
)
type nopLogger struct{}
func (nopLogger) Log(logger.Level, string, ...any) {}
func makeSG(groupID uint64) *subgroup.SubGroup {
return &subgroup.SubGroup{
Header: subgroup.Header{GroupID: groupID},
}
}
func TestReordererFirstPush(t *testing.T) {
r := &Reorderer{MaxReordered: 5, Parent: nopLogger{}}
r.Initialize()
sg0 := makeSG(0)
out, err := r.Push(sg0)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg0}, out)
}
func TestReordererInOrder(t *testing.T) {
r := &Reorderer{MaxReordered: 5, Parent: nopLogger{}}
r.Initialize()
sg0 := makeSG(0)
out, err := r.Push(sg0)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg0}, out)
sg1 := makeSG(1)
out, err = r.Push(sg1)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg1}, out)
sg2 := makeSG(2)
out, err = r.Push(sg2)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg2}, out)
}
func TestReordererStale(t *testing.T) {
r := &Reorderer{MaxReordered: 5, Parent: nopLogger{}}
r.Initialize()
sg5 := makeSG(5)
out, err := r.Push(sg5)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg5}, out)
// same GroupID again
out, err = r.Push(makeSG(5))
require.NoError(t, err)
require.Nil(t, out)
// older GroupID
out, err = r.Push(makeSG(3))
require.NoError(t, err)
require.Nil(t, out)
}
func TestReordererOutOfOrderPending(t *testing.T) {
r := &Reorderer{MaxReordered: 5, Parent: nopLogger{}}
r.Initialize()
sg0 := makeSG(0)
out, err := r.Push(sg0)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg0}, out)
// sg2 arrives before sg1; held in pending
out, err = r.Push(makeSG(2))
require.NoError(t, err)
require.Nil(t, out)
}
func TestReordererOutOfOrderFilled(t *testing.T) {
r := &Reorderer{MaxReordered: 5, Parent: nopLogger{}}
r.Initialize()
sg0 := makeSG(0)
out, err := r.Push(sg0)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg0}, out)
// sg2 arrives out of order, held in pending
sg2 := makeSG(2)
out, err = r.Push(sg2)
require.NoError(t, err)
require.Nil(t, out)
// sg1 (the missing piece) arrives; gap is now filled; sg1 and the
// already-pending sg2 are both returned together
sg1 := makeSG(1)
out, err = r.Push(sg1)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg1, sg2}, out)
}
func TestReordererDrainAfterFill(t *testing.T) { //nolint:dupl
r := &Reorderer{MaxReordered: 5, Parent: nopLogger{}}
r.Initialize()
sg0 := makeSG(0)
out, err := r.Push(sg0)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg0}, out)
// sg2 and sg3 arrive before sg1
sg2 := makeSG(2)
out, err = r.Push(sg2)
require.NoError(t, err)
require.Nil(t, out)
sg3 := makeSG(3)
out, err = r.Push(sg3)
require.NoError(t, err)
require.Nil(t, out)
// sg1 arrives; fills the gap; sg1, sg2, and sg3 are all returned together
sg1 := makeSG(1)
out, err = r.Push(sg1)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg1, sg2, sg3}, out)
}
func TestReordererMaxReordered(t *testing.T) { //nolint:dupl
r := &Reorderer{MaxReordered: 2, Parent: nopLogger{}}
r.Initialize()
sg0 := makeSG(0)
out, err := r.Push(sg0)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg0}, out)
// first out-of-order item (pending=1, not over limit)
sg2 := makeSG(2)
out, err = r.Push(sg2)
require.NoError(t, err)
require.Nil(t, out)
// second out-of-order item (pending=2, not over limit)
sg4 := makeSG(4)
out, err = r.Push(sg4)
require.NoError(t, err)
require.Nil(t, out)
// third out-of-order item pushes pending to 3, which exceeds MaxReordered=2;
// all available items are flushed in order, skipping the missing ones (1, 3)
sg5 := makeSG(5)
out, err = r.Push(sg5)
require.NoError(t, err)
require.Equal(t, []*subgroup.SubGroup{sg2, sg4, sg5}, out)
}
+70
View File
@@ -0,0 +1,70 @@
// Package subgroup implements the subgroup stream.
package subgroup
import (
"io"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
// Header is the SUBGROUP_HEADER structure.
// spec: draft-18, section 11.4.2
type Header struct {
Properties bool
FirstObject bool
TrackAlias uint64
GroupID uint64
}
func (h *Header) read(r io.Reader) error {
var b [1]byte
_, err := r.Read(b[:])
if err != nil {
return err
}
h.Properties = (b[0] & 0x01) != 0
h.FirstObject = (b[0] & 0x40) != 0
var trackAlias varint.Varint
err = trackAlias.Read(r)
if err != nil {
return err
}
h.TrackAlias = uint64(trackAlias)
var groupID varint.Varint
err = groupID.Read(r)
if err != nil {
return err
}
h.GroupID = uint64(groupID)
return nil
}
func (h Header) marshalSize() int {
st := varint.Varint(0x30)
if h.Properties {
st |= 0x01
}
if h.FirstObject {
st |= 0x40
}
return st.MarshalSize() +
varint.Varint(h.TrackAlias).MarshalSize() +
varint.Varint(h.GroupID).MarshalSize()
}
func (h Header) marshalTo(buf []byte) int {
st := varint.Varint(0x30)
if h.Properties {
st |= 0x01
}
if h.FirstObject {
st |= 0x40
}
pos := st.MarshalTo(buf)
pos += varint.Varint(h.TrackAlias).MarshalTo(buf[pos:])
pos += varint.Varint(h.GroupID).MarshalTo(buf[pos:])
return pos
}
+120
View File
@@ -0,0 +1,120 @@
package subgroup
import (
"fmt"
"io"
"github.com/bluenviron/mediamtx/internal/protocols/moq/property"
"github.com/bluenviron/mediamtx/internal/protocols/moq/varint"
)
const maxPayloadSize = 10 * 1024 * 1024
// Object is an object of a subgroup stream.
// spec: draft-18, section 11.4.2
type Object struct {
IDDelta uint64
Properties property.Properties
Payload []byte
}
func (o *Object) read(r io.Reader, header *Header) error {
var idDelta varint.Varint
if err := idDelta.Read(r); err != nil {
return err
}
o.IDDelta = uint64(idDelta)
if header.Properties {
var propsLen varint.Varint
err := propsLen.Read(r)
if err != nil {
return err
}
if propsLen > 0 {
props := make([]byte, propsLen)
_, err = io.ReadFull(r, props)
if err != nil {
return err
}
err = o.Properties.Unmarshal(props)
if err != nil {
return err
}
}
}
var payloadLen varint.Varint
err := payloadLen.Read(r)
if err != nil {
return err
}
if payloadLen == 0 {
var status varint.Varint
if err = status.Read(r); err != nil {
return err
}
if status != 0x03 && status != 0x04 {
return fmt.Errorf("unexpected status: 0x%x", status)
}
return nil
}
if payloadLen > maxPayloadSize {
return fmt.Errorf("payload too large: %d", payloadLen)
}
o.Payload = make([]byte, payloadLen)
_, err = io.ReadFull(r, o.Payload)
if err != nil {
return err
}
return nil
}
func (o Object) marshalSize(header *Header) int {
propsFieldSize := 0
if header.Properties {
propsLen := o.Properties.MarshalSize()
propsFieldSize = varint.Varint(propsLen).MarshalSize() + propsLen
}
if len(o.Payload) == 0 {
return varint.Varint(o.IDDelta).MarshalSize() + propsFieldSize + 2
}
return varint.Varint(o.IDDelta).MarshalSize() +
propsFieldSize +
varint.Varint(len(o.Payload)).MarshalSize() +
len(o.Payload)
}
func (o Object) marshalTo(buf []byte, header *Header) int {
n := varint.Varint(o.IDDelta).MarshalTo(buf)
if header.Properties {
propsLen := o.Properties.MarshalSize()
n += varint.Varint(propsLen).MarshalTo(buf[n:])
n += o.Properties.MarshalTo(buf[n:])
}
if len(o.Payload) == 0 {
buf[n] = 0x00
n++
buf[n] = 0x03
n++
return n
}
n += varint.Varint(len(o.Payload)).MarshalTo(buf[n:])
n += copy(buf[n:], o.Payload)
return n
}
@@ -0,0 +1,70 @@
package subgroup
import (
"fmt"
"io"
)
// SubGroup is a subgroup stream.
type SubGroup struct {
Header Header
Objects []Object
}
// Read reads a SubGroup.
func (s *SubGroup) Read(r io.Reader) error {
if err := s.Header.read(r); err != nil {
return err
}
// for the moment, support reading a single object only.
var o1 Object
err := o1.read(r, &s.Header)
if err != nil {
return err
}
s.Objects = []Object{o1}
var o2 Object
err = o2.read(r, &s.Header)
if err != nil {
return err
}
if len(o2.Payload) != 0 {
return fmt.Errorf("unexpected second object")
}
return nil
}
func (s SubGroup) marshalSize() int {
n := s.Header.marshalSize()
for _, o := range s.Objects {
n += o.marshalSize(&s.Header)
}
n += Object{Payload: nil}.marshalSize(&s.Header)
return n
}
func (s SubGroup) marshalTo(buf []byte) int {
n := s.Header.marshalTo(buf)
for _, o := range s.Objects {
n += o.marshalTo(buf[n:], &s.Header)
}
n += Object{Payload: nil}.marshalTo(buf[n:], &s.Header)
return n
}
// Marshal encodes a SubGroup.
func (s *SubGroup) Marshal() []byte {
buf := make([]byte, s.marshalSize())
s.marshalTo(buf)
return buf
}
@@ -0,0 +1,92 @@
package subgroup
import (
"bytes"
"testing"
"github.com/bluenviron/mediamtx/internal/protocols/moq/property"
"github.com/stretchr/testify/require"
)
var cases = []struct {
name string
enc []byte
dec SubGroup
}{
{
name: "stream without properties",
enc: []byte{
0x30, // type (Properties=false, FirstObject=false)
0x01, // TrackAlias = 1
0x00, // GroupID = 0
0x00, // object IDDelta = 0
0x05, // payload length = 5
0x68, 0x65, 0x6c, 0x6c, 0x6f, // payload = "hello"
0x00, // end-of-stream IDDelta = 0
0x00, // payload length = 0 (end-of-stream)
0x03, // status = EndOfGroup
},
dec: SubGroup{
Header: Header{
Properties: false,
FirstObject: false,
TrackAlias: 1,
GroupID: 0,
},
Objects: []Object{{
Payload: []byte("hello"),
}},
},
},
{
name: "stream with properties",
enc: []byte{
0x31, // type (Properties=true)
0x01, // TrackAlias = 1
0x00, // GroupID = 0
0x00, // object IDDelta = 0
0x03, // properties length = 3
0x06, // property type delta = 6 (Timestamp)
0x83, 0xe8, // Timestamp value = 1000
0x05, // payload length = 5
0x68, 0x65, 0x6c, 0x6c, 0x6f, // payload = "hello"
0x00, // end-of-stream IDDelta = 0
0x00, // properties length = 0
0x00, // payload length = 0 (end-of-stream)
0x03, // status = EndOfGroup
},
dec: SubGroup{
Header: Header{
Properties: true,
FirstObject: false,
TrackAlias: 1,
GroupID: 0,
},
Objects: []Object{{
Properties: property.Properties{
new(property.Timestamp(1000)),
},
Payload: []byte("hello"),
}},
},
},
}
func TestUnmarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
var s SubGroup
err := s.Read(bytes.NewReader(ca.enc))
require.NoError(t, err)
require.Equal(t, ca.dec, s)
})
}
}
func TestMarshal(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
require.Equal(t, ca.enc, ca.dec.Marshal())
})
}
}
+199
View File
@@ -0,0 +1,199 @@
package moq
import (
"fmt"
"strings"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/protocols/moq/catalog"
"github.com/bluenviron/mediamtx/internal/protocols/moq/property"
"github.com/bluenviron/mediamtx/internal/protocols/moq/subgroup"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
)
func findTimestamp(props []property.Property) (int64, bool) {
for _, pr := range props {
if ts, ok := pr.(*property.Timestamp); ok {
return int64(*ts), true
}
}
return 0, false
}
// ToStream maps a Media-over-QUIC stream to a MediaMTX stream.
func ToStream(
cat *catalog.Catalog,
subStream **stream.SubStream,
) (
[]*description.Media,
map[uint64]func(sg *subgroup.SubGroup) error,
error,
) {
var medias []*description.Media
writeFuncs := make(map[uint64]func(sg *subgroup.SubGroup) error)
wrap := func(wrapped func(payload []byte, pts int64) error) func(sg *subgroup.SubGroup) error {
return func(sg *subgroup.SubGroup) error {
ts, ok := findTimestamp(sg.Objects[0].Properties)
if !ok {
return fmt.Errorf("timestamp property is required")
}
return wrapped(sg.Objects[0].Payload, ts)
}
}
for i, track := range cat.Tracks {
trackAlias := uint64(i + 1)
switch {
case strings.HasPrefix(track.Codec, "av01"):
forma := &format.AV1{
PayloadTyp: 96,
}
media := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{forma},
}
medias = append(medias, media)
writeFuncs[trackAlias] = wrap(func(payload []byte, pts int64) error {
var bs av1.Bitstream
err := bs.Unmarshal(payload)
if err != nil {
return err
}
(*subStream).WriteUnit(media, forma, &unit.Unit{PTS: pts, Payload: unit.PayloadAV1(bs)})
return nil
})
case strings.HasPrefix(track.Codec, "vp09"):
forma := &format.VP9{
PayloadTyp: 96,
}
media := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{forma},
}
medias = append(medias, media)
writeFuncs[trackAlias] = wrap(func(payload []byte, pts int64) error {
(*subStream).WriteUnit(media, forma, &unit.Unit{PTS: pts, Payload: unit.PayloadVP9(payload)})
return nil
})
case track.Codec == "vp8":
forma := &format.VP8{
PayloadTyp: 96,
}
media := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{forma},
}
medias = append(medias, media)
writeFuncs[trackAlias] = wrap(func(payload []byte, pts int64) error {
(*subStream).WriteUnit(media, forma, &unit.Unit{PTS: pts, Payload: unit.PayloadVP8(payload)})
return nil
})
case strings.HasPrefix(track.Codec, "avc3"):
forma := &format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}
media := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{forma},
}
medias = append(medias, media)
writeFuncs[trackAlias] = wrap(func(payload []byte, pts int64) error {
var nalus h264.AVCC
err := nalus.Unmarshal(payload)
if err != nil {
return err
}
(*subStream).WriteUnit(media, forma, &unit.Unit{PTS: pts, Payload: unit.PayloadH264(nalus)})
return nil
})
case strings.HasPrefix(track.Codec, "hev1"):
forma := &format.H265{
PayloadTyp: 96,
}
media := &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{forma},
}
medias = append(medias, media)
writeFuncs[trackAlias] = wrap(func(payload []byte, pts int64) error {
var nalus h264.AVCC
err := nalus.Unmarshal(payload)
if err != nil {
return err
}
(*subStream).WriteUnit(media, forma, &unit.Unit{PTS: pts, Payload: unit.PayloadH265(nalus)})
return nil
})
case track.Codec == "opus":
channels := track.Channels
if channels == 0 {
channels = 2
}
forma := &format.Opus{
PayloadTyp: 96,
ChannelCount: channels,
}
media := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{forma},
}
medias = append(medias, media)
writeFuncs[trackAlias] = wrap(func(payload []byte, pts int64) error {
(*subStream).WriteUnit(media, forma, &unit.Unit{PTS: pts, Payload: unit.PayloadOpus{payload}})
return nil
})
case strings.HasPrefix(track.Codec, "mp4a"):
config := &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: track.Samplerate,
ChannelConfig: uint8(track.Channels),
}
forma := &format.MPEG4Audio{
PayloadTyp: 96,
Config: config,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
media := &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{forma},
}
medias = append(medias, media)
writeFuncs[trackAlias] = wrap(func(payload []byte, pts int64) error {
(*subStream).WriteUnit(media, forma, &unit.Unit{PTS: pts, Payload: unit.PayloadMPEG4Audio{payload}})
return nil
})
default:
return nil, nil, fmt.Errorf("unsupported codec: %s", track.Codec)
}
}
return medias, writeFuncs, nil
}
+69
View File
@@ -0,0 +1,69 @@
package moq
import (
"testing"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/protocols/moq/catalog"
"github.com/stretchr/testify/require"
)
func TestToStream(t *testing.T) {
cat := &catalog.Catalog{
Tracks: []catalog.Track{
{Codec: "av01.0.04M.08"},
{Codec: "vp09.00.10.08"},
{Codec: "vp8"},
{Codec: "hev1.1.6.L93.B0"},
{Codec: "avc3.640028"},
{Codec: "opus"},
{Codec: "mp4a.40.2", Samplerate: 44100, Channels: 2},
},
}
medias, _, err := ToStream(cat, nil)
require.NoError(t, err)
require.Equal(t, []*description.Media{
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.AV1{PayloadTyp: 96}},
},
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP9{PayloadTyp: 96}},
},
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP8{PayloadTyp: 96}},
},
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H265{PayloadTyp: 96}},
},
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{PayloadTyp: 96, PacketizationMode: 1}},
},
{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{PayloadTyp: 96, ChannelCount: 2}},
},
{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 44100,
ChannelConfig: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}},
},
}, medias)
}
+18
View File
@@ -0,0 +1,18 @@
package moq
import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/unit"
)
// Track is a Media-over-QUIC track.
type Track struct {
Codec string
Samplerate int
Channels int
InitData string
Media *description.Media
Format format.Format
OnData func(u *unit.Unit, wrapped func([]byte, int64) error) error
}
+245
View File
@@ -0,0 +1,245 @@
// Package varint implements variable-length integers.
package varint
import (
"fmt"
"io"
)
// Varint is a variable-length integer.
// spec: draft-18, section 1.4.1
type Varint uint64
// Read reads a Varint from a Reader.
func (v *Varint) Read(r io.Reader) error {
var first [1]byte
_, err := io.ReadFull(r, first[:])
if err != nil {
return err
}
b := first[0]
var size int
switch {
case b&0x80 == 0:
*v = Varint(b)
return nil
case b&0xC0 == 0x80:
size = 2
case b&0xE0 == 0xC0:
size = 3
case b&0xF0 == 0xE0:
size = 4
case b&0xF8 == 0xF0:
size = 5
case b&0xFC == 0xF8:
size = 6
case b&0xFE == 0xFC:
size = 7
case b == 0xFE:
size = 8
case b == 0xFF:
size = 9
default:
return fmt.Errorf("unsupported varint prefix: 0x%02X", b)
}
rest := make([]byte, size-1)
_, err = io.ReadFull(r, rest)
if err != nil {
return err
}
switch size { //nolint:dupl
case 2:
*v = Varint(b&0x3F)<<8 | Varint(rest[0])
case 3:
*v = Varint(b&0x1F)<<16 | Varint(rest[0])<<8 | Varint(rest[1])
case 4:
*v = Varint(b&0x0F)<<24 | Varint(rest[0])<<16 | Varint(rest[1])<<8 |
Varint(rest[2])
case 5:
*v = Varint(b&0x07)<<32 | Varint(rest[0])<<24 | Varint(rest[1])<<16 |
Varint(rest[2])<<8 | Varint(rest[3])
case 6:
*v = Varint(b&0x03)<<40 | Varint(rest[0])<<32 | Varint(rest[1])<<24 |
Varint(rest[2])<<16 | Varint(rest[3])<<8 | Varint(rest[4])
case 7:
*v = Varint(b&0x01)<<48 | Varint(rest[0])<<40 | Varint(rest[1])<<32 |
Varint(rest[2])<<24 | Varint(rest[3])<<16 | Varint(rest[4])<<8 | Varint(rest[5])
case 8:
*v = Varint(rest[0])<<48 | Varint(rest[1])<<40 | Varint(rest[2])<<32 |
Varint(rest[3])<<24 | Varint(rest[4])<<16 | Varint(rest[5])<<8 | Varint(rest[6])
default: // 9
*v = Varint(rest[0])<<56 | Varint(rest[1])<<48 | Varint(rest[2])<<40 |
Varint(rest[3])<<32 | Varint(rest[4])<<24 | Varint(rest[5])<<16 |
Varint(rest[6])<<8 | Varint(rest[7])
}
return nil
}
// Unmarshal decodes a Varinat from a buffer.
func (v *Varint) Unmarshal(buf []byte) (int, error) {
if len(buf) == 0 {
return 0, fmt.Errorf("not enough bytes")
}
b := buf[0]
var size int
switch {
case b&0x80 == 0:
*v = Varint(b)
return 1, nil
case b&0xC0 == 0x80:
size = 2
case b&0xE0 == 0xC0:
size = 3
case b&0xF0 == 0xE0:
size = 4
case b&0xF8 == 0xF0:
size = 5
case b&0xFC == 0xF8:
size = 6
case b&0xFE == 0xFC:
size = 7
case b == 0xFE:
size = 8
case b == 0xFF:
size = 9
default:
return 0, fmt.Errorf("unsupported varint prefix: 0x%02X", b)
}
if len(buf) < size {
return 0, fmt.Errorf("not enough bytes")
}
switch size { //nolint:dupl
case 2:
*v = Varint(b&0x3F)<<8 | Varint(buf[1])
case 3:
*v = Varint(b&0x1F)<<16 | Varint(buf[1])<<8 | Varint(buf[2])
case 4:
*v = Varint(b&0x0F)<<24 | Varint(buf[1])<<16 | Varint(buf[2])<<8 |
Varint(buf[3])
case 5:
*v = Varint(b&0x07)<<32 | Varint(buf[1])<<24 | Varint(buf[2])<<16 |
Varint(buf[3])<<8 | Varint(buf[4])
case 6:
*v = Varint(b&0x03)<<40 | Varint(buf[1])<<32 | Varint(buf[2])<<24 |
Varint(buf[3])<<16 | Varint(buf[4])<<8 | Varint(buf[5])
case 7:
*v = Varint(b&0x01)<<48 | Varint(buf[1])<<40 | Varint(buf[2])<<32 |
Varint(buf[3])<<24 | Varint(buf[4])<<16 | Varint(buf[5])<<8 | Varint(buf[6])
case 8:
*v = Varint(buf[1])<<48 | Varint(buf[2])<<40 | Varint(buf[3])<<32 |
Varint(buf[4])<<24 | Varint(buf[5])<<16 | Varint(buf[6])<<8 | Varint(buf[7])
default: // 9
*v = Varint(buf[1])<<56 | Varint(buf[2])<<48 | Varint(buf[3])<<40 |
Varint(buf[4])<<32 | Varint(buf[5])<<24 | Varint(buf[6])<<16 |
Varint(buf[7])<<8 | Varint(buf[8])
}
return size, nil
}
// MarshalSize returns the number of bytes required to marshal the Varint.
func (v Varint) MarshalSize() int {
switch {
case v < 1<<7:
return 1
case v < 1<<14:
return 2
case v < 1<<21:
return 3
case v < 1<<28:
return 4
case v < 1<<35:
return 5
case v < 1<<42:
return 6
case v < 1<<49:
return 7
case v < 1<<56:
return 8
default:
return 9
}
}
// MarshalTo encodes the Varint to a buffer.
func (v Varint) MarshalTo(buf []byte) int {
switch {
case v < 1<<7:
buf[0] = byte(v)
return 1
case v < 1<<14:
buf[0] = 0x80 | byte(v>>8)
buf[1] = byte(v)
return 2
case v < 1<<21:
buf[0] = 0xC0 | byte(v>>16)
buf[1] = byte(v >> 8)
buf[2] = byte(v)
return 3
case v < 1<<28:
buf[0] = 0xE0 | byte(v>>24)
buf[1] = byte(v >> 16)
buf[2] = byte(v >> 8)
buf[3] = byte(v)
return 4
case v < 1<<35:
buf[0] = 0xF0 | byte(v>>32)
buf[1] = byte(v >> 24)
buf[2] = byte(v >> 16)
buf[3] = byte(v >> 8)
buf[4] = byte(v)
return 5
case v < 1<<42:
buf[0] = 0xF8 | byte(v>>40)
buf[1] = byte(v >> 32)
buf[2] = byte(v >> 24)
buf[3] = byte(v >> 16)
buf[4] = byte(v >> 8)
buf[5] = byte(v)
return 6
case v < 1<<49:
buf[0] = 0xFC | byte(v>>48)
buf[1] = byte(v >> 40)
buf[2] = byte(v >> 32)
buf[3] = byte(v >> 24)
buf[4] = byte(v >> 16)
buf[5] = byte(v >> 8)
buf[6] = byte(v)
return 7
case v < 1<<56:
buf[0] = 0xFE
buf[1] = byte(v >> 48)
buf[2] = byte(v >> 40)
buf[3] = byte(v >> 32)
buf[4] = byte(v >> 24)
buf[5] = byte(v >> 16)
buf[6] = byte(v >> 8)
buf[7] = byte(v)
return 8
default:
buf[0] = 0xFF
buf[1] = byte(v >> 56)
buf[2] = byte(v >> 48)
buf[3] = byte(v >> 40)
buf[4] = byte(v >> 32)
buf[5] = byte(v >> 24)
buf[6] = byte(v >> 16)
buf[7] = byte(v >> 8)
buf[8] = byte(v)
return 9
}
}
// Marshal encodes the Varint to bytes.
func (v Varint) Marshal() []byte {
buf := make([]byte, v.MarshalSize())
v.MarshalTo(buf)
return buf
}

Some files were not shown because too many files have changed in this diff Show More