1
0
Fork 0
forked from External/mediamtx

Compare commits

..

50 commits

Author SHA1 Message Date
6934441a5e add initial dockerfile 2024-07-24 18:04:08 -07:00
09ad8e7160 import changes 2024-07-24 16:52:58 -07:00
dependabot[bot]
da5420a788
build(deps): bump github.com/datarhei/gosrt (#3559)
Bumps [github.com/datarhei/gosrt](https://github.com/datarhei/gosrt) from 0.6.1-0.20240708145230-390712a1b3f7 to 0.7.0.
- [Commits](https://github.com/datarhei/gosrt/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/datarhei/gosrt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-17 21:29:04 +02:00
Alessandro Ros
3152388c1f
fix gosrt import (#3549) 2024-07-12 13:56:44 +02:00
Alessandro Ros
ed15f9dde5
deps: restore link to datarhei/gosrt (#3546) 2024-07-11 13:33:03 +02:00
github-actions[bot]
976f2252a5
bump hls.js to v1.5.13 (#3540)
Co-authored-by: mediamtx-bot <bot@mediamtx>
2024-07-08 18:08:49 +02:00
Alessandro Ros
c3265a554b
fix hls.js update workflow (#3538) 2024-07-08 18:06:51 +02:00
Alessandro Ros
1a4fd9cfca
bump dependencies (#3537) 2024-07-07 19:51:43 +02:00
Alessandro Ros
3f1d182d2c
fix support for HTTP preflight requests (#1792) (#3535) 2024-07-06 21:45:15 +02:00
Alessandro Ros
342c257df5
srt: process connection requests in parallel (#3382) (#3534) 2024-07-05 22:17:40 +02:00
dependabot[bot]
c4987d020a
build(deps): bump golang.org/x/term from 0.21.0 to 0.22.0 (#3533)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/term/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 19:29:20 +02:00
dependabot[bot]
3c2d103234
build(deps): bump golang.org/x/crypto from 0.24.0 to 0.25.0 (#3532)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/crypto/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 19:28:57 +02:00
dependabot[bot]
c2d60e5a9b
build(deps): bump golang.org/x/sys from 0.21.0 to 0.22.0 (#3530) 2024-07-04 20:07:59 +02:00
Alessandro Ros
afec0cd86c
rtmp: fix publishing from DJI FlightHub Sync (#3301) (#3504) 2024-07-01 16:03:12 +02:00
Alessandro Ros
a1dc9f45f5
webrtc: support publishing H265 tracks (#3435) (#3492)
IMPORTANT NOTE: this doesn't allow to read H265 tracks with WebRTC,
just to publish them. The inability to read H265 tracks with WebRTC is
not in any way related to the server but depends on browsers and on the
fact that they are not legally entitled to embed a H265 decoder inside
them.
2024-06-19 21:02:08 +02:00
Alessandro Ros
65d90f7cc6
allow using MTX_QUERY inside source (#3486)
this allows to pass query parameters to sources, for instance:

source: rtsp://my_host/my_path?$MTX_QUERY
sourceOnDemand: true
2024-06-18 22:10:26 +02:00
Bouke van der Bijl
dfa2e81e61
Save a hash of the hls.min.js release (#3464)
This ensures the downloaded hls.js matches exactly and removes a dependency on cdn.jsdelivr.net
2024-06-17 23:15:18 +02:00
dependabot[bot]
7dcfd2e46d
build(deps): bump github.com/gorilla/websocket from 1.5.2 to 1.5.3 (#3470)
Bumps [github.com/gorilla/websocket](https://github.com/gorilla/websocket) from 1.5.2 to 1.5.3.
- [Release notes](https://github.com/gorilla/websocket/releases)
- [Commits](https://github.com/gorilla/websocket/compare/v1.5.2...v1.5.3)

---
updated-dependencies:
- dependency-name: github.com/gorilla/websocket
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 23:12:10 +02:00
Alessandro Ros
f227971517
fix UDP-related tests (#3484)
for some reason, using "localhost" with UDP inside GitHub Actions and
Docker stopped working.
2024-06-17 23:09:25 +02:00
Alessandro Ros
9554fc4ba0
prevent mixing together legacy and current auth mechanism (#3258) (#3460) 2024-06-12 17:38:55 +02:00
Alessandro Ros
39ae1269ad
webrtc: support passing username and password through Bearer Token (#3248) (#3459) 2024-06-11 23:37:59 +02:00
Alessandro Ros
caa9fa6ca0
webrtc, hls: support passing JWT through Authorization header (#3248) (#3458) 2024-06-11 23:33:01 +02:00
Alessandro Ros
80a133afc9
bump dependencies (#3457) 2024-06-11 22:43:27 +02:00
Alessandro Ros
3eabe6ac54
expose MTX_SEGMENT_DURATION to runOnRecordSegmentComplete (#3440) (#2983) (#3456)
* improve tests

* add duration to OnSegmentComplete

* expose MTX_SEGMENT_DURATION to runOnRecordSegmentComplete

* add tests
2024-06-11 18:30:40 +02:00
Jacob Su
3a2594d610
rtmp: fix error when publishing AAC audio tracks (#3414)
* fix single aac file rtmp publish error.

* add tests

---------

Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com>
2024-06-11 13:26:45 +02:00
dependabot[bot]
e996ae5a21
build(deps): bump github.com/gorilla/websocket from 1.5.1 to 1.5.2 (#3450) 2024-06-10 20:01:34 +02:00
Alessandro Ros
427249877c
webrtc: fix error "Failed to setup RTCP mux" on some readers (#3381) (#3449) 2024-06-10 15:43:52 +02:00
Alessandro Ros
095921dc26
webrtc: on browsers, display error messages from server (#3448) 2024-06-10 15:41:05 +02:00
Alessandro Ros
1911294539
bump pion/webrtc (#3447) 2024-06-10 12:51:24 +02:00
Alessandro Ros
d0c8e89223
fix webrtc/G722 tests (#3444) 2024-06-10 10:15:25 +02:00
Alessandro Ros
5fe2819546
webrtc: set fmtp of outgoing VP9 and multiopus tracks (#3446) 2024-06-10 09:54:08 +02:00
Alessandro Ros
511b276b4d
webrtc: support reading G711 16khz tracks (#2848) (#3445) 2024-06-10 00:57:26 +02:00
Alessandro Ros
427fea30ed
fix webrtc/VP9 tests (#3443) 2024-06-09 23:18:47 +02:00
Alessandro Ros
44953c8e05
webrtc: fix supported AV1 profiles (#3442) 2024-06-09 23:09:55 +02:00
Alessandro Ros
d7bc304e52
webrtc: speed up gathering of incoming tracks (#3441) 2024-06-09 22:58:40 +02:00
Alessandro Ros
eaf47e6598
webrtc: support reading, publishing, proxying LPCM tracks (#3437) 2024-06-09 22:51:16 +02:00
Alessandro Ros
bf8b68d757
bump pion/webrtc and pion/ice (#3436) 2024-06-08 23:38:10 +02:00
Alessandro Ros
16d0bb7200
webrtc: fix proxying PCMU tracks (#3427) 2024-06-05 12:46:55 +02:00
github-actions[bot]
b9e9b08759
bump hls.js to v1.5.11 (#3426) 2024-06-05 09:00:22 +02:00
Alessandro Ros
efb51044db
api: fix crash when itemsPerPage is zero and there are items (#3425) 2024-06-04 23:09:50 +02:00
dependabot[bot]
011ea47142
build(deps): bump golang.org/x/crypto from 0.23.0 to 0.24.0 (#3423)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 22:26:37 +02:00
dependabot[bot]
ca5529e10a
build(deps): bump golang.org/x/term from 0.20.0 to 0.21.0 (#3424)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/term/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 22:16:05 +02:00
github-actions[bot]
8bd717cf96
bump hls.js to v1.5.10 (#3421) 2024-06-04 10:27:05 +02:00
Alessandro Ros
ca1638976b
webrtc: support reading and publishing multichannel Opus (#3371) (#3375) 2024-06-03 00:26:32 +02:00
Alessandro Ros
c37e8953fa
webrtc: show error when setLocalDescription or createOffer fail (#3417) 2024-06-02 23:19:58 +02:00
Alessandro Ros
ca6e1259fb
webrtc: support reading and proxying stereo PCMU/PCMA tracks (#3402) 2024-06-02 23:08:54 +02:00
Alessandro Ros
6da8aee64f
webrtc: show error in case setRemoteDescription fails (#3416) 2024-06-02 22:14:36 +02:00
Alessandro Ros
f4b7f147a5
api, playback: add CORS headers on non-existing pages too (#1792) (#3410) 2024-05-30 14:29:05 +02:00
Jason Walton
1c2f95f609
webrtc: allow configuring timeouts (#3404) (#3406)
* webrtc: allow configuring timeouts (#3404)

* fix from code inspect
2024-05-30 13:36:58 +02:00
github-actions[bot]
500c5ef350
bump hls.js to v1.5.9 (#3407)
Co-authored-by: mediamtx-bot <bot@mediamtx>
2024-05-30 10:35:03 +02:00
88 changed files with 3334 additions and 1069 deletions

View file

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

View file

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

View file

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

View file

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

1
.gitignore vendored
View file

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

17
Dockerfile Normal file
View file

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

View file

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

View file

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

32
go.mod
View file

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

93
go.sum
View file

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

View file

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

View file

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

63
internal/api/paginate.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
c5ef2cf356b103bf7a19dd4d14257c9e00163551ed03bbf96bf22a12458a1250

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,24 +27,11 @@ func (l *listener) run() {
func (l *listener) runInner() error {
for {
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
})
req, err := l.ln.Accept2()
if err != nil {
return err
}
if conn == nil {
continue
}
sconn.setConn(conn)
l.parent.newConnRequest(req)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,14 +3,16 @@ package webrtc
import (
"bytes"
"context"
"io"
"net/http"
"net/url"
"reflect"
"testing"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
@ -24,10 +26,6 @@ import (
"github.com/stretchr/testify/require"
)
func uint16Ptr(v uint16) *uint16 {
return &v
}
func checkClose(t *testing.T, closeFunc func() error) {
require.NoError(t, closeFunc())
}
@ -74,40 +72,38 @@ func (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {
}
type dummyPathManager struct {
path *dummyPath
findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error)
addPublisher func(req defs.PathAddPublisherReq) (defs.Path, error)
addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
}
func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, auth.Error{}
}
return &conf.Path{}, nil
return pm.findPathConf(req)
}
func (pm *dummyPathManager) AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, error) {
return pm.path, nil
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
return pm.addPublisher(req)
}
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
if req.AccessRequest.Name == "nonexisting" {
return nil, nil, defs.PathNoOnePublishingError{}
}
return pm.path, pm.path.stream, nil
return pm.addReader(req)
}
func initializeTestServer(t *testing.T) *Server {
path := &dummyPath{
streamCreated: make(chan struct{}),
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
},
}
pathManager := &dummyPathManager{path: path}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
AllowOrigin: "*",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
WriteQueueSize: 512,
@ -117,8 +113,10 @@ func initializeTestServer(t *testing.T) *Server {
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
PathManager: pm,
Parent: test.NilLogger,
}
err := s.Initialize()
@ -149,7 +147,7 @@ func TestServerStaticPages(t *testing.T) {
}
}
func TestServerOptionsPreflight(t *testing.T) {
func TestPreflightRequest(t *testing.T) {
s := initializeTestServer(t)
defer s.Close()
@ -157,11 +155,10 @@ func TestServerOptionsPreflight(t *testing.T) {
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
// preflight requests must always work, without authentication
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886/teststream/whip", nil)
req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886", nil)
require.NoError(t, err)
req.Header.Set("Access-Control-Request-Method", "OPTIONS")
req.Header.Add("Access-Control-Request-Method", "GET")
res, err := hc.Do(req)
require.NoError(t, err)
@ -169,12 +166,24 @@ func TestServerOptionsPreflight(t *testing.T) {
require.Equal(t, http.StatusNoContent, res.StatusCode)
_, ok := res.Header["Link"]
require.Equal(t, false, ok)
byts, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods"))
require.Equal(t, "Authorization, Content-Type, If-Match", res.Header.Get("Access-Control-Allow-Headers"))
require.Equal(t, byts, []byte{})
}
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{
Address: "127.0.0.1:8886",
@ -195,9 +204,11 @@ func TestServerOptionsICEServer(t *testing.T) {
Username: "myuser",
Password: "mypass",
}},
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
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)
@ -232,7 +243,20 @@ func TestServerPublish(t *testing.T) {
streamCreated: make(chan struct{}),
}
pathManager := &dummyPathManager{path: path}
pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
@ -249,6 +273,8 @@ func TestServerPublish(t *testing.T) {
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
@ -330,9 +356,263 @@ func TestServerPublish(t *testing.T) {
}
func TestServerRead(t *testing.T) {
for _, ca := range []struct {
name string
medias []*description.Media
unit unit.Unit
outRTPPayload []byte
}{
{
"av1",
[]*description.Media{{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.AV1{
PayloadTyp: 96,
}},
}},
&unit.AV1{
TU: [][]byte{{1, 2}},
},
[]byte{0, 2, 1, 2},
},
{
"vp9",
[]*description.Media{{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP9{
PayloadTyp: 96,
}},
}},
&unit.VP9{
Frame: []byte{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34},
},
[]byte{
0x8f, 0xa0, 0xfd, 0x18, 0x07, 0x80, 0x03, 0x24,
0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00,
0x77, 0xf0, 0x32, 0x34,
},
},
{
"vp8",
[]*description.Media{{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP8{
PayloadTyp: 96,
}},
}},
&unit.VP8{
Frame: []byte{1, 2},
},
[]byte{0x10, 1, 2},
},
{
"h264",
[]*description.Media{test.MediaH264},
&unit.H264{
AU: [][]byte{
{5, 1},
},
},
[]byte{
0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,
0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,
0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,
0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,
0x07, 0x08, 0x00, 0x02, 0x05, 0x01,
},
},
{
"opus",
[]*description.Media{{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
}},
}},
&unit.Opus{
Packets: [][]byte{{1, 2}},
},
[]byte{1, 2},
},
{
"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) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
stream, err := stream.New(
str, err := stream.New(
1460,
desc,
true,
@ -340,9 +620,18 @@ func TestServerRead(t *testing.T) {
)
require.NoError(t, err)
path := &dummyPath{stream: stream}
path := &dummyPath{stream: str}
pathManager := &dummyPathManager{path: path}
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",
@ -359,80 +648,158 @@ func TestServerRead(t *testing.T) {
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
PathManager: pm,
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")
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}
wc := &webrtc.WHIPClient{
HTTPClient: hc,
URL: u,
Log: test.NilLogger,
}
writerDone := make(chan struct{})
defer func() { <-writerDone }()
writerTerminate := make(chan struct{})
defer close(writerTerminate)
go func() {
defer close(writerDone)
for {
select {
case <-time.After(100 * time.Millisecond):
case <-writerTerminate:
return
}
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
NTP: time.Time{},
},
AU: [][]byte{
{5, 1},
},
})
}
}()
tracks, err := wc.Read(context.Background())
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer checkClose(t, wc.Close)
defer pc.Close() //nolint:errcheck
pkt, err := tracks[0].ReadRTP()
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
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{
0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,
0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,
0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,
0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,
0x07, 0x08, 0x00, 0x02, 0x05, 0x01,
},
}, pkt)
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 TestServerPostNotFound(t *testing.T) {
s := initializeTestServer(t)
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()
tr := &http.Transport{}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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