1
0
Fork 0
forked from External/mediamtx

Compare commits

..

2 commits

Author SHA1 Message Date
aler9
a16ef00e88 temp 2024-05-30 23:10:26 +02:00
aler9
f6b0a72453 set server version when compiling from source too 2024-05-30 13:31:57 +02:00
88 changed files with 1105 additions and 3370 deletions

View file

@ -3,6 +3,7 @@
/binaries /binaries
/coverage*.txt /coverage*.txt
/apidocs/*.html /apidocs/*.html
/internal/core/version.go
/internal/servers/hls/hls.min.js /internal/servers/hls/hls.min.js
/internal/protocols/rpicamera/exe/text_font.h /internal/protocols/rpicamera/exe/text_font.h
/internal/protocols/rpicamera/exe/exe /internal/protocols/rpicamera/exe/exe

View file

@ -19,26 +19,23 @@ jobs:
&& git config user.email bot@mediamtx && git config user.email bot@mediamtx
&& ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs) && ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs)
- run: | - run: >
set -e
VERSION=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases?per_page=1 | grep tag_name | sed 's/\s\+"tag_name": "\(.\+\)",/\1/') VERSION=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases?per_page=1 | grep tag_name | sed 's/\s\+"tag_name": "\(.\+\)",/\1/')
HASH=$(curl -sL https://github.com/video-dev/hls.js/releases/download/$VERSION/release.zip -o- | sha256sum | cut -f1 -d ' ') && echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION
echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION && echo VERSION=$VERSION >> $GITHUB_ENV
echo $HASH > internal/servers/hls/hlsjsdownloader/HASH
echo VERSION=$VERSION >> $GITHUB_ENV
- id: check_repo - id: check_repo
run: > run: >
test -n "$(git status --porcelain)" && echo "update=1" >> "$GITHUB_OUTPUT" || echo "update=0" >> "$GITHUB_OUTPUT" echo "clean=$(git status --porcelain)" >> "$GITHUB_OUTPUT"
- if: ${{ steps.check_repo.outputs.update == '1' }} - if: ${{ steps.check_repo.outputs.clean != '' }}
run: > run: >
git reset ${GITHUB_REF_NAME} git reset ${GITHUB_REF_NAME}
&& git add . && git add .
&& git commit -m "bump hls.js to ${VERSION}" && git commit -m "bump hls.js to ${VERSION}"
&& git push --set-upstream origin deps/hlsjs --force && git push --set-upstream origin deps/hlsjs --force
- if: ${{ steps.check_repo.outputs.update == '1' }} - if: ${{ steps.check_repo.outputs.clean != '' }}
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -17,7 +17,7 @@ jobs:
with: with:
go-version: "1.22" go-version: "1.22"
- run: touch internal/servers/hls/hls.min.js - run: go generate ./...
- uses: golangci/golangci-lint-action@v4 - uses: golangci/golangci-lint-action@v4
with: with:

View file

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: make test - run: make test
@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: make test32 - run: make test32
@ -31,10 +31,15 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: "1.22" go-version: "1.22"
- name: GitHub Tag Name example
run: |
echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME"
echo "Tag name from github.ref_name: ${{ github.ref_name }}"
- run: make test-highlevel-nodocker - run: make test-highlevel-nodocker

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
/binaries /binaries
/coverage*.txt /coverage*.txt
/apidocs/*.html /apidocs/*.html
/internal/core/version.go
/internal/servers/hls/hls.min.js /internal/servers/hls/hls.min.js
/internal/protocols/rpicamera/exe/text_font.h /internal/protocols/rpicamera/exe/text_font.h
/internal/protocols/rpicamera/exe/exe /internal/protocols/rpicamera/exe/exe

View file

@ -1,17 +0,0 @@
## build ergo binary
FROM docker.io/golang:1.22-alpine AS build-env
RUN apk upgrade -U --force-refresh --no-cache
RUN apk add --no-cache --purge --clean-protected -l -u make git
# copy ergo source
WORKDIR /go/src/cef.icu/mediamtx
COPY . .
RUN go generate ./...
RUN CGO_ENABLED=0 go build .
WORKDIR /mediamtx
RUN mkdir conf && cp /go/src/cef.icu/mediamtx/mediamtx .
EXPOSE 8189/udp 8198/tcp 1935/tcp 8889/tcp 9997/tcp
ENTRYPOINT ["/mediamtx/mediamtx"]

View file

@ -22,8 +22,8 @@ Live streams can be published to the server with:
|--------|--------|------------|------------| |--------|--------|------------|------------|
|[SRT clients](#srt-clients)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3| |[SRT clients](#srt-clients)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
|[SRT cameras and servers](#srt-cameras-and-servers)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3| |[SRT cameras and servers](#srt-cameras-and-servers)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
|[WebRTC clients](#webrtc-clients)|Browser-based, WHIP|AV1, VP9, VP8, H265, H264|Opus, G722, G711 (PCMA, PCMU)| |[WebRTC clients](#webrtc-clients)|Browser-based, WHIP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)|
|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H265, H264|Opus, G722, G711 (PCMA, PCMU)| |[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)|
|[RTSP clients](#rtsp-clients)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec| |[RTSP clients](#rtsp-clients)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|[RTSP cameras and servers](#rtsp-cameras-and-servers)|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec| |[RTSP cameras and servers](#rtsp-cameras-and-servers)|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM| |[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM|
@ -134,8 +134,7 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
* [SRT-specific features](#srt-specific-features) * [SRT-specific features](#srt-specific-features)
* [Standard stream ID syntax](#standard-stream-id-syntax) * [Standard stream ID syntax](#standard-stream-id-syntax)
* [WebRTC-specific features](#webrtc-specific-features) * [WebRTC-specific features](#webrtc-specific-features)
* [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep) * [Connectivity issues](#connectivity-issues)
* [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues)
* [RTSP-specific features](#rtsp-specific-features) * [RTSP-specific features](#rtsp-specific-features)
* [Transport protocols](#transport-protocols) * [Transport protocols](#transport-protocols)
* [Encryption](#encryption) * [Encryption](#encryption)
@ -339,7 +338,6 @@ Latest versions of OBS Studio can publish to the server with the [WebRTC / WHIP
* Service: `WHIP` * Service: `WHIP`
* Server: `http://localhost:8889/mystream/whip` * Server: `http://localhost:8889/mystream/whip`
* Bearer Token: `myuser:mypass` (if internal authentication is enabled) or JWT (if JWT-based authentication is enabled)
Save the configuration and click `Start streaming`. Save the configuration and click `Start streaming`.
@ -612,9 +610,7 @@ WHIP is a WebRTC extensions that allows to publish streams by using a URL, witho
http://localhost:8889/mystream/whip http://localhost:8889/mystream/whip
``` ```
Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep). Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations.
Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).
Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio). Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio).
@ -880,9 +876,7 @@ WHEP is a WebRTC extensions that allows to read streams by using a URL, without
http://localhost:8889/mystream/whep http://localhost:8889/mystream/whep
``` ```
Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep). Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations.
Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).
Known clients that can read with WebRTC and WHEP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [web browsers](#web-browsers-1). Known clients that can read with WebRTC and WHEP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [web browsers](#web-browsers-1).
@ -1186,20 +1180,12 @@ The JWT is expected to contain the `mediamtx_permissions` scope, with a list of
} }
``` ```
Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of any other protocol), for instance (RTSP): Clients are expected to pass the JWT in query parameters, for instance:
``` ```
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT
``` ```
For instance (HLS):
```
GET /mypath/index.m3u8 HTTP/1.1
Host: example.com
Authorization: Bearer MY_JWT
```
Here's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide such JWTs: Here's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide such JWTs:
1. Start Keycloak: 1. Start Keycloak:
@ -1683,7 +1669,6 @@ pathDefaults:
# * G1, G2, ...: regular expression groups, if path name is # * G1, G2, ...: regular expression groups, if path name is
# a regular expression. # a regular expression.
# * MTX_SEGMENT_PATH: segment file path # * MTX_SEGMENT_PATH: segment file path
# * MTX_SEGMENT_DURATION: segment duration
runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH
``` ```
@ -1844,35 +1829,7 @@ Where:
### WebRTC-specific features ### WebRTC-specific features
#### Authenticating with WHIP/WHEP #### Connectivity issues
When using WHIP or WHEP to establish a WebRTC connection, there are multiple ways to provide credentials.
If internal authentication or HTTP-based authentication is enabled, username and password can be passed through the `Authentication: Basic` header:
```
Authentication: Basic [base64_encoded_credentials]
```
Username and password can be also passed through the `Authentication: Bearer` header (since it's mandated by the specification):
```
Authentication: Bearer username:password
```
If JWT-based authentication is enabled, JWT can be passed through the `Authentication: Bearer` header:
```
Authentication: Bearer [jwt]
```
The JWT can also be passed through query parameters:
```
http://localhost:8889/mystream/whip?jwt=[jwt]
```
#### Solving WebRTC connectivity issues
If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection. If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection.

View file

@ -246,10 +246,6 @@ components:
type: string type: string
clientOnly: clientOnly:
type: boolean type: boolean
webrtcHandshakeTimeout:
type: string
webrtcTrackGatherTimeout:
type: string
# SRT server # SRT server
srt: srt:

32
go.mod
View file

@ -8,19 +8,19 @@ require (
github.com/MicahParks/keyfunc/v3 v3.3.3 github.com/MicahParks/keyfunc/v3 v3.3.3
github.com/abema/go-mp4 v1.2.0 github.com/abema/go-mp4 v1.2.0
github.com/alecthomas/kong v0.9.0 github.com/alecthomas/kong v0.9.0
github.com/bluenviron/gohlslib v1.4.0 github.com/bluenviron/gohlslib v1.3.3
github.com/bluenviron/gortsplib/v4 v4.10.2 github.com/bluenviron/gortsplib/v4 v4.10.0
github.com/bluenviron/mediacommon v1.12.1 github.com/bluenviron/mediacommon v1.11.0
github.com/datarhei/gosrt v0.7.0 github.com/datarhei/gosrt v0.6.0
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gookit/color v1.5.4 github.com/gookit/color v1.5.4
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/matthewhartstonge/argon2 v1.0.0 github.com/matthewhartstonge/argon2 v1.0.0
github.com/pion/ice/v2 v2.3.24 github.com/pion/ice/v2 v2.3.11
github.com/pion/interceptor v0.1.29 github.com/pion/interceptor v0.1.29
github.com/pion/logging v0.2.2 github.com/pion/logging v0.2.2
github.com/pion/rtcp v1.2.14 github.com/pion/rtcp v1.2.14
@ -28,9 +28,9 @@ require (
github.com/pion/sdp/v3 v3.0.9 github.com/pion/sdp/v3 v3.0.9
github.com/pion/webrtc/v3 v3.2.22 github.com/pion/webrtc/v3 v3.2.22
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.25.0 golang.org/x/crypto v0.23.0
golang.org/x/sys v0.22.0 golang.org/x/sys v0.20.0
golang.org/x/term v0.22.0 golang.org/x/term v0.20.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@ -58,20 +58,20 @@ require (
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pion/datachannel v1.5.5 // indirect github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/mdns v0.0.12 // indirect github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.16 // indirect github.com/pion/sctp v1.8.8 // indirect
github.com/pion/srtp/v2 v2.0.18 // indirect github.com/pion/srtp/v2 v2.0.18 // indirect
github.com/pion/stun v0.6.1 // indirect github.com/pion/stun v0.6.1 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect github.com/pion/transport/v2 v2.2.3 // indirect
github.com/pion/turn/v2 v2.1.3 // indirect github.com/pion/turn/v2 v2.1.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
@ -79,6 +79,6 @@ require (
replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5 replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5
replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1
replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6

93
go.sum
View file

@ -10,22 +10,22 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 h1:Vax9SzYE68ZYLwFaK7lnCV2ZhX9/YqAJX6xxROPRqEM= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 h1:fD6eZt+3/t8bzFn6ZZA2eP63xBP06v3EPfPJu8DO8ys=
github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 h1:WtKhXOpd8lgTeXF3RQVOzkNRuy83ygvWEpMYD2aoY3Q= github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 h1:wMd3D1mLghoYYh31STig8Kwm2qi8QyQKUy09qUUZrVw=
github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E= github.com/bluenviron/gohlslib v1.3.3 h1:Ji4PW9QHHCbpBteZCKk+rGY6emFNSGVFMsAa/3xFChk=
github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo= github.com/bluenviron/gohlslib v1.3.3/go.mod h1:MQcRjI9fYBNb9QhZO3RydgtbfCRhjogj6YMrpCDuTvY=
github.com/bluenviron/gortsplib/v4 v4.10.2 h1:O7HPRG8Pv4zUbyYD0HYH4Ufu1Hg9FJGTlizx6a09hL0= github.com/bluenviron/gortsplib/v4 v4.10.0 h1:9vJsUDuBgSinm41CR6yWnSMZ7TRWeB/oiAuN4lo30bU=
github.com/bluenviron/gortsplib/v4 v4.10.2/go.mod h1:re/L/vYh2wLPElQNAYah+bRFHJs0aRkM1MLX3WJ3N6M= github.com/bluenviron/gortsplib/v4 v4.10.0/go.mod h1:iLJ1tmwGMbaN04ZYh/KRlAHsCbz9Rycn7cPAvdR+Vkc=
github.com/bluenviron/mediacommon v1.12.1 h1:sgDJaKV6OXrPCSO0KPp9zi/pwNWtKHenn5/dvjtY+Tg= github.com/bluenviron/mediacommon v1.11.0 h1:1xY4QGYz7da9tsV2Xvd+ol+Ul5qq2g7ADJtIlVkQSRI=
github.com/bluenviron/mediacommon v1.12.1/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= github.com/bluenviron/mediacommon v1.11.0/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
@ -37,8 +37,8 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/datarhei/gosrt v0.7.0 h1:1/IY66HVVgqGA9zkmL5l6jUFuI8t/76WkuamSkJqHqs= github.com/datarhei/gosrt v0.6.0 h1:HrrXAw90V78ok4WMIhX6se1aTHPCn82Sg2hj+PhdmGc=
github.com/datarhei/gosrt v0.7.0/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= github.com/datarhei/gosrt v0.6.0/go.mod h1:fsOWdLSHUHShHjgi/46h6wjtdQrtnSdAQFnlas8ONxs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -81,13 +81,13 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@ -137,23 +137,28 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg= github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg=
github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY= github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U=
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE= github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
@ -162,19 +167,20 @@ github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA= github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -207,11 +213,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -224,14 +230,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -254,39 +261,41 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -10,6 +10,7 @@ import (
"os" "os"
"reflect" "reflect"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -35,6 +36,58 @@ func interfaceIsEmpty(i interface{}) bool {
return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil() return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil()
} }
func paginate2(itemsPtr interface{}, itemsPerPage int, page int) int {
ritems := reflect.ValueOf(itemsPtr).Elem()
itemsLen := ritems.Len()
if itemsLen == 0 {
return 0
}
pageCount := (itemsLen / itemsPerPage)
if (itemsLen % itemsPerPage) != 0 {
pageCount++
}
min := page * itemsPerPage
if min > itemsLen {
min = itemsLen
}
max := (page + 1) * itemsPerPage
if max > itemsLen {
max = itemsLen
}
ritems.Set(ritems.Slice(min, max))
return pageCount
}
func paginate(itemsPtr interface{}, itemsPerPageStr string, pageStr string) (int, error) {
itemsPerPage := 100
if itemsPerPageStr != "" {
tmp, err := strconv.ParseUint(itemsPerPageStr, 10, 31)
if err != nil {
return 0, err
}
itemsPerPage = int(tmp)
}
page := 0
if pageStr != "" {
tmp, err := strconv.ParseUint(pageStr, 10, 31)
if err != nil {
return 0, err
}
page = int(tmp)
}
return paginate2(itemsPtr, itemsPerPage, page), nil
}
func sortedKeys(paths map[string]*conf.Path) []string { func sortedKeys(paths map[string]*conf.Path) []string {
ret := make([]string, len(paths)) ret := make([]string, len(paths))
i := 0 i := 0
@ -137,7 +190,6 @@ func (a *API) Initialize() error {
router := gin.New() router := gin.New()
router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(a.middlewareOrigin, a.middlewareAuth)
group := router.Group("/", a.middlewareOrigin, a.middlewareAuth) group := router.Group("/", a.middlewareOrigin, a.middlewareAuth)
group.GET("/v3/config/global/get", a.onConfigGlobalGet) group.GET("/v3/config/global/get", a.onConfigGlobalGet)
@ -251,15 +303,6 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) {
func (a *API) middlewareOrigin(ctx *gin.Context) { func (a *API) middlewareOrigin(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// preflight requests
if ctx.Request.Method == http.MethodOptions &&
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
ctx.AbortWithStatus(http.StatusNoContent)
return
}
} }
func (a *API) middlewareAuth(ctx *gin.Context) { func (a *API) middlewareAuth(ctx *gin.Context) {

View file

@ -74,41 +74,36 @@ func checkError(t *testing.T, msg string, body io.Reader) {
require.Equal(t, map[string]interface{}{"error": msg}, resErr) require.Equal(t, map[string]interface{}{"error": msg}, resErr)
} }
func TestPreflightRequest(t *testing.T) { func TestPaginate(t *testing.T) {
api := API{ items := make([]int, 5)
Address: "localhost:9997", for i := 0; i < 5; i++ {
AllowOrigin: "*", items[i] = i
ReadTimeout: conf.StringDuration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: &testParent{},
} }
err := api.Initialize()
pageCount, err := paginate(&items, "1", "1")
require.NoError(t, err) require.NoError(t, err)
defer api.Close() require.Equal(t, 5, pageCount)
require.Equal(t, []int{1}, items)
tr := &http.Transport{} items = make([]int, 5)
defer tr.CloseIdleConnections() for i := 0; i < 5; i++ {
hc := &http.Client{Transport: tr} items[i] = i
}
req, err := http.NewRequest(http.MethodOptions, "http://localhost:9997", nil) pageCount, err = paginate(&items, "3", "2")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, pageCount)
require.Equal(t, []int{}, items)
req.Header.Add("Access-Control-Request-Method", "GET") items = make([]int, 6)
for i := 0; i < 6; i++ {
items[i] = i
}
res, err := hc.Do(req) pageCount, err = paginate(&items, "4", "1")
require.NoError(t, err) require.NoError(t, err)
defer res.Body.Close() require.Equal(t, 2, pageCount)
require.Equal(t, []int{4, 5}, items)
require.Equal(t, http.StatusNoContent, res.StatusCode)
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods"))
require.Equal(t, "Authorization, Content-Type", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
} }
func TestConfigAuth(t *testing.T) { func TestConfigAuth(t *testing.T) {
@ -284,14 +279,16 @@ func TestConfigPathDefaultsPatch(t *testing.T) {
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch", httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch",
map[string]interface{}{ map[string]interface{}{
"recordFormat": "fmp4", "readUser": "myuser",
"readPass": "mypass",
}, nil) }, nil)
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
var out map[string]interface{} var out map[string]interface{}
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out) httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out)
require.Equal(t, "fmp4", out["recordFormat"]) require.Equal(t, "myuser", out["readUser"])
require.Equal(t, "mypass", out["readPass"])
} }
func TestConfigPathsList(t *testing.T) { func TestConfigPathsList(t *testing.T) {

View file

@ -1,63 +0,0 @@
package api
import (
"fmt"
"reflect"
"strconv"
)
func paginate2(itemsPtr interface{}, itemsPerPage int, page int) int {
ritems := reflect.ValueOf(itemsPtr).Elem()
itemsLen := ritems.Len()
if itemsLen == 0 {
return 0
}
pageCount := (itemsLen / itemsPerPage)
if (itemsLen % itemsPerPage) != 0 {
pageCount++
}
min := page * itemsPerPage
if min > itemsLen {
min = itemsLen
}
max := (page + 1) * itemsPerPage
if max > itemsLen {
max = itemsLen
}
ritems.Set(ritems.Slice(min, max))
return pageCount
}
func paginate(itemsPtr interface{}, itemsPerPageStr string, pageStr string) (int, error) {
itemsPerPage := 100
if itemsPerPageStr != "" {
tmp, err := strconv.ParseUint(itemsPerPageStr, 10, 31)
if err != nil {
return 0, err
}
itemsPerPage = int(tmp)
if itemsPerPage == 0 {
return 0, fmt.Errorf("invalid items per page")
}
}
page := 0
if pageStr != "" {
tmp, err := strconv.ParseUint(pageStr, 10, 31)
if err != nil {
return 0, err
}
page = int(tmp)
}
return paginate2(itemsPtr, itemsPerPage, page), nil
}

View file

@ -1,65 +0,0 @@
package api
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestPaginate(t *testing.T) {
func() {
items := make([]int, 5)
for i := 0; i < 5; i++ {
items[i] = i
}
pageCount, err := paginate(&items, "1", "1")
require.NoError(t, err)
require.Equal(t, 5, pageCount)
require.Equal(t, []int{1}, items)
}()
func() {
items := make([]int, 5)
for i := 0; i < 5; i++ {
items[i] = i
}
pageCount, err := paginate(&items, "3", "2")
require.NoError(t, err)
require.Equal(t, 2, pageCount)
require.Equal(t, []int{}, items)
}()
func() {
items := make([]int, 6)
for i := 0; i < 6; i++ {
items[i] = i
}
pageCount, err := paginate(&items, "4", "1")
require.NoError(t, err)
require.Equal(t, 2, pageCount)
require.Equal(t, []int{4, 5}, items)
}()
func() {
items := make([]int, 0)
pageCount, err := paginate(&items, "1", "0")
require.NoError(t, err)
require.Equal(t, 0, pageCount)
require.Equal(t, []int{}, items)
}()
}
func FuzzPaginate(f *testing.F) {
f.Fuzz(func(_ *testing.T, str1 string, str2 string) {
items := make([]int, 6)
for i := 0; i < 6; i++ {
items[i] = i
}
paginate(&items, str1, str2) //nolint:errcheck
})
}

View file

@ -1,3 +0,0 @@
go test fuzz v1
string("A")
string("0")

View file

@ -1,3 +0,0 @@
go test fuzz v1
string("1")
string("A")

View file

@ -1,3 +0,0 @@
go test fuzz v1
string("0")
string("")

View file

@ -94,25 +94,24 @@ func mustParseCIDR(v string) net.IPNet {
return *ne return *ne
} }
func anyPathHasDeprecatedCredentials(pathDefaults Path, paths map[string]*OptionalPath) bool { func credentialIsNotEmpty(c *Credential) bool {
if pathDefaults.PublishUser != nil || return c != nil && *c != ""
pathDefaults.PublishPass != nil || }
pathDefaults.PublishIPs != nil ||
pathDefaults.ReadUser != nil ||
pathDefaults.ReadPass != nil ||
pathDefaults.ReadIPs != nil {
return true
}
func ipNetworkIsNotEmpty(i *IPNetworks) bool {
return i != nil && len(*i) != 0
}
func anyPathHasDeprecatedCredentials(paths map[string]*OptionalPath) bool {
for _, pa := range paths { for _, pa := range paths {
if pa != nil { if pa != nil {
rva := reflect.ValueOf(pa.Values).Elem() rva := reflect.ValueOf(pa.Values).Elem()
if rva.FieldByName("PublishUser").Interface().(*Credential) != nil || if credentialIsNotEmpty(rva.FieldByName("PublishUser").Interface().(*Credential)) ||
rva.FieldByName("PublishPass").Interface().(*Credential) != nil || credentialIsNotEmpty(rva.FieldByName("PublishPass").Interface().(*Credential)) ||
rva.FieldByName("PublishIPs").Interface().(*IPNetworks) != nil || ipNetworkIsNotEmpty(rva.FieldByName("PublishIPs").Interface().(*IPNetworks)) ||
rva.FieldByName("ReadUser").Interface().(*Credential) != nil || credentialIsNotEmpty(rva.FieldByName("ReadUser").Interface().(*Credential)) ||
rva.FieldByName("ReadPass").Interface().(*Credential) != nil || credentialIsNotEmpty(rva.FieldByName("ReadPass").Interface().(*Credential)) ||
rva.FieldByName("ReadIPs").Interface().(*IPNetworks) != nil { ipNetworkIsNotEmpty(rva.FieldByName("ReadIPs").Interface().(*IPNetworks)) {
return true return true
} }
} }
@ -120,40 +119,6 @@ func anyPathHasDeprecatedCredentials(pathDefaults Path, paths map[string]*Option
return false return false
} }
var defaultAuthInternalUsers = AuthInternalUsers{
{
User: "any",
Pass: "",
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionPublish,
},
{
Action: AuthActionRead,
},
{
Action: AuthActionPlayback,
},
},
},
{
User: "any",
Pass: "",
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionAPI,
},
{
Action: AuthActionMetrics,
},
{
Action: AuthActionPprof,
},
},
},
}
// Conf is a configuration. // Conf is a configuration.
// WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092 // WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092
type Conf struct { type Conf struct {
@ -273,8 +238,6 @@ type Conf struct {
WebRTCIPsFromInterfacesList []string `json:"webrtcIPsFromInterfacesList"` WebRTCIPsFromInterfacesList []string `json:"webrtcIPsFromInterfacesList"`
WebRTCAdditionalHosts []string `json:"webrtcAdditionalHosts"` WebRTCAdditionalHosts []string `json:"webrtcAdditionalHosts"`
WebRTCICEServers2 WebRTCICEServers `json:"webrtcICEServers2"` WebRTCICEServers2 WebRTCICEServers `json:"webrtcICEServers2"`
WebRTCHandshakeTimeout StringDuration `json:"webrtcHandshakeTimeout"`
WebRTCTrackGatherTimeout StringDuration `json:"webrtcTrackGatherTimeout"`
WebRTCICEUDPMuxAddress *string `json:"webrtcICEUDPMuxAddress,omitempty"` // deprecated WebRTCICEUDPMuxAddress *string `json:"webrtcICEUDPMuxAddress,omitempty"` // deprecated
WebRTCICETCPMuxAddress *string `json:"webrtcICETCPMuxAddress,omitempty"` // deprecated WebRTCICETCPMuxAddress *string `json:"webrtcICETCPMuxAddress,omitempty"` // deprecated
WebRTCICEHostNAT1To1IPs *[]string `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated WebRTCICEHostNAT1To1IPs *[]string `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated
@ -311,7 +274,39 @@ func (conf *Conf) setDefaults() {
conf.UDPMaxPayloadSize = 1472 conf.UDPMaxPayloadSize = 1472
// Authentication // Authentication
conf.AuthInternalUsers = defaultAuthInternalUsers conf.AuthInternalUsers = []AuthInternalUser{
{
User: "any",
Pass: "",
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionPublish,
},
{
Action: AuthActionRead,
},
{
Action: AuthActionPlayback,
},
},
},
{
User: "any",
Pass: "",
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionAPI,
},
{
Action: AuthActionMetrics,
},
{
Action: AuthActionPprof,
},
},
},
}
conf.AuthHTTPExclude = []AuthInternalUserPermission{ conf.AuthHTTPExclude = []AuthInternalUserPermission{
{ {
Action: AuthActionAPI, Action: AuthActionAPI,
@ -397,8 +392,6 @@ func (conf *Conf) setDefaults() {
conf.WebRTCIPsFromInterfacesList = []string{} conf.WebRTCIPsFromInterfacesList = []string{}
conf.WebRTCAdditionalHosts = []string{} conf.WebRTCAdditionalHosts = []string{}
conf.WebRTCICEServers2 = []WebRTCICEServer{} conf.WebRTCICEServers2 = []WebRTCICEServer{}
conf.WebRTCHandshakeTimeout = 10 * StringDuration(time.Second)
conf.WebRTCTrackGatherTimeout = 2 * StringDuration(time.Second)
// SRT server // SRT server
conf.SRT = true conf.SRT = true
@ -504,6 +497,7 @@ func (conf *Conf) Validate() error {
} }
// Authentication // Authentication
if conf.ExternalAuthenticationURL != nil { if conf.ExternalAuthenticationURL != nil {
conf.AuthMethod = AuthMethodHTTP conf.AuthMethod = AuthMethodHTTP
conf.AuthHTTPAddress = *conf.ExternalAuthenticationURL conf.AuthHTTPAddress = *conf.ExternalAuthenticationURL
@ -519,15 +513,17 @@ func (conf *Conf) Validate() error {
return fmt.Errorf("'authJWTJWKS' must be a HTTP URL") return fmt.Errorf("'authJWTJWKS' must be a HTTP URL")
} }
deprecatedCredentialsMode := false deprecatedCredentialsMode := false
if anyPathHasDeprecatedCredentials(conf.PathDefaults, conf.OptionalPaths) { if credentialIsNotEmpty(conf.PathDefaults.PublishUser) ||
if conf.AuthInternalUsers != nil && !reflect.DeepEqual(conf.AuthInternalUsers, defaultAuthInternalUsers) { credentialIsNotEmpty(conf.PathDefaults.PublishPass) ||
return fmt.Errorf("authInternalUsers and legacy credentials " + ipNetworkIsNotEmpty(conf.PathDefaults.PublishIPs) ||
"(publishUser, publishPass, publishIPs, readUser, readPass, readIPs) cannot be used together") credentialIsNotEmpty(conf.PathDefaults.ReadUser) ||
} credentialIsNotEmpty(conf.PathDefaults.ReadPass) ||
ipNetworkIsNotEmpty(conf.PathDefaults.ReadIPs) ||
anyPathHasDeprecatedCredentials(conf.OptionalPaths) {
conf.AuthInternalUsers = []AuthInternalUser{ conf.AuthInternalUsers = []AuthInternalUser{
{ {
User: "any", User: "any",
Pass: "",
Permissions: []AuthInternalUserPermission{ Permissions: []AuthInternalUserPermission{
{ {
Action: AuthActionPlayback, Action: AuthActionPlayback,
@ -536,6 +532,7 @@ func (conf *Conf) Validate() error {
}, },
{ {
User: "any", User: "any",
Pass: "",
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")}, IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
Permissions: []AuthInternalUserPermission{ Permissions: []AuthInternalUserPermission{
{ {

View file

@ -192,66 +192,6 @@ func TestConfEncryption(t *testing.T) {
require.Equal(t, true, ok) require.Equal(t, true, ok)
} }
func TestConfDeprecatedAuth(t *testing.T) {
tmpf, err := createTempFile([]byte(
"paths:\n" +
" cam:\n" +
" readUser: myuser\n" +
" readPass: mypass\n"))
require.NoError(t, err)
defer os.Remove(tmpf)
conf, _, err := Load(tmpf, nil)
require.NoError(t, err)
require.Equal(t, AuthInternalUsers{
{
User: "any",
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionPlayback,
},
},
},
{
User: "any",
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionAPI,
},
{
Action: AuthActionMetrics,
},
{
Action: AuthActionPprof,
},
},
},
{
User: "any",
IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionPublish,
Path: "cam",
},
},
},
{
User: "myuser",
Pass: "mypass",
IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionRead,
Path: "cam",
},
},
},
}, conf.AuthInternalUsers)
}
func TestConfErrors(t *testing.T) { func TestConfErrors(t *testing.T) {
for _, ca := range []struct { for _, ca := range []struct {
name string name string

View file

@ -180,10 +180,6 @@ type Path struct {
RunOnUnread string `json:"runOnUnread"` RunOnUnread string `json:"runOnUnread"`
RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"` RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"`
RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"` RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"`
// Custom hooks
HTTPOnReady string `json:"httpOnReady"`
HTTPOnNotReady string `json:"httpOnNotReady"`
} }
func (pconf *Path) setDefaults() { func (pconf *Path) setDefaults() {
@ -403,17 +399,17 @@ func (pconf *Path) validate(
if deprecatedCredentialsMode { if deprecatedCredentialsMode {
func() { func() {
var user Credential = "any" var user Credential = "any"
if pconf.PublishUser != nil && *pconf.PublishUser != "" { if credentialIsNotEmpty(pconf.PublishUser) {
user = *pconf.PublishUser user = *pconf.PublishUser
} }
var pass Credential var pass Credential
if pconf.PublishPass != nil && *pconf.PublishPass != "" { if credentialIsNotEmpty(pconf.PublishPass) {
pass = *pconf.PublishPass pass = *pconf.PublishPass
} }
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")} ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
if pconf.PublishIPs != nil && len(*pconf.PublishIPs) != 0 { if ipNetworkIsNotEmpty(pconf.PublishIPs) {
ips = *pconf.PublishIPs ips = *pconf.PublishIPs
} }
@ -435,17 +431,17 @@ func (pconf *Path) validate(
func() { func() {
var user Credential = "any" var user Credential = "any"
if pconf.ReadUser != nil && *pconf.ReadUser != "" { if credentialIsNotEmpty(pconf.ReadUser) {
user = *pconf.ReadUser user = *pconf.ReadUser
} }
var pass Credential var pass Credential
if pconf.ReadPass != nil && *pconf.ReadPass != "" { if credentialIsNotEmpty(pconf.ReadPass) {
pass = *pconf.ReadPass pass = *pconf.ReadPass
} }
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")} ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
if pconf.ReadIPs != nil && len(*pconf.ReadIPs) != 0 { if ipNetworkIsNotEmpty(pconf.ReadIPs) {
ips = *pconf.ReadIPs ips = *pconf.ReadIPs
} }

View file

@ -546,7 +546,7 @@ func TestAPIProtocolListGet(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) w := mpegts.NewWriter(bw, []*mpegts.Track{track})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track, 0, 0, true, [][]byte{{1}}) err = w.WriteH26x(track, 0, 0, true, [][]byte{{1}})
require.NoError(t, err) require.NoError(t, err)
err = bw.Flush() err = bw.Flush()
@ -1021,7 +1021,7 @@ func TestAPIProtocolKick(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) w := mpegts.NewWriter(bw, []*mpegts.Track{track})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track, 0, 0, true, [][]byte{{1}}) err = w.WriteH26x(track, 0, 0, true, [][]byte{{1}})
require.NoError(t, err) require.NoError(t, err)
err = bw.Flush() err = bw.Flush()

View file

@ -34,7 +34,7 @@ import (
"github.com/bluenviron/mediamtx/internal/servers/webrtc" "github.com/bluenviron/mediamtx/internal/servers/webrtc"
) )
var version = "v0.0.0" //go:generate go run ./versiongetter
var defaultConfPaths = []string{ var defaultConfPaths = []string{
"rtsp-simple-server.yml", "rtsp-simple-server.yml",
@ -580,8 +580,6 @@ func (p *Core) createResources(initial bool) error {
IPsFromInterfacesList: p.conf.WebRTCIPsFromInterfacesList, IPsFromInterfacesList: p.conf.WebRTCIPsFromInterfacesList,
AdditionalHosts: p.conf.WebRTCAdditionalHosts, AdditionalHosts: p.conf.WebRTCAdditionalHosts,
ICEServers: p.conf.WebRTCICEServers2, ICEServers: p.conf.WebRTCICEServers2,
HandshakeTimeout: p.conf.WebRTCHandshakeTimeout,
TrackGatherTimeout: p.conf.WebRTCTrackGatherTimeout,
ExternalCmdPool: p.externalCmdPool, ExternalCmdPool: p.externalCmdPool,
PathManager: p.pathManager, PathManager: p.pathManager,
Parent: p, Parent: p,
@ -850,8 +848,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
!reflect.DeepEqual(newConf.WebRTCIPsFromInterfacesList, p.conf.WebRTCIPsFromInterfacesList) || !reflect.DeepEqual(newConf.WebRTCIPsFromInterfacesList, p.conf.WebRTCIPsFromInterfacesList) ||
!reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) || !reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) ||
!reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) || !reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
newConf.WebRTCHandshakeTimeout != p.conf.WebRTCHandshakeTimeout ||
newConf.WebRTCTrackGatherTimeout != p.conf.WebRTCTrackGatherTimeout ||
closeMetrics || closeMetrics ||
closePathManager || closePathManager ||
closeLogger closeLogger

View file

@ -218,7 +218,7 @@ webrtc_sessions_bytes_sent 0
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) w := mpegts.NewWriter(bw, []*mpegts.Track{track})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track, 0, 0, true, [][]byte{ err = w.WriteH26x(track, 0, 0, true, [][]byte{
test.FormatH264.SPS, test.FormatH264.SPS,
test.FormatH264.PPS, test.FormatH264.PPS,
{0x05, 1}, // IDR {0x05, 1}, // IDR

View file

@ -169,19 +169,26 @@ func (pa *path) run() {
if pa.conf.Source == "redirect" { if pa.conf.Source == "redirect" {
pa.source = &sourceRedirect{} pa.source = &sourceRedirect{}
} else if pa.conf.HasStaticSource() { } else if pa.conf.HasStaticSource() {
resolvedSource := pa.conf.Source
if len(pa.matches) > 1 {
for i, ma := range pa.matches[1:] {
resolvedSource = strings.ReplaceAll(resolvedSource, "$G"+strconv.FormatInt(int64(i+1), 10), ma)
}
}
pa.source = &staticSourceHandler{ pa.source = &staticSourceHandler{
conf: pa.conf, conf: pa.conf,
logLevel: pa.logLevel, logLevel: pa.logLevel,
readTimeout: pa.readTimeout, readTimeout: pa.readTimeout,
writeTimeout: pa.writeTimeout, writeTimeout: pa.writeTimeout,
writeQueueSize: pa.writeQueueSize, writeQueueSize: pa.writeQueueSize,
matches: pa.matches, resolvedSource: resolvedSource,
parent: pa, parent: pa,
} }
pa.source.(*staticSourceHandler).initialize() pa.source.(*staticSourceHandler).initialize()
if !pa.conf.SourceOnDemand { if !pa.conf.SourceOnDemand {
pa.source.(*staticSourceHandler).start(false, "") pa.source.(*staticSourceHandler).start(false)
} }
} }
@ -424,7 +431,7 @@ func (pa *path) doDescribe(req defs.PathDescribeReq) {
if pa.conf.HasOnDemandStaticSource() { if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateInitial { if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
pa.onDemandStaticSourceStart(req.AccessRequest.Query) pa.onDemandStaticSourceStart()
} }
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req) pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)
return return
@ -532,7 +539,7 @@ func (pa *path) doAddReader(req defs.PathAddReaderReq) {
if pa.conf.HasOnDemandStaticSource() { if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateInitial { if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
pa.onDemandStaticSourceStart(req.AccessRequest.Query) pa.onDemandStaticSourceStart()
} }
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req) pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)
return return
@ -648,8 +655,8 @@ func (pa *path) shouldClose() bool {
len(pa.readerAddRequestsOnHold) == 0 len(pa.readerAddRequestsOnHold) == 0
} }
func (pa *path) onDemandStaticSourceStart(query string) { func (pa *path) onDemandStaticSourceStart() {
pa.source.(*staticSourceHandler).start(true, query) pa.source.(*staticSourceHandler).start(true)
pa.onDemandStaticSourceReadyTimer.Stop() pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout)) pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))
@ -799,11 +806,10 @@ func (pa *path) startRecording() {
nil) nil)
} }
}, },
OnSegmentComplete: func(segmentPath string, segmentDuration time.Duration) { OnSegmentComplete: func(segmentPath string) {
if pa.conf.RunOnRecordSegmentComplete != "" { if pa.conf.RunOnRecordSegmentComplete != "" {
env := pa.ExternalCmdEnv() env := pa.ExternalCmdEnv()
env["MTX_SEGMENT_PATH"] = segmentPath env["MTX_SEGMENT_PATH"] = segmentPath
env["MTX_SEGMENT_DURATION"] = strconv.FormatFloat(segmentDuration.Seconds(), 'f', -1, 64)
pa.Log(logger.Info, "runOnRecordSegmentComplete command launched") pa.Log(logger.Info, "runOnRecordSegmentComplete command launched")
externalcmd.NewCmd( externalcmd.NewCmd(

View file

@ -105,12 +105,12 @@ func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Respo
var _ defs.Path = &path{} var _ defs.Path = &path{}
func TestPathRunOnDemand(t *testing.T) { func TestPathRunOnDemand(t *testing.T) {
onDemand := filepath.Join(os.TempDir(), "on_demand") onDemandFile := filepath.Join(os.TempDir(), "ondemand")
onUnDemand := filepath.Join(os.TempDir(), "on_undemand") onUnDemandFile := filepath.Join(os.TempDir(), "onundemand")
srcFile := filepath.Join(os.TempDir(), "ondemand.go") srcFile := filepath.Join(os.TempDir(), "ondemand.go")
err := os.WriteFile(srcFile, err := os.WriteFile(srcFile,
[]byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemand)), 0o644) []byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemandFile)), 0o644)
require.NoError(t, err) require.NoError(t, err)
execFile := filepath.Join(os.TempDir(), "ondemand_cmd") execFile := filepath.Join(os.TempDir(), "ondemand_cmd")
@ -125,8 +125,8 @@ func TestPathRunOnDemand(t *testing.T) {
for _, ca := range []string{"describe", "setup", "describe and setup"} { for _, ca := range []string{"describe", "setup", "describe and setup"} {
t.Run(ca, func(t *testing.T) { t.Run(ca, func(t *testing.T) {
defer os.Remove(onDemand) defer os.Remove(onDemandFile)
defer os.Remove(onUnDemand) defer os.Remove(onUnDemandFile)
p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+ p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
"hls: no\n"+ "hls: no\n"+
@ -135,7 +135,7 @@ func TestPathRunOnDemand(t *testing.T) {
" '~^(on)demand$':\n"+ " '~^(on)demand$':\n"+
" runOnDemand: %s\n"+ " runOnDemand: %s\n"+
" runOnDemandCloseAfter: 1s\n"+ " runOnDemandCloseAfter: 1s\n"+
" runOnUnDemand: touch %s\n", execFile, onUnDemand)) " runOnUnDemand: touch %s\n", execFile, onUnDemandFile))
require.Equal(t, true, ok) require.Equal(t, true, ok)
defer p1.Close() defer p1.Close()
@ -204,14 +204,14 @@ func TestPathRunOnDemand(t *testing.T) {
}() }()
for { for {
_, err := os.Stat(onUnDemand) _, err := os.Stat(onUnDemandFile)
if err == nil { if err == nil {
break break
} }
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
_, err := os.Stat(onDemand) _, err := os.Stat(onDemandFile)
require.NoError(t, err) require.NoError(t, err)
}) })
} }
@ -220,11 +220,11 @@ func TestPathRunOnDemand(t *testing.T) {
func TestPathRunOnConnect(t *testing.T) { func TestPathRunOnConnect(t *testing.T) {
for _, ca := range []string{"rtsp", "rtmp", "srt"} { for _, ca := range []string{"rtsp", "rtmp", "srt"} {
t.Run(ca, func(t *testing.T) { t.Run(ca, func(t *testing.T) {
onConnect := filepath.Join(os.TempDir(), "on_connect") onConnectFile := filepath.Join(os.TempDir(), "onconnect")
defer os.Remove(onConnect) defer os.Remove(onConnectFile)
onDisconnect := filepath.Join(os.TempDir(), "on_disconnect") onDisconnectFile := filepath.Join(os.TempDir(), "ondisconnect")
defer os.Remove(onDisconnect) defer os.Remove(onDisconnectFile)
func() { func() {
p, ok := newInstance(fmt.Sprintf( p, ok := newInstance(fmt.Sprintf(
@ -232,7 +232,7 @@ func TestPathRunOnConnect(t *testing.T) {
" test:\n"+ " test:\n"+
"runOnConnect: touch %s\n"+ "runOnConnect: touch %s\n"+
"runOnDisconnect: touch %s\n", "runOnDisconnect: touch %s\n",
onConnect, onDisconnect)) onConnectFile, onDisconnectFile))
require.Equal(t, true, ok) require.Equal(t, true, ok)
defer p.Close() defer p.Close()
@ -273,21 +273,21 @@ func TestPathRunOnConnect(t *testing.T) {
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
}() }()
_, err := os.Stat(onConnect) _, err := os.Stat(onConnectFile)
require.NoError(t, err) require.NoError(t, err)
_, err = os.Stat(onDisconnect) _, err = os.Stat(onDisconnectFile)
require.NoError(t, err) require.NoError(t, err)
}) })
} }
} }
func TestPathRunOnReady(t *testing.T) { func TestPathRunOnReady(t *testing.T) {
onReady := filepath.Join(os.TempDir(), "on_ready") onReadyFile := filepath.Join(os.TempDir(), "onready")
defer os.Remove(onReady) defer os.Remove(onReadyFile)
onNotReady := filepath.Join(os.TempDir(), "on_unready") onNotReadyFile := filepath.Join(os.TempDir(), "onunready")
defer os.Remove(onNotReady) defer os.Remove(onNotReadyFile)
func() { func() {
p, ok := newInstance(fmt.Sprintf("rtmp: no\n"+ p, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
@ -297,7 +297,7 @@ func TestPathRunOnReady(t *testing.T) {
" test:\n"+ " test:\n"+
" runOnReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+ " runOnReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
" runOnNotReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n", " runOnNotReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
onReady, onNotReady)) onReadyFile, onNotReadyFile))
require.Equal(t, true, ok) require.Equal(t, true, ok)
defer p.Close() defer p.Close()
@ -312,11 +312,11 @@ func TestPathRunOnReady(t *testing.T) {
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
}() }()
byts, err := os.ReadFile(onReady) byts, err := os.ReadFile(onReadyFile)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "test query=value\n", string(byts)) require.Equal(t, "test query=value\n", string(byts))
byts, err = os.ReadFile(onNotReady) byts, err = os.ReadFile(onNotReadyFile)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "test query=value\n", string(byts)) require.Equal(t, "test query=value\n", string(byts))
} }
@ -324,11 +324,11 @@ func TestPathRunOnReady(t *testing.T) {
func TestPathRunOnRead(t *testing.T) { func TestPathRunOnRead(t *testing.T) {
for _, ca := range []string{"rtsp", "rtmp", "srt", "webrtc"} { for _, ca := range []string{"rtsp", "rtmp", "srt", "webrtc"} {
t.Run(ca, func(t *testing.T) { t.Run(ca, func(t *testing.T) {
onRead := filepath.Join(os.TempDir(), "on_read") onReadFile := filepath.Join(os.TempDir(), "onread")
defer os.Remove(onRead) defer os.Remove(onReadFile)
onUnread := filepath.Join(os.TempDir(), "on_unread") onUnreadFile := filepath.Join(os.TempDir(), "onunread")
defer os.Remove(onUnread) defer os.Remove(onUnreadFile)
func() { func() {
p, ok := newInstance(fmt.Sprintf( p, ok := newInstance(fmt.Sprintf(
@ -336,7 +336,7 @@ func TestPathRunOnRead(t *testing.T) {
" test:\n"+ " test:\n"+
" runOnRead: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+ " runOnRead: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
" runOnUnread: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n", " runOnUnread: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
onRead, onUnread)) onReadFile, onUnreadFile))
require.Equal(t, true, ok) require.Equal(t, true, ok)
defer p.Close() defer p.Close()
@ -449,79 +449,17 @@ func TestPathRunOnRead(t *testing.T) {
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
}() }()
byts, err := os.ReadFile(onRead) byts, err := os.ReadFile(onReadFile)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "test query=value\n", string(byts)) require.Equal(t, "test query=value\n", string(byts))
byts, err = os.ReadFile(onUnread) byts, err = os.ReadFile(onUnreadFile)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "test query=value\n", string(byts)) require.Equal(t, "test query=value\n", string(byts))
}) })
} }
} }
func TestPathRunOnRecordSegment(t *testing.T) {
onRecordSegmentCreate := filepath.Join(os.TempDir(), "on_record_segment_create")
defer os.Remove(onRecordSegmentCreate)
onRecordSegmentComplete := filepath.Join(os.TempDir(), "on_record_segment_complete")
defer os.Remove(onRecordSegmentComplete)
recordDir, err := os.MkdirTemp("", "rtsp-path-record")
require.NoError(t, err)
defer os.RemoveAll(recordDir)
func() {
p, ok := newInstance("record: yes\n" +
"recordPath: " + filepath.Join(recordDir, "%path/%Y-%m-%d_%H-%M-%S-%f") + "\n" +
"paths:\n" +
" test:\n" +
" runOnRecordSegmentCreate: " +
"sh -c 'echo \"$MTX_SEGMENT_PATH\" > " + onRecordSegmentCreate + "'\n" +
" runOnRecordSegmentComplete: " +
"sh -c 'echo \"$MTX_SEGMENT_PATH $MTX_SEGMENT_DURATION\" > " + onRecordSegmentComplete + "'\n")
require.Equal(t, true, ok)
defer p.Close()
media0 := test.UniqueMediaH264()
source := gortsplib.Client{}
err = source.StartRecording(
"rtsp://localhost:8554/test",
&description.Session{Medias: []*description.Media{media0}})
require.NoError(t, err)
defer source.Close()
for i := 0; i < 4; i++ {
err = source.WritePacketRTP(media0, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 96,
SequenceNumber: 1123 + uint16(i),
Timestamp: 45343 + 90000*uint32(i),
SSRC: 563423,
},
Payload: []byte{5},
})
require.NoError(t, err)
}
time.Sleep(500 * time.Millisecond)
}()
byts, err := os.ReadFile(onRecordSegmentCreate)
require.NoError(t, err)
require.Equal(t, true, strings.HasPrefix(string(byts), recordDir))
byts, err = os.ReadFile(onRecordSegmentComplete)
require.NoError(t, err)
parts := strings.Split(string(byts[:len(byts)-1]), " ")
require.Equal(t, true, strings.HasPrefix(parts[0], recordDir))
require.Equal(t, "3", parts[1])
}
func TestPathMaxReaders(t *testing.T) { func TestPathMaxReaders(t *testing.T) {
p, ok := newInstance("paths:\n" + p, ok := newInstance("paths:\n" +
" all_others:\n" + " all_others:\n" +
@ -700,14 +638,13 @@ func TestPathFallback(t *testing.T) {
} }
} }
func TestPathResolveSource(t *testing.T) { func TestPathSourceRegexp(t *testing.T) {
var stream *gortsplib.ServerStream var stream *gortsplib.ServerStream
s := gortsplib.Server{ s := gortsplib.Server{
Handler: &testServer{ Handler: &testServer{
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx, onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) { ) (*base.Response, *gortsplib.ServerStream, error) {
require.Equal(t, "key=val", ctx.Query)
require.Equal(t, "/a", ctx.Path) require.Equal(t, "/a", ctx.Path)
return &base.Response{ return &base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@ -737,7 +674,7 @@ func TestPathResolveSource(t *testing.T) {
p, ok := newInstance( p, ok := newInstance(
"paths:\n" + "paths:\n" +
" '~^test_(.+)$':\n" + " '~^test_(.+)$':\n" +
" source: rtsp://127.0.0.1:8555/$G1?$MTX_QUERY\n" + " source: rtsp://127.0.0.1:8555/$G1\n" +
" sourceOnDemand: yes\n" + " sourceOnDemand: yes\n" +
" 'all':\n") " 'all':\n")
require.Equal(t, true, ok) require.Equal(t, true, ok)
@ -745,7 +682,7 @@ func TestPathResolveSource(t *testing.T) {
reader := gortsplib.Client{} reader := gortsplib.Client{}
u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a?key=val") u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a")
require.NoError(t, err) require.NoError(t, err)
err = reader.Start(u.Scheme, u.Host) err = reader.Start(u.Scheme, u.Host)

View file

@ -3,7 +3,6 @@ package core
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
@ -23,18 +22,6 @@ const (
staticSourceHandlerRetryPause = 5 * time.Second staticSourceHandlerRetryPause = 5 * time.Second
) )
func resolveSource(s string, matches []string, query string) string {
if len(matches) > 1 {
for i, ma := range matches[1:] {
s = strings.ReplaceAll(s, "$G"+strconv.FormatInt(int64(i+1), 10), ma)
}
}
s = strings.ReplaceAll(s, "$MTX_QUERY", query)
return s
}
type staticSourceHandlerParent interface { type staticSourceHandlerParent interface {
logger.Writer logger.Writer
staticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq) staticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)
@ -48,14 +35,13 @@ type staticSourceHandler struct {
readTimeout conf.StringDuration readTimeout conf.StringDuration
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
writeQueueSize int writeQueueSize int
matches []string resolvedSource string
parent staticSourceHandlerParent parent staticSourceHandlerParent
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
instance defs.StaticSource instance defs.StaticSource
running bool running bool
query string
// in // in
chReloadConf chan *conf.Path chReloadConf chan *conf.Path
@ -72,57 +58,60 @@ func (s *staticSourceHandler) initialize() {
s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq) s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
switch { switch {
case strings.HasPrefix(s.conf.Source, "rtsp://") || case strings.HasPrefix(s.resolvedSource, "rtsp://") ||
strings.HasPrefix(s.conf.Source, "rtsps://"): strings.HasPrefix(s.resolvedSource, "rtsps://"):
s.instance = &rtspsource.Source{ s.instance = &rtspsource.Source{
ResolvedSource: s.resolvedSource,
ReadTimeout: s.readTimeout, ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout, WriteTimeout: s.writeTimeout,
WriteQueueSize: s.writeQueueSize, WriteQueueSize: s.writeQueueSize,
Parent: s, Parent: s,
} }
case strings.HasPrefix(s.conf.Source, "rtmp://") || case strings.HasPrefix(s.resolvedSource, "rtmp://") ||
strings.HasPrefix(s.conf.Source, "rtmps://"): strings.HasPrefix(s.resolvedSource, "rtmps://"):
s.instance = &rtmpsource.Source{ s.instance = &rtmpsource.Source{
ReadTimeout: s.readTimeout, ResolvedSource: s.resolvedSource,
WriteTimeout: s.writeTimeout, ReadTimeout: s.readTimeout,
Parent: s, WriteTimeout: s.writeTimeout,
Parent: s,
} }
case strings.HasPrefix(s.conf.Source, "http://") || case strings.HasPrefix(s.resolvedSource, "http://") ||
strings.HasPrefix(s.conf.Source, "https://"): strings.HasPrefix(s.resolvedSource, "https://"):
s.instance = &hlssource.Source{ s.instance = &hlssource.Source{
ReadTimeout: s.readTimeout, ResolvedSource: s.resolvedSource,
Parent: s, ReadTimeout: s.readTimeout,
Parent: s,
} }
case strings.HasPrefix(s.conf.Source, "udp://"): case strings.HasPrefix(s.resolvedSource, "udp://"):
s.instance = &udpsource.Source{ s.instance = &udpsource.Source{
ReadTimeout: s.readTimeout, ResolvedSource: s.resolvedSource,
Parent: s, ReadTimeout: s.readTimeout,
Parent: s,
} }
case strings.HasPrefix(s.conf.Source, "srt://"): case strings.HasPrefix(s.resolvedSource, "srt://"):
s.instance = &srtsource.Source{ s.instance = &srtsource.Source{
ReadTimeout: s.readTimeout, ResolvedSource: s.resolvedSource,
Parent: s, ReadTimeout: s.readTimeout,
Parent: s,
} }
case strings.HasPrefix(s.conf.Source, "whep://") || case strings.HasPrefix(s.resolvedSource, "whep://") ||
strings.HasPrefix(s.conf.Source, "wheps://"): strings.HasPrefix(s.resolvedSource, "wheps://"):
s.instance = &webrtcsource.Source{ s.instance = &webrtcsource.Source{
ReadTimeout: s.readTimeout, ResolvedSource: s.resolvedSource,
Parent: s, ReadTimeout: s.readTimeout,
Parent: s,
} }
case s.conf.Source == "rpiCamera": case s.resolvedSource == "rpiCamera":
s.instance = &rpicamerasource.Source{ s.instance = &rpicamerasource.Source{
LogLevel: s.logLevel, LogLevel: s.logLevel,
Parent: s, Parent: s,
} }
default:
panic("should not happen")
} }
} }
@ -130,16 +119,12 @@ func (s *staticSourceHandler) close(reason string) {
s.stop(reason) s.stop(reason)
} }
func (s *staticSourceHandler) start(onDemand bool, query string) { func (s *staticSourceHandler) start(onDemand bool) {
if s.running { if s.running {
panic("should not happen") panic("should not happen")
} }
s.running = true s.running = true
s.query = query
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
s.done = make(chan struct{})
s.instance.Log(logger.Info, "started%s", s.instance.Log(logger.Info, "started%s",
func() string { func() string {
if onDemand { if onDemand {
@ -148,6 +133,9 @@ func (s *staticSourceHandler) start(onDemand bool, query string) {
return "" return ""
}()) }())
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
s.done = make(chan struct{})
go s.run() go s.run()
} }
@ -157,7 +145,6 @@ func (s *staticSourceHandler) stop(reason string) {
} }
s.running = false s.running = false
s.instance.Log(logger.Info, "stopped: %s", reason) s.instance.Log(logger.Info, "stopped: %s", reason)
s.ctxCancel() s.ctxCancel()
@ -180,15 +167,12 @@ func (s *staticSourceHandler) run() {
runReloadConf := make(chan *conf.Path) runReloadConf := make(chan *conf.Path)
recreate := func() { recreate := func() {
resolvedSource := resolveSource(s.conf.Source, s.matches, s.query)
runCtx, runCtxCancel = context.WithCancel(context.Background()) runCtx, runCtxCancel = context.WithCancel(context.Background())
go func() { go func() {
runErr <- s.instance.Run(defs.StaticSourceRunParams{ runErr <- s.instance.Run(defs.StaticSourceRunParams{
Context: runCtx, Context: runCtx,
ResolvedSource: resolvedSource, Conf: s.conf,
Conf: s.conf, ReloadConf: runReloadConf,
ReloadConf: runReloadConf,
}) })
}() }()
} }

View file

@ -0,0 +1,63 @@
// Package main contains an utility to get the server version
package main
import (
"bytes"
"fmt"
"html/template"
"log"
"os"
"os/exec"
)
var tpl = template.Must(template.New("").Parse(
`// autogenerated:yes
package core
const version = "{{ .Version }}"
`))
func do() error {
log.Println("getting version...")
temp, _ := exec.Command("git", "status").CombinedOutput()
/*if err != nil {
return err
}*/
fmt.Println(string(temp))
stdout, err := exec.Command("git", "describe", "--tags").Output()
fmt.Println(string(stdout))
if err != nil {
return err
}
version := string(stdout[:len(stdout)-1])
var buf bytes.Buffer
err = tpl.Execute(&buf, map[string]interface{}{
"Version": version,
})
if err != nil {
return err
}
err = os.WriteFile("version.go", buf.Bytes(), 0o644)
if err != nil {
return err
}
log.Println("ok")
return nil
}
func main() {
err := do()
if err != nil {
log.Printf("ERR: %v", err)
os.Exit(1)
}
}

View file

@ -23,8 +23,7 @@ type StaticSourceParent interface {
// StaticSourceRunParams is the set of params passed to Run(). // StaticSourceRunParams is the set of params passed to Run().
type StaticSourceRunParams struct { type StaticSourceRunParams struct {
Context context.Context Context context.Context
ResolvedSource string Conf *conf.Path
Conf *conf.Path ReloadConf chan *conf.Path
ReloadConf chan *conf.Path
} }

View file

@ -20,7 +20,7 @@ func newGeneric(
generateRTPPackets bool, generateRTPPackets bool,
) (*formatProcessorGeneric, error) { ) (*formatProcessorGeneric, error) {
if generateRTPPackets { if generateRTPPackets {
return nil, fmt.Errorf("we don't know how to generate RTP packets of format %T", forma) return nil, fmt.Errorf("we don't know how to generate RTP packets of format %+v", forma)
} }
return &formatProcessorGeneric{ return &formatProcessorGeneric{

View file

@ -1,13 +1,10 @@
package hooks package hooks
import ( import (
"bytes"
"encoding/json"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"net/http"
) )
// OnReadyParams are the parameters of OnReady. // OnReadyParams are the parameters of OnReady.
@ -44,16 +41,6 @@ func OnReady(params OnReadyParams) func() {
}) })
} }
if params.Conf.HTTPOnReady != "" {
obj := map[string]any{
"query": params.Query,
"desc": params.Desc,
"env": params.ExternalCmdEnv,
}
jsonValue, _ := json.Marshal(obj)
http.Post(params.Conf.HTTPOnReady, "application/json", bytes.NewBuffer(jsonValue))
}
return func() { return func() {
if onReadyCmd != nil { if onReadyCmd != nil {
onReadyCmd.Close() onReadyCmd.Close()
@ -69,15 +56,5 @@ func OnReady(params OnReadyParams) func() {
env, env,
nil) nil)
} }
if params.Conf.HTTPOnNotReady != "" {
obj := map[string]any{
"query": params.Query,
"desc": params.Desc,
"env": params.ExternalCmdEnv,
}
jsonValue, _ := json.Marshal(obj)
http.Post(params.Conf.HTTPOnNotReady, "application/json", bytes.NewBuffer(jsonValue))
}
} }
} }

View file

@ -107,15 +107,6 @@ func (m *Metrics) onRequest(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", m.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Origin", m.AllowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// preflight requests
if ctx.Request.Method == http.MethodOptions &&
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
ctx.Writer.WriteHeader(http.StatusNoContent)
return
}
if ctx.Request.URL.Path != "/metrics" || ctx.Request.Method != http.MethodGet { if ctx.Request.URL.Path != "/metrics" || ctx.Request.Method != http.MethodGet {
return return
} }

View file

@ -1,49 +0,0 @@
package metrics
import (
"io"
"net/http"
"testing"
"time"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require"
)
func TestPreflightRequest(t *testing.T) {
api := Metrics{
Address: "localhost:9998",
AllowOrigin: "*",
ReadTimeout: conf.StringDuration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: test.NilLogger,
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
req, err := http.NewRequest(http.MethodOptions, "http://localhost:9998", nil)
require.NoError(t, err)
req.Header.Add("Access-Control-Request-Method", "GET")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusNoContent, res.StatusCode)
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods"))
require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
}

View file

@ -43,10 +43,7 @@ type Server struct {
func (s *Server) Initialize() error { func (s *Server) Initialize() error {
router := gin.New() router := gin.New()
router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(s.middlewareOrigin)
group := router.Group("/", s.middlewareOrigin) group := router.Group("/", s.middlewareOrigin)
group.GET("/list", s.onList) group.GET("/list", s.onList)
group.GET("/get", s.onGet) group.GET("/get", s.onGet)
@ -109,15 +106,6 @@ func (s *Server) safeFindPathConf(name string) (*conf.Path, error) {
func (s *Server) middlewareOrigin(ctx *gin.Context) { func (s *Server) middlewareOrigin(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// preflight requests
if ctx.Request.Method == http.MethodOptions &&
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
ctx.AbortWithStatus(http.StatusNoContent)
return
}
} }
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool { func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {

View file

@ -1,48 +0,0 @@
package playback
import (
"io"
"net/http"
"testing"
"time"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require"
)
func TestPreflightRequest(t *testing.T) {
s := &Server{
Address: "127.0.0.1:9996",
AllowOrigin: "*",
ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: test.NilLogger,
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
req, err := http.NewRequest(http.MethodOptions, "http://localhost:9996", nil)
require.NoError(t, err)
req.Header.Add("Access-Control-Request-Method", "GET")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusNoContent, res.StatusCode)
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods"))
require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
}

View file

@ -83,15 +83,6 @@ func (pp *PPROF) onRequest(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", pp.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Origin", pp.AllowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// preflight requests
if ctx.Request.Method == http.MethodOptions &&
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
ctx.Writer.WriteHeader(http.StatusNoContent)
return
}
user, pass, hasCredentials := ctx.Request.BasicAuth() user, pass, hasCredentials := ctx.Request.BasicAuth()
err := pp.AuthManager.Authenticate(&auth.Request{ err := pp.AuthManager.Authenticate(&auth.Request{

View file

@ -1,48 +0,0 @@
package pprof
import (
"io"
"net/http"
"testing"
"time"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require"
)
func TestPreflightRequest(t *testing.T) {
s := &PPROF{
Address: "127.0.0.1:9999",
AllowOrigin: "*",
ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: test.NilLogger,
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
req, err := http.NewRequest(http.MethodOptions, "http://localhost:9999", nil)
require.NoError(t, err)
req.Header.Add("Access-Control-Request-Method", "GET")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusNoContent, res.StatusCode)
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods"))
require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
}

View file

@ -69,7 +69,7 @@ func FromStream(
} }
sconn.SetWriteDeadline(time.Now().Add(writeTimeout)) sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err = (*w).WriteH265(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
if err != nil { if err != nil {
return err return err
} }
@ -102,7 +102,7 @@ func FromStream(
} }
sconn.SetWriteDeadline(time.Now().Add(writeTimeout)) sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
err = (*w).WriteH264(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU) err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU)
if err != nil { if err != nil {
return err return err
} }

View file

@ -41,7 +41,7 @@ func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, e
}}, }},
} }
r.OnDataH265(track, func(pts int64, _ int64, au [][]byte) error { r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{ (*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{
Base: unit.Base{ Base: unit.Base{
NTP: time.Now(), NTP: time.Now(),
@ -61,7 +61,7 @@ func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, e
}}, }},
} }
r.OnDataH264(track, func(pts int64, _ int64, au [][]byte) error { r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error {
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{ (*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{
Base: unit.Base{ Base: unit.Base{
NTP: time.Now(), NTP: time.Now(),

View file

@ -182,7 +182,7 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
} }
if !hasVideo && !hasAudio { if !hasVideo && !hasAudio {
return nil, nil, nil return nil, nil, fmt.Errorf("metadata doesn't contain any track")
} }
firstReceived := false firstReceived := false
@ -327,9 +327,6 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
} }
if audioTrack == nil { if audioTrack == nil {
if len(msg.Payload) == 0 {
continue
}
switch { switch {
case msg.Codec == message.CodecMPEG4Audio && case msg.Codec == message.CodecMPEG4Audio &&
msg.AACType == message.AudioAACTypeConfig: msg.AACType == message.AudioAACTypeConfig:
@ -523,9 +520,7 @@ func (r *Reader) readTracks() (format.Format, format.Format, error) {
return nil, nil, err return nil, nil, err
} }
if videoTrack != nil || audioTrack != nil { return videoTrack, audioTrack, nil
return videoTrack, audioTrack, nil
}
} }
} }
} }

View file

@ -244,64 +244,6 @@ func TestReadTracks(t *testing.T) {
return buf return buf
}(), }(),
}, },
&message.Audio{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
Rate: message.Rate44100,
Depth: message.Depth16,
IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: func() []byte {
enc, err2 := mpeg4audio.Config{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
}.Marshal()
require.NoError(t, err2)
return enc
}(),
},
},
},
{
"h264 + aac, issue mediamtx/3301 (metadata without tracks)",
&format.H264{
PayloadTyp: 96,
SPS: test.FormatH264.SPS,
PPS: test.FormatH264.PPS,
PacketizationMode: 1,
},
&format.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.Config{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
},
[]message.Message{
&message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 1,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{
Key: "metadatacreator",
Value: "Agora.io SDK",
},
{
Key: "encoder",
Value: "Agora.io Encoder",
},
},
},
},
&message.Video{ &message.Video{
ChunkStreamID: message.VideoChunkStreamID, ChunkStreamID: message.VideoChunkStreamID,
MessageStreamID: 0x1000000, MessageStreamID: 0x1000000,
@ -390,77 +332,6 @@ func TestReadTracks(t *testing.T) {
}, },
}, },
}, },
{
"aac, issue mediamtx/3414 (empty audio payload)",
nil,
&format.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.Config{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
},
[]message.Message{
&message.DataAMF0{
ChunkStreamID: 4,
MessageStreamID: 1,
Payload: []interface{}{
"@setDataFrame",
"onMetaData",
amf0.Object{
{
Key: "videodatarate",
Value: float64(0),
},
{
Key: "videocodecid",
Value: float64(0),
},
{
Key: "audiodatarate",
Value: float64(0),
},
{
Key: "audiocodecid",
Value: float64(message.CodecMPEG4Audio),
},
},
},
},
&message.Audio{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
Rate: message.Rate44100,
Depth: message.Depth16,
IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: nil,
},
&message.Audio{
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
Rate: message.Rate44100,
Depth: message.Depth16,
IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: func() []byte {
enc, err2 := mpeg4audio.Config{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
}.Marshal()
require.NoError(t, err2)
return enc
}(),
},
},
},
{ {
"h265 + aac, obs studio pre 29.1 h265", "h265 + aac, obs studio pre 29.1 h265",
&format.H265{ &format.H265{

View file

@ -5,7 +5,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/liberrors" "github.com/bluenviron/gortsplib/v4/pkg/liberrors"
"github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer" "github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer"
@ -20,50 +19,13 @@ const (
keyFrameInterval = 2 * time.Second keyFrameInterval = 2 * time.Second
) )
const (
mimeTypeMultiopus = "audio/multiopus"
mimeTypeL16 = "audio/L16"
)
var incomingVideoCodecs = []webrtc.RTPCodecParameters{ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
SDPFmtpLine: "profile=1",
},
PayloadType: 96,
},
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1, MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000, ClockRate: 90000,
}, },
PayloadType: 97, PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=3",
},
PayloadType: 98,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=2",
},
PayloadType: 99,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=1",
},
PayloadType: 100,
}, },
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
@ -71,21 +33,22 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
ClockRate: 90000, ClockRate: 90000,
SDPFmtpLine: "profile-id=0", SDPFmtpLine: "profile-id=0",
}, },
PayloadType: 101, PayloadType: 97,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=1",
},
PayloadType: 98,
}, },
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8, MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000, ClockRate: 90000,
}, },
PayloadType: 102, PayloadType: 99,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
},
PayloadType: 103,
}, },
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
@ -93,7 +56,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
ClockRate: 90000, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
}, },
PayloadType: 104, PayloadType: 100,
}, },
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
@ -101,65 +64,11 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
ClockRate: 90000, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
}, },
PayloadType: 105, PayloadType: 101,
}, },
} }
var incomingAudioCodecs = []webrtc.RTPCodecParameters{ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeMultiopus,
ClockRate: 48000,
Channels: 3,
SDPFmtpLine: "channel_mapping=0,2,1;num_streams=2;coupled_streams=1",
},
PayloadType: 112,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeMultiopus,
ClockRate: 48000,
Channels: 4,
SDPFmtpLine: "channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2",
},
PayloadType: 113,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeMultiopus,
ClockRate: 48000,
Channels: 5,
SDPFmtpLine: "channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2",
},
PayloadType: 114,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeMultiopus,
ClockRate: 48000,
Channels: 6,
SDPFmtpLine: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2",
},
PayloadType: 115,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeMultiopus,
ClockRate: 48000,
Channels: 7,
SDPFmtpLine: "channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4",
},
PayloadType: 116,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeMultiopus,
ClockRate: 48000,
Channels: 8,
SDPFmtpLine: "channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4",
},
PayloadType: 117,
},
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus, MimeType: webrtc.MimeTypeOpus,
@ -176,22 +85,6 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
}, },
PayloadType: 9, PayloadType: 9,
}, },
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
Channels: 2,
},
PayloadType: 118,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA,
ClockRate: 8000,
Channels: 2,
},
PayloadType: 119,
},
{ {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU, MimeType: webrtc.MimeTypePCMU,
@ -206,30 +99,6 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
}, },
PayloadType: 8, PayloadType: 8,
}, },
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeL16,
ClockRate: 8000,
Channels: 2,
},
PayloadType: 120,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeL16,
ClockRate: 16000,
Channels: 2,
},
PayloadType: 121,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeL16,
ClockRate: 48000,
Channels: 2,
},
PayloadType: 122,
},
} }
// IncomingTrack is an incoming track. // IncomingTrack is an incoming track.
@ -237,7 +106,6 @@ type IncomingTrack struct {
track *webrtc.TrackRemote track *webrtc.TrackRemote
log logger.Writer log logger.Writer
typ description.MediaType
format format.Format format format.Format
reorderer *rtpreorderer.Reorderer reorderer *rtpreorderer.Reorderer
pkts []*rtp.Packet pkts []*rtp.Packet
@ -255,47 +123,35 @@ func newIncomingTrack(
reorderer: rtpreorderer.New(), reorderer: rtpreorderer.New(),
} }
isVideo := false
switch strings.ToLower(track.Codec().MimeType) { switch strings.ToLower(track.Codec().MimeType) {
case strings.ToLower(webrtc.MimeTypeAV1): case strings.ToLower(webrtc.MimeTypeAV1):
t.typ = description.MediaTypeVideo isVideo = true
t.format = &format.AV1{ t.format = &format.AV1{
PayloadTyp: uint8(track.PayloadType()), PayloadTyp: uint8(track.PayloadType()),
} }
case strings.ToLower(webrtc.MimeTypeVP9): case strings.ToLower(webrtc.MimeTypeVP9):
t.typ = description.MediaTypeVideo isVideo = true
t.format = &format.VP9{ t.format = &format.VP9{
PayloadTyp: uint8(track.PayloadType()), PayloadTyp: uint8(track.PayloadType()),
} }
case strings.ToLower(webrtc.MimeTypeVP8): case strings.ToLower(webrtc.MimeTypeVP8):
t.typ = description.MediaTypeVideo isVideo = true
t.format = &format.VP8{ t.format = &format.VP8{
PayloadTyp: uint8(track.PayloadType()), PayloadTyp: uint8(track.PayloadType()),
} }
case strings.ToLower(webrtc.MimeTypeH265):
t.typ = description.MediaTypeVideo
t.format = &format.H265{
PayloadTyp: uint8(track.PayloadType()),
}
case strings.ToLower(webrtc.MimeTypeH264): case strings.ToLower(webrtc.MimeTypeH264):
t.typ = description.MediaTypeVideo isVideo = true
t.format = &format.H264{ t.format = &format.H264{
PayloadTyp: uint8(track.PayloadType()), PayloadTyp: uint8(track.PayloadType()),
PacketizationMode: 1, PacketizationMode: 1,
} }
case strings.ToLower(mimeTypeMultiopus):
t.typ = description.MediaTypeAudio
t.format = &format.Opus{
PayloadTyp: uint8(track.PayloadType()),
ChannelCount: int(track.Codec().Channels),
}
case strings.ToLower(webrtc.MimeTypeOpus): case strings.ToLower(webrtc.MimeTypeOpus):
t.typ = description.MediaTypeAudio
t.format = &format.Opus{ t.format = &format.Opus{
PayloadTyp: uint8(track.PayloadType()), PayloadTyp: uint8(track.PayloadType()),
ChannelCount: func() int { ChannelCount: func() int {
@ -307,60 +163,26 @@ func newIncomingTrack(
} }
case strings.ToLower(webrtc.MimeTypeG722): case strings.ToLower(webrtc.MimeTypeG722):
t.typ = description.MediaTypeAudio
t.format = &format.G722{} t.format = &format.G722{}
case strings.ToLower(webrtc.MimeTypePCMU): case strings.ToLower(webrtc.MimeTypePCMU):
t.typ = description.MediaTypeAudio
channels := track.Codec().Channels
if channels == 0 {
channels = 1
}
payloadType := uint8(0)
if channels > 1 {
payloadType = 118
}
t.format = &format.G711{ t.format = &format.G711{
PayloadTyp: payloadType, PayloadTyp: 0,
MULaw: true, MULaw: true,
SampleRate: 8000, SampleRate: 8000,
ChannelCount: int(channels), ChannelCount: 1,
} }
case strings.ToLower(webrtc.MimeTypePCMA): case strings.ToLower(webrtc.MimeTypePCMA):
t.typ = description.MediaTypeAudio
channels := track.Codec().Channels
if channels == 0 {
channels = 1
}
payloadType := uint8(8)
if channels > 1 {
payloadType = 119
}
t.format = &format.G711{ t.format = &format.G711{
PayloadTyp: payloadType, PayloadTyp: 8,
MULaw: false, MULaw: false,
SampleRate: 8000, SampleRate: 8000,
ChannelCount: int(channels), ChannelCount: 1,
}
case strings.ToLower(mimeTypeL16):
t.typ = description.MediaTypeAudio
t.format = &format.LPCM{
PayloadTyp: uint8(track.PayloadType()),
BitDepth: 16,
SampleRate: int(track.Codec().ClockRate),
ChannelCount: int(track.Codec().Channels),
} }
default: default:
return nil, fmt.Errorf("unsupported codec: %+v", track.Codec().RTPCodecCapability) return nil, fmt.Errorf("unsupported codec: %v", track.Codec())
} }
// read incoming RTCP packets to make interceptors work // read incoming RTCP packets to make interceptors work
@ -375,7 +197,7 @@ func newIncomingTrack(
}() }()
// send period key frame requests // send period key frame requests
if t.typ == description.MediaTypeVideo { if isVideo {
go func() { go func() {
keyframeTicker := time.NewTicker(keyFrameInterval) keyframeTicker := time.NewTicker(keyFrameInterval)
defer keyframeTicker.Stop() defer keyframeTicker.Stop()

View file

@ -8,15 +8,6 @@ import (
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
) )
var multichannelOpusSDP = map[int]string{
3: "channel_mapping=0,2,1;num_streams=2;coupled_streams=1",
4: "channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2",
5: "channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2",
6: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2",
7: "channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4",
8: "channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4",
}
// OutgoingTrack is a WebRTC outgoing track // OutgoingTrack is a WebRTC outgoing track
type OutgoingTrack struct { type OutgoingTrack struct {
Format format.Format Format format.Format
@ -40,9 +31,9 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9, MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000, ClockRate: 90000,
SDPFmtpLine: "profile-id=0", SDPFmtpLine: "profile-id=1",
}, },
PayloadType: 96, PayloadType: 98,
}, nil }, nil
case *format.VP8: case *format.VP8:
@ -51,16 +42,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
MimeType: webrtc.MimeTypeVP8, MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000, ClockRate: 90000,
}, },
PayloadType: 96, PayloadType: 99,
}, nil
case *format.H265:
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
},
PayloadType: 96,
}, nil }, nil
case *format.H264: case *format.H264:
@ -70,42 +52,18 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
ClockRate: 90000, ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
}, },
PayloadType: 96, PayloadType: 101,
}, nil }, nil
case *format.Opus: case *format.Opus:
switch forma.ChannelCount { return webrtc.RTPCodecParameters{
case 1, 2: RTPCodecCapability: webrtc.RTPCodecCapability{
return webrtc.RTPCodecParameters{ MimeType: webrtc.MimeTypeOpus,
RTPCodecCapability: webrtc.RTPCodecCapability{ ClockRate: 48000,
MimeType: webrtc.MimeTypeOpus, Channels: 2,
ClockRate: 48000, },
Channels: 2, PayloadType: 111,
SDPFmtpLine: func() string { }, nil
s := "minptime=10;useinbandfec=1"
if forma.ChannelCount == 2 {
s += ";stereo=1;sprop-stereo=1"
}
return s
}(),
},
PayloadType: 96,
}, nil
case 3, 4, 5, 6, 7, 8:
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeMultiopus,
ClockRate: 48000,
Channels: uint16(forma.ChannelCount),
SDPFmtpLine: multichannelOpusSDP[forma.ChannelCount],
},
PayloadType: 96,
}, nil
default:
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount)
}
case *format.G722: case *format.G722:
return webrtc.RTPCodecParameters{ return webrtc.RTPCodecParameters{
@ -117,91 +75,22 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
}, nil }, nil
case *format.G711: case *format.G711:
// These are the sample rates and channels supported by Chrome. if forma.MULaw {
// Different sample rates and channels can be streamed too but we don't want compatibility issues.
// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23
if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 &&
forma.ClockRate() != 32000 && forma.ClockRate() != 48000 {
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported clock rate: %d", forma.ClockRate())
}
if forma.ChannelCount != 1 && forma.ChannelCount != 2 {
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount)
}
if forma.SampleRate == 8000 {
if forma.MULaw {
if forma.ChannelCount != 1 {
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: uint32(forma.SampleRate),
Channels: uint16(forma.ChannelCount),
},
PayloadType: 96,
}, nil
}
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
},
PayloadType: 0,
}, nil
}
if forma.ChannelCount != 1 {
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA,
ClockRate: uint32(forma.SampleRate),
Channels: uint16(forma.ChannelCount),
},
PayloadType: 96,
}, nil
}
return webrtc.RTPCodecParameters{ return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA, MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000, ClockRate: 8000,
}, },
PayloadType: 8, PayloadType: 0,
}, nil }, nil
} }
return webrtc.RTPCodecParameters{ return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{ RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeL16, MimeType: webrtc.MimeTypePCMA,
ClockRate: uint32(forma.ClockRate()), ClockRate: 8000,
Channels: uint16(forma.ChannelCount),
}, },
PayloadType: 96, PayloadType: 8,
}, nil
case *format.LPCM:
if forma.BitDepth != 16 {
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported LPCM bit depth: %d", forma.BitDepth)
}
// These are the sample rates and channels supported by Chrome.
// Different sample rates and channels can be streamed too but we don't want compatibility issues.
// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23
if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 &&
forma.ClockRate() != 32000 && forma.ClockRate() != 48000 {
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported clock rate: %d", forma.ClockRate())
}
if forma.ChannelCount != 1 && forma.ChannelCount != 2 {
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount)
}
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeL16,
ClockRate: uint32(forma.ClockRate()),
Channels: uint16(forma.ChannelCount),
},
PayloadType: 96,
}, nil }, nil
default: default:
@ -214,7 +103,6 @@ func (t *OutgoingTrack) isVideo() bool {
case *format.AV1, case *format.AV1,
*format.VP9, *format.VP9,
*format.VP8, *format.VP8,
*format.H265,
*format.H264: *format.H264:
return true return true
} }

View file

@ -2,7 +2,6 @@ package webrtc
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"sync" "sync"
@ -10,15 +9,15 @@ import (
"github.com/pion/ice/v2" "github.com/pion/ice/v2"
"github.com/pion/interceptor" "github.com/pion/interceptor"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
const ( const (
webrtcStreamID = "mediamtx" webrtcHandshakeTimeout = 10 * time.Second
webrtcTrackGatherTimeout = 2 * time.Second
webrtcStreamID = "mediamtx"
) )
func stringInSlice(a string, list []string) bool { func stringInSlice(a string, list []string) bool {
@ -30,37 +29,6 @@ func stringInSlice(a string, list []string) bool {
return false return false
} }
// TracksAreValid checks whether tracks in the SDP are valid
func TracksAreValid(medias []*sdp.MediaDescription) error {
videoTrack := false
audioTrack := false
for _, media := range medias {
switch media.MediaName.Media {
case "video":
if videoTrack {
return fmt.Errorf("only a single video and a single audio track are supported")
}
videoTrack = true
case "audio":
if audioTrack {
return fmt.Errorf("only a single video and a single audio track are supported")
}
audioTrack = true
default:
return fmt.Errorf("unsupported media '%s'", media.MediaName.Media)
}
}
if !videoTrack && !audioTrack {
return fmt.Errorf("no valid tracks count")
}
return nil
}
type trackRecvPair struct { type trackRecvPair struct {
track *webrtc.TrackRemote track *webrtc.TrackRemote
receiver *webrtc.RTPReceiver receiver *webrtc.RTPReceiver
@ -71,8 +39,6 @@ type PeerConnection struct {
ICEServers []webrtc.ICEServer ICEServers []webrtc.ICEServer
ICEUDPMux ice.UDPMux ICEUDPMux ice.UDPMux
ICETCPMux ice.TCPMux ICETCPMux ice.TCPMux
HandshakeTimeout conf.StringDuration
TrackGatherTimeout conf.StringDuration
LocalRandomUDP bool LocalRandomUDP bool
IPsFromInterfaces bool IPsFromInterfaces bool
IPsFromInterfacesList []string IPsFromInterfacesList []string
@ -128,9 +94,6 @@ func (co *PeerConnection) Start() error {
mediaEngine := &webrtc.MediaEngine{} mediaEngine := &webrtc.MediaEngine{}
if co.Publish { if co.Publish {
videoSetupped := false
audioSetupped := false
for _, tr := range co.OutgoingTracks { for _, tr := range co.OutgoingTracks {
params, err := tr.codecParameters() params, err := tr.codecParameters()
if err != nil { if err != nil {
@ -140,10 +103,8 @@ func (co *PeerConnection) Start() error {
var codecType webrtc.RTPCodecType var codecType webrtc.RTPCodecType
if tr.isVideo() { if tr.isVideo() {
codecType = webrtc.RTPCodecTypeVideo codecType = webrtc.RTPCodecTypeVideo
videoSetupped = true
} else { } else {
codecType = webrtc.RTPCodecTypeAudio codecType = webrtc.RTPCodecTypeAudio
audioSetupped = true
} }
err = mediaEngine.RegisterCodec(params, codecType) err = mediaEngine.RegisterCodec(params, codecType)
@ -151,33 +112,6 @@ func (co *PeerConnection) Start() error {
return err return err
} }
} }
// always register at least a video and a audio codec
// otherwise handshake will fail or audio will be muted on some clients (like Firefox)
if !videoSetupped {
err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
PayloadType: 96,
}, webrtc.RTPCodecTypeVideo)
if err != nil {
return err
}
}
if !audioSetupped {
err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
},
PayloadType: 0,
}, webrtc.RTPCodecTypeAudio)
if err != nil {
return err
}
}
} else { } else {
for _, codec := range incomingVideoCodecs { for _, codec := range incomingVideoCodecs {
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo) err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)
@ -326,8 +260,8 @@ func (co *PeerConnection) SetAnswer(answer *webrtc.SessionDescription) error {
} }
// AddRemoteCandidate adds a remote candidate. // AddRemoteCandidate adds a remote candidate.
func (co *PeerConnection) AddRemoteCandidate(candidate *webrtc.ICECandidateInit) error { func (co *PeerConnection) AddRemoteCandidate(candidate webrtc.ICECandidateInit) error {
return co.wr.AddICECandidate(*candidate) return co.wr.AddICECandidate(candidate)
} }
// CreateFullAnswer creates a full answer. // CreateFullAnswer creates a full answer.
@ -342,8 +276,8 @@ func (co *PeerConnection) CreateFullAnswer(
answer, err := co.wr.CreateAnswer(nil) answer, err := co.wr.CreateAnswer(nil)
if err != nil { if err != nil {
if errors.Is(err, webrtc.ErrSenderWithNoCodecs) { if err.Error() == "unable to populate media section, RTPSender created with no codecs" {
return nil, fmt.Errorf("codecs not supported by client") return nil, fmt.Errorf("track codecs are not supported by remote")
} }
return nil, err return nil, err
} }
@ -353,7 +287,7 @@ func (co *PeerConnection) CreateFullAnswer(
return nil, err return nil, err
} }
err = co.waitGatheringDone(ctx) err = co.WaitGatheringDone(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -361,7 +295,8 @@ func (co *PeerConnection) CreateFullAnswer(
return co.wr.LocalDescription(), nil return co.wr.LocalDescription(), nil
} }
func (co *PeerConnection) waitGatheringDone(ctx context.Context) error { // WaitGatheringDone waits until candidate gathering is complete.
func (co *PeerConnection) WaitGatheringDone(ctx context.Context) error {
for { for {
select { select {
case <-co.NewLocalCandidate(): case <-co.NewLocalCandidate():
@ -377,7 +312,7 @@ func (co *PeerConnection) waitGatheringDone(ctx context.Context) error {
func (co *PeerConnection) WaitUntilConnected( func (co *PeerConnection) WaitUntilConnected(
ctx context.Context, ctx context.Context,
) error { ) error {
t := time.NewTimer(time.Duration(co.HandshakeTimeout)) t := time.NewTimer(webrtcHandshakeTimeout)
defer t.Stop() defer t.Stop()
outer: outer:
@ -398,21 +333,19 @@ outer:
} }
// GatherIncomingTracks gathers incoming tracks. // GatherIncomingTracks gathers incoming tracks.
func (co *PeerConnection) GatherIncomingTracks(ctx context.Context) ([]*IncomingTrack, error) { func (co *PeerConnection) GatherIncomingTracks(
var sdp sdp.SessionDescription ctx context.Context,
sdp.Unmarshal([]byte(co.wr.RemoteDescription().SDP)) //nolint:errcheck maxCount int,
) ([]*IncomingTrack, error) {
maxTrackCount := len(sdp.MediaDescriptions)
var tracks []*IncomingTrack var tracks []*IncomingTrack
t := time.NewTimer(time.Duration(co.TrackGatherTimeout)) t := time.NewTimer(webrtcTrackGatherTimeout)
defer t.Stop() defer t.Stop()
for { for {
select { select {
case <-t.C: case <-t.C:
if len(tracks) != 0 { if maxCount == 0 && len(tracks) != 0 {
return tracks, nil return tracks, nil
} }
return nil, fmt.Errorf("deadline exceeded while waiting tracks") return nil, fmt.Errorf("deadline exceeded while waiting tracks")
@ -424,7 +357,7 @@ func (co *PeerConnection) GatherIncomingTracks(ctx context.Context) ([]*Incoming
} }
tracks = append(tracks, track) tracks = append(tracks, track)
if len(tracks) >= maxTrackCount { if len(tracks) == maxCount || len(tracks) >= 2 {
return tracks, nil return tracks, nil
} }

View file

@ -1,31 +1,22 @@
package webrtc package webrtc
import ( import (
"context"
"testing" "testing"
"time" "time"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test" "github.com/bluenviron/mediamtx/internal/test"
"github.com/pion/rtp"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestPeerConnectionCloseImmediately(t *testing.T) { func TestPeerConnectionCloseAfterError(t *testing.T) {
pc := &PeerConnection{ pc := &PeerConnection{
HandshakeTimeout: conf.StringDuration(10 * time.Second), LocalRandomUDP: true,
TrackGatherTimeout: conf.StringDuration(2 * time.Second), IPsFromInterfaces: true,
LocalRandomUDP: true, Publish: false,
IPsFromInterfaces: true, Log: test.NilLogger,
Publish: false,
Log: test.NilLogger,
} }
err := pc.Start() err := pc.Start()
require.NoError(t, err) require.NoError(t, err)
defer pc.Close()
_, err = pc.CreatePartialOffer() _, err = pc.CreatePartialOffer()
require.NoError(t, err) require.NoError(t, err)
@ -35,454 +26,3 @@ func TestPeerConnectionCloseImmediately(t *testing.T) {
pc.Close() pc.Close()
} }
func TestPeerConnectionPublishRead(t *testing.T) {
for _, ca := range []struct {
name string
in format.Format
webrtcOut webrtc.RTPCodecCapability
out format.Format
}{
{
"av1",
&format.AV1{
PayloadTyp: 96,
},
webrtc.RTPCodecCapability{
MimeType: "video/AV1",
ClockRate: 90000,
},
&format.AV1{
PayloadTyp: 96,
},
},
{
"vp9",
&format.VP9{
PayloadTyp: 96,
},
webrtc.RTPCodecCapability{
MimeType: "video/VP9",
ClockRate: 90000,
SDPFmtpLine: "profile-id=0",
},
&format.VP9{
PayloadTyp: 96,
},
},
{
"vp8",
&format.VP8{
PayloadTyp: 96,
},
webrtc.RTPCodecCapability{
MimeType: "video/VP8",
ClockRate: 90000,
},
&format.VP8{
PayloadTyp: 96,
},
},
{
"h265",
test.FormatH265,
webrtc.RTPCodecCapability{
MimeType: "video/H265",
ClockRate: 90000,
},
&format.H265{
PayloadTyp: 96,
},
},
{
"h264",
test.FormatH264,
webrtc.RTPCodecCapability{
MimeType: "video/H264",
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
},
&format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
},
},
{
"opus multichannel",
&format.Opus{
PayloadTyp: 112,
ChannelCount: 6,
},
webrtc.RTPCodecCapability{
MimeType: "audio/multiopus",
ClockRate: 48000,
Channels: 6,
SDPFmtpLine: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2",
},
&format.Opus{
PayloadTyp: 96,
ChannelCount: 6,
},
},
{
"opus stereo",
&format.Opus{
PayloadTyp: 111,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/opus",
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
},
&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
},
},
{
"opus mono",
&format.Opus{
PayloadTyp: 111,
ChannelCount: 1,
},
webrtc.RTPCodecCapability{
MimeType: "audio/opus",
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
},
&format.Opus{
PayloadTyp: 96,
ChannelCount: 1,
},
},
{
"g722",
&format.G722{},
webrtc.RTPCodecCapability{
MimeType: "audio/G722",
ClockRate: 8000,
},
&format.G722{},
},
{
"g711 pcma 8khz mono",
&format.G711{
PayloadTyp: 8,
SampleRate: 8000,
ChannelCount: 1,
},
webrtc.RTPCodecCapability{
MimeType: "audio/PCMA",
ClockRate: 8000,
},
&format.G711{
PayloadTyp: 8,
SampleRate: 8000,
ChannelCount: 1,
},
},
{
"g711 pcmu 8khz mono",
&format.G711{
MULaw: true,
PayloadTyp: 0,
SampleRate: 8000,
ChannelCount: 1,
},
webrtc.RTPCodecCapability{
MimeType: "audio/PCMU",
ClockRate: 8000,
},
&format.G711{
MULaw: true,
PayloadTyp: 0,
SampleRate: 8000,
ChannelCount: 1,
},
},
{
"g711 pcma 8khz stereo",
&format.G711{
PayloadTyp: 96,
SampleRate: 8000,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/PCMA",
ClockRate: 8000,
Channels: 2,
},
&format.G711{
PayloadTyp: 119,
SampleRate: 8000,
ChannelCount: 2,
},
},
{
"g711 pcmu 8khz stereo",
&format.G711{
MULaw: true,
PayloadTyp: 96,
SampleRate: 8000,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/PCMU",
ClockRate: 8000,
Channels: 2,
},
&format.G711{
MULaw: true,
PayloadTyp: 118,
SampleRate: 8000,
ChannelCount: 2,
},
},
{
"g711 pcma 16khz stereo",
&format.G711{
PayloadTyp: 96,
SampleRate: 16000,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/L16",
ClockRate: 16000,
Channels: 2,
},
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 16000,
ChannelCount: 2,
},
},
{
"g711 pcmu 16khz stereo",
&format.G711{
MULaw: true,
PayloadTyp: 96,
SampleRate: 16000,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/L16",
ClockRate: 16000,
Channels: 2,
},
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 16000,
ChannelCount: 2,
},
},
{
"l16 8khz stereo",
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 8000,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/L16",
ClockRate: 8000,
Channels: 2,
},
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 8000,
ChannelCount: 2,
},
},
{
"l16 16khz stereo",
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 16000,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/L16",
ClockRate: 16000,
Channels: 2,
},
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 16000,
ChannelCount: 2,
},
},
{
"l16 48khz stereo",
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 48000,
ChannelCount: 2,
},
webrtc.RTPCodecCapability{
MimeType: "audio/L16",
ClockRate: 48000,
Channels: 2,
},
&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 48000,
ChannelCount: 2,
},
},
} {
t.Run(ca.name, func(t *testing.T) {
pc1 := &PeerConnection{
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: true,
OutgoingTracks: []*OutgoingTrack{{
Format: ca.in,
}},
Log: test.NilLogger,
}
err := pc1.Start()
require.NoError(t, err)
defer pc1.Close()
pc2 := &PeerConnection{
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err = pc2.Start()
require.NoError(t, err)
defer pc2.Close()
offer, err := pc1.CreatePartialOffer()
require.NoError(t, err)
answer, err := pc2.CreateFullAnswer(context.Background(), offer)
require.NoError(t, err)
err = pc1.SetAnswer(answer)
require.NoError(t, err)
go func() {
for {
select {
case cnd := <-pc1.NewLocalCandidate():
err2 := pc2.AddRemoteCandidate(cnd)
require.NoError(t, err2)
case <-pc1.Connected():
return
}
}
}()
err = pc1.WaitUntilConnected(context.Background())
require.NoError(t, err)
err = pc2.WaitUntilConnected(context.Background())
require.NoError(t, err)
err = pc1.OutgoingTracks[0].WriteRTP(&rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 111,
SequenceNumber: 1123,
Timestamp: 45343,
SSRC: 563424,
},
Payload: []byte{5, 2},
})
require.NoError(t, err)
inc, err := pc2.GatherIncomingTracks(context.Background())
require.NoError(t, err)
exp := ca.webrtcOut
exp.RTCPFeedback = inc[0].track.Codec().RTPCodecCapability.RTCPFeedback
require.Equal(t, exp, inc[0].track.Codec().RTPCodecCapability)
require.Equal(t, ca.out, inc[0].Format())
})
}
}
// test that an audio codec is present regardless of the fact that an audio track is not.
func TestPeerConnectionFallbackCodecs(t *testing.T) {
pc1 := &PeerConnection{
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err := pc1.Start()
require.NoError(t, err)
defer pc1.Close()
pc2 := &PeerConnection{
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: true,
OutgoingTracks: []*OutgoingTrack{{
Format: &format.AV1{
PayloadTyp: 96,
},
}},
Log: test.NilLogger,
}
err = pc2.Start()
require.NoError(t, err)
defer pc2.Close()
offer, err := pc1.CreatePartialOffer()
require.NoError(t, err)
answer, err := pc2.CreateFullAnswer(context.Background(), offer)
require.NoError(t, err)
var s sdp.SessionDescription
err = s.Unmarshal([]byte(answer.SDP))
require.NoError(t, err)
require.Equal(t, []*sdp.MediaDescription{
{
MediaName: sdp.MediaName{
Media: "video",
Port: sdp.RangedPort{Value: 9},
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
Formats: []string{"97"},
},
ConnectionInformation: s.MediaDescriptions[0].ConnectionInformation,
Attributes: s.MediaDescriptions[0].Attributes,
},
{
MediaName: sdp.MediaName{
Media: "audio",
Port: sdp.RangedPort{Value: 9},
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
Formats: []string{"0"},
},
ConnectionInformation: s.MediaDescriptions[1].ConnectionInformation,
Attributes: s.MediaDescriptions[1].Attributes,
},
}, s.MediaDescriptions)
}

View file

@ -0,0 +1,37 @@
package webrtc
import (
"fmt"
"github.com/pion/sdp/v3"
)
// TrackCount returns the track count.
func TrackCount(medias []*sdp.MediaDescription) (int, error) {
videoTrack := false
audioTrack := false
trackCount := 0
for _, media := range medias {
switch media.MediaName.Media {
case "video":
if videoTrack {
return 0, fmt.Errorf("only a single video and a single audio track are supported")
}
videoTrack = true
case "audio":
if audioTrack {
return 0, fmt.Errorf("only a single video and a single audio track are supported")
}
audioTrack = true
default:
return 0, fmt.Errorf("unsupported media '%s'", media.MediaName.Media)
}
trackCount++
}
return trackCount, nil
}

View file

@ -10,9 +10,21 @@ func TracksToMedias(tracks []*IncomingTrack) []*description.Media {
ret := make([]*description.Media, len(tracks)) ret := make([]*description.Media, len(tracks))
for i, track := range tracks { for i, track := range tracks {
forma := track.Format()
var mediaType description.MediaType
switch forma.(type) {
case *format.AV1, *format.VP9, *format.VP8, *format.H264:
mediaType = description.MediaTypeVideo
default:
mediaType = description.MediaTypeAudio
}
ret[i] = &description.Media{ ret[i] = &description.Media{
Type: track.typ, Type: mediaType,
Formats: []format.Format{track.format}, Formats: []format.Format{forma},
} }
} }

View file

@ -13,16 +13,10 @@ import (
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpp" "github.com/bluenviron/mediamtx/internal/protocols/httpp"
) )
const (
webrtcHandshakeTimeout = 10 * time.Second
webrtcTrackGatherTimeout = 2 * time.Second
)
// WHIPClient is a WHIP client. // WHIPClient is a WHIP client.
type WHIPClient struct { type WHIPClient struct {
HTTPClient *http.Client HTTPClient *http.Client
@ -54,14 +48,12 @@ func (c *WHIPClient) Publish(
} }
c.pc = &PeerConnection{ c.pc = &PeerConnection{
ICEServers: iceServers, ICEServers: iceServers,
HandshakeTimeout: conf.StringDuration(10 * time.Second), LocalRandomUDP: true,
TrackGatherTimeout: conf.StringDuration(2 * time.Second), IPsFromInterfaces: true,
LocalRandomUDP: true, Publish: true,
IPsFromInterfaces: true, OutgoingTracks: outgoingTracks,
Publish: true, Log: c.Log,
OutgoingTracks: outgoingTracks,
Log: c.Log,
} }
err = c.pc.Start() err = c.pc.Start()
if err != nil { if err != nil {
@ -130,13 +122,11 @@ func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) {
} }
c.pc = &PeerConnection{ c.pc = &PeerConnection{
ICEServers: iceServers, ICEServers: iceServers,
HandshakeTimeout: conf.StringDuration(10 * time.Second), LocalRandomUDP: true,
TrackGatherTimeout: conf.StringDuration(2 * time.Second), IPsFromInterfaces: true,
LocalRandomUDP: true, Publish: false,
IPsFromInterfaces: true, Log: c.Log,
Publish: false,
Log: c.Log,
} }
err = c.pc.Start() err = c.pc.Start()
if err != nil { if err != nil {
@ -169,7 +159,8 @@ func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) {
return nil, err return nil, err
} }
err = TracksAreValid(sdp.MediaDescriptions) // check that there are at most two tracks
_, err = TrackCount(sdp.MediaDescriptions)
if err != nil { if err != nil {
c.deleteSession(context.Background()) //nolint:errcheck c.deleteSession(context.Background()) //nolint:errcheck
c.pc.Close() c.pc.Close()
@ -209,7 +200,7 @@ outer:
} }
} }
tracks, err := c.pc.GatherIncomingTracks(ctx) tracks, err := c.pc.GatherIncomingTracks(ctx, 0)
if err != nil { if err != nil {
c.deleteSession(context.Background()) //nolint:errcheck c.deleteSession(context.Background()) //nolint:errcheck
c.pc.Close() c.pc.Close()

View file

@ -8,12 +8,6 @@ import (
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
) )
// OnSegmentCreateFunc is the prototype of the function passed as OnSegmentCreate
type OnSegmentCreateFunc = func(path string)
// OnSegmentCompleteFunc is the prototype of the function passed as OnSegmentComplete
type OnSegmentCompleteFunc = func(path string, duration time.Duration)
// Agent writes recordings to disk. // Agent writes recordings to disk.
type Agent struct { type Agent struct {
WriteQueueSize int WriteQueueSize int
@ -23,8 +17,8 @@ type Agent struct {
SegmentDuration time.Duration SegmentDuration time.Duration
PathName string PathName string
Stream *stream.Stream Stream *stream.Stream
OnSegmentCreate OnSegmentCreateFunc OnSegmentCreate OnSegmentFunc
OnSegmentComplete OnSegmentCompleteFunc OnSegmentComplete OnSegmentFunc
Parent logger.Writer Parent logger.Writer
restartPause time.Duration restartPause time.Duration
@ -42,7 +36,7 @@ func (w *Agent) Initialize() {
} }
} }
if w.OnSegmentComplete == nil { if w.OnSegmentComplete == nil {
w.OnSegmentComplete = func(string, time.Duration) { w.OnSegmentComplete = func(string) {
} }
} }
if w.restartPause == 0 { if w.restartPause == 0 {

View file

@ -11,6 +11,9 @@ import (
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
// OnSegmentFunc is the prototype of the function passed as runOnSegmentStart / runOnSegmentComplete
type OnSegmentFunc = func(string)
type sample struct { type sample struct {
*fmp4.PartSample *fmp4.PartSample
dts time.Duration dts time.Duration

View file

@ -68,15 +68,12 @@ func TestAgent(t *testing.T) {
}, },
}} }}
writeToStream := func(stream *stream.Stream, startDTS time.Duration, startNTP time.Time) { writeToStream := func(stream *stream.Stream, ntp time.Time) {
for i := 0; i < 2; i++ { for i := 0; i < 3; i++ {
pts := startDTS + time.Duration(i)*100*time.Millisecond
ntp := startNTP.Add(time.Duration(i*60) * time.Second)
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{ stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{ Base: unit.Base{
PTS: pts, PTS: (50 + time.Duration(i)) * time.Second,
NTP: ntp, NTP: ntp.Add(time.Duration(i) * 60 * time.Second),
}, },
AU: [][]byte{ AU: [][]byte{
test.FormatH264.SPS, test.FormatH264.SPS,
@ -87,7 +84,7 @@ func TestAgent(t *testing.T) {
stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H265{ stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H265{
Base: unit.Base{ Base: unit.Base{
PTS: pts, PTS: (50 + time.Duration(i)) * time.Second,
}, },
AU: [][]byte{ AU: [][]byte{
test.FormatH265.VPS, test.FormatH265.VPS,
@ -99,21 +96,21 @@ func TestAgent(t *testing.T) {
stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{ stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{
Base: unit.Base{ Base: unit.Base{
PTS: pts, PTS: (50 + time.Duration(i)) * time.Second,
}, },
AUs: [][]byte{{1, 2, 3, 4}}, AUs: [][]byte{{1, 2, 3, 4}},
}) })
stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{ stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{
Base: unit.Base{ Base: unit.Base{
PTS: pts, PTS: (50 + time.Duration(i)) * time.Second,
}, },
Samples: []byte{1, 2, 3, 4}, Samples: []byte{1, 2, 3, 4},
}) })
stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.LPCM{ stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.LPCM{
Base: unit.Base{ Base: unit.Base{
PTS: pts, PTS: (50 + time.Duration(i)) * time.Second,
}, },
Samples: []byte{1, 2, 3, 4}, Samples: []byte{1, 2, 3, 4},
}) })
@ -147,15 +144,6 @@ func TestAgent(t *testing.T) {
f = conf.RecordFormatMPEGTS f = conf.RecordFormatMPEGTS
} }
var ext string
if ca == "fmp4" {
ext = "mp4"
} else {
ext = "ts"
}
n := 0
w := &Agent{ w := &Agent{
WriteQueueSize: 1024, WriteQueueSize: 1024,
PathFormat: recordPath, PathFormat: recordPath,
@ -164,30 +152,10 @@ func TestAgent(t *testing.T) {
SegmentDuration: 1 * time.Second, SegmentDuration: 1 * time.Second,
PathName: "mypath", PathName: "mypath",
Stream: stream, Stream: stream,
OnSegmentCreate: func(segPath string) { OnSegmentCreate: func(_ string) {
switch n {
case 0:
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext), segPath)
case 1:
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext), segPath)
default:
require.Equal(t, filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext), segPath)
}
segCreated <- struct{}{} segCreated <- struct{}{}
}, },
OnSegmentComplete: func(segPath string, du time.Duration) { OnSegmentComplete: func(_ string) {
switch n {
case 0:
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext), segPath)
require.Equal(t, 2*time.Second, du)
case 1:
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext), segPath)
require.Equal(t, 100*time.Millisecond, du)
default:
require.Equal(t, filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext), segPath)
require.Equal(t, 100*time.Millisecond, du)
}
n++
segDone <- struct{}{} segDone <- struct{}{}
}, },
Parent: test.NilLogger, Parent: test.NilLogger,
@ -195,13 +163,7 @@ func TestAgent(t *testing.T) {
} }
w.Initialize() w.Initialize()
writeToStream(stream, writeToStream(stream, time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC))
50*time.Second,
time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC))
writeToStream(stream,
52*time.Second,
time.Date(2008, 0o5, 20, 22, 16, 25, 0, time.UTC))
// simulate a write error // simulate a write error
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{ stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
@ -218,68 +180,74 @@ func TestAgent(t *testing.T) {
<-segDone <-segDone
} }
var ext string
if ca == "fmp4" { if ca == "fmp4" {
var init fmp4.Init ext = "mp4"
} else {
ext = "ts"
}
if ca == "fmp4" {
func() { func() {
f, err2 := os.Open(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext)) f, err2 := os.Open(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext))
require.NoError(t, err2) require.NoError(t, err2)
defer f.Close() defer f.Close()
var init fmp4.Init
err2 = init.Unmarshal(f) err2 = init.Unmarshal(f)
require.NoError(t, err2) require.NoError(t, err2)
}()
require.Equal(t, fmp4.Init{ require.Equal(t, fmp4.Init{
Tracks: []*fmp4.InitTrack{ Tracks: []*fmp4.InitTrack{
{ {
ID: 1, ID: 1,
TimeScale: 90000, TimeScale: 90000,
Codec: &fmp4.CodecH264{ Codec: &fmp4.CodecH264{
SPS: test.FormatH264.SPS, SPS: test.FormatH264.SPS,
PPS: test.FormatH264.PPS, PPS: test.FormatH264.PPS,
},
}, },
}, {
{ ID: 2,
ID: 2, TimeScale: 90000,
TimeScale: 90000, Codec: &fmp4.CodecH265{
Codec: &fmp4.CodecH265{ VPS: test.FormatH265.VPS,
VPS: test.FormatH265.VPS, SPS: test.FormatH265.SPS,
SPS: test.FormatH265.SPS, PPS: test.FormatH265.PPS,
PPS: test.FormatH265.PPS, },
}, },
}, {
{ ID: 3,
ID: 3, TimeScale: 44100,
TimeScale: 44100, Codec: &fmp4.CodecMPEG4Audio{
Codec: &fmp4.CodecMPEG4Audio{ Config: mpeg4audio.Config{
Config: mpeg4audio.Config{ Type: 2,
Type: 2, SampleRate: 44100,
ChannelCount: 2,
},
},
},
{
ID: 4,
TimeScale: 8000,
Codec: &fmp4.CodecLPCM{
BitDepth: 16,
SampleRate: 8000,
ChannelCount: 1,
},
},
{
ID: 5,
TimeScale: 44100,
Codec: &fmp4.CodecLPCM{
BitDepth: 16,
SampleRate: 44100, SampleRate: 44100,
ChannelCount: 2, ChannelCount: 2,
}, },
}, },
}, },
{ }, init)
ID: 4, }()
TimeScale: 8000,
Codec: &fmp4.CodecLPCM{
BitDepth: 16,
SampleRate: 8000,
ChannelCount: 1,
},
},
{
ID: 5,
TimeScale: 44100,
Codec: &fmp4.CodecLPCM{
BitDepth: 16,
SampleRate: 44100,
ChannelCount: 2,
},
},
},
}, init)
_, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext)) _, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext))
require.NoError(t, err) require.NoError(t, err)
@ -293,19 +261,17 @@ func TestAgent(t *testing.T) {
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
writeToStream(stream, writeToStream(stream, time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC))
300*time.Second,
time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC))
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
w.Close() w.Close()
<-segCreated
<-segDone
_, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext)) _, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext))
require.NoError(t, err) require.NoError(t, err)
_, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-16-25-000000."+ext))
require.NoError(t, err)
}) })
} }
} }

