diff --git a/.dockerignore b/.dockerignore index f327c60a..71d9f635 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.github/workflows/bump_hls_js.yml b/.github/workflows/bump_hls_js.yml index 10b14d15..dfe767bd 100644 --- a/.github/workflows/bump_hls_js.yml +++ b/.github/workflows/bump_hls_js.yml @@ -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 }} diff --git a/.github/workflows/code_lint.yml b/.github/workflows/code_lint.yml index d9cafd22..ea7e7bd0 100644 --- a/.github/workflows/code_lint.yml +++ b/.github/workflows/code_lint.yml @@ -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: diff --git a/.github/workflows/code_test.yml b/.github/workflows/code_test.yml index bb5a1409..42aebda0 100644 --- a/.github/workflows/code_test.yml +++ b/.github/workflows/code_test.yml @@ -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 diff --git a/.gitignore b/.gitignore index 66f36121..73ffd9a1 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index def91df2..00000000 --- a/Dockerfile +++ /dev/null @@ -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"] diff --git a/README.md b/README.md index 3e09e429..f6fb91f9 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 93802e49..d4f6e9f0 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -246,10 +246,6 @@ components: type: string clientOnly: type: boolean - webrtcHandshakeTimeout: - type: string - webrtcTrackGatherTimeout: - type: string # SRT server srt: diff --git a/go.mod b/go.mod index 1029528d..211b046f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 42f27977..8ef3b0a0 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/api/api.go b/internal/api/api.go index c0324624..642b99cb 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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) { diff --git a/internal/api/api_test.go b/internal/api/api_test.go index a2a6a5ec..f34c307a 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -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) { diff --git a/internal/api/paginate.go b/internal/api/paginate.go deleted file mode 100644 index 8be72062..00000000 --- a/internal/api/paginate.go +++ /dev/null @@ -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 -} diff --git a/internal/api/paginate_test.go b/internal/api/paginate_test.go deleted file mode 100644 index cd5daea8..00000000 --- a/internal/api/paginate_test.go +++ /dev/null @@ -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 - }) -} diff --git a/internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0 b/internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0 deleted file mode 100644 index 8e9f4e5e..00000000 --- a/internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -string("A") -string("0") diff --git a/internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e b/internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e deleted file mode 100644 index 57b56556..00000000 --- a/internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -string("1") -string("A") diff --git a/internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0 b/internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0 deleted file mode 100644 index befd7df0..00000000 --- a/internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0 +++ /dev/null @@ -1,3 +0,0 @@ -go test fuzz v1 -string("0") -string("") diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 8516d271..de97c202 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -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{ { diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 0c26086c..540abdaf 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -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 diff --git a/internal/conf/path.go b/internal/conf/path.go index 98018dbd..042eae22 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -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 } diff --git a/internal/core/api_test.go b/internal/core/api_test.go index 3390727c..33cf0a3d 100644 --- a/internal/core/api_test.go +++ b/internal/core/api_test.go @@ -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() diff --git a/internal/core/core.go b/internal/core/core.go index 2eeadcdb..2dd81949 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -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 diff --git a/internal/core/metrics_test.go b/internal/core/metrics_test.go index e586c840..5237e57c 100644 --- a/internal/core/metrics_test.go +++ b/internal/core/metrics_test.go @@ -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 diff --git a/internal/core/path.go b/internal/core/path.go index 2119927c..226b7c3d 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -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( diff --git a/internal/core/path_test.go b/internal/core/path_test.go index 5b123061..90e31900 100644 --- a/internal/core/path_test.go +++ b/internal/core/path_test.go @@ -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) diff --git a/internal/core/static_source_handler.go b/internal/core/static_source_handler.go index adec4f5b..57f4c983 100644 --- a/internal/core/static_source_handler.go +++ b/internal/core/static_source_handler.go @@ -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, }) }() } diff --git a/internal/core/versiongetter/main.go b/internal/core/versiongetter/main.go new file mode 100644 index 00000000..80965223 --- /dev/null +++ b/internal/core/versiongetter/main.go @@ -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) + } +} diff --git a/internal/defs/static_source.go b/internal/defs/static_source.go index dd5adbac..eaf1cf42 100644 --- a/internal/defs/static_source.go +++ b/internal/defs/static_source.go @@ -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 } diff --git a/internal/formatprocessor/generic.go b/internal/formatprocessor/generic.go index 1faba6a8..0140aa80 100644 --- a/internal/formatprocessor/generic.go +++ b/internal/formatprocessor/generic.go @@ -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{ diff --git a/internal/hooks/on_ready.go b/internal/hooks/on_ready.go index 22c0f8a2..f6e7e9fd 100644 --- a/internal/hooks/on_ready.go +++ b/internal/hooks/on_ready.go @@ -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)) - } - } } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 11a5af72..96c1a645 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -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 } diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go deleted file mode 100644 index 7bd3099d..00000000 --- a/internal/metrics/metrics_test.go +++ /dev/null @@ -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{}) -} diff --git a/internal/playback/server.go b/internal/playback/server.go index 52086169..3ed4f33a 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -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 { diff --git a/internal/playback/server_test.go b/internal/playback/server_test.go deleted file mode 100644 index 9fb905c6..00000000 --- a/internal/playback/server_test.go +++ /dev/null @@ -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{}) -} diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index 2cb0164f..ff6f2dc2 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -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{ diff --git a/internal/pprof/pprof_test.go b/internal/pprof/pprof_test.go deleted file mode 100644 index d26de965..00000000 --- a/internal/pprof/pprof_test.go +++ /dev/null @@ -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{}) -} diff --git a/internal/protocols/mpegts/from_stream.go b/internal/protocols/mpegts/from_stream.go index 47abf82a..48989aff 100644 --- a/internal/protocols/mpegts/from_stream.go +++ b/internal/protocols/mpegts/from_stream.go @@ -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 } diff --git a/internal/protocols/mpegts/to_stream.go b/internal/protocols/mpegts/to_stream.go index 282a78ae..d3c17228 100644 --- a/internal/protocols/mpegts/to_stream.go +++ b/internal/protocols/mpegts/to_stream.go @@ -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(), diff --git a/internal/protocols/rtmp/reader.go b/internal/protocols/rtmp/reader.go index b111780b..64f8042f 100644 --- a/internal/protocols/rtmp/reader.go +++ b/internal/protocols/rtmp/reader.go @@ -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 } } } diff --git a/internal/protocols/rtmp/reader_test.go b/internal/protocols/rtmp/reader_test.go index f5a2e9a5..052eeb19 100644 --- a/internal/protocols/rtmp/reader_test.go +++ b/internal/protocols/rtmp/reader_test.go @@ -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{ diff --git a/internal/protocols/webrtc/incoming_track.go b/internal/protocols/webrtc/incoming_track.go index 71016e96..26986340 100644 --- a/internal/protocols/webrtc/incoming_track.go +++ b/internal/protocols/webrtc/incoming_track.go @@ -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() diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index adc60e51..f02c66fe 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -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 } diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go index 5f1daee6..ca8b3165 100644 --- a/internal/protocols/webrtc/peer_connection.go +++ b/internal/protocols/webrtc/peer_connection.go @@ -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 } diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index 60454133..d4d0fa7c 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -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) -} diff --git a/internal/protocols/webrtc/track_count.go b/internal/protocols/webrtc/track_count.go new file mode 100644 index 00000000..99e9abea --- /dev/null +++ b/internal/protocols/webrtc/track_count.go @@ -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 +} diff --git a/internal/protocols/webrtc/tracks_to_medias.go b/internal/protocols/webrtc/tracks_to_medias.go index 81ea0850..809eeec4 100644 --- a/internal/protocols/webrtc/tracks_to_medias.go +++ b/internal/protocols/webrtc/tracks_to_medias.go @@ -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}, } } diff --git a/internal/protocols/webrtc/whip_client.go b/internal/protocols/webrtc/whip_client.go index 5c1154b8..40511200 100644 --- a/internal/protocols/webrtc/whip_client.go +++ b/internal/protocols/webrtc/whip_client.go @@ -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() diff --git a/internal/record/agent.go b/internal/record/agent.go index f7189f67..7ecde7db 100644 --- a/internal/record/agent.go +++ b/internal/record/agent.go @@ -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 { diff --git a/internal/record/agent_instance.go b/internal/record/agent_instance.go index bce6e3d1..8e722a8f 100644 --- a/internal/record/agent_instance.go +++ b/internal/record/agent_instance.go @@ -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 diff --git a/internal/record/agent_test.go b/internal/record/agent_test.go index fb342e9e..0f2a18c6 100644 --- a/internal/record/agent_test.go +++ b/internal/record/agent_test.go @@ -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) }) } } diff --git a/internal/record/format_fmp4.go b/internal/record/format_fmp4.go index 5d67b065..a8a0f8cb 100644 --- a/internal/record/format_fmp4.go +++ b/internal/record/format_fmp4.go @@ -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 } } diff --git a/internal/record/format_fmp4_part.go b/internal/record/format_fmp4_part.go index a960c9a7..1f1968fe 100644 --- a/internal/record/format_fmp4_part.go +++ b/internal/record/format_fmp4_part.go @@ -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{ diff --git a/internal/record/format_fmp4_segment.go b/internal/record/format_fmp4_segment.go index 7920fcb3..f0695c00 100644 --- a/internal/record/format_fmp4_segment.go +++ b/internal/record/format_fmp4_segment.go @@ -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) } diff --git a/internal/record/format_fmp4_track.go b/internal/record/format_fmp4_track.go index fd7a075e..1ff214f2 100644 --- a/internal/record/format_fmp4_track.go +++ b/internal/record/format_fmp4_track.go @@ -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 diff --git a/internal/record/format_mpegts.go b/internal/record/format_mpegts.go index 381cb839..60f778cf 100644 --- a/internal/record/format_mpegts.go +++ b/internal/record/format_mpegts.go @@ -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) } diff --git a/internal/record/format_mpegts_segment.go b/internal/record/format_mpegts_segment.go index 754c93aa..06e16883 100644 --- a/internal/record/format_mpegts_segment.go +++ b/internal/record/format_mpegts_segment.go @@ -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) } } diff --git a/internal/servers/hls/hlsjsdownloader/HASH b/internal/servers/hls/hlsjsdownloader/HASH deleted file mode 100644 index 894fba52..00000000 --- a/internal/servers/hls/hlsjsdownloader/HASH +++ /dev/null @@ -1 +0,0 @@ -c5ef2cf356b103bf7a19dd4d14257c9e00163551ed03bbf96bf22a12458a1250 diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index 77fb9c77..1097babb 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.13 +v1.5.8 diff --git a/internal/servers/hls/hlsjsdownloader/main.go b/internal/servers/hls/hlsjsdownloader/main.go index 7b9f3357..41008dec 100644 --- a/internal/servers/hls/hlsjsdownloader/main.go +++ b/internal/servers/hls/hlsjsdownloader/main.go @@ -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 diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index 882e3443..50d019c0 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -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, diff --git a/internal/servers/hls/muxer_instance.go b/internal/servers/hls/muxer_instance.go index c1d87c7f..a2290760 100644 --- a/internal/servers/hls/muxer_instance.go +++ b/internal/servers/hls/muxer_instance.go @@ -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) } diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index b014702b..0db2bb65 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -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() diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go index 01bfeecd..971fb5af 100644 --- a/internal/servers/srt/conn.go +++ b/internal/servers/srt/conn.go @@ -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(): } } diff --git a/internal/servers/srt/listener.go b/internal/servers/srt/listener.go index 000ba895..38b9e297 100644 --- a/internal/servers/srt/listener.go +++ b/internal/servers/srt/listener.go @@ -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) } } diff --git a/internal/servers/srt/server.go b/internal/servers/srt/server.go index 33b26f29..2c1977b8 100644 --- a/internal/servers/srt/server.go +++ b/internal/servers/srt/server.go @@ -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 } } diff --git a/internal/servers/srt/server_test.go b/internal/servers/srt/server_test.go index 8e0f9573..74645d64 100644 --- a/internal/servers/srt/server_test.go +++ b/internal/servers/srt/server_test.go @@ -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{ diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 4255b907..fe9b1462 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -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, diff --git a/internal/servers/webrtc/publish_index.html b/internal/servers/webrtc/publish_index.html index 832006f1..82981175 100644 --- a/internal/servers/webrtc/publish_index.html +++ b/internal/servers/webrtc/publish_index.html @@ -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(''); } diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index 718f1fbc..4839eb87 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -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); diff --git a/internal/servers/webrtc/server.go b/internal/servers/webrtc/server.go index a5392032..ba855eb6 100644 --- a/internal/servers/webrtc/server.go +++ b/internal/servers/webrtc/server.go @@ -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 diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 6a05433c..197917db 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -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{} diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index 421b4a18..cb98580d 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -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} } diff --git a/internal/staticsources/hls/source.go b/internal/staticsources/hls/source.go index 7e333f72..4e81d42f 100644 --- a/internal/staticsources/hls/source.go +++ b/internal/staticsources/hls/source.go @@ -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, diff --git a/internal/staticsources/hls/source_test.go b/internal/staticsources/hls/source_test.go index 6da53776..b4d9faef 100644 --- a/internal/staticsources/hls/source_test.go +++ b/internal/staticsources/hls/source_test.go @@ -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() diff --git a/internal/staticsources/rtmp/source.go b/internal/staticsources/rtmp/source.go index 91a62100..8ad22667 100644 --- a/internal/staticsources/rtmp/source.go +++ b/internal/staticsources/rtmp/source.go @@ -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 } diff --git a/internal/staticsources/rtmp/source_test.go b/internal/staticsources/rtmp/source_test.go index 2258ee33..925a157f 100644 --- a/internal/staticsources/rtmp/source_test.go +++ b/internal/staticsources/rtmp/source_test.go @@ -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", }, diff --git a/internal/staticsources/rtsp/source.go b/internal/staticsources/rtsp/source.go index e7a2d8ab..be603149 100644 --- a/internal/staticsources/rtsp/source.go +++ b/internal/staticsources/rtsp/source.go @@ -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 } diff --git a/internal/staticsources/rtsp/source_test.go b/internal/staticsources/rtsp/source_test.go index 10494e28..91b5ae7e 100644 --- a/internal/staticsources/rtsp/source_test.go +++ b/internal/staticsources/rtsp/source_test.go @@ -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() diff --git a/internal/staticsources/srt/source.go b/internal/staticsources/srt/source.go index 69e65c3f..8026ed3e 100644 --- a/internal/staticsources/srt/source.go +++ b/internal/staticsources/srt/source.go @@ -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 } diff --git a/internal/staticsources/srt/source_test.go b/internal/staticsources/srt/source_test.go index 230aeeb3..07d179ee 100644 --- a/internal/staticsources/srt/source_test.go +++ b/internal/staticsources/srt/source_test.go @@ -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() diff --git a/internal/staticsources/udp/source.go b/internal/staticsources/udp/source.go index 0d899f32..1202b2f1 100644 --- a/internal/staticsources/udp/source.go +++ b/internal/staticsources/udp/source.go @@ -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 { diff --git a/internal/staticsources/udp/source_test.go b/internal/staticsources/udp/source_test.go index fa64dc7e..f13bd3ad 100644 --- a/internal/staticsources/udp/source_test.go +++ b/internal/staticsources/udp/source_test.go @@ -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) diff --git a/internal/staticsources/webrtc/source.go b/internal/staticsources/webrtc/source.go index f2004a7d..3fab2a77 100644 --- a/internal/staticsources/webrtc/source.go +++ b/internal/staticsources/webrtc/source.go @@ -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 } diff --git a/internal/staticsources/webrtc/source_test.go b/internal/staticsources/webrtc/source_test.go index c8b2842f..ef809dd3 100644 --- a/internal/staticsources/webrtc/source_test.go +++ b/internal/staticsources/webrtc/source_test.go @@ -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() diff --git a/internal/test/source_tester.go b/internal/test/source_tester.go index 92d6e4d3..7a52501a 100644 --- a/internal/test/source_tester.go +++ b/internal/test/source_tester.go @@ -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) }() diff --git a/mediamtx.yml b/mediamtx.yml index f936a305..cdc45cb0 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -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: ############################################### diff --git a/scripts/binaries.mk b/scripts/binaries.mk index c9693fc3..d3252766 100644 --- a/scripts/binaries.mk +++ b/scripts/binaries.mk @@ -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/" diff --git a/scripts/lint.mk b/scripts/lint.mk index 5245ac8c..5b03fcea 100644 --- a/scripts/lint.mk +++ b/scripts/lint.mk @@ -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