forked from External/mediamtx
Compare commits
2 commits
main
...
feature/ve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a16ef00e88 | ||
|
|
f6b0a72453 |
88 changed files with 1105 additions and 3370 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
/binaries
|
/binaries
|
||||||
/coverage*.txt
|
/coverage*.txt
|
||||||
/apidocs/*.html
|
/apidocs/*.html
|
||||||
|
/internal/core/version.go
|
||||||
/internal/servers/hls/hls.min.js
|
/internal/servers/hls/hls.min.js
|
||||||
/internal/protocols/rpicamera/exe/text_font.h
|
/internal/protocols/rpicamera/exe/text_font.h
|
||||||
/internal/protocols/rpicamera/exe/exe
|
/internal/protocols/rpicamera/exe/exe
|
||||||
|
|
|
||||||
15
.github/workflows/bump_hls_js.yml
vendored
15
.github/workflows/bump_hls_js.yml
vendored
|
|
@ -19,26 +19,23 @@ jobs:
|
||||||
&& git config user.email bot@mediamtx
|
&& git config user.email bot@mediamtx
|
||||||
&& ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs)
|
&& ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs)
|
||||||
|
|
||||||
- run: |
|
- run: >
|
||||||
set -e
|
|
||||||
VERSION=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases?per_page=1 | grep tag_name | sed 's/\s\+"tag_name": "\(.\+\)",/\1/')
|
VERSION=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases?per_page=1 | grep tag_name | sed 's/\s\+"tag_name": "\(.\+\)",/\1/')
|
||||||
HASH=$(curl -sL https://github.com/video-dev/hls.js/releases/download/$VERSION/release.zip -o- | sha256sum | cut -f1 -d ' ')
|
&& echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION
|
||||||
echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION
|
&& echo VERSION=$VERSION >> $GITHUB_ENV
|
||||||
echo $HASH > internal/servers/hls/hlsjsdownloader/HASH
|
|
||||||
echo VERSION=$VERSION >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- id: check_repo
|
- id: check_repo
|
||||||
run: >
|
run: >
|
||||||
test -n "$(git status --porcelain)" && echo "update=1" >> "$GITHUB_OUTPUT" || echo "update=0" >> "$GITHUB_OUTPUT"
|
echo "clean=$(git status --porcelain)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- if: ${{ steps.check_repo.outputs.update == '1' }}
|
- if: ${{ steps.check_repo.outputs.clean != '' }}
|
||||||
run: >
|
run: >
|
||||||
git reset ${GITHUB_REF_NAME}
|
git reset ${GITHUB_REF_NAME}
|
||||||
&& git add .
|
&& git add .
|
||||||
&& git commit -m "bump hls.js to ${VERSION}"
|
&& git commit -m "bump hls.js to ${VERSION}"
|
||||||
&& git push --set-upstream origin deps/hlsjs --force
|
&& git push --set-upstream origin deps/hlsjs --force
|
||||||
|
|
||||||
- if: ${{ steps.check_repo.outputs.update == '1' }}
|
- if: ${{ steps.check_repo.outputs.clean != '' }}
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
2
.github/workflows/code_lint.yml
vendored
2
.github/workflows/code_lint.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: "1.22"
|
go-version: "1.22"
|
||||||
|
|
||||||
- run: touch internal/servers/hls/hls.min.js
|
- run: go generate ./...
|
||||||
|
|
||||||
- uses: golangci/golangci-lint-action@v4
|
- uses: golangci/golangci-lint-action@v4
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
11
.github/workflows/code_test.yml
vendored
11
.github/workflows/code_test.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- run: make test
|
- run: make test
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ jobs:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- run: make test32
|
- run: make test32
|
||||||
|
|
||||||
|
|
@ -31,10 +31,15 @@ jobs:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: "1.22"
|
go-version: "1.22"
|
||||||
|
|
||||||
|
- name: GitHub Tag Name example
|
||||||
|
run: |
|
||||||
|
echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME"
|
||||||
|
echo "Tag name from github.ref_name: ${{ github.ref_name }}"
|
||||||
|
|
||||||
- run: make test-highlevel-nodocker
|
- run: make test-highlevel-nodocker
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
||||||
/binaries
|
/binaries
|
||||||
/coverage*.txt
|
/coverage*.txt
|
||||||
/apidocs/*.html
|
/apidocs/*.html
|
||||||
|
/internal/core/version.go
|
||||||
/internal/servers/hls/hls.min.js
|
/internal/servers/hls/hls.min.js
|
||||||
/internal/protocols/rpicamera/exe/text_font.h
|
/internal/protocols/rpicamera/exe/text_font.h
|
||||||
/internal/protocols/rpicamera/exe/exe
|
/internal/protocols/rpicamera/exe/exe
|
||||||
|
|
|
||||||
17
Dockerfile
17
Dockerfile
|
|
@ -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"]
|
|
||||||
57
README.md
57
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 clients](#srt-clients)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
|
||||||
|[SRT cameras and servers](#srt-cameras-and-servers)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
|
|[SRT cameras and servers](#srt-cameras-and-servers)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
|
||||||
|[WebRTC clients](#webrtc-clients)|Browser-based, WHIP|AV1, VP9, VP8, H265, H264|Opus, G722, G711 (PCMA, PCMU)|
|
|[WebRTC clients](#webrtc-clients)|Browser-based, WHIP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)|
|
||||||
|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H265, H264|Opus, G722, G711 (PCMA, PCMU)|
|
|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)|
|
||||||
|[RTSP clients](#rtsp-clients)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|
|[RTSP clients](#rtsp-clients)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|
||||||
|[RTSP cameras and servers](#rtsp-cameras-and-servers)|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|
|[RTSP cameras and servers](#rtsp-cameras-and-servers)|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
|
||||||
|[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM|
|
|[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM|
|
||||||
|
|
@ -134,8 +134,7 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
|
||||||
* [SRT-specific features](#srt-specific-features)
|
* [SRT-specific features](#srt-specific-features)
|
||||||
* [Standard stream ID syntax](#standard-stream-id-syntax)
|
* [Standard stream ID syntax](#standard-stream-id-syntax)
|
||||||
* [WebRTC-specific features](#webrtc-specific-features)
|
* [WebRTC-specific features](#webrtc-specific-features)
|
||||||
* [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep)
|
* [Connectivity issues](#connectivity-issues)
|
||||||
* [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues)
|
|
||||||
* [RTSP-specific features](#rtsp-specific-features)
|
* [RTSP-specific features](#rtsp-specific-features)
|
||||||
* [Transport protocols](#transport-protocols)
|
* [Transport protocols](#transport-protocols)
|
||||||
* [Encryption](#encryption)
|
* [Encryption](#encryption)
|
||||||
|
|
@ -339,7 +338,6 @@ Latest versions of OBS Studio can publish to the server with the [WebRTC / WHIP
|
||||||
|
|
||||||
* Service: `WHIP`
|
* Service: `WHIP`
|
||||||
* Server: `http://localhost:8889/mystream/whip`
|
* Server: `http://localhost:8889/mystream/whip`
|
||||||
* Bearer Token: `myuser:mypass` (if internal authentication is enabled) or JWT (if JWT-based authentication is enabled)
|
|
||||||
|
|
||||||
Save the configuration and click `Start streaming`.
|
Save the configuration and click `Start streaming`.
|
||||||
|
|
||||||
|
|
@ -612,9 +610,7 @@ WHIP is a WebRTC extensions that allows to publish streams by using a URL, witho
|
||||||
http://localhost:8889/mystream/whip
|
http://localhost:8889/mystream/whip
|
||||||
```
|
```
|
||||||
|
|
||||||
Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep).
|
Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations.
|
||||||
|
|
||||||
Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).
|
|
||||||
|
|
||||||
Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio).
|
Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio).
|
||||||
|
|
||||||
|
|
@ -880,9 +876,7 @@ WHEP is a WebRTC extensions that allows to read streams by using a URL, without
|
||||||
http://localhost:8889/mystream/whep
|
http://localhost:8889/mystream/whep
|
||||||
```
|
```
|
||||||
|
|
||||||
Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep).
|
Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations.
|
||||||
|
|
||||||
Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues).
|
|
||||||
|
|
||||||
Known clients that can read with WebRTC and WHEP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [web browsers](#web-browsers-1).
|
Known clients that can read with WebRTC and WHEP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [web browsers](#web-browsers-1).
|
||||||
|
|
||||||
|
|
@ -1186,20 +1180,12 @@ The JWT is expected to contain the `mediamtx_permissions` scope, with a list of
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of any other protocol), for instance (RTSP):
|
Clients are expected to pass the JWT in query parameters, for instance:
|
||||||
|
|
||||||
```
|
```
|
||||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT
|
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT
|
||||||
```
|
```
|
||||||
|
|
||||||
For instance (HLS):
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /mypath/index.m3u8 HTTP/1.1
|
|
||||||
Host: example.com
|
|
||||||
Authorization: Bearer MY_JWT
|
|
||||||
```
|
|
||||||
|
|
||||||
Here's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide such JWTs:
|
Here's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide such JWTs:
|
||||||
|
|
||||||
1. Start Keycloak:
|
1. Start Keycloak:
|
||||||
|
|
@ -1683,7 +1669,6 @@ pathDefaults:
|
||||||
# * G1, G2, ...: regular expression groups, if path name is
|
# * G1, G2, ...: regular expression groups, if path name is
|
||||||
# a regular expression.
|
# a regular expression.
|
||||||
# * MTX_SEGMENT_PATH: segment file path
|
# * MTX_SEGMENT_PATH: segment file path
|
||||||
# * MTX_SEGMENT_DURATION: segment duration
|
|
||||||
runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH
|
runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1844,35 +1829,7 @@ Where:
|
||||||
|
|
||||||
### WebRTC-specific features
|
### WebRTC-specific features
|
||||||
|
|
||||||
#### Authenticating with WHIP/WHEP
|
#### Connectivity issues
|
||||||
|
|
||||||
When using WHIP or WHEP to establish a WebRTC connection, there are multiple ways to provide credentials.
|
|
||||||
|
|
||||||
If internal authentication or HTTP-based authentication is enabled, username and password can be passed through the `Authentication: Basic` header:
|
|
||||||
|
|
||||||
```
|
|
||||||
Authentication: Basic [base64_encoded_credentials]
|
|
||||||
```
|
|
||||||
|
|
||||||
Username and password can be also passed through the `Authentication: Bearer` header (since it's mandated by the specification):
|
|
||||||
|
|
||||||
```
|
|
||||||
Authentication: Bearer username:password
|
|
||||||
```
|
|
||||||
|
|
||||||
If JWT-based authentication is enabled, JWT can be passed through the `Authentication: Bearer` header:
|
|
||||||
|
|
||||||
```
|
|
||||||
Authentication: Bearer [jwt]
|
|
||||||
```
|
|
||||||
|
|
||||||
The JWT can also be passed through query parameters:
|
|
||||||
|
|
||||||
```
|
|
||||||
http://localhost:8889/mystream/whip?jwt=[jwt]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Solving WebRTC connectivity issues
|
|
||||||
|
|
||||||
If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection.
|
If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,10 +246,6 @@ components:
|
||||||
type: string
|
type: string
|
||||||
clientOnly:
|
clientOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
webrtcHandshakeTimeout:
|
|
||||||
type: string
|
|
||||||
webrtcTrackGatherTimeout:
|
|
||||||
type: string
|
|
||||||
|
|
||||||
# SRT server
|
# SRT server
|
||||||
srt:
|
srt:
|
||||||
|
|
|
||||||
32
go.mod
32
go.mod
|
|
@ -8,19 +8,19 @@ require (
|
||||||
github.com/MicahParks/keyfunc/v3 v3.3.3
|
github.com/MicahParks/keyfunc/v3 v3.3.3
|
||||||
github.com/abema/go-mp4 v1.2.0
|
github.com/abema/go-mp4 v1.2.0
|
||||||
github.com/alecthomas/kong v0.9.0
|
github.com/alecthomas/kong v0.9.0
|
||||||
github.com/bluenviron/gohlslib v1.4.0
|
github.com/bluenviron/gohlslib v1.3.3
|
||||||
github.com/bluenviron/gortsplib/v4 v4.10.2
|
github.com/bluenviron/gortsplib/v4 v4.10.0
|
||||||
github.com/bluenviron/mediacommon v1.12.1
|
github.com/bluenviron/mediacommon v1.11.0
|
||||||
github.com/datarhei/gosrt v0.7.0
|
github.com/datarhei/gosrt v0.6.0
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gookit/color v1.5.4
|
github.com/gookit/color v1.5.4
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/matthewhartstonge/argon2 v1.0.0
|
github.com/matthewhartstonge/argon2 v1.0.0
|
||||||
github.com/pion/ice/v2 v2.3.24
|
github.com/pion/ice/v2 v2.3.11
|
||||||
github.com/pion/interceptor v0.1.29
|
github.com/pion/interceptor v0.1.29
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/logging v0.2.2
|
||||||
github.com/pion/rtcp v1.2.14
|
github.com/pion/rtcp v1.2.14
|
||||||
|
|
@ -28,9 +28,9 @@ require (
|
||||||
github.com/pion/sdp/v3 v3.0.9
|
github.com/pion/sdp/v3 v3.0.9
|
||||||
github.com/pion/webrtc/v3 v3.2.22
|
github.com/pion/webrtc/v3 v3.2.22
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.23.0
|
||||||
golang.org/x/sys v0.22.0
|
golang.org/x/sys v0.20.0
|
||||||
golang.org/x/term v0.22.0
|
golang.org/x/term v0.20.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -58,20 +58,20 @@ require (
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pion/datachannel v1.5.5 // indirect
|
github.com/pion/datachannel v1.5.5 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||||
github.com/pion/mdns v0.0.12 // indirect
|
github.com/pion/mdns v0.0.9 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.16 // indirect
|
github.com/pion/sctp v1.8.8 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||||
github.com/pion/stun v0.6.1 // indirect
|
github.com/pion/stun v0.6.1 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
github.com/pion/transport/v2 v2.2.3 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.3 // indirect
|
github.com/pion/turn/v2 v2.1.3 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
@ -79,6 +79,6 @@ require (
|
||||||
|
|
||||||
replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5
|
replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5
|
||||||
|
|
||||||
replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9
|
replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1
|
||||||
|
|
||||||
replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06
|
replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6
|
||||||
|
|
|
||||||
93
go.sum
93
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/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 h1:Vax9SzYE68ZYLwFaK7lnCV2ZhX9/YqAJX6xxROPRqEM=
|
github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 h1:fD6eZt+3/t8bzFn6ZZA2eP63xBP06v3EPfPJu8DO8ys=
|
||||||
github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
|
||||||
github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 h1:WtKhXOpd8lgTeXF3RQVOzkNRuy83ygvWEpMYD2aoY3Q=
|
github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 h1:wMd3D1mLghoYYh31STig8Kwm2qi8QyQKUy09qUUZrVw=
|
||||||
github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
|
github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||||
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
|
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
|
||||||
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
|
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
|
||||||
github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E=
|
github.com/bluenviron/gohlslib v1.3.3 h1:Ji4PW9QHHCbpBteZCKk+rGY6emFNSGVFMsAa/3xFChk=
|
||||||
github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo=
|
github.com/bluenviron/gohlslib v1.3.3/go.mod h1:MQcRjI9fYBNb9QhZO3RydgtbfCRhjogj6YMrpCDuTvY=
|
||||||
github.com/bluenviron/gortsplib/v4 v4.10.2 h1:O7HPRG8Pv4zUbyYD0HYH4Ufu1Hg9FJGTlizx6a09hL0=
|
github.com/bluenviron/gortsplib/v4 v4.10.0 h1:9vJsUDuBgSinm41CR6yWnSMZ7TRWeB/oiAuN4lo30bU=
|
||||||
github.com/bluenviron/gortsplib/v4 v4.10.2/go.mod h1:re/L/vYh2wLPElQNAYah+bRFHJs0aRkM1MLX3WJ3N6M=
|
github.com/bluenviron/gortsplib/v4 v4.10.0/go.mod h1:iLJ1tmwGMbaN04ZYh/KRlAHsCbz9Rycn7cPAvdR+Vkc=
|
||||||
github.com/bluenviron/mediacommon v1.12.1 h1:sgDJaKV6OXrPCSO0KPp9zi/pwNWtKHenn5/dvjtY+Tg=
|
github.com/bluenviron/mediacommon v1.11.0 h1:1xY4QGYz7da9tsV2Xvd+ol+Ul5qq2g7ADJtIlVkQSRI=
|
||||||
github.com/bluenviron/mediacommon v1.12.1/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec=
|
github.com/bluenviron/mediacommon v1.11.0/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
|
@ -37,8 +37,8 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/datarhei/gosrt v0.7.0 h1:1/IY66HVVgqGA9zkmL5l6jUFuI8t/76WkuamSkJqHqs=
|
github.com/datarhei/gosrt v0.6.0 h1:HrrXAw90V78ok4WMIhX6se1aTHPCn82Sg2hj+PhdmGc=
|
||||||
github.com/datarhei/gosrt v0.7.0/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE=
|
github.com/datarhei/gosrt v0.6.0/go.mod h1:fsOWdLSHUHShHjgi/46h6wjtdQrtnSdAQFnlas8ONxs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|
@ -81,13 +81,13 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
|
@ -137,23 +137,28 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew
|
||||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
|
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||||
|
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
|
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||||
|
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg=
|
github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg=
|
||||||
github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||||
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U=
|
||||||
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||||
|
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||||
|
|
@ -162,19 +167,20 @@ github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||||
|
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA=
|
||||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
|
||||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
|
||||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||||
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
|
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
|
||||||
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
||||||
|
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
|
||||||
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
|
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
|
||||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
@ -207,11 +213,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
|
@ -224,14 +230,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|
@ -254,39 +261,41 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -35,6 +36,58 @@ func interfaceIsEmpty(i interface{}) bool {
|
||||||
return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil()
|
return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func paginate2(itemsPtr interface{}, itemsPerPage int, page int) int {
|
||||||
|
ritems := reflect.ValueOf(itemsPtr).Elem()
|
||||||
|
|
||||||
|
itemsLen := ritems.Len()
|
||||||
|
if itemsLen == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pageCount := (itemsLen / itemsPerPage)
|
||||||
|
if (itemsLen % itemsPerPage) != 0 {
|
||||||
|
pageCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
min := page * itemsPerPage
|
||||||
|
if min > itemsLen {
|
||||||
|
min = itemsLen
|
||||||
|
}
|
||||||
|
|
||||||
|
max := (page + 1) * itemsPerPage
|
||||||
|
if max > itemsLen {
|
||||||
|
max = itemsLen
|
||||||
|
}
|
||||||
|
|
||||||
|
ritems.Set(ritems.Slice(min, max))
|
||||||
|
|
||||||
|
return pageCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func paginate(itemsPtr interface{}, itemsPerPageStr string, pageStr string) (int, error) {
|
||||||
|
itemsPerPage := 100
|
||||||
|
|
||||||
|
if itemsPerPageStr != "" {
|
||||||
|
tmp, err := strconv.ParseUint(itemsPerPageStr, 10, 31)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
itemsPerPage = int(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := 0
|
||||||
|
|
||||||
|
if pageStr != "" {
|
||||||
|
tmp, err := strconv.ParseUint(pageStr, 10, 31)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
page = int(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return paginate2(itemsPtr, itemsPerPage, page), nil
|
||||||
|
}
|
||||||
|
|
||||||
func sortedKeys(paths map[string]*conf.Path) []string {
|
func sortedKeys(paths map[string]*conf.Path) []string {
|
||||||
ret := make([]string, len(paths))
|
ret := make([]string, len(paths))
|
||||||
i := 0
|
i := 0
|
||||||
|
|
@ -137,7 +190,6 @@ func (a *API) Initialize() error {
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
|
router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
|
||||||
|
|
||||||
router.NoRoute(a.middlewareOrigin, a.middlewareAuth)
|
|
||||||
group := router.Group("/", a.middlewareOrigin, a.middlewareAuth)
|
group := router.Group("/", a.middlewareOrigin, a.middlewareAuth)
|
||||||
|
|
||||||
group.GET("/v3/config/global/get", a.onConfigGlobalGet)
|
group.GET("/v3/config/global/get", a.onConfigGlobalGet)
|
||||||
|
|
@ -251,15 +303,6 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) {
|
||||||
func (a *API) middlewareOrigin(ctx *gin.Context) {
|
func (a *API) middlewareOrigin(ctx *gin.Context) {
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin)
|
ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin)
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
|
||||||
// preflight requests
|
|
||||||
if ctx.Request.Method == http.MethodOptions &&
|
|
||||||
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE")
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
|
||||||
ctx.AbortWithStatus(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) middlewareAuth(ctx *gin.Context) {
|
func (a *API) middlewareAuth(ctx *gin.Context) {
|
||||||
|
|
|
||||||
|
|
@ -74,41 +74,36 @@ func checkError(t *testing.T, msg string, body io.Reader) {
|
||||||
require.Equal(t, map[string]interface{}{"error": msg}, resErr)
|
require.Equal(t, map[string]interface{}{"error": msg}, resErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreflightRequest(t *testing.T) {
|
func TestPaginate(t *testing.T) {
|
||||||
api := API{
|
items := make([]int, 5)
|
||||||
Address: "localhost:9997",
|
for i := 0; i < 5; i++ {
|
||||||
AllowOrigin: "*",
|
items[i] = i
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
AuthManager: test.NilAuthManager,
|
|
||||||
Parent: &testParent{},
|
|
||||||
}
|
}
|
||||||
err := api.Initialize()
|
|
||||||
|
pageCount, err := paginate(&items, "1", "1")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer api.Close()
|
require.Equal(t, 5, pageCount)
|
||||||
|
require.Equal(t, []int{1}, items)
|
||||||
|
|
||||||
tr := &http.Transport{}
|
items = make([]int, 5)
|
||||||
defer tr.CloseIdleConnections()
|
for i := 0; i < 5; i++ {
|
||||||
hc := &http.Client{Transport: tr}
|
items[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodOptions, "http://localhost:9997", nil)
|
pageCount, err = paginate(&items, "3", "2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, pageCount)
|
||||||
|
require.Equal(t, []int{}, items)
|
||||||
|
|
||||||
req.Header.Add("Access-Control-Request-Method", "GET")
|
items = make([]int, 6)
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
items[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
res, err := hc.Do(req)
|
pageCount, err = paginate(&items, "4", "1")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer res.Body.Close()
|
require.Equal(t, 2, pageCount)
|
||||||
|
require.Equal(t, []int{4, 5}, items)
|
||||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
|
||||||
|
|
||||||
byts, err := io.ReadAll(res.Body)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
|
|
||||||
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
|
|
||||||
require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods"))
|
|
||||||
require.Equal(t, "Authorization, Content-Type", res.Header.Get("Access-Control-Allow-Headers"))
|
|
||||||
require.Equal(t, byts, []byte{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigAuth(t *testing.T) {
|
func TestConfigAuth(t *testing.T) {
|
||||||
|
|
@ -284,14 +279,16 @@ func TestConfigPathDefaultsPatch(t *testing.T) {
|
||||||
|
|
||||||
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch",
|
httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"recordFormat": "fmp4",
|
"readUser": "myuser",
|
||||||
|
"readPass": "mypass",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
var out map[string]interface{}
|
var out map[string]interface{}
|
||||||
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out)
|
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out)
|
||||||
require.Equal(t, "fmp4", out["recordFormat"])
|
require.Equal(t, "myuser", out["readUser"])
|
||||||
|
require.Equal(t, "mypass", out["readPass"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigPathsList(t *testing.T) {
|
func TestConfigPathsList(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
go test fuzz v1
|
|
||||||
string("A")
|
|
||||||
string("0")
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
go test fuzz v1
|
|
||||||
string("1")
|
|
||||||
string("A")
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
go test fuzz v1
|
|
||||||
string("0")
|
|
||||||
string("")
|
|
||||||
|
|
@ -94,25 +94,24 @@ func mustParseCIDR(v string) net.IPNet {
|
||||||
return *ne
|
return *ne
|
||||||
}
|
}
|
||||||
|
|
||||||
func anyPathHasDeprecatedCredentials(pathDefaults Path, paths map[string]*OptionalPath) bool {
|
func credentialIsNotEmpty(c *Credential) bool {
|
||||||
if pathDefaults.PublishUser != nil ||
|
return c != nil && *c != ""
|
||||||
pathDefaults.PublishPass != nil ||
|
}
|
||||||
pathDefaults.PublishIPs != nil ||
|
|
||||||
pathDefaults.ReadUser != nil ||
|
|
||||||
pathDefaults.ReadPass != nil ||
|
|
||||||
pathDefaults.ReadIPs != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func ipNetworkIsNotEmpty(i *IPNetworks) bool {
|
||||||
|
return i != nil && len(*i) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyPathHasDeprecatedCredentials(paths map[string]*OptionalPath) bool {
|
||||||
for _, pa := range paths {
|
for _, pa := range paths {
|
||||||
if pa != nil {
|
if pa != nil {
|
||||||
rva := reflect.ValueOf(pa.Values).Elem()
|
rva := reflect.ValueOf(pa.Values).Elem()
|
||||||
if rva.FieldByName("PublishUser").Interface().(*Credential) != nil ||
|
if credentialIsNotEmpty(rva.FieldByName("PublishUser").Interface().(*Credential)) ||
|
||||||
rva.FieldByName("PublishPass").Interface().(*Credential) != nil ||
|
credentialIsNotEmpty(rva.FieldByName("PublishPass").Interface().(*Credential)) ||
|
||||||
rva.FieldByName("PublishIPs").Interface().(*IPNetworks) != nil ||
|
ipNetworkIsNotEmpty(rva.FieldByName("PublishIPs").Interface().(*IPNetworks)) ||
|
||||||
rva.FieldByName("ReadUser").Interface().(*Credential) != nil ||
|
credentialIsNotEmpty(rva.FieldByName("ReadUser").Interface().(*Credential)) ||
|
||||||
rva.FieldByName("ReadPass").Interface().(*Credential) != nil ||
|
credentialIsNotEmpty(rva.FieldByName("ReadPass").Interface().(*Credential)) ||
|
||||||
rva.FieldByName("ReadIPs").Interface().(*IPNetworks) != nil {
|
ipNetworkIsNotEmpty(rva.FieldByName("ReadIPs").Interface().(*IPNetworks)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -120,40 +119,6 @@ func anyPathHasDeprecatedCredentials(pathDefaults Path, paths map[string]*Option
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultAuthInternalUsers = AuthInternalUsers{
|
|
||||||
{
|
|
||||||
User: "any",
|
|
||||||
Pass: "",
|
|
||||||
Permissions: []AuthInternalUserPermission{
|
|
||||||
{
|
|
||||||
Action: AuthActionPublish,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: AuthActionRead,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: AuthActionPlayback,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
User: "any",
|
|
||||||
Pass: "",
|
|
||||||
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
|
|
||||||
Permissions: []AuthInternalUserPermission{
|
|
||||||
{
|
|
||||||
Action: AuthActionAPI,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: AuthActionMetrics,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: AuthActionPprof,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conf is a configuration.
|
// Conf is a configuration.
|
||||||
// WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092
|
// WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092
|
||||||
type Conf struct {
|
type Conf struct {
|
||||||
|
|
@ -273,8 +238,6 @@ type Conf struct {
|
||||||
WebRTCIPsFromInterfacesList []string `json:"webrtcIPsFromInterfacesList"`
|
WebRTCIPsFromInterfacesList []string `json:"webrtcIPsFromInterfacesList"`
|
||||||
WebRTCAdditionalHosts []string `json:"webrtcAdditionalHosts"`
|
WebRTCAdditionalHosts []string `json:"webrtcAdditionalHosts"`
|
||||||
WebRTCICEServers2 WebRTCICEServers `json:"webrtcICEServers2"`
|
WebRTCICEServers2 WebRTCICEServers `json:"webrtcICEServers2"`
|
||||||
WebRTCHandshakeTimeout StringDuration `json:"webrtcHandshakeTimeout"`
|
|
||||||
WebRTCTrackGatherTimeout StringDuration `json:"webrtcTrackGatherTimeout"`
|
|
||||||
WebRTCICEUDPMuxAddress *string `json:"webrtcICEUDPMuxAddress,omitempty"` // deprecated
|
WebRTCICEUDPMuxAddress *string `json:"webrtcICEUDPMuxAddress,omitempty"` // deprecated
|
||||||
WebRTCICETCPMuxAddress *string `json:"webrtcICETCPMuxAddress,omitempty"` // deprecated
|
WebRTCICETCPMuxAddress *string `json:"webrtcICETCPMuxAddress,omitempty"` // deprecated
|
||||||
WebRTCICEHostNAT1To1IPs *[]string `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated
|
WebRTCICEHostNAT1To1IPs *[]string `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated
|
||||||
|
|
@ -311,7 +274,39 @@ func (conf *Conf) setDefaults() {
|
||||||
conf.UDPMaxPayloadSize = 1472
|
conf.UDPMaxPayloadSize = 1472
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
conf.AuthInternalUsers = defaultAuthInternalUsers
|
conf.AuthInternalUsers = []AuthInternalUser{
|
||||||
|
{
|
||||||
|
User: "any",
|
||||||
|
Pass: "",
|
||||||
|
Permissions: []AuthInternalUserPermission{
|
||||||
|
{
|
||||||
|
Action: AuthActionPublish,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: AuthActionRead,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: AuthActionPlayback,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
User: "any",
|
||||||
|
Pass: "",
|
||||||
|
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
|
||||||
|
Permissions: []AuthInternalUserPermission{
|
||||||
|
{
|
||||||
|
Action: AuthActionAPI,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: AuthActionMetrics,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: AuthActionPprof,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
conf.AuthHTTPExclude = []AuthInternalUserPermission{
|
conf.AuthHTTPExclude = []AuthInternalUserPermission{
|
||||||
{
|
{
|
||||||
Action: AuthActionAPI,
|
Action: AuthActionAPI,
|
||||||
|
|
@ -397,8 +392,6 @@ func (conf *Conf) setDefaults() {
|
||||||
conf.WebRTCIPsFromInterfacesList = []string{}
|
conf.WebRTCIPsFromInterfacesList = []string{}
|
||||||
conf.WebRTCAdditionalHosts = []string{}
|
conf.WebRTCAdditionalHosts = []string{}
|
||||||
conf.WebRTCICEServers2 = []WebRTCICEServer{}
|
conf.WebRTCICEServers2 = []WebRTCICEServer{}
|
||||||
conf.WebRTCHandshakeTimeout = 10 * StringDuration(time.Second)
|
|
||||||
conf.WebRTCTrackGatherTimeout = 2 * StringDuration(time.Second)
|
|
||||||
|
|
||||||
// SRT server
|
// SRT server
|
||||||
conf.SRT = true
|
conf.SRT = true
|
||||||
|
|
@ -504,6 +497,7 @@ func (conf *Conf) Validate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
|
|
||||||
if conf.ExternalAuthenticationURL != nil {
|
if conf.ExternalAuthenticationURL != nil {
|
||||||
conf.AuthMethod = AuthMethodHTTP
|
conf.AuthMethod = AuthMethodHTTP
|
||||||
conf.AuthHTTPAddress = *conf.ExternalAuthenticationURL
|
conf.AuthHTTPAddress = *conf.ExternalAuthenticationURL
|
||||||
|
|
@ -519,15 +513,17 @@ func (conf *Conf) Validate() error {
|
||||||
return fmt.Errorf("'authJWTJWKS' must be a HTTP URL")
|
return fmt.Errorf("'authJWTJWKS' must be a HTTP URL")
|
||||||
}
|
}
|
||||||
deprecatedCredentialsMode := false
|
deprecatedCredentialsMode := false
|
||||||
if anyPathHasDeprecatedCredentials(conf.PathDefaults, conf.OptionalPaths) {
|
if credentialIsNotEmpty(conf.PathDefaults.PublishUser) ||
|
||||||
if conf.AuthInternalUsers != nil && !reflect.DeepEqual(conf.AuthInternalUsers, defaultAuthInternalUsers) {
|
credentialIsNotEmpty(conf.PathDefaults.PublishPass) ||
|
||||||
return fmt.Errorf("authInternalUsers and legacy credentials " +
|
ipNetworkIsNotEmpty(conf.PathDefaults.PublishIPs) ||
|
||||||
"(publishUser, publishPass, publishIPs, readUser, readPass, readIPs) cannot be used together")
|
credentialIsNotEmpty(conf.PathDefaults.ReadUser) ||
|
||||||
}
|
credentialIsNotEmpty(conf.PathDefaults.ReadPass) ||
|
||||||
|
ipNetworkIsNotEmpty(conf.PathDefaults.ReadIPs) ||
|
||||||
|
anyPathHasDeprecatedCredentials(conf.OptionalPaths) {
|
||||||
conf.AuthInternalUsers = []AuthInternalUser{
|
conf.AuthInternalUsers = []AuthInternalUser{
|
||||||
{
|
{
|
||||||
User: "any",
|
User: "any",
|
||||||
|
Pass: "",
|
||||||
Permissions: []AuthInternalUserPermission{
|
Permissions: []AuthInternalUserPermission{
|
||||||
{
|
{
|
||||||
Action: AuthActionPlayback,
|
Action: AuthActionPlayback,
|
||||||
|
|
@ -536,6 +532,7 @@ func (conf *Conf) Validate() error {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
User: "any",
|
User: "any",
|
||||||
|
Pass: "",
|
||||||
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
|
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
|
||||||
Permissions: []AuthInternalUserPermission{
|
Permissions: []AuthInternalUserPermission{
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -192,66 +192,6 @@ func TestConfEncryption(t *testing.T) {
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfDeprecatedAuth(t *testing.T) {
|
|
||||||
tmpf, err := createTempFile([]byte(
|
|
||||||
"paths:\n" +
|
|
||||||
" cam:\n" +
|
|
||||||
" readUser: myuser\n" +
|
|
||||||
" readPass: mypass\n"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.Remove(tmpf)
|
|
||||||
|
|
||||||
conf, _, err := Load(tmpf, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, AuthInternalUsers{
|
|
||||||
{
|
|
||||||
User: "any",
|
|
||||||
Permissions: []AuthInternalUserPermission{
|
|
||||||
{
|
|
||||||
Action: AuthActionPlayback,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
User: "any",
|
|
||||||
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
|
|
||||||
Permissions: []AuthInternalUserPermission{
|
|
||||||
{
|
|
||||||
Action: AuthActionAPI,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: AuthActionMetrics,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Action: AuthActionPprof,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
User: "any",
|
|
||||||
IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")},
|
|
||||||
Permissions: []AuthInternalUserPermission{
|
|
||||||
{
|
|
||||||
Action: AuthActionPublish,
|
|
||||||
Path: "cam",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
User: "myuser",
|
|
||||||
Pass: "mypass",
|
|
||||||
IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")},
|
|
||||||
Permissions: []AuthInternalUserPermission{
|
|
||||||
{
|
|
||||||
Action: AuthActionRead,
|
|
||||||
Path: "cam",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, conf.AuthInternalUsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfErrors(t *testing.T) {
|
func TestConfErrors(t *testing.T) {
|
||||||
for _, ca := range []struct {
|
for _, ca := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -180,10 +180,6 @@ type Path struct {
|
||||||
RunOnUnread string `json:"runOnUnread"`
|
RunOnUnread string `json:"runOnUnread"`
|
||||||
RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"`
|
RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"`
|
||||||
RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"`
|
RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"`
|
||||||
|
|
||||||
// Custom hooks
|
|
||||||
HTTPOnReady string `json:"httpOnReady"`
|
|
||||||
HTTPOnNotReady string `json:"httpOnNotReady"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pconf *Path) setDefaults() {
|
func (pconf *Path) setDefaults() {
|
||||||
|
|
@ -403,17 +399,17 @@ func (pconf *Path) validate(
|
||||||
if deprecatedCredentialsMode {
|
if deprecatedCredentialsMode {
|
||||||
func() {
|
func() {
|
||||||
var user Credential = "any"
|
var user Credential = "any"
|
||||||
if pconf.PublishUser != nil && *pconf.PublishUser != "" {
|
if credentialIsNotEmpty(pconf.PublishUser) {
|
||||||
user = *pconf.PublishUser
|
user = *pconf.PublishUser
|
||||||
}
|
}
|
||||||
|
|
||||||
var pass Credential
|
var pass Credential
|
||||||
if pconf.PublishPass != nil && *pconf.PublishPass != "" {
|
if credentialIsNotEmpty(pconf.PublishPass) {
|
||||||
pass = *pconf.PublishPass
|
pass = *pconf.PublishPass
|
||||||
}
|
}
|
||||||
|
|
||||||
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
|
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
|
||||||
if pconf.PublishIPs != nil && len(*pconf.PublishIPs) != 0 {
|
if ipNetworkIsNotEmpty(pconf.PublishIPs) {
|
||||||
ips = *pconf.PublishIPs
|
ips = *pconf.PublishIPs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,17 +431,17 @@ func (pconf *Path) validate(
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
var user Credential = "any"
|
var user Credential = "any"
|
||||||
if pconf.ReadUser != nil && *pconf.ReadUser != "" {
|
if credentialIsNotEmpty(pconf.ReadUser) {
|
||||||
user = *pconf.ReadUser
|
user = *pconf.ReadUser
|
||||||
}
|
}
|
||||||
|
|
||||||
var pass Credential
|
var pass Credential
|
||||||
if pconf.ReadPass != nil && *pconf.ReadPass != "" {
|
if credentialIsNotEmpty(pconf.ReadPass) {
|
||||||
pass = *pconf.ReadPass
|
pass = *pconf.ReadPass
|
||||||
}
|
}
|
||||||
|
|
||||||
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
|
ips := IPNetworks{mustParseCIDR("0.0.0.0/0")}
|
||||||
if pconf.ReadIPs != nil && len(*pconf.ReadIPs) != 0 {
|
if ipNetworkIsNotEmpty(pconf.ReadIPs) {
|
||||||
ips = *pconf.ReadIPs
|
ips = *pconf.ReadIPs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -546,7 +546,7 @@ func TestAPIProtocolListGet(t *testing.T) {
|
||||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{{1}})
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{{1}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = bw.Flush()
|
err = bw.Flush()
|
||||||
|
|
@ -1021,7 +1021,7 @@ func TestAPIProtocolKick(t *testing.T) {
|
||||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{{1}})
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{{1}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = bw.Flush()
|
err = bw.Flush()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import (
|
||||||
"github.com/bluenviron/mediamtx/internal/servers/webrtc"
|
"github.com/bluenviron/mediamtx/internal/servers/webrtc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "v0.0.0"
|
//go:generate go run ./versiongetter
|
||||||
|
|
||||||
var defaultConfPaths = []string{
|
var defaultConfPaths = []string{
|
||||||
"rtsp-simple-server.yml",
|
"rtsp-simple-server.yml",
|
||||||
|
|
@ -580,8 +580,6 @@ func (p *Core) createResources(initial bool) error {
|
||||||
IPsFromInterfacesList: p.conf.WebRTCIPsFromInterfacesList,
|
IPsFromInterfacesList: p.conf.WebRTCIPsFromInterfacesList,
|
||||||
AdditionalHosts: p.conf.WebRTCAdditionalHosts,
|
AdditionalHosts: p.conf.WebRTCAdditionalHosts,
|
||||||
ICEServers: p.conf.WebRTCICEServers2,
|
ICEServers: p.conf.WebRTCICEServers2,
|
||||||
HandshakeTimeout: p.conf.WebRTCHandshakeTimeout,
|
|
||||||
TrackGatherTimeout: p.conf.WebRTCTrackGatherTimeout,
|
|
||||||
ExternalCmdPool: p.externalCmdPool,
|
ExternalCmdPool: p.externalCmdPool,
|
||||||
PathManager: p.pathManager,
|
PathManager: p.pathManager,
|
||||||
Parent: p,
|
Parent: p,
|
||||||
|
|
@ -850,8 +848,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||||
!reflect.DeepEqual(newConf.WebRTCIPsFromInterfacesList, p.conf.WebRTCIPsFromInterfacesList) ||
|
!reflect.DeepEqual(newConf.WebRTCIPsFromInterfacesList, p.conf.WebRTCIPsFromInterfacesList) ||
|
||||||
!reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) ||
|
!reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) ||
|
||||||
!reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
|
!reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
|
||||||
newConf.WebRTCHandshakeTimeout != p.conf.WebRTCHandshakeTimeout ||
|
|
||||||
newConf.WebRTCTrackGatherTimeout != p.conf.WebRTCTrackGatherTimeout ||
|
|
||||||
closeMetrics ||
|
closeMetrics ||
|
||||||
closePathManager ||
|
closePathManager ||
|
||||||
closeLogger
|
closeLogger
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ webrtc_sessions_bytes_sent 0
|
||||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{
|
||||||
test.FormatH264.SPS,
|
test.FormatH264.SPS,
|
||||||
test.FormatH264.PPS,
|
test.FormatH264.PPS,
|
||||||
{0x05, 1}, // IDR
|
{0x05, 1}, // IDR
|
||||||
|
|
|
||||||
|
|
@ -169,19 +169,26 @@ func (pa *path) run() {
|
||||||
if pa.conf.Source == "redirect" {
|
if pa.conf.Source == "redirect" {
|
||||||
pa.source = &sourceRedirect{}
|
pa.source = &sourceRedirect{}
|
||||||
} else if pa.conf.HasStaticSource() {
|
} else if pa.conf.HasStaticSource() {
|
||||||
|
resolvedSource := pa.conf.Source
|
||||||
|
if len(pa.matches) > 1 {
|
||||||
|
for i, ma := range pa.matches[1:] {
|
||||||
|
resolvedSource = strings.ReplaceAll(resolvedSource, "$G"+strconv.FormatInt(int64(i+1), 10), ma)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pa.source = &staticSourceHandler{
|
pa.source = &staticSourceHandler{
|
||||||
conf: pa.conf,
|
conf: pa.conf,
|
||||||
logLevel: pa.logLevel,
|
logLevel: pa.logLevel,
|
||||||
readTimeout: pa.readTimeout,
|
readTimeout: pa.readTimeout,
|
||||||
writeTimeout: pa.writeTimeout,
|
writeTimeout: pa.writeTimeout,
|
||||||
writeQueueSize: pa.writeQueueSize,
|
writeQueueSize: pa.writeQueueSize,
|
||||||
matches: pa.matches,
|
resolvedSource: resolvedSource,
|
||||||
parent: pa,
|
parent: pa,
|
||||||
}
|
}
|
||||||
pa.source.(*staticSourceHandler).initialize()
|
pa.source.(*staticSourceHandler).initialize()
|
||||||
|
|
||||||
if !pa.conf.SourceOnDemand {
|
if !pa.conf.SourceOnDemand {
|
||||||
pa.source.(*staticSourceHandler).start(false, "")
|
pa.source.(*staticSourceHandler).start(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,7 +431,7 @@ func (pa *path) doDescribe(req defs.PathDescribeReq) {
|
||||||
|
|
||||||
if pa.conf.HasOnDemandStaticSource() {
|
if pa.conf.HasOnDemandStaticSource() {
|
||||||
if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
|
if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
|
||||||
pa.onDemandStaticSourceStart(req.AccessRequest.Query)
|
pa.onDemandStaticSourceStart()
|
||||||
}
|
}
|
||||||
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)
|
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)
|
||||||
return
|
return
|
||||||
|
|
@ -532,7 +539,7 @@ func (pa *path) doAddReader(req defs.PathAddReaderReq) {
|
||||||
|
|
||||||
if pa.conf.HasOnDemandStaticSource() {
|
if pa.conf.HasOnDemandStaticSource() {
|
||||||
if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
|
if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
|
||||||
pa.onDemandStaticSourceStart(req.AccessRequest.Query)
|
pa.onDemandStaticSourceStart()
|
||||||
}
|
}
|
||||||
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)
|
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)
|
||||||
return
|
return
|
||||||
|
|
@ -648,8 +655,8 @@ func (pa *path) shouldClose() bool {
|
||||||
len(pa.readerAddRequestsOnHold) == 0
|
len(pa.readerAddRequestsOnHold) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pa *path) onDemandStaticSourceStart(query string) {
|
func (pa *path) onDemandStaticSourceStart() {
|
||||||
pa.source.(*staticSourceHandler).start(true, query)
|
pa.source.(*staticSourceHandler).start(true)
|
||||||
|
|
||||||
pa.onDemandStaticSourceReadyTimer.Stop()
|
pa.onDemandStaticSourceReadyTimer.Stop()
|
||||||
pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))
|
pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))
|
||||||
|
|
@ -799,11 +806,10 @@ func (pa *path) startRecording() {
|
||||||
nil)
|
nil)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnSegmentComplete: func(segmentPath string, segmentDuration time.Duration) {
|
OnSegmentComplete: func(segmentPath string) {
|
||||||
if pa.conf.RunOnRecordSegmentComplete != "" {
|
if pa.conf.RunOnRecordSegmentComplete != "" {
|
||||||
env := pa.ExternalCmdEnv()
|
env := pa.ExternalCmdEnv()
|
||||||
env["MTX_SEGMENT_PATH"] = segmentPath
|
env["MTX_SEGMENT_PATH"] = segmentPath
|
||||||
env["MTX_SEGMENT_DURATION"] = strconv.FormatFloat(segmentDuration.Seconds(), 'f', -1, 64)
|
|
||||||
|
|
||||||
pa.Log(logger.Info, "runOnRecordSegmentComplete command launched")
|
pa.Log(logger.Info, "runOnRecordSegmentComplete command launched")
|
||||||
externalcmd.NewCmd(
|
externalcmd.NewCmd(
|
||||||
|
|
|
||||||
|
|
@ -105,12 +105,12 @@ func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Respo
|
||||||
var _ defs.Path = &path{}
|
var _ defs.Path = &path{}
|
||||||
|
|
||||||
func TestPathRunOnDemand(t *testing.T) {
|
func TestPathRunOnDemand(t *testing.T) {
|
||||||
onDemand := filepath.Join(os.TempDir(), "on_demand")
|
onDemandFile := filepath.Join(os.TempDir(), "ondemand")
|
||||||
onUnDemand := filepath.Join(os.TempDir(), "on_undemand")
|
onUnDemandFile := filepath.Join(os.TempDir(), "onundemand")
|
||||||
|
|
||||||
srcFile := filepath.Join(os.TempDir(), "ondemand.go")
|
srcFile := filepath.Join(os.TempDir(), "ondemand.go")
|
||||||
err := os.WriteFile(srcFile,
|
err := os.WriteFile(srcFile,
|
||||||
[]byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemand)), 0o644)
|
[]byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemandFile)), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
execFile := filepath.Join(os.TempDir(), "ondemand_cmd")
|
execFile := filepath.Join(os.TempDir(), "ondemand_cmd")
|
||||||
|
|
@ -125,8 +125,8 @@ func TestPathRunOnDemand(t *testing.T) {
|
||||||
|
|
||||||
for _, ca := range []string{"describe", "setup", "describe and setup"} {
|
for _, ca := range []string{"describe", "setup", "describe and setup"} {
|
||||||
t.Run(ca, func(t *testing.T) {
|
t.Run(ca, func(t *testing.T) {
|
||||||
defer os.Remove(onDemand)
|
defer os.Remove(onDemandFile)
|
||||||
defer os.Remove(onUnDemand)
|
defer os.Remove(onUnDemandFile)
|
||||||
|
|
||||||
p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
|
p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
|
||||||
"hls: no\n"+
|
"hls: no\n"+
|
||||||
|
|
@ -135,7 +135,7 @@ func TestPathRunOnDemand(t *testing.T) {
|
||||||
" '~^(on)demand$':\n"+
|
" '~^(on)demand$':\n"+
|
||||||
" runOnDemand: %s\n"+
|
" runOnDemand: %s\n"+
|
||||||
" runOnDemandCloseAfter: 1s\n"+
|
" runOnDemandCloseAfter: 1s\n"+
|
||||||
" runOnUnDemand: touch %s\n", execFile, onUnDemand))
|
" runOnUnDemand: touch %s\n", execFile, onUnDemandFile))
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
defer p1.Close()
|
defer p1.Close()
|
||||||
|
|
||||||
|
|
@ -204,14 +204,14 @@ func TestPathRunOnDemand(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_, err := os.Stat(onUnDemand)
|
_, err := os.Stat(onUnDemandFile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := os.Stat(onDemand)
|
_, err := os.Stat(onDemandFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -220,11 +220,11 @@ func TestPathRunOnDemand(t *testing.T) {
|
||||||
func TestPathRunOnConnect(t *testing.T) {
|
func TestPathRunOnConnect(t *testing.T) {
|
||||||
for _, ca := range []string{"rtsp", "rtmp", "srt"} {
|
for _, ca := range []string{"rtsp", "rtmp", "srt"} {
|
||||||
t.Run(ca, func(t *testing.T) {
|
t.Run(ca, func(t *testing.T) {
|
||||||
onConnect := filepath.Join(os.TempDir(), "on_connect")
|
onConnectFile := filepath.Join(os.TempDir(), "onconnect")
|
||||||
defer os.Remove(onConnect)
|
defer os.Remove(onConnectFile)
|
||||||
|
|
||||||
onDisconnect := filepath.Join(os.TempDir(), "on_disconnect")
|
onDisconnectFile := filepath.Join(os.TempDir(), "ondisconnect")
|
||||||
defer os.Remove(onDisconnect)
|
defer os.Remove(onDisconnectFile)
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
p, ok := newInstance(fmt.Sprintf(
|
p, ok := newInstance(fmt.Sprintf(
|
||||||
|
|
@ -232,7 +232,7 @@ func TestPathRunOnConnect(t *testing.T) {
|
||||||
" test:\n"+
|
" test:\n"+
|
||||||
"runOnConnect: touch %s\n"+
|
"runOnConnect: touch %s\n"+
|
||||||
"runOnDisconnect: touch %s\n",
|
"runOnDisconnect: touch %s\n",
|
||||||
onConnect, onDisconnect))
|
onConnectFile, onDisconnectFile))
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
defer p.Close()
|
defer p.Close()
|
||||||
|
|
||||||
|
|
@ -273,21 +273,21 @@ func TestPathRunOnConnect(t *testing.T) {
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, err := os.Stat(onConnect)
|
_, err := os.Stat(onConnectFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.Stat(onDisconnect)
|
_, err = os.Stat(onDisconnectFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPathRunOnReady(t *testing.T) {
|
func TestPathRunOnReady(t *testing.T) {
|
||||||
onReady := filepath.Join(os.TempDir(), "on_ready")
|
onReadyFile := filepath.Join(os.TempDir(), "onready")
|
||||||
defer os.Remove(onReady)
|
defer os.Remove(onReadyFile)
|
||||||
|
|
||||||
onNotReady := filepath.Join(os.TempDir(), "on_unready")
|
onNotReadyFile := filepath.Join(os.TempDir(), "onunready")
|
||||||
defer os.Remove(onNotReady)
|
defer os.Remove(onNotReadyFile)
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
p, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
|
p, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
|
||||||
|
|
@ -297,7 +297,7 @@ func TestPathRunOnReady(t *testing.T) {
|
||||||
" test:\n"+
|
" test:\n"+
|
||||||
" runOnReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
|
" runOnReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
|
||||||
" runOnNotReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
|
" runOnNotReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
|
||||||
onReady, onNotReady))
|
onReadyFile, onNotReadyFile))
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
defer p.Close()
|
defer p.Close()
|
||||||
|
|
||||||
|
|
@ -312,11 +312,11 @@ func TestPathRunOnReady(t *testing.T) {
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
byts, err := os.ReadFile(onReady)
|
byts, err := os.ReadFile(onReadyFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "test query=value\n", string(byts))
|
require.Equal(t, "test query=value\n", string(byts))
|
||||||
|
|
||||||
byts, err = os.ReadFile(onNotReady)
|
byts, err = os.ReadFile(onNotReadyFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "test query=value\n", string(byts))
|
require.Equal(t, "test query=value\n", string(byts))
|
||||||
}
|
}
|
||||||
|
|
@ -324,11 +324,11 @@ func TestPathRunOnReady(t *testing.T) {
|
||||||
func TestPathRunOnRead(t *testing.T) {
|
func TestPathRunOnRead(t *testing.T) {
|
||||||
for _, ca := range []string{"rtsp", "rtmp", "srt", "webrtc"} {
|
for _, ca := range []string{"rtsp", "rtmp", "srt", "webrtc"} {
|
||||||
t.Run(ca, func(t *testing.T) {
|
t.Run(ca, func(t *testing.T) {
|
||||||
onRead := filepath.Join(os.TempDir(), "on_read")
|
onReadFile := filepath.Join(os.TempDir(), "onread")
|
||||||
defer os.Remove(onRead)
|
defer os.Remove(onReadFile)
|
||||||
|
|
||||||
onUnread := filepath.Join(os.TempDir(), "on_unread")
|
onUnreadFile := filepath.Join(os.TempDir(), "onunread")
|
||||||
defer os.Remove(onUnread)
|
defer os.Remove(onUnreadFile)
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
p, ok := newInstance(fmt.Sprintf(
|
p, ok := newInstance(fmt.Sprintf(
|
||||||
|
|
@ -336,7 +336,7 @@ func TestPathRunOnRead(t *testing.T) {
|
||||||
" test:\n"+
|
" test:\n"+
|
||||||
" runOnRead: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
|
" runOnRead: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
|
||||||
" runOnUnread: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
|
" runOnUnread: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
|
||||||
onRead, onUnread))
|
onReadFile, onUnreadFile))
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
defer p.Close()
|
defer p.Close()
|
||||||
|
|
||||||
|
|
@ -449,79 +449,17 @@ func TestPathRunOnRead(t *testing.T) {
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
byts, err := os.ReadFile(onRead)
|
byts, err := os.ReadFile(onReadFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "test query=value\n", string(byts))
|
require.Equal(t, "test query=value\n", string(byts))
|
||||||
|
|
||||||
byts, err = os.ReadFile(onUnread)
|
byts, err = os.ReadFile(onUnreadFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "test query=value\n", string(byts))
|
require.Equal(t, "test query=value\n", string(byts))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPathRunOnRecordSegment(t *testing.T) {
|
|
||||||
onRecordSegmentCreate := filepath.Join(os.TempDir(), "on_record_segment_create")
|
|
||||||
defer os.Remove(onRecordSegmentCreate)
|
|
||||||
|
|
||||||
onRecordSegmentComplete := filepath.Join(os.TempDir(), "on_record_segment_complete")
|
|
||||||
defer os.Remove(onRecordSegmentComplete)
|
|
||||||
|
|
||||||
recordDir, err := os.MkdirTemp("", "rtsp-path-record")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.RemoveAll(recordDir)
|
|
||||||
|
|
||||||
func() {
|
|
||||||
p, ok := newInstance("record: yes\n" +
|
|
||||||
"recordPath: " + filepath.Join(recordDir, "%path/%Y-%m-%d_%H-%M-%S-%f") + "\n" +
|
|
||||||
"paths:\n" +
|
|
||||||
" test:\n" +
|
|
||||||
" runOnRecordSegmentCreate: " +
|
|
||||||
"sh -c 'echo \"$MTX_SEGMENT_PATH\" > " + onRecordSegmentCreate + "'\n" +
|
|
||||||
" runOnRecordSegmentComplete: " +
|
|
||||||
"sh -c 'echo \"$MTX_SEGMENT_PATH $MTX_SEGMENT_DURATION\" > " + onRecordSegmentComplete + "'\n")
|
|
||||||
require.Equal(t, true, ok)
|
|
||||||
defer p.Close()
|
|
||||||
|
|
||||||
media0 := test.UniqueMediaH264()
|
|
||||||
|
|
||||||
source := gortsplib.Client{}
|
|
||||||
|
|
||||||
err = source.StartRecording(
|
|
||||||
"rtsp://localhost:8554/test",
|
|
||||||
&description.Session{Medias: []*description.Media{media0}})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer source.Close()
|
|
||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
err = source.WritePacketRTP(media0, &rtp.Packet{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: true,
|
|
||||||
PayloadType: 96,
|
|
||||||
SequenceNumber: 1123 + uint16(i),
|
|
||||||
Timestamp: 45343 + 90000*uint32(i),
|
|
||||||
SSRC: 563423,
|
|
||||||
},
|
|
||||||
Payload: []byte{5},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}()
|
|
||||||
|
|
||||||
byts, err := os.ReadFile(onRecordSegmentCreate)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, true, strings.HasPrefix(string(byts), recordDir))
|
|
||||||
|
|
||||||
byts, err = os.ReadFile(onRecordSegmentComplete)
|
|
||||||
require.NoError(t, err)
|
|
||||||
parts := strings.Split(string(byts[:len(byts)-1]), " ")
|
|
||||||
require.Equal(t, true, strings.HasPrefix(parts[0], recordDir))
|
|
||||||
require.Equal(t, "3", parts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPathMaxReaders(t *testing.T) {
|
func TestPathMaxReaders(t *testing.T) {
|
||||||
p, ok := newInstance("paths:\n" +
|
p, ok := newInstance("paths:\n" +
|
||||||
" all_others:\n" +
|
" all_others:\n" +
|
||||||
|
|
@ -700,14 +638,13 @@ func TestPathFallback(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPathResolveSource(t *testing.T) {
|
func TestPathSourceRegexp(t *testing.T) {
|
||||||
var stream *gortsplib.ServerStream
|
var stream *gortsplib.ServerStream
|
||||||
|
|
||||||
s := gortsplib.Server{
|
s := gortsplib.Server{
|
||||||
Handler: &testServer{
|
Handler: &testServer{
|
||||||
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||||
) (*base.Response, *gortsplib.ServerStream, error) {
|
) (*base.Response, *gortsplib.ServerStream, error) {
|
||||||
require.Equal(t, "key=val", ctx.Query)
|
|
||||||
require.Equal(t, "/a", ctx.Path)
|
require.Equal(t, "/a", ctx.Path)
|
||||||
return &base.Response{
|
return &base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
|
|
@ -737,7 +674,7 @@ func TestPathResolveSource(t *testing.T) {
|
||||||
p, ok := newInstance(
|
p, ok := newInstance(
|
||||||
"paths:\n" +
|
"paths:\n" +
|
||||||
" '~^test_(.+)$':\n" +
|
" '~^test_(.+)$':\n" +
|
||||||
" source: rtsp://127.0.0.1:8555/$G1?$MTX_QUERY\n" +
|
" source: rtsp://127.0.0.1:8555/$G1\n" +
|
||||||
" sourceOnDemand: yes\n" +
|
" sourceOnDemand: yes\n" +
|
||||||
" 'all':\n")
|
" 'all':\n")
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
|
|
@ -745,7 +682,7 @@ func TestPathResolveSource(t *testing.T) {
|
||||||
|
|
||||||
reader := gortsplib.Client{}
|
reader := gortsplib.Client{}
|
||||||
|
|
||||||
u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a?key=val")
|
u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = reader.Start(u.Scheme, u.Host)
|
err = reader.Start(u.Scheme, u.Host)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package core
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -23,18 +22,6 @@ const (
|
||||||
staticSourceHandlerRetryPause = 5 * time.Second
|
staticSourceHandlerRetryPause = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolveSource(s string, matches []string, query string) string {
|
|
||||||
if len(matches) > 1 {
|
|
||||||
for i, ma := range matches[1:] {
|
|
||||||
s = strings.ReplaceAll(s, "$G"+strconv.FormatInt(int64(i+1), 10), ma)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s = strings.ReplaceAll(s, "$MTX_QUERY", query)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
type staticSourceHandlerParent interface {
|
type staticSourceHandlerParent interface {
|
||||||
logger.Writer
|
logger.Writer
|
||||||
staticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)
|
staticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)
|
||||||
|
|
@ -48,14 +35,13 @@ type staticSourceHandler struct {
|
||||||
readTimeout conf.StringDuration
|
readTimeout conf.StringDuration
|
||||||
writeTimeout conf.StringDuration
|
writeTimeout conf.StringDuration
|
||||||
writeQueueSize int
|
writeQueueSize int
|
||||||
matches []string
|
resolvedSource string
|
||||||
parent staticSourceHandlerParent
|
parent staticSourceHandlerParent
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ctxCancel func()
|
ctxCancel func()
|
||||||
instance defs.StaticSource
|
instance defs.StaticSource
|
||||||
running bool
|
running bool
|
||||||
query string
|
|
||||||
|
|
||||||
// in
|
// in
|
||||||
chReloadConf chan *conf.Path
|
chReloadConf chan *conf.Path
|
||||||
|
|
@ -72,57 +58,60 @@ func (s *staticSourceHandler) initialize() {
|
||||||
s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
|
s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(s.conf.Source, "rtsp://") ||
|
case strings.HasPrefix(s.resolvedSource, "rtsp://") ||
|
||||||
strings.HasPrefix(s.conf.Source, "rtsps://"):
|
strings.HasPrefix(s.resolvedSource, "rtsps://"):
|
||||||
s.instance = &rtspsource.Source{
|
s.instance = &rtspsource.Source{
|
||||||
|
ResolvedSource: s.resolvedSource,
|
||||||
ReadTimeout: s.readTimeout,
|
ReadTimeout: s.readTimeout,
|
||||||
WriteTimeout: s.writeTimeout,
|
WriteTimeout: s.writeTimeout,
|
||||||
WriteQueueSize: s.writeQueueSize,
|
WriteQueueSize: s.writeQueueSize,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(s.conf.Source, "rtmp://") ||
|
case strings.HasPrefix(s.resolvedSource, "rtmp://") ||
|
||||||
strings.HasPrefix(s.conf.Source, "rtmps://"):
|
strings.HasPrefix(s.resolvedSource, "rtmps://"):
|
||||||
s.instance = &rtmpsource.Source{
|
s.instance = &rtmpsource.Source{
|
||||||
ReadTimeout: s.readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
WriteTimeout: s.writeTimeout,
|
ReadTimeout: s.readTimeout,
|
||||||
Parent: s,
|
WriteTimeout: s.writeTimeout,
|
||||||
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(s.conf.Source, "http://") ||
|
case strings.HasPrefix(s.resolvedSource, "http://") ||
|
||||||
strings.HasPrefix(s.conf.Source, "https://"):
|
strings.HasPrefix(s.resolvedSource, "https://"):
|
||||||
s.instance = &hlssource.Source{
|
s.instance = &hlssource.Source{
|
||||||
ReadTimeout: s.readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
Parent: s,
|
ReadTimeout: s.readTimeout,
|
||||||
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(s.conf.Source, "udp://"):
|
case strings.HasPrefix(s.resolvedSource, "udp://"):
|
||||||
s.instance = &udpsource.Source{
|
s.instance = &udpsource.Source{
|
||||||
ReadTimeout: s.readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
Parent: s,
|
ReadTimeout: s.readTimeout,
|
||||||
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(s.conf.Source, "srt://"):
|
case strings.HasPrefix(s.resolvedSource, "srt://"):
|
||||||
s.instance = &srtsource.Source{
|
s.instance = &srtsource.Source{
|
||||||
ReadTimeout: s.readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
Parent: s,
|
ReadTimeout: s.readTimeout,
|
||||||
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(s.conf.Source, "whep://") ||
|
case strings.HasPrefix(s.resolvedSource, "whep://") ||
|
||||||
strings.HasPrefix(s.conf.Source, "wheps://"):
|
strings.HasPrefix(s.resolvedSource, "wheps://"):
|
||||||
s.instance = &webrtcsource.Source{
|
s.instance = &webrtcsource.Source{
|
||||||
ReadTimeout: s.readTimeout,
|
ResolvedSource: s.resolvedSource,
|
||||||
Parent: s,
|
ReadTimeout: s.readTimeout,
|
||||||
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
case s.conf.Source == "rpiCamera":
|
case s.resolvedSource == "rpiCamera":
|
||||||
s.instance = &rpicamerasource.Source{
|
s.instance = &rpicamerasource.Source{
|
||||||
LogLevel: s.logLevel,
|
LogLevel: s.logLevel,
|
||||||
Parent: s,
|
Parent: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
|
||||||
panic("should not happen")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,16 +119,12 @@ func (s *staticSourceHandler) close(reason string) {
|
||||||
s.stop(reason)
|
s.stop(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *staticSourceHandler) start(onDemand bool, query string) {
|
func (s *staticSourceHandler) start(onDemand bool) {
|
||||||
if s.running {
|
if s.running {
|
||||||
panic("should not happen")
|
panic("should not happen")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.running = true
|
s.running = true
|
||||||
s.query = query
|
|
||||||
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
|
|
||||||
s.done = make(chan struct{})
|
|
||||||
|
|
||||||
s.instance.Log(logger.Info, "started%s",
|
s.instance.Log(logger.Info, "started%s",
|
||||||
func() string {
|
func() string {
|
||||||
if onDemand {
|
if onDemand {
|
||||||
|
|
@ -148,6 +133,9 @@ func (s *staticSourceHandler) start(onDemand bool, query string) {
|
||||||
return ""
|
return ""
|
||||||
}())
|
}())
|
||||||
|
|
||||||
|
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
s.done = make(chan struct{})
|
||||||
|
|
||||||
go s.run()
|
go s.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,7 +145,6 @@ func (s *staticSourceHandler) stop(reason string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s.running = false
|
s.running = false
|
||||||
|
|
||||||
s.instance.Log(logger.Info, "stopped: %s", reason)
|
s.instance.Log(logger.Info, "stopped: %s", reason)
|
||||||
|
|
||||||
s.ctxCancel()
|
s.ctxCancel()
|
||||||
|
|
@ -180,15 +167,12 @@ func (s *staticSourceHandler) run() {
|
||||||
runReloadConf := make(chan *conf.Path)
|
runReloadConf := make(chan *conf.Path)
|
||||||
|
|
||||||
recreate := func() {
|
recreate := func() {
|
||||||
resolvedSource := resolveSource(s.conf.Source, s.matches, s.query)
|
|
||||||
|
|
||||||
runCtx, runCtxCancel = context.WithCancel(context.Background())
|
runCtx, runCtxCancel = context.WithCancel(context.Background())
|
||||||
go func() {
|
go func() {
|
||||||
runErr <- s.instance.Run(defs.StaticSourceRunParams{
|
runErr <- s.instance.Run(defs.StaticSourceRunParams{
|
||||||
Context: runCtx,
|
Context: runCtx,
|
||||||
ResolvedSource: resolvedSource,
|
Conf: s.conf,
|
||||||
Conf: s.conf,
|
ReloadConf: runReloadConf,
|
||||||
ReloadConf: runReloadConf,
|
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
internal/core/versiongetter/main.go
Normal file
63
internal/core/versiongetter/main.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Package main contains an utility to get the server version
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tpl = template.Must(template.New("").Parse(
|
||||||
|
`// autogenerated:yes
|
||||||
|
package core
|
||||||
|
|
||||||
|
const version = "{{ .Version }}"
|
||||||
|
`))
|
||||||
|
|
||||||
|
func do() error {
|
||||||
|
log.Println("getting version...")
|
||||||
|
|
||||||
|
temp, _ := exec.Command("git", "status").CombinedOutput()
|
||||||
|
/*if err != nil {
|
||||||
|
return err
|
||||||
|
}*/
|
||||||
|
|
||||||
|
fmt.Println(string(temp))
|
||||||
|
|
||||||
|
stdout, err := exec.Command("git", "describe", "--tags").Output()
|
||||||
|
|
||||||
|
fmt.Println(string(stdout))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
version := string(stdout[:len(stdout)-1])
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = tpl.Execute(&buf, map[string]interface{}{
|
||||||
|
"Version": version,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile("version.go", buf.Bytes(), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("ok")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := do()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERR: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,8 +23,7 @@ type StaticSourceParent interface {
|
||||||
|
|
||||||
// StaticSourceRunParams is the set of params passed to Run().
|
// StaticSourceRunParams is the set of params passed to Run().
|
||||||
type StaticSourceRunParams struct {
|
type StaticSourceRunParams struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
ResolvedSource string
|
Conf *conf.Path
|
||||||
Conf *conf.Path
|
ReloadConf chan *conf.Path
|
||||||
ReloadConf chan *conf.Path
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ func newGeneric(
|
||||||
generateRTPPackets bool,
|
generateRTPPackets bool,
|
||||||
) (*formatProcessorGeneric, error) {
|
) (*formatProcessorGeneric, error) {
|
||||||
if generateRTPPackets {
|
if generateRTPPackets {
|
||||||
return nil, fmt.Errorf("we don't know how to generate RTP packets of format %T", forma)
|
return nil, fmt.Errorf("we don't know how to generate RTP packets of format %+v", forma)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &formatProcessorGeneric{
|
return &formatProcessorGeneric{
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
package hooks
|
package hooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// OnReadyParams are the parameters of OnReady.
|
// OnReadyParams are the parameters of OnReady.
|
||||||
|
|
@ -44,16 +41,6 @@ func OnReady(params OnReadyParams) func() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.Conf.HTTPOnReady != "" {
|
|
||||||
obj := map[string]any{
|
|
||||||
"query": params.Query,
|
|
||||||
"desc": params.Desc,
|
|
||||||
"env": params.ExternalCmdEnv,
|
|
||||||
}
|
|
||||||
jsonValue, _ := json.Marshal(obj)
|
|
||||||
http.Post(params.Conf.HTTPOnReady, "application/json", bytes.NewBuffer(jsonValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
if onReadyCmd != nil {
|
if onReadyCmd != nil {
|
||||||
onReadyCmd.Close()
|
onReadyCmd.Close()
|
||||||
|
|
@ -69,15 +56,5 @@ func OnReady(params OnReadyParams) func() {
|
||||||
env,
|
env,
|
||||||
nil)
|
nil)
|
||||||
}
|
}
|
||||||
if params.Conf.HTTPOnNotReady != "" {
|
|
||||||
obj := map[string]any{
|
|
||||||
"query": params.Query,
|
|
||||||
"desc": params.Desc,
|
|
||||||
"env": params.ExternalCmdEnv,
|
|
||||||
}
|
|
||||||
jsonValue, _ := json.Marshal(obj)
|
|
||||||
http.Post(params.Conf.HTTPOnNotReady, "application/json", bytes.NewBuffer(jsonValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,15 +107,6 @@ func (m *Metrics) onRequest(ctx *gin.Context) {
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", m.AllowOrigin)
|
ctx.Writer.Header().Set("Access-Control-Allow-Origin", m.AllowOrigin)
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
|
||||||
// preflight requests
|
|
||||||
if ctx.Request.Method == http.MethodOptions &&
|
|
||||||
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
|
||||||
ctx.Writer.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.Request.URL.Path != "/metrics" || ctx.Request.Method != http.MethodGet {
|
if ctx.Request.URL.Path != "/metrics" || ctx.Request.Method != http.MethodGet {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{})
|
|
||||||
}
|
|
||||||
|
|
@ -43,10 +43,7 @@ type Server struct {
|
||||||
func (s *Server) Initialize() error {
|
func (s *Server) Initialize() error {
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
|
router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
|
||||||
|
|
||||||
router.NoRoute(s.middlewareOrigin)
|
|
||||||
group := router.Group("/", s.middlewareOrigin)
|
group := router.Group("/", s.middlewareOrigin)
|
||||||
|
|
||||||
group.GET("/list", s.onList)
|
group.GET("/list", s.onList)
|
||||||
group.GET("/get", s.onGet)
|
group.GET("/get", s.onGet)
|
||||||
|
|
||||||
|
|
@ -109,15 +106,6 @@ func (s *Server) safeFindPathConf(name string) (*conf.Path, error) {
|
||||||
func (s *Server) middlewareOrigin(ctx *gin.Context) {
|
func (s *Server) middlewareOrigin(ctx *gin.Context) {
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin)
|
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin)
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
|
||||||
// preflight requests
|
|
||||||
if ctx.Request.Method == http.MethodOptions &&
|
|
||||||
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
|
||||||
ctx.AbortWithStatus(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
|
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
|
||||||
|
|
|
||||||
|
|
@ -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{})
|
|
||||||
}
|
|
||||||
|
|
@ -83,15 +83,6 @@ func (pp *PPROF) onRequest(ctx *gin.Context) {
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Origin", pp.AllowOrigin)
|
ctx.Writer.Header().Set("Access-Control-Allow-Origin", pp.AllowOrigin)
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
|
||||||
// preflight requests
|
|
||||||
if ctx.Request.Method == http.MethodOptions &&
|
|
||||||
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
|
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
|
||||||
ctx.Writer.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
||||||
|
|
||||||
err := pp.AuthManager.Authenticate(&auth.Request{
|
err := pp.AuthManager.Authenticate(&auth.Request{
|
||||||
|
|
|
||||||
|
|
@ -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{})
|
|
||||||
}
|
|
||||||
|
|
@ -69,7 +69,7 @@ func FromStream(
|
||||||
}
|
}
|
||||||
|
|
||||||
sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
||||||
err = (*w).WriteH265(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
|
err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ func FromStream(
|
||||||
}
|
}
|
||||||
|
|
||||||
sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
||||||
err = (*w).WriteH264(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU)
|
err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, e
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
r.OnDataH265(track, func(pts int64, _ int64, au [][]byte) error {
|
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{
|
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
NTP: time.Now(),
|
NTP: time.Now(),
|
||||||
|
|
@ -61,7 +61,7 @@ func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, e
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
r.OnDataH264(track, func(pts int64, _ int64, au [][]byte) error {
|
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{
|
(*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
NTP: time.Now(),
|
NTP: time.Now(),
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasVideo && !hasAudio {
|
if !hasVideo && !hasAudio {
|
||||||
return nil, nil, nil
|
return nil, nil, fmt.Errorf("metadata doesn't contain any track")
|
||||||
}
|
}
|
||||||
|
|
||||||
firstReceived := false
|
firstReceived := false
|
||||||
|
|
@ -327,9 +327,6 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
|
||||||
}
|
}
|
||||||
|
|
||||||
if audioTrack == nil {
|
if audioTrack == nil {
|
||||||
if len(msg.Payload) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch {
|
switch {
|
||||||
case msg.Codec == message.CodecMPEG4Audio &&
|
case msg.Codec == message.CodecMPEG4Audio &&
|
||||||
msg.AACType == message.AudioAACTypeConfig:
|
msg.AACType == message.AudioAACTypeConfig:
|
||||||
|
|
@ -523,9 +520,7 @@ func (r *Reader) readTracks() (format.Format, format.Format, error) {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if videoTrack != nil || audioTrack != nil {
|
return videoTrack, audioTrack, nil
|
||||||
return videoTrack, audioTrack, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -244,64 +244,6 @@ func TestReadTracks(t *testing.T) {
|
||||||
return buf
|
return buf
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
&message.Audio{
|
|
||||||
ChunkStreamID: message.AudioChunkStreamID,
|
|
||||||
MessageStreamID: 0x1000000,
|
|
||||||
Codec: message.CodecMPEG4Audio,
|
|
||||||
Rate: message.Rate44100,
|
|
||||||
Depth: message.Depth16,
|
|
||||||
IsStereo: true,
|
|
||||||
AACType: message.AudioAACTypeConfig,
|
|
||||||
Payload: func() []byte {
|
|
||||||
enc, err2 := mpeg4audio.Config{
|
|
||||||
Type: 2,
|
|
||||||
SampleRate: 44100,
|
|
||||||
ChannelCount: 2,
|
|
||||||
}.Marshal()
|
|
||||||
require.NoError(t, err2)
|
|
||||||
return enc
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"h264 + aac, issue mediamtx/3301 (metadata without tracks)",
|
|
||||||
&format.H264{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
SPS: test.FormatH264.SPS,
|
|
||||||
PPS: test.FormatH264.PPS,
|
|
||||||
PacketizationMode: 1,
|
|
||||||
},
|
|
||||||
&format.MPEG4Audio{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
Config: &mpeg4audio.Config{
|
|
||||||
Type: 2,
|
|
||||||
SampleRate: 44100,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
SizeLength: 13,
|
|
||||||
IndexLength: 3,
|
|
||||||
IndexDeltaLength: 3,
|
|
||||||
},
|
|
||||||
[]message.Message{
|
|
||||||
&message.DataAMF0{
|
|
||||||
ChunkStreamID: 4,
|
|
||||||
MessageStreamID: 1,
|
|
||||||
Payload: []interface{}{
|
|
||||||
"@setDataFrame",
|
|
||||||
"onMetaData",
|
|
||||||
amf0.Object{
|
|
||||||
{
|
|
||||||
Key: "metadatacreator",
|
|
||||||
Value: "Agora.io SDK",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "encoder",
|
|
||||||
Value: "Agora.io Encoder",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&message.Video{
|
&message.Video{
|
||||||
ChunkStreamID: message.VideoChunkStreamID,
|
ChunkStreamID: message.VideoChunkStreamID,
|
||||||
MessageStreamID: 0x1000000,
|
MessageStreamID: 0x1000000,
|
||||||
|
|
@ -390,77 +332,6 @@ func TestReadTracks(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"aac, issue mediamtx/3414 (empty audio payload)",
|
|
||||||
nil,
|
|
||||||
&format.MPEG4Audio{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
Config: &mpeg4audio.Config{
|
|
||||||
Type: 2,
|
|
||||||
SampleRate: 44100,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
SizeLength: 13,
|
|
||||||
IndexLength: 3,
|
|
||||||
IndexDeltaLength: 3,
|
|
||||||
},
|
|
||||||
[]message.Message{
|
|
||||||
&message.DataAMF0{
|
|
||||||
ChunkStreamID: 4,
|
|
||||||
MessageStreamID: 1,
|
|
||||||
Payload: []interface{}{
|
|
||||||
"@setDataFrame",
|
|
||||||
"onMetaData",
|
|
||||||
amf0.Object{
|
|
||||||
{
|
|
||||||
Key: "videodatarate",
|
|
||||||
Value: float64(0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "videocodecid",
|
|
||||||
Value: float64(0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "audiodatarate",
|
|
||||||
Value: float64(0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "audiocodecid",
|
|
||||||
Value: float64(message.CodecMPEG4Audio),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&message.Audio{
|
|
||||||
ChunkStreamID: message.AudioChunkStreamID,
|
|
||||||
MessageStreamID: 0x1000000,
|
|
||||||
Codec: message.CodecMPEG4Audio,
|
|
||||||
Rate: message.Rate44100,
|
|
||||||
Depth: message.Depth16,
|
|
||||||
IsStereo: true,
|
|
||||||
AACType: message.AudioAACTypeConfig,
|
|
||||||
Payload: nil,
|
|
||||||
},
|
|
||||||
&message.Audio{
|
|
||||||
ChunkStreamID: message.AudioChunkStreamID,
|
|
||||||
MessageStreamID: 0x1000000,
|
|
||||||
Codec: message.CodecMPEG4Audio,
|
|
||||||
Rate: message.Rate44100,
|
|
||||||
Depth: message.Depth16,
|
|
||||||
IsStereo: true,
|
|
||||||
AACType: message.AudioAACTypeConfig,
|
|
||||||
Payload: func() []byte {
|
|
||||||
enc, err2 := mpeg4audio.Config{
|
|
||||||
Type: 2,
|
|
||||||
SampleRate: 44100,
|
|
||||||
ChannelCount: 2,
|
|
||||||
}.Marshal()
|
|
||||||
require.NoError(t, err2)
|
|
||||||
return enc
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"h265 + aac, obs studio pre 29.1 h265",
|
"h265 + aac, obs studio pre 29.1 h265",
|
||||||
&format.H265{
|
&format.H265{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
|
"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer"
|
"github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer"
|
||||||
|
|
@ -20,50 +19,13 @@ const (
|
||||||
keyFrameInterval = 2 * time.Second
|
keyFrameInterval = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
mimeTypeMultiopus = "audio/multiopus"
|
|
||||||
mimeTypeL16 = "audio/L16"
|
|
||||||
)
|
|
||||||
|
|
||||||
var incomingVideoCodecs = []webrtc.RTPCodecParameters{
|
var incomingVideoCodecs = []webrtc.RTPCodecParameters{
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypeAV1,
|
|
||||||
ClockRate: 90000,
|
|
||||||
SDPFmtpLine: "profile=1",
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
MimeType: webrtc.MimeTypeAV1,
|
MimeType: webrtc.MimeTypeAV1,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
},
|
},
|
||||||
PayloadType: 97,
|
PayloadType: 96,
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypeVP9,
|
|
||||||
ClockRate: 90000,
|
|
||||||
SDPFmtpLine: "profile-id=3",
|
|
||||||
},
|
|
||||||
PayloadType: 98,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypeVP9,
|
|
||||||
ClockRate: 90000,
|
|
||||||
SDPFmtpLine: "profile-id=2",
|
|
||||||
},
|
|
||||||
PayloadType: 99,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypeVP9,
|
|
||||||
ClockRate: 90000,
|
|
||||||
SDPFmtpLine: "profile-id=1",
|
|
||||||
},
|
|
||||||
PayloadType: 100,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
|
|
@ -71,21 +33,22 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
SDPFmtpLine: "profile-id=0",
|
SDPFmtpLine: "profile-id=0",
|
||||||
},
|
},
|
||||||
PayloadType: 101,
|
PayloadType: 97,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
|
MimeType: webrtc.MimeTypeVP9,
|
||||||
|
ClockRate: 90000,
|
||||||
|
SDPFmtpLine: "profile-id=1",
|
||||||
|
},
|
||||||
|
PayloadType: 98,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
MimeType: webrtc.MimeTypeVP8,
|
MimeType: webrtc.MimeTypeVP8,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
},
|
},
|
||||||
PayloadType: 102,
|
PayloadType: 99,
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypeH265,
|
|
||||||
ClockRate: 90000,
|
|
||||||
},
|
|
||||||
PayloadType: 103,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
|
|
@ -93,7 +56,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
||||||
},
|
},
|
||||||
PayloadType: 104,
|
PayloadType: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
|
|
@ -101,65 +64,11 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||||
},
|
},
|
||||||
PayloadType: 105,
|
PayloadType: 101,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var incomingAudioCodecs = []webrtc.RTPCodecParameters{
|
var incomingAudioCodecs = []webrtc.RTPCodecParameters{
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeMultiopus,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 3,
|
|
||||||
SDPFmtpLine: "channel_mapping=0,2,1;num_streams=2;coupled_streams=1",
|
|
||||||
},
|
|
||||||
PayloadType: 112,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeMultiopus,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 4,
|
|
||||||
SDPFmtpLine: "channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2",
|
|
||||||
},
|
|
||||||
PayloadType: 113,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeMultiopus,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 5,
|
|
||||||
SDPFmtpLine: "channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2",
|
|
||||||
},
|
|
||||||
PayloadType: 114,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeMultiopus,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 6,
|
|
||||||
SDPFmtpLine: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2",
|
|
||||||
},
|
|
||||||
PayloadType: 115,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeMultiopus,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 7,
|
|
||||||
SDPFmtpLine: "channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4",
|
|
||||||
},
|
|
||||||
PayloadType: 116,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeMultiopus,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 8,
|
|
||||||
SDPFmtpLine: "channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4",
|
|
||||||
},
|
|
||||||
PayloadType: 117,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
MimeType: webrtc.MimeTypeOpus,
|
MimeType: webrtc.MimeTypeOpus,
|
||||||
|
|
@ -176,22 +85,6 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
|
||||||
},
|
},
|
||||||
PayloadType: 9,
|
PayloadType: 9,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypePCMU,
|
|
||||||
ClockRate: 8000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
PayloadType: 118,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypePCMA,
|
|
||||||
ClockRate: 8000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
PayloadType: 119,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
MimeType: webrtc.MimeTypePCMU,
|
MimeType: webrtc.MimeTypePCMU,
|
||||||
|
|
@ -206,30 +99,6 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
|
||||||
},
|
},
|
||||||
PayloadType: 8,
|
PayloadType: 8,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeL16,
|
|
||||||
ClockRate: 8000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
PayloadType: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeL16,
|
|
||||||
ClockRate: 16000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
PayloadType: 121,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeL16,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
PayloadType: 122,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IncomingTrack is an incoming track.
|
// IncomingTrack is an incoming track.
|
||||||
|
|
@ -237,7 +106,6 @@ type IncomingTrack struct {
|
||||||
track *webrtc.TrackRemote
|
track *webrtc.TrackRemote
|
||||||
log logger.Writer
|
log logger.Writer
|
||||||
|
|
||||||
typ description.MediaType
|
|
||||||
format format.Format
|
format format.Format
|
||||||
reorderer *rtpreorderer.Reorderer
|
reorderer *rtpreorderer.Reorderer
|
||||||
pkts []*rtp.Packet
|
pkts []*rtp.Packet
|
||||||
|
|
@ -255,47 +123,35 @@ func newIncomingTrack(
|
||||||
reorderer: rtpreorderer.New(),
|
reorderer: rtpreorderer.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isVideo := false
|
||||||
|
|
||||||
switch strings.ToLower(track.Codec().MimeType) {
|
switch strings.ToLower(track.Codec().MimeType) {
|
||||||
case strings.ToLower(webrtc.MimeTypeAV1):
|
case strings.ToLower(webrtc.MimeTypeAV1):
|
||||||
t.typ = description.MediaTypeVideo
|
isVideo = true
|
||||||
t.format = &format.AV1{
|
t.format = &format.AV1{
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
PayloadTyp: uint8(track.PayloadType()),
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeVP9):
|
case strings.ToLower(webrtc.MimeTypeVP9):
|
||||||
t.typ = description.MediaTypeVideo
|
isVideo = true
|
||||||
t.format = &format.VP9{
|
t.format = &format.VP9{
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
PayloadTyp: uint8(track.PayloadType()),
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeVP8):
|
case strings.ToLower(webrtc.MimeTypeVP8):
|
||||||
t.typ = description.MediaTypeVideo
|
isVideo = true
|
||||||
t.format = &format.VP8{
|
t.format = &format.VP8{
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
PayloadTyp: uint8(track.PayloadType()),
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeH265):
|
|
||||||
t.typ = description.MediaTypeVideo
|
|
||||||
t.format = &format.H265{
|
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeH264):
|
case strings.ToLower(webrtc.MimeTypeH264):
|
||||||
t.typ = description.MediaTypeVideo
|
isVideo = true
|
||||||
t.format = &format.H264{
|
t.format = &format.H264{
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
PayloadTyp: uint8(track.PayloadType()),
|
||||||
PacketizationMode: 1,
|
PacketizationMode: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.ToLower(mimeTypeMultiopus):
|
|
||||||
t.typ = description.MediaTypeAudio
|
|
||||||
t.format = &format.Opus{
|
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
|
||||||
ChannelCount: int(track.Codec().Channels),
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeOpus):
|
case strings.ToLower(webrtc.MimeTypeOpus):
|
||||||
t.typ = description.MediaTypeAudio
|
|
||||||
t.format = &format.Opus{
|
t.format = &format.Opus{
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
PayloadTyp: uint8(track.PayloadType()),
|
||||||
ChannelCount: func() int {
|
ChannelCount: func() int {
|
||||||
|
|
@ -307,60 +163,26 @@ func newIncomingTrack(
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypeG722):
|
case strings.ToLower(webrtc.MimeTypeG722):
|
||||||
t.typ = description.MediaTypeAudio
|
|
||||||
t.format = &format.G722{}
|
t.format = &format.G722{}
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypePCMU):
|
case strings.ToLower(webrtc.MimeTypePCMU):
|
||||||
t.typ = description.MediaTypeAudio
|
|
||||||
|
|
||||||
channels := track.Codec().Channels
|
|
||||||
if channels == 0 {
|
|
||||||
channels = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadType := uint8(0)
|
|
||||||
if channels > 1 {
|
|
||||||
payloadType = 118
|
|
||||||
}
|
|
||||||
|
|
||||||
t.format = &format.G711{
|
t.format = &format.G711{
|
||||||
PayloadTyp: payloadType,
|
PayloadTyp: 0,
|
||||||
MULaw: true,
|
MULaw: true,
|
||||||
SampleRate: 8000,
|
SampleRate: 8000,
|
||||||
ChannelCount: int(channels),
|
ChannelCount: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.ToLower(webrtc.MimeTypePCMA):
|
case strings.ToLower(webrtc.MimeTypePCMA):
|
||||||
t.typ = description.MediaTypeAudio
|
|
||||||
|
|
||||||
channels := track.Codec().Channels
|
|
||||||
if channels == 0 {
|
|
||||||
channels = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadType := uint8(8)
|
|
||||||
if channels > 1 {
|
|
||||||
payloadType = 119
|
|
||||||
}
|
|
||||||
|
|
||||||
t.format = &format.G711{
|
t.format = &format.G711{
|
||||||
PayloadTyp: payloadType,
|
PayloadTyp: 8,
|
||||||
MULaw: false,
|
MULaw: false,
|
||||||
SampleRate: 8000,
|
SampleRate: 8000,
|
||||||
ChannelCount: int(channels),
|
ChannelCount: 1,
|
||||||
}
|
|
||||||
|
|
||||||
case strings.ToLower(mimeTypeL16):
|
|
||||||
t.typ = description.MediaTypeAudio
|
|
||||||
t.format = &format.LPCM{
|
|
||||||
PayloadTyp: uint8(track.PayloadType()),
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: int(track.Codec().ClockRate),
|
|
||||||
ChannelCount: int(track.Codec().Channels),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported codec: %+v", track.Codec().RTPCodecCapability)
|
return nil, fmt.Errorf("unsupported codec: %v", track.Codec())
|
||||||
}
|
}
|
||||||
|
|
||||||
// read incoming RTCP packets to make interceptors work
|
// read incoming RTCP packets to make interceptors work
|
||||||
|
|
@ -375,7 +197,7 @@ func newIncomingTrack(
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// send period key frame requests
|
// send period key frame requests
|
||||||
if t.typ == description.MediaTypeVideo {
|
if isVideo {
|
||||||
go func() {
|
go func() {
|
||||||
keyframeTicker := time.NewTicker(keyFrameInterval)
|
keyframeTicker := time.NewTicker(keyFrameInterval)
|
||||||
defer keyframeTicker.Stop()
|
defer keyframeTicker.Stop()
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,6 @@ import (
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var multichannelOpusSDP = map[int]string{
|
|
||||||
3: "channel_mapping=0,2,1;num_streams=2;coupled_streams=1",
|
|
||||||
4: "channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2",
|
|
||||||
5: "channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2",
|
|
||||||
6: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2",
|
|
||||||
7: "channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4",
|
|
||||||
8: "channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4",
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutgoingTrack is a WebRTC outgoing track
|
// OutgoingTrack is a WebRTC outgoing track
|
||||||
type OutgoingTrack struct {
|
type OutgoingTrack struct {
|
||||||
Format format.Format
|
Format format.Format
|
||||||
|
|
@ -40,9 +31,9 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
MimeType: webrtc.MimeTypeVP9,
|
MimeType: webrtc.MimeTypeVP9,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
SDPFmtpLine: "profile-id=0",
|
SDPFmtpLine: "profile-id=1",
|
||||||
},
|
},
|
||||||
PayloadType: 96,
|
PayloadType: 98,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case *format.VP8:
|
case *format.VP8:
|
||||||
|
|
@ -51,16 +42,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
|
||||||
MimeType: webrtc.MimeTypeVP8,
|
MimeType: webrtc.MimeTypeVP8,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
},
|
},
|
||||||
PayloadType: 96,
|
PayloadType: 99,
|
||||||
}, nil
|
|
||||||
|
|
||||||
case *format.H265:
|
|
||||||
return webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypeH265,
|
|
||||||
ClockRate: 90000,
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case *format.H264:
|
case *format.H264:
|
||||||
|
|
@ -70,42 +52,18 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||||
},
|
},
|
||||||
PayloadType: 96,
|
PayloadType: 101,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case *format.Opus:
|
case *format.Opus:
|
||||||
switch forma.ChannelCount {
|
return webrtc.RTPCodecParameters{
|
||||||
case 1, 2:
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
return webrtc.RTPCodecParameters{
|
MimeType: webrtc.MimeTypeOpus,
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
ClockRate: 48000,
|
||||||
MimeType: webrtc.MimeTypeOpus,
|
Channels: 2,
|
||||||
ClockRate: 48000,
|
},
|
||||||
Channels: 2,
|
PayloadType: 111,
|
||||||
SDPFmtpLine: func() string {
|
}, nil
|
||||||
s := "minptime=10;useinbandfec=1"
|
|
||||||
if forma.ChannelCount == 2 {
|
|
||||||
s += ";stereo=1;sprop-stereo=1"
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
case 3, 4, 5, 6, 7, 8:
|
|
||||||
return webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeMultiopus,
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: uint16(forma.ChannelCount),
|
|
||||||
SDPFmtpLine: multichannelOpusSDP[forma.ChannelCount],
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
case *format.G722:
|
case *format.G722:
|
||||||
return webrtc.RTPCodecParameters{
|
return webrtc.RTPCodecParameters{
|
||||||
|
|
@ -117,91 +75,22 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) {
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case *format.G711:
|
case *format.G711:
|
||||||
// These are the sample rates and channels supported by Chrome.
|
if forma.MULaw {
|
||||||
// Different sample rates and channels can be streamed too but we don't want compatibility issues.
|
|
||||||
// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23
|
|
||||||
if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 &&
|
|
||||||
forma.ClockRate() != 32000 && forma.ClockRate() != 48000 {
|
|
||||||
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported clock rate: %d", forma.ClockRate())
|
|
||||||
}
|
|
||||||
if forma.ChannelCount != 1 && forma.ChannelCount != 2 {
|
|
||||||
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
if forma.SampleRate == 8000 {
|
|
||||||
if forma.MULaw {
|
|
||||||
if forma.ChannelCount != 1 {
|
|
||||||
return webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypePCMU,
|
|
||||||
ClockRate: uint32(forma.SampleRate),
|
|
||||||
Channels: uint16(forma.ChannelCount),
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypePCMU,
|
|
||||||
ClockRate: 8000,
|
|
||||||
},
|
|
||||||
PayloadType: 0,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if forma.ChannelCount != 1 {
|
|
||||||
return webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypePCMA,
|
|
||||||
ClockRate: uint32(forma.SampleRate),
|
|
||||||
Channels: uint16(forma.ChannelCount),
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return webrtc.RTPCodecParameters{
|
return webrtc.RTPCodecParameters{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
MimeType: webrtc.MimeTypePCMA,
|
MimeType: webrtc.MimeTypePCMU,
|
||||||
ClockRate: 8000,
|
ClockRate: 8000,
|
||||||
},
|
},
|
||||||
PayloadType: 8,
|
PayloadType: 0,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return webrtc.RTPCodecParameters{
|
return webrtc.RTPCodecParameters{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||||
MimeType: mimeTypeL16,
|
MimeType: webrtc.MimeTypePCMA,
|
||||||
ClockRate: uint32(forma.ClockRate()),
|
ClockRate: 8000,
|
||||||
Channels: uint16(forma.ChannelCount),
|
|
||||||
},
|
},
|
||||||
PayloadType: 96,
|
PayloadType: 8,
|
||||||
}, nil
|
|
||||||
|
|
||||||
case *format.LPCM:
|
|
||||||
if forma.BitDepth != 16 {
|
|
||||||
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported LPCM bit depth: %d", forma.BitDepth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// These are the sample rates and channels supported by Chrome.
|
|
||||||
// Different sample rates and channels can be streamed too but we don't want compatibility issues.
|
|
||||||
// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23
|
|
||||||
if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 &&
|
|
||||||
forma.ClockRate() != 32000 && forma.ClockRate() != 48000 {
|
|
||||||
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported clock rate: %d", forma.ClockRate())
|
|
||||||
}
|
|
||||||
if forma.ChannelCount != 1 && forma.ChannelCount != 2 {
|
|
||||||
return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: mimeTypeL16,
|
|
||||||
ClockRate: uint32(forma.ClockRate()),
|
|
||||||
Channels: uint16(forma.ChannelCount),
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -214,7 +103,6 @@ func (t *OutgoingTrack) isVideo() bool {
|
||||||
case *format.AV1,
|
case *format.AV1,
|
||||||
*format.VP9,
|
*format.VP9,
|
||||||
*format.VP8,
|
*format.VP8,
|
||||||
*format.H265,
|
|
||||||
*format.H264:
|
*format.H264:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -10,15 +9,15 @@ import (
|
||||||
|
|
||||||
"github.com/pion/ice/v2"
|
"github.com/pion/ice/v2"
|
||||||
"github.com/pion/interceptor"
|
"github.com/pion/interceptor"
|
||||||
"github.com/pion/sdp/v3"
|
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
webrtcStreamID = "mediamtx"
|
webrtcHandshakeTimeout = 10 * time.Second
|
||||||
|
webrtcTrackGatherTimeout = 2 * time.Second
|
||||||
|
webrtcStreamID = "mediamtx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func stringInSlice(a string, list []string) bool {
|
func stringInSlice(a string, list []string) bool {
|
||||||
|
|
@ -30,37 +29,6 @@ func stringInSlice(a string, list []string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TracksAreValid checks whether tracks in the SDP are valid
|
|
||||||
func TracksAreValid(medias []*sdp.MediaDescription) error {
|
|
||||||
videoTrack := false
|
|
||||||
audioTrack := false
|
|
||||||
|
|
||||||
for _, media := range medias {
|
|
||||||
switch media.MediaName.Media {
|
|
||||||
case "video":
|
|
||||||
if videoTrack {
|
|
||||||
return fmt.Errorf("only a single video and a single audio track are supported")
|
|
||||||
}
|
|
||||||
videoTrack = true
|
|
||||||
|
|
||||||
case "audio":
|
|
||||||
if audioTrack {
|
|
||||||
return fmt.Errorf("only a single video and a single audio track are supported")
|
|
||||||
}
|
|
||||||
audioTrack = true
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported media '%s'", media.MediaName.Media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !videoTrack && !audioTrack {
|
|
||||||
return fmt.Errorf("no valid tracks count")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type trackRecvPair struct {
|
type trackRecvPair struct {
|
||||||
track *webrtc.TrackRemote
|
track *webrtc.TrackRemote
|
||||||
receiver *webrtc.RTPReceiver
|
receiver *webrtc.RTPReceiver
|
||||||
|
|
@ -71,8 +39,6 @@ type PeerConnection struct {
|
||||||
ICEServers []webrtc.ICEServer
|
ICEServers []webrtc.ICEServer
|
||||||
ICEUDPMux ice.UDPMux
|
ICEUDPMux ice.UDPMux
|
||||||
ICETCPMux ice.TCPMux
|
ICETCPMux ice.TCPMux
|
||||||
HandshakeTimeout conf.StringDuration
|
|
||||||
TrackGatherTimeout conf.StringDuration
|
|
||||||
LocalRandomUDP bool
|
LocalRandomUDP bool
|
||||||
IPsFromInterfaces bool
|
IPsFromInterfaces bool
|
||||||
IPsFromInterfacesList []string
|
IPsFromInterfacesList []string
|
||||||
|
|
@ -128,9 +94,6 @@ func (co *PeerConnection) Start() error {
|
||||||
mediaEngine := &webrtc.MediaEngine{}
|
mediaEngine := &webrtc.MediaEngine{}
|
||||||
|
|
||||||
if co.Publish {
|
if co.Publish {
|
||||||
videoSetupped := false
|
|
||||||
audioSetupped := false
|
|
||||||
|
|
||||||
for _, tr := range co.OutgoingTracks {
|
for _, tr := range co.OutgoingTracks {
|
||||||
params, err := tr.codecParameters()
|
params, err := tr.codecParameters()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -140,10 +103,8 @@ func (co *PeerConnection) Start() error {
|
||||||
var codecType webrtc.RTPCodecType
|
var codecType webrtc.RTPCodecType
|
||||||
if tr.isVideo() {
|
if tr.isVideo() {
|
||||||
codecType = webrtc.RTPCodecTypeVideo
|
codecType = webrtc.RTPCodecTypeVideo
|
||||||
videoSetupped = true
|
|
||||||
} else {
|
} else {
|
||||||
codecType = webrtc.RTPCodecTypeAudio
|
codecType = webrtc.RTPCodecTypeAudio
|
||||||
audioSetupped = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mediaEngine.RegisterCodec(params, codecType)
|
err = mediaEngine.RegisterCodec(params, codecType)
|
||||||
|
|
@ -151,33 +112,6 @@ func (co *PeerConnection) Start() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// always register at least a video and a audio codec
|
|
||||||
// otherwise handshake will fail or audio will be muted on some clients (like Firefox)
|
|
||||||
if !videoSetupped {
|
|
||||||
err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypeVP8,
|
|
||||||
ClockRate: 90000,
|
|
||||||
},
|
|
||||||
PayloadType: 96,
|
|
||||||
}, webrtc.RTPCodecTypeVideo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !audioSetupped {
|
|
||||||
err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
||||||
MimeType: webrtc.MimeTypePCMU,
|
|
||||||
ClockRate: 8000,
|
|
||||||
},
|
|
||||||
PayloadType: 0,
|
|
||||||
}, webrtc.RTPCodecTypeAudio)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for _, codec := range incomingVideoCodecs {
|
for _, codec := range incomingVideoCodecs {
|
||||||
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)
|
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)
|
||||||
|
|
@ -326,8 +260,8 @@ func (co *PeerConnection) SetAnswer(answer *webrtc.SessionDescription) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRemoteCandidate adds a remote candidate.
|
// AddRemoteCandidate adds a remote candidate.
|
||||||
func (co *PeerConnection) AddRemoteCandidate(candidate *webrtc.ICECandidateInit) error {
|
func (co *PeerConnection) AddRemoteCandidate(candidate webrtc.ICECandidateInit) error {
|
||||||
return co.wr.AddICECandidate(*candidate)
|
return co.wr.AddICECandidate(candidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateFullAnswer creates a full answer.
|
// CreateFullAnswer creates a full answer.
|
||||||
|
|
@ -342,8 +276,8 @@ func (co *PeerConnection) CreateFullAnswer(
|
||||||
|
|
||||||
answer, err := co.wr.CreateAnswer(nil)
|
answer, err := co.wr.CreateAnswer(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, webrtc.ErrSenderWithNoCodecs) {
|
if err.Error() == "unable to populate media section, RTPSender created with no codecs" {
|
||||||
return nil, fmt.Errorf("codecs not supported by client")
|
return nil, fmt.Errorf("track codecs are not supported by remote")
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +287,7 @@ func (co *PeerConnection) CreateFullAnswer(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = co.waitGatheringDone(ctx)
|
err = co.WaitGatheringDone(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -361,7 +295,8 @@ func (co *PeerConnection) CreateFullAnswer(
|
||||||
return co.wr.LocalDescription(), nil
|
return co.wr.LocalDescription(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (co *PeerConnection) waitGatheringDone(ctx context.Context) error {
|
// WaitGatheringDone waits until candidate gathering is complete.
|
||||||
|
func (co *PeerConnection) WaitGatheringDone(ctx context.Context) error {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-co.NewLocalCandidate():
|
case <-co.NewLocalCandidate():
|
||||||
|
|
@ -377,7 +312,7 @@ func (co *PeerConnection) waitGatheringDone(ctx context.Context) error {
|
||||||
func (co *PeerConnection) WaitUntilConnected(
|
func (co *PeerConnection) WaitUntilConnected(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) error {
|
) error {
|
||||||
t := time.NewTimer(time.Duration(co.HandshakeTimeout))
|
t := time.NewTimer(webrtcHandshakeTimeout)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
|
|
@ -398,21 +333,19 @@ outer:
|
||||||
}
|
}
|
||||||
|
|
||||||
// GatherIncomingTracks gathers incoming tracks.
|
// GatherIncomingTracks gathers incoming tracks.
|
||||||
func (co *PeerConnection) GatherIncomingTracks(ctx context.Context) ([]*IncomingTrack, error) {
|
func (co *PeerConnection) GatherIncomingTracks(
|
||||||
var sdp sdp.SessionDescription
|
ctx context.Context,
|
||||||
sdp.Unmarshal([]byte(co.wr.RemoteDescription().SDP)) //nolint:errcheck
|
maxCount int,
|
||||||
|
) ([]*IncomingTrack, error) {
|
||||||
maxTrackCount := len(sdp.MediaDescriptions)
|
|
||||||
|
|
||||||
var tracks []*IncomingTrack
|
var tracks []*IncomingTrack
|
||||||
|
|
||||||
t := time.NewTimer(time.Duration(co.TrackGatherTimeout))
|
t := time.NewTimer(webrtcTrackGatherTimeout)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
if len(tracks) != 0 {
|
if maxCount == 0 && len(tracks) != 0 {
|
||||||
return tracks, nil
|
return tracks, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("deadline exceeded while waiting tracks")
|
return nil, fmt.Errorf("deadline exceeded while waiting tracks")
|
||||||
|
|
@ -424,7 +357,7 @@ func (co *PeerConnection) GatherIncomingTracks(ctx context.Context) ([]*Incoming
|
||||||
}
|
}
|
||||||
tracks = append(tracks, track)
|
tracks = append(tracks, track)
|
||||||
|
|
||||||
if len(tracks) >= maxTrackCount {
|
if len(tracks) == maxCount || len(tracks) >= 2 {
|
||||||
return tracks, nil
|
return tracks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,22 @@
|
||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/test"
|
"github.com/bluenviron/mediamtx/internal/test"
|
||||||
"github.com/pion/rtp"
|
|
||||||
"github.com/pion/sdp/v3"
|
|
||||||
"github.com/pion/webrtc/v3"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPeerConnectionCloseImmediately(t *testing.T) {
|
func TestPeerConnectionCloseAfterError(t *testing.T) {
|
||||||
pc := &PeerConnection{
|
pc := &PeerConnection{
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
LocalRandomUDP: true,
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
IPsFromInterfaces: true,
|
||||||
LocalRandomUDP: true,
|
Publish: false,
|
||||||
IPsFromInterfaces: true,
|
Log: test.NilLogger,
|
||||||
Publish: false,
|
|
||||||
Log: test.NilLogger,
|
|
||||||
}
|
}
|
||||||
err := pc.Start()
|
err := pc.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer pc.Close()
|
|
||||||
|
|
||||||
_, err = pc.CreatePartialOffer()
|
_, err = pc.CreatePartialOffer()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -35,454 +26,3 @@ func TestPeerConnectionCloseImmediately(t *testing.T) {
|
||||||
|
|
||||||
pc.Close()
|
pc.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPeerConnectionPublishRead(t *testing.T) {
|
|
||||||
for _, ca := range []struct {
|
|
||||||
name string
|
|
||||||
in format.Format
|
|
||||||
webrtcOut webrtc.RTPCodecCapability
|
|
||||||
out format.Format
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"av1",
|
|
||||||
&format.AV1{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "video/AV1",
|
|
||||||
ClockRate: 90000,
|
|
||||||
},
|
|
||||||
&format.AV1{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vp9",
|
|
||||||
&format.VP9{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "video/VP9",
|
|
||||||
ClockRate: 90000,
|
|
||||||
SDPFmtpLine: "profile-id=0",
|
|
||||||
},
|
|
||||||
&format.VP9{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"vp8",
|
|
||||||
&format.VP8{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "video/VP8",
|
|
||||||
ClockRate: 90000,
|
|
||||||
},
|
|
||||||
&format.VP8{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"h265",
|
|
||||||
test.FormatH265,
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "video/H265",
|
|
||||||
ClockRate: 90000,
|
|
||||||
},
|
|
||||||
&format.H265{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"h264",
|
|
||||||
test.FormatH264,
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "video/H264",
|
|
||||||
ClockRate: 90000,
|
|
||||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
|
||||||
},
|
|
||||||
&format.H264{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
PacketizationMode: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"opus multichannel",
|
|
||||||
&format.Opus{
|
|
||||||
PayloadTyp: 112,
|
|
||||||
ChannelCount: 6,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/multiopus",
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 6,
|
|
||||||
SDPFmtpLine: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2",
|
|
||||||
},
|
|
||||||
&format.Opus{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
ChannelCount: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"opus stereo",
|
|
||||||
&format.Opus{
|
|
||||||
PayloadTyp: 111,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/opus",
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 2,
|
|
||||||
SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
|
|
||||||
},
|
|
||||||
&format.Opus{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"opus mono",
|
|
||||||
&format.Opus{
|
|
||||||
PayloadTyp: 111,
|
|
||||||
ChannelCount: 1,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/opus",
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 2,
|
|
||||||
SDPFmtpLine: "minptime=10;useinbandfec=1",
|
|
||||||
},
|
|
||||||
&format.Opus{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
ChannelCount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g722",
|
|
||||||
&format.G722{},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/G722",
|
|
||||||
ClockRate: 8000,
|
|
||||||
},
|
|
||||||
&format.G722{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 pcma 8khz mono",
|
|
||||||
&format.G711{
|
|
||||||
PayloadTyp: 8,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 1,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/PCMA",
|
|
||||||
ClockRate: 8000,
|
|
||||||
},
|
|
||||||
&format.G711{
|
|
||||||
PayloadTyp: 8,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 pcmu 8khz mono",
|
|
||||||
&format.G711{
|
|
||||||
MULaw: true,
|
|
||||||
PayloadTyp: 0,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 1,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/PCMU",
|
|
||||||
ClockRate: 8000,
|
|
||||||
},
|
|
||||||
&format.G711{
|
|
||||||
MULaw: true,
|
|
||||||
PayloadTyp: 0,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 pcma 8khz stereo",
|
|
||||||
&format.G711{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/PCMA",
|
|
||||||
ClockRate: 8000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
&format.G711{
|
|
||||||
PayloadTyp: 119,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 pcmu 8khz stereo",
|
|
||||||
&format.G711{
|
|
||||||
MULaw: true,
|
|
||||||
PayloadTyp: 96,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/PCMU",
|
|
||||||
ClockRate: 8000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
&format.G711{
|
|
||||||
MULaw: true,
|
|
||||||
PayloadTyp: 118,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 pcma 16khz stereo",
|
|
||||||
&format.G711{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
SampleRate: 16000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/L16",
|
|
||||||
ClockRate: 16000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 16000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 pcmu 16khz stereo",
|
|
||||||
&format.G711{
|
|
||||||
MULaw: true,
|
|
||||||
PayloadTyp: 96,
|
|
||||||
SampleRate: 16000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/L16",
|
|
||||||
ClockRate: 16000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 16000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"l16 8khz stereo",
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/L16",
|
|
||||||
ClockRate: 8000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"l16 16khz stereo",
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 16000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/L16",
|
|
||||||
ClockRate: 16000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 16000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"l16 48khz stereo",
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 48000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
webrtc.RTPCodecCapability{
|
|
||||||
MimeType: "audio/L16",
|
|
||||||
ClockRate: 48000,
|
|
||||||
Channels: 2,
|
|
||||||
},
|
|
||||||
&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 48000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(ca.name, func(t *testing.T) {
|
|
||||||
pc1 := &PeerConnection{
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
LocalRandomUDP: true,
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
Publish: true,
|
|
||||||
OutgoingTracks: []*OutgoingTrack{{
|
|
||||||
Format: ca.in,
|
|
||||||
}},
|
|
||||||
Log: test.NilLogger,
|
|
||||||
}
|
|
||||||
err := pc1.Start()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer pc1.Close()
|
|
||||||
|
|
||||||
pc2 := &PeerConnection{
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
LocalRandomUDP: true,
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
Publish: false,
|
|
||||||
Log: test.NilLogger,
|
|
||||||
}
|
|
||||||
err = pc2.Start()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer pc2.Close()
|
|
||||||
|
|
||||||
offer, err := pc1.CreatePartialOffer()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
answer, err := pc2.CreateFullAnswer(context.Background(), offer)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = pc1.SetAnswer(answer)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case cnd := <-pc1.NewLocalCandidate():
|
|
||||||
err2 := pc2.AddRemoteCandidate(cnd)
|
|
||||||
require.NoError(t, err2)
|
|
||||||
|
|
||||||
case <-pc1.Connected():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = pc1.WaitUntilConnected(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = pc2.WaitUntilConnected(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = pc1.OutgoingTracks[0].WriteRTP(&rtp.Packet{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: true,
|
|
||||||
PayloadType: 111,
|
|
||||||
SequenceNumber: 1123,
|
|
||||||
Timestamp: 45343,
|
|
||||||
SSRC: 563424,
|
|
||||||
},
|
|
||||||
Payload: []byte{5, 2},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
inc, err := pc2.GatherIncomingTracks(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
exp := ca.webrtcOut
|
|
||||||
exp.RTCPFeedback = inc[0].track.Codec().RTPCodecCapability.RTCPFeedback
|
|
||||||
require.Equal(t, exp, inc[0].track.Codec().RTPCodecCapability)
|
|
||||||
|
|
||||||
require.Equal(t, ca.out, inc[0].Format())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that an audio codec is present regardless of the fact that an audio track is not.
|
|
||||||
func TestPeerConnectionFallbackCodecs(t *testing.T) {
|
|
||||||
pc1 := &PeerConnection{
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
LocalRandomUDP: true,
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
Publish: false,
|
|
||||||
Log: test.NilLogger,
|
|
||||||
}
|
|
||||||
err := pc1.Start()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer pc1.Close()
|
|
||||||
|
|
||||||
pc2 := &PeerConnection{
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
LocalRandomUDP: true,
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
Publish: true,
|
|
||||||
OutgoingTracks: []*OutgoingTrack{{
|
|
||||||
Format: &format.AV1{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
Log: test.NilLogger,
|
|
||||||
}
|
|
||||||
err = pc2.Start()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer pc2.Close()
|
|
||||||
|
|
||||||
offer, err := pc1.CreatePartialOffer()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
answer, err := pc2.CreateFullAnswer(context.Background(), offer)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var s sdp.SessionDescription
|
|
||||||
err = s.Unmarshal([]byte(answer.SDP))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, []*sdp.MediaDescription{
|
|
||||||
{
|
|
||||||
MediaName: sdp.MediaName{
|
|
||||||
Media: "video",
|
|
||||||
Port: sdp.RangedPort{Value: 9},
|
|
||||||
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
|
|
||||||
Formats: []string{"97"},
|
|
||||||
},
|
|
||||||
ConnectionInformation: s.MediaDescriptions[0].ConnectionInformation,
|
|
||||||
Attributes: s.MediaDescriptions[0].Attributes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MediaName: sdp.MediaName{
|
|
||||||
Media: "audio",
|
|
||||||
Port: sdp.RangedPort{Value: 9},
|
|
||||||
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
|
|
||||||
Formats: []string{"0"},
|
|
||||||
},
|
|
||||||
ConnectionInformation: s.MediaDescriptions[1].ConnectionInformation,
|
|
||||||
Attributes: s.MediaDescriptions[1].Attributes,
|
|
||||||
},
|
|
||||||
}, s.MediaDescriptions)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
37
internal/protocols/webrtc/track_count.go
Normal file
37
internal/protocols/webrtc/track_count.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pion/sdp/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TrackCount returns the track count.
|
||||||
|
func TrackCount(medias []*sdp.MediaDescription) (int, error) {
|
||||||
|
videoTrack := false
|
||||||
|
audioTrack := false
|
||||||
|
trackCount := 0
|
||||||
|
|
||||||
|
for _, media := range medias {
|
||||||
|
switch media.MediaName.Media {
|
||||||
|
case "video":
|
||||||
|
if videoTrack {
|
||||||
|
return 0, fmt.Errorf("only a single video and a single audio track are supported")
|
||||||
|
}
|
||||||
|
videoTrack = true
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
if audioTrack {
|
||||||
|
return 0, fmt.Errorf("only a single video and a single audio track are supported")
|
||||||
|
}
|
||||||
|
audioTrack = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unsupported media '%s'", media.MediaName.Media)
|
||||||
|
}
|
||||||
|
|
||||||
|
trackCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackCount, nil
|
||||||
|
}
|
||||||
|
|
@ -10,9 +10,21 @@ func TracksToMedias(tracks []*IncomingTrack) []*description.Media {
|
||||||
ret := make([]*description.Media, len(tracks))
|
ret := make([]*description.Media, len(tracks))
|
||||||
|
|
||||||
for i, track := range tracks {
|
for i, track := range tracks {
|
||||||
|
forma := track.Format()
|
||||||
|
|
||||||
|
var mediaType description.MediaType
|
||||||
|
|
||||||
|
switch forma.(type) {
|
||||||
|
case *format.AV1, *format.VP9, *format.VP8, *format.H264:
|
||||||
|
mediaType = description.MediaTypeVideo
|
||||||
|
|
||||||
|
default:
|
||||||
|
mediaType = description.MediaTypeAudio
|
||||||
|
}
|
||||||
|
|
||||||
ret[i] = &description.Media{
|
ret[i] = &description.Media{
|
||||||
Type: track.typ,
|
Type: mediaType,
|
||||||
Formats: []format.Format{track.format},
|
Formats: []format.Format{forma},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,10 @@ import (
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
webrtcHandshakeTimeout = 10 * time.Second
|
|
||||||
webrtcTrackGatherTimeout = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// WHIPClient is a WHIP client.
|
// WHIPClient is a WHIP client.
|
||||||
type WHIPClient struct {
|
type WHIPClient struct {
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
|
|
@ -54,14 +48,12 @@ func (c *WHIPClient) Publish(
|
||||||
}
|
}
|
||||||
|
|
||||||
c.pc = &PeerConnection{
|
c.pc = &PeerConnection{
|
||||||
ICEServers: iceServers,
|
ICEServers: iceServers,
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
LocalRandomUDP: true,
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
IPsFromInterfaces: true,
|
||||||
LocalRandomUDP: true,
|
Publish: true,
|
||||||
IPsFromInterfaces: true,
|
OutgoingTracks: outgoingTracks,
|
||||||
Publish: true,
|
Log: c.Log,
|
||||||
OutgoingTracks: outgoingTracks,
|
|
||||||
Log: c.Log,
|
|
||||||
}
|
}
|
||||||
err = c.pc.Start()
|
err = c.pc.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -130,13 +122,11 @@ func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.pc = &PeerConnection{
|
c.pc = &PeerConnection{
|
||||||
ICEServers: iceServers,
|
ICEServers: iceServers,
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
LocalRandomUDP: true,
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
IPsFromInterfaces: true,
|
||||||
LocalRandomUDP: true,
|
Publish: false,
|
||||||
IPsFromInterfaces: true,
|
Log: c.Log,
|
||||||
Publish: false,
|
|
||||||
Log: c.Log,
|
|
||||||
}
|
}
|
||||||
err = c.pc.Start()
|
err = c.pc.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -169,7 +159,8 @@ func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = TracksAreValid(sdp.MediaDescriptions)
|
// check that there are at most two tracks
|
||||||
|
_, err = TrackCount(sdp.MediaDescriptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.deleteSession(context.Background()) //nolint:errcheck
|
c.deleteSession(context.Background()) //nolint:errcheck
|
||||||
c.pc.Close()
|
c.pc.Close()
|
||||||
|
|
@ -209,7 +200,7 @@ outer:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks, err := c.pc.GatherIncomingTracks(ctx)
|
tracks, err := c.pc.GatherIncomingTracks(ctx, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.deleteSession(context.Background()) //nolint:errcheck
|
c.deleteSession(context.Background()) //nolint:errcheck
|
||||||
c.pc.Close()
|
c.pc.Close()
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,6 @@ import (
|
||||||
"github.com/bluenviron/mediamtx/internal/stream"
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OnSegmentCreateFunc is the prototype of the function passed as OnSegmentCreate
|
|
||||||
type OnSegmentCreateFunc = func(path string)
|
|
||||||
|
|
||||||
// OnSegmentCompleteFunc is the prototype of the function passed as OnSegmentComplete
|
|
||||||
type OnSegmentCompleteFunc = func(path string, duration time.Duration)
|
|
||||||
|
|
||||||
// Agent writes recordings to disk.
|
// Agent writes recordings to disk.
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
WriteQueueSize int
|
WriteQueueSize int
|
||||||
|
|
@ -23,8 +17,8 @@ type Agent struct {
|
||||||
SegmentDuration time.Duration
|
SegmentDuration time.Duration
|
||||||
PathName string
|
PathName string
|
||||||
Stream *stream.Stream
|
Stream *stream.Stream
|
||||||
OnSegmentCreate OnSegmentCreateFunc
|
OnSegmentCreate OnSegmentFunc
|
||||||
OnSegmentComplete OnSegmentCompleteFunc
|
OnSegmentComplete OnSegmentFunc
|
||||||
Parent logger.Writer
|
Parent logger.Writer
|
||||||
|
|
||||||
restartPause time.Duration
|
restartPause time.Duration
|
||||||
|
|
@ -42,7 +36,7 @@ func (w *Agent) Initialize() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w.OnSegmentComplete == nil {
|
if w.OnSegmentComplete == nil {
|
||||||
w.OnSegmentComplete = func(string, time.Duration) {
|
w.OnSegmentComplete = func(string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w.restartPause == 0 {
|
if w.restartPause == 0 {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import (
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OnSegmentFunc is the prototype of the function passed as runOnSegmentStart / runOnSegmentComplete
|
||||||
|
type OnSegmentFunc = func(string)
|
||||||
|
|
||||||
type sample struct {
|
type sample struct {
|
||||||
*fmp4.PartSample
|
*fmp4.PartSample
|
||||||
dts time.Duration
|
dts time.Duration
|
||||||
|
|
|
||||||
|
|
@ -68,15 +68,12 @@ func TestAgent(t *testing.T) {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
writeToStream := func(stream *stream.Stream, startDTS time.Duration, startNTP time.Time) {
|
writeToStream := func(stream *stream.Stream, ntp time.Time) {
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
pts := startDTS + time.Duration(i)*100*time.Millisecond
|
|
||||||
ntp := startNTP.Add(time.Duration(i*60) * time.Second)
|
|
||||||
|
|
||||||
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
PTS: pts,
|
PTS: (50 + time.Duration(i)) * time.Second,
|
||||||
NTP: ntp,
|
NTP: ntp.Add(time.Duration(i) * 60 * time.Second),
|
||||||
},
|
},
|
||||||
AU: [][]byte{
|
AU: [][]byte{
|
||||||
test.FormatH264.SPS,
|
test.FormatH264.SPS,
|
||||||
|
|
@ -87,7 +84,7 @@ func TestAgent(t *testing.T) {
|
||||||
|
|
||||||
stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H265{
|
stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H265{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
PTS: pts,
|
PTS: (50 + time.Duration(i)) * time.Second,
|
||||||
},
|
},
|
||||||
AU: [][]byte{
|
AU: [][]byte{
|
||||||
test.FormatH265.VPS,
|
test.FormatH265.VPS,
|
||||||
|
|
@ -99,21 +96,21 @@ func TestAgent(t *testing.T) {
|
||||||
|
|
||||||
stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{
|
stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
PTS: pts,
|
PTS: (50 + time.Duration(i)) * time.Second,
|
||||||
},
|
},
|
||||||
AUs: [][]byte{{1, 2, 3, 4}},
|
AUs: [][]byte{{1, 2, 3, 4}},
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{
|
stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
PTS: pts,
|
PTS: (50 + time.Duration(i)) * time.Second,
|
||||||
},
|
},
|
||||||
Samples: []byte{1, 2, 3, 4},
|
Samples: []byte{1, 2, 3, 4},
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.LPCM{
|
stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.LPCM{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
PTS: pts,
|
PTS: (50 + time.Duration(i)) * time.Second,
|
||||||
},
|
},
|
||||||
Samples: []byte{1, 2, 3, 4},
|
Samples: []byte{1, 2, 3, 4},
|
||||||
})
|
})
|
||||||
|
|
@ -147,15 +144,6 @@ func TestAgent(t *testing.T) {
|
||||||
f = conf.RecordFormatMPEGTS
|
f = conf.RecordFormatMPEGTS
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext string
|
|
||||||
if ca == "fmp4" {
|
|
||||||
ext = "mp4"
|
|
||||||
} else {
|
|
||||||
ext = "ts"
|
|
||||||
}
|
|
||||||
|
|
||||||
n := 0
|
|
||||||
|
|
||||||
w := &Agent{
|
w := &Agent{
|
||||||
WriteQueueSize: 1024,
|
WriteQueueSize: 1024,
|
||||||
PathFormat: recordPath,
|
PathFormat: recordPath,
|
||||||
|
|
@ -164,30 +152,10 @@ func TestAgent(t *testing.T) {
|
||||||
SegmentDuration: 1 * time.Second,
|
SegmentDuration: 1 * time.Second,
|
||||||
PathName: "mypath",
|
PathName: "mypath",
|
||||||
Stream: stream,
|
Stream: stream,
|
||||||
OnSegmentCreate: func(segPath string) {
|
OnSegmentCreate: func(_ string) {
|
||||||
switch n {
|
|
||||||
case 0:
|
|
||||||
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext), segPath)
|
|
||||||
case 1:
|
|
||||||
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext), segPath)
|
|
||||||
default:
|
|
||||||
require.Equal(t, filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext), segPath)
|
|
||||||
}
|
|
||||||
segCreated <- struct{}{}
|
segCreated <- struct{}{}
|
||||||
},
|
},
|
||||||
OnSegmentComplete: func(segPath string, du time.Duration) {
|
OnSegmentComplete: func(_ string) {
|
||||||
switch n {
|
|
||||||
case 0:
|
|
||||||
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext), segPath)
|
|
||||||
require.Equal(t, 2*time.Second, du)
|
|
||||||
case 1:
|
|
||||||
require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext), segPath)
|
|
||||||
require.Equal(t, 100*time.Millisecond, du)
|
|
||||||
default:
|
|
||||||
require.Equal(t, filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext), segPath)
|
|
||||||
require.Equal(t, 100*time.Millisecond, du)
|
|
||||||
}
|
|
||||||
n++
|
|
||||||
segDone <- struct{}{}
|
segDone <- struct{}{}
|
||||||
},
|
},
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
|
|
@ -195,13 +163,7 @@ func TestAgent(t *testing.T) {
|
||||||
}
|
}
|
||||||
w.Initialize()
|
w.Initialize()
|
||||||
|
|
||||||
writeToStream(stream,
|
writeToStream(stream, time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC))
|
||||||
50*time.Second,
|
|
||||||
time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC))
|
|
||||||
|
|
||||||
writeToStream(stream,
|
|
||||||
52*time.Second,
|
|
||||||
time.Date(2008, 0o5, 20, 22, 16, 25, 0, time.UTC))
|
|
||||||
|
|
||||||
// simulate a write error
|
// simulate a write error
|
||||||
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
||||||
|
|
@ -218,68 +180,74 @@ func TestAgent(t *testing.T) {
|
||||||
<-segDone
|
<-segDone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ext string
|
||||||
if ca == "fmp4" {
|
if ca == "fmp4" {
|
||||||
var init fmp4.Init
|
ext = "mp4"
|
||||||
|
} else {
|
||||||
|
ext = "ts"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ca == "fmp4" {
|
||||||
func() {
|
func() {
|
||||||
f, err2 := os.Open(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext))
|
f, err2 := os.Open(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext))
|
||||||
require.NoError(t, err2)
|
require.NoError(t, err2)
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
|
var init fmp4.Init
|
||||||
err2 = init.Unmarshal(f)
|
err2 = init.Unmarshal(f)
|
||||||
require.NoError(t, err2)
|
require.NoError(t, err2)
|
||||||
}()
|
|
||||||
|
|
||||||
require.Equal(t, fmp4.Init{
|
require.Equal(t, fmp4.Init{
|
||||||
Tracks: []*fmp4.InitTrack{
|
Tracks: []*fmp4.InitTrack{
|
||||||
{
|
{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
TimeScale: 90000,
|
TimeScale: 90000,
|
||||||
Codec: &fmp4.CodecH264{
|
Codec: &fmp4.CodecH264{
|
||||||
SPS: test.FormatH264.SPS,
|
SPS: test.FormatH264.SPS,
|
||||||
PPS: test.FormatH264.PPS,
|
PPS: test.FormatH264.PPS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
ID: 2,
|
||||||
ID: 2,
|
TimeScale: 90000,
|
||||||
TimeScale: 90000,
|
Codec: &fmp4.CodecH265{
|
||||||
Codec: &fmp4.CodecH265{
|
VPS: test.FormatH265.VPS,
|
||||||
VPS: test.FormatH265.VPS,
|
SPS: test.FormatH265.SPS,
|
||||||
SPS: test.FormatH265.SPS,
|
PPS: test.FormatH265.PPS,
|
||||||
PPS: test.FormatH265.PPS,
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
ID: 3,
|
||||||
ID: 3,
|
TimeScale: 44100,
|
||||||
TimeScale: 44100,
|
Codec: &fmp4.CodecMPEG4Audio{
|
||||||
Codec: &fmp4.CodecMPEG4Audio{
|
Config: mpeg4audio.Config{
|
||||||
Config: mpeg4audio.Config{
|
Type: 2,
|
||||||
Type: 2,
|
SampleRate: 44100,
|
||||||
|
ChannelCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 4,
|
||||||
|
TimeScale: 8000,
|
||||||
|
Codec: &fmp4.CodecLPCM{
|
||||||
|
BitDepth: 16,
|
||||||
|
SampleRate: 8000,
|
||||||
|
ChannelCount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 5,
|
||||||
|
TimeScale: 44100,
|
||||||
|
Codec: &fmp4.CodecLPCM{
|
||||||
|
BitDepth: 16,
|
||||||
SampleRate: 44100,
|
SampleRate: 44100,
|
||||||
ChannelCount: 2,
|
ChannelCount: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
}, init)
|
||||||
ID: 4,
|
}()
|
||||||
TimeScale: 8000,
|
|
||||||
Codec: &fmp4.CodecLPCM{
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 5,
|
|
||||||
TimeScale: 44100,
|
|
||||||
Codec: &fmp4.CodecLPCM{
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 44100,
|
|
||||||
ChannelCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, init)
|
|
||||||
|
|
||||||
_, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext))
|
_, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -293,19 +261,17 @@ func TestAgent(t *testing.T) {
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
writeToStream(stream,
|
writeToStream(stream, time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC))
|
||||||
300*time.Second,
|
|
||||||
time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC))
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
w.Close()
|
w.Close()
|
||||||
|
|
||||||
<-segCreated
|
|
||||||
<-segDone
|
|
||||||
|
|
||||||
_, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext))
|
_, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-16-25-000000."+ext))
|
||||||
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ func (f *formatFMP4) initialize() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: sampl,
|
PartSample: sampl,
|
||||||
dts: tunit.PTS,
|
dts: tunit.PTS,
|
||||||
ntp: tunit.NTP,
|
ntp: tunit.NTP,
|
||||||
|
|
@ -261,7 +261,7 @@ func (f *formatFMP4) initialize() {
|
||||||
firstReceived = true
|
firstReceived = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
IsNonSyncSample: !randomAccess,
|
IsNonSyncSample: !randomAccess,
|
||||||
Payload: tunit.Frame,
|
Payload: tunit.Frame,
|
||||||
|
|
@ -364,7 +364,7 @@ func (f *formatFMP4) initialize() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: sampl,
|
PartSample: sampl,
|
||||||
dts: dts,
|
dts: dts,
|
||||||
ntp: tunit.NTP,
|
ntp: tunit.NTP,
|
||||||
|
|
@ -435,7 +435,7 @@ func (f *formatFMP4) initialize() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: sampl,
|
PartSample: sampl,
|
||||||
dts: dts,
|
dts: dts,
|
||||||
ntp: tunit.NTP,
|
ntp: tunit.NTP,
|
||||||
|
|
@ -494,7 +494,7 @@ func (f *formatFMP4) initialize() {
|
||||||
}
|
}
|
||||||
lastPTS = tunit.PTS
|
lastPTS = tunit.PTS
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: tunit.Frame,
|
Payload: tunit.Frame,
|
||||||
IsNonSyncSample: !randomAccess,
|
IsNonSyncSample: !randomAccess,
|
||||||
|
|
@ -547,7 +547,7 @@ func (f *formatFMP4) initialize() {
|
||||||
}
|
}
|
||||||
lastPTS = tunit.PTS
|
lastPTS = tunit.PTS
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: tunit.Frame,
|
Payload: tunit.Frame,
|
||||||
IsNonSyncSample: !randomAccess,
|
IsNonSyncSample: !randomAccess,
|
||||||
|
|
@ -583,7 +583,7 @@ func (f *formatFMP4) initialize() {
|
||||||
updateCodecs()
|
updateCodecs()
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: tunit.Frame,
|
Payload: tunit.Frame,
|
||||||
},
|
},
|
||||||
|
|
@ -607,7 +607,7 @@ func (f *formatFMP4) initialize() {
|
||||||
var dt time.Duration
|
var dt time.Duration
|
||||||
|
|
||||||
for _, packet := range tunit.Packets {
|
for _, packet := range tunit.Packets {
|
||||||
err := track.write(&sample{
|
err := track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: packet,
|
Payload: packet,
|
||||||
},
|
},
|
||||||
|
|
@ -642,7 +642,7 @@ func (f *formatFMP4) initialize() {
|
||||||
dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit *
|
dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit *
|
||||||
time.Second / sampleRate
|
time.Second / sampleRate
|
||||||
|
|
||||||
err := track.write(&sample{
|
err := track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: au,
|
Payload: au,
|
||||||
},
|
},
|
||||||
|
|
@ -688,7 +688,7 @@ func (f *formatFMP4) initialize() {
|
||||||
updateCodecs()
|
updateCodecs()
|
||||||
}
|
}
|
||||||
|
|
||||||
err = track.write(&sample{
|
err = track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: frame,
|
Payload: frame,
|
||||||
},
|
},
|
||||||
|
|
@ -756,7 +756,7 @@ func (f *formatFMP4) initialize() {
|
||||||
dt := time.Duration(i) * time.Duration(ac3.SamplesPerFrame) *
|
dt := time.Duration(i) * time.Duration(ac3.SamplesPerFrame) *
|
||||||
time.Second / time.Duration(codec.SampleRate)
|
time.Second / time.Duration(codec.SampleRate)
|
||||||
|
|
||||||
err = track.write(&sample{
|
err = track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: frame,
|
Payload: frame,
|
||||||
},
|
},
|
||||||
|
|
@ -796,7 +796,7 @@ func (f *formatFMP4) initialize() {
|
||||||
out = g711.DecodeAlaw(tunit.Samples)
|
out = g711.DecodeAlaw(tunit.Samples)
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: out,
|
Payload: out,
|
||||||
},
|
},
|
||||||
|
|
@ -820,7 +820,7 @@ func (f *formatFMP4) initialize() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.write(&sample{
|
return track.record(&sample{
|
||||||
PartSample: &fmp4.PartSample{
|
PartSample: &fmp4.PartSample{
|
||||||
Payload: tunit.Samples,
|
Payload: tunit.Samples,
|
||||||
},
|
},
|
||||||
|
|
@ -838,12 +838,6 @@ func (f *formatFMP4) initialize() {
|
||||||
|
|
||||||
func (f *formatFMP4) close() {
|
func (f *formatFMP4) close() {
|
||||||
if f.currentSegment != nil {
|
if f.currentSegment != nil {
|
||||||
for _, track := range f.tracks {
|
|
||||||
if track.nextSample != nil && track.nextSample.dts > f.currentSegment.lastDTS {
|
|
||||||
f.currentSegment.lastDTS = track.nextSample.dts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.currentSegment.close() //nolint:errcheck
|
f.currentSegment.close() //nolint:errcheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ func (p *formatFMP4Part) close() error {
|
||||||
return writePart(p.s.fi, p.sequenceNumber, p.partTracks)
|
return writePart(p.s.fi, p.sequenceNumber, p.partTracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *formatFMP4Part) write(track *formatFMP4Track, sample *sample) error {
|
func (p *formatFMP4Part) record(track *formatFMP4Track, sample *sample) error {
|
||||||
partTrack, ok := p.partTracks[track]
|
partTrack, ok := p.partTracks[track]
|
||||||
if !ok {
|
if !ok {
|
||||||
partTrack = &fmp4.PartTrack{
|
partTrack = &fmp4.PartTrack{
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,9 @@ type formatFMP4Segment struct {
|
||||||
path string
|
path string
|
||||||
fi *os.File
|
fi *os.File
|
||||||
curPart *formatFMP4Part
|
curPart *formatFMP4Part
|
||||||
lastDTS time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *formatFMP4Segment) initialize() {
|
func (s *formatFMP4Segment) initialize() {
|
||||||
s.lastDTS = s.startDTS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *formatFMP4Segment) close() error {
|
func (s *formatFMP4Segment) close() error {
|
||||||
|
|
@ -61,17 +59,14 @@ func (s *formatFMP4Segment) close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err2 == nil {
|
if err2 == nil {
|
||||||
duration := s.lastDTS - s.startDTS
|
s.f.a.agent.OnSegmentComplete(s.path)
|
||||||
s.f.a.agent.OnSegmentComplete(s.path, duration)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error {
|
func (s *formatFMP4Segment) record(track *formatFMP4Track, sample *sample) error {
|
||||||
s.lastDTS = sample.dts
|
|
||||||
|
|
||||||
if s.curPart == nil {
|
if s.curPart == nil {
|
||||||
s.curPart = &formatFMP4Part{
|
s.curPart = &formatFMP4Part{
|
||||||
s: s,
|
s: s,
|
||||||
|
|
@ -97,5 +92,5 @@ func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error
|
||||||
s.f.nextSequenceNumber++
|
s.f.nextSequenceNumber++
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.curPart.write(track, sample)
|
return s.curPart.record(track, sample)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ type formatFMP4Track struct {
|
||||||
nextSample *sample
|
nextSample *sample
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *formatFMP4Track) write(sample *sample) error {
|
func (t *formatFMP4Track) record(sample *sample) error {
|
||||||
// wait the first video sample before setting hasVideo
|
// wait the first video sample before setting hasVideo
|
||||||
if t.initTrack.Codec.IsVideo() {
|
if t.initTrack.Codec.IsVideo() {
|
||||||
t.f.hasVideo = true
|
t.f.hasVideo = true
|
||||||
|
|
@ -35,7 +35,7 @@ func (t *formatFMP4Track) write(sample *sample) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := t.f.currentSegment.write(t, sample)
|
err := t.f.currentSegment.record(t, sample)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +43,6 @@ func (t *formatFMP4Track) write(sample *sample) error {
|
||||||
if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) &&
|
if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) &&
|
||||||
!t.nextSample.IsNonSyncSample &&
|
!t.nextSample.IsNonSyncSample &&
|
||||||
(t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration {
|
(t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration {
|
||||||
t.f.currentSegment.lastDTS = t.nextSample.dts
|
|
||||||
err := t.f.currentSegment.close()
|
err := t.f.currentSegment.close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ func (f *formatMPEGTS) initialize() {
|
||||||
for _, media := range f.a.agent.Stream.Desc().Medias {
|
for _, media := range f.a.agent.Stream.Desc().Medias {
|
||||||
for _, forma := range media.Formats {
|
for _, forma := range media.Formats {
|
||||||
switch forma := forma.(type) {
|
switch forma := forma.(type) {
|
||||||
case *rtspformat.H265: //nolint:dupl
|
case *rtspformat.H265:
|
||||||
track := addTrack(forma, &mpegts.CodecH265{})
|
track := addTrack(forma, &mpegts.CodecH265{})
|
||||||
|
|
||||||
var dtsExtractor *h265.DTSExtractor
|
var dtsExtractor *h265.DTSExtractor
|
||||||
|
|
@ -91,18 +91,10 @@ func (f *formatMPEGTS) initialize() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.write(
|
return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, randomAccess, tunit.AU)
|
||||||
dts,
|
|
||||||
tunit.NTP,
|
|
||||||
true,
|
|
||||||
randomAccess,
|
|
||||||
func() error {
|
|
||||||
return f.mw.WriteH265(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
case *rtspformat.H264: //nolint:dupl
|
case *rtspformat.H264:
|
||||||
track := addTrack(forma, &mpegts.CodecH264{})
|
track := addTrack(forma, &mpegts.CodecH264{})
|
||||||
|
|
||||||
var dtsExtractor *h264.DTSExtractor
|
var dtsExtractor *h264.DTSExtractor
|
||||||
|
|
@ -113,10 +105,10 @@ func (f *formatMPEGTS) initialize() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
randomAccess := h264.IDRPresent(tunit.AU)
|
idrPresent := h264.IDRPresent(tunit.AU)
|
||||||
|
|
||||||
if dtsExtractor == nil {
|
if dtsExtractor == nil {
|
||||||
if !randomAccess {
|
if !idrPresent {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
dtsExtractor = h264.NewDTSExtractor()
|
dtsExtractor = h264.NewDTSExtractor()
|
||||||
|
|
@ -127,15 +119,7 @@ func (f *formatMPEGTS) initialize() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.write(
|
return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, idrPresent, tunit.AU)
|
||||||
dts,
|
|
||||||
tunit.NTP,
|
|
||||||
true,
|
|
||||||
randomAccess,
|
|
||||||
func() error {
|
|
||||||
return f.mw.WriteH264(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
case *rtspformat.MPEG4Video:
|
case *rtspformat.MPEG4Video:
|
||||||
|
|
@ -157,17 +141,15 @@ func (f *formatMPEGTS) initialize() {
|
||||||
}
|
}
|
||||||
lastPTS = tunit.PTS
|
lastPTS = tunit.PTS
|
||||||
|
|
||||||
|
f.hasVideo = true
|
||||||
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
|
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
|
||||||
|
|
||||||
return f.write(
|
err := f.setupSegment(tunit.PTS, tunit.NTP, true, randomAccess)
|
||||||
tunit.PTS,
|
if err != nil {
|
||||||
tunit.NTP,
|
return err
|
||||||
true,
|
}
|
||||||
randomAccess,
|
|
||||||
func() error {
|
return f.mw.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
|
||||||
return f.mw.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
case *rtspformat.MPEG1Video:
|
case *rtspformat.MPEG1Video:
|
||||||
|
|
@ -189,17 +171,15 @@ func (f *formatMPEGTS) initialize() {
|
||||||
}
|
}
|
||||||
lastPTS = tunit.PTS
|
lastPTS = tunit.PTS
|
||||||
|
|
||||||
|
f.hasVideo = true
|
||||||
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8})
|
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8})
|
||||||
|
|
||||||
return f.write(
|
err := f.setupSegment(tunit.PTS, tunit.NTP, true, randomAccess)
|
||||||
tunit.PTS,
|
if err != nil {
|
||||||
tunit.NTP,
|
return err
|
||||||
true,
|
}
|
||||||
randomAccess,
|
|
||||||
func() error {
|
return f.mw.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
|
||||||
return f.mw.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
case *rtspformat.Opus:
|
case *rtspformat.Opus:
|
||||||
|
|
@ -213,15 +193,12 @@ func (f *formatMPEGTS) initialize() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.write(
|
err := f.setupSegment(tunit.PTS, tunit.NTP, false, true)
|
||||||
tunit.PTS,
|
if err != nil {
|
||||||
tunit.NTP,
|
return err
|
||||||
false,
|
}
|
||||||
true,
|
|
||||||
func() error {
|
return f.mw.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets)
|
||||||
return f.mw.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
case *rtspformat.MPEG4Audio:
|
case *rtspformat.MPEG4Audio:
|
||||||
|
|
@ -235,15 +212,12 @@ func (f *formatMPEGTS) initialize() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.write(
|
err := f.setupSegment(tunit.PTS, tunit.NTP, false, true)
|
||||||
tunit.PTS,
|
if err != nil {
|
||||||
tunit.NTP,
|
return err
|
||||||
false,
|
}
|
||||||
true,
|
|
||||||
func() error {
|
return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs)
|
||||||
return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
case *rtspformat.MPEG1Audio:
|
case *rtspformat.MPEG1Audio:
|
||||||
|
|
@ -255,15 +229,12 @@ func (f *formatMPEGTS) initialize() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.write(
|
err := f.setupSegment(tunit.PTS, tunit.NTP, false, true)
|
||||||
tunit.PTS,
|
if err != nil {
|
||||||
tunit.NTP,
|
return err
|
||||||
false,
|
}
|
||||||
true,
|
|
||||||
func() error {
|
return f.mw.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames)
|
||||||
return f.mw.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
case *rtspformat.AC3:
|
case *rtspformat.AC3:
|
||||||
|
|
@ -277,25 +248,17 @@ func (f *formatMPEGTS) initialize() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.write(
|
for i, frame := range tunit.Frames {
|
||||||
tunit.PTS,
|
framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame*
|
||||||
tunit.NTP,
|
time.Second/sampleRate
|
||||||
false,
|
|
||||||
true,
|
|
||||||
func() error {
|
|
||||||
for i, frame := range tunit.Frames {
|
|
||||||
framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame*
|
|
||||||
time.Second/sampleRate
|
|
||||||
|
|
||||||
err := f.mw.WriteAC3(track, durationGoToMPEGTS(framePTS), frame)
|
err := f.mw.WriteAC3(track, durationGoToMPEGTS(framePTS), frame)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,17 +278,12 @@ func (f *formatMPEGTS) close() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *formatMPEGTS) write(
|
func (f *formatMPEGTS) setupSegment(
|
||||||
dts time.Duration,
|
dts time.Duration,
|
||||||
ntp time.Time,
|
ntp time.Time,
|
||||||
isVideo bool,
|
isVideo bool,
|
||||||
randomAccess bool,
|
randomAccess bool,
|
||||||
writeCB func() error,
|
|
||||||
) error {
|
) error {
|
||||||
if isVideo {
|
|
||||||
f.hasVideo = true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case f.currentSegment == nil:
|
case f.currentSegment == nil:
|
||||||
f.currentSegment = &formatMPEGTSSegment{
|
f.currentSegment = &formatMPEGTSSegment{
|
||||||
|
|
@ -337,7 +295,6 @@ func (f *formatMPEGTS) write(
|
||||||
case (!f.hasVideo || isVideo) &&
|
case (!f.hasVideo || isVideo) &&
|
||||||
randomAccess &&
|
randomAccess &&
|
||||||
(dts-f.currentSegment.startDTS) >= f.a.agent.SegmentDuration:
|
(dts-f.currentSegment.startDTS) >= f.a.agent.SegmentDuration:
|
||||||
f.currentSegment.lastDTS = dts
|
|
||||||
err := f.currentSegment.close()
|
err := f.currentSegment.close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -359,7 +316,23 @@ func (f *formatMPEGTS) write(
|
||||||
f.currentSegment.lastFlush = dts
|
f.currentSegment.lastFlush = dts
|
||||||
}
|
}
|
||||||
|
|
||||||
f.currentSegment.lastDTS = dts
|
return nil
|
||||||
|
}
|
||||||
return writeCB()
|
|
||||||
|
func (f *formatMPEGTS) recordH26x(
|
||||||
|
track *mpegts.Track,
|
||||||
|
pts time.Duration,
|
||||||
|
dts time.Duration,
|
||||||
|
ntp time.Time,
|
||||||
|
randomAccess bool,
|
||||||
|
au [][]byte,
|
||||||
|
) error {
|
||||||
|
f.hasVideo = true
|
||||||
|
|
||||||
|
err := f.setupSegment(dts, ntp, true, randomAccess)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.mw.WriteH26x(track, durationGoToMPEGTS(pts), durationGoToMPEGTS(dts), randomAccess, au)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,13 @@ type formatMPEGTSSegment struct {
|
||||||
startDTS time.Duration
|
startDTS time.Duration
|
||||||
startNTP time.Time
|
startNTP time.Time
|
||||||
|
|
||||||
|
lastFlush time.Duration
|
||||||
path string
|
path string
|
||||||
fi *os.File
|
fi *os.File
|
||||||
lastFlush time.Duration
|
|
||||||
lastDTS time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *formatMPEGTSSegment) initialize() {
|
func (s *formatMPEGTSSegment) initialize() {
|
||||||
s.lastFlush = s.startDTS
|
s.lastFlush = s.startDTS
|
||||||
s.lastDTS = s.startDTS
|
|
||||||
s.f.dw.setTarget(s)
|
s.f.dw.setTarget(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,8 +34,7 @@ func (s *formatMPEGTSSegment) close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err2 == nil {
|
if err2 == nil {
|
||||||
duration := s.lastDTS - s.startDTS
|
s.f.a.agent.OnSegmentComplete(s.path)
|
||||||
s.f.a.agent.OnSegmentComplete(s.path, duration)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
c5ef2cf356b103bf7a19dd4d14257c9e00163551ed03bbf96bf22a12458a1250
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
v1.5.13
|
v1.5.8
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -16,16 +11,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func do() error {
|
func do() error {
|
||||||
|
log.Println("downloading hls.js...")
|
||||||
|
|
||||||
buf, err := os.ReadFile("./hlsjsdownloader/VERSION")
|
buf, err := os.ReadFile("./hlsjsdownloader/VERSION")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
version := strings.TrimSpace(string(buf))
|
version := strings.TrimSpace(string(buf))
|
||||||
|
|
||||||
log.Printf("downloading hls.js version %s...", version)
|
res, err := http.Get("https://cdn.jsdelivr.net/npm/hls.js@" + version + "/dist/hls.min.js")
|
||||||
|
|
||||||
res, err := http.Get("https://github.com/video-dev/hls.js/releases/download/" + version + "/release.zip")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -35,38 +29,15 @@ func do() error {
|
||||||
return fmt.Errorf("bad status code: %v", res.StatusCode)
|
return fmt.Errorf("bad status code: %v", res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
zipBuf, err := io.ReadAll(res.Body)
|
buf, err = io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hashBuf, err := os.ReadFile("./hlsjsdownloader/HASH")
|
err = os.WriteFile("hls.min.js", buf, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hash := make([]byte, hex.DecodedLen(len(hashBuf)))
|
|
||||||
|
|
||||||
if _, err = hex.Decode(hash, bytes.TrimSpace(hashBuf)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if sum := sha256.Sum256(zipBuf); !bytes.Equal(sum[:], hash) {
|
|
||||||
return fmt.Errorf("hash mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
z, err := zip.NewReader(bytes.NewReader(zipBuf), int64(len(zipBuf)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
hls, err := fs.ReadFile(z, "dist/hls.min.js")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.WriteFile("hls.min.js", hls, 0o644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("ok")
|
log.Println("ok")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
gopath "path"
|
gopath "path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -37,17 +36,6 @@ func mergePathAndQuery(path string, rawQuery string) string {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func addJWTFromAuthorization(rawQuery string, auth string) string {
|
|
||||||
jwt := strings.TrimPrefix(auth, "Bearer ")
|
|
||||||
if rawQuery != "" {
|
|
||||||
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
|
|
||||||
v.Set("jwt", jwt)
|
|
||||||
return v.Encode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url.Values{"jwt": []string{jwt}}.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpServer struct {
|
type httpServer struct {
|
||||||
address string
|
address string
|
||||||
encryption bool
|
encryption bool
|
||||||
|
|
@ -102,11 +90,9 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
||||||
|
|
||||||
switch ctx.Request.Method {
|
switch ctx.Request.Method {
|
||||||
case http.MethodOptions:
|
case http.MethodOptions:
|
||||||
if ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
|
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
|
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range")
|
||||||
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range")
|
ctx.Writer.WriteHeader(http.StatusNoContent)
|
||||||
ctx.Writer.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
|
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
|
@ -159,15 +145,10 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
||||||
|
|
||||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
||||||
|
|
||||||
q := ctx.Request.URL.RawQuery
|
|
||||||
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
|
|
||||||
q = addJWTFromAuthorization(q, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: dir,
|
Name: dir,
|
||||||
Query: q,
|
Query: ctx.Request.URL.RawQuery,
|
||||||
Publish: false,
|
Publish: false,
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
User: user,
|
User: user,
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mi.hmuxer.WriteH265(tunit.NTP, tunit.PTS, tunit.AU)
|
err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("muxer error: %w", err)
|
return fmt.Errorf("muxer error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +185,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mi.hmuxer.WriteH264(tunit.NTP, tunit.PTS, tunit.AU)
|
err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("muxer error: %w", err)
|
return fmt.Errorf("muxer error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package hls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -12,6 +11,8 @@ import (
|
||||||
"github.com/bluenviron/gohlslib"
|
"github.com/bluenviron/gohlslib"
|
||||||
"github.com/bluenviron/gohlslib/pkg/codecs"
|
"github.com/bluenviron/gohlslib/pkg/codecs"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/auth"
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||||
|
|
@ -49,52 +50,21 @@ func (pa *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dummyPathManager struct {
|
type dummyPathManager struct {
|
||||||
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error)
|
stream *stream.Stream
|
||||||
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
return pm.findPathConf(req)
|
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||||
|
return nil, auth.Error{}
|
||||||
|
}
|
||||||
|
return &conf.Path{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
return pm.addReader(req)
|
if req.AccessRequest.Name == "nonexisting" {
|
||||||
}
|
return nil, nil, fmt.Errorf("not found")
|
||||||
|
|
||||||
func TestPreflightRequest(t *testing.T) {
|
|
||||||
s := &Server{
|
|
||||||
Address: "127.0.0.1:8888",
|
|
||||||
AllowOrigin: "*",
|
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
}
|
||||||
err := s.Initialize()
|
return &dummyPath{}, pm.stream, nil
|
||||||
require.NoError(t, err)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
tr := &http.Transport{}
|
|
||||||
defer tr.CloseIdleConnections()
|
|
||||||
hc := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8888", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
req.Header.Add("Access-Control-Request-Method", "GET")
|
|
||||||
|
|
||||||
res, err := hc.Do(req)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
|
||||||
|
|
||||||
byts, err := io.ReadAll(res.Body)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
|
|
||||||
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
|
|
||||||
require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods"))
|
|
||||||
require.Equal(t, "Authorization, Range", res.Header.Get("Access-Control-Allow-Headers"))
|
|
||||||
require.Equal(t, byts, []byte{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerNotFound(t *testing.T) {
|
func TestServerNotFound(t *testing.T) {
|
||||||
|
|
@ -103,19 +73,6 @@ func TestServerNotFound(t *testing.T) {
|
||||||
"always remux on",
|
"always remux on",
|
||||||
} {
|
} {
|
||||||
t.Run(ca, func(t *testing.T) {
|
t.Run(ca, func(t *testing.T) {
|
||||||
pm := &dummyPathManager{
|
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "nonexisting", req.AccessRequest.Name)
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
require.Equal(t, "nonexisting", req.AccessRequest.Name)
|
|
||||||
return nil, nil, fmt.Errorf("not found")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Address: "127.0.0.1:8888",
|
Address: "127.0.0.1:8888",
|
||||||
Encryption: false,
|
Encryption: false,
|
||||||
|
|
@ -132,7 +89,7 @@ func TestServerNotFound(t *testing.T) {
|
||||||
Directory: "",
|
Directory: "",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 512,
|
WriteQueueSize: 512,
|
||||||
PathManager: pm,
|
PathManager: &dummyPathManager{},
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
}
|
}
|
||||||
err := s.Initialize()
|
err := s.Initialize()
|
||||||
|
|
@ -170,7 +127,7 @@ func TestServerRead(t *testing.T) {
|
||||||
t.Run("always remux off", func(t *testing.T) {
|
t.Run("always remux off", func(t *testing.T) {
|
||||||
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
||||||
|
|
||||||
str, err := stream.New(
|
stream, err := stream.New(
|
||||||
1460,
|
1460,
|
||||||
desc,
|
desc,
|
||||||
true,
|
true,
|
||||||
|
|
@ -178,18 +135,7 @@ func TestServerRead(t *testing.T) {
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pm := &dummyPathManager{
|
pathManager := &dummyPathManager{stream: stream}
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
||||||
return &dummyPath{}, str, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Address: "127.0.0.1:8888",
|
Address: "127.0.0.1:8888",
|
||||||
|
|
@ -207,7 +153,7 @@ func TestServerRead(t *testing.T) {
|
||||||
Directory: "",
|
Directory: "",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 512,
|
WriteQueueSize: 512,
|
||||||
PathManager: pm,
|
PathManager: pathManager,
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
}
|
}
|
||||||
err = s.Initialize()
|
err = s.Initialize()
|
||||||
|
|
@ -229,6 +175,7 @@ func TestServerRead(t *testing.T) {
|
||||||
require.Equal(t, time.Duration(0), pts)
|
require.Equal(t, time.Duration(0), pts)
|
||||||
require.Equal(t, time.Duration(0), dts)
|
require.Equal(t, time.Duration(0), dts)
|
||||||
require.Equal(t, [][]byte{
|
require.Equal(t, [][]byte{
|
||||||
|
{byte(h264.NALUTypeAccessUnitDelimiter), 0xf0},
|
||||||
test.FormatH264.SPS,
|
test.FormatH264.SPS,
|
||||||
test.FormatH264.PPS,
|
test.FormatH264.PPS,
|
||||||
{5, 1},
|
{5, 1},
|
||||||
|
|
@ -247,7 +194,7 @@ func TestServerRead(t *testing.T) {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
|
stream.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
NTP: time.Time{},
|
NTP: time.Time{},
|
||||||
PTS: time.Duration(i) * time.Second,
|
PTS: time.Duration(i) * time.Second,
|
||||||
|
|
@ -265,7 +212,7 @@ func TestServerRead(t *testing.T) {
|
||||||
t.Run("always remux on", func(t *testing.T) {
|
t.Run("always remux on", func(t *testing.T) {
|
||||||
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
||||||
|
|
||||||
str, err := stream.New(
|
stream, err := stream.New(
|
||||||
1460,
|
1460,
|
||||||
desc,
|
desc,
|
||||||
true,
|
true,
|
||||||
|
|
@ -273,18 +220,7 @@ func TestServerRead(t *testing.T) {
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pm := &dummyPathManager{
|
pathManager := &dummyPathManager{stream: stream}
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
require.Equal(t, "mystream", req.AccessRequest.Name)
|
|
||||||
return &dummyPath{}, str, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Address: "127.0.0.1:8888",
|
Address: "127.0.0.1:8888",
|
||||||
|
|
@ -302,7 +238,7 @@ func TestServerRead(t *testing.T) {
|
||||||
Directory: "",
|
Directory: "",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 512,
|
WriteQueueSize: 512,
|
||||||
PathManager: pm,
|
PathManager: pathManager,
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
}
|
}
|
||||||
err = s.Initialize()
|
err = s.Initialize()
|
||||||
|
|
@ -314,7 +250,7 @@ func TestServerRead(t *testing.T) {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
|
stream.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
NTP: time.Time{},
|
NTP: time.Time{},
|
||||||
PTS: time.Duration(i) * time.Second,
|
PTS: time.Duration(i) * time.Second,
|
||||||
|
|
@ -340,6 +276,7 @@ func TestServerRead(t *testing.T) {
|
||||||
require.Equal(t, time.Duration(0), pts)
|
require.Equal(t, time.Duration(0), pts)
|
||||||
require.Equal(t, time.Duration(0), dts)
|
require.Equal(t, time.Duration(0), dts)
|
||||||
require.Equal(t, [][]byte{
|
require.Equal(t, [][]byte{
|
||||||
|
{0x09, 0xf0},
|
||||||
test.FormatH264.SPS,
|
test.FormatH264.SPS,
|
||||||
test.FormatH264.PPS,
|
test.FormatH264.PPS,
|
||||||
{5, 1},
|
{5, 1},
|
||||||
|
|
@ -359,102 +296,6 @@ func TestServerRead(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerReadAuthorizationHeader(t *testing.T) {
|
|
||||||
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
|
||||||
|
|
||||||
str, err := stream.New(
|
|
||||||
1460,
|
|
||||||
desc,
|
|
||||||
true,
|
|
||||||
test.NilLogger,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pm := &dummyPathManager{
|
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
return &dummyPath{}, str, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
Address: "127.0.0.1:8888",
|
|
||||||
Encryption: false,
|
|
||||||
ServerKey: "",
|
|
||||||
ServerCert: "",
|
|
||||||
AlwaysRemux: true,
|
|
||||||
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
|
|
||||||
SegmentCount: 7,
|
|
||||||
SegmentDuration: conf.StringDuration(1 * time.Second),
|
|
||||||
PartDuration: conf.StringDuration(200 * time.Millisecond),
|
|
||||||
SegmentMaxSize: 50 * 1024 * 1024,
|
|
||||||
AllowOrigin: "",
|
|
||||||
TrustedProxies: conf.IPNetworks{},
|
|
||||||
Directory: "",
|
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
WriteQueueSize: 512,
|
|
||||||
PathManager: pm,
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
|
||||||
err = s.Initialize()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
s.PathReady(&dummyPath{})
|
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{
|
|
||||||
Base: unit.Base{
|
|
||||||
NTP: time.Time{},
|
|
||||||
PTS: time.Duration(i) * time.Second,
|
|
||||||
},
|
|
||||||
AU: [][]byte{
|
|
||||||
{5, 1}, // IDR
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &gohlslib.Client{
|
|
||||||
URI: "http://127.0.0.1:8888/mystream/index.m3u8",
|
|
||||||
OnRequest: func(r *http.Request) {
|
|
||||||
r.Header.Set("Authorization", "Bearer testing")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
recv := make(chan struct{})
|
|
||||||
|
|
||||||
c.OnTracks = func(tracks []*gohlslib.Track) error {
|
|
||||||
require.Equal(t, []*gohlslib.Track{{
|
|
||||||
Codec: &codecs.H264{},
|
|
||||||
}}, tracks)
|
|
||||||
|
|
||||||
c.OnDataH26x(tracks[0], func(pts, dts time.Duration, au [][]byte) {
|
|
||||||
require.Equal(t, time.Duration(0), pts)
|
|
||||||
require.Equal(t, time.Duration(0), dts)
|
|
||||||
require.Equal(t, [][]byte{
|
|
||||||
test.FormatH264.SPS,
|
|
||||||
test.FormatH264.PPS,
|
|
||||||
{5, 1},
|
|
||||||
}, au)
|
|
||||||
close(recv)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.Start()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() { <-c.Wait() }()
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
<-recv
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDirectory(t *testing.T) {
|
func TestDirectory(t *testing.T) {
|
||||||
dir, err := os.MkdirTemp("", "mediamtx-playback")
|
dir, err := os.MkdirTemp("", "mediamtx-playback")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -462,7 +303,7 @@ func TestDirectory(t *testing.T) {
|
||||||
|
|
||||||
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
||||||
|
|
||||||
str, err := stream.New(
|
stream, err := stream.New(
|
||||||
1460,
|
1460,
|
||||||
desc,
|
desc,
|
||||||
true,
|
true,
|
||||||
|
|
@ -470,11 +311,7 @@ func TestDirectory(t *testing.T) {
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
pm := &dummyPathManager{
|
pathManager := &dummyPathManager{stream: stream}
|
||||||
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
return &dummyPath{}, str, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Address: "127.0.0.1:8888",
|
Address: "127.0.0.1:8888",
|
||||||
|
|
@ -492,7 +329,7 @@ func TestDirectory(t *testing.T) {
|
||||||
Directory: filepath.Join(dir, "mydir"),
|
Directory: filepath.Join(dir, "mydir"),
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 512,
|
WriteQueueSize: 512,
|
||||||
PathManager: pm,
|
PathManager: pathManager,
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
}
|
}
|
||||||
err = s.Initialize()
|
err = s.Initialize()
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,9 @@ type conn struct {
|
||||||
pathName string
|
pathName string
|
||||||
query string
|
query string
|
||||||
sconn srt.Conn
|
sconn srt.Conn
|
||||||
|
|
||||||
|
chNew chan srtNewConnReq
|
||||||
|
chSetConn chan srt.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) initialize() {
|
func (c *conn) initialize() {
|
||||||
|
|
@ -81,6 +84,8 @@ func (c *conn) initialize() {
|
||||||
|
|
||||||
c.created = time.Now()
|
c.created = time.Now()
|
||||||
c.uuid = uuid.New()
|
c.uuid = uuid.New()
|
||||||
|
c.chNew = make(chan srtNewConnReq)
|
||||||
|
c.chSetConn = make(chan srt.Conn)
|
||||||
|
|
||||||
c.Log(logger.Info, "opened")
|
c.Log(logger.Info, "opened")
|
||||||
|
|
||||||
|
|
@ -125,20 +130,36 @@ func (c *conn) run() { //nolint:dupl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) runInner() error {
|
func (c *conn) runInner() error {
|
||||||
|
var req srtNewConnReq
|
||||||
|
select {
|
||||||
|
case req = <-c.chNew:
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return errors.New("terminated")
|
||||||
|
}
|
||||||
|
|
||||||
|
answerSent, err := c.runInner2(req)
|
||||||
|
|
||||||
|
if !answerSent {
|
||||||
|
req.res <- nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) runInner2(req srtNewConnReq) (bool, error) {
|
||||||
var streamID streamID
|
var streamID streamID
|
||||||
err := streamID.unmarshal(c.connReq.StreamId())
|
err := streamID.unmarshal(req.connReq.StreamId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.connReq.Reject(srt.REJ_PEER)
|
return false, fmt.Errorf("invalid stream ID '%s': %w", req.connReq.StreamId(), err)
|
||||||
return fmt.Errorf("invalid stream ID '%s': %w", c.connReq.StreamId(), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if streamID.mode == streamIDModePublish {
|
if streamID.mode == streamIDModePublish {
|
||||||
return c.runPublish(&streamID)
|
return c.runPublish(req, &streamID)
|
||||||
}
|
}
|
||||||
return c.runRead(&streamID)
|
return c.runRead(req, &streamID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) runPublish(streamID *streamID) error {
|
func (c *conn) runPublish(req srtNewConnReq, streamID *streamID) (bool, error) {
|
||||||
path, err := c.pathManager.AddPublisher(defs.PathAddPublisherReq{
|
path, err := c.pathManager.AddPublisher(defs.PathAddPublisherReq{
|
||||||
Author: c,
|
Author: c,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
|
|
@ -157,24 +178,21 @@ func (c *conn) runPublish(streamID *streamID) error {
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
// wait some seconds to mitigate brute force attacks
|
// wait some seconds to mitigate brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
c.connReq.Reject(srt.REJ_PEER)
|
return false, terr
|
||||||
return terr
|
|
||||||
}
|
}
|
||||||
c.connReq.Reject(srt.REJ_PEER)
|
return false, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: c})
|
defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: c})
|
||||||
|
|
||||||
err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTPublishPassphrase)
|
err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTPublishPassphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.connReq.Reject(srt.REJ_PEER)
|
return false, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sconn, err := c.connReq.Accept()
|
sconn, err := c.exchangeRequestWithConn(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
|
|
@ -192,12 +210,12 @@ func (c *conn) runPublish(streamID *streamID) error {
|
||||||
select {
|
select {
|
||||||
case err := <-readerErr:
|
case err := <-readerErr:
|
||||||
sconn.Close()
|
sconn.Close()
|
||||||
return err
|
return true, err
|
||||||
|
|
||||||
case <-c.ctx.Done():
|
case <-c.ctx.Done():
|
||||||
sconn.Close()
|
sconn.Close()
|
||||||
<-readerErr
|
<-readerErr
|
||||||
return errors.New("terminated")
|
return true, errors.New("terminated")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,7 +256,7 @@ func (c *conn) runPublishReader(sconn srt.Conn, path defs.Path) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) runRead(streamID *streamID) error {
|
func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) {
|
||||||
path, stream, err := c.pathManager.AddReader(defs.PathAddReaderReq{
|
path, stream, err := c.pathManager.AddReader(defs.PathAddReaderReq{
|
||||||
Author: c,
|
Author: c,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
|
|
@ -256,24 +274,21 @@ func (c *conn) runRead(streamID *streamID) error {
|
||||||
if errors.As(err, &terr) {
|
if errors.As(err, &terr) {
|
||||||
// wait some seconds to mitigate brute force attacks
|
// wait some seconds to mitigate brute force attacks
|
||||||
<-time.After(auth.PauseAfterError)
|
<-time.After(auth.PauseAfterError)
|
||||||
c.connReq.Reject(srt.REJ_PEER)
|
return false, err
|
||||||
return terr
|
|
||||||
}
|
}
|
||||||
c.connReq.Reject(srt.REJ_PEER)
|
return false, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer path.RemoveReader(defs.PathRemoveReaderReq{Author: c})
|
defer path.RemoveReader(defs.PathRemoveReaderReq{Author: c})
|
||||||
|
|
||||||
err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTReadPassphrase)
|
err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTReadPassphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.connReq.Reject(srt.REJ_PEER)
|
return false, err
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sconn, err := c.connReq.Accept()
|
sconn, err := c.exchangeRequestWithConn(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return true, err
|
||||||
}
|
}
|
||||||
defer sconn.Close()
|
defer sconn.Close()
|
||||||
|
|
||||||
|
|
@ -292,7 +307,7 @@ func (c *conn) runRead(streamID *streamID) error {
|
||||||
|
|
||||||
err = mpegts.FromStream(stream, writer, bw, sconn, time.Duration(c.writeTimeout))
|
err = mpegts.FromStream(stream, writer, bw, sconn, time.Duration(c.writeTimeout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Log(logger.Info, "is reading from path '%s', %s",
|
c.Log(logger.Info, "is reading from path '%s', %s",
|
||||||
|
|
@ -316,10 +331,41 @@ func (c *conn) runRead(streamID *streamID) error {
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-c.ctx.Done():
|
case <-c.ctx.Done():
|
||||||
return fmt.Errorf("terminated")
|
return true, fmt.Errorf("terminated")
|
||||||
|
|
||||||
case err := <-writer.Error():
|
case err := <-writer.Error():
|
||||||
return err
|
return true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) {
|
||||||
|
req.res <- c
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sconn := <-c.chSetConn:
|
||||||
|
return sconn, nil
|
||||||
|
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return nil, errors.New("terminated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// new is called by srtListener through srtServer.
|
||||||
|
func (c *conn) new(req srtNewConnReq) *conn {
|
||||||
|
select {
|
||||||
|
case c.chNew <- req:
|
||||||
|
return <-req.res
|
||||||
|
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setConn is called by srtListener .
|
||||||
|
func (c *conn) setConn(sconn srt.Conn) {
|
||||||
|
select {
|
||||||
|
case c.chSetConn <- sconn:
|
||||||
|
case <-c.ctx.Done():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,24 @@ func (l *listener) run() {
|
||||||
|
|
||||||
func (l *listener) runInner() error {
|
func (l *listener) runInner() error {
|
||||||
for {
|
for {
|
||||||
req, err := l.ln.Accept2()
|
var sconn *conn
|
||||||
|
conn, _, err := l.ln.Accept(func(req srt.ConnRequest) srt.ConnType {
|
||||||
|
sconn = l.parent.newConnRequest(req)
|
||||||
|
if sconn == nil {
|
||||||
|
return srt.REJECT
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently it's the same to return SUBSCRIBE or PUBLISH
|
||||||
|
return srt.SUBSCRIBE
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.parent.newConnRequest(req)
|
if conn == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sconn.setConn(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ func srtMaxPayloadSize(u int) int {
|
||||||
return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet
|
return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type srtNewConnReq struct {
|
||||||
|
connReq srt.ConnRequest
|
||||||
|
res chan *conn
|
||||||
|
}
|
||||||
|
|
||||||
type serverAPIConnsListRes struct {
|
type serverAPIConnsListRes struct {
|
||||||
data *defs.APISRTConnList
|
data *defs.APISRTConnList
|
||||||
err error
|
err error
|
||||||
|
|
@ -85,7 +90,7 @@ type Server struct {
|
||||||
conns map[*conn]struct{}
|
conns map[*conn]struct{}
|
||||||
|
|
||||||
// in
|
// in
|
||||||
chNewConnRequest chan srt.ConnRequest
|
chNewConnRequest chan srtNewConnReq
|
||||||
chAcceptErr chan error
|
chAcceptErr chan error
|
||||||
chCloseConn chan *conn
|
chCloseConn chan *conn
|
||||||
chAPIConnsList chan serverAPIConnsListReq
|
chAPIConnsList chan serverAPIConnsListReq
|
||||||
|
|
@ -108,7 +113,7 @@ func (s *Server) Initialize() error {
|
||||||
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
|
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
s.conns = make(map[*conn]struct{})
|
s.conns = make(map[*conn]struct{})
|
||||||
s.chNewConnRequest = make(chan srt.ConnRequest)
|
s.chNewConnRequest = make(chan srtNewConnReq)
|
||||||
s.chAcceptErr = make(chan error)
|
s.chAcceptErr = make(chan error)
|
||||||
s.chCloseConn = make(chan *conn)
|
s.chCloseConn = make(chan *conn)
|
||||||
s.chAPIConnsList = make(chan serverAPIConnsListReq)
|
s.chAPIConnsList = make(chan serverAPIConnsListReq)
|
||||||
|
|
@ -160,7 +165,7 @@ outer:
|
||||||
writeTimeout: s.WriteTimeout,
|
writeTimeout: s.WriteTimeout,
|
||||||
writeQueueSize: s.WriteQueueSize,
|
writeQueueSize: s.WriteQueueSize,
|
||||||
udpMaxPayloadSize: s.UDPMaxPayloadSize,
|
udpMaxPayloadSize: s.UDPMaxPayloadSize,
|
||||||
connReq: req,
|
connReq: req.connReq,
|
||||||
runOnConnect: s.RunOnConnect,
|
runOnConnect: s.RunOnConnect,
|
||||||
runOnConnectRestart: s.RunOnConnectRestart,
|
runOnConnectRestart: s.RunOnConnectRestart,
|
||||||
runOnDisconnect: s.RunOnDisconnect,
|
runOnDisconnect: s.RunOnDisconnect,
|
||||||
|
|
@ -171,6 +176,7 @@ outer:
|
||||||
}
|
}
|
||||||
c.initialize()
|
c.initialize()
|
||||||
s.conns[c] = struct{}{}
|
s.conns[c] = struct{}{}
|
||||||
|
req.res <- c
|
||||||
|
|
||||||
case c := <-s.chCloseConn:
|
case c := <-s.chCloseConn:
|
||||||
delete(s.conns, c)
|
delete(s.conns, c)
|
||||||
|
|
@ -230,11 +236,20 @@ func (s *Server) findConnByUUID(uuid uuid.UUID) *conn {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newConnRequest is called by srtListener.
|
// newConnRequest is called by srtListener.
|
||||||
func (s *Server) newConnRequest(connReq srt.ConnRequest) {
|
func (s *Server) newConnRequest(connReq srt.ConnRequest) *conn {
|
||||||
|
req := srtNewConnReq{
|
||||||
|
connReq: connReq,
|
||||||
|
res: make(chan *conn),
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case s.chNewConnRequest <- connReq:
|
case s.chNewConnRequest <- req:
|
||||||
|
c := <-req.res
|
||||||
|
|
||||||
|
return c.new(req)
|
||||||
|
|
||||||
case <-s.ctx.Done():
|
case <-s.ctx.Done():
|
||||||
connReq.Reject(srt.REJ_CLOSE)
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ func TestServerPublish(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
u := "srt://127.0.0.1:8890?streamid=publish:mypath:myuser:mypass"
|
u := "srt://localhost:8890?streamid=publish:mypath:myuser:mypass"
|
||||||
|
|
||||||
srtConf := srt.DefaultConfig()
|
srtConf := srt.DefaultConfig()
|
||||||
address, err := srtConf.UnmarshalURL(u)
|
address, err := srtConf.UnmarshalURL(u)
|
||||||
|
|
@ -127,7 +127,7 @@ func TestServerPublish(t *testing.T) {
|
||||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{
|
||||||
test.FormatH264.SPS,
|
test.FormatH264.SPS,
|
||||||
test.FormatH264.PPS,
|
test.FormatH264.PPS,
|
||||||
{0x05, 1}, // IDR
|
{0x05, 1}, // IDR
|
||||||
|
|
@ -156,7 +156,7 @@ func TestServerPublish(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{
|
||||||
{5, 2},
|
{5, 2},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -205,7 +205,7 @@ func TestServerRead(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
u := "srt://127.0.0.1:8890?streamid=read:mypath:myuser:mypass"
|
u := "srt://localhost:8890?streamid=read:mypath:myuser:mypass"
|
||||||
|
|
||||||
srtConf := srt.DefaultConfig()
|
srtConf := srt.DefaultConfig()
|
||||||
address, err := srtConf.UnmarshalURL(u)
|
address, err := srtConf.UnmarshalURL(u)
|
||||||
|
|
@ -237,7 +237,7 @@ func TestServerRead(t *testing.T) {
|
||||||
|
|
||||||
received := false
|
received := false
|
||||||
|
|
||||||
r.OnDataH264(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error {
|
r.OnDataH26x(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error {
|
||||||
require.Equal(t, int64(0), pts)
|
require.Equal(t, int64(0), pts)
|
||||||
require.Equal(t, int64(0), dts)
|
require.Equal(t, int64(0), dts)
|
||||||
require.Equal(t, [][]byte{
|
require.Equal(t, [][]byte{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -60,17 +59,6 @@ func sessionLocation(publish bool, path string, secret uuid.UUID) string {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func addJWTFromAuthorization(rawQuery string, auth string) string {
|
|
||||||
jwt := strings.TrimPrefix(auth, "Bearer ")
|
|
||||||
if rawQuery != "" {
|
|
||||||
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
|
|
||||||
v.Set("jwt", jwt)
|
|
||||||
return v.Encode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url.Values{"jwt": []string{jwt}}.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpServer struct {
|
type httpServer struct {
|
||||||
address string
|
address string
|
||||||
encryption bool
|
encryption bool
|
||||||
|
|
@ -121,23 +109,11 @@ func (s *httpServer) close() {
|
||||||
|
|
||||||
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
|
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
|
||||||
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
user, pass, hasCredentials := ctx.Request.BasicAuth()
|
||||||
q := ctx.Request.URL.RawQuery
|
|
||||||
|
|
||||||
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
|
|
||||||
// JWT in authorization bearer -> JWT in query parameters
|
|
||||||
q = addJWTFromAuthorization(q, h)
|
|
||||||
|
|
||||||
// credentials in authorization bearer -> credentials in authorization basic
|
|
||||||
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
|
|
||||||
user = parts[0]
|
|
||||||
pass = parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: pathName,
|
Name: pathName,
|
||||||
Query: q,
|
Query: ctx.Request.URL.RawQuery,
|
||||||
Publish: publish,
|
Publish: publish,
|
||||||
IP: net.ParseIP(ctx.ClientIP()),
|
IP: net.ParseIP(ctx.ClientIP()),
|
||||||
User: user,
|
User: user,
|
||||||
|
|
@ -201,23 +177,11 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, pass, _ := ctx.Request.BasicAuth()
|
user, pass, _ := ctx.Request.BasicAuth()
|
||||||
q := ctx.Request.URL.RawQuery
|
|
||||||
|
|
||||||
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
|
|
||||||
// JWT in authorization bearer -> JWT in query parameters
|
|
||||||
q = addJWTFromAuthorization(q, h)
|
|
||||||
|
|
||||||
// credentials in authorization bearer -> credentials in authorization basic
|
|
||||||
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
|
|
||||||
user = parts[0]
|
|
||||||
pass = parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res := s.parent.newSession(webRTCNewSessionReq{
|
res := s.parent.newSession(webRTCNewSessionReq{
|
||||||
pathName: pathName,
|
pathName: pathName,
|
||||||
remoteAddr: httpp.RemoteAddr(ctx),
|
remoteAddr: httpp.RemoteAddr(ctx),
|
||||||
query: q,
|
query: ctx.Request.URL.RawQuery,
|
||||||
user: user,
|
user: user,
|
||||||
pass: pass,
|
pass: pass,
|
||||||
offer: offer,
|
offer: offer,
|
||||||
|
|
|
||||||
|
|
@ -376,10 +376,11 @@ const editOffer = (sdp) => {
|
||||||
const sections = sdp.split('m=');
|
const sections = sdp.split('m=');
|
||||||
|
|
||||||
for (let i = 0; i < sections.length; i++) {
|
for (let i = 0; i < sections.length; i++) {
|
||||||
if (sections[i].startsWith('video')) {
|
const section = sections[i];
|
||||||
sections[i] = setCodec(sections[i], videoForm.codec.value);
|
if (section.startsWith('video')) {
|
||||||
} else if (sections[i].startsWith('audio')) {
|
sections[i] = setCodec(section, videoForm.codec.value);
|
||||||
sections[i] = setAudioBitrate(setCodec(sections[i], audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked);
|
} else if (section.startsWith('audio')) {
|
||||||
|
sections[i] = setAudioBitrate(setCodec(section, audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,8 +391,9 @@ const editAnswer = (sdp) => {
|
||||||
const sections = sdp.split('m=');
|
const sections = sdp.split('m=');
|
||||||
|
|
||||||
for (let i = 0; i < sections.length; i++) {
|
for (let i = 0; i < sections.length; i++) {
|
||||||
if (sections[i].startsWith('video')) {
|
const section = sections[i];
|
||||||
sections[i] = setVideoBitrate(sections[i], videoForm.bitrate.value);
|
if (section.startsWith('video')) {
|
||||||
|
sections[i] = setVideoBitrate(section, videoForm.bitrate.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,7 +411,7 @@ const sendLocalCandidates = (candidates) => {
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status !== 204) {
|
if (res.status !== 204) {
|
||||||
throw new Error(`bad status code ${res.status}`);
|
throw new Error('bad status code');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
@ -441,16 +443,12 @@ const onRemoteAnswer = (sdp) => {
|
||||||
pc.setRemoteDescription(new RTCSessionDescription({
|
pc.setRemoteDescription(new RTCSessionDescription({
|
||||||
type: 'answer',
|
type: 'answer',
|
||||||
sdp,
|
sdp,
|
||||||
}))
|
}));
|
||||||
.then(() => {
|
|
||||||
if (queuedCandidates.length !== 0) {
|
if (queuedCandidates.length !== 0) {
|
||||||
sendLocalCandidates(queuedCandidates);
|
sendLocalCandidates(queuedCandidates);
|
||||||
queuedCandidates = [];
|
queuedCandidates = [];
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
onError(err.toString());
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendOffer = (offer) => {
|
const sendOffer = (offer) => {
|
||||||
|
|
@ -464,20 +462,13 @@ const sendOffer = (offer) => {
|
||||||
body: offer,
|
body: offer,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
switch (res.status) {
|
if (res.status !== 201) {
|
||||||
case 201:
|
throw new Error('bad status code');
|
||||||
break;
|
|
||||||
case 400:
|
|
||||||
return res.json().then((e) => { throw new Error(e.error); });
|
|
||||||
default:
|
|
||||||
throw new Error(`bad status code ${res.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionUrl = new URL(res.headers.get('location'), window.location.href).toString();
|
sessionUrl = new URL(res.headers.get('location'), window.location.href).toString();
|
||||||
|
return res.text();
|
||||||
return res.text()
|
|
||||||
.then((answer) => onRemoteAnswer(answer));
|
|
||||||
})
|
})
|
||||||
|
.then((answer) => onRemoteAnswer(answer))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
onError(err.toString(), true);
|
onError(err.toString(), true);
|
||||||
});
|
});
|
||||||
|
|
@ -487,16 +478,8 @@ const createOffer = () => {
|
||||||
pc.createOffer()
|
pc.createOffer()
|
||||||
.then((offer) => {
|
.then((offer) => {
|
||||||
offerData = parseOffer(offer.sdp);
|
offerData = parseOffer(offer.sdp);
|
||||||
pc.setLocalDescription(offer)
|
pc.setLocalDescription(offer);
|
||||||
.then(() => {
|
sendOffer(offer.sdp);
|
||||||
sendOffer(offer.sdp);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
onError(err.toString());
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
onError(err.toString());
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -506,7 +489,7 @@ const onConnectionState = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pc.iceConnectionState === 'disconnected') {
|
if (pc.iceConnectionState === 'disconnected') {
|
||||||
onError('peer connection closed', true);
|
onError('peer connection disconnected', true);
|
||||||
} else if (pc.iceConnectionState === 'connected') {
|
} else if (pc.iceConnectionState === 'connected') {
|
||||||
setMessage('');
|
setMessage('');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ const retryPause = 2000;
|
||||||
const video = document.getElementById('video');
|
const video = document.getElementById('video');
|
||||||
const message = document.getElementById('message');
|
const message = document.getElementById('message');
|
||||||
|
|
||||||
let nonAdvertisedCodecs = [];
|
|
||||||
let pc = null;
|
let pc = null;
|
||||||
let restartTimeout = null;
|
let restartTimeout = null;
|
||||||
let sessionUrl = '';
|
let sessionUrl = '';
|
||||||
|
|
@ -88,14 +87,14 @@ const linkToIceServers = (links) => (
|
||||||
}) : []
|
}) : []
|
||||||
);
|
);
|
||||||
|
|
||||||
const parseOffer = (sdp) => {
|
const parseOffer = (offer) => {
|
||||||
const ret = {
|
const ret = {
|
||||||
iceUfrag: '',
|
iceUfrag: '',
|
||||||
icePwd: '',
|
icePwd: '',
|
||||||
medias: [],
|
medias: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const line of sdp.split('\r\n')) {
|
for (const line of offer.split('\r\n')) {
|
||||||
if (line.startsWith('m=')) {
|
if (line.startsWith('m=')) {
|
||||||
ret.medias.push(line.slice('m='.length));
|
ret.medias.push(line.slice('m='.length));
|
||||||
} else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
|
} else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
|
||||||
|
|
@ -108,74 +107,6 @@ const parseOffer = (sdp) => {
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableStereoPcmau = (section) => {
|
|
||||||
let lines = section.split('\r\n');
|
|
||||||
|
|
||||||
lines[0] += ' 118';
|
|
||||||
lines.splice(lines.length - 1, 0, 'a=rtpmap:118 PCMU/8000/2');
|
|
||||||
lines.splice(lines.length - 1, 0, 'a=rtcp-fb:118 transport-cc');
|
|
||||||
|
|
||||||
lines[0] += ' 119';
|
|
||||||
lines.splice(lines.length - 1, 0, 'a=rtpmap:119 PCMA/8000/2');
|
|
||||||
lines.splice(lines.length - 1, 0, 'a=rtcp-fb:119 transport-cc');
|
|
||||||
|
|
||||||
return lines.join('\r\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableMultichannelOpus = (section) => {
|
|
||||||
let lines = section.split('\r\n');
|
|
||||||
|
|
||||||
lines[0] += " 112";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:112 multiopus/48000/3");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=fmtp:112 channel_mapping=0,2,1;num_streams=2;coupled_streams=1");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:112 transport-cc");
|
|
||||||
|
|
||||||
lines[0] += " 113";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:113 multiopus/48000/4");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=fmtp:113 channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:113 transport-cc");
|
|
||||||
|
|
||||||
lines[0] += " 114";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:114 multiopus/48000/5");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=fmtp:114 channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:114 transport-cc");
|
|
||||||
|
|
||||||
lines[0] += " 115";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:115 multiopus/48000/6");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=fmtp:115 channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:115 transport-cc");
|
|
||||||
|
|
||||||
lines[0] += " 116";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:116 multiopus/48000/7");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=fmtp:116 channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:116 transport-cc");
|
|
||||||
|
|
||||||
lines[0] += " 117";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:117 multiopus/48000/8");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=fmtp:117 channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:117 transport-cc");
|
|
||||||
|
|
||||||
return lines.join('\r\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableL16 = (section) => {
|
|
||||||
let lines = section.split('\r\n');
|
|
||||||
|
|
||||||
lines[0] += " 120";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:120 L16/8000/2");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:120 transport-cc");
|
|
||||||
|
|
||||||
lines[0] += " 121";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:121 L16/16000/2");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:121 transport-cc");
|
|
||||||
|
|
||||||
lines[0] += " 122";
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtpmap:122 L16/48000/2");
|
|
||||||
lines.splice(lines.length - 1, 0, "a=rtcp-fb:122 transport-cc");
|
|
||||||
|
|
||||||
return lines.join('\r\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableStereoOpus = (section) => {
|
const enableStereoOpus = (section) => {
|
||||||
let opusPayloadFormat = '';
|
let opusPayloadFormat = '';
|
||||||
let lines = section.split('\r\n');
|
let lines = section.split('\r\n');
|
||||||
|
|
@ -205,30 +136,17 @@ const enableStereoOpus = (section) => {
|
||||||
return lines.join('\r\n');
|
return lines.join('\r\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
const editOffer = (sdp) => {
|
const editOffer = (offer) => {
|
||||||
const sections = sdp.split('m=');
|
const sections = offer.sdp.split('m=');
|
||||||
|
|
||||||
for (let i = 0; i < sections.length; i++) {
|
for (let i = 0; i < sections.length; i++) {
|
||||||
if (sections[i].startsWith('audio')) {
|
const section = sections[i];
|
||||||
sections[i] = enableStereoOpus(sections[i]);
|
if (section.startsWith('audio')) {
|
||||||
|
sections[i] = enableStereoOpus(section);
|
||||||
if (nonAdvertisedCodecs.includes('pcma/8000/2')) {
|
|
||||||
sections[i] = enableStereoPcmau(sections[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nonAdvertisedCodecs.includes('multiopus/48000/6')) {
|
|
||||||
sections[i] = enableMultichannelOpus(sections[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nonAdvertisedCodecs.includes('L16/48000/2')) {
|
|
||||||
sections[i] = enableL16(sections[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sections.join('m=');
|
offer.sdp = sections.join('m=');
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateSdpFragment = (od, candidates) => {
|
const generateSdpFragment = (od, candidates) => {
|
||||||
|
|
@ -265,70 +183,6 @@ const loadStream = () => {
|
||||||
requestICEServers();
|
requestICEServers();
|
||||||
};
|
};
|
||||||
|
|
||||||
const supportsNonAdvertisedCodec = (codec, fmtp) => (
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const pc = new RTCPeerConnection({ iceServers: [] });
|
|
||||||
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
||||||
pc.createOffer()
|
|
||||||
.then((offer) => {
|
|
||||||
if (offer.sdp.includes(' ' + codec)) { // codec is advertised, there's no need to add it manually
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sections = offer.sdp.split('m=audio');
|
|
||||||
const lines = sections[1].split('\r\n');
|
|
||||||
lines[0] += ' 118';
|
|
||||||
lines.splice(lines.length - 1, 0, 'a=rtpmap:118 ' + codec);
|
|
||||||
if (fmtp !== undefined) {
|
|
||||||
lines.splice(lines.length - 1, 0, 'a=fmtp:118 ' + fmtp);
|
|
||||||
}
|
|
||||||
sections[1] = lines.join('\r\n');
|
|
||||||
offer.sdp = sections.join('m=audio');
|
|
||||||
return pc.setLocalDescription(offer);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return pc.setRemoteDescription(new RTCSessionDescription({
|
|
||||||
type: 'answer',
|
|
||||||
sdp: 'v=0\r\n'
|
|
||||||
+ 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n'
|
|
||||||
+ 's=-\r\n'
|
|
||||||
+ 't=0 0\r\n'
|
|
||||||
+ 'a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n'
|
|
||||||
+ 'm=audio 9 UDP/TLS/RTP/SAVPF 118\r\n'
|
|
||||||
+ 'c=IN IP4 0.0.0.0\r\n'
|
|
||||||
+ 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n'
|
|
||||||
+ 'a=ice-ufrag:29e036dc\r\n'
|
|
||||||
+ 'a=sendonly\r\n'
|
|
||||||
+ 'a=rtcp-mux\r\n'
|
|
||||||
+ 'a=rtpmap:118 ' + codec + '\r\n'
|
|
||||||
+ ((fmtp !== undefined) ? 'a=fmtp:118 ' + fmtp + '\r\n' : ''),
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
resolve(true);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
resolve(false);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
pc.close();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const getNonAdvertisedCodecs = () => {
|
|
||||||
Promise.all([
|
|
||||||
['pcma/8000/2'],
|
|
||||||
['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'],
|
|
||||||
['L16/48000/2']
|
|
||||||
].map((c) => supportsNonAdvertisedCodec(c[0], c[1]).then((r) => (r) ? c[0] : false)))
|
|
||||||
.then((c) => c.filter((e) => e !== false))
|
|
||||||
.then((codecs) => {
|
|
||||||
nonAdvertisedCodecs = codecs;
|
|
||||||
loadStream();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onError = (err) => {
|
const onError = (err) => {
|
||||||
if (restartTimeout === null) {
|
if (restartTimeout === null) {
|
||||||
setMessage(err + ', retrying in some seconds');
|
setMessage(err + ', retrying in some seconds');
|
||||||
|
|
@ -400,16 +254,12 @@ const onRemoteAnswer = (sdp) => {
|
||||||
pc.setRemoteDescription(new RTCSessionDescription({
|
pc.setRemoteDescription(new RTCSessionDescription({
|
||||||
type: 'answer',
|
type: 'answer',
|
||||||
sdp,
|
sdp,
|
||||||
}))
|
}));
|
||||||
.then(() => {
|
|
||||||
if (queuedCandidates.length !== 0) {
|
if (queuedCandidates.length !== 0) {
|
||||||
sendLocalCandidates(queuedCandidates);
|
sendLocalCandidates(queuedCandidates);
|
||||||
queuedCandidates = [];
|
queuedCandidates = [];
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
onError(err.toString());
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendOffer = (offer) => {
|
const sendOffer = (offer) => {
|
||||||
|
|
@ -426,16 +276,11 @@ const sendOffer = (offer) => {
|
||||||
break;
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
throw new Error('stream not found');
|
throw new Error('stream not found');
|
||||||
case 400:
|
|
||||||
return res.json().then((e) => { throw new Error(e.error); });
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`bad status code ${res.status}`);
|
throw new Error(`bad status code ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionUrl = new URL(res.headers.get('location'), window.location.href).toString();
|
sessionUrl = new URL(res.headers.get('location'), window.location.href).toString();
|
||||||
|
return res.text();
|
||||||
return res.text()
|
|
||||||
.then((sdp) => onRemoteAnswer(sdp));
|
|
||||||
})
|
})
|
||||||
.then((sdp) => onRemoteAnswer(sdp))
|
.then((sdp) => onRemoteAnswer(sdp))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
@ -446,18 +291,10 @@ const sendOffer = (offer) => {
|
||||||
const createOffer = () => {
|
const createOffer = () => {
|
||||||
pc.createOffer()
|
pc.createOffer()
|
||||||
.then((offer) => {
|
.then((offer) => {
|
||||||
offer.sdp = editOffer(offer.sdp);
|
editOffer(offer);
|
||||||
offerData = parseOffer(offer.sdp);
|
offerData = parseOffer(offer.sdp);
|
||||||
pc.setLocalDescription(offer)
|
pc.setLocalDescription(offer);
|
||||||
.then(() => {
|
sendOffer(offer);
|
||||||
sendOffer(offer);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
onError(err.toString());
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
onError(err.toString());
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -467,7 +304,7 @@ const onConnectionState = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pc.iceConnectionState === 'disconnected') {
|
if (pc.iceConnectionState === 'disconnected') {
|
||||||
onError('peer connection closed');
|
onError('peer connection disconnected');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -525,7 +362,7 @@ const loadAttributesFromQuery = () => {
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
loadAttributesFromQuery();
|
loadAttributesFromQuery();
|
||||||
getNonAdvertisedCodecs();
|
loadStream();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', init);
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,6 @@ type Server struct {
|
||||||
IPsFromInterfacesList []string
|
IPsFromInterfacesList []string
|
||||||
AdditionalHosts []string
|
AdditionalHosts []string
|
||||||
ICEServers []conf.WebRTCICEServer
|
ICEServers []conf.WebRTCICEServer
|
||||||
HandshakeTimeout conf.StringDuration
|
|
||||||
TrackGatherTimeout conf.StringDuration
|
|
||||||
ExternalCmdPool *externalcmd.Pool
|
ExternalCmdPool *externalcmd.Pool
|
||||||
PathManager serverPathManager
|
PathManager serverPathManager
|
||||||
Parent serverParent
|
Parent serverParent
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,14 @@ package webrtc
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/auth"
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||||
|
|
@ -26,6 +24,10 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func uint16Ptr(v uint16) *uint16 {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
func checkClose(t *testing.T, closeFunc func() error) {
|
func checkClose(t *testing.T, closeFunc func() error) {
|
||||||
require.NoError(t, closeFunc())
|
require.NoError(t, closeFunc())
|
||||||
}
|
}
|
||||||
|
|
@ -72,38 +74,40 @@ func (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dummyPathManager struct {
|
type dummyPathManager struct {
|
||||||
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error)
|
path *dummyPath
|
||||||
addPublisher func(req defs.PathAddPublisherReq) (defs.Path, error)
|
|
||||||
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
||||||
return pm.findPathConf(req)
|
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
|
||||||
|
return nil, auth.Error{}
|
||||||
|
}
|
||||||
|
return &conf.Path{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
|
func (pm *dummyPathManager) AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, error) {
|
||||||
return pm.addPublisher(req)
|
return pm.path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
return pm.addReader(req)
|
if req.AccessRequest.Name == "nonexisting" {
|
||||||
|
return nil, nil, defs.PathNoOnePublishingError{}
|
||||||
|
}
|
||||||
|
return pm.path, pm.path.stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeTestServer(t *testing.T) *Server {
|
func initializeTestServer(t *testing.T) *Server {
|
||||||
pm := &dummyPathManager{
|
path := &dummyPath{
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
streamCreated: make(chan struct{}),
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pathManager := &dummyPathManager{path: path}
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Address: "127.0.0.1:8886",
|
Address: "127.0.0.1:8886",
|
||||||
Encryption: false,
|
Encryption: false,
|
||||||
ServerKey: "",
|
ServerKey: "",
|
||||||
ServerCert: "",
|
ServerCert: "",
|
||||||
AllowOrigin: "*",
|
AllowOrigin: "",
|
||||||
TrustedProxies: conf.IPNetworks{},
|
TrustedProxies: conf.IPNetworks{},
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 512,
|
WriteQueueSize: 512,
|
||||||
|
|
@ -113,10 +117,8 @@ func initializeTestServer(t *testing.T) *Server {
|
||||||
IPsFromInterfacesList: []string{},
|
IPsFromInterfacesList: []string{},
|
||||||
AdditionalHosts: []string{},
|
AdditionalHosts: []string{},
|
||||||
ICEServers: []conf.WebRTCICEServer{},
|
ICEServers: []conf.WebRTCICEServer{},
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
ExternalCmdPool: nil,
|
ExternalCmdPool: nil,
|
||||||
PathManager: pm,
|
PathManager: pathManager,
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
}
|
}
|
||||||
err := s.Initialize()
|
err := s.Initialize()
|
||||||
|
|
@ -147,7 +149,7 @@ func TestServerStaticPages(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreflightRequest(t *testing.T) {
|
func TestServerOptionsPreflight(t *testing.T) {
|
||||||
s := initializeTestServer(t)
|
s := initializeTestServer(t)
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
|
|
@ -155,10 +157,11 @@ func TestPreflightRequest(t *testing.T) {
|
||||||
defer tr.CloseIdleConnections()
|
defer tr.CloseIdleConnections()
|
||||||
hc := &http.Client{Transport: tr}
|
hc := &http.Client{Transport: tr}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886", nil)
|
// preflight requests must always work, without authentication
|
||||||
|
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886/teststream/whip", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
req.Header.Add("Access-Control-Request-Method", "GET")
|
req.Header.Set("Access-Control-Request-Method", "OPTIONS")
|
||||||
|
|
||||||
res, err := hc.Do(req)
|
res, err := hc.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -166,24 +169,12 @@ func TestPreflightRequest(t *testing.T) {
|
||||||
|
|
||||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
||||||
|
|
||||||
byts, err := io.ReadAll(res.Body)
|
_, ok := res.Header["Link"]
|
||||||
require.NoError(t, err)
|
require.Equal(t, false, ok)
|
||||||
|
|
||||||
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
|
|
||||||
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
|
|
||||||
require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods"))
|
|
||||||
require.Equal(t, "Authorization, Content-Type, If-Match", res.Header.Get("Access-Control-Allow-Headers"))
|
|
||||||
require.Equal(t, byts, []byte{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerOptionsICEServer(t *testing.T) {
|
func TestServerOptionsICEServer(t *testing.T) {
|
||||||
pathManager := &dummyPathManager{
|
pathManager := &dummyPathManager{}
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Address: "127.0.0.1:8886",
|
Address: "127.0.0.1:8886",
|
||||||
|
|
@ -204,11 +195,9 @@ func TestServerOptionsICEServer(t *testing.T) {
|
||||||
Username: "myuser",
|
Username: "myuser",
|
||||||
Password: "mypass",
|
Password: "mypass",
|
||||||
}},
|
}},
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
ExternalCmdPool: nil,
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
PathManager: pathManager,
|
||||||
ExternalCmdPool: nil,
|
Parent: test.NilLogger,
|
||||||
PathManager: pathManager,
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
}
|
||||||
err := s.Initialize()
|
err := s.Initialize()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -243,20 +232,7 @@ func TestServerPublish(t *testing.T) {
|
||||||
streamCreated: make(chan struct{}),
|
streamCreated: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
pathManager := &dummyPathManager{
|
pathManager := &dummyPathManager{path: path}
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) {
|
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return path, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
Address: "127.0.0.1:8886",
|
Address: "127.0.0.1:8886",
|
||||||
|
|
@ -273,8 +249,6 @@ func TestServerPublish(t *testing.T) {
|
||||||
IPsFromInterfacesList: []string{},
|
IPsFromInterfacesList: []string{},
|
||||||
AdditionalHosts: []string{},
|
AdditionalHosts: []string{},
|
||||||
ICEServers: []conf.WebRTCICEServer{},
|
ICEServers: []conf.WebRTCICEServer{},
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
ExternalCmdPool: nil,
|
ExternalCmdPool: nil,
|
||||||
PathManager: pathManager,
|
PathManager: pathManager,
|
||||||
Parent: test.NilLogger,
|
Parent: test.NilLogger,
|
||||||
|
|
@ -356,450 +330,109 @@ func TestServerPublish(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerRead(t *testing.T) {
|
func TestServerRead(t *testing.T) {
|
||||||
for _, ca := range []struct {
|
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
||||||
name string
|
|
||||||
medias []*description.Media
|
stream, err := stream.New(
|
||||||
unit unit.Unit
|
1460,
|
||||||
outRTPPayload []byte
|
desc,
|
||||||
}{
|
true,
|
||||||
{
|
test.NilLogger,
|
||||||
"av1",
|
)
|
||||||
[]*description.Media{{
|
require.NoError(t, err)
|
||||||
Type: description.MediaTypeVideo,
|
|
||||||
Formats: []format.Format{&format.AV1{
|
path := &dummyPath{stream: stream}
|
||||||
PayloadTyp: 96,
|
|
||||||
}},
|
pathManager := &dummyPathManager{path: path}
|
||||||
}},
|
|
||||||
&unit.AV1{
|
s := &Server{
|
||||||
TU: [][]byte{{1, 2}},
|
Address: "127.0.0.1:8886",
|
||||||
},
|
Encryption: false,
|
||||||
[]byte{0, 2, 1, 2},
|
ServerKey: "",
|
||||||
},
|
ServerCert: "",
|
||||||
{
|
AllowOrigin: "",
|
||||||
"vp9",
|
TrustedProxies: conf.IPNetworks{},
|
||||||
[]*description.Media{{
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Type: description.MediaTypeVideo,
|
WriteQueueSize: 512,
|
||||||
Formats: []format.Format{&format.VP9{
|
LocalUDPAddress: "127.0.0.1:8887",
|
||||||
PayloadTyp: 96,
|
LocalTCPAddress: "127.0.0.1:8887",
|
||||||
}},
|
IPsFromInterfaces: true,
|
||||||
}},
|
IPsFromInterfacesList: []string{},
|
||||||
&unit.VP9{
|
AdditionalHosts: []string{},
|
||||||
Frame: []byte{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34},
|
ICEServers: []conf.WebRTCICEServer{},
|
||||||
},
|
ExternalCmdPool: nil,
|
||||||
[]byte{
|
PathManager: pathManager,
|
||||||
0x8f, 0xa0, 0xfd, 0x18, 0x07, 0x80, 0x03, 0x24,
|
Parent: test.NilLogger,
|
||||||
0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00,
|
}
|
||||||
0x77, 0xf0, 0x32, 0x34,
|
err = s.Initialize()
|
||||||
},
|
require.NoError(t, err)
|
||||||
},
|
defer s.Close()
|
||||||
{
|
|
||||||
"vp8",
|
u, err := url.Parse("http://myuser:mypass@localhost:8886/teststream/whep?param=value")
|
||||||
[]*description.Media{{
|
require.NoError(t, err)
|
||||||
Type: description.MediaTypeVideo,
|
|
||||||
Formats: []format.Format{&format.VP8{
|
tr := &http.Transport{}
|
||||||
PayloadTyp: 96,
|
defer tr.CloseIdleConnections()
|
||||||
}},
|
hc := &http.Client{Transport: tr}
|
||||||
}},
|
|
||||||
&unit.VP8{
|
wc := &webrtc.WHIPClient{
|
||||||
Frame: []byte{1, 2},
|
HTTPClient: hc,
|
||||||
},
|
URL: u,
|
||||||
[]byte{0x10, 1, 2},
|
Log: test.NilLogger,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"h264",
|
writerDone := make(chan struct{})
|
||||||
[]*description.Media{test.MediaH264},
|
defer func() { <-writerDone }()
|
||||||
&unit.H264{
|
|
||||||
|
writerTerminate := make(chan struct{})
|
||||||
|
defer close(writerTerminate)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(writerDone)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
case <-writerTerminate:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
||||||
|
Base: unit.Base{
|
||||||
|
NTP: time.Time{},
|
||||||
|
},
|
||||||
AU: [][]byte{
|
AU: [][]byte{
|
||||||
{5, 1},
|
{5, 1},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
[]byte{
|
}
|
||||||
0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,
|
}()
|
||||||
0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,
|
|
||||||
0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,
|
tracks, err := wc.Read(context.Background())
|
||||||
0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,
|
require.NoError(t, err)
|
||||||
0x07, 0x08, 0x00, 0x02, 0x05, 0x01,
|
defer checkClose(t, wc.Close)
|
||||||
},
|
|
||||||
|
pkt, err := tracks[0].ReadRTP()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
|
PayloadType: 101,
|
||||||
|
SequenceNumber: pkt.SequenceNumber,
|
||||||
|
Timestamp: pkt.Timestamp,
|
||||||
|
SSRC: pkt.SSRC,
|
||||||
|
CSRC: []uint32{},
|
||||||
},
|
},
|
||||||
{
|
Payload: []byte{
|
||||||
"opus",
|
0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,
|
||||||
[]*description.Media{{
|
0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,
|
||||||
Type: description.MediaTypeAudio,
|
0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,
|
||||||
Formats: []format.Format{&format.Opus{
|
0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,
|
||||||
PayloadTyp: 96,
|
0x07, 0x08, 0x00, 0x02, 0x05, 0x01,
|
||||||
ChannelCount: 2,
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
&unit.Opus{
|
|
||||||
Packets: [][]byte{{1, 2}},
|
|
||||||
},
|
|
||||||
[]byte{1, 2},
|
|
||||||
},
|
},
|
||||||
{
|
}, pkt)
|
||||||
"g722",
|
|
||||||
[]*description.Media{{
|
|
||||||
Type: description.MediaTypeAudio,
|
|
||||||
Formats: []format.Format{&format.G722{}},
|
|
||||||
}},
|
|
||||||
&unit.Generic{
|
|
||||||
Base: unit.Base{
|
|
||||||
RTPPackets: []*rtp.Packet{{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: true,
|
|
||||||
PayloadType: 9,
|
|
||||||
SequenceNumber: 1123,
|
|
||||||
Timestamp: 45343,
|
|
||||||
SSRC: 563423,
|
|
||||||
},
|
|
||||||
Payload: []byte{1, 2},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[]byte{1, 2},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 8khz mono",
|
|
||||||
[]*description.Media{{
|
|
||||||
Type: description.MediaTypeAudio,
|
|
||||||
Formats: []format.Format{&format.G711{
|
|
||||||
MULaw: true,
|
|
||||||
SampleRate: 8000,
|
|
||||||
ChannelCount: 1,
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
&unit.G711{
|
|
||||||
Samples: []byte{1, 2, 3},
|
|
||||||
},
|
|
||||||
[]byte{1, 2, 3},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"g711 16khz stereo",
|
|
||||||
[]*description.Media{{
|
|
||||||
Type: description.MediaTypeAudio,
|
|
||||||
Formats: []format.Format{&format.G711{
|
|
||||||
MULaw: true,
|
|
||||||
SampleRate: 16000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
&unit.G711{
|
|
||||||
Samples: []byte{1, 2, 3, 4},
|
|
||||||
},
|
|
||||||
[]byte{0x86, 0x84, 0x8a, 0x84, 0x8e, 0x84, 0x92, 0x84},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"lpcm",
|
|
||||||
[]*description.Media{{
|
|
||||||
Type: description.MediaTypeAudio,
|
|
||||||
Formats: []format.Format{&format.LPCM{
|
|
||||||
PayloadTyp: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
SampleRate: 48000,
|
|
||||||
ChannelCount: 2,
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
&unit.LPCM{
|
|
||||||
Samples: []byte{1, 2, 3, 4},
|
|
||||||
},
|
|
||||||
[]byte{1, 2, 3, 4},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(ca.name, func(t *testing.T) {
|
|
||||||
desc := &description.Session{Medias: ca.medias}
|
|
||||||
|
|
||||||
str, err := stream.New(
|
|
||||||
1460,
|
|
||||||
desc,
|
|
||||||
reflect.TypeOf(ca.unit) != reflect.TypeOf(&unit.Generic{}),
|
|
||||||
test.NilLogger,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
path := &dummyPath{stream: str}
|
|
||||||
|
|
||||||
pathManager := &dummyPathManager{
|
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return path, str, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
Address: "127.0.0.1:8886",
|
|
||||||
Encryption: false,
|
|
||||||
ServerKey: "",
|
|
||||||
ServerCert: "",
|
|
||||||
AllowOrigin: "",
|
|
||||||
TrustedProxies: conf.IPNetworks{},
|
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
WriteQueueSize: 512,
|
|
||||||
LocalUDPAddress: "127.0.0.1:8887",
|
|
||||||
LocalTCPAddress: "127.0.0.1:8887",
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
IPsFromInterfacesList: []string{},
|
|
||||||
AdditionalHosts: []string{},
|
|
||||||
ICEServers: []conf.WebRTCICEServer{},
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
ExternalCmdPool: nil,
|
|
||||||
PathManager: pathManager,
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
|
||||||
err = s.Initialize()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
u, err := url.Parse("http://myuser:mypass@localhost:8886/teststream/whep?param=value")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tr := &http.Transport{}
|
|
||||||
defer tr.CloseIdleConnections()
|
|
||||||
hc := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
wc := &webrtc.WHIPClient{
|
|
||||||
HTTPClient: hc,
|
|
||||||
URL: u,
|
|
||||||
Log: test.NilLogger,
|
|
||||||
}
|
|
||||||
|
|
||||||
writerDone := make(chan struct{})
|
|
||||||
defer func() { <-writerDone }()
|
|
||||||
|
|
||||||
writerTerminate := make(chan struct{})
|
|
||||||
defer close(writerTerminate)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(writerDone)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
case <-writerTerminate:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r := reflect.New(reflect.TypeOf(ca.unit).Elem())
|
|
||||||
r.Elem().Set(reflect.ValueOf(ca.unit).Elem())
|
|
||||||
|
|
||||||
if g, ok := r.Interface().(*unit.Generic); ok {
|
|
||||||
clone := *g.RTPPackets[0]
|
|
||||||
str.WriteRTPPacket(desc.Medias[0], desc.Medias[0].Formats[0], &clone, time.Time{}, 0)
|
|
||||||
} else {
|
|
||||||
str.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(unit.Unit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tracks, err := wc.Read(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer checkClose(t, wc.Close)
|
|
||||||
|
|
||||||
pkt, err := tracks[0].ReadRTP()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, ca.outRTPPayload, pkt.Payload)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerReadAuthorizationBearerJWT(t *testing.T) {
|
func TestServerPostNotFound(t *testing.T) {
|
||||||
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
s := initializeTestServer(t)
|
||||||
|
|
||||||
str, err := stream.New(
|
|
||||||
1460,
|
|
||||||
desc,
|
|
||||||
true,
|
|
||||||
test.NilLogger,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
path := &dummyPath{stream: str}
|
|
||||||
|
|
||||||
pm := &dummyPathManager{
|
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
|
|
||||||
return path, str, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
Address: "127.0.0.1:8886",
|
|
||||||
Encryption: false,
|
|
||||||
ServerKey: "",
|
|
||||||
ServerCert: "",
|
|
||||||
AllowOrigin: "",
|
|
||||||
TrustedProxies: conf.IPNetworks{},
|
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
WriteQueueSize: 512,
|
|
||||||
LocalUDPAddress: "127.0.0.1:8887",
|
|
||||||
LocalTCPAddress: "127.0.0.1:8887",
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
IPsFromInterfacesList: []string{},
|
|
||||||
AdditionalHosts: []string{},
|
|
||||||
ICEServers: []conf.WebRTCICEServer{},
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
ExternalCmdPool: nil,
|
|
||||||
PathManager: pm,
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
|
||||||
err = s.Initialize()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
tr := &http.Transport{}
|
|
||||||
defer tr.CloseIdleConnections()
|
|
||||||
hc := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer pc.Close() //nolint:errcheck
|
|
||||||
|
|
||||||
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
offer, err := pc.CreateOffer(nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost,
|
|
||||||
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/sdp")
|
|
||||||
req.Header.Set("Authorization", "Bearer testing")
|
|
||||||
|
|
||||||
res, err := hc.Do(req)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusCreated, res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerReadAuthorizationUserPass(t *testing.T) {
|
|
||||||
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
|
|
||||||
|
|
||||||
str, err := stream.New(
|
|
||||||
1460,
|
|
||||||
desc,
|
|
||||||
true,
|
|
||||||
test.NilLogger,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
path := &dummyPath{stream: str}
|
|
||||||
|
|
||||||
pm := &dummyPathManager{
|
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return path, str, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
Address: "127.0.0.1:8886",
|
|
||||||
Encryption: false,
|
|
||||||
ServerKey: "",
|
|
||||||
ServerCert: "",
|
|
||||||
AllowOrigin: "",
|
|
||||||
TrustedProxies: conf.IPNetworks{},
|
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
WriteQueueSize: 512,
|
|
||||||
LocalUDPAddress: "127.0.0.1:8887",
|
|
||||||
LocalTCPAddress: "127.0.0.1:8887",
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
IPsFromInterfacesList: []string{},
|
|
||||||
AdditionalHosts: []string{},
|
|
||||||
ICEServers: []conf.WebRTCICEServer{},
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
ExternalCmdPool: nil,
|
|
||||||
PathManager: pm,
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
|
||||||
err = s.Initialize()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
tr := &http.Transport{}
|
|
||||||
defer tr.CloseIdleConnections()
|
|
||||||
hc := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer pc.Close() //nolint:errcheck
|
|
||||||
|
|
||||||
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
offer, err := pc.CreateOffer(nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost,
|
|
||||||
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/sdp")
|
|
||||||
req.Header.Set("Authorization", "Bearer myuser:mypass")
|
|
||||||
|
|
||||||
res, err := hc.Do(req)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusCreated, res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerReadNotFound(t *testing.T) {
|
|
||||||
pm := &dummyPathManager{
|
|
||||||
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
|
|
||||||
require.Equal(t, "myuser", req.AccessRequest.User)
|
|
||||||
require.Equal(t, "mypass", req.AccessRequest.Pass)
|
|
||||||
return &conf.Path{}, nil
|
|
||||||
},
|
|
||||||
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
|
||||||
return nil, nil, defs.PathNoOnePublishingError{}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &Server{
|
|
||||||
Address: "127.0.0.1:8886",
|
|
||||||
Encryption: false,
|
|
||||||
ServerKey: "",
|
|
||||||
ServerCert: "",
|
|
||||||
AllowOrigin: "",
|
|
||||||
TrustedProxies: conf.IPNetworks{},
|
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
WriteQueueSize: 512,
|
|
||||||
LocalUDPAddress: "127.0.0.1:8887",
|
|
||||||
LocalTCPAddress: "127.0.0.1:8887",
|
|
||||||
IPsFromInterfaces: true,
|
|
||||||
IPsFromInterfacesList: []string{},
|
|
||||||
AdditionalHosts: []string{},
|
|
||||||
ICEServers: []conf.WebRTCICEServer{},
|
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
|
||||||
ExternalCmdPool: nil,
|
|
||||||
PathManager: pm,
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
|
||||||
err := s.Initialize()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
tr := &http.Transport{}
|
tr := &http.Transport{}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -15,11 +14,9 @@ import (
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8"
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/rtptime"
|
"github.com/bluenviron/gortsplib/v4/pkg/rtptime"
|
||||||
"github.com/bluenviron/mediacommon/pkg/codecs/g711"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pion/ice/v2"
|
"github.com/pion/ice/v2"
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
|
|
@ -37,23 +34,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var errNoSupportedCodecs = errors.New(
|
var errNoSupportedCodecs = errors.New(
|
||||||
"the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711, LPCM")
|
"the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711")
|
||||||
|
|
||||||
type setupStreamFunc func(*webrtc.OutgoingTrack) error
|
type setupStreamFunc func(*webrtc.OutgoingTrack) error
|
||||||
|
|
||||||
func uint16Ptr(v uint16) *uint16 {
|
|
||||||
return &v
|
|
||||||
}
|
|
||||||
|
|
||||||
func randUint32() (uint32, error) {
|
|
||||||
var b [4]byte
|
|
||||||
_, err := rand.Read(b[:])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findVideoTrack(
|
func findVideoTrack(
|
||||||
stream *stream.Stream,
|
stream *stream.Stream,
|
||||||
writer *asyncwriter.Writer,
|
writer *asyncwriter.Writer,
|
||||||
|
|
@ -102,9 +86,8 @@ func findVideoTrack(
|
||||||
if vp9Format != nil {
|
if vp9Format != nil {
|
||||||
return vp9Format, func(track *webrtc.OutgoingTrack) error {
|
return vp9Format, func(track *webrtc.OutgoingTrack) error {
|
||||||
encoder := &rtpvp9.Encoder{
|
encoder := &rtpvp9.Encoder{
|
||||||
PayloadType: 96,
|
PayloadType: 96,
|
||||||
PayloadMaxSize: webrtcPayloadMaxSize,
|
PayloadMaxSize: webrtcPayloadMaxSize,
|
||||||
InitialPictureID: uint16Ptr(8445),
|
|
||||||
}
|
}
|
||||||
err := encoder.Init()
|
err := encoder.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -233,6 +216,10 @@ func findAudioTrack(
|
||||||
|
|
||||||
if opusFormat != nil {
|
if opusFormat != nil {
|
||||||
return opusFormat, func(track *webrtc.OutgoingTrack) error {
|
return opusFormat, func(track *webrtc.OutgoingTrack) error {
|
||||||
|
if opusFormat.ChannelCount > 2 {
|
||||||
|
return fmt.Errorf("unsupported Opus channel count: %d", opusFormat.ChannelCount)
|
||||||
|
}
|
||||||
|
|
||||||
stream.AddReader(writer, media, opusFormat, func(u unit.Unit) error {
|
stream.AddReader(writer, media, opusFormat, func(u unit.Unit) error {
|
||||||
for _, pkt := range u.GetRTPPackets() {
|
for _, pkt := range u.GetRTPPackets() {
|
||||||
track.WriteRTP(pkt) //nolint:errcheck
|
track.WriteRTP(pkt) //nolint:errcheck
|
||||||
|
|
@ -265,115 +252,16 @@ func findAudioTrack(
|
||||||
|
|
||||||
if g711Format != nil {
|
if g711Format != nil {
|
||||||
return g711Format, func(track *webrtc.OutgoingTrack) error {
|
return g711Format, func(track *webrtc.OutgoingTrack) error {
|
||||||
if g711Format.SampleRate == 8000 {
|
if g711Format.SampleRate != 8000 {
|
||||||
curTimestamp, err := randUint32()
|
return fmt.Errorf("unsupported G711 sample rate")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AddReader(writer, media, g711Format, func(u unit.Unit) error {
|
|
||||||
for _, pkt := range u.GetRTPPackets() {
|
|
||||||
// recompute timestamp from scratch.
|
|
||||||
// Chrome requires a precise timestamp that FFmpeg doesn't provide.
|
|
||||||
pkt.Timestamp = curTimestamp
|
|
||||||
curTimestamp += uint32(len(pkt.Payload)) / uint32(g711Format.ChannelCount)
|
|
||||||
|
|
||||||
track.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
encoder := &rtplpcm.Encoder{
|
|
||||||
PayloadType: 96,
|
|
||||||
PayloadMaxSize: webrtcPayloadMaxSize,
|
|
||||||
BitDepth: 16,
|
|
||||||
ChannelCount: g711Format.ChannelCount,
|
|
||||||
}
|
|
||||||
err := encoder.Init()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
curTimestamp, err := randUint32()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AddReader(writer, media, g711Format, func(u unit.Unit) error {
|
|
||||||
tunit := u.(*unit.G711)
|
|
||||||
|
|
||||||
if tunit.Samples == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var lpcmSamples []byte
|
|
||||||
if g711Format.MULaw {
|
|
||||||
lpcmSamples = g711.DecodeMulaw(tunit.Samples)
|
|
||||||
} else {
|
|
||||||
lpcmSamples = g711.DecodeAlaw(tunit.Samples)
|
|
||||||
}
|
|
||||||
|
|
||||||
packets, err := encoder.Encode(lpcmSamples)
|
|
||||||
if err != nil {
|
|
||||||
return nil //nolint:nilerr
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkt := range packets {
|
|
||||||
// recompute timestamp from scratch.
|
|
||||||
// Chrome requires a precise timestamp that FFmpeg doesn't provide.
|
|
||||||
pkt.Timestamp = curTimestamp
|
|
||||||
curTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(g711Format.ChannelCount)
|
|
||||||
|
|
||||||
track.WriteRTP(pkt) //nolint:errcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var lpcmFormat *format.LPCM
|
|
||||||
media = stream.Desc().FindFormat(&lpcmFormat)
|
|
||||||
|
|
||||||
if lpcmFormat != nil {
|
|
||||||
return lpcmFormat, func(track *webrtc.OutgoingTrack) error {
|
|
||||||
encoder := &rtplpcm.Encoder{
|
|
||||||
PayloadType: 96,
|
|
||||||
BitDepth: 16,
|
|
||||||
ChannelCount: lpcmFormat.ChannelCount,
|
|
||||||
PayloadMaxSize: webrtcPayloadMaxSize,
|
|
||||||
}
|
|
||||||
err := encoder.Init()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
curTimestamp, err := randUint32()
|
if g711Format.ChannelCount != 1 {
|
||||||
if err != nil {
|
return fmt.Errorf("unsupported G711 channel count")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.AddReader(writer, media, lpcmFormat, func(u unit.Unit) error {
|
stream.AddReader(writer, media, g711Format, func(u unit.Unit) error {
|
||||||
tunit := u.(*unit.LPCM)
|
for _, pkt := range u.GetRTPPackets() {
|
||||||
|
|
||||||
if tunit.Samples == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
packets, err := encoder.Encode(tunit.Samples)
|
|
||||||
if err != nil {
|
|
||||||
return nil //nolint:nilerr
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkt := range packets {
|
|
||||||
// recompute timestamp from scratch.
|
|
||||||
// Chrome requires a precise timestamp that FFmpeg doesn't provide.
|
|
||||||
pkt.Timestamp = curTimestamp
|
|
||||||
curTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(lpcmFormat.ChannelCount)
|
|
||||||
|
|
||||||
track.WriteRTP(pkt) //nolint:errcheck
|
track.WriteRTP(pkt) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -521,8 +409,6 @@ func (s *session) runPublish() (int, error) {
|
||||||
|
|
||||||
pc := &webrtc.PeerConnection{
|
pc := &webrtc.PeerConnection{
|
||||||
ICEServers: iceServers,
|
ICEServers: iceServers,
|
||||||
HandshakeTimeout: s.parent.HandshakeTimeout,
|
|
||||||
TrackGatherTimeout: s.parent.TrackGatherTimeout,
|
|
||||||
IPsFromInterfaces: s.ipsFromInterfaces,
|
IPsFromInterfaces: s.ipsFromInterfaces,
|
||||||
IPsFromInterfacesList: s.ipsFromInterfacesList,
|
IPsFromInterfacesList: s.ipsFromInterfacesList,
|
||||||
AdditionalHosts: s.additionalHosts,
|
AdditionalHosts: s.additionalHosts,
|
||||||
|
|
@ -545,7 +431,7 @@ func (s *session) runPublish() (int, error) {
|
||||||
return http.StatusBadRequest, err
|
return http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = webrtc.TracksAreValid(sdp.MediaDescriptions)
|
trackCount, err := webrtc.TrackCount(sdp.MediaDescriptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// RFC draft-ietf-wish-whip
|
// RFC draft-ietf-wish-whip
|
||||||
// if the number of audio and or video
|
// if the number of audio and or video
|
||||||
|
|
@ -573,7 +459,7 @@ func (s *session) runPublish() (int, error) {
|
||||||
s.pc = pc
|
s.pc = pc
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
|
|
||||||
tracks, err := pc.GatherIncomingTracks(s.ctx)
|
tracks, err := pc.GatherIncomingTracks(s.ctx, trackCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
@ -680,8 +566,6 @@ func (s *session) runRead() (int, error) {
|
||||||
|
|
||||||
pc := &webrtc.PeerConnection{
|
pc := &webrtc.PeerConnection{
|
||||||
ICEServers: iceServers,
|
ICEServers: iceServers,
|
||||||
HandshakeTimeout: s.parent.HandshakeTimeout,
|
|
||||||
TrackGatherTimeout: s.parent.TrackGatherTimeout,
|
|
||||||
IPsFromInterfaces: s.ipsFromInterfaces,
|
IPsFromInterfaces: s.ipsFromInterfaces,
|
||||||
IPsFromInterfacesList: s.ipsFromInterfacesList,
|
IPsFromInterfacesList: s.ipsFromInterfacesList,
|
||||||
AdditionalHosts: s.additionalHosts,
|
AdditionalHosts: s.additionalHosts,
|
||||||
|
|
@ -776,7 +660,7 @@ func (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) {
|
||||||
select {
|
select {
|
||||||
case req := <-s.chAddCandidates:
|
case req := <-s.chAddCandidates:
|
||||||
for _, candidate := range req.candidates {
|
for _, candidate := range req.candidates {
|
||||||
err := pc.AddRemoteCandidate(candidate)
|
err := pc.AddRemoteCandidate(*candidate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.res <- webRTCAddSessionCandidatesRes{err: err}
|
req.res <- webRTCAddSessionCandidatesRes{err: err}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ import (
|
||||||
|
|
||||||
// Source is a HLS static source.
|
// Source is a HLS static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ReadTimeout conf.StringDuration
|
ResolvedSource string
|
||||||
Parent defs.StaticSourceParent
|
ReadTimeout conf.StringDuration
|
||||||
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
// Log implements logger.Writer.
|
||||||
|
|
@ -48,7 +49,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
|
|
||||||
var c *gohlslib.Client
|
var c *gohlslib.Client
|
||||||
c = &gohlslib.Client{
|
c = &gohlslib.Client{
|
||||||
URI: params.ResolvedSource,
|
URI: s.ResolvedSource,
|
||||||
HTTPClient: &http.Client{
|
HTTPClient: &http.Client{
|
||||||
Timeout: time.Duration(s.ReadTimeout),
|
Timeout: time.Duration(s.ReadTimeout),
|
||||||
Transport: tr,
|
Transport: tr,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ func TestSource(t *testing.T) {
|
||||||
err := w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}})
|
err := w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track1, 2*90000, 2*90000, true, [][]byte{
|
err = w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{
|
||||||
{7, 1, 2, 3}, // SPS
|
{7, 1, 2, 3}, // SPS
|
||||||
{8}, // PPS
|
{8}, // PPS
|
||||||
})
|
})
|
||||||
|
|
@ -90,10 +90,10 @@ func TestSource(t *testing.T) {
|
||||||
te := test.NewSourceTester(
|
te := test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
Parent: p,
|
ResolvedSource: "http://localhost:5780/stream.m3u8",
|
||||||
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"http://localhost:5780/stream.m3u8",
|
|
||||||
&conf.Path{},
|
&conf.Path{},
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,10 @@ import (
|
||||||
|
|
||||||
// Source is a RTMP static source.
|
// Source is a RTMP static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ReadTimeout conf.StringDuration
|
ResolvedSource string
|
||||||
WriteTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
Parent defs.StaticSourceParent
|
WriteTimeout conf.StringDuration
|
||||||
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
// Log implements logger.Writer.
|
||||||
|
|
@ -37,7 +38,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
u, err := url.Parse(params.ResolvedSource)
|
u, err := url.Parse(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,24 +64,24 @@ func TestSource(t *testing.T) {
|
||||||
te = test.NewSourceTester(
|
te = test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ResolvedSource: "rtmp://localhost/teststream",
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Parent: p,
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rtmp://localhost/teststream",
|
|
||||||
&conf.Path{},
|
&conf.Path{},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
te = test.NewSourceTester(
|
te = test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ResolvedSource: "rtmps://localhost/teststream",
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
Parent: p,
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rtmps://localhost/teststream",
|
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ func createRangeHeader(cnf *conf.Path) (*headers.Range, error) {
|
||||||
|
|
||||||
// Source is a RTSP static source.
|
// Source is a RTSP static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
|
ResolvedSource string
|
||||||
ReadTimeout conf.StringDuration
|
ReadTimeout conf.StringDuration
|
||||||
WriteTimeout conf.StringDuration
|
WriteTimeout conf.StringDuration
|
||||||
WriteQueueSize int
|
WriteQueueSize int
|
||||||
|
|
@ -103,7 +104,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := base.ParseURL(params.ResolvedSource)
|
u, err := base.ParseURL(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,13 +138,13 @@ func TestSource(t *testing.T) {
|
||||||
te = test.NewSourceTester(
|
te = test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsp://testuser:testpass@localhost:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rtsp://testuser:testpass@localhost:8555/teststream",
|
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
RTSPTransport: sp,
|
RTSPTransport: sp,
|
||||||
},
|
},
|
||||||
|
|
@ -153,13 +153,13 @@ func TestSource(t *testing.T) {
|
||||||
te = test.NewSourceTester(
|
te = test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsps://testuser:testpass@localhost:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rtsps://testuser:testpass@localhost:8555/teststream",
|
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
|
||||||
},
|
},
|
||||||
|
|
@ -241,13 +241,13 @@ func TestRTSPSourceNoPassword(t *testing.T) {
|
||||||
te := test.NewSourceTester(
|
te := test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsp://testuser:@127.0.0.1:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rtsp://testuser:@127.0.0.1:8555/teststream",
|
|
||||||
&conf.Path{
|
&conf.Path{
|
||||||
RTSPTransport: sp,
|
RTSPTransport: sp,
|
||||||
},
|
},
|
||||||
|
|
@ -338,13 +338,13 @@ func TestRTSPSourceRange(t *testing.T) {
|
||||||
te := test.NewSourceTester(
|
te := test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
|
ResolvedSource: "rtsp://127.0.0.1:8555/teststream",
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteTimeout: conf.StringDuration(10 * time.Second),
|
WriteTimeout: conf.StringDuration(10 * time.Second),
|
||||||
WriteQueueSize: 2048,
|
WriteQueueSize: 2048,
|
||||||
Parent: p,
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rtsp://127.0.0.1:8555/teststream",
|
|
||||||
cnf,
|
cnf,
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ import (
|
||||||
|
|
||||||
// Source is a SRT static source.
|
// Source is a SRT static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ReadTimeout conf.StringDuration
|
ResolvedSource string
|
||||||
Parent defs.StaticSourceParent
|
ReadTimeout conf.StringDuration
|
||||||
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
// Log implements logger.Writer.
|
||||||
|
|
@ -31,7 +32,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
conf := srt.DefaultConfig()
|
conf := srt.DefaultConfig()
|
||||||
address, err := conf.UnmarshalURL(params.ResolvedSource)
|
address, err := conf.UnmarshalURL(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSource(t *testing.T) {
|
func TestSource(t *testing.T) {
|
||||||
ln, err := srt.Listen("srt", "127.0.0.1:9002", srt.DefaultConfig())
|
ln, err := srt.Listen("srt", "localhost:9002", srt.DefaultConfig())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer ln.Close()
|
defer ln.Close()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
req, err := ln.Accept2()
|
conn, _, err := ln.Accept(func(req srt.ConnRequest) srt.ConnType {
|
||||||
require.NoError(t, err)
|
require.Equal(t, "sidname", req.StreamId())
|
||||||
|
err := req.SetPassphrase("ttest1234567")
|
||||||
require.Equal(t, "sidname", req.StreamId())
|
if err != nil {
|
||||||
err = req.SetPassphrase("ttest1234567")
|
return srt.REJECT
|
||||||
require.NoError(t, err)
|
}
|
||||||
|
return srt.SUBSCRIBE
|
||||||
conn, err := req.Accept()
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, conn)
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
track := &mpegts.Track{
|
track := &mpegts.Track{
|
||||||
|
|
@ -39,7 +40,7 @@ func TestSource(t *testing.T) {
|
||||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{{ // IDR
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR
|
||||||
5, 1,
|
5, 1,
|
||||||
}})
|
}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -54,11 +55,11 @@ func TestSource(t *testing.T) {
|
||||||
te := test.NewSourceTester(
|
te := test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ResolvedSource: "srt://localhost:9002?streamid=sidname&passphrase=ttest1234567",
|
||||||
Parent: p,
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"srt://127.0.0.1:9002?streamid=sidname&passphrase=ttest1234567",
|
|
||||||
&conf.Path{},
|
&conf.Path{},
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,9 @@ type packetConn interface {
|
||||||
|
|
||||||
// Source is a UDP static source.
|
// Source is a UDP static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ReadTimeout conf.StringDuration
|
ResolvedSource string
|
||||||
Parent defs.StaticSourceParent
|
ReadTimeout conf.StringDuration
|
||||||
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
// Log implements logger.Writer.
|
||||||
|
|
@ -58,7 +59,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
hostPort := params.ResolvedSource[len("udp://"):]
|
hostPort := s.ResolvedSource[len("udp://"):]
|
||||||
|
|
||||||
addr, err := net.ResolveUDPAddr("udp", hostPort)
|
addr, err := net.ResolveUDPAddr("udp", hostPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,18 @@ func TestSource(t *testing.T) {
|
||||||
te := test.NewSourceTester(
|
te := test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ResolvedSource: "udp://localhost:9001",
|
||||||
Parent: p,
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"udp://127.0.0.1:9001",
|
|
||||||
&conf.Path{},
|
&conf.Path{},
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
conn, err := net.Dial("udp", "127.0.0.1:9001")
|
conn, err := net.Dial("udp", "localhost:9001")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
|
@ -41,12 +41,12 @@ func TestSource(t *testing.T) {
|
||||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
w := mpegts.NewWriter(bw, []*mpegts.Track{track})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{{ // IDR
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR
|
||||||
5, 1,
|
5, 1,
|
||||||
}})
|
}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.WriteH264(track, 0, 0, true, [][]byte{{ // non-IDR
|
err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // non-IDR
|
||||||
5, 2,
|
5, 2,
|
||||||
}})
|
}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ import (
|
||||||
|
|
||||||
// Source is a WebRTC static source.
|
// Source is a WebRTC static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ReadTimeout conf.StringDuration
|
ResolvedSource string
|
||||||
Parent defs.StaticSourceParent
|
ReadTimeout conf.StringDuration
|
||||||
|
Parent defs.StaticSourceParent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
// Log implements logger.Writer.
|
||||||
|
|
@ -32,7 +33,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
s.Log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
u, err := url.Parse(params.ResolvedSource)
|
u, err := url.Parse(s.ResolvedSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,11 @@ func TestSource(t *testing.T) {
|
||||||
ChannelCount: 2,
|
ChannelCount: 2,
|
||||||
}}}
|
}}}
|
||||||
pc := &webrtc.PeerConnection{
|
pc := &webrtc.PeerConnection{
|
||||||
LocalRandomUDP: true,
|
LocalRandomUDP: true,
|
||||||
IPsFromInterfaces: true,
|
IPsFromInterfaces: true,
|
||||||
Publish: true,
|
Publish: true,
|
||||||
HandshakeTimeout: conf.StringDuration(10 * time.Second),
|
OutgoingTracks: outgoingTracks,
|
||||||
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
|
Log: test.NilLogger,
|
||||||
OutgoingTracks: outgoingTracks,
|
|
||||||
Log: test.NilLogger,
|
|
||||||
}
|
}
|
||||||
err := pc.Start()
|
err := pc.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -121,11 +119,11 @@ func TestSource(t *testing.T) {
|
||||||
te := test.NewSourceTester(
|
te := test.NewSourceTester(
|
||||||
func(p defs.StaticSourceParent) defs.StaticSource {
|
func(p defs.StaticSourceParent) defs.StaticSource {
|
||||||
return &Source{
|
return &Source{
|
||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ResolvedSource: "whep://localhost:9003/my/resource",
|
||||||
Parent: p,
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
|
Parent: p,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"whep://localhost:9003/my/resource",
|
|
||||||
&conf.Path{},
|
&conf.Path{},
|
||||||
)
|
)
|
||||||
defer te.Close()
|
defer te.Close()
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,7 @@ type SourceTester struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSourceTester allocates a SourceTester.
|
// NewSourceTester allocates a SourceTester.
|
||||||
func NewSourceTester(
|
func NewSourceTester(createFunc func(defs.StaticSourceParent) defs.StaticSource, conf *conf.Path) *SourceTester {
|
||||||
createFunc func(defs.StaticSourceParent) defs.StaticSource,
|
|
||||||
resolvedSource string,
|
|
||||||
conf *conf.Path,
|
|
||||||
) *SourceTester {
|
|
||||||
ctx, ctxCancel := context.WithCancel(context.Background())
|
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
t := &SourceTester{
|
t := &SourceTester{
|
||||||
|
|
@ -42,9 +38,8 @@ func NewSourceTester(
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s.Run(defs.StaticSourceRunParams{ //nolint:errcheck
|
s.Run(defs.StaticSourceRunParams{ //nolint:errcheck
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
ResolvedSource: resolvedSource,
|
Conf: conf,
|
||||||
Conf: conf,
|
|
||||||
})
|
})
|
||||||
close(t.done)
|
close(t.done)
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
13
mediamtx.yml
13
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
|
# This is the JWKS URL that will be used to pull (once) the public key that allows
|
||||||
# to validate JWTs.
|
# to validate JWTs.
|
||||||
authJWTJWKS:
|
authJWTJWKS:
|
||||||
|
|
@ -381,10 +381,6 @@ webrtcICEServers2: []
|
||||||
# username: ''
|
# username: ''
|
||||||
# password: ''
|
# password: ''
|
||||||
# clientOnly: false
|
# clientOnly: false
|
||||||
# Time to wait for the WebRTC handshake to complete.
|
|
||||||
webrtcHandshakeTimeout: 10s
|
|
||||||
# Maximum time to gather video tracks.
|
|
||||||
webrtcTrackGatherTimeout: 2s
|
|
||||||
|
|
||||||
###############################################
|
###############################################
|
||||||
# Global settings -> SRT server
|
# Global settings -> SRT server
|
||||||
|
|
@ -418,10 +414,8 @@ pathDefaults:
|
||||||
# * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS
|
# * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS
|
||||||
# * redirect -> the stream is provided by another path or server
|
# * redirect -> the stream is provided by another path or server
|
||||||
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera
|
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera
|
||||||
# The following variables can be used in the source string:
|
# If path name is a regular expression, $G1, G2, etc will be replaced
|
||||||
# * $MTX_QUERY: query parameters (passed by first reader)
|
# with regular expression groups.
|
||||||
# * $G1, $G2, ...: regular expression groups, if path name is
|
|
||||||
# a regular expression.
|
|
||||||
source: publisher
|
source: publisher
|
||||||
# If the source is a URL, and the source certificate is self-signed
|
# If the source is a URL, and the source certificate is self-signed
|
||||||
# or invalid, you can provide the fingerprint of the certificate in order to
|
# or invalid, you can provide the fingerprint of the certificate in order to
|
||||||
|
|
@ -677,7 +671,6 @@ pathDefaults:
|
||||||
# * G1, G2, ...: regular expression groups, if path name is
|
# * G1, G2, ...: regular expression groups, if path name is
|
||||||
# a regular expression.
|
# a regular expression.
|
||||||
# * MTX_SEGMENT_PATH: segment file path
|
# * MTX_SEGMENT_PATH: segment file path
|
||||||
# * MTX_SEGMENT_DURATION: segment duration
|
|
||||||
runOnRecordSegmentComplete:
|
runOnRecordSegmentComplete:
|
||||||
|
|
||||||
###############################################
|
###############################################
|
||||||
|
|
|
||||||
|
|
@ -29,36 +29,36 @@ RUN cp mediamtx.yml LICENSE tmp/
|
||||||
RUN go generate ./...
|
RUN go generate ./...
|
||||||
|
|
||||||
FROM build-base AS build-windows-amd64
|
FROM build-base AS build-windows-amd64
|
||||||
RUN GOOS=windows GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME).exe
|
RUN GOOS=windows GOARCH=amd64 go build -o tmp/$(BINARY_NAME).exe
|
||||||
RUN cd tmp && zip -q ../binaries/$(BINARY_NAME)_$${VERSION}_windows_amd64.zip $(BINARY_NAME).exe mediamtx.yml LICENSE
|
RUN cd tmp && zip -q ../binaries/$(BINARY_NAME)_$${VERSION}_windows_amd64.zip $(BINARY_NAME).exe mediamtx.yml LICENSE
|
||||||
|
|
||||||
FROM build-base AS build-linux-amd64
|
FROM build-base AS build-linux-amd64
|
||||||
RUN GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME)
|
RUN GOOS=linux GOARCH=amd64 go build -o tmp/$(BINARY_NAME)
|
||||||
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
||||||
|
|
||||||
FROM build-base AS build-darwin-amd64
|
FROM build-base AS build-darwin-amd64
|
||||||
RUN GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME)
|
RUN GOOS=darwin GOARCH=amd64 go build -o tmp/$(BINARY_NAME)
|
||||||
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
||||||
|
|
||||||
FROM build-base AS build-darwin-arm64
|
FROM build-base AS build-darwin-arm64
|
||||||
RUN GOOS=darwin GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME)
|
RUN GOOS=darwin GOARCH=arm64 go build -o tmp/$(BINARY_NAME)
|
||||||
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_arm64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_arm64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
||||||
|
|
||||||
FROM build-base AS build-linux-armv6
|
FROM build-base AS build-linux-armv6
|
||||||
COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
|
COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
|
||||||
RUN GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera
|
RUN GOOS=linux GOARCH=arm GOARM=6 go build -o tmp/$(BINARY_NAME) -tags rpicamera
|
||||||
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv6.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv6.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
||||||
RUN rm internal/protocols/rpicamera/exe/exe
|
RUN rm internal/protocols/rpicamera/exe/exe
|
||||||
|
|
||||||
FROM build-base AS build-linux-armv7
|
FROM build-base AS build-linux-armv7
|
||||||
COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
|
COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
|
||||||
RUN GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera
|
RUN GOOS=linux GOARCH=arm GOARM=7 go build -o tmp/$(BINARY_NAME) -tags rpicamera
|
||||||
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv7.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv7.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
||||||
RUN rm internal/protocols/rpicamera/exe/exe
|
RUN rm internal/protocols/rpicamera/exe/exe
|
||||||
|
|
||||||
FROM build-base AS build-linux-arm64
|
FROM build-base AS build-linux-arm64
|
||||||
COPY --from=rpicamera64 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
|
COPY --from=rpicamera64 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/
|
||||||
RUN GOOS=linux GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera
|
RUN GOOS=linux GOARCH=arm64 go build -o tmp/$(BINARY_NAME) -tags rpicamera
|
||||||
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_arm64v8.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_arm64v8.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE
|
||||||
RUN rm internal/protocols/rpicamera/exe/exe
|
RUN rm internal/protocols/rpicamera/exe/exe
|
||||||
|
|
||||||
|
|
@ -75,7 +75,6 @@ export DOCKERFILE_BINARIES
|
||||||
|
|
||||||
binaries:
|
binaries:
|
||||||
echo "$$DOCKERFILE_BINARIES" | DOCKER_BUILDKIT=1 docker build . -f - \
|
echo "$$DOCKERFILE_BINARIES" | DOCKER_BUILDKIT=1 docker build . -f - \
|
||||||
--build-arg VERSION=$$(git describe --tags) \
|
|
||||||
-t temp
|
-t temp
|
||||||
docker run --rm -v $(PWD):/out \
|
docker run --rm -v $(PWD):/out \
|
||||||
temp sh -c "rm -rf /out/binaries && cp -r /s/binaries /out/"
|
temp sh -c "rm -rf /out/binaries && cp -r /s/binaries /out/"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
lint:
|
lint:
|
||||||
touch internal/servers/hls/hls.min.js
|
go generate ./...
|
||||||
docker run --rm -v $(PWD):/app -w /app \
|
docker run --rm -v $(PWD):/app -w /app \
|
||||||
$(LINT_IMAGE) \
|
$(LINT_IMAGE) \
|
||||||
golangci-lint run -v
|
golangci-lint run -v
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue