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
/coverage*.txt
/apidocs/*.html
/internal/core/version.go
/internal/servers/hls/hls.min.js
/internal/protocols/rpicamera/exe/text_font.h
/internal/protocols/rpicamera/exe/exe

View file

@ -19,26 +19,23 @@ jobs:
&& git config user.email bot@mediamtx
&& ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs)
- run: |
set -e
- run: >
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 $HASH > internal/servers/hls/hlsjsdownloader/HASH
echo VERSION=$VERSION >> $GITHUB_ENV
&& echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION
&& echo VERSION=$VERSION >> $GITHUB_ENV
- id: check_repo
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: >
git reset ${GITHUB_REF_NAME}
&& git add .
&& git commit -m "bump hls.js to ${VERSION}"
&& 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
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View file

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

View file

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- run: make test
@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- run: make test32
@ -31,10 +31,15 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-go@v2
with:
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

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
/binaries
/coverage*.txt
/apidocs/*.html
/internal/core/version.go
/internal/servers/hls/hls.min.js
/internal/protocols/rpicamera/exe/text_font.h
/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 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 servers](#webrtc-servers)|WHEP|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, 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 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|
@ -134,8 +134,7 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
* [SRT-specific features](#srt-specific-features)
* [Standard stream ID syntax](#standard-stream-id-syntax)
* [WebRTC-specific features](#webrtc-specific-features)
* [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep)
* [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues)
* [Connectivity issues](#connectivity-issues)
* [RTSP-specific features](#rtsp-specific-features)
* [Transport protocols](#transport-protocols)
* [Encryption](#encryption)
@ -339,7 +338,6 @@ Latest versions of OBS Studio can publish to the server with the [WebRTC / WHIP
* Service: `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`.
@ -612,9 +610,7 @@ WHIP is a WebRTC extensions that allows to publish streams by using a URL, witho
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, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).
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.
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
```
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, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).
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.
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
```
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:
1. Start Keycloak:
@ -1683,7 +1669,6 @@ pathDefaults:
# * G1, G2, ...: regular expression groups, if path name is
# a regular expression.
# * 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
```
@ -1844,35 +1829,7 @@ Where:
### WebRTC-specific features
#### Authenticating with WHIP/WHEP
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
#### 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.

View file

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

32
go.mod
View file

@ -8,19 +8,19 @@ require (
github.com/MicahParks/keyfunc/v3 v3.3.3
github.com/abema/go-mp4 v1.2.0
github.com/alecthomas/kong v0.9.0
github.com/bluenviron/gohlslib v1.4.0
github.com/bluenviron/gortsplib/v4 v4.10.2
github.com/bluenviron/mediacommon v1.12.1
github.com/datarhei/gosrt v0.7.0
github.com/bluenviron/gohlslib v1.3.3
github.com/bluenviron/gortsplib/v4 v4.10.0
github.com/bluenviron/mediacommon v1.11.0
github.com/datarhei/gosrt v0.6.0
github.com/fsnotify/fsnotify v1.7.0
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
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/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/logging v0.2.2
github.com/pion/rtcp v1.2.14
@ -28,9 +28,9 @@ require (
github.com/pion/sdp/v3 v3.0.9
github.com/pion/webrtc/v3 v3.2.22
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.25.0
golang.org/x/sys v0.22.0
golang.org/x/term v0.22.0
golang.org/x/crypto v0.23.0
golang.org/x/sys v0.20.0
golang.org/x/term v0.20.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/pion/datachannel v1.5.5 // 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/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/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/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.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 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/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
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-20240608212222-2eebc68350c9/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 h1:WtKhXOpd8lgTeXF3RQVOzkNRuy83ygvWEpMYD2aoY3Q=
github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 h1:fD6eZt+3/t8bzFn6ZZA2eP63xBP06v3EPfPJu8DO8ys=
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-20231112223655-e402ed2689c6 h1:wMd3D1mLghoYYh31STig8Kwm2qi8QyQKUy09qUUZrVw=
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/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
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/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/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E=
github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo=
github.com/bluenviron/gortsplib/v4 v4.10.2 h1:O7HPRG8Pv4zUbyYD0HYH4Ufu1Hg9FJGTlizx6a09hL0=
github.com/bluenviron/gortsplib/v4 v4.10.2/go.mod h1:re/L/vYh2wLPElQNAYah+bRFHJs0aRkM1MLX3WJ3N6M=
github.com/bluenviron/mediacommon v1.12.1 h1:sgDJaKV6OXrPCSO0KPp9zi/pwNWtKHenn5/dvjtY+Tg=
github.com/bluenviron/mediacommon v1.12.1/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec=
github.com/bluenviron/gohlslib v1.3.3 h1:Ji4PW9QHHCbpBteZCKk+rGY6emFNSGVFMsAa/3xFChk=
github.com/bluenviron/gohlslib v1.3.3/go.mod h1:MQcRjI9fYBNb9QhZO3RydgtbfCRhjogj6YMrpCDuTvY=
github.com/bluenviron/gortsplib/v4 v4.10.0 h1:9vJsUDuBgSinm41CR6yWnSMZ7TRWeB/oiAuN4lo30bU=
github.com/bluenviron/gortsplib/v4 v4.10.0/go.mod h1:iLJ1tmwGMbaN04ZYh/KRlAHsCbz9Rycn7cPAvdR+Vkc=
github.com/bluenviron/mediacommon v1.11.0 h1:1xY4QGYz7da9tsV2Xvd+ol+Ul5qq2g7ADJtIlVkQSRI=
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/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
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/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
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.7.0/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE=
github.com/datarhei/gosrt v0.6.0 h1:HrrXAw90V78ok4WMIhX6se1aTHPCn82Sg2hj+PhdmGc=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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.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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
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.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
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/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
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/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/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/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/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.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
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/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.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
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.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/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.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U=
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/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
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/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/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.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.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.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
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/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
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/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
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-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.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.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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-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.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
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.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
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-20190423024810-112230192c58/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.1.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.6.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.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.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
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-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.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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
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.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
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.3/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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
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/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -10,6 +10,7 @@ import (
"os"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
@ -35,6 +36,58 @@ func interfaceIsEmpty(i interface{}) bool {
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 {
ret := make([]string, len(paths))
i := 0
@ -137,7 +190,6 @@ func (a *API) Initialize() error {
router := gin.New()
router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(a.middlewareOrigin, a.middlewareAuth)
group := router.Group("/", a.middlewareOrigin, a.middlewareAuth)
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) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin)
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) {

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)
}
func TestPreflightRequest(t *testing.T) {
api := API{
Address: "localhost:9997",
AllowOrigin: "*",
ReadTimeout: conf.StringDuration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: &testParent{},
func TestPaginate(t *testing.T) {
items := make([]int, 5)
for i := 0; i < 5; i++ {
items[i] = i
}
err := api.Initialize()
pageCount, err := paginate(&items, "1", "1")
require.NoError(t, err)
defer api.Close()
require.Equal(t, 5, pageCount)
require.Equal(t, []int{1}, items)
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
items = make([]int, 5)
for i := 0; i < 5; i++ {
items[i] = i
}
req, err := http.NewRequest(http.MethodOptions, "http://localhost:9997", nil)
pageCount, err = paginate(&items, "3", "2")
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)
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, 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{})
require.Equal(t, 2, pageCount)
require.Equal(t, []int{4, 5}, items)
}
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",
map[string]interface{}{
"recordFormat": "fmp4",
"readUser": "myuser",
"readPass": "mypass",
}, nil)
time.Sleep(500 * time.Millisecond)
var out map[string]interface{}
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) {

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

View file

@ -192,66 +192,6 @@ func TestConfEncryption(t *testing.T) {
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) {
for _, ca := range []struct {
name string

View file

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

View file

@ -546,7 +546,7 @@ func TestAPIProtocolListGet(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
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)
err = bw.Flush()
@ -1021,7 +1021,7 @@ func TestAPIProtocolKick(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
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)
err = bw.Flush()

View file

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

View file

@ -218,7 +218,7 @@ webrtc_sessions_bytes_sent 0
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
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.PPS,
{0x05, 1}, // IDR

View file

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

View file

@ -105,12 +105,12 @@ func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Respo
var _ defs.Path = &path{}
func TestPathRunOnDemand(t *testing.T) {
onDemand := filepath.Join(os.TempDir(), "on_demand")
onUnDemand := filepath.Join(os.TempDir(), "on_undemand")
onDemandFile := filepath.Join(os.TempDir(), "ondemand")
onUnDemandFile := filepath.Join(os.TempDir(), "onundemand")
srcFile := filepath.Join(os.TempDir(), "ondemand.go")
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)
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"} {
t.Run(ca, func(t *testing.T) {
defer os.Remove(onDemand)
defer os.Remove(onUnDemand)
defer os.Remove(onDemandFile)
defer os.Remove(onUnDemandFile)
p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
"hls: no\n"+
@ -135,7 +135,7 @@ func TestPathRunOnDemand(t *testing.T) {
" '~^(on)demand$':\n"+
" runOnDemand: %s\n"+
" runOnDemandCloseAfter: 1s\n"+
" runOnUnDemand: touch %s\n", execFile, onUnDemand))
" runOnUnDemand: touch %s\n", execFile, onUnDemandFile))
require.Equal(t, true, ok)
defer p1.Close()
@ -204,14 +204,14 @@ func TestPathRunOnDemand(t *testing.T) {
}()
for {
_, err := os.Stat(onUnDemand)
_, err := os.Stat(onUnDemandFile)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
_, err := os.Stat(onDemand)
_, err := os.Stat(onDemandFile)
require.NoError(t, err)
})
}
@ -220,11 +220,11 @@ func TestPathRunOnDemand(t *testing.T) {
func TestPathRunOnConnect(t *testing.T) {
for _, ca := range []string{"rtsp", "rtmp", "srt"} {
t.Run(ca, func(t *testing.T) {
onConnect := filepath.Join(os.TempDir(), "on_connect")
defer os.Remove(onConnect)
onConnectFile := filepath.Join(os.TempDir(), "onconnect")
defer os.Remove(onConnectFile)
onDisconnect := filepath.Join(os.TempDir(), "on_disconnect")
defer os.Remove(onDisconnect)
onDisconnectFile := filepath.Join(os.TempDir(), "ondisconnect")
defer os.Remove(onDisconnectFile)
func() {
p, ok := newInstance(fmt.Sprintf(
@ -232,7 +232,7 @@ func TestPathRunOnConnect(t *testing.T) {
" test:\n"+
"runOnConnect: touch %s\n"+
"runOnDisconnect: touch %s\n",
onConnect, onDisconnect))
onConnectFile, onDisconnectFile))
require.Equal(t, true, ok)
defer p.Close()
@ -273,21 +273,21 @@ func TestPathRunOnConnect(t *testing.T) {
time.Sleep(500 * time.Millisecond)
}()
_, err := os.Stat(onConnect)
_, err := os.Stat(onConnectFile)
require.NoError(t, err)
_, err = os.Stat(onDisconnect)
_, err = os.Stat(onDisconnectFile)
require.NoError(t, err)
})
}
}
func TestPathRunOnReady(t *testing.T) {
onReady := filepath.Join(os.TempDir(), "on_ready")
defer os.Remove(onReady)
onReadyFile := filepath.Join(os.TempDir(), "onready")
defer os.Remove(onReadyFile)
onNotReady := filepath.Join(os.TempDir(), "on_unready")
defer os.Remove(onNotReady)
onNotReadyFile := filepath.Join(os.TempDir(), "onunready")
defer os.Remove(onNotReadyFile)
func() {
p, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
@ -297,7 +297,7 @@ func TestPathRunOnReady(t *testing.T) {
" test:\n"+
" runOnReady: 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)
defer p.Close()
@ -312,11 +312,11 @@ func TestPathRunOnReady(t *testing.T) {
time.Sleep(500 * time.Millisecond)
}()
byts, err := os.ReadFile(onReady)
byts, err := os.ReadFile(onReadyFile)
require.NoError(t, err)
require.Equal(t, "test query=value\n", string(byts))
byts, err = os.ReadFile(onNotReady)
byts, err = os.ReadFile(onNotReadyFile)
require.NoError(t, err)
require.Equal(t, "test query=value\n", string(byts))
}
@ -324,11 +324,11 @@ func TestPathRunOnReady(t *testing.T) {
func TestPathRunOnRead(t *testing.T) {
for _, ca := range []string{"rtsp", "rtmp", "srt", "webrtc"} {
t.Run(ca, func(t *testing.T) {
onRead := filepath.Join(os.TempDir(), "on_read")
defer os.Remove(onRead)
onReadFile := filepath.Join(os.TempDir(), "onread")
defer os.Remove(onReadFile)
onUnread := filepath.Join(os.TempDir(), "on_unread")
defer os.Remove(onUnread)
onUnreadFile := filepath.Join(os.TempDir(), "onunread")
defer os.Remove(onUnreadFile)
func() {
p, ok := newInstance(fmt.Sprintf(
@ -336,7 +336,7 @@ func TestPathRunOnRead(t *testing.T) {
" test:\n"+
" runOnRead: 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)
defer p.Close()
@ -449,79 +449,17 @@ func TestPathRunOnRead(t *testing.T) {
time.Sleep(500 * time.Millisecond)
}()
byts, err := os.ReadFile(onRead)
byts, err := os.ReadFile(onReadFile)
require.NoError(t, err)
require.Equal(t, "test query=value\n", string(byts))
byts, err = os.ReadFile(onUnread)
byts, err = os.ReadFile(onUnreadFile)
require.NoError(t, err)
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) {
p, ok := newInstance("paths:\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
s := gortsplib.Server{
Handler: &testServer{
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
) (*base.Response, *gortsplib.ServerStream, error) {
require.Equal(t, "key=val", ctx.Query)
require.Equal(t, "/a", ctx.Path)
return &base.Response{
StatusCode: base.StatusOK,
@ -737,7 +674,7 @@ func TestPathResolveSource(t *testing.T) {
p, ok := newInstance(
"paths:\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" +
" 'all':\n")
require.Equal(t, true, ok)
@ -745,7 +682,7 @@ func TestPathResolveSource(t *testing.T) {
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)
err = reader.Start(u.Scheme, u.Host)

View file

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

View file

@ -20,7 +20,7 @@ func newGeneric(
generateRTPPackets bool,
) (*formatProcessorGeneric, error) {
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{

View file

@ -1,13 +1,10 @@
package hooks
import (
"bytes"
"encoding/json"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"net/http"
)
// 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() {
if onReadyCmd != nil {
onReadyCmd.Close()
@ -69,15 +56,5 @@ func OnReady(params OnReadyParams) func() {
env,
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-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 {
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 {
router := gin.New()
router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(s.middlewareOrigin)
group := router.Group("/", s.middlewareOrigin)
group.GET("/list", s.onList)
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) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin)
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 {

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-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()
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))
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 {
return err
}
@ -102,7 +102,7 @@ func FromStream(
}
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 {
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{
Base: unit.Base{
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{
Base: unit.Base{
NTP: time.Now(),

View file

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

View file

@ -5,7 +5,6 @@ import (
"strings"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
"github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer"
@ -20,50 +19,13 @@ const (
keyFrameInterval = 2 * time.Second
)
const (
mimeTypeMultiopus = "audio/multiopus"
mimeTypeL16 = "audio/L16"
)
var incomingVideoCodecs = []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
SDPFmtpLine: "profile=1",
},
PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
},
PayloadType: 97,
},
{
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,
PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
@ -71,21 +33,22 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
ClockRate: 90000,
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{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
PayloadType: 102,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
},
PayloadType: 103,
PayloadType: 99,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
@ -93,7 +56,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
},
PayloadType: 104,
PayloadType: 100,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
@ -101,65 +64,11 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
},
PayloadType: 105,
PayloadType: 101,
},
}
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{
MimeType: webrtc.MimeTypeOpus,
@ -176,22 +85,6 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
},
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{
MimeType: webrtc.MimeTypePCMU,
@ -206,30 +99,6 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
},
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.
@ -237,7 +106,6 @@ type IncomingTrack struct {
track *webrtc.TrackRemote
log logger.Writer
typ description.MediaType
format format.Format
reorderer *rtpreorderer.Reorderer
pkts []*rtp.Packet
@ -255,47 +123,35 @@ func newIncomingTrack(
reorderer: rtpreorderer.New(),
}
isVideo := false
switch strings.ToLower(track.Codec().MimeType) {
case strings.ToLower(webrtc.MimeTypeAV1):
t.typ = description.MediaTypeVideo
isVideo = true
t.format = &format.AV1{
PayloadTyp: uint8(track.PayloadType()),
}
case strings.ToLower(webrtc.MimeTypeVP9):
t.typ = description.MediaTypeVideo
isVideo = true
t.format = &format.VP9{
PayloadTyp: uint8(track.PayloadType()),
}
case strings.ToLower(webrtc.MimeTypeVP8):
t.typ = description.MediaTypeVideo
isVideo = true
t.format = &format.VP8{
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):
t.typ = description.MediaTypeVideo
isVideo = true
t.format = &format.H264{
PayloadTyp: uint8(track.PayloadType()),
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):
t.typ = description.MediaTypeAudio
t.format = &format.Opus{
PayloadTyp: uint8(track.PayloadType()),
ChannelCount: func() int {
@ -307,60 +163,26 @@ func newIncomingTrack(
}
case strings.ToLower(webrtc.MimeTypeG722):
t.typ = description.MediaTypeAudio
t.format = &format.G722{}
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{
PayloadTyp: payloadType,
PayloadTyp: 0,
MULaw: true,
SampleRate: 8000,
ChannelCount: int(channels),
ChannelCount: 1,
}
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{
PayloadTyp: payloadType,
PayloadTyp: 8,
MULaw: false,
SampleRate: 8000,
ChannelCount: int(channels),
}
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),
ChannelCount: 1,
}
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
@ -375,7 +197,7 @@ func newIncomingTrack(
}()
// send period key frame requests
if t.typ == description.MediaTypeVideo {
if isVideo {
go func() {
keyframeTicker := time.NewTicker(keyFrameInterval)
defer keyframeTicker.Stop()

View file

@ -8,15 +8,6 @@ import (
"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
type OutgoingTrack struct {
Format format.Format
@ -40,9 +31,9 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=0",
SDPFmtpLine: "profile-id=1",
},
PayloadType: 96,
PayloadType: 98,
}, nil
case *format.VP8:
@ -51,16 +42,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
PayloadType: 96,
}, nil
case *format.H265:
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
},
PayloadType: 96,
PayloadType: 99,
}, nil
case *format.H264:
@ -70,42 +52,18 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
},
PayloadType: 96,
PayloadType: 101,
}, nil
case *format.Opus:
switch forma.ChannelCount {
case 1, 2:
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: func() string {
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)
}
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
},
PayloadType: 111,
}, nil
case *format.G722:
return webrtc.RTPCodecParameters{
@ -117,91 +75,22 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
}, nil
case *format.G711:
// 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)
}
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
}
if forma.MULaw {
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA,
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
},
PayloadType: 8,
PayloadType: 0,
}, nil
}
return webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mimeTypeL16,
ClockRate: uint32(forma.ClockRate()),
Channels: uint16(forma.ChannelCount),
MimeType: webrtc.MimeTypePCMA,
ClockRate: 8000,
},
PayloadType: 96,
}, 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,
PayloadType: 8,
}, nil
default:
@ -214,7 +103,6 @@ func (t *OutgoingTrack) isVideo() bool {
case *format.AV1,
*format.VP9,
*format.VP8,
*format.H265,
*format.H264:
return true
}

View file

@ -2,7 +2,6 @@ package webrtc
import (
"context"
"errors"
"fmt"
"strconv"
"sync"
@ -10,15 +9,15 @@ import (
"github.com/pion/ice/v2"
"github.com/pion/interceptor"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
)
const (
webrtcStreamID = "mediamtx"
webrtcHandshakeTimeout = 10 * time.Second
webrtcTrackGatherTimeout = 2 * time.Second
webrtcStreamID = "mediamtx"
)
func stringInSlice(a string, list []string) bool {
@ -30,37 +29,6 @@ func stringInSlice(a string, list []string) bool {
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 {
track *webrtc.TrackRemote
receiver *webrtc.RTPReceiver
@ -71,8 +39,6 @@ type PeerConnection struct {
ICEServers []webrtc.ICEServer
ICEUDPMux ice.UDPMux
ICETCPMux ice.TCPMux
HandshakeTimeout conf.StringDuration
TrackGatherTimeout conf.StringDuration
LocalRandomUDP bool
IPsFromInterfaces bool
IPsFromInterfacesList []string
@ -128,9 +94,6 @@ func (co *PeerConnection) Start() error {
mediaEngine := &webrtc.MediaEngine{}
if co.Publish {
videoSetupped := false
audioSetupped := false
for _, tr := range co.OutgoingTracks {
params, err := tr.codecParameters()
if err != nil {
@ -140,10 +103,8 @@ func (co *PeerConnection) Start() error {
var codecType webrtc.RTPCodecType
if tr.isVideo() {
codecType = webrtc.RTPCodecTypeVideo
videoSetupped = true
} else {
codecType = webrtc.RTPCodecTypeAudio
audioSetupped = true
}
err = mediaEngine.RegisterCodec(params, codecType)
@ -151,33 +112,6 @@ func (co *PeerConnection) Start() error {
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 {
for _, codec := range incomingVideoCodecs {
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)
@ -326,8 +260,8 @@ func (co *PeerConnection) SetAnswer(answer *webrtc.SessionDescription) error {
}
// AddRemoteCandidate adds a remote candidate.
func (co *PeerConnection) AddRemoteCandidate(candidate *webrtc.ICECandidateInit) error {
return co.wr.AddICECandidate(*candidate)
func (co *PeerConnection) AddRemoteCandidate(candidate webrtc.ICECandidateInit) error {
return co.wr.AddICECandidate(candidate)
}
// CreateFullAnswer creates a full answer.
@ -342,8 +276,8 @@ func (co *PeerConnection) CreateFullAnswer(
answer, err := co.wr.CreateAnswer(nil)
if err != nil {
if errors.Is(err, webrtc.ErrSenderWithNoCodecs) {
return nil, fmt.Errorf("codecs not supported by client")
if err.Error() == "unable to populate media section, RTPSender created with no codecs" {
return nil, fmt.Errorf("track codecs are not supported by remote")
}
return nil, err
}
@ -353,7 +287,7 @@ func (co *PeerConnection) CreateFullAnswer(
return nil, err
}
err = co.waitGatheringDone(ctx)
err = co.WaitGatheringDone(ctx)
if err != nil {
return nil, err
}
@ -361,7 +295,8 @@ func (co *PeerConnection) CreateFullAnswer(
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 {
select {
case <-co.NewLocalCandidate():
@ -377,7 +312,7 @@ func (co *PeerConnection) waitGatheringDone(ctx context.Context) error {
func (co *PeerConnection) WaitUntilConnected(
ctx context.Context,
) error {
t := time.NewTimer(time.Duration(co.HandshakeTimeout))
t := time.NewTimer(webrtcHandshakeTimeout)
defer t.Stop()
outer:
@ -398,21 +333,19 @@ outer:
}
// GatherIncomingTracks gathers incoming tracks.
func (co *PeerConnection) GatherIncomingTracks(ctx context.Context) ([]*IncomingTrack, error) {
var sdp sdp.SessionDescription
sdp.Unmarshal([]byte(co.wr.RemoteDescription().SDP)) //nolint:errcheck
maxTrackCount := len(sdp.MediaDescriptions)
func (co *PeerConnection) GatherIncomingTracks(
ctx context.Context,
maxCount int,
) ([]*IncomingTrack, error) {
var tracks []*IncomingTrack
t := time.NewTimer(time.Duration(co.TrackGatherTimeout))
t := time.NewTimer(webrtcTrackGatherTimeout)
defer t.Stop()
for {
select {
case <-t.C:
if len(tracks) != 0 {
if maxCount == 0 && len(tracks) != 0 {
return tracks, nil
}
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)
if len(tracks) >= maxTrackCount {
if len(tracks) == maxCount || len(tracks) >= 2 {
return tracks, nil
}

View file

@ -1,31 +1,22 @@
package webrtc
import (
"context"
"testing"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/conf"
"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"
)
func TestPeerConnectionCloseImmediately(t *testing.T) {
func TestPeerConnectionCloseAfterError(t *testing.T) {
pc := &PeerConnection{
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
LocalRandomUDP: true,
IPsFromInterfaces: true,
Publish: false,
Log: test.NilLogger,
}
err := pc.Start()
require.NoError(t, err)
defer pc.Close()
_, err = pc.CreatePartialOffer()
require.NoError(t, err)
@ -35,454 +26,3 @@ func TestPeerConnectionCloseImmediately(t *testing.T) {
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))
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{
Type: track.typ,
Formats: []format.Format{track.format},
Type: mediaType,
Formats: []format.Format{forma},
}
}

View file

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

View file

@ -8,12 +8,6 @@ import (
"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.
type Agent struct {
WriteQueueSize int
@ -23,8 +17,8 @@ type Agent struct {
SegmentDuration time.Duration
PathName string
Stream *stream.Stream
OnSegmentCreate OnSegmentCreateFunc
OnSegmentComplete OnSegmentCompleteFunc
OnSegmentCreate OnSegmentFunc
OnSegmentComplete OnSegmentFunc
Parent logger.Writer
restartPause time.Duration
@ -42,7 +36,7 @@ func (w *Agent) Initialize() {
}
}
if w.OnSegmentComplete == nil {
w.OnSegmentComplete = func(string, time.Duration) {
w.OnSegmentComplete = func(string) {
}
}
if w.restartPause == 0 {

View file

@ -11,6 +11,9 @@ import (
"github.com/bluenviron/mediamtx/internal/logger"
)
// OnSegmentFunc is the prototype of the function passed as runOnSegmentStart / runOnSegmentComplete
type OnSegmentFunc = func(string)
type sample struct {
*fmp4.PartSample
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) {
for i := 0; i < 2; i++ {
pts := startDTS + time.Duration(i)*100*time.Millisecond
ntp := startNTP.Add(time.Duration(i*60) * time.Second)
writeToStream := func(stream *stream.Stream, ntp time.Time) {
for i := 0; i < 3; i++ {
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
PTS: pts,
NTP: ntp,
PTS: (50 + time.Duration(i)) * time.Second,
NTP: ntp.Add(time.Duration(i) * 60 * time.Second),
},
AU: [][]byte{
test.FormatH264.SPS,
@ -87,7 +84,7 @@ func TestAgent(t *testing.T) {
stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H265{
Base: unit.Base{
PTS: pts,
PTS: (50 + time.Duration(i)) * time.Second,
},
AU: [][]byte{
test.FormatH265.VPS,
@ -99,21 +96,21 @@ func TestAgent(t *testing.T) {
stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{
Base: unit.Base{
PTS: pts,
PTS: (50 + time.Duration(i)) * time.Second,
},
AUs: [][]byte{{1, 2, 3, 4}},
})
stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{
Base: unit.Base{
PTS: pts,
PTS: (50 + time.Duration(i)) * time.Second,
},
Samples: []byte{1, 2, 3, 4},
})
stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.LPCM{
Base: unit.Base{
PTS: pts,
PTS: (50 + time.Duration(i)) * time.Second,
},
Samples: []byte{1, 2, 3, 4},
})
@ -147,15 +144,6 @@ func TestAgent(t *testing.T) {
f = conf.RecordFormatMPEGTS
}
var ext string
if ca == "fmp4" {
ext = "mp4"
} else {
ext = "ts"
}
n := 0
w := &Agent{
WriteQueueSize: 1024,
PathFormat: recordPath,
@ -164,30 +152,10 @@ func TestAgent(t *testing.T) {
SegmentDuration: 1 * time.Second,
PathName: "mypath",
Stream: stream,
OnSegmentCreate: func(segPath 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)
}
OnSegmentCreate: func(_ string) {
segCreated <- struct{}{}
},
OnSegmentComplete: func(segPath string, du time.Duration) {
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++
OnSegmentComplete: func(_ string) {
segDone <- struct{}{}
},
Parent: test.NilLogger,
@ -195,13 +163,7 @@ func TestAgent(t *testing.T) {
}
w.Initialize()
writeToStream(stream,
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))
writeToStream(stream, time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC))
// simulate a write error
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
@ -218,68 +180,74 @@ func TestAgent(t *testing.T) {
<-segDone
}
var ext string
if ca == "fmp4" {
var init fmp4.Init
ext = "mp4"
} else {
ext = "ts"
}
if ca == "fmp4" {
func() {
f, err2 := os.Open(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext))
require.NoError(t, err2)
defer f.Close()
var init fmp4.Init
err2 = init.Unmarshal(f)
require.NoError(t, err2)
}()
require.Equal(t, fmp4.Init{
Tracks: []*fmp4.InitTrack{
{
ID: 1,
TimeScale: 90000,
Codec: &fmp4.CodecH264{
SPS: test.FormatH264.SPS,
PPS: test.FormatH264.PPS,
require.Equal(t, fmp4.Init{
Tracks: []*fmp4.InitTrack{
{
ID: 1,
TimeScale: 90000,
Codec: &fmp4.CodecH264{
SPS: test.FormatH264.SPS,
PPS: test.FormatH264.PPS,
},
},
},
{
ID: 2,
TimeScale: 90000,
Codec: &fmp4.CodecH265{
VPS: test.FormatH265.VPS,
SPS: test.FormatH265.SPS,
PPS: test.FormatH265.PPS,
{
ID: 2,
TimeScale: 90000,
Codec: &fmp4.CodecH265{
VPS: test.FormatH265.VPS,
SPS: test.FormatH265.SPS,
PPS: test.FormatH265.PPS,
},
},
},
{
ID: 3,
TimeScale: 44100,
Codec: &fmp4.CodecMPEG4Audio{
Config: mpeg4audio.Config{
Type: 2,
{
ID: 3,
TimeScale: 44100,
Codec: &fmp4.CodecMPEG4Audio{
Config: mpeg4audio.Config{
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,
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,
ChannelCount: 2,
},
},
},
}, init)
}, init)
}()
_, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext))
require.NoError(t, err)
@ -293,19 +261,17 @@ func TestAgent(t *testing.T) {
time.Sleep(50 * time.Millisecond)
writeToStream(stream,
300*time.Second,
time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC))
writeToStream(stream, time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC))
time.Sleep(50 * time.Millisecond)
w.Close()
<-segCreated
<-segDone
_, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext))
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 track.write(&sample{
return track.record(&sample{
PartSample: sampl,
dts: tunit.PTS,
ntp: tunit.NTP,
@ -261,7 +261,7 @@ func (f *formatFMP4) initialize() {
firstReceived = true
}
return track.write(&sample{
return track.record(&sample{
PartSample: &fmp4.PartSample{
IsNonSyncSample: !randomAccess,
Payload: tunit.Frame,
@ -364,7 +364,7 @@ func (f *formatFMP4) initialize() {
return err
}
return track.write(&sample{
return track.record(&sample{
PartSample: sampl,
dts: dts,
ntp: tunit.NTP,
@ -435,7 +435,7 @@ func (f *formatFMP4) initialize() {
return err
}
return track.write(&sample{
return track.record(&sample{
PartSample: sampl,
dts: dts,
ntp: tunit.NTP,
@ -494,7 +494,7 @@ func (f *formatFMP4) initialize() {
}
lastPTS = tunit.PTS
return track.write(&sample{
return track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: tunit.Frame,
IsNonSyncSample: !randomAccess,
@ -547,7 +547,7 @@ func (f *formatFMP4) initialize() {
}
lastPTS = tunit.PTS
return track.write(&sample{
return track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: tunit.Frame,
IsNonSyncSample: !randomAccess,
@ -583,7 +583,7 @@ func (f *formatFMP4) initialize() {
updateCodecs()
}
return track.write(&sample{
return track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: tunit.Frame,
},
@ -607,7 +607,7 @@ func (f *formatFMP4) initialize() {
var dt time.Duration
for _, packet := range tunit.Packets {
err := track.write(&sample{
err := track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: packet,
},
@ -642,7 +642,7 @@ func (f *formatFMP4) initialize() {
dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit *
time.Second / sampleRate
err := track.write(&sample{
err := track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: au,
},
@ -688,7 +688,7 @@ func (f *formatFMP4) initialize() {
updateCodecs()
}
err = track.write(&sample{
err = track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: frame,
},
@ -756,7 +756,7 @@ func (f *formatFMP4) initialize() {
dt := time.Duration(i) * time.Duration(ac3.SamplesPerFrame) *
time.Second / time.Duration(codec.SampleRate)
err = track.write(&sample{
err = track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: frame,
},
@ -796,7 +796,7 @@ func (f *formatFMP4) initialize() {
out = g711.DecodeAlaw(tunit.Samples)
}
return track.write(&sample{
return track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: out,
},
@ -820,7 +820,7 @@ func (f *formatFMP4) initialize() {
return nil
}
return track.write(&sample{
return track.record(&sample{
PartSample: &fmp4.PartSample{
Payload: tunit.Samples,
},
@ -838,12 +838,6 @@ func (f *formatFMP4) initialize() {
func (f *formatFMP4) close() {
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
}
}

View file

@ -81,7 +81,7 @@ func (p *formatFMP4Part) close() error {
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]
if !ok {
partTrack = &fmp4.PartTrack{

View file

@ -39,11 +39,9 @@ type formatFMP4Segment struct {
path string
fi *os.File
curPart *formatFMP4Part
lastDTS time.Duration
}
func (s *formatFMP4Segment) initialize() {
s.lastDTS = s.startDTS
}
func (s *formatFMP4Segment) close() error {
@ -61,17 +59,14 @@ func (s *formatFMP4Segment) close() error {
}
if err2 == nil {
duration := s.lastDTS - s.startDTS
s.f.a.agent.OnSegmentComplete(s.path, duration)
s.f.a.agent.OnSegmentComplete(s.path)
}
}
return err
}
func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error {
s.lastDTS = sample.dts
func (s *formatFMP4Segment) record(track *formatFMP4Track, sample *sample) error {
if s.curPart == nil {
s.curPart = &formatFMP4Part{
s: s,
@ -97,5 +92,5 @@ func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error
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
}
func (t *formatFMP4Track) write(sample *sample) error {
func (t *formatFMP4Track) record(sample *sample) error {
// wait the first video sample before setting hasVideo
if t.initTrack.Codec.IsVideo() {
t.f.hasVideo = true
@ -35,7 +35,7 @@ func (t *formatFMP4Track) write(sample *sample) error {
return nil
}
err := t.f.currentSegment.write(t, sample)
err := t.f.currentSegment.record(t, sample)
if err != nil {
return err
}
@ -43,7 +43,6 @@ func (t *formatFMP4Track) write(sample *sample) error {
if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) &&
!t.nextSample.IsNonSyncSample &&
(t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration {
t.f.currentSegment.lastDTS = t.nextSample.dts
err := t.f.currentSegment.close()
if err != nil {
return err

View file

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

View file

@ -1 +0,0 @@
c5ef2cf356b103bf7a19dd4d14257c9e00163551ed03bbf96bf22a12458a1250

View file

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

View file

@ -2,13 +2,8 @@
package main
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
@ -16,16 +11,15 @@ import (
)
func do() error {
log.Println("downloading hls.js...")
buf, err := os.ReadFile("./hlsjsdownloader/VERSION")
if err != nil {
return err
}
version := strings.TrimSpace(string(buf))
log.Printf("downloading hls.js version %s...", version)
res, err := http.Get("https://github.com/video-dev/hls.js/releases/download/" + version + "/release.zip")
res, err := http.Get("https://cdn.jsdelivr.net/npm/hls.js@" + version + "/dist/hls.min.js")
if err != nil {
return err
}
@ -35,38 +29,15 @@ func do() error {
return fmt.Errorf("bad status code: %v", res.StatusCode)
}
zipBuf, err := io.ReadAll(res.Body)
buf, err = io.ReadAll(res.Body)
if err != nil {
return err
}
hashBuf, err := os.ReadFile("./hlsjsdownloader/HASH")
err = os.WriteFile("hls.min.js", buf, 0o644)
if err != nil {
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")
return nil

View file

@ -5,7 +5,6 @@ import (
"errors"
"net"
"net/http"
"net/url"
gopath "path"
"strings"
"time"
@ -37,17 +36,6 @@ func mergePathAndQuery(path string, rawQuery string) string {
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 {
address string
encryption bool
@ -102,11 +90,9 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
switch ctx.Request.Method {
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-Headers", "Authorization, Range")
ctx.Writer.WriteHeader(http.StatusNoContent)
}
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range")
ctx.Writer.WriteHeader(http.StatusNoContent)
return
case http.MethodGet:
@ -159,15 +145,10 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
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{
AccessRequest: defs.PathAccessRequest{
Name: dir,
Query: q,
Query: ctx.Request.URL.RawQuery,
Publish: false,
IP: net.ParseIP(ctx.ClientIP()),
User: user,

View file

@ -155,7 +155,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track {
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 {
return fmt.Errorf("muxer error: %w", err)
}
@ -185,7 +185,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track {
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 {
return fmt.Errorf("muxer error: %w", err)
}

View file

@ -2,7 +2,6 @@ package hls
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
@ -12,6 +11,8 @@ import (
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs"
"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/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
@ -49,52 +50,21 @@ func (pa *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
}
type dummyPathManager struct {
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error)
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
stream *stream.Stream
}
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) {
return pm.addReader(req)
}
func TestPreflightRequest(t *testing.T) {
s := &Server{
Address: "127.0.0.1:8888",
AllowOrigin: "*",
ReadTimeout: conf.StringDuration(10 * time.Second),
Parent: test.NilLogger,
if req.AccessRequest.Name == "nonexisting" {
return nil, nil, fmt.Errorf("not found")
}
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: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{})
return &dummyPath{}, pm.stream, nil
}
func TestServerNotFound(t *testing.T) {
@ -103,19 +73,6 @@ func TestServerNotFound(t *testing.T) {
"always remux on",
} {
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{
Address: "127.0.0.1:8888",
Encryption: false,
@ -132,7 +89,7 @@ func TestServerNotFound(t *testing.T) {
Directory: "",
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
PathManager: pm,
PathManager: &dummyPathManager{},
Parent: test.NilLogger,
}
err := s.Initialize()
@ -170,7 +127,7 @@ func TestServerRead(t *testing.T) {
t.Run("always remux off", func(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
stream, err := stream.New(
1460,
desc,
true,
@ -178,18 +135,7 @@ func TestServerRead(t *testing.T) {
)
require.NoError(t, err)
pm := &dummyPathManager{
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
},
}
pathManager := &dummyPathManager{stream: stream}
s := &Server{
Address: "127.0.0.1:8888",
@ -207,7 +153,7 @@ func TestServerRead(t *testing.T) {
Directory: "",
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
PathManager: pm,
PathManager: pathManager,
Parent: test.NilLogger,
}
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), dts)
require.Equal(t, [][]byte{
{byte(h264.NALUTypeAccessUnitDelimiter), 0xf0},
test.FormatH264.SPS,
test.FormatH264.PPS,
{5, 1},
@ -247,7 +194,7 @@ func TestServerRead(t *testing.T) {
go func() {
time.Sleep(100 * time.Millisecond)
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{
NTP: time.Time{},
PTS: time.Duration(i) * time.Second,
@ -265,7 +212,7 @@ func TestServerRead(t *testing.T) {
t.Run("always remux on", func(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
stream, err := stream.New(
1460,
desc,
true,
@ -273,18 +220,7 @@ func TestServerRead(t *testing.T) {
)
require.NoError(t, err)
pm := &dummyPathManager{
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
},
}
pathManager := &dummyPathManager{stream: stream}
s := &Server{
Address: "127.0.0.1:8888",
@ -302,7 +238,7 @@ func TestServerRead(t *testing.T) {
Directory: "",
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
PathManager: pm,
PathManager: pathManager,
Parent: test.NilLogger,
}
err = s.Initialize()
@ -314,7 +250,7 @@ func TestServerRead(t *testing.T) {
time.Sleep(100 * time.Millisecond)
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{
NTP: time.Time{},
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), dts)
require.Equal(t, [][]byte{
{0x09, 0xf0},
test.FormatH264.SPS,
test.FormatH264.PPS,
{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) {
dir, err := os.MkdirTemp("", "mediamtx-playback")
require.NoError(t, err)
@ -462,7 +303,7 @@ func TestDirectory(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
stream, err := stream.New(
1460,
desc,
true,
@ -470,11 +311,7 @@ func TestDirectory(t *testing.T) {
)
require.NoError(t, err)
pm := &dummyPathManager{
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return &dummyPath{}, str, nil
},
}
pathManager := &dummyPathManager{stream: stream}
s := &Server{
Address: "127.0.0.1:8888",
@ -492,7 +329,7 @@ func TestDirectory(t *testing.T) {
Directory: filepath.Join(dir, "mydir"),
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
PathManager: pm,
PathManager: pathManager,
Parent: test.NilLogger,
}
err = s.Initialize()

View file

@ -74,6 +74,9 @@ type conn struct {
pathName string
query string
sconn srt.Conn
chNew chan srtNewConnReq
chSetConn chan srt.Conn
}
func (c *conn) initialize() {
@ -81,6 +84,8 @@ func (c *conn) initialize() {
c.created = time.Now()
c.uuid = uuid.New()
c.chNew = make(chan srtNewConnReq)
c.chSetConn = make(chan srt.Conn)
c.Log(logger.Info, "opened")
@ -125,20 +130,36 @@ func (c *conn) run() { //nolint:dupl
}
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
err := streamID.unmarshal(c.connReq.StreamId())
err := streamID.unmarshal(req.connReq.StreamId())
if err != nil {
c.connReq.Reject(srt.REJ_PEER)
return fmt.Errorf("invalid stream ID '%s': %w", c.connReq.StreamId(), err)
return false, fmt.Errorf("invalid stream ID '%s': %w", req.connReq.StreamId(), err)
}
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{
Author: c,
AccessRequest: defs.PathAccessRequest{
@ -157,24 +178,21 @@ func (c *conn) runPublish(streamID *streamID) error {
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER)
return terr
return false, terr
}
c.connReq.Reject(srt.REJ_PEER)
return err
return false, err
}
defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: c})
err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTPublishPassphrase)
err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTPublishPassphrase)
if err != nil {
c.connReq.Reject(srt.REJ_PEER)
return err
return false, err
}
sconn, err := c.connReq.Accept()
sconn, err := c.exchangeRequestWithConn(req)
if err != nil {
return err
return true, err
}
c.mutex.Lock()
@ -192,12 +210,12 @@ func (c *conn) runPublish(streamID *streamID) error {
select {
case err := <-readerErr:
sconn.Close()
return err
return true, err
case <-c.ctx.Done():
sconn.Close()
<-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{
Author: c,
AccessRequest: defs.PathAccessRequest{
@ -256,24 +274,21 @@ func (c *conn) runRead(streamID *streamID) error {
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER)
return terr
return false, err
}
c.connReq.Reject(srt.REJ_PEER)
return err
return false, err
}
defer path.RemoveReader(defs.PathRemoveReaderReq{Author: c})
err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTReadPassphrase)
err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTReadPassphrase)
if err != nil {
c.connReq.Reject(srt.REJ_PEER)
return err
return false, err
}
sconn, err := c.connReq.Accept()
sconn, err := c.exchangeRequestWithConn(req)
if err != nil {
return err
return true, err
}
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))
if err != nil {
return err
return true, err
}
c.Log(logger.Info, "is reading from path '%s', %s",
@ -316,10 +331,41 @@ func (c *conn) runRead(streamID *streamID) error {
select {
case <-c.ctx.Done():
return fmt.Errorf("terminated")
return true, fmt.Errorf("terminated")
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 {
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 {
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
}
type srtNewConnReq struct {
connReq srt.ConnRequest
res chan *conn
}
type serverAPIConnsListRes struct {
data *defs.APISRTConnList
err error
@ -85,7 +90,7 @@ type Server struct {
conns map[*conn]struct{}
// in
chNewConnRequest chan srt.ConnRequest
chNewConnRequest chan srtNewConnReq
chAcceptErr chan error
chCloseConn chan *conn
chAPIConnsList chan serverAPIConnsListReq
@ -108,7 +113,7 @@ func (s *Server) Initialize() error {
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
s.conns = make(map[*conn]struct{})
s.chNewConnRequest = make(chan srt.ConnRequest)
s.chNewConnRequest = make(chan srtNewConnReq)
s.chAcceptErr = make(chan error)
s.chCloseConn = make(chan *conn)
s.chAPIConnsList = make(chan serverAPIConnsListReq)
@ -160,7 +165,7 @@ outer:
writeTimeout: s.WriteTimeout,
writeQueueSize: s.WriteQueueSize,
udpMaxPayloadSize: s.UDPMaxPayloadSize,
connReq: req,
connReq: req.connReq,
runOnConnect: s.RunOnConnect,
runOnConnectRestart: s.RunOnConnectRestart,
runOnDisconnect: s.RunOnDisconnect,
@ -171,6 +176,7 @@ outer:
}
c.initialize()
s.conns[c] = struct{}{}
req.res <- c
case c := <-s.chCloseConn:
delete(s.conns, c)
@ -230,11 +236,20 @@ func (s *Server) findConnByUUID(uuid uuid.UUID) *conn {
}
// 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 {
case s.chNewConnRequest <- connReq:
case s.chNewConnRequest <- req:
c := <-req.res
return c.new(req)
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)
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()
address, err := srtConf.UnmarshalURL(u)
@ -127,7 +127,7 @@ func TestServerPublish(t *testing.T) {
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
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.PPS,
{0x05, 1}, // IDR
@ -156,7 +156,7 @@ func TestServerPublish(t *testing.T) {
return nil
})
err = w.WriteH264(track, 0, 0, true, [][]byte{
err = w.WriteH26x(track, 0, 0, true, [][]byte{
{5, 2},
})
require.NoError(t, err)
@ -205,7 +205,7 @@ func TestServerRead(t *testing.T) {
require.NoError(t, err)
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()
address, err := srtConf.UnmarshalURL(u)
@ -237,7 +237,7 @@ func TestServerRead(t *testing.T) {
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), dts)
require.Equal(t, [][]byte{

View file

@ -7,7 +7,6 @@ import (
"io"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"
@ -60,17 +59,6 @@ func sessionLocation(publish bool, path string, secret uuid.UUID) string {
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 {
address string
encryption bool
@ -121,23 +109,11 @@ func (s *httpServer) close() {
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
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{
AccessRequest: defs.PathAccessRequest{
Name: pathName,
Query: q,
Query: ctx.Request.URL.RawQuery,
Publish: publish,
IP: net.ParseIP(ctx.ClientIP()),
User: user,
@ -201,23 +177,11 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
}
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{
pathName: pathName,
remoteAddr: httpp.RemoteAddr(ctx),
query: q,
query: ctx.Request.URL.RawQuery,
user: user,
pass: pass,
offer: offer,

View file

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

View file

@ -50,7 +50,6 @@ const retryPause = 2000;
const video = document.getElementById('video');
const message = document.getElementById('message');
let nonAdvertisedCodecs = [];
let pc = null;
let restartTimeout = null;
let sessionUrl = '';
@ -88,14 +87,14 @@ const linkToIceServers = (links) => (
}) : []
);
const parseOffer = (sdp) => {
const parseOffer = (offer) => {
const ret = {
iceUfrag: '',
icePwd: '',
medias: [],
};
for (const line of sdp.split('\r\n')) {
for (const line of offer.split('\r\n')) {
if (line.startsWith('m=')) {
ret.medias.push(line.slice('m='.length));
} else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
@ -108,74 +107,6 @@ const parseOffer = (sdp) => {
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) => {
let opusPayloadFormat = '';
let lines = section.split('\r\n');
@ -205,30 +136,17 @@ const enableStereoOpus = (section) => {
return lines.join('\r\n');
};
const editOffer = (sdp) => {
const sections = sdp.split('m=');
const editOffer = (offer) => {
const sections = offer.sdp.split('m=');
for (let i = 0; i < sections.length; i++) {
if (sections[i].startsWith('audio')) {
sections[i] = enableStereoOpus(sections[i]);
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;
const section = sections[i];
if (section.startsWith('audio')) {
sections[i] = enableStereoOpus(section);
}
}
return sections.join('m=');
offer.sdp = sections.join('m=');
};
const generateSdpFragment = (od, candidates) => {
@ -265,70 +183,6 @@ const loadStream = () => {
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) => {
if (restartTimeout === null) {
setMessage(err + ', retrying in some seconds');
@ -400,16 +254,12 @@ const onRemoteAnswer = (sdp) => {
pc.setRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp,
}))
.then(() => {
if (queuedCandidates.length !== 0) {
sendLocalCandidates(queuedCandidates);
queuedCandidates = [];
}
})
.catch((err) => {
onError(err.toString());
});
}));
if (queuedCandidates.length !== 0) {
sendLocalCandidates(queuedCandidates);
queuedCandidates = [];
}
};
const sendOffer = (offer) => {
@ -426,16 +276,11 @@ const sendOffer = (offer) => {
break;
case 404:
throw new Error('stream not found');
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();
return res.text()
.then((sdp) => onRemoteAnswer(sdp));
return res.text();
})
.then((sdp) => onRemoteAnswer(sdp))
.catch((err) => {
@ -446,18 +291,10 @@ const sendOffer = (offer) => {
const createOffer = () => {
pc.createOffer()
.then((offer) => {
offer.sdp = editOffer(offer.sdp);
editOffer(offer);
offerData = parseOffer(offer.sdp);
pc.setLocalDescription(offer)
.then(() => {
sendOffer(offer);
})
.catch((err) => {
onError(err.toString());
});
})
.catch((err) => {
onError(err.toString());
pc.setLocalDescription(offer);
sendOffer(offer);
});
};
@ -467,7 +304,7 @@ const onConnectionState = () => {
}
if (pc.iceConnectionState === 'disconnected') {
onError('peer connection closed');
onError('peer connection disconnected');
}
};
@ -525,7 +362,7 @@ const loadAttributesFromQuery = () => {
const init = () => {
loadAttributesFromQuery();
getNonAdvertisedCodecs();
loadStream();
};
window.addEventListener('DOMContentLoaded', init);

View file

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

View file

@ -3,16 +3,14 @@ package webrtc
import (
"bytes"
"context"
"io"
"net/http"
"net/url"
"reflect"
"testing"
"time"
"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/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
@ -26,6 +24,10 @@ import (
"github.com/stretchr/testify/require"
)
func uint16Ptr(v uint16) *uint16 {
return &v
}
func checkClose(t *testing.T, closeFunc func() error) {
require.NoError(t, closeFunc())
}
@ -72,38 +74,40 @@ func (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
}
type dummyPathManager struct {
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error)
addPublisher func(req defs.PathAddPublisherReq) (defs.Path, error)
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
path *dummyPath
}
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) {
return pm.addPublisher(req)
func (pm *dummyPathManager) AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, error) {
return pm.path, nil
}
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 {
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
},
path := &dummyPath{
streamCreated: make(chan struct{}),
}
pathManager := &dummyPathManager{path: path}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "*",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
@ -113,10 +117,8 @@ func initializeTestServer(t *testing.T) *Server {
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
PathManager: pathManager,
Parent: test.NilLogger,
}
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)
defer s.Close()
@ -155,10 +157,11 @@ func TestPreflightRequest(t *testing.T) {
defer tr.CloseIdleConnections()
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)
req.Header.Add("Access-Control-Request-Method", "GET")
req.Header.Set("Access-Control-Request-Method", "OPTIONS")
res, err := hc.Do(req)
require.NoError(t, err)
@ -166,24 +169,12 @@ func TestPreflightRequest(t *testing.T) {
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, If-Match", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
_, ok := res.Header["Link"]
require.Equal(t, false, ok)
}
func TestServerOptionsICEServer(t *testing.T) {
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
},
}
pathManager := &dummyPathManager{}
s := &Server{
Address: "127.0.0.1:8886",
@ -204,11 +195,9 @@ func TestServerOptionsICEServer(t *testing.T) {
Username: "myuser",
Password: "mypass",
}},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
}
err := s.Initialize()
require.NoError(t, err)
@ -243,20 +232,7 @@ func TestServerPublish(t *testing.T) {
streamCreated: make(chan struct{}),
}
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
},
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
},
}
pathManager := &dummyPathManager{path: path}
s := &Server{
Address: "127.0.0.1:8886",
@ -273,8 +249,6 @@ func TestServerPublish(t *testing.T) {
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,
@ -356,450 +330,109 @@ func TestServerPublish(t *testing.T) {
}
func TestServerRead(t *testing.T) {
for _, ca := range []struct {
name string
medias []*description.Media
unit unit.Unit
outRTPPayload []byte
}{
{
"av1",
[]*description.Media{{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.AV1{
PayloadTyp: 96,
}},
}},
&unit.AV1{
TU: [][]byte{{1, 2}},
},
[]byte{0, 2, 1, 2},
},
{
"vp9",
[]*description.Media{{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP9{
PayloadTyp: 96,
}},
}},
&unit.VP9{
Frame: []byte{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34},
},
[]byte{
0x8f, 0xa0, 0xfd, 0x18, 0x07, 0x80, 0x03, 0x24,
0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00,
0x77, 0xf0, 0x32, 0x34,
},
},
{
"vp8",
[]*description.Media{{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP8{
PayloadTyp: 96,
}},
}},
&unit.VP8{
Frame: []byte{1, 2},
},
[]byte{0x10, 1, 2},
},
{
"h264",
[]*description.Media{test.MediaH264},
&unit.H264{
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
stream, err := stream.New(
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: stream}
pathManager := &dummyPathManager{path: path}
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{},
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
}
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Time{},
},
AU: [][]byte{
{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,
0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,
0x07, 0x08, 0x00, 0x02, 0x05, 0x01,
},
})
}
}()
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, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
PayloadType: 101,
SequenceNumber: pkt.SequenceNumber,
Timestamp: pkt.Timestamp,
SSRC: pkt.SSRC,
CSRC: []uint32{},
},
{
"opus",
[]*description.Media{{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
}},
}},
&unit.Opus{
Packets: [][]byte{{1, 2}},
},
[]byte{1, 2},
Payload: []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,
0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,
0x07, 0x08, 0x00, 0x02, 0x05, 0x01,
},
{
"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)
})
}
}, pkt)
}
func TestServerReadAuthorizationBearerJWT(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, "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)
func TestServerPostNotFound(t *testing.T) {
s := initializeTestServer(t)
defer s.Close()
tr := &http.Transport{}

View file

@ -2,7 +2,6 @@ package webrtc
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
@ -15,11 +14,9 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
"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/rtpvp9"
"github.com/bluenviron/gortsplib/v4/pkg/rtptime"
"github.com/bluenviron/mediacommon/pkg/codecs/g711"
"github.com/google/uuid"
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
@ -37,23 +34,10 @@ import (
)
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
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(
stream *stream.Stream,
writer *asyncwriter.Writer,
@ -102,9 +86,8 @@ func findVideoTrack(
if vp9Format != nil {
return vp9Format, func(track *webrtc.OutgoingTrack) error {
encoder := &rtpvp9.Encoder{
PayloadType: 96,
PayloadMaxSize: webrtcPayloadMaxSize,
InitialPictureID: uint16Ptr(8445),
PayloadType: 96,
PayloadMaxSize: webrtcPayloadMaxSize,
}
err := encoder.Init()
if err != nil {
@ -233,6 +216,10 @@ func findAudioTrack(
if opusFormat != nil {
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 {
for _, pkt := range u.GetRTPPackets() {
track.WriteRTP(pkt) //nolint:errcheck
@ -265,115 +252,16 @@ func findAudioTrack(
if g711Format != nil {
return g711Format, func(track *webrtc.OutgoingTrack) error {
if g711Format.SampleRate == 8000 {
curTimestamp, err := randUint32()
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
if g711Format.SampleRate != 8000 {
return fmt.Errorf("unsupported G711 sample rate")
}
curTimestamp, err := randUint32()
if err != nil {
return err
if g711Format.ChannelCount != 1 {
return fmt.Errorf("unsupported G711 channel count")
}
stream.AddReader(writer, media, lpcmFormat, func(u unit.Unit) error {
tunit := u.(*unit.LPCM)
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)
stream.AddReader(writer, media, g711Format, func(u unit.Unit) error {
for _, pkt := range u.GetRTPPackets() {
track.WriteRTP(pkt) //nolint:errcheck
}
@ -521,8 +409,6 @@ func (s *session) runPublish() (int, error) {
pc := &webrtc.PeerConnection{
ICEServers: iceServers,
HandshakeTimeout: s.parent.HandshakeTimeout,
TrackGatherTimeout: s.parent.TrackGatherTimeout,
IPsFromInterfaces: s.ipsFromInterfaces,
IPsFromInterfacesList: s.ipsFromInterfacesList,
AdditionalHosts: s.additionalHosts,
@ -545,7 +431,7 @@ func (s *session) runPublish() (int, error) {
return http.StatusBadRequest, err
}
err = webrtc.TracksAreValid(sdp.MediaDescriptions)
trackCount, err := webrtc.TrackCount(sdp.MediaDescriptions)
if err != nil {
// RFC draft-ietf-wish-whip
// if the number of audio and or video
@ -573,7 +459,7 @@ func (s *session) runPublish() (int, error) {
s.pc = pc
s.mutex.Unlock()
tracks, err := pc.GatherIncomingTracks(s.ctx)
tracks, err := pc.GatherIncomingTracks(s.ctx, trackCount)
if err != nil {
return 0, err
}
@ -680,8 +566,6 @@ func (s *session) runRead() (int, error) {
pc := &webrtc.PeerConnection{
ICEServers: iceServers,
HandshakeTimeout: s.parent.HandshakeTimeout,
TrackGatherTimeout: s.parent.TrackGatherTimeout,
IPsFromInterfaces: s.ipsFromInterfaces,
IPsFromInterfacesList: s.ipsFromInterfacesList,
AdditionalHosts: s.additionalHosts,
@ -776,7 +660,7 @@ func (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) {
select {
case req := <-s.chAddCandidates:
for _, candidate := range req.candidates {
err := pc.AddRemoteCandidate(candidate)
err := pc.AddRemoteCandidate(*candidate)
if err != nil {
req.res <- webRTCAddSessionCandidatesRes{err: err}
}

View file

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

View file

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

View file

@ -23,9 +23,10 @@ import (
// Source is a RTMP static source.
type Source struct {
ReadTimeout conf.StringDuration
WriteTimeout conf.StringDuration
Parent defs.StaticSourceParent
ResolvedSource string
ReadTimeout conf.StringDuration
WriteTimeout conf.StringDuration
Parent defs.StaticSourceParent
}
// 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 {
s.Log(logger.Debug, "connecting")
u, err := url.Parse(params.ResolvedSource)
u, err := url.Parse(s.ResolvedSource)
if err != nil {
return err
}

View file

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

View file

@ -62,6 +62,7 @@ func createRangeHeader(cnf *conf.Path) (*headers.Range, error) {
// Source is a RTSP static source.
type Source struct {
ResolvedSource string
ReadTimeout conf.StringDuration
WriteTimeout conf.StringDuration
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 {
return err
}

View file

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

View file

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

View file

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

View file

@ -45,8 +45,9 @@ type packetConn interface {
// Source is a UDP static source.
type Source struct {
ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
ResolvedSource string
ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
}
// 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 {
s.Log(logger.Debug, "connecting")
hostPort := params.ResolvedSource[len("udp://"):]
hostPort := s.ResolvedSource[len("udp://"):]
addr, err := net.ResolveUDPAddr("udp", hostPort)
if err != nil {

View file

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

View file

@ -19,8 +19,9 @@ import (
// Source is a WebRTC static source.
type Source struct {
ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
ResolvedSource string
ReadTimeout conf.StringDuration
Parent defs.StaticSourceParent
}
// 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 {
s.Log(logger.Debug, "connecting")
u, err := url.Parse(params.ResolvedSource)
u, err := url.Parse(s.ResolvedSource)
if err != nil {
return err
}

View file

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

View file

@ -24,11 +24,7 @@ type SourceTester struct {
}
// NewSourceTester allocates a SourceTester.
func NewSourceTester(
createFunc func(defs.StaticSourceParent) defs.StaticSource,
resolvedSource string,
conf *conf.Path,
) *SourceTester {
func NewSourceTester(createFunc func(defs.StaticSourceParent) defs.StaticSource, conf *conf.Path) *SourceTester {
ctx, ctxCancel := context.WithCancel(context.Background())
t := &SourceTester{
@ -42,9 +38,8 @@ func NewSourceTester(
go func() {
s.Run(defs.StaticSourceRunParams{ //nolint:errcheck
Context: ctx,
ResolvedSource: resolvedSource,
Conf: conf,
Context: ctx,
Conf: conf,
})
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
# to validate JWTs.
authJWTJWKS:
@ -381,10 +381,6 @@ webrtcICEServers2: []
# username: ''
# password: ''
# 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
@ -418,10 +414,8 @@ pathDefaults:
# * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS
# * redirect -> the stream is provided by another path or server
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera
# The following variables can be used in the source string:
# * $MTX_QUERY: query parameters (passed by first reader)
# * $G1, $G2, ...: regular expression groups, if path name is
# a regular expression.
# If path name is a regular expression, $G1, G2, etc will be replaced
# with regular expression groups.
source: publisher
# 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
@ -677,7 +671,6 @@ pathDefaults:
# * G1, G2, ...: regular expression groups, if path name is
# a regular expression.
# * MTX_SEGMENT_PATH: segment file path
# * MTX_SEGMENT_DURATION: segment duration
runOnRecordSegmentComplete:
###############################################

View file

@ -29,36 +29,36 @@ RUN cp mediamtx.yml LICENSE tmp/
RUN go generate ./...
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
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
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
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
FROM build-base AS build-linux-armv6
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 rm internal/protocols/rpicamera/exe/exe
FROM build-base AS build-linux-armv7
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 rm internal/protocols/rpicamera/exe/exe
FROM build-base AS build-linux-arm64
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 rm internal/protocols/rpicamera/exe/exe
@ -75,7 +75,6 @@ export DOCKERFILE_BINARIES
binaries:
echo "$$DOCKERFILE_BINARIES" | DOCKER_BUILDKIT=1 docker build . -f - \
--build-arg VERSION=$$(git describe --tags) \
-t temp
docker run --rm -v $(PWD):/out \
temp sh -c "rm -rf /out/binaries && cp -r /s/binaries /out/"

View file

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