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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
# 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 |
|
||||
| [WebRTC: Real-Time Communication in Browsers](https://www.w3.org/TR/webrtc/) | WebRTC |
|
||||
| [RFC8835, Transports for WebRTC](https://datatracker.ietf.org/doc/html/rfc8835) | WebRTC |
|
||||
| [RFC7742, WebRTC Video Processing and Codec Requirements](https://datatracker.ietf.org/doc/html/rfc7742) | WebRTC |
|
||||
| [RFC7874, WebRTC Audio Codec and Processing Requirements](https://datatracker.ietf.org/doc/html/rfc7874) | WebRTC |
|
||||
| [RFC7875, Additional WebRTC Audio Codecs for Interoperability](https://datatracker.ietf.org/doc/html/rfc7875) | WebRTC |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| 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 |
|
||||
| [WebRTC: Real-Time Communication in Browsers](https://www.w3.org/TR/webrtc/) | WebRTC |
|
||||
| [RFC8835, Transports for WebRTC](https://datatracker.ietf.org/doc/html/rfc8835) | WebRTC |
|
||||
| [RFC7742, WebRTC Video Processing and Codec Requirements](https://datatracker.ietf.org/doc/html/rfc7742) | WebRTC |
|
||||
| [RFC7874, WebRTC Audio Codec and Processing Requirements](https://datatracker.ietf.org/doc/html/rfc7874) | WebRTC |
|
||||
| [RFC7875, Additional WebRTC Audio Codecs for Interoperability](https://datatracker.ietf.org/doc/html/rfc7875) | WebRTC |
|
||||
| [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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
ProtocolHLS Protocol = "hls"
|
||||
ProtocolWebRTC Protocol = "webrtc"
|
||||
ProtocolSRT Protocol = "srt"
|
||||
ProtocolMoQ Protocol = "moq"
|
||||
)
|
||||
|
||||
// Request is an authentication request.
|
||||
|
||||
@@ -2,18 +2,64 @@
|
||||
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
|
||||
Parent logger.Writer
|
||||
CertPath string
|
||||
KeyPath string
|
||||
AllowAuto bool
|
||||
Parent logger.Writer
|
||||
|
||||
certWatcher, keyWatcher *confwatcher.ConfWatcher
|
||||
cert *tls.Certificate
|
||||
@@ -26,38 +72,88 @@ type CertLoader struct {
|
||||
func (cl *CertLoader) Initialize() error {
|
||||
cl.done = make(chan struct{})
|
||||
|
||||
if cl.AllowAuto && cl.KeyPath == autoKeyName && cl.CertPath == autoCertName {
|
||||
watch, err := cl.initializeAuto()
|
||||
if err != nil {
|
||||
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}
|
||||
err := cl.certWatcher.Initialize()
|
||||
if err != nil {
|
||||
if err := cl.certWatcher.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl.keyWatcher = &confwatcher.ConfWatcher{FilePath: cl.KeyPath}
|
||||
err = cl.keyWatcher.Initialize()
|
||||
if err != nil {
|
||||
if err := cl.keyWatcher.Initialize(); err != nil {
|
||||
cl.certWatcher.Close() //nolint:errcheck
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(cl.CertPath, cl.KeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl.certMu.Lock()
|
||||
cl.cert = &cert
|
||||
cl.certMu.Unlock()
|
||||
|
||||
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)
|
||||
cl.certWatcher.Close() //nolint:errcheck
|
||||
cl.keyWatcher.Close() //nolint:errcheck
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -345,10 +345,12 @@ func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err2 := pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())
|
||||
if err2 != nil {
|
||||
req.Res <- defs.PathDescribeRes{Err: err2}
|
||||
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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -67,9 +68,10 @@ func (s *Server) Initialize() error {
|
||||
}
|
||||
|
||||
s.loader = &certloader.CertLoader{
|
||||
CertPath: s.ServerCert,
|
||||
KeyPath: s.ServerKey,
|
||||
Parent: s.Parent,
|
||||
CertPath: s.ServerCert,
|
||||
KeyPath: s.ServerKey,
|
||||
AllowAuto: s.AllowAutoCert,
|
||||
Parent: s.Parent,
|
||||
}
|
||||
err := s.loader.Initialize()
|
||||
if err != nil {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{
|
||||
¶meter.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{
|
||||
¶meter.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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user