View file

@ -191,7 +191,7 @@ func (f *formatFMP4) initialize() {
return err return err
} }
return track.write(&sample{ return track.record(&sample{
PartSample: sampl, PartSample: sampl,
dts: tunit.PTS, dts: tunit.PTS,
ntp: tunit.NTP, ntp: tunit.NTP,
@ -261,7 +261,7 @@ func (f *formatFMP4) initialize() {
firstReceived = true firstReceived = true
} }
return track.write(&sample{ return track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
IsNonSyncSample: !randomAccess, IsNonSyncSample: !randomAccess,
Payload: tunit.Frame, Payload: tunit.Frame,
@ -364,7 +364,7 @@ func (f *formatFMP4) initialize() {
return err return err
} }
return track.write(&sample{ return track.record(&sample{
PartSample: sampl, PartSample: sampl,
dts: dts, dts: dts,
ntp: tunit.NTP, ntp: tunit.NTP,
@ -435,7 +435,7 @@ func (f *formatFMP4) initialize() {
return err return err
} }
return track.write(&sample{ return track.record(&sample{
PartSample: sampl, PartSample: sampl,
dts: dts, dts: dts,
ntp: tunit.NTP, ntp: tunit.NTP,
@ -494,7 +494,7 @@ func (f *formatFMP4) initialize() {
} }
lastPTS = tunit.PTS lastPTS = tunit.PTS
return track.write(&sample{ return track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: tunit.Frame, Payload: tunit.Frame,
IsNonSyncSample: !randomAccess, IsNonSyncSample: !randomAccess,
@ -547,7 +547,7 @@ func (f *formatFMP4) initialize() {
} }
lastPTS = tunit.PTS lastPTS = tunit.PTS
return track.write(&sample{ return track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: tunit.Frame, Payload: tunit.Frame,
IsNonSyncSample: !randomAccess, IsNonSyncSample: !randomAccess,
@ -583,7 +583,7 @@ func (f *formatFMP4) initialize() {
updateCodecs() updateCodecs()
} }
return track.write(&sample{ return track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: tunit.Frame, Payload: tunit.Frame,
}, },
@ -607,7 +607,7 @@ func (f *formatFMP4) initialize() {
var dt time.Duration var dt time.Duration
for _, packet := range tunit.Packets { for _, packet := range tunit.Packets {
err := track.write(&sample{ err := track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: packet, Payload: packet,
}, },
@ -642,7 +642,7 @@ func (f *formatFMP4) initialize() {
dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit * dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit *
time.Second / sampleRate time.Second / sampleRate
err := track.write(&sample{ err := track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: au, Payload: au,
}, },
@ -688,7 +688,7 @@ func (f *formatFMP4) initialize() {
updateCodecs() updateCodecs()
} }
err = track.write(&sample{ err = track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: frame, Payload: frame,
}, },
@ -756,7 +756,7 @@ func (f *formatFMP4) initialize() {
dt := time.Duration(i) * time.Duration(ac3.SamplesPerFrame) * dt := time.Duration(i) * time.Duration(ac3.SamplesPerFrame) *
time.Second / time.Duration(codec.SampleRate) time.Second / time.Duration(codec.SampleRate)
err = track.write(&sample{ err = track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: frame, Payload: frame,
}, },
@ -796,7 +796,7 @@ func (f *formatFMP4) initialize() {
out = g711.DecodeAlaw(tunit.Samples) out = g711.DecodeAlaw(tunit.Samples)
} }
return track.write(&sample{ return track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: out, Payload: out,
}, },
@ -820,7 +820,7 @@ func (f *formatFMP4) initialize() {
return nil return nil
} }
return track.write(&sample{ return track.record(&sample{
PartSample: &fmp4.PartSample{ PartSample: &fmp4.PartSample{
Payload: tunit.Samples, Payload: tunit.Samples,
}, },
@ -838,12 +838,6 @@ func (f *formatFMP4) initialize() {
func (f *formatFMP4) close() { func (f *formatFMP4) close() {
if f.currentSegment != nil { if f.currentSegment != nil {
for _, track := range f.tracks {
if track.nextSample != nil && track.nextSample.dts > f.currentSegment.lastDTS {
f.currentSegment.lastDTS = track.nextSample.dts
}
}
f.currentSegment.close() //nolint:errcheck f.currentSegment.close() //nolint:errcheck
} }
} }

View file

@ -81,7 +81,7 @@ func (p *formatFMP4Part) close() error {
return writePart(p.s.fi, p.sequenceNumber, p.partTracks) return writePart(p.s.fi, p.sequenceNumber, p.partTracks)
} }
func (p *formatFMP4Part) write(track *formatFMP4Track, sample *sample) error { func (p *formatFMP4Part) record(track *formatFMP4Track, sample *sample) error {
partTrack, ok := p.partTracks[track] partTrack, ok := p.partTracks[track]
if !ok { if !ok {
partTrack = &fmp4.PartTrack{ partTrack = &fmp4.PartTrack{

View file

@ -39,11 +39,9 @@ type formatFMP4Segment struct {
path string path string
fi *os.File fi *os.File
curPart *formatFMP4Part curPart *formatFMP4Part
lastDTS time.Duration
} }
func (s *formatFMP4Segment) initialize() { func (s *formatFMP4Segment) initialize() {
s.lastDTS = s.startDTS
} }
func (s *formatFMP4Segment) close() error { func (s *formatFMP4Segment) close() error {
@ -61,17 +59,14 @@ func (s *formatFMP4Segment) close() error {
} }
if err2 == nil { if err2 == nil {
duration := s.lastDTS - s.startDTS s.f.a.agent.OnSegmentComplete(s.path)
s.f.a.agent.OnSegmentComplete(s.path, duration)
} }
} }
return err return err
} }
func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error { func (s *formatFMP4Segment) record(track *formatFMP4Track, sample *sample) error {
s.lastDTS = sample.dts
if s.curPart == nil { if s.curPart == nil {
s.curPart = &formatFMP4Part{ s.curPart = &formatFMP4Part{
s: s, s: s,
@ -97,5 +92,5 @@ func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error
s.f.nextSequenceNumber++ s.f.nextSequenceNumber++
} }
return s.curPart.write(track, sample) return s.curPart.record(track, sample)
} }

View file

@ -11,7 +11,7 @@ type formatFMP4Track struct {
nextSample *sample nextSample *sample
} }
func (t *formatFMP4Track) write(sample *sample) error { func (t *formatFMP4Track) record(sample *sample) error {
// wait the first video sample before setting hasVideo // wait the first video sample before setting hasVideo
if t.initTrack.Codec.IsVideo() { if t.initTrack.Codec.IsVideo() {
t.f.hasVideo = true t.f.hasVideo = true
@ -35,7 +35,7 @@ func (t *formatFMP4Track) write(sample *sample) error {
return nil return nil
} }
err := t.f.currentSegment.write(t, sample) err := t.f.currentSegment.record(t, sample)
if err != nil { if err != nil {
return err return err
} }
@ -43,7 +43,6 @@ func (t *formatFMP4Track) write(sample *sample) error {
if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) && if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) &&
!t.nextSample.IsNonSyncSample && !t.nextSample.IsNonSyncSample &&
(t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration { (t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration {
t.f.currentSegment.lastDTS = t.nextSample.dts
err := t.f.currentSegment.close() err := t.f.currentSegment.close()
if err != nil { if err != nil {
return err return err

View file

@ -66,7 +66,7 @@ func (f *formatMPEGTS) initialize() {
for _, media := range f.a.agent.Stream.Desc().Medias { for _, media := range f.a.agent.Stream.Desc().Medias {
for _, forma := range media.Formats { for _, forma := range media.Formats {
switch forma := forma.(type) { switch forma := forma.(type) {
case *rtspformat.H265: //nolint:dupl case *rtspformat.H265:
track := addTrack(forma, &mpegts.CodecH265{}) track := addTrack(forma, &mpegts.CodecH265{})
var dtsExtractor *h265.DTSExtractor var dtsExtractor *h265.DTSExtractor
@ -91,18 +91,10 @@ func (f *formatMPEGTS) initialize() {
return err return err
} }
return f.write( return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, randomAccess, tunit.AU)
dts,
tunit.NTP,
true,
randomAccess,
func() error {
return f.mw.WriteH265(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
},
)
}) })
case *rtspformat.H264: //nolint:dupl case *rtspformat.H264:
track := addTrack(forma, &mpegts.CodecH264{}) track := addTrack(forma, &mpegts.CodecH264{})
var dtsExtractor *h264.DTSExtractor var dtsExtractor *h264.DTSExtractor
@ -113,10 +105,10 @@ func (f *formatMPEGTS) initialize() {
return nil return nil
} }
randomAccess := h264.IDRPresent(tunit.AU) idrPresent := h264.IDRPresent(tunit.AU)
if dtsExtractor == nil { if dtsExtractor == nil {
if !randomAccess { if !idrPresent {
return nil return nil
} }
dtsExtractor = h264.NewDTSExtractor() dtsExtractor = h264.NewDTSExtractor()
@ -127,15 +119,7 @@ func (f *formatMPEGTS) initialize() {
return err return err
} }
return f.write( return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, idrPresent, tunit.AU)
dts,
tunit.NTP,
true,
randomAccess,
func() error {
return f.mw.WriteH264(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
},
)
}) })
case *rtspformat.MPEG4Video: case *rtspformat.MPEG4Video:
@ -157,17 +141,15 @@ func (f *formatMPEGTS) initialize() {
} }
lastPTS = tunit.PTS lastPTS = tunit.PTS
f.hasVideo = true
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)}) randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
return f.write( err := f.setupSegment(tunit.PTS, tunit.NTP, true, randomAccess)
tunit.PTS, if err != nil {
tunit.NTP, return err
true, }
randomAccess,
func() error { return f.mw.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
return f.mw.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
},
)
}) })
case *rtspformat.MPEG1Video: case *rtspformat.MPEG1Video:
@ -189,17 +171,15 @@ func (f *formatMPEGTS) initialize() {
} }
lastPTS = tunit.PTS lastPTS = tunit.PTS
f.hasVideo = true
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8}) randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8})
return f.write( err := f.setupSegment(tunit.PTS, tunit.NTP, true, randomAccess)
tunit.PTS, if err != nil {
tunit.NTP, return err
true, }
randomAccess,
func() error { return f.mw.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
return f.mw.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
},
)
}) })
case *rtspformat.Opus: case *rtspformat.Opus:
@ -213,15 +193,12 @@ func (f *formatMPEGTS) initialize() {
return nil return nil
} }
return f.write( err := f.setupSegment(tunit.PTS, tunit.NTP, false, true)
tunit.PTS, if err != nil {
tunit.NTP, return err
false, }
true,
func() error { return f.mw.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets)
return f.mw.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets)
},
)
}) })
case *rtspformat.MPEG4Audio: case *rtspformat.MPEG4Audio:
@ -235,15 +212,12 @@ func (f *formatMPEGTS) initialize() {
return nil return nil
} }
return f.write( err := f.setupSegment(tunit.PTS, tunit.NTP, false, true)
tunit.PTS, if err != nil {
tunit.NTP, return err
false, }
true,
func() error { return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs)
return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs)
},
)
}) })
case *rtspformat.MPEG1Audio: case *rtspformat.MPEG1Audio:
@ -255,15 +229,12 @@ func (f *formatMPEGTS) initialize() {
return nil return nil
} }
return f.write( err := f.setupSegment(tunit.PTS, tunit.NTP, false, true)
tunit.PTS, if err != nil {
tunit.NTP, return err
false, }
true,
func() error { return f.mw.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames)
return f.mw.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames)
},
)
}) })
case *rtspformat.AC3: case *rtspformat.AC3:
@ -277,25 +248,17 @@ func (f *formatMPEGTS) initialize() {
return nil return nil
} }
return f.write( for i, frame := range tunit.Frames {
tunit.PTS, framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame*
tunit.NTP, time.Second/sampleRate
false,
true,
func() error {
for i, frame := range tunit.Frames {
framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame*
time.Second/sampleRate
err := f.mw.WriteAC3(track, durationGoToMPEGTS(framePTS), frame) err := f.mw.WriteAC3(track, durationGoToMPEGTS(framePTS), frame)
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
},
)
}) })
} }
} }
@ -315,17 +278,12 @@ func (f *formatMPEGTS) close() {
} }
} }
func (f *formatMPEGTS) write( func (f *formatMPEGTS) setupSegment(
dts time.Duration, dts time.Duration,
ntp time.Time, ntp time.Time,
isVideo bool, isVideo bool,
randomAccess bool, randomAccess bool,
writeCB func() error,
) error { ) error {
if isVideo {
f.hasVideo = true
}
switch { switch {
case f.currentSegment == nil: case f.currentSegment == nil:
f.currentSegment = &formatMPEGTSSegment{ f.currentSegment = &formatMPEGTSSegment{
@ -337,7 +295,6 @@ func (f *formatMPEGTS) write(
case (!f.hasVideo || isVideo) && case (!f.hasVideo || isVideo) &&
randomAccess && randomAccess &&
(dts-f.currentSegment.startDTS) >= f.a.agent.SegmentDuration: (dts-f.currentSegment.startDTS) >= f.a.agent.SegmentDuration:
f.currentSegment.lastDTS = dts
err := f.currentSegment.close() err := f.currentSegment.close()
if err != nil { if err != nil {
return err return err
@ -359,7 +316,23 @@ func (f *formatMPEGTS) write(
f.currentSegment.lastFlush = dts f.currentSegment.lastFlush = dts
} }
f.currentSegment.lastDTS = dts return nil
}
return writeCB()
func (f *formatMPEGTS) recordH26x(
track *mpegts.Track,
pts time.Duration,
dts time.Duration,
ntp time.Time,
randomAccess bool,
au [][]byte,
) error {
f.hasVideo = true
err := f.setupSegment(dts, ntp, true, randomAccess)
if err != nil {
return err
}
return f.mw.WriteH26x(track, durationGoToMPEGTS(pts), durationGoToMPEGTS(dts), randomAccess, au)
} }

View file

@ -13,15 +13,13 @@ type formatMPEGTSSegment struct {
startDTS time.Duration startDTS time.Duration
startNTP time.Time startNTP time.Time
lastFlush time.Duration
path string path string
fi *os.File fi *os.File
lastFlush time.Duration
lastDTS time.Duration
} }
func (s *formatMPEGTSSegment) initialize() { func (s *formatMPEGTSSegment) initialize() {
s.lastFlush = s.startDTS s.lastFlush = s.startDTS
s.lastDTS = s.startDTS
s.f.dw.setTarget(s) s.f.dw.setTarget(s)
} }
@ -36,8 +34,7 @@ func (s *formatMPEGTSSegment) close() error {
} }
if err2 == nil { if err2 == nil {
duration := s.lastDTS - s.startDTS s.f.a.agent.OnSegmentComplete(s.path)
s.f.a.agent.OnSegmentComplete(s.path, duration)
} }
} }

View file

@ -1 +0,0 @@
c5ef2cf356b103bf7a19dd4d14257c9e00163551ed03bbf96bf22a12458a1250

View file

@ -1 +1 @@
v1.5.13 v1.5.8

View file

@ -2,13 +2,8 @@
package main package main
import ( import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -16,16 +11,15 @@ import (
) )
func do() error { func do() error {
log.Println("downloading hls.js...")
buf, err := os.ReadFile("./hlsjsdownloader/VERSION") buf, err := os.ReadFile("./hlsjsdownloader/VERSION")
if err != nil { if err != nil {
return err return err
} }
version := strings.TrimSpace(string(buf)) version := strings.TrimSpace(string(buf))
log.Printf("downloading hls.js version %s...", version) res, err := http.Get("https://cdn.jsdelivr.net/npm/hls.js@" + version + "/dist/hls.min.js")
res, err := http.Get("https://github.com/video-dev/hls.js/releases/download/" + version + "/release.zip")
if err != nil { if err != nil {
return err return err
} }
@ -35,38 +29,15 @@ func do() error {
return fmt.Errorf("bad status code: %v", res.StatusCode) return fmt.Errorf("bad status code: %v", res.StatusCode)
} }
zipBuf, err := io.ReadAll(res.Body) buf, err = io.ReadAll(res.Body)
if err != nil { if err != nil {
return err return err
} }
hashBuf, err := os.ReadFile("./hlsjsdownloader/HASH") err = os.WriteFile("hls.min.js", buf, 0o644)
if err != nil { if err != nil {
return err return err
} }
hash := make([]byte, hex.DecodedLen(len(hashBuf)))
if _, err = hex.Decode(hash, bytes.TrimSpace(hashBuf)); err != nil {
return err
}
if sum := sha256.Sum256(zipBuf); !bytes.Equal(sum[:], hash) {
return fmt.Errorf("hash mismatch")
}
z, err := zip.NewReader(bytes.NewReader(zipBuf), int64(len(zipBuf)))
if err != nil {
return err
}
hls, err := fs.ReadFile(z, "dist/hls.min.js")
if err != nil {
return err
}
if err = os.WriteFile("hls.min.js", hls, 0o644); err != nil {
return err
}
log.Println("ok") log.Println("ok")
return nil return nil

View file

@ -5,7 +5,6 @@ import (
"errors" "errors"
"net" "net"
"net/http" "net/http"
"net/url"
gopath "path" gopath "path"
"strings" "strings"
"time" "time"
@ -37,17 +36,6 @@ func mergePathAndQuery(path string, rawQuery string) string {
return res return res
} }
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
type httpServer struct { type httpServer struct {
address string address string
encryption bool encryption bool
@ -102,11 +90,9 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
switch ctx.Request.Method { switch ctx.Request.Method {
case http.MethodOptions: case http.MethodOptions:
if ctx.Request.Header.Get("Access-Control-Request-Method") != "" { ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range") ctx.Writer.WriteHeader(http.StatusNoContent)
ctx.Writer.WriteHeader(http.StatusNoContent)
}
return return
case http.MethodGet: case http.MethodGet:
@ -159,15 +145,10 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
user, pass, hasCredentials := ctx.Request.BasicAuth() user, pass, hasCredentials := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
q = addJWTFromAuthorization(q, h)
}
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
Name: dir, Name: dir,
Query: q, Query: ctx.Request.URL.RawQuery,
Publish: false, Publish: false,
IP: net.ParseIP(ctx.ClientIP()), IP: net.ParseIP(ctx.ClientIP()),
User: user, User: user,

View file

@ -155,7 +155,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track {
return nil return nil
} }
err := mi.hmuxer.WriteH265(tunit.NTP, tunit.PTS, tunit.AU) err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU)
if err != nil { if err != nil {
return fmt.Errorf("muxer error: %w", err) return fmt.Errorf("muxer error: %w", err)
} }
@ -185,7 +185,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track {
return nil return nil
} }
err := mi.hmuxer.WriteH264(tunit.NTP, tunit.PTS, tunit.AU) err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU)
if err != nil { if err != nil {
return fmt.Errorf("muxer error: %w", err) return fmt.Errorf("muxer error: %w", err)
} }

View file

@ -2,7 +2,6 @@ package hls
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -12,6 +11,8 @@ import (
"github.com/bluenviron/gohlslib" "github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs" "github.com/bluenviron/gohlslib/pkg/codecs"
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
@ -49,52 +50,21 @@ func (pa *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
} }
type dummyPathManager struct { type dummyPathManager struct {
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error) stream *stream.Stream
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
} }
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) { func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
return pm.findPathConf(req) if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, auth.Error{}
}
return &conf.Path{}, nil
} }
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return pm.addReader(req) if req.AccessRequest.Name == "nonexisting" {
} return nil, nil, fmt.Errorf("not found")
func TestPreflightRequest(t *testing.T) {
s := &Server{
Address: "127.0.0.1:8888",
AllowOrigin: "*",
ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: test.NilLogger,
} }
err := s.Initialize() return &dummyPath{}, pm.stream, nil
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8888", nil)
require.NoError(t, err)
req.Header.Add("Access-Control-Request-Method", "GET")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusNoContent, res.StatusCode)
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods"))
require.Equal(t, "Authorization, Range", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
} }
func TestServerNotFound(t *testing.T) { func TestServerNotFound(t *testing.T) {
@ -103,19 +73,6 @@ func TestServerNotFound(t *testing.T) {
"always remux on", "always remux on",
} { } {
t.Run(ca, func(t *testing.T) { t.Run(ca, func(t *testing.T) {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "nonexisting", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "nonexisting", req.AccessRequest.Name)
return nil, nil, fmt.Errorf("not found")
},
}
s := &Server{ s := &Server{
Address: "127.0.0.1:8888", Address: "127.0.0.1:8888",
Encryption: false, Encryption: false,
@ -132,7 +89,7 @@ func TestServerNotFound(t *testing.T) {
Directory: "", Directory: "",
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512, WriteQueueSize: 512,
PathManager: pm, PathManager: &dummyPathManager{},
Parent: test.NilLogger, Parent: test.NilLogger,
} }
err := s.Initialize() err := s.Initialize()
@ -170,7 +127,7 @@ func TestServerRead(t *testing.T) {
t.Run("always remux off", func(t *testing.T) { t.Run("always remux off", func(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}} desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New( stream, err := stream.New(
1460, 1460,
desc, desc,
true, true,
@ -178,18 +135,7 @@ func TestServerRead(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
pm := &dummyPathManager{ pathManager := &dummyPathManager{stream: stream}
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "mystream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "mystream", req.AccessRequest.Name)
return &dummyPath{}, str, nil
},
}
s := &Server{ s := &Server{
Address: "127.0.0.1:8888", Address: "127.0.0.1:8888",
@ -207,7 +153,7 @@ func TestServerRead(t *testing.T) {
Directory: "", Directory: "",
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512, WriteQueueSize: 512,
PathManager: pm, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
err = s.Initialize() err = s.Initialize()
@ -229,6 +175,7 @@ func TestServerRead(t *testing.T) {
require.Equal(t, time.Duration(0), pts) require.Equal(t, time.Duration(0), pts)
require.Equal(t, time.Duration(0), dts) require.Equal(t, time.Duration(0), dts)
require.Equal(t, [][]byte{ require.Equal(t, [][]byte{
{byte(h264.NALUTypeAccessUnitDelimiter), 0xf0},
test.FormatH264.SPS, test.FormatH264.SPS,
test.FormatH264.PPS, test.FormatH264.PPS,
{5, 1}, {5, 1},
@ -247,7 +194,7 @@ func TestServerRead(t *testing.T) {
go func() { go func() {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{ stream.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
Base: unit.Base{ Base: unit.Base{
NTP: time.Time{}, NTP: time.Time{},
PTS: time.Duration(i) * time.Second, PTS: time.Duration(i) * time.Second,
@ -265,7 +212,7 @@ func TestServerRead(t *testing.T) {
t.Run("always remux on", func(t *testing.T) { t.Run("always remux on", func(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}} desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New( stream, err := stream.New(
1460, 1460,
desc, desc,
true, true,
@ -273,18 +220,7 @@ func TestServerRead(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
pm := &dummyPathManager{ pathManager := &dummyPathManager{stream: stream}
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "mystream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "mystream", req.AccessRequest.Name)
return &dummyPath{}, str, nil
},
}
s := &Server{ s := &Server{
Address: "127.0.0.1:8888", Address: "127.0.0.1:8888",
@ -302,7 +238,7 @@ func TestServerRead(t *testing.T) {
Directory: "", Directory: "",
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512, WriteQueueSize: 512,
PathManager: pm, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
err = s.Initialize() err = s.Initialize()
@ -314,7 +250,7 @@ func TestServerRead(t *testing.T) {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{ stream.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
Base: unit.Base{ Base: unit.Base{
NTP: time.Time{}, NTP: time.Time{},
PTS: time.Duration(i) * time.Second, PTS: time.Duration(i) * time.Second,
@ -340,6 +276,7 @@ func TestServerRead(t *testing.T) {
require.Equal(t, time.Duration(0), pts) require.Equal(t, time.Duration(0), pts)
require.Equal(t, time.Duration(0), dts) require.Equal(t, time.Duration(0), dts)
require.Equal(t, [][]byte{ require.Equal(t, [][]byte{
{0x09, 0xf0},
test.FormatH264.SPS, test.FormatH264.SPS,
test.FormatH264.PPS, test.FormatH264.PPS,
{5, 1}, {5, 1},
@ -359,102 +296,6 @@ func TestServerRead(t *testing.T) {
}) })
} }
func TestServerReadAuthorizationHeader(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return &conf.Path{}, nil
},
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return &dummyPath{}, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8888",
Encryption: false,
ServerKey: "",
ServerCert: "",
AlwaysRemux: true,
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
SegmentCount: 7,
SegmentDuration: conf.StringDuration(1 * time.Second),
PartDuration: conf.StringDuration(200 * time.Millisecond),
SegmentMaxSize: 50 * 1024 * 1024,
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
Directory: "",
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
s.PathReady(&dummyPath{})
time.Sleep(100 * time.Millisecond)
for i := 0; i < 4; i++ {
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
Base: unit.Base{
NTP: time.Time{},
PTS: time.Duration(i) * time.Second,
},
AU: [][]byte{
{5, 1}, // IDR
},
})
}
c := &gohlslib.Client{
URI: "http://127.0.0.1:8888/mystream/index.m3u8",
OnRequest: func(r *http.Request) {
r.Header.Set("Authorization", "Bearer testing")
},
}
recv := make(chan struct{})
c.OnTracks = func(tracks []*gohlslib.Track) error {
require.Equal(t, []*gohlslib.Track{{
Codec: &codecs.H264{},
}}, tracks)
c.OnDataH26x(tracks[0], func(pts, dts time.Duration, au [][]byte) {
require.Equal(t, time.Duration(0), pts)
require.Equal(t, time.Duration(0), dts)
require.Equal(t, [][]byte{
test.FormatH264.SPS,
test.FormatH264.PPS,
{5, 1},
}, au)
close(recv)
})
return nil
}
err = c.Start()
require.NoError(t, err)
defer func() { <-c.Wait() }()
defer c.Close()
<-recv
}
func TestDirectory(t *testing.T) { func TestDirectory(t *testing.T) {
dir, err := os.MkdirTemp("", "mediamtx-playback") dir, err := os.MkdirTemp("", "mediamtx-playback")
require.NoError(t, err) require.NoError(t, err)
@ -462,7 +303,7 @@ func TestDirectory(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}} desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New( stream, err := stream.New(
1460, 1460,
desc, desc,
true, true,
@ -470,11 +311,7 @@ func TestDirectory(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
pm := &dummyPathManager{ pathManager := &dummyPathManager{stream: stream}
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return &dummyPath{}, str, nil
},
}
s := &Server{ s := &Server{
Address: "127.0.0.1:8888", Address: "127.0.0.1:8888",
@ -492,7 +329,7 @@ func TestDirectory(t *testing.T) {
Directory: filepath.Join(dir, "mydir"), Directory: filepath.Join(dir, "mydir"),
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512, WriteQueueSize: 512,
PathManager: pm, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
err = s.Initialize() err = s.Initialize()

View file

@ -74,6 +74,9 @@ type conn struct {
pathName string pathName string
query string query string
sconn srt.Conn sconn srt.Conn
chNew chan srtNewConnReq
chSetConn chan srt.Conn
} }
func (c *conn) initialize() { func (c *conn) initialize() {
@ -81,6 +84,8 @@ func (c *conn) initialize() {
c.created = time.Now() c.created = time.Now()
c.uuid = uuid.New() c.uuid = uuid.New()
c.chNew = make(chan srtNewConnReq)
c.chSetConn = make(chan srt.Conn)
c.Log(logger.Info, "opened") c.Log(logger.Info, "opened")
@ -125,20 +130,36 @@ func (c *conn) run() { //nolint:dupl
} }
func (c *conn) runInner() error { func (c *conn) runInner() error {
var req srtNewConnReq
select {
case req = <-c.chNew:
case <-c.ctx.Done():
return errors.New("terminated")
}
answerSent, err := c.runInner2(req)
if !answerSent {
req.res <- nil
}
return err
}
func (c *conn) runInner2(req srtNewConnReq) (bool, error) {
var streamID streamID var streamID streamID
err := streamID.unmarshal(c.connReq.StreamId()) err := streamID.unmarshal(req.connReq.StreamId())
if err != nil { if err != nil {
c.connReq.Reject(srt.REJ_PEER) return false, fmt.Errorf("invalid stream ID '%s': %w", req.connReq.StreamId(), err)
return fmt.Errorf("invalid stream ID '%s': %w", c.connReq.StreamId(), err)
} }
if streamID.mode == streamIDModePublish { if streamID.mode == streamIDModePublish {
return c.runPublish(&streamID) return c.runPublish(req, &streamID)
} }
return c.runRead(&streamID) return c.runRead(req, &streamID)
} }
func (c *conn) runPublish(streamID *streamID) error { func (c *conn) runPublish(req srtNewConnReq, streamID *streamID) (bool, error) {
path, err := c.pathManager.AddPublisher(defs.PathAddPublisherReq{ path, err := c.pathManager.AddPublisher(defs.PathAddPublisherReq{
Author: c, Author: c,
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
@ -157,24 +178,21 @@ func (c *conn) runPublish(streamID *streamID) error {
if errors.As(err, &terr) { if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks // wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError) <-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER) return false, terr
return terr
} }
c.connReq.Reject(srt.REJ_PEER) return false, err
return err
} }
defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: c}) defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: c})
err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTPublishPassphrase) err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTPublishPassphrase)
if err != nil { if err != nil {
c.connReq.Reject(srt.REJ_PEER) return false, err
return err
} }
sconn, err := c.connReq.Accept() sconn, err := c.exchangeRequestWithConn(req)
if err != nil { if err != nil {
return err return true, err
} }
c.mutex.Lock() c.mutex.Lock()
@ -192,12 +210,12 @@ func (c *conn) runPublish(streamID *streamID) error {
select { select {
case err := <-readerErr: case err := <-readerErr:
sconn.Close() sconn.Close()
return err return true, err
case <-c.ctx.Done(): case <-c.ctx.Done():
sconn.Close() sconn.Close()
<-readerErr <-readerErr
return errors.New("terminated") return true, errors.New("terminated")
} }
} }
@ -238,7 +256,7 @@ func (c *conn) runPublishReader(sconn srt.Conn, path defs.Path) error {
} }
} }
func (c *conn) runRead(streamID *streamID) error { func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) {
path, stream, err := c.pathManager.AddReader(defs.PathAddReaderReq{ path, stream, err := c.pathManager.AddReader(defs.PathAddReaderReq{
Author: c, Author: c,
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
@ -256,24 +274,21 @@ func (c *conn) runRead(streamID *streamID) error {
if errors.As(err, &terr) { if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks // wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError) <-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER) return false, err
return terr
} }
c.connReq.Reject(srt.REJ_PEER) return false, err
return err
} }
defer path.RemoveReader(defs.PathRemoveReaderReq{Author: c}) defer path.RemoveReader(defs.PathRemoveReaderReq{Author: c})
err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTReadPassphrase) err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTReadPassphrase)
if err != nil { if err != nil {
c.connReq.Reject(srt.REJ_PEER) return false, err
return err
} }
sconn, err := c.connReq.Accept() sconn, err := c.exchangeRequestWithConn(req)
if err != nil { if err != nil {
return err return true, err
} }
defer sconn.Close() defer sconn.Close()
@ -292,7 +307,7 @@ func (c *conn) runRead(streamID *streamID) error {
err = mpegts.FromStream(stream, writer, bw, sconn, time.Duration(c.writeTimeout)) err = mpegts.FromStream(stream, writer, bw, sconn, time.Duration(c.writeTimeout))
if err != nil { if err != nil {
return err return true, err
} }
c.Log(logger.Info, "is reading from path '%s', %s", c.Log(logger.Info, "is reading from path '%s', %s",
@ -316,10 +331,41 @@ func (c *conn) runRead(streamID *streamID) error {
select { select {
case <-c.ctx.Done(): case <-c.ctx.Done():
return fmt.Errorf("terminated") return true, fmt.Errorf("terminated")
case err := <-writer.Error(): case err := <-writer.Error():
return err return true, err
}
}
func (c *conn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) {
req.res <- c
select {
case sconn := <-c.chSetConn:
return sconn, nil
case <-c.ctx.Done():
return nil, errors.New("terminated")
}
}
// new is called by srtListener through srtServer.
func (c *conn) new(req srtNewConnReq) *conn {
select {
case c.chNew <- req:
return <-req.res
case <-c.ctx.Done():
return nil
}
}
// setConn is called by srtListener .
func (c *conn) setConn(sconn srt.Conn) {
select {
case c.chSetConn <- sconn:
case <-c.ctx.Done():
} }
} }

View file

@ -27,11 +27,24 @@ func (l *listener) run() {
func (l *listener) runInner() error { func (l *listener) runInner() error {
for { for {
req, err := l.ln.Accept2() var sconn *conn
conn, _, err := l.ln.Accept(func(req srt.ConnRequest) srt.ConnType {
sconn = l.parent.newConnRequest(req)
if sconn == nil {
return srt.REJECT
}
// currently it's the same to return SUBSCRIBE or PUBLISH
return srt.SUBSCRIBE
})
if err != nil { if err != nil {
return err return err
} }
l.parent.newConnRequest(req) if conn == nil {
continue
}
sconn.setConn(conn)
} }
} }

View file

@ -26,6 +26,11 @@ func srtMaxPayloadSize(u int) int {
return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet
} }
type srtNewConnReq struct {
connReq srt.ConnRequest
res chan *conn
}
type serverAPIConnsListRes struct { type serverAPIConnsListRes struct {
data *defs.APISRTConnList data *defs.APISRTConnList
err error err error
@ -85,7 +90,7 @@ type Server struct {
conns map[*conn]struct{} conns map[*conn]struct{}
// in // in
chNewConnRequest chan srt.ConnRequest chNewConnRequest chan srtNewConnReq
chAcceptErr chan error chAcceptErr chan error
chCloseConn chan *conn chCloseConn chan *conn
chAPIConnsList chan serverAPIConnsListReq chAPIConnsList chan serverAPIConnsListReq
@ -108,7 +113,7 @@ func (s *Server) Initialize() error {
s.ctx, s.ctxCancel = context.WithCancel(context.Background()) s.ctx, s.ctxCancel = context.WithCancel(context.Background())
s.conns = make(map[*conn]struct{}) s.conns = make(map[*conn]struct{})
s.chNewConnRequest = make(chan srt.ConnRequest) s.chNewConnRequest = make(chan srtNewConnReq)
s.chAcceptErr = make(chan error) s.chAcceptErr = make(chan error)
s.chCloseConn = make(chan *conn) s.chCloseConn = make(chan *conn)
s.chAPIConnsList = make(chan serverAPIConnsListReq) s.chAPIConnsList = make(chan serverAPIConnsListReq)
@ -160,7 +165,7 @@ outer:
writeTimeout: s.WriteTimeout, writeTimeout: s.WriteTimeout,
writeQueueSize: s.WriteQueueSize, writeQueueSize: s.WriteQueueSize,
udpMaxPayloadSize: s.UDPMaxPayloadSize, udpMaxPayloadSize: s.UDPMaxPayloadSize,
connReq: req, connReq: req.connReq,
runOnConnect: s.RunOnConnect, runOnConnect: s.RunOnConnect,
runOnConnectRestart: s.RunOnConnectRestart, runOnConnectRestart: s.RunOnConnectRestart,
runOnDisconnect: s.RunOnDisconnect, runOnDisconnect: s.RunOnDisconnect,
@ -171,6 +176,7 @@ outer:
} }
c.initialize() c.initialize()
s.conns[c] = struct{}{} s.conns[c] = struct{}{}
req.res <- c
case c := <-s.chCloseConn: case c := <-s.chCloseConn:
delete(s.conns, c) delete(s.conns, c)
@ -230,11 +236,20 @@ func (s *Server) findConnByUUID(uuid uuid.UUID) *conn {
} }
// newConnRequest is called by srtListener. // newConnRequest is called by srtListener.
func (s *Server) newConnRequest(connReq srt.ConnRequest) { func (s *Server) newConnRequest(connReq srt.ConnRequest) *conn {
req := srtNewConnReq{
connReq: connReq,
res: make(chan *conn),
}
select { select {
case s.chNewConnRequest <- connReq: case s.chNewConnRequest <- req:
c := <-req.res
return c.new(req)
case <-s.ctx.Done(): case <-s.ctx.Done():
connReq.Reject(srt.REJ_CLOSE) return nil
} }
} }

View file

@ -106,7 +106,7 @@ func TestServerPublish(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer s.Close() defer s.Close()
u := "srt://127.0.0.1:8890?streamid=publish:mypath:myuser:mypass" u := "srt://localhost:8890?streamid=publish:mypath:myuser:mypass"
srtConf := srt.DefaultConfig() srtConf := srt.DefaultConfig()
address, err := srtConf.UnmarshalURL(u) address, err := srtConf.UnmarshalURL(u)
@ -127,7 +127,7 @@ func TestServerPublish(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) w := mpegts.NewWriter(bw, []*mpegts.Track{track})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track, 0, 0, true, [][]byte{ err = w.WriteH26x(track, 0, 0, true, [][]byte{
test.FormatH264.SPS, test.FormatH264.SPS,
test.FormatH264.PPS, test.FormatH264.PPS,
{0x05, 1}, // IDR {0x05, 1}, // IDR
@ -156,7 +156,7 @@ func TestServerPublish(t *testing.T) {
return nil return nil
}) })
err = w.WriteH264(track, 0, 0, true, [][]byte{ err = w.WriteH26x(track, 0, 0, true, [][]byte{
{5, 2}, {5, 2},
}) })
require.NoError(t, err) require.NoError(t, err)
@ -205,7 +205,7 @@ func TestServerRead(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer s.Close() defer s.Close()
u := "srt://127.0.0.1:8890?streamid=read:mypath:myuser:mypass" u := "srt://localhost:8890?streamid=read:mypath:myuser:mypass"
srtConf := srt.DefaultConfig() srtConf := srt.DefaultConfig()
address, err := srtConf.UnmarshalURL(u) address, err := srtConf.UnmarshalURL(u)
@ -237,7 +237,7 @@ func TestServerRead(t *testing.T) {
received := false received := false
r.OnDataH264(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error { r.OnDataH26x(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error {
require.Equal(t, int64(0), pts) require.Equal(t, int64(0), pts)
require.Equal(t, int64(0), dts) require.Equal(t, int64(0), dts)
require.Equal(t, [][]byte{ require.Equal(t, [][]byte{

View file

@ -7,7 +7,6 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -60,17 +59,6 @@ func sessionLocation(publish bool, path string, secret uuid.UUID) string {
return ret return ret
} }
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
type httpServer struct { type httpServer struct {
address string address string
encryption bool encryption bool
@ -121,23 +109,11 @@ func (s *httpServer) close() {
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool { func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
user, pass, hasCredentials := ctx.Request.BasicAuth() user, pass, hasCredentials := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters
q = addJWTFromAuthorization(q, h)
// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}
}
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ _, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
Name: pathName, Name: pathName,
Query: q, Query: ctx.Request.URL.RawQuery,
Publish: publish, Publish: publish,
IP: net.ParseIP(ctx.ClientIP()), IP: net.ParseIP(ctx.ClientIP()),
User: user, User: user,
@ -201,23 +177,11 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
} }
user, pass, _ := ctx.Request.BasicAuth() user, pass, _ := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters
q = addJWTFromAuthorization(q, h)
// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}
}
res := s.parent.newSession(webRTCNewSessionReq{ res := s.parent.newSession(webRTCNewSessionReq{
pathName: pathName, pathName: pathName,
remoteAddr: httpp.RemoteAddr(ctx), remoteAddr: httpp.RemoteAddr(ctx),
query: q, query: ctx.Request.URL.RawQuery,
user: user, user: user,
pass: pass, pass: pass,
offer: offer, offer: offer,

View file

@ -376,10 +376,11 @@ const editOffer = (sdp) => {
const sections = sdp.split('m='); const sections = sdp.split('m=');
for (let i = 0; i < sections.length; i++) { for (let i = 0; i < sections.length; i++) {
if (sections[i].startsWith('video')) { const section = sections[i];
sections[i] = setCodec(sections[i], videoForm.codec.value); if (section.startsWith('video')) {
} else if (sections[i].startsWith('audio')) { sections[i] = setCodec(section, videoForm.codec.value);
sections[i] = setAudioBitrate(setCodec(sections[i], audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked); } else if (section.startsWith('audio')) {
sections[i] = setAudioBitrate(setCodec(section, audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked);
} }
} }
@ -390,8 +391,9 @@ const editAnswer = (sdp) => {
const sections = sdp.split('m='); const sections = sdp.split('m=');
for (let i = 0; i < sections.length; i++) { for (let i = 0; i < sections.length; i++) {
if (sections[i].startsWith('video')) { const section = sections[i];
sections[i] = setVideoBitrate(sections[i], videoForm.bitrate.value); if (section.startsWith('video')) {
sections[i] = setVideoBitrate(section, videoForm.bitrate.value);
} }
} }
@ -409,7 +411,7 @@ const sendLocalCandidates = (candidates) => {
}) })
.then((res) => { .then((res) => {
if (res.status !== 204) { if (res.status !== 204) {
throw new Error(`bad status code ${res.status}`); throw new Error('bad status code');
} }
}) })
.catch((err) => { .catch((err) => {
@ -441,16 +443,12 @@ const onRemoteAnswer = (sdp) => {
pc.setRemoteDescription(new RTCSessionDescription({ pc.setRemoteDescription(new RTCSessionDescription({
type: 'answer', type: 'answer',
sdp, sdp,
})) }));
.then(() => {
if (queuedCandidates.length !== 0) { if (queuedCandidates.length !== 0) {
sendLocalCandidates(queuedCandidates); sendLocalCandidates(queuedCandidates);
queuedCandidates = []; queuedCandidates = [];
} }
})
.catch((err) => {
onError(err.toString());
});
}; };
const sendOffer = (offer) => { const sendOffer = (offer) => {
@ -464,20 +462,13 @@ const sendOffer = (offer) => {
body: offer, body: offer,
}) })
.then((res) => { .then((res) => {
switch (res.status) { if (res.status !== 201) {
case 201: throw new Error('bad status code');
break;
case 400:
return res.json().then((e) => { throw new Error(e.error); });
default:
throw new Error(`bad status code ${res.status}`);
} }
sessionUrl = new URL(res.headers.get('location'), window.location.href).toString(); sessionUrl = new URL(res.headers.get('location'), window.location.href).toString();
return res.text();
return res.text()
.then((answer) => onRemoteAnswer(answer));
}) })
.then((answer) => onRemoteAnswer(answer))
.catch((err) => { .catch((err) => {
onError(err.toString(), true); onError(err.toString(), true);
}); });
@ -487,16 +478,8 @@ const createOffer = () => {
pc.createOffer() pc.createOffer()
.then((offer) => { .then((offer) => {
offerData = parseOffer(offer.sdp); offerData = parseOffer(offer.sdp);
pc.setLocalDescription(offer) pc.setLocalDescription(offer);
.then(() => { sendOffer(offer.sdp);
sendOffer(offer.sdp);
})
.catch((err) => {
onError(err.toString());
});
})
.catch((err) => {
onError(err.toString());
}); });
}; };
@ -506,7 +489,7 @@ const onConnectionState = () => {
} }
if (pc.iceConnectionState === 'disconnected') { if (pc.iceConnectionState === 'disconnected') {
onError('peer connection closed', true); onError('peer connection disconnected', true);
} else if (pc.iceConnectionState === 'connected') { } else if (pc.iceConnectionState === 'connected') {
setMessage(''); setMessage('');
} }

View file

@ -50,7 +50,6 @@ const retryPause = 2000;
const video = document.getElementById('video'); const video = document.getElementById('video');
const message = document.getElementById('message'); const message = document.getElementById('message');
let nonAdvertisedCodecs = [];
let pc = null; let pc = null;
let restartTimeout = null; let restartTimeout = null;
let sessionUrl = ''; let sessionUrl = '';
@ -88,14 +87,14 @@ const linkToIceServers = (links) => (
}) : [] }) : []
); );
const parseOffer = (sdp) => { const parseOffer = (offer) => {
const ret = { const ret = {
iceUfrag: '', iceUfrag: '',
icePwd: '', icePwd: '',
medias: [], medias: [],
}; };
for (const line of sdp.split('\r\n')) { for (const line of offer.split('\r\n')) {
if (line.startsWith('m=')) { if (line.startsWith('m=')) {
ret.medias.push(line.slice('m='.length)); ret.medias.push(line.slice('m='.length));
} else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) { } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
@ -108,74 +107,6 @@ const parseOffer = (sdp) => {
return ret; return ret;
}; };
const enableStereoPcmau = (section) => {
let lines = section.split('\r\n');
lines[0] += ' 118';
lines.splice(lines.length - 1, 0, 'a=rtpmap:118 PCMU/8000/2');
lines.splice(lines.length - 1, 0, 'a=rtcp-fb:118 transport-cc');
lines[0] += ' 119';
lines.splice(lines.length - 1, 0, 'a=rtpmap:119 PCMA/8000/2');
lines.splice(lines.length - 1, 0, 'a=rtcp-fb:119 transport-cc');
return lines.join('\r\n');
};
const enableMultichannelOpus = (section) => {
let lines = section.split('\r\n');
lines[0] += " 112";
lines.splice(lines.length - 1, 0, "a=rtpmap:112 multiopus/48000/3");
lines.splice(lines.length - 1, 0, "a=fmtp:112 channel_mapping=0,2,1;num_streams=2;coupled_streams=1");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:112 transport-cc");
lines[0] += " 113";
lines.splice(lines.length - 1, 0, "a=rtpmap:113 multiopus/48000/4");
lines.splice(lines.length - 1, 0, "a=fmtp:113 channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:113 transport-cc");
lines[0] += " 114";
lines.splice(lines.length - 1, 0, "a=rtpmap:114 multiopus/48000/5");
lines.splice(lines.length - 1, 0, "a=fmtp:114 channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:114 transport-cc");
lines[0] += " 115";
lines.splice(lines.length - 1, 0, "a=rtpmap:115 multiopus/48000/6");
lines.splice(lines.length - 1, 0, "a=fmtp:115 channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:115 transport-cc");
lines[0] += " 116";
lines.splice(lines.length - 1, 0, "a=rtpmap:116 multiopus/48000/7");
lines.splice(lines.length - 1, 0, "a=fmtp:116 channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:116 transport-cc");
lines[0] += " 117";
lines.splice(lines.length - 1, 0, "a=rtpmap:117 multiopus/48000/8");
lines.splice(lines.length - 1, 0, "a=fmtp:117 channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:117 transport-cc");
return lines.join('\r\n');
};
const enableL16 = (section) => {
let lines = section.split('\r\n');
lines[0] += " 120";
lines.splice(lines.length - 1, 0, "a=rtpmap:120 L16/8000/2");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:120 transport-cc");
lines[0] += " 121";
lines.splice(lines.length - 1, 0, "a=rtpmap:121 L16/16000/2");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:121 transport-cc");
lines[0] += " 122";
lines.splice(lines.length - 1, 0, "a=rtpmap:122 L16/48000/2");
lines.splice(lines.length - 1, 0, "a=rtcp-fb:122 transport-cc");
return lines.join('\r\n');
};
const enableStereoOpus = (section) => { const enableStereoOpus = (section) => {
let opusPayloadFormat = ''; let opusPayloadFormat = '';
let lines = section.split('\r\n'); let lines = section.split('\r\n');
@ -205,30 +136,17 @@ const enableStereoOpus = (section) => {
return lines.join('\r\n'); return lines.join('\r\n');
}; };
const editOffer = (sdp) => { const editOffer = (offer) => {
const sections = sdp.split('m='); const sections = offer.sdp.split('m=');
for (let i = 0; i < sections.length; i++) { for (let i = 0; i < sections.length; i++) {
if (sections[i].startsWith('audio')) { const section = sections[i];
sections[i] = enableStereoOpus(sections[i]); if (section.startsWith('audio')) {
sections[i] = enableStereoOpus(section);
if (nonAdvertisedCodecs.includes('pcma/8000/2')) {
sections[i] = enableStereoPcmau(sections[i]);
}
if (nonAdvertisedCodecs.includes('multiopus/48000/6')) {
sections[i] = enableMultichannelOpus(sections[i]);
}
if (nonAdvertisedCodecs.includes('L16/48000/2')) {
sections[i] = enableL16(sections[i]);
}
break;
} }
} }
return sections.join('m='); offer.sdp = sections.join('m=');
}; };
const generateSdpFragment = (od, candidates) => { const generateSdpFragment = (od, candidates) => {
@ -265,70 +183,6 @@ const loadStream = () => {
requestICEServers(); requestICEServers();
}; };
const supportsNonAdvertisedCodec = (codec, fmtp) => (
new Promise((resolve, reject) => {
const pc = new RTCPeerConnection({ iceServers: [] });
pc.addTransceiver('audio', { direction: 'recvonly' });
pc.createOffer()
.then((offer) => {
if (offer.sdp.includes(' ' + codec)) { // codec is advertised, there's no need to add it manually
resolve(false);
return;
}
const sections = offer.sdp.split('m=audio');
const lines = sections[1].split('\r\n');
lines[0] += ' 118';
lines.splice(lines.length - 1, 0, 'a=rtpmap:118 ' + codec);
if (fmtp !== undefined) {
lines.splice(lines.length - 1, 0, 'a=fmtp:118 ' + fmtp);
}
sections[1] = lines.join('\r\n');
offer.sdp = sections.join('m=audio');
return pc.setLocalDescription(offer);
})
.then(() => {
return pc.setRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp: 'v=0\r\n'
+ 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n'
+ 's=-\r\n'
+ 't=0 0\r\n'
+ 'a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n'
+ 'm=audio 9 UDP/TLS/RTP/SAVPF 118\r\n'
+ 'c=IN IP4 0.0.0.0\r\n'
+ 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n'
+ 'a=ice-ufrag:29e036dc\r\n'
+ 'a=sendonly\r\n'
+ 'a=rtcp-mux\r\n'
+ 'a=rtpmap:118 ' + codec + '\r\n'
+ ((fmtp !== undefined) ? 'a=fmtp:118 ' + fmtp + '\r\n' : ''),
}));
})
.then(() => {
resolve(true);
})
.catch((err) => {
resolve(false);
})
.finally(() => {
pc.close();
});
})
);
const getNonAdvertisedCodecs = () => {
Promise.all([
['pcma/8000/2'],
['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'],
['L16/48000/2']
].map((c) => supportsNonAdvertisedCodec(c[0], c[1]).then((r) => (r) ? c[0] : false)))
.then((c) => c.filter((e) => e !== false))
.then((codecs) => {
nonAdvertisedCodecs = codecs;
loadStream();
});
};
const onError = (err) => { const onError = (err) => {
if (restartTimeout === null) { if (restartTimeout === null) {
setMessage(err + ', retrying in some seconds'); setMessage(err + ', retrying in some seconds');
@ -400,16 +254,12 @@ const onRemoteAnswer = (sdp) => {
pc.setRemoteDescription(new RTCSessionDescription({ pc.setRemoteDescription(new RTCSessionDescription({
type: 'answer', type: 'answer',
sdp, sdp,
})) }));
.then(() => {
if (queuedCandidates.length !== 0) { if (queuedCandidates.length !== 0) {
sendLocalCandidates(queuedCandidates); sendLocalCandidates(queuedCandidates);
queuedCandidates = []; queuedCandidates = [];
} }
})
.catch((err) => {
onError(err.toString());
});
}; };
const sendOffer = (offer) => { const sendOffer = (offer) => {
@ -426,16 +276,11 @@ const sendOffer = (offer) => {
break; break;
case 404: case 404:
throw new Error('stream not found'); throw new Error('stream not found');
case 400:
return res.json().then((e) => { throw new Error(e.error); });
default: default:
throw new Error(`bad status code ${res.status}`); throw new Error(`bad status code ${res.status}`);
} }
sessionUrl = new URL(res.headers.get('location'), window.location.href).toString(); sessionUrl = new URL(res.headers.get('location'), window.location.href).toString();
return res.text();
return res.text()
.then((sdp) => onRemoteAnswer(sdp));
}) })
.then((sdp) => onRemoteAnswer(sdp)) .then((sdp) => onRemoteAnswer(sdp))
.catch((err) => { .catch((err) => {
@ -446,18 +291,10 @@ const sendOffer = (offer) => {
const createOffer = () => { const createOffer = () => {
pc.createOffer() pc.createOffer()
.then((offer) => { .then((offer) => {
offer.sdp = editOffer(offer.sdp); editOffer(offer);
offerData = parseOffer(offer.sdp); offerData = parseOffer(offer.sdp);
pc.setLocalDescription(offer) pc.setLocalDescription(offer);
.then(() => { sendOffer(offer);
sendOffer(offer);
})
.catch((err) => {
onError(err.toString());
});
})
.catch((err) => {
onError(err.toString());
}); });
}; };
@ -467,7 +304,7 @@ const onConnectionState = () => {
} }
if (pc.iceConnectionState === 'disconnected') { if (pc.iceConnectionState === 'disconnected') {
onError('peer connection closed'); onError('peer connection disconnected');
} }
}; };
@ -525,7 +362,7 @@ const loadAttributesFromQuery = () => {
const init = () => { const init = () => {
loadAttributesFromQuery(); loadAttributesFromQuery();
getNonAdvertisedCodecs(); loadStream();
}; };
window.addEventListener('DOMContentLoaded', init); window.addEventListener('DOMContentLoaded', init);

View file

@ -192,8 +192,6 @@ type Server struct {
IPsFromInterfacesList []string IPsFromInterfacesList []string
AdditionalHosts []string AdditionalHosts []string
ICEServers []conf.WebRTCICEServer ICEServers []conf.WebRTCICEServer
HandshakeTimeout conf.StringDuration
TrackGatherTimeout conf.StringDuration
ExternalCmdPool *externalcmd.Pool ExternalCmdPool *externalcmd.Pool
PathManager serverPathManager PathManager serverPathManager
Parent serverParent Parent serverParent

View file

@ -3,16 +3,14 @@ package webrtc
import ( import (
"bytes" "bytes"
"context" "context"
"io"
"net/http" "net/http"
"net/url" "net/url"
"reflect"
"testing" "testing"
"time" "time"
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
@ -26,6 +24,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func uint16Ptr(v uint16) *uint16 {
return &v
}
func checkClose(t *testing.T, closeFunc func() error) { func checkClose(t *testing.T, closeFunc func() error) {
require.NoError(t, closeFunc()) require.NoError(t, closeFunc())
} }
@ -72,38 +74,40 @@ func (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
} }
type dummyPathManager struct { type dummyPathManager struct {
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error) path *dummyPath
addPublisher func(req defs.PathAddPublisherReq) (defs.Path, error)
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
} }
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) { func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
return pm.findPathConf(req) if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, auth.Error{}
}
return &conf.Path{}, nil
} }
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) { func (pm *dummyPathManager) AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, error) {
return pm.addPublisher(req) return pm.path, nil
} }
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return pm.addReader(req) if req.AccessRequest.Name == "nonexisting" {
return nil, nil, defs.PathNoOnePublishingError{}
}
return pm.path, pm.path.stream, nil
} }
func initializeTestServer(t *testing.T) *Server { func initializeTestServer(t *testing.T) *Server {
pm := &dummyPathManager{ path := &dummyPath{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { streamCreated: make(chan struct{}),
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
} }
pathManager := &dummyPathManager{path: path}
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
Encryption: false, Encryption: false,
ServerKey: "", ServerKey: "",
ServerCert: "", ServerCert: "",
AllowOrigin: "*", AllowOrigin: "",
TrustedProxies: conf.IPNetworks{}, TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512, WriteQueueSize: 512,
@ -113,10 +117,8 @@ func initializeTestServer(t *testing.T) *Server {
IPsFromInterfacesList: []string{}, IPsFromInterfacesList: []string{},
AdditionalHosts: []string{}, AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{}, ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil, ExternalCmdPool: nil,
PathManager: pm, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
err := s.Initialize() err := s.Initialize()
@ -147,7 +149,7 @@ func TestServerStaticPages(t *testing.T) {
} }
} }
func TestPreflightRequest(t *testing.T) { func TestServerOptionsPreflight(t *testing.T) {
s := initializeTestServer(t) s := initializeTestServer(t)
defer s.Close() defer s.Close()
@ -155,10 +157,11 @@ func TestPreflightRequest(t *testing.T) {
defer tr.CloseIdleConnections() defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr} hc := &http.Client{Transport: tr}
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886", nil) // preflight requests must always work, without authentication
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886/teststream/whip", nil)
require.NoError(t, err) require.NoError(t, err)
req.Header.Add("Access-Control-Request-Method", "GET") req.Header.Set("Access-Control-Request-Method", "OPTIONS")
res, err := hc.Do(req) res, err := hc.Do(req)
require.NoError(t, err) require.NoError(t, err)
@ -166,24 +169,12 @@ func TestPreflightRequest(t *testing.T) {
require.Equal(t, http.StatusNoContent, res.StatusCode) require.Equal(t, http.StatusNoContent, res.StatusCode)
byts, err := io.ReadAll(res.Body) _, ok := res.Header["Link"]
require.NoError(t, err) require.Equal(t, false, ok)
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods"))
require.Equal(t, "Authorization, Content-Type, If-Match", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
} }
func TestServerOptionsICEServer(t *testing.T) { func TestServerOptionsICEServer(t *testing.T) {
pathManager := &dummyPathManager{ pathManager := &dummyPathManager{}
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
}
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
@ -204,11 +195,9 @@ func TestServerOptionsICEServer(t *testing.T) {
Username: "myuser", Username: "myuser",
Password: "mypass", Password: "mypass",
}}, }},
HandshakeTimeout: conf.StringDuration(10 * time.Second), ExternalCmdPool: nil,
TrackGatherTimeout: conf.StringDuration(2 * time.Second), PathManager: pathManager,
ExternalCmdPool: nil, Parent: test.NilLogger,
PathManager: pathManager,
Parent: test.NilLogger,
} }
err := s.Initialize() err := s.Initialize()
require.NoError(t, err) require.NoError(t, err)
@ -243,20 +232,7 @@ func TestServerPublish(t *testing.T) {
streamCreated: make(chan struct{}), streamCreated: make(chan struct{}),
} }
pathManager := &dummyPathManager{ pathManager := &dummyPathManager{path: path}
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, nil
},
}
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
@ -273,8 +249,6 @@ func TestServerPublish(t *testing.T) {
IPsFromInterfacesList: []string{}, IPsFromInterfacesList: []string{},
AdditionalHosts: []string{}, AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{}, ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil, ExternalCmdPool: nil,
PathManager: pathManager, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
@ -356,450 +330,109 @@ func TestServerPublish(t *testing.T) {
} }
func TestServerRead(t *testing.T) { func TestServerRead(t *testing.T) {
for _, ca := range []struct { desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
name string
medias []*description.Media stream, err := stream.New(
unit unit.Unit 1460,
outRTPPayload []byte desc,
}{ true,
{ test.NilLogger,
"av1", )
[]*description.Media{{ require.NoError(t, err)
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.AV1{ path := &dummyPath{stream: stream}
PayloadTyp: 96,
}}, pathManager := &dummyPathManager{path: path}
}},
&unit.AV1{ s := &Server{
TU: [][]byte{{1, 2}}, Address: "127.0.0.1:8886",
}, Encryption: false,
[]byte{0, 2, 1, 2}, ServerKey: "",
}, ServerCert: "",
{ AllowOrigin: "",
"vp9", TrustedProxies: conf.IPNetworks{},
[]*description.Media{{ ReadTimeout: conf.StringDuration(10 * time.Second),
Type: description.MediaTypeVideo, WriteQueueSize: 512,
Formats: []format.Format{&format.VP9{ LocalUDPAddress: "127.0.0.1:8887",
PayloadTyp: 96, LocalTCPAddress: "127.0.0.1:8887",
}}, IPsFromInterfaces: true,
}}, IPsFromInterfacesList: []string{},
&unit.VP9{ AdditionalHosts: []string{},
Frame: []byte{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34}, ICEServers: []conf.WebRTCICEServer{},
}, ExternalCmdPool: nil,
[]byte{ PathManager: pathManager,
0x8f, 0xa0, 0xfd, 0x18, 0x07, 0x80, 0x03, 0x24, Parent: test.NilLogger,
0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00, }
0x77, 0xf0, 0x32, 0x34, err = s.Initialize()
}, require.NoError(t, err)
}, defer s.Close()
{
"vp8", u, err := url.Parse("http://myuser:mypass@localhost:8886/teststream/whep?param=value")
[]*description.Media{{ require.NoError(t, err)
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP8{ tr := &http.Transport{}
PayloadTyp: 96, defer tr.CloseIdleConnections()
}}, hc := &http.Client{Transport: tr}
}},
&unit.VP8{ wc := &webrtc.WHIPClient{
Frame: []byte{1, 2}, HTTPClient: hc,
}, URL: u,
[]byte{0x10, 1, 2}, Log: test.NilLogger,
}, }
{
"h264", writerDone := make(chan struct{})
[]*description.Media{test.MediaH264}, defer func() { <-writerDone }()
&unit.H264{
writerTerminate := make(chan struct{})
defer close(writerTerminate)
go func() {
defer close(writerDone)
for {
select {
case <-time.After(100 * time.Millisecond):
case <-writerTerminate:
return
}
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Time{},
},
AU: [][]byte{ AU: [][]byte{
{5, 1}, {5, 1},
}, },
}, })
[]byte{ }
0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, }()
0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,
0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, tracks, err := wc.Read(context.Background())
0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06, require.NoError(t, err)
0x07, 0x08, 0x00, 0x02, 0x05, 0x01, defer checkClose(t, wc.Close)
},
pkt, err := tracks[0].ReadRTP()
require.NoError(t, err)
require.Equal(t, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 101,
SequenceNumber: pkt.SequenceNumber,
Timestamp: pkt.Timestamp,
SSRC: pkt.SSRC,
CSRC: []uint32{},
}, },
{ Payload: []byte{
"opus", 0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,
[]*description.Media{{ 0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,
Type: description.MediaTypeAudio, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,
Formats: []format.Format{&format.Opus{ 0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,
PayloadTyp: 96, 0x07, 0x08, 0x00, 0x02, 0x05, 0x01,
ChannelCount: 2,
}},
}},
&unit.Opus{
Packets: [][]byte{{1, 2}},
},
[]byte{1, 2},
}, },
{ }, pkt)
"g722",
[]*description.Media{{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.G722{}},
}},
&unit.Generic{
Base: unit.Base{
RTPPackets: []*rtp.Packet{{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 9,
SequenceNumber: 1123,
Timestamp: 45343,
SSRC: 563423,
},
Payload: []byte{1, 2},
}},
},
},
[]byte{1, 2},
},
{
"g711 8khz mono",
[]*description.Media{{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.G711{
MULaw: true,
SampleRate: 8000,
ChannelCount: 1,
}},
}},
&unit.G711{
Samples: []byte{1, 2, 3},
},
[]byte{1, 2, 3},
},
{
"g711 16khz stereo",
[]*description.Media{{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.G711{
MULaw: true,
SampleRate: 16000,
ChannelCount: 2,
}},
}},
&unit.G711{
Samples: []byte{1, 2, 3, 4},
},
[]byte{0x86, 0x84, 0x8a, 0x84, 0x8e, 0x84, 0x92, 0x84},
},
{
"lpcm",
[]*description.Media{{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: 48000,
ChannelCount: 2,
}},
}},
&unit.LPCM{
Samples: []byte{1, 2, 3, 4},
},
[]byte{1, 2, 3, 4},
},
} {
t.Run(ca.name, func(t *testing.T) {
desc := &description.Session{Medias: ca.medias}
str, err := stream.New(
1460,
desc,
reflect.TypeOf(ca.unit) != reflect.TypeOf(&unit.Generic{}),
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: str}
pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
u, err := url.Parse("http://myuser:mypass@localhost:8886/teststream/whep?param=value")
require.NoError(t, err)
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
wc := &webrtc.WHIPClient{
HTTPClient: hc,
URL: u,
Log: test.NilLogger,
}
writerDone := make(chan struct{})
defer func() { <-writerDone }()
writerTerminate := make(chan struct{})
defer close(writerTerminate)
go func() {
defer close(writerDone)
for {
select {
case <-time.After(100 * time.Millisecond):
case <-writerTerminate:
return
}
r := reflect.New(reflect.TypeOf(ca.unit).Elem())
r.Elem().Set(reflect.ValueOf(ca.unit).Elem())
if g, ok := r.Interface().(*unit.Generic); ok {
clone := *g.RTPPackets[0]
str.WriteRTPPacket(desc.Medias[0], desc.Medias[0].Formats[0], &clone, time.Time{}, 0)
} else {
str.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(unit.Unit))
}
}
}()
tracks, err := wc.Read(context.Background())
require.NoError(t, err)
defer checkClose(t, wc.Close)
pkt, err := tracks[0].ReadRTP()
require.NoError(t, err)
require.Equal(t, ca.outRTPPayload, pkt.Payload)
})
}
} }
func TestServerReadAuthorizationBearerJWT(t *testing.T) { func TestServerPostNotFound(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}} s := initializeTestServer(t)
str, err := stream.New(
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: str}
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return path, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPost,
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/sdp")
req.Header.Set("Authorization", "Bearer testing")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusCreated, res.StatusCode)
}
func TestServerReadAuthorizationUserPass(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: str}
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPost,
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/sdp")
req.Header.Set("Authorization", "Bearer myuser:mypass")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusCreated, res.StatusCode)
}
func TestServerReadNotFound(t *testing.T) {
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return nil, nil, defs.PathNoOnePublishingError{}
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close() defer s.Close()
tr := &http.Transport{} tr := &http.Transport{}

View file

@ -2,7 +2,6 @@ package webrtc
import ( import (
"context" "context"
"crypto/rand"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@ -15,11 +14,9 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" "github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
"github.com/bluenviron/gortsplib/v4/pkg/rtptime" "github.com/bluenviron/gortsplib/v4/pkg/rtptime"
"github.com/bluenviron/mediacommon/pkg/codecs/g711"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pion/ice/v2" "github.com/pion/ice/v2"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
@ -37,23 +34,10 @@ import (
) )
var errNoSupportedCodecs = errors.New( var errNoSupportedCodecs = errors.New(
"the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711, LPCM") "the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711")
type setupStreamFunc func(*webrtc.OutgoingTrack) error type setupStreamFunc func(*webrtc.OutgoingTrack) error
func uint16Ptr(v uint16) *uint16 {
return &v
}
func randUint32() (uint32, error) {
var b [4]byte
_, err := rand.Read(b[:])
if err != nil {
return 0, err
}
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
}
func findVideoTrack( func findVideoTrack(
stream *stream.Stream, stream *stream.Stream,
writer *asyncwriter.Writer, writer *asyncwriter.Writer,
@ -102,9 +86,8 @@ func findVideoTrack(
if vp9Format != nil { if vp9Format != nil {
return vp9Format, func(track *webrtc.OutgoingTrack) error { return vp9Format, func(track *webrtc.OutgoingTrack) error {
encoder := &rtpvp9.Encoder{ encoder := &rtpvp9.Encoder{
PayloadType: 96, PayloadType: 96,
PayloadMaxSize: webrtcPayloadMaxSize, PayloadMaxSize: webrtcPayloadMaxSize,
InitialPictureID: uint16Ptr(8445),
} }
err := encoder.Init() err := encoder.Init()
if err != nil { if err != nil {
@ -233,6 +216,10 @@ func findAudioTrack(
if opusFormat != nil { if opusFormat != nil {
return opusFormat, func(track *webrtc.OutgoingTrack) error { return opusFormat, func(track *webrtc.OutgoingTrack) error {
if opusFormat.ChannelCount > 2 {
return fmt.Errorf("unsupported Opus channel count: %d", opusFormat.ChannelCount)
}
stream.AddReader(writer, media, opusFormat, func(u unit.Unit) error { stream.AddReader(writer, media, opusFormat, func(u unit.Unit) error {
for _, pkt := range u.GetRTPPackets() { for _, pkt := range u.GetRTPPackets() {
track.WriteRTP(pkt) //nolint:errcheck track.WriteRTP(pkt) //nolint:errcheck
@ -265,115 +252,16 @@ func findAudioTrack(
if g711Format != nil { if g711Format != nil {
return g711Format, func(track *webrtc.OutgoingTrack) error { return g711Format, func(track *webrtc.OutgoingTrack) error {
if g711Format.SampleRate == 8000 { if g711Format.SampleRate != 8000 {
curTimestamp, err := randUint32() return fmt.Errorf("unsupported G711 sample rate")
if err != nil {
return err
}
stream.AddReader(writer, media, g711Format, func(u unit.Unit) error {
for _, pkt := range u.GetRTPPackets() {
// recompute timestamp from scratch.
// Chrome requires a precise timestamp that FFmpeg doesn't provide.
pkt.Timestamp = curTimestamp
curTimestamp += uint32(len(pkt.Payload)) / uint32(g711Format.ChannelCount)
track.WriteRTP(pkt) //nolint:errcheck
}
return nil
})
} else {
encoder := &rtplpcm.Encoder{
PayloadType: 96,
PayloadMaxSize: webrtcPayloadMaxSize,
BitDepth: 16,
ChannelCount: g711Format.ChannelCount,
}
err := encoder.Init()
if err != nil {
return err
}
curTimestamp, err := randUint32()
if err != nil {
return err
}
stream.AddReader(writer, media, g711Format, func(u unit.Unit) error {
tunit := u.(*unit.G711)
if tunit.Samples == nil {
return nil
}
var lpcmSamples []byte
if g711Format.MULaw {
lpcmSamples = g711.DecodeMulaw(tunit.Samples)
} else {
lpcmSamples = g711.DecodeAlaw(tunit.Samples)
}
packets, err := encoder.Encode(lpcmSamples)
if err != nil {
return nil //nolint:nilerr
}
for _, pkt := range packets {
// recompute timestamp from scratch.
// Chrome requires a precise timestamp that FFmpeg doesn't provide.
pkt.Timestamp = curTimestamp
curTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(g711Format.ChannelCount)
track.WriteRTP(pkt) //nolint:errcheck
}
return nil
})
}
return nil
}
}
var lpcmFormat *format.LPCM
media = stream.Desc().FindFormat(&lpcmFormat)
if lpcmFormat != nil {
return lpcmFormat, func(track *webrtc.OutgoingTrack) error {
encoder := &rtplpcm.Encoder{
PayloadType: 96,
BitDepth: 16,
ChannelCount: lpcmFormat.ChannelCount,
PayloadMaxSize: webrtcPayloadMaxSize,
}
err := encoder.Init()
if err != nil {
return err
} }
curTimestamp, err := randUint32() if g711Format.ChannelCount != 1 {
if err != nil { return fmt.Errorf("unsupported G711 channel count")
return err
} }
stream.AddReader(writer, media, lpcmFormat, func(u unit.Unit) error { stream.AddReader(writer, media, g711Format, func(u unit.Unit) error {
tunit := u.(*unit.LPCM) for _, pkt := range u.GetRTPPackets() {
if tunit.Samples == nil {
return nil
}
packets, err := encoder.Encode(tunit.Samples)
if err != nil {
return nil //nolint:nilerr
}
for _, pkt := range packets {
// recompute timestamp from scratch.
// Chrome requires a precise timestamp that FFmpeg doesn't provide.
pkt.Timestamp = curTimestamp
curTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(lpcmFormat.ChannelCount)
track.WriteRTP(pkt) //nolint:errcheck track.WriteRTP(pkt) //nolint:errcheck
} }
@ -521,8 +409,6 @@ func (s *session) runPublish() (int, error) {
pc := &webrtc.PeerConnection{ pc := &webrtc.PeerConnection{
ICEServers: iceServers, ICEServers: iceServers,
HandshakeTimeout: s.parent.HandshakeTimeout,
TrackGatherTimeout: s.parent.TrackGatherTimeout,
IPsFromInterfaces: s.ipsFromInterfaces, IPsFromInterfaces: s.ipsFromInterfaces,
IPsFromInterfacesList: s.ipsFromInterfacesList, IPsFromInterfacesList: s.ipsFromInterfacesList,
AdditionalHosts: s.additionalHosts, AdditionalHosts: s.additionalHosts,
@ -545,7 +431,7 @@ func (s *session) runPublish() (int, error) {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
err = webrtc.TracksAreValid(sdp.MediaDescriptions) trackCount, err := webrtc.TrackCount(sdp.MediaDescriptions)
if err != nil { if err != nil {
// RFC draft-ietf-wish-whip // RFC draft-ietf-wish-whip
// if the number of audio and or video // if the number of audio and or video
@ -573,7 +459,7 @@ func (s *session) runPublish() (int, error) {
s.pc = pc s.pc = pc
s.mutex.Unlock() s.mutex.Unlock()
tracks, err := pc.GatherIncomingTracks(s.ctx) tracks, err := pc.GatherIncomingTracks(s.ctx, trackCount)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -680,8 +566,6 @@ func (s *session) runRead() (int, error) {
pc := &webrtc.PeerConnection{ pc := &webrtc.PeerConnection{
ICEServers: iceServers, ICEServers: iceServers,
HandshakeTimeout: s.parent.HandshakeTimeout,
TrackGatherTimeout: s.parent.TrackGatherTimeout,
IPsFromInterfaces: s.ipsFromInterfaces, IPsFromInterfaces: s.ipsFromInterfaces,
IPsFromInterfacesList: s.ipsFromInterfacesList, IPsFromInterfacesList: s.ipsFromInterfacesList,
AdditionalHosts: s.additionalHosts, AdditionalHosts: s.additionalHosts,
@ -776,7 +660,7 @@ func (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) {
select { select {
case req := <-s.chAddCandidates: case req := <-s.chAddCandidates:
for _, candidate := range req.candidates { for _, candidate := range req.candidates {
err := pc.AddRemoteCandidate(candidate) err := pc.AddRemoteCandidate(*candidate)
if err != nil { if err != nil {
req.res <- webRTCAddSessionCandidatesRes{err: err} req.res <- webRTCAddSessionCandidatesRes{err: err}
} }

View file

@ -20,8 +20,9 @@ import (
// Source is a HLS static source. // Source is a HLS static source.
type Source struct { type Source struct {
ReadTimeout conf.StringDuration ResolvedSource string
Parent defs.StaticSourceParent ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
} }
// Log implements logger.Writer. // Log implements logger.Writer.
@ -48,7 +49,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
var c *gohlslib.Client var c *gohlslib.Client
c = &gohlslib.Client{ c = &gohlslib.Client{
URI: params.ResolvedSource, URI: s.ResolvedSource,
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Timeout: time.Duration(s.ReadTimeout), Timeout: time.Duration(s.ReadTimeout),
Transport: tr, Transport: tr,

View file

@ -63,7 +63,7 @@ func TestSource(t *testing.T) {
err := w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}}) err := w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track1, 2*90000, 2*90000, true, [][]byte{ err = w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{
{7, 1, 2, 3}, // SPS {7, 1, 2, 3}, // SPS
{8}, // PPS {8}, // PPS
}) })
@ -90,10 +90,10 @@ func TestSource(t *testing.T) {
te := test.NewSourceTester( te := test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
Parent: p, ResolvedSource: "http://localhost:5780/stream.m3u8",
Parent: p,
} }
}, },
"http://localhost:5780/stream.m3u8",
&conf.Path{}, &conf.Path{},
) )
defer te.Close() defer te.Close()

View file

@ -23,9 +23,10 @@ import (
// Source is a RTMP static source. // Source is a RTMP static source.
type Source struct { type Source struct {
ReadTimeout conf.StringDuration ResolvedSource string
WriteTimeout conf.StringDuration ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent WriteTimeout conf.StringDuration
Parent defs.StaticSourceParent
} }
// Log implements logger.Writer. // Log implements logger.Writer.
@ -37,7 +38,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
func (s *Source) Run(params defs.StaticSourceRunParams) error { func (s *Source) Run(params defs.StaticSourceRunParams) error {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
u, err := url.Parse(params.ResolvedSource) u, err := url.Parse(s.ResolvedSource)
if err != nil { if err != nil {
return err return err
} }

View file

@ -64,24 +64,24 @@ func TestSource(t *testing.T) {
te = test.NewSourceTester( te = test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ReadTimeout: conf.StringDuration(10 * time.Second), ResolvedSource: "rtmp://localhost/teststream",
WriteTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: p, WriteTimeout: conf.StringDuration(10 * time.Second),
Parent: p,
} }
}, },
"rtmp://localhost/teststream",
&conf.Path{}, &conf.Path{},
) )
} else { } else {
te = test.NewSourceTester( te = test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ReadTimeout: conf.StringDuration(10 * time.Second), ResolvedSource: "rtmps://localhost/teststream",
WriteTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: p, WriteTimeout: conf.StringDuration(10 * time.Second),
Parent: p,
} }
}, },
"rtmps://localhost/teststream",
&conf.Path{ &conf.Path{
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739", SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
}, },

View file

@ -62,6 +62,7 @@ func createRangeHeader(cnf *conf.Path) (*headers.Range, error) {
// Source is a RTSP static source. // Source is a RTSP static source.
type Source struct { type Source struct {
ResolvedSource string
ReadTimeout conf.StringDuration ReadTimeout conf.StringDuration
WriteTimeout conf.StringDuration WriteTimeout conf.StringDuration
WriteQueueSize int WriteQueueSize int
@ -103,7 +104,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
}, },
} }
u, err := base.ParseURL(params.ResolvedSource) u, err := base.ParseURL(s.ResolvedSource)
if err != nil { if err != nil {
return err return err
} }

View file

@ -138,13 +138,13 @@ func TestSource(t *testing.T) {
te = test.NewSourceTester( te = test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ResolvedSource: "rtsp://testuser:testpass@localhost:8555/teststream",
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 2048, WriteQueueSize: 2048,
Parent: p, Parent: p,
} }
}, },
"rtsp://testuser:testpass@localhost:8555/teststream",
&conf.Path{ &conf.Path{
RTSPTransport: sp, RTSPTransport: sp,
}, },
@ -153,13 +153,13 @@ func TestSource(t *testing.T) {
te = test.NewSourceTester( te = test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ResolvedSource: "rtsps://testuser:testpass@localhost:8555/teststream",
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 2048, WriteQueueSize: 2048,
Parent: p, Parent: p,
} }
}, },
"rtsps://testuser:testpass@localhost:8555/teststream",
&conf.Path{ &conf.Path{
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739", SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
}, },
@ -241,13 +241,13 @@ func TestRTSPSourceNoPassword(t *testing.T) {
te := test.NewSourceTester( te := test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ResolvedSource: "rtsp://testuser:@127.0.0.1:8555/teststream",
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 2048, WriteQueueSize: 2048,
Parent: p, Parent: p,
} }
}, },
"rtsp://testuser:@127.0.0.1:8555/teststream",
&conf.Path{ &conf.Path{
RTSPTransport: sp, RTSPTransport: sp,
}, },
@ -338,13 +338,13 @@ func TestRTSPSourceRange(t *testing.T) {
te := test.NewSourceTester( te := test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ResolvedSource: "rtsp://127.0.0.1:8555/teststream",
ReadTimeout: conf.StringDuration(10 * time.Second), ReadTimeout: conf.StringDuration(10 * time.Second),
WriteTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 2048, WriteQueueSize: 2048,
Parent: p, Parent: p,
} }
}, },
"rtsp://127.0.0.1:8555/teststream",
cnf, cnf,
) )
defer te.Close() defer te.Close()

View file

@ -17,8 +17,9 @@ import (
// Source is a SRT static source. // Source is a SRT static source.
type Source struct { type Source struct {
ReadTimeout conf.StringDuration ResolvedSource string
Parent defs.StaticSourceParent ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
} }
// Log implements logger.Writer. // Log implements logger.Writer.
@ -31,7 +32,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
conf := srt.DefaultConfig() conf := srt.DefaultConfig()
address, err := conf.UnmarshalURL(params.ResolvedSource) address, err := conf.UnmarshalURL(s.ResolvedSource)
if err != nil { if err != nil {
return err return err
} }

View file

@ -15,20 +15,21 @@ import (
) )
func TestSource(t *testing.T) { func TestSource(t *testing.T) {
ln, err := srt.Listen("srt", "127.0.0.1:9002", srt.DefaultConfig()) ln, err := srt.Listen("srt", "localhost:9002", srt.DefaultConfig())
require.NoError(t, err) require.NoError(t, err)
defer ln.Close() defer ln.Close()
go func() { go func() {
req, err := ln.Accept2() conn, _, err := ln.Accept(func(req srt.ConnRequest) srt.ConnType {
require.NoError(t, err) require.Equal(t, "sidname", req.StreamId())
err := req.SetPassphrase("ttest1234567")
require.Equal(t, "sidname", req.StreamId()) if err != nil {
err = req.SetPassphrase("ttest1234567") return srt.REJECT
require.NoError(t, err) }
return srt.SUBSCRIBE
conn, err := req.Accept() })
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, conn)
defer conn.Close() defer conn.Close()
track := &mpegts.Track{ track := &mpegts.Track{
@ -39,7 +40,7 @@ func TestSource(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) w := mpegts.NewWriter(bw, []*mpegts.Track{track})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track, 0, 0, true, [][]byte{{ // IDR err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR
5, 1, 5, 1,
}}) }})
require.NoError(t, err) require.NoError(t, err)
@ -54,11 +55,11 @@ func TestSource(t *testing.T) {
te := test.NewSourceTester( te := test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ReadTimeout: conf.StringDuration(10 * time.Second), ResolvedSource: "srt://localhost:9002?streamid=sidname&passphrase=ttest1234567",
Parent: p, ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: p,
} }
}, },
"srt://127.0.0.1:9002?streamid=sidname&passphrase=ttest1234567",
&conf.Path{}, &conf.Path{},
) )
defer te.Close() defer te.Close()

View file

@ -45,8 +45,9 @@ type packetConn interface {
// Source is a UDP static source. // Source is a UDP static source.
type Source struct { type Source struct {
ReadTimeout conf.StringDuration ResolvedSource string
Parent defs.StaticSourceParent ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
} }
// Log implements logger.Writer. // Log implements logger.Writer.
@ -58,7 +59,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
func (s *Source) Run(params defs.StaticSourceRunParams) error { func (s *Source) Run(params defs.StaticSourceRunParams) error {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
hostPort := params.ResolvedSource[len("udp://"):] hostPort := s.ResolvedSource[len("udp://"):]
addr, err := net.ResolveUDPAddr("udp", hostPort) addr, err := net.ResolveUDPAddr("udp", hostPort)
if err != nil { if err != nil {

View file

@ -18,18 +18,18 @@ func TestSource(t *testing.T) {
te := test.NewSourceTester( te := test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ReadTimeout: conf.StringDuration(10 * time.Second), ResolvedSource: "udp://localhost:9001",
Parent: p, ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: p,
} }
}, },
"udp://127.0.0.1:9001",
&conf.Path{}, &conf.Path{},
) )
defer te.Close() defer te.Close()
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
conn, err := net.Dial("udp", "127.0.0.1:9001") conn, err := net.Dial("udp", "localhost:9001")
require.NoError(t, err) require.NoError(t, err)
defer conn.Close() defer conn.Close()
@ -41,12 +41,12 @@ func TestSource(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) w := mpegts.NewWriter(bw, []*mpegts.Track{track})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track, 0, 0, true, [][]byte{{ // IDR err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR
5, 1, 5, 1,
}}) }})
require.NoError(t, err) require.NoError(t, err)
err = w.WriteH264(track, 0, 0, true, [][]byte{{ // non-IDR err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // non-IDR
5, 2, 5, 2,
}}) }})
require.NoError(t, err) require.NoError(t, err)

View file

@ -19,8 +19,9 @@ import (
// Source is a WebRTC static source. // Source is a WebRTC static source.
type Source struct { type Source struct {
ReadTimeout conf.StringDuration ResolvedSource string
Parent defs.StaticSourceParent ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
} }
// Log implements logger.Writer. // Log implements logger.Writer.
@ -32,7 +33,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
func (s *Source) Run(params defs.StaticSourceRunParams) error { func (s *Source) Run(params defs.StaticSourceRunParams) error {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
u, err := url.Parse(params.ResolvedSource) u, err := url.Parse(s.ResolvedSource)
if err != nil { if err != nil {
return err return err
} }

View file

@ -32,13 +32,11 @@ func TestSource(t *testing.T) {
ChannelCount: 2, ChannelCount: 2,
}}} }}}
pc := &webrtc.PeerConnection{ pc := &webrtc.PeerConnection{
LocalRandomUDP: true, LocalRandomUDP: true,
IPsFromInterfaces: true, IPsFromInterfaces: true,
Publish: true, Publish: true,
HandshakeTimeout: conf.StringDuration(10 * time.Second), OutgoingTracks: outgoingTracks,
TrackGatherTimeout: conf.StringDuration(2 * time.Second), Log: test.NilLogger,
OutgoingTracks: outgoingTracks,
Log: test.NilLogger,
} }
err := pc.Start() err := pc.Start()
require.NoError(t, err) require.NoError(t, err)
@ -121,11 +119,11 @@ func TestSource(t *testing.T) {
te := test.NewSourceTester( te := test.NewSourceTester(
func(p defs.StaticSourceParent) defs.StaticSource { func(p defs.StaticSourceParent) defs.StaticSource {
return &Source{ return &Source{
ReadTimeout: conf.StringDuration(10 * time.Second), ResolvedSource: "whep://localhost:9003/my/resource",
Parent: p, ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: p,
} }
}, },
"whep://localhost:9003/my/resource",
&conf.Path{}, &conf.Path{},
) )
defer te.Close() defer te.Close()

View file

@ -24,11 +24,7 @@ type SourceTester struct {
} }
// NewSourceTester allocates a SourceTester. // NewSourceTester allocates a SourceTester.
func NewSourceTester( func NewSourceTester(createFunc func(defs.StaticSourceParent) defs.StaticSource, conf *conf.Path) *SourceTester {
createFunc func(defs.StaticSourceParent) defs.StaticSource,
resolvedSource string,
conf *conf.Path,
) *SourceTester {
ctx, ctxCancel := context.WithCancel(context.Background()) ctx, ctxCancel := context.WithCancel(context.Background())
t := &SourceTester{ t := &SourceTester{
@ -42,9 +38,8 @@ func NewSourceTester(
go func() { go func() {
s.Run(defs.StaticSourceRunParams{ //nolint:errcheck s.Run(defs.StaticSourceRunParams{ //nolint:errcheck
Context: ctx, Context: ctx,
ResolvedSource: resolvedSource, Conf: conf,
Conf: conf,
}) })
close(t.done) close(t.done)
}() }()

View file

@ -117,7 +117,7 @@ authHTTPExclude:
# } # }
# ] # ]
# } # }
# Users are expected to pass the JWT in the Authorization header or as a query parameter. # Users are then expected to pass the JWT as a query parameter, i.e. ?jwt=...
# This is the JWKS URL that will be used to pull (once) the public key that allows # This is the JWKS URL that will be used to pull (once) the public key that allows
# to validate JWTs. # to validate JWTs.
authJWTJWKS: authJWTJWKS:
@ -381,10 +381,6 @@ webrtcICEServers2: []
# username: '' # username: ''
# password: '' # password: ''
# clientOnly: false # clientOnly: false
# Time to wait for the WebRTC handshake to complete.
webrtcHandshakeTimeout: 10s
# Maximum time to gather video tracks.
webrtcTrackGatherTimeout: 2s
############################################### ###############################################
# Global settings -> SRT server # Global settings -> SRT server
@ -418,10 +414,8 @@ pathDefaults:
# * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS # * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS
# * redirect -> the stream is provided by another path or server # * redirect -> the stream is provided by another path or server
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera # * rpiCamera -> the stream is provided by a Raspberry Pi Camera
# The following variables can be used in the source string: # If path name is a regular expression, $G1, G2, etc will be replaced
# * $MTX_QUERY: query parameters (passed by first reader) # with regular expression groups.
# * $G1, $G2, ...: regular expression groups, if path name is
# a regular expression.
source: publisher source: publisher
# If the source is a URL, and the source certificate is self-signed # If the source is a URL, and the source certificate is self-signed
# or invalid, you can provide the fingerprint of the certificate in order to # or invalid, you can provide the fingerprint of the certificate in order to
@ -677,7 +671,6 @@ pathDefaults:
# * G1, G2, ...: regular expression groups, if path name is # * G1, G2, ...: regular expression groups, if path name is
# a regular expression. # a regular expression.
# * MTX_SEGMENT_PATH: segment file path # * MTX_SEGMENT_PATH: segment file path
# * MTX_SEGMENT_DURATION: segment duration
runOnRecordSegmentComplete: runOnRecordSegmentComplete:
############################################### ###############################################

View file

@ -29,36 +29,36 @@ RUN cp mediamtx.yml LICENSE tmp/
RUN go generate ./... RUN go generate ./...
FROM build-base AS build-windows-amd64 FROM build-base AS build-windows-amd64
RUN GOOS=windows GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME).exe RUN GOOS=windows GOARCH=amd64 go build -o tmp/$(BINARY_NAME).exe
RUN cd tmp && zip -q ../binaries/$(BINARY_NAME)_$${VERSION}_windows_amd64.zip $(BINARY_NAME).exe mediamtx.yml LICENSE RUN cd tmp && zip -q ../binaries/$(BINARY_NAME)_$${VERSION}_windows_amd64.zip $(BINARY_NAME).exe mediamtx.yml LICENSE
FROM build-base AS build-linux-amd64 FROM build-base AS build-linux-amd64
RUN GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN GOOS=linux GOARCH=amd64 go build -o tmp/$(BINARY_NAME)
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
FROM build-base AS build-darwin-amd64 FROM build-base AS build-darwin-amd64
RUN GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN GOOS=darwin GOARCH=amd64 go build -o tmp/$(BINARY_NAME)
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
FROM build-base AS build-darwin-arm64 FROM build-base AS build-darwin-arm64
RUN GOOS=darwin GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) RUN GOOS=darwin GOARCH=arm64 go build -o tmp/$(BINARY_NAME)
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_arm64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_arm64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
FROM build-base AS build-linux-armv6 FROM build-base AS build-linux-armv6
COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
RUN GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera RUN GOOS=linux GOARCH=arm GOARM=6 go build -o tmp/$(BINARY_NAME) -tags rpicamera
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv6.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv6.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
RUN rm internal/protocols/rpicamera/exe/exe RUN rm internal/protocols/rpicamera/exe/exe
FROM build-base AS build-linux-armv7 FROM build-base AS build-linux-armv7
COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
RUN GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera RUN GOOS=linux GOARCH=arm GOARM=7 go build -o tmp/$(BINARY_NAME) -tags rpicamera
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv7.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv7.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
RUN rm internal/protocols/rpicamera/exe/exe RUN rm internal/protocols/rpicamera/exe/exe
FROM build-base AS build-linux-arm64 FROM build-base AS build-linux-arm64
COPY --from=rpicamera64 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ COPY --from=rpicamera64 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
RUN GOOS=linux GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera RUN GOOS=linux GOARCH=arm64 go build -o tmp/$(BINARY_NAME) -tags rpicamera
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_arm64v8.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_arm64v8.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
RUN rm internal/protocols/rpicamera/exe/exe RUN rm internal/protocols/rpicamera/exe/exe
@ -75,7 +75,6 @@ export DOCKERFILE_BINARIES
binaries: binaries:
echo "$$DOCKERFILE_BINARIES" | DOCKER_BUILDKIT=1 docker build . -f - \ echo "$$DOCKERFILE_BINARIES" | DOCKER_BUILDKIT=1 docker build . -f - \
--build-arg VERSION=$$(git describe --tags) \
-t temp -t temp
docker run --rm -v $(PWD):/out \ docker run --rm -v $(PWD):/out \
temp sh -c "rm -rf /out/binaries && cp -r /s/binaries /out/" temp sh -c "rm -rf /out/binaries && cp -r /s/binaries /out/"

View file

@ -1,5 +1,5 @@
lint: lint:
touch internal/servers/hls/hls.min.js go generate ./...
docker run --rm -v $(PWD):/app -w /app \ docker run --rm -v $(PWD):/app -w /app \
$(LINT_IMAGE) \ $(LINT_IMAGE) \
golangci-lint run -v golangci-lint run -v