diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index 93c2d2ac62f..9c49fd22306 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -63,7 +63,7 @@ yes / no
+
## Describe the feature
Description
diff --git a/.github/workflows/bump-hls-js.yml b/.github/workflows/bump_hls_js.yml
similarity index 84%
rename from .github/workflows/bump-hls-js.yml
rename to .github/workflows/bump_hls_js.yml
index 69c4443289a..cd78035b05a 100644
--- a/.github/workflows/bump-hls-js.yml
+++ b/.github/workflows/bump_hls_js.yml
@@ -1,4 +1,4 @@
-name: bump-hls-js
+name: bump_hls_js
on:
schedule:
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
jobs:
- bump-hls-js:
+ bump_hls_js:
runs-on: ubuntu-20.04
steps:
@@ -20,8 +20,8 @@ jobs:
&& ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs)
- run: >
- curl -o internal/core/hls.min.js https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js
- && echo VERSION=$(cat internal/core/hls.min.js | grep -o '"version",get:function(){return".*"}' | sed 's/"version",get:function(){return"\(.*\)"}/\1/') >> $GITHUB_ENV
+ curl -o internal/servers/hls/hls.min.js https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js
+ && echo VERSION=$(cat internal/servers/hls/hls.min.js | grep -o '"version",get:function(){return".*"}' | sed 's/"version",get:function(){return"\(.*\)"}/\1/') >> $GITHUB_ENV
- id: check_repo
run: >
diff --git a/.github/workflows/lint.yml b/.github/workflows/code_lint.yml
similarity index 83%
rename from .github/workflows/lint.yml
rename to .github/workflows/code_lint.yml
index 5f7f364f6dd..e21c9a719b3 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/code_lint.yml
@@ -1,4 +1,4 @@
-name: lint
+name: code_lint
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main ]
jobs:
- code:
+ golangci_lint:
runs-on: ubuntu-22.04
steps:
@@ -19,9 +19,9 @@ jobs:
- uses: golangci/golangci-lint-action@v3
with:
- version: v1.53.3
+ version: v1.55.0
- mod-tidy:
+ mod_tidy:
runs-on: ubuntu-22.04
steps:
@@ -29,13 +29,13 @@ jobs:
- uses: actions/setup-go@v2
with:
- go-version: "1.20"
+ go-version: "1.21"
- run: |
go mod tidy
git diff --exit-code
- apidocs:
+ api_docs:
runs-on: ubuntu-22.04
steps:
diff --git a/.github/workflows/test.yml b/.github/workflows/code_test.yml
similarity index 89%
rename from .github/workflows/test.yml
rename to .github/workflows/code_test.yml
index fd8dff8c4f1..de222864941 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/code_test.yml
@@ -1,4 +1,4 @@
-name: test
+name: code_test
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main ]
jobs:
- test64:
+ test_64:
runs-on: ubuntu-22.04
steps:
@@ -19,7 +19,7 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
- test32:
+ test_32:
runs-on: ubuntu-22.04
steps:
@@ -35,6 +35,6 @@ jobs:
- uses: actions/setup-go@v2
with:
- go-version: "1.20"
+ go-version: "1.21"
- run: make test-highlevel-nodocker
diff --git a/.github/workflows/issue-lint.yml b/.github/workflows/issue_lint.yml
similarity index 97%
rename from .github/workflows/issue-lint.yml
rename to .github/workflows/issue_lint.yml
index b3dd6effb43..4a2185cc189 100644
--- a/.github/workflows/issue-lint.yml
+++ b/.github/workflows/issue_lint.yml
@@ -1,11 +1,11 @@
-name: issue-lint
+name: issue_lint
on:
issues:
types: [opened]
jobs:
- issue-lint:
+ issue_lint:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/issue-lock.yml b/.github/workflows/issue_lock.yml
similarity index 97%
rename from .github/workflows/issue-lock.yml
rename to .github/workflows/issue_lock.yml
index 6d370542cb9..a9e9f208c4e 100644
--- a/.github/workflows/issue-lock.yml
+++ b/.github/workflows/issue_lock.yml
@@ -1,4 +1,4 @@
-name: issue-lock
+name: issue_lock
on:
schedule:
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
jobs:
- issue-lock:
+ issue_lock:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/binaries.yml b/.github/workflows/nightly_binaries.yml
similarity index 84%
rename from .github/workflows/binaries.yml
rename to .github/workflows/nightly_binaries.yml
index 14fe6821a72..419a8dc071c 100644
--- a/.github/workflows/binaries.yml
+++ b/.github/workflows/nightly_binaries.yml
@@ -1,10 +1,10 @@
-name: binaries
+name: nightly_binaries
on:
workflow_dispatch:
jobs:
- binaries:
+ nightly_binaries:
runs-on: ubuntu-22.04
steps:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 32914f9e7a7..99639ceb757 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,13 +19,11 @@ jobs:
name: binaries
path: binaries
- github:
+ github_release:
needs: binaries
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
-
- uses: actions/download-artifact@v3
with:
name: binaries
@@ -58,6 +56,51 @@ jobs:
});
}
+ github_notify_issues:
+ needs: github_release
+ runs-on: ubuntu-22.04
+
+ steps:
+ - uses: actions/github-script@v6
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { repo: { owner, repo } } = context;
+
+ const tags = await github.rest.repos.listTags({
+ owner,
+ repo,
+ });
+
+ const curTag = tags.data[0];
+ const prevTag = tags.data[1];
+
+ const diff = await github.rest.repos.compareCommitsWithBasehead({
+ owner,
+ repo,
+ basehead: `${prevTag.commit.sha}...${curTag.commit.sha}`,
+ });
+
+ const issues = {};
+
+ for (const commit of diff.data.commits) {
+ for (const match of commit.commit.message.matchAll(/(^| |\()#([0-9]+)( |\)|$)/g)) {
+ issues[match[2]] = 1;
+ }
+ }
+
+ for (const issue in issues) {
+ try {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: parseInt(issue),
+ body: `This issue is mentioned in release ${curTag.name} 🚀\n`
+ + `Check out the entire changelog by [clicking here](https://github.com/${owner}/${repo}/releases/tag/${curTag.name})`,
+ });
+ } catch (exc) {}
+ }
+
dockerhub:
needs: binaries
runs-on: ubuntu-22.04
@@ -87,7 +130,7 @@ jobs:
DOCKER_USER_LEGACY: ${{ secrets.DOCKER_USER_LEGACY }}
DOCKER_PASSWORD_LEGACY: ${{ secrets.DOCKER_PASSWORD_LEGACY }}
- apidocs:
+ api_docs:
needs: binaries
runs-on: ubuntu-22.04
diff --git a/.golangci.yml b/.golangci.yml
index 593d6b20fe5..d2c93841a05 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,7 +1,10 @@
linters:
enable:
+ - asciicheck
+ - bidichk
- bodyclose
- dupl
+ - errorlint
- exportloopref
- gochecknoinits
- gocritic
@@ -13,6 +16,7 @@ linters:
- prealloc
- revive
- unconvert
+ - tparallel
- wastedassign
- whitespace
diff --git a/Makefile b/Makefile
index 1b3d08a8587..137eb9232c2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
-BASE_IMAGE = golang:1.20-alpine3.18
-LINT_IMAGE = golangci/golangci-lint:v1.53.3
+BASE_IMAGE = golang:1.21-alpine3.18
+LINT_IMAGE = golangci/golangci-lint:v1.55.2
NODE_IMAGE = node:16-alpine3.18
ALPINE_IMAGE = alpine:3.18
RPI32_IMAGE = balenalib/raspberry-pi:bullseye-run-20230712
diff --git a/README.md b/README.md
index d4cf6bd1c13..a630cccb55a 100644
--- a/README.md
+++ b/README.md
@@ -22,11 +22,11 @@ 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|
-|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H264|Opus, G722, G711|
-|[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, 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, 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)|
+|[WebRTC clients](#webrtc-clients)|Browser-based, WHIP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)|
+|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)|
+|[RTSP clients](#rtsp-clients)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
+|[RTSP cameras and servers](#rtsp-cameras-and-servers)|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec|
+|[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM|
|[RTMP cameras and servers](#rtmp-cameras-and-servers)|RTMP, RTMPS, Enhanced RTMP|H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3)|
|[HLS cameras and servers](#hls-cameras-and-servers)|Low-Latency HLS, MP4-based HLS, legacy HLS|AV1, VP9, H265, H264|Opus, MPEG-4 Audio (AAC)|
|[UDP/MPEG-TS](#udpmpeg-ts)|Unicast, broadcast, multicast|H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
@@ -37,11 +37,18 @@ And can be read from the server with:
|protocol|variants|video codecs|audio codecs|
|--------|--------|------------|------------|
|[SRT](#srt)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
-|[WebRTC](#webrtc)|Browser-based, WHEP|AV1, VP9, VP8, H264|Opus, G722, G711|
-|[RTSP](#rtsp)|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, LPCM and any RTP-compatible codec|
+|[WebRTC](#webrtc)|Browser-based, WHEP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)|
+|[RTSP](#rtsp)|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](#rtmp)|RTMP, RTMPS, Enhanced RTMP|H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3)|
|[HLS](#hls)|Low-Latency HLS, MP4-based HLS, legacy HLS|AV1, VP9, H265, H264|Opus, MPEG-4 Audio (AAC)|
+And can be recorded with:
+
+|format|video codecs|audio codecs|
+|------|------------|------------|
+|[fMP4](#record-streams-to-disk)|AV1, VP9, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM|
+|[MPEG-TS](#record-streams-to-disk)|H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
+
**Features**
* Publish live streams to the server
@@ -49,6 +56,7 @@ And can be read from the server with:
* Streams are automatically converted from a protocol to another
* Serve multiple streams at once in separate paths
* Record streams to disk
+* Playback recordings
* Authenticate users; use internal or external authentication
* Redirect readers to other RTSP servers (load balancing)
* Query and control the server through the API
@@ -67,7 +75,7 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
* [Standalone binary](#standalone-binary)
* [Docker image](#docker-image)
* [Arch Linux package](#arch-linux-package)
- * [OpenWRT package](#openwrt-package)
+ * [OpenWrt binary](#openwrt-binary)
* [Basic usage](#basic-usage)
* [Publish to the server](#publish-to-the-server)
* [By software](#by-software)
@@ -108,32 +116,44 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
* [Encrypt the configuration](#encrypt-the-configuration)
* [Remuxing, re-encoding, compression](#remuxing-re-encoding-compression)
* [Record streams to disk](#record-streams-to-disk)
- * [Forward streams to another server](#forward-streams-to-another-server)
+ * [Playback recordings](#playback-recordings)
+ * [Forward streams to other servers](#forward-streams-to-other-servers)
+ * [Proxy requests to other servers](#proxy-requests-to-other-servers)
* [On-demand publishing](#on-demand-publishing)
* [Start on boot](#start-on-boot)
+ * [Linux](#linux)
+ * [OpenWrt](#openwrt)
+ * [Windows](#windows)
* [Hooks](#hooks)
* [API](#api)
* [Metrics](#metrics)
* [pprof](#pprof)
+ * [SRT-specific features](#srt-specific-features)
+ * [Standard stream ID syntax](#standard-stream-id-syntax)
+ * [WebRTC-specific features](#webrtc-specific-features)
+ * [Connectivity issues](#connectivity-issues)
* [RTSP-specific features](#rtsp-specific-features)
* [Transport protocols](#transport-protocols)
* [Encryption](#encryption)
* [Corrupted frames](#corrupted-frames)
* [RTMP-specific features](#rtmp-specific-features)
* [Encryption](#encryption-1)
- * [WebRTC-specific features](#webrtc-specific-features)
- * [Connectivity issues](#connectivity-issues)
* [Compile from source](#compile-from-source)
+ * [Standard](#standard)
+ * [Raspberry Pi](#raspberry-pi)
+ * [OpenWrt](#openwrt-1)
+ * [Cross compile](#cross-compile)
+ * [Compile for all supported platforms](#compile-for-all-supported-platforms)
* [Specifications](#specifications)
* [Related projects](#related-projects)
## Installation
-There are several installation methods available: standalone binary, Docker image, Arch Linux package and OpenWRT package.
+There are several installation methods available: standalone binary, Docker image, Arch Linux package and OpenWrt binary.
### Standalone binary
-1. Download and extract a standalone binary from the [release page](https://github.com/bluenviron/mediamtx/releases).
+1. Download and extract a standalone binary from the [release page](https://github.com/bluenviron/mediamtx/releases) that corresponds to your operating system and architecture.
2. Start the server:
@@ -163,14 +183,18 @@ The `--network=host` flag is mandatory since Docker can change the source port o
```
docker run --rm -it \
-e MTX_PROTOCOLS=tcp \
+-e MTX_WEBRTCADDITIONALHOSTS=192.168.x.x \
-p 8554:8554 \
-p 1935:1935 \
-p 8888:8888 \
-p 8889:8889 \
-p 8890:8890/udp \
+-p 8189:8189/udp \
bluenviron/mediamtx
```
+set `MTX_WEBRTCADDITIONALHOSTS` to your local IP address.
+
### Arch Linux package
If you are running the Arch Linux distribution, run:
@@ -181,39 +205,11 @@ cd mediamtx
makepkg -si
```
-### OpenWRT package
+### OpenWrt binary
-1. In a x86 Linux system, download the OpenWRT SDK corresponding to the wanted OpenWRT version and target from the [OpenWRT website](https://downloads.openwrt.org/releases/) and extract it.
+If the architecture of the OpenWrt device is amd64, armv6, armv7 or arm64, use the [standalone binary method](#standalone-binary) and download a Linux binary that corresponds to your architecture.
-2. Open a terminal in the SDK folder and setup the SDK:
-
- ```sh
- ./scripts/feeds update -a
- ./scripts/feeds install -a
- make defconfig
- ```
-
-3. Download the server Makefile and set the server version inside the file:
-
- ```sh
- mkdir package/mediamtx
- wget -O package/mediamtx/Makefile https://raw.githubusercontent.com/bluenviron/mediamtx/main/openwrt.mk
- sed -i "s/v0.0.0/$(git ls-remote --tags --sort=v:refname https://github.com/bluenviron/mediamtx | tail -n1 | sed 's/.*\///; s/\^{}//')/" package/mediamtx/Makefile
- ```
-
-4. Compile the server:
-
- ```sh
- make package/mediamtx/compile -j$(nproc)
- ```
-
-5. Transfer the .ipk file from `bin/packages/*/base` to the OpenWRT system
-
-6. Install it with:
-
- ```sh
- opkg install [ipk-file-name].ipk
- ```
+Otherwise, [compile the server from source](#openwrt-1).
## Basic usage
@@ -424,7 +420,7 @@ This web page can be embedded into another web page by using an iframe:
```
-For more advanced setups, you can create and serve a custom web page by starting from the [source code of the publish page](internal/core/webrtc_publish_index.html).
+For more advanced setups, you can create and serve a custom web page by starting from the [source code of the publish page](internal/servers/webrtc/publish_index.html).
### By device
@@ -495,6 +491,8 @@ docker run --rm -it \
bluenviron/mediamtx:latest-rpi
```
+Be aware that the Docker image is not compatible with cameras that requires a custom `libcamera` (like some ArduCam products), since it comes with a standard `libcamera` included.
+
Camera settings can be changed by using the `rpiCamera*` parameters:
```yml
@@ -565,6 +563,8 @@ If credentials are enabled, append username and password to `streamid`;
srt://localhost:8890?streamid=publish:mystream:user:pass&pkt_size=1316
```
+If you need to use the standard stream ID syntax instead of the custom one in use by this server, see [Standard stream ID syntax](#standard-stream-id-syntax).
+
If you want to publish a stream by using a client in listening mode (i.e. with `mode=listener` appended to the URL), read the next section.
Known clients that can publish with SRT are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio).
@@ -636,7 +636,7 @@ paths:
The resulting stream will be available in path `/proxied`.
-The server supports any number of source streams (count is just limited by hardware capability) it's enough to add additional entries to the paths section:
+The server supports any number of source streams (count is just limited by available hardware resources) it's enough to add additional entries to the paths section:
```yml
paths:
@@ -781,8 +781,6 @@ In order to use the UDP-multicast transport protocol, append `?vlcmulticast` to
vlc --network-caching=50 rtsp://localhost:8554/mystream?vlcmulticast
```
-You can change the transport protocol by using the `--rtsp_` flag:
-
##### Ubuntu bug
The VLC shipped with Ubuntu 21.10 doesn't support playing RTSP due to a license issue (see [here](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=982299) and [here](https://stackoverflow.com/questions/69766748/cvlc-cannot-play-rtsp-omxplayer-instead-can)). To fix the issue, remove the default VLC instance and install the snap version:
@@ -812,7 +810,7 @@ This web page can be embedded into another web page by using an iframe:
```
-For more advanced setups, you can create and serve a custom web page by starting from the [source code of the read page](internal/core/webrtc_read_index.html).
+For more advanced setups, you can create and serve a custom web page by starting from the [source code of the read page](internal/servers/webrtc/read_index.html).
Web browsers can also read a stream with the [HLS protocol](#hls). Latency is higher but there are less problems related to connectivity between server and clients, furthermore the server load can be balanced by using a common HTTP CDN (like CloudFront or Cloudflare), and this allows to handle readers in the order of millions. Visit the web page:
@@ -841,9 +839,11 @@ Replace `mystream` with the path name.
If credentials are enabled, append username and password to `streamid`;
```
-srt://localhost:8890?streamid=publish:mystream:user:pass
+srt://localhost:8890?streamid=read:mystream:user:pass
```
+If you need to use the standard stream ID syntax instead of the custom one in use by this server, see [Standard stream ID syntax](#standard-stream-id-syntax).
+
Known clients that can read with SRT are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [VLC](#vlc).
#### WebRTC
@@ -876,7 +876,7 @@ Known clients that can read with RTSP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstr
##### Latency
-The RTSP protocol doesn't introduce any latency by itself. Latency is usually introduced by clients, that put frames in a buffer to compensate network fluctuations. In order to decrease latency, the best way consists in tuning the client. For instance, latency can be decreased with VLC by decreasing the Network caching parameter, that is available in the Open network stream dialog or alternatively ca be set with the command line:
+The RTSP protocol doesn't introduce any latency by itself. Latency is usually introduced by clients, that put frames in a buffer to compensate network fluctuations. In order to decrease latency, the best way consists in tuning the client. For instance, in VLC, latency can be decreased by decreasing the Network caching parameter, that is available in the "Open network stream" dialog or alternatively can be set with the command line:
```
vlc --network-caching=50 rtsp://...
@@ -884,7 +884,7 @@ vlc --network-caching=50 rtsp://...
#### RTMP
-RTMP is a protocol that allows to read and publish streams, but is less versatile and less efficient than RTSP and WebRTC ((doesn't support UDP, doesn't support most RTSP codecs, doesn't support feedback mechanism)). Streams can be read from the server by using the URL:
+RTMP is a protocol that allows to read and publish streams, but is less versatile and less efficient than RTSP and WebRTC (doesn't support UDP, doesn't support most RTSP codecs, doesn't support feedback mechanism). Streams can be read from the server by using the URL:
```
rtmp://localhost/mystream
@@ -1003,7 +1003,7 @@ There are 3 ways to change the configuration:
MTX_RTSPADDRESS="127.0.0.1:8554" ./mediamtx
```
- Parameters that have array as value can be overriden by setting a comma-separated list. For example:
+ Parameters that have array as value can be overridden by setting a comma-separated list. For example:
```
MTX_PROTOCOLS="tcp,udp"
@@ -1025,13 +1025,12 @@ There are 3 ways to change the configuration:
### Authentication
-Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
+Edit `mediamtx.yml` and set `publishUser` and `publishPass`:
```yml
-paths:
- all:
- publishUser: myuser
- publishPass: mypass
+pathDefaults:
+ publishUser: myuser
+ publishPass: mypass
```
Only publishers that provide both username and password will be able to proceed:
@@ -1043,28 +1042,39 @@ ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://myuser:mypass@local
It's possible to setup authentication for readers too:
```yml
-paths:
- all:
- publishUser: myuser
- publishPass: mypass
+pathDefaults:
+ readUser: myuser
+ readPass: mypass
+```
+
+If storing plain credentials in the configuration file is a security problem, username and passwords can be stored as hashed strings. The Argon2 and SHA256 hashing algorithms are supported.
+
+To use Argon2, the string must be hashed using Argon2id (recommended) or Argon2i:
- readUser: user
- readPass: userpass
+```
+echo -n "mypass" | argon2 saltItWithSalt -id -l 32 -e
+```
+
+Then stored with the `argon2:` prefix:
+
+```yml
+pathDefaults:
+ readUser: argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$OGGO0eCMN0ievb4YGSzvS/H+Vajx1pcbUmtLp2tRqRU
+ readPass: argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$oct3kOiFywTdDdt19kT07hdvmsPTvt9zxAUho2DLqZw
```
-If storing plain credentials in the configuration file is a security problem, username and passwords can be stored as sha256-hashed strings; a string must be hashed with sha256 and encoded with base64:
+To use SHA256, the string must be hashed with SHA256 and encoded with base64:
```
-echo -n "userpass" | openssl dgst -binary -sha256 | openssl base64
+echo -n "mypass" | openssl dgst -binary -sha256 | openssl base64
```
Then stored with the `sha256:` prefix:
```yml
-paths:
- all:
- readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
- readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
+pathDefaults:
+ readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
+ readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
```
**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting the credentials in transit.
@@ -1129,7 +1139,7 @@ To change the format, codec or compression of a stream, use _FFmpeg_ or _GStream
```yml
paths:
- all:
+ compressed:
original:
runOnReady: >
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
@@ -1143,20 +1153,18 @@ paths:
To save available streams to disk, set the `record` and the `recordPath` parameter in the configuration file:
```yml
-# Record streams to disk.
-record: yes
-# Path of recording segments.
-# Extension is added automatically.
-# Available variables are %path (path name), %Y %m %d %H %M %S %f (time in strftime format)
-recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f
+pathDefaults:
+ # Record streams to disk.
+ record: yes
+ # Path of recording segments.
+ # Extension is added automatically.
+ # Available variables are %path (path name), %Y %m %d %H %M %S %f %s (time in strftime format)
+ recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f
```
All available recording parameters are listed in the [sample configuration file](/mediamtx.yml).
-Currently the server supports recording tracks encoded with the following codecs:
-
-* Video: AV1, VP9, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG
-* Audio: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3
+Be aware that not all codecs can be saved with all formats, as described in the compatibility matrix at the beginning of the README.
To upload recordings to a remote location, you can use _MediaMTX_ together with [rclone](https://github.com/rclone/rclone), a command line tool that provides file synchronization capabilities with a huge variety of services (including S3, FTP, SMB, Google Drive):
@@ -1171,35 +1179,84 @@ To upload recordings to a remote location, you can use _MediaMTX_ together with
3. Place `rclone` into the `runOnInit` and `runOnRecordSegmentComplete` hooks:
```yml
- record: yes
-
- paths:
- mypath:
- # this is needed to sync segments after a crash.
- # replace myconfig with the name of the rclone config.
- runOnInit: rclone sync -v ./recordings myconfig:/my-path/recordings
-
- # this is called when a segment has been finalized.
- # replace myconfig with the name of the rclone config.
- runOnRecordSegmentComplete: rclone sync -v --min-age=1ms ./recordings myconfig:/my-path/recordings
+ pathDefaults:
+ # this is needed to sync segments after a crash.
+ # replace myconfig with the name of the rclone config.
+ runOnInit: rclone sync -v ./recordings myconfig:/my-path/recordings
+
+ # this is called when a segment has been finalized.
+ # replace myconfig with the name of the rclone config.
+ runOnRecordSegmentComplete: rclone sync -v --min-age=1ms ./recordings myconfig:/my-path/recordings
```
If you want to delete local segments after they are uploaded, replace `rclone sync` with `rclone move`.
-### Forward streams to another server
+### Playback recordings
+
+Recordings can be served to users through a dedicated HTTP server, that can be enabled inside the configuration:
+
+```yml
+playback: yes
+playbackAddress: :9996
+```
+
+The server can be queried for recordings by using the URL:
+
+```
+http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]&format=[format]
+```
+
+Where:
+
+* [mypath] is the path name
+* [start_date] is the start date in RFC3339 format
+* [duration] is the maximum duration of the recording in Golang format (example: 20s, 20h)
+* [format] must be fmp4
+
+All parameters must be [url-encoded](https://www.urlencoder.org/).
+
+For instance:
+
+```
+http://localhost:9996/get?path=stream2&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200s&format=fmp4
+```
+
+The resulting stream is natively compatible with any browser, therefore its URL can be directly inserted into a \ tag:
+
+```html
+
+
+
+```
+
+### Forward streams to other servers
To forward incoming streams to another server, use _FFmpeg_ inside the `runOnReady` parameter:
+```yml
+pathDefaults:
+ runOnReady: >
+ ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
+ -c copy
+ -f rtsp rtsp://other-server:8554/another-path
+ runOnReadyRestart: yes
+```
+
+### Proxy requests to other servers
+
+The server allows to proxy incoming requests to other servers or cameras. This is useful to expose servers or cameras behind a NAT. Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
+
```yml
paths:
- all:
- runOnReady: >
- ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
- -c copy
- -f rtsp rtsp://another-server/another-path
- runOnReadyRestart: yes
+ "~^proxy_(.+)$":
+ # If path name is a regular expression, $G1, G2, etc will be replaced
+ # with regular expression groups.
+ source: rtsp://other-server:8554/$G1
+ sourceOnDemand: yes
```
+All requests addressed to `rtsp://server:8854/proxy_a` will be forwarded to `rtsp://other-server:8854/a` and so on.
+
### On-demand publishing
Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
@@ -1217,16 +1274,16 @@ The command inserted into `runOnDemand` will start only when a client requests t
#### Linux
-Systemd is the service manager used by Ubuntu, Debian and many other Linux distributions, and allows to launch _MediaMTX_ on boot.
+On most Linux distributions (including Ubuntu and Debian, but not OpenWrt), _systemd_ is in charge of managing services and starting them on boot.
-Download a release bundle from the [release page](https://github.com/bluenviron/mediamtx/releases), unzip it, and move the executable and configuration in the system:
+Move the server executable and configuration in global folders:
```sh
sudo mv mediamtx /usr/local/bin/
sudo mv mediamtx.yml /usr/local/etc/
```
-Create the service:
+Create a _systemd_ service:
```sh
sudo tee /etc/systemd/system/mediamtx.service >/dev/null << EOF
@@ -1247,6 +1304,47 @@ sudo systemctl enable mediamtx
sudo systemctl start mediamtx
```
+#### OpenWrt
+
+Move the server executable and configuration in global folders:
+
+```sh
+mv mediamtx /usr/bin/
+mkdir -p /usr/etc && mv mediamtx.yml /usr/etc/
+```
+
+Create a procd service:
+
+```sh
+tee /etc/init.d/mediamtx >/dev/null << EOF
+#!/bin/sh /etc/rc.common
+USE_PROCD=1
+START=95
+STOP=01
+start_service() {
+ procd_open_instance
+ procd_set_param command /usr/bin/mediamtx
+ procd_set_param stdout 1
+ procd_set_param stderr 1
+ procd_close_instance
+}
+EOF
+```
+
+Enable and start the service:
+
+```sh
+chmod +x /etc/init.d/mediamtx
+/etc/init.d/mediamtx enable
+/etc/init.d/mediamtx start
+```
+
+Read the server logs:
+
+```sh
+logread
+```
+
#### Windows
Download the [WinSW v2 executable](https://github.com/winsw/winsw/releases/download/v2.11.0/WinSW-x64.exe) and place it into the same folder of `mediamtx.exe`.
@@ -1277,6 +1375,7 @@ The server allows to specify commands that are executed when a certain event hap
`runOnConnect` allows to run a command when a client connects to the server:
```yml
+# Command to run when a client connects to the server.
# This is terminated with SIGINT when a client disconnects from the server.
# The following environment variables are available:
# * RTSP_PORT: RTSP server port
@@ -1290,6 +1389,7 @@ runOnConnectRestart: no
`runOnDisconnect` allows to run a command when a client disconnects from the server:
```yml
+# Command to run when a client disconnects from the server.
# Environment variables are the same of runOnConnect.
runOnDisconnect: curl http://my-custom-server/webhook?conn_type=$MTX_CONN_TYPE&conn_id=$MTX_CONN_ID
```
@@ -1299,7 +1399,8 @@ runOnDisconnect: curl http://my-custom-server/webhook?conn_type=$MTX_CONN_TYPE&c
```yml
paths:
mypath:
- # This is terminated with SIGINT when the program closes.
+ # Command to run when this path is initialized.
+ # This can be used to publish a stream when the server is launched.
# The following environment variables are available:
# * MTX_PATH: path name
# * RTSP_PORT: RTSP server port
@@ -1313,87 +1414,113 @@ paths:
`runOnDemand` allows to run a command when a path is requested by a reader. This can be used to publish a stream on demand:
```yml
-paths:
- mypath:
- # This is terminated with SIGINT when the program closes.
- # The following environment variables are available:
- # * MTX_PATH: path name
- # * RTSP_PORT: RTSP server port
- # * G1, G2, ...: regular expression groups, if path name is
- # a regular expression.
- runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath
- # Restart the command if it exits.
- runOnDemandRestart: no
+pathDefaults:
+ # Command to run when this path is requested by a reader
+ # and no one is publishing to this path yet.
+ # This is terminated with SIGINT when there are no readers anymore.
+ # The following environment variables are available:
+ # * MTX_PATH: path name
+ # * MTX_QUERY: query parameters (passed by first reader)
+ # * RTSP_PORT: RTSP server port
+ # * G1, G2, ...: regular expression groups, if path name is
+ # a regular expression.
+ runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath
+ # Restart the command if it exits.
+ runOnDemandRestart: no
+```
+
+`runOnUnDemand` allows to run a command when there are no readers anymore:
+
+```yml
+pathDefaults:
+ # Command to run when there are no readers anymore.
+ # Environment variables are the same of runOnDemand.
+ runOnUnDemand:
```
`runOnReady` allows to run a command when a stream is ready to be read:
```yml
-paths:
- mypath:
- # This is terminated with SIGINT when the stream is not ready anymore.
- # The following environment variables are available:
- # * MTX_PATH: path name
- # * MTX_SOURCE_TYPE: source type
- # * MTX_SOURCE_ID: source ID
- # * RTSP_PORT: RTSP server port
- # * G1, G2, ...: regular expression groups, if path name is
- # a regular expression.
- runOnReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID
- # Restart the command if it exits.
- runOnReadyRestart: no
+pathDefaults:
+ # Command to run when the stream is ready to be read, whenever it is
+ # published by a client or pulled from a server / camera.
+ # This is terminated with SIGINT when the stream is not ready anymore.
+ # The following environment variables are available:
+ # * MTX_PATH: path name
+ # * MTX_QUERY: query parameters (passed by publisher)
+ # * MTX_SOURCE_TYPE: source type
+ # * MTX_SOURCE_ID: source ID
+ # * RTSP_PORT: RTSP server port
+ # * G1, G2, ...: regular expression groups, if path name is
+ # a regular expression.
+ runOnReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID
+ # Restart the command if it exits.
+ runOnReadyRestart: no
```
`runOnNotReady` allows to run a command when a stream is not available anymore:
```yml
-paths:
- mypath:
- # Environment variables are the same of runOnReady.
- runOnNotReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID
+pathDefaults:
+ # Command to run when the stream is not available anymore.
+ # Environment variables are the same of runOnReady.
+ runOnNotReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID
```
`runOnRead` allows to run a command when a client starts reading:
```yml
-paths:
- mypath:
- # This is terminated with SIGINT when a client stops reading.
- # The following environment variables are available:
- # * MTX_PATH: path name
- # * MTX_READER_TYPE: reader type
- # * MTX_READER_ID: reader ID
- # * RTSP_PORT: RTSP server port
- # * G1, G2, ...: regular expression groups, if path name is
- # a regular expression.
- runOnRead: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID
- # Restart the command if it exits.
- runOnReadRestart: no
+pathDefaults:
+ # Command to run when a client starts reading.
+ # This is terminated with SIGINT when a client stops reading.
+ # The following environment variables are available:
+ # * MTX_PATH: path name
+ # * MTX_QUERY: query parameters (passed by reader)
+ # * MTX_READER_TYPE: reader type
+ # * MTX_READER_ID: reader ID
+ # * RTSP_PORT: RTSP server port
+ # * G1, G2, ...: regular expression groups, if path name is
+ # a regular expression.
+ runOnRead: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID
+ # Restart the command if it exits.
+ runOnReadRestart: no
```
`runOnUnread` allows to run a command when a client stops reading:
```yml
-paths:
- mypath:
- # Command to run when a client stops reading.
- # Environment variables are the same of runOnRead.
- runOnUnread: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID
+pathDefaults:
+ # Command to run when a client stops reading.
+ # Environment variables are the same of runOnRead.
+ runOnUnread: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID
```
-`runOnRecordSegmentComplete` allows to run a command when a record segment is complete:
+`runOnRecordSegmentCreate` allows to run a command when a recording segment is created:
```yml
-paths:
- mypath:
- # Command to run when a record segment is complete.
- # The following environment variables are available:
- # * MTX_PATH: path name
- # * RTSP_PORT: RTSP server port
- # * G1, G2, ...: regular expression groups, if path name is
- # a regular expression.
- # * MTX_SEGMENT_PATH: segment file path
- runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH
+pathDefaults:
+ # Command to run when a recording segment is created.
+ # The following environment variables are available:
+ # * MTX_PATH: path name
+ # * RTSP_PORT: RTSP server port
+ # * G1, G2, ...: regular expression groups, if path name is
+ # a regular expression.
+ # * MTX_SEGMENT_PATH: segment file path
+ runOnRecordSegmentCreate: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH
+```
+
+`runOnRecordSegmentComplete` allows to run a command when a recording segment is complete:
+
+```yml
+pathDefaults:
+ # Command to run when a recording segment is complete.
+ # The following environment variables are available:
+ # * MTX_PATH: path name
+ # * RTSP_PORT: RTSP server port
+ # * G1, G2, ...: regular expression groups, if path name is
+ # a regular expression.
+ # * MTX_SEGMENT_PATH: segment file path
+ runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH
```
### API
@@ -1426,6 +1553,7 @@ Obtaining:
# metrics of every path
paths{name="[path_name]",state="[state]"} 1
paths_bytes_received{name="[path_name]",state="[state]"} 1234
+paths_bytes_sent{name="[path_name]",state="[state]"} 1234
# metrics of every HLS muxer
hls_muxers{name="[name]"} 1
@@ -1456,8 +1584,18 @@ rtmp_conns{id="[id]",state="[state]"} 1
rtmp_conns_bytes_received{id="[id]",state="[state]"} 1234
rtmp_conns_bytes_sent{id="[id]",state="[state]"} 187
+# metrics of every RTMPS connection
+rtmps_conns{id="[id]",state="[state]"} 1
+rtmps_conns_bytes_received{id="[id]",state="[state]"} 1234
+rtmps_conns_bytes_sent{id="[id]",state="[state]"} 187
+
+# metrics of every SRT connection
+srt_conns{id="[id]",state="[state]"} 1
+srt_conns_bytes_received{id="[id]",state="[state]"} 1234
+srt_conns_bytes_sent{id="[id]",state="[state]"} 187
+
# metrics of every WebRTC session
-webrtc_sessions{id="[id]"} 1
+webrtc_sessions{id="[id]",state="[state]"} 1
webrtc_sessions_bytes_received{id="[id]",state="[state]"} 1234
webrtc_sessions_bytes_sent{id="[id]",state="[state]"} 187
```
@@ -1472,6 +1610,84 @@ go tool pprof -text http://localhost:9999/debug/pprof/heap
go tool pprof -text http://localhost:9999/debug/pprof/profile?seconds=30
```
+### SRT-specific features
+
+#### Standard stream ID syntax
+
+In SRT, the stream ID is a string that is sent to the counterpart in order to advertise what action the caller is gonna do (publish or read), the path and the credentials. All these informations have to be encoded into a single string. This server supports two stream ID syntaxes, a custom one (that is the one reported in rest of the README) and also a [standard one](https://github.com/Haivision/srt/blob/master/docs/features/access-control.md) proposed by the authors of the protocol and sometimes enforced by some hardware. The standard syntax can be used in this way:
+
+```
+srt://localhost:8890?streamid=#!::m=publish,r=mypath,u=myuser,s=mypass&pkt_size=1316
+```
+
+Where:
+
+* key `m` contains the action (`publish` or `request`)
+* key `r` contains the path
+* key `u` contains the username
+* key `s` contains the password
+
+### WebRTC-specific features
+
+#### 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.
+
+Make sure that `webrtcAdditionalHosts` includes your public IPs, that are IPs that can be used by clients to reach the server. If clients are on the same LAN as the server, then insert the LAN address of the server. If clients are coming from the internet, insert the public IP address of the server, or alternatively a DNS name, if you have one. You can insert multiple values to support all scenarios:
+
+```yml
+webrtcAdditionalHosts: [192.168.x.x, 1.2.3.4, my-dns.example.org, ...]
+```
+
+If there's a NAT / container between server and clients, it must be configured to route all incoming UDP packets on port 8189 to the server. If you're using Docker, this can be achieved with the flag:
+
+```sh
+docker run --rm -it \
+-p 8189:8189/udp
+....
+bluenviron/mediamtx
+```
+
+If you still have problems, maybe the UDP protocol is blocked by a firewall. Enable the local TCP listener:
+
+```yml
+# any port of choice
+webrtcLocalTCPAddress: :8189
+```
+
+If there's a NAT / container between server and clients, it must be configured to route all incoming TCP packets on port 8189 to the server.
+
+If you still have problems, enable a STUN server:
+
+```yml
+# STUN servers allows to obtain and share the public IP of the server.
+webrtcICEServers2:
+ - url: stun:stun.l.google.com:19302
+```
+
+If you really still have problems, you can force all WebRTC/ICE connections to pass through a TURN server, like coturn, that must be configured externally. The server address and credentials must be set in the configuration file:
+
+```yml
+# TURN/TURNS servers forces all traffic through them.
+webrtcICEServers2:
+- url: turn:host:port
+ username: user
+ password: password
+```
+
+Where user and pass are the username and password of the server. Note that port is not optional.
+
+If the server uses a secret-based authentication (for instance, coturn with the use-auth-secret option), it must be configured by using AUTH_SECRET as username, and the secret as password:
+
+```yml
+webrtcICEServers2:
+- url: turn:host:port
+ username: AUTH_SECRET
+ password: secret
+```
+
+where secret is the secret of the TURN server. MediaMTX will generate a set of credentials by using the secret, and credentials will be sent to clients before the WebRTC/ICE connection is established.
+
### RTSP-specific features
#### Transport protocols
@@ -1530,7 +1746,7 @@ In some scenarios, when publishing or reading from the server with RTSP, frames
paths:
test:
source: rtsp://..
- sourceProtocol: tcp
+ rtspTransport: tcp
```
* The stream throughput is too big to be handled by the network between server and readers. Upgrade the network or decrease the stream bitrate by re-encoding it.
@@ -1562,98 +1778,95 @@ rtmps://localhost:1937/...
Be aware that RTMPS is currently unsupported by all major players. However, you can use a proxy like [stunnel](https://www.stunnel.org) or [nginx](https://nginx.org/) or a dedicated _MediaMTX_ instance to decrypt streams before reading them.
-### WebRTC-specific features
-
-#### Connectivity issues
+## Compile from source
-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 (the browser and the server) to establish a connection (WebRTC/ICE connection).
+### Standard
-A first method consists into forcing all WebRTC/ICE connections to pass through a single UDP server port, by using the parameters:
+Install git and Go ≥ 1.21. Clone the repository, enter into the folder and start the building process:
-```yml
-# public IP of the server
-webrtcICEHostNAT1To1IPs: [192.168.x.x]
-# any port of choice
-webrtcICEUDPMuxAddress: :8189
+```sh
+git clone https://github.com/bluenviron/mediamtx
+cd mediamtx
+CGO_ENABLED=0 go build .
```
-The NAT / container must then be configured in order to route all incoming UDP packets on port 8189 to the server. If you're using Docker, this can be achieved with the flag:
+The command will produce the `mediamtx` binary.
+
+### Raspberry Pi
+
+The server can be compiled with native support for the Raspberry Pi Camera. Compilation must be performed on a Raspberry Pi, with the following dependencies:
+
+* Go ≥ 1.21
+* `libcamera-dev`
+* `libfreetype-dev`
+* `xxd`
+
+Download the repository, open a terminal in it and run:
```sh
-docker run --rm -it \
--p 8189:8189/udp
-....
-bluenviron/mediamtx
+cd internal/protocols/rpicamera/exe
+make
+cd ../../../../
+go build -tags rpicamera .
```
-If the UDP protocol is blocked by a firewall, all WebRTC/ICE connections can be forced to pass through a single TCP server port:
+The command will produce the `mediamtx` binary.
-```yml
-# public IP of the server
-webrtcICEHostNAT1To1IPs: [192.168.x.x]
-# any port of choice
-webrtcICETCPMuxAddress: :8189
-```
+### OpenWrt
-The NAT / container must then be configured in order to redirect all incoming TCP packets on port 8189 to the server. If you're using Docker, this can be achieved with the flag:
+The compilation procedure is the same as the standard one. On the OpenWrt device, install git and Go:
```sh
-docker run --rm -it \
--p 8189:8189
-....
-bluenviron/mediamtx
+opkg update
+opkg install golang git git-http
```
-Finally, if none of these methods work, you can force all WebRTC/ICE connections to pass through a TURN server, like coturn, that must be configured externally. The server address and credentials must be set in the configuration file:
+Clone the repository, enter into the folder and start the building process:
-```yml
-webrtcICEServers2:
-- url: turn:host:port
- username: user
- password: password
+```sh
+git clone https://github.com/bluenviron/mediamtx
+cd mediamtx
+CGO_ENABLED=0 go build .
```
-Where user and pass are the username and password of the server. Note that port is not optional.
+The command will produce the `mediamtx` binary.
-If the server uses a secret-based authentication (for instance, coturn with the use-auth-secret option), it must be configured by using AUTH_SECRET as username, and the secret as password:
+If the OpenWrt device doesn't have enough resources to compile, you can [cross compile](#cross-compile) from another machine.
-```yml
-webrtcICEServers2:
-- url: turn:host:port
- username: AUTH_SECRET
- password: secret
-```
+### Cross compile
-where secret is the secret of the TURN server. MediaMTX will generate a set of credentials by using the secret, and credentials will be sent to clients before the WebRTC/ICE connection is established.
+Cross compilation allows to build an executable for a target machine from another machine with different operating system or architecture. This is useful in case the target machine doesn't have enough resources for compilation or if you don't want to install the compilation dependencies on it.
-## Compile from source
+On the machine you want to use to compile, install git and Go ≥ 1.21. Clone the repository, enter into the folder and start the building process:
-### Standard
+```sh
+git clone https://github.com/bluenviron/mediamtx
+cd mediamtx
+CGO_ENABLED=0 GOOS=my_os GOARCH=my_arch go build .
+```
-Install Go ≥ 1.20, download the repository, open a terminal in it and run:
+Replace `my_os` and `my_arch` with the operating system and architecture of your target machine. A list of all supported combinations can be obtained with:
```sh
-go build .
+go tool dist list
```
-The command will produce the `mediamtx` binary.
+For instance:
-### Raspberry Pi
+```sh
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build .
+```
-The server can be compiled with native support for the Raspberry Pi Camera. Compilation must be performed on a Raspberry Pi, with the following dependencies:
+In case of the `arm` architecture, there's an additional flag available, `GOARM`, that allows to set the ARM version:
-* Go ≥ 1.20
-* `libcamera-dev`
-* `libfreetype-dev`
-* `xxd`
+```sh
+CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build .
+```
-Download the repository, open a terminal in it and run:
+In case of the `mips` architecture, there's an additional flag available, `GOMIPS`, that allows to set additional parameters:
```sh
-cd internal/rpicamera/exe
-make
-cd ../../../
-go build -tags rpicamera .
+CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build .
```
The command will produce the `mediamtx` binary.
diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml
index 9c775973fd2..ad82a07bbe5 100644
--- a/apidocs/openapi.yaml
+++ b/apidocs/openapi.yaml
@@ -15,7 +15,13 @@ security: []
components:
schemas:
- Conf:
+ Error:
+ type: object
+ properties:
+ error:
+ type: string
+
+ GlobalConf:
type: object
properties:
# General
@@ -37,10 +43,6 @@ components:
type: integer
externalAuthenticationURL:
type: string
- api:
- type: boolean
- apiAddress:
- type: string
metrics:
type: boolean
metricsAddress:
@@ -56,7 +58,19 @@ components:
runOnDisconnect:
type: string
- # RTSP
+ # API
+ api:
+ type: boolean
+ apiAddress:
+ type: string
+
+ # Playback server
+ playback:
+ type: boolean
+ playbackAddress:
+ type: string
+
+ # RTSP server
rtsp:
type: boolean
protocols:
@@ -88,7 +102,7 @@ components:
items:
type: string
- # RTMP
+ # RTMP server
rtmp:
type: boolean
rtmpAddress:
@@ -102,7 +116,7 @@ components:
rtmpServerCert:
type: string
- # HLS
+ # HLS server
hls:
type: boolean
hlsAddress:
@@ -134,7 +148,7 @@ components:
hlsDirectory:
type: string
- # WebRTC
+ # WebRTC server
webrtc:
type: boolean
webrtcAddress:
@@ -151,6 +165,20 @@ components:
type: array
items:
type: string
+ webrtcLocalUDPAddress:
+ type: string
+ webrtcLocalTCPAddress:
+ type: string
+ webrtcIPsFromInterfaces:
+ type: boolean
+ webrtcIPsFromInterfacesList:
+ type: array
+ items:
+ type: string
+ webrtcAdditionalHosts:
+ type: array
+ items:
+ type: string
webrtcICEServers2:
type: array
items:
@@ -162,44 +190,19 @@ components:
type: string
password:
type: string
- webrtcICEHostNAT1To1IPs:
- type: array
- items:
- type: string
- webrtcICEUDPMuxAddress:
- type: string
- webrtcICETCPMuxAddress:
- type: string
- # SRT
+ # SRT server
srt:
type: boolean
srtAddress:
type: string
- # Record
- record:
- type: boolean
- recordPath:
- type: string
- recordFormat:
- type: string
- recordPartDuration:
- type: string
- recordSegmentDuration:
- type: string
- recordDeleteAfter:
- type: string
-
- # Paths
- paths:
- type: object
- additionalProperties:
- $ref: '#/components/schemas/PathConf'
-
PathConf:
type: object
properties:
+ name:
+ type: string
+
# General
source:
type: string
@@ -215,8 +218,24 @@ components:
type: integer
srtReadPassphrase:
type: string
+ fallback:
+ type: string
+
+ # Record and playback
record:
type: boolean
+ playback:
+ type: boolean
+ recordPath:
+ type: string
+ recordFormat:
+ type: string
+ recordPartDuration:
+ type: string
+ recordSegmentDuration:
+ type: string
+ recordDeleteAfter:
+ type: string
# Authentication
publishUser:
@@ -236,29 +255,27 @@ components:
items:
type: string
- # Publisher
+ # Publisher source
overridePublisher:
type: boolean
- fallback:
- type: string
srtPublishPassphrase:
type: string
- # RTSP
- sourceProtocol:
+ # RTSP source
+ rtspTransport:
type: string
- sourceAnyPortEnable:
+ rtspAnyPort:
type: boolean
rtspRangeType:
type: string
rtspRangeStart:
type: string
- # Redirect
+ # Redirect source
sourceRedirect:
type: string
- # Raspberry Pi Camera
+ # Raspberry Pi Camera source
rpiCameraCamID:
type: integer
rpiCameraWidth:
@@ -337,6 +354,8 @@ components:
type: string
runOnDemandCloseAfter:
type: string
+ runOnUnDemand:
+ type: string
runOnReady:
type: string
runOnReadyRestart:
@@ -349,9 +368,21 @@ components:
type: boolean
runOnUnread:
type: string
+ runOnRecordSegmentCreate:
+ type: string
runOnRecordSegmentComplete:
type: string
+ PathConfList:
+ type: object
+ properties:
+ pageCount:
+ type: integer
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/PathConf'
+
Path:
type: object
properties:
@@ -359,10 +390,8 @@ components:
type: string
confName:
type: string
- conf:
- $ref: '#/components/schemas/PathConf'
source:
- $ref: '#/components/schemas/PathSourceOrReader'
+ $ref: '#/components/schemas/PathSource'
nullable: true
ready:
type: boolean
@@ -376,12 +405,15 @@ components:
bytesReceived:
type: integer
format: int64
+ bytesSent:
+ type: integer
+ format: int64
readers:
type: array
items:
- $ref: '#/components/schemas/PathSourceOrReader'
+ $ref: '#/components/schemas/PathReader'
- PathsList:
+ PathList:
type: object
properties:
pageCount:
@@ -391,19 +423,17 @@ components:
items:
$ref: '#/components/schemas/Path'
- PathSourceOrReader:
+ PathSource:
type: object
properties:
type:
type: string
enum:
- - hlsMuxer
- hlsSource
- redirect
- rpiCameraSource
- rtmpConn
- rtmpSource
- - rtmpsSession
- rtspSession
- rtspSource
- rtspsSession
@@ -415,6 +445,21 @@ components:
id:
type: string
+ PathReader:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - hlsMuxer
+ - rtmpConn
+ - rtspSession
+ - rtspsSession
+ - srtConn
+ - webRTCSession
+ id:
+ type: string
+
HLSMuxer:
type: object
properties:
@@ -428,7 +473,7 @@ components:
type: integer
format: int64
- HLSMuxersList:
+ HLSMuxerList:
type: object
properties:
pageCount:
@@ -452,6 +497,8 @@ components:
enum: [idle, read, publish]
path:
type: string
+ query:
+ type: string
bytesReceived:
type: integer
format: int64
@@ -459,7 +506,7 @@ components:
type: integer
format: int64
- RTMPConnsList:
+ RTMPConnList:
type: object
properties:
pageCount:
@@ -485,7 +532,7 @@ components:
type: integer
format: int64
- RTSPConnsList:
+ RTSPConnList:
type: object
properties:
pageCount:
@@ -509,6 +556,8 @@ components:
enum: [idle, read, publish]
path:
type: string
+ query:
+ type: string
transport:
type: string
nullable: true
@@ -519,7 +568,7 @@ components:
type: integer
format: int64
- RTSPSessionsList:
+ RTSPSessionList:
type: object
properties:
pageCount:
@@ -543,6 +592,8 @@ components:
enum: [idle, read, publish]
path:
type: string
+ query:
+ type: string
bytesReceived:
type: integer
format: int64
@@ -550,7 +601,7 @@ components:
type: integer
format: int64
- SRTConnsList:
+ SRTConnList:
type: object
properties:
pageCount:
@@ -580,6 +631,8 @@ components:
enum: [read, publish]
path:
type: string
+ query:
+ type: string
bytesReceived:
type: integer
format: int64
@@ -587,7 +640,7 @@ components:
type: integer
format: int64
- WebRTCSessionsList:
+ WebRTCSessionList:
type: object
properties:
pageCount:
@@ -598,10 +651,10 @@ components:
$ref: '#/components/schemas/WebRTCSession'
paths:
- /v2/config/get:
+ /v3/config/global/get:
get:
- operationId: configGet
- summary: returns the configuration.
+ operationId: configGlobalGet
+ summary: returns the global configuration.
description: ''
responses:
'200':
@@ -609,35 +662,179 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/Conf'
+ $ref: '#/components/schemas/GlobalConf'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/config/set:
- post:
- operationId: configSet
- summary: changes the configuration.
- description: all fields are optional. paths can't be edited with this request, use /v2/config/paths/{operation}/{name} to edit them.
+ /v3/config/global/patch:
+ patch:
+ operationId: configGlobalSet
+ summary: patches the global configuration.
+ description: all fields are optional.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GlobalConf'
+ responses:
+ '200':
+ description: the request was successful.
+ '400':
+ description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '500':
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /v3/config/pathdefaults/get:
+ get:
+ operationId: configPathDefaultsGet
+ summary: returns the default path configuration.
+ description: ''
+ responses:
+ '200':
+ description: the request was successful.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PathConf'
+ '400':
+ description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '500':
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /v3/config/pathdefaults/patch:
+ patch:
+ operationId: configPathDefaultsPatch
+ summary: patches the default path configuration.
+ description: all fields are optional.
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/Conf'
+ $ref: '#/components/schemas/PathConf'
+ responses:
+ '200':
+ description: the request was successful.
+ '400':
+ description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '500':
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /v3/config/paths/list:
+ get:
+ operationId: configPathsList
+ summary: returns all path configurations.
+ description: ''
+ parameters:
+ - name: page
+ in: query
+ description: page number.
+ schema:
+ type: integer
+ default: 0
+ - name: itemsPerPage
+ in: query
+ description: items per page.
+ schema:
+ type: integer
+ default: 100
+ responses:
+ '200':
+ description: the request was successful.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PathConfList'
+ '400':
+ description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '500':
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /v3/config/paths/get/{name}:
+ get:
+ operationId: configPathsGet
+ summary: returns a path configuration.
+ description: ''
+ parameters:
+ - name: name
+ in: path
+ required: true
+ description: the name of the path.
+ schema:
+ type: string
responses:
'200':
description: the request was successful.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PathConf'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ description: path not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/config/paths/add/{name}:
+ /v3/config/paths/add/{name}:
post:
operationId: configPathsAdd
- summary: adds the configuration of a path.
+ summary: adds a path configuration.
description: all fields are optional.
parameters:
- name: name
@@ -657,13 +854,21 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/config/paths/edit/{name}:
- post:
- operationId: configPathsEdit
- summary: changes the configuration of a path.
+ /v3/config/paths/patch/{name}:
+ patch:
+ operationId: configPathsPatch
+ summary: patches a path configuration.
description: all fields are optional.
parameters:
- name: name
@@ -683,15 +888,67 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
- description: configuration not found.
+ description: path not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/config/paths/remove/{name}:
+ /v3/config/paths/replace/{name}:
post:
- operationId: configPathsRemove
- summary: removes the configuration of a path.
+ operationId: configPathsReplace
+ summary: replaces all values of a path configuration.
+ description: all fields are optional.
+ parameters:
+ - name: name
+ in: path
+ required: true
+ description: the name of the path.
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PathConf'
+ responses:
+ '200':
+ description: the request was successful.
+ '400':
+ description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ description: path not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '500':
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /v3/config/paths/delete/{name}:
+ delete:
+ operationId: configPathsDelete
+ summary: removes a path configuration.
description: ''
parameters:
- name: name
@@ -705,12 +962,24 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
- description: configuration not found.
+ description: path not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/hlsmuxers/list:
+ /v3/hlsmuxers/list:
get:
operationId: hlsMuxersList
summary: returns all HLS muxers.
@@ -734,13 +1003,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/HLSMuxersList'
+ $ref: '#/components/schemas/HLSMuxerList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/hlsmuxers/get/{name}:
+ /v3/hlsmuxers/get/{name}:
get:
operationId: hlsMuxersGet
summary: returns a HLS muxer.
@@ -761,12 +1038,24 @@ paths:
$ref: '#/components/schemas/HLSMuxer'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: muxer not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/paths/list:
+ /v3/paths/list:
get:
operationId: pathsList
summary: returns all paths.
@@ -790,13 +1079,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/PathsList'
+ $ref: '#/components/schemas/PathList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/paths/get/{name}:
+ /v3/paths/get/{name}:
get:
operationId: pathsGet
summary: returns a path.
@@ -817,12 +1114,24 @@ paths:
$ref: '#/components/schemas/Path'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: path not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspconns/list:
+ /v3/rtspconns/list:
get:
operationId: rtspConnsList
summary: returns all RTSP connections.
@@ -846,13 +1155,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/RTSPConnsList'
+ $ref: '#/components/schemas/RTSPConnList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspconns/get/{id}:
+ /v3/rtspconns/get/{id}:
get:
operationId: rtspConnsGet
summary: returns a RTSP connection.
@@ -873,12 +1190,24 @@ paths:
$ref: '#/components/schemas/RTSPConn'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspsessions/list:
+ /v3/rtspsessions/list:
get:
operationId: rtspSessionsList
summary: returns all RTSP sessions.
@@ -902,13 +1231,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/RTSPSessionsList'
+ $ref: '#/components/schemas/RTSPSessionList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspsessions/get/{id}:
+ /v3/rtspsessions/get/{id}:
get:
operationId: rtspSessionsGet
summary: returns a RTSP session.
@@ -929,12 +1266,24 @@ paths:
$ref: '#/components/schemas/RTSPSession'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspsessions/kick/{id}:
+ /v3/rtspsessions/kick/{id}:
post:
operationId: rtspSessionsKick
summary: kicks out a RTSP session from the server.
@@ -951,12 +1300,24 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspsconns/list:
+ /v3/rtspsconns/list:
get:
operationId: rtspsConnsList
summary: returns all RTSPS connections.
@@ -980,13 +1341,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/RTSPConnsList'
+ $ref: '#/components/schemas/RTSPConnList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspsconns/get/{id}:
+ /v3/rtspsconns/get/{id}:
get:
operationId: rtspsConnsGet
summary: returns a RTSPS connection.
@@ -1007,12 +1376,24 @@ paths:
$ref: '#/components/schemas/RTSPConn'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspssessions/list:
+ /v3/rtspssessions/list:
get:
operationId: rtspsSessionsList
summary: returns all RTSPS sessions.
@@ -1036,15 +1417,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/RTSPSessionsList'
+ $ref: '#/components/schemas/RTSPSessionList'
'400':
description: invalid request.
- '404':
- description: session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspssessions/get/{id}:
+ /v3/rtspssessions/get/{id}:
get:
operationId: rtspsSessionsGet
summary: returns a RTSPS session.
@@ -1065,12 +1452,24 @@ paths:
$ref: '#/components/schemas/RTSPSession'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtspssessions/kick/{id}:
+ /v3/rtspssessions/kick/{id}:
post:
operationId: rtspsSessionsKick
summary: kicks out a RTSPS session from the server.
@@ -1087,12 +1486,24 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtmpconns/list:
+ /v3/rtmpconns/list:
get:
operationId: rtmpConnsList
summary: returns all RTMP connections.
@@ -1116,13 +1527,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/RTMPConnsList'
+ $ref: '#/components/schemas/RTMPConnList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtmpconns/get/{id}:
+ /v3/rtmpconns/get/{id}:
get:
operationId: rtmpConnectionsGet
summary: returns a RTMP connection.
@@ -1143,12 +1562,24 @@ paths:
$ref: '#/components/schemas/RTMPConn'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtmpconns/kick/{id}:
+ /v3/rtmpconns/kick/{id}:
post:
operationId: rtmpConnsKick
summary: kicks out a RTMP connection from the server.
@@ -1165,12 +1596,24 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
- description: session not found.
+ description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtmpsconns/list:
+ /v3/rtmpsconns/list:
get:
operationId: rtmpsConnsList
summary: returns all RTMPS connections.
@@ -1194,13 +1637,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/RTMPConnsList'
+ $ref: '#/components/schemas/RTMPConnList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtmpsconns/get/{id}:
+ /v3/rtmpsconns/get/{id}:
get:
operationId: rtmpsConnectionsGet
summary: returns a RTMPS connection.
@@ -1221,12 +1672,24 @@ paths:
$ref: '#/components/schemas/RTMPConn'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/rtmpsconns/kick/{id}:
+ /v3/rtmpsconns/kick/{id}:
post:
operationId: rtmpsConnsKick
summary: kicks out a RTMPS connection from the server.
@@ -1243,12 +1706,24 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
- description: session not found.
+ description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/srtconns/list:
+ /v3/srtconns/list:
get:
operationId: srtConnsList
summary: returns all SRT connections.
@@ -1272,13 +1747,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/SRTConnsList'
+ $ref: '#/components/schemas/SRTConnList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/srtconns/get/{id}:
+ /v3/srtconns/get/{id}:
get:
operationId: srtConnsGet
summary: returns a SRT connection.
@@ -1299,12 +1782,24 @@ paths:
$ref: '#/components/schemas/SRTConn'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/srtconns/kick/{id}:
+ /v3/srtconns/kick/{id}:
post:
operationId: srtConnsKick
summary: kicks out a SRT connection from the server.
@@ -1321,12 +1816,24 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: connection not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/webrtcsessions/list:
+ /v3/webrtcsessions/list:
get:
operationId: webrtcSessionsList
summary: returns all WebRTC sessions.
@@ -1350,13 +1857,21 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/WebRTCSessionsList'
+ $ref: '#/components/schemas/WebRTCSessionList'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/webrtcsessions/get/{id}:
+ /v3/webrtcsessions/get/{id}:
get:
operationId: webrtcSessionsGet
summary: returns a WebRTC session.
@@ -1377,12 +1892,24 @@ paths:
$ref: '#/components/schemas/WebRTCSession'
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
- /v2/webrtcsessions/kick/{id}:
+ /v3/webrtcsessions/kick/{id}:
post:
operationId: webrtcSessionsKick
summary: kicks out a WebRTC session from the server.
@@ -1399,7 +1926,19 @@ paths:
description: the request was successful.
'400':
description: invalid request.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'404':
description: session not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
'500':
- description: internal server error.
+ description: server error.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
diff --git a/bench/proxy/start.sh b/bench/proxy/start.sh
index 2678b580fef..2afe40eeb34 100644
--- a/bench/proxy/start.sh
+++ b/bench/proxy/start.sh
@@ -15,7 +15,7 @@ CONF="${CONF}rtspAddress: :8555\n"
CONF="${CONF}rtpAddress: :8002\n"
CONF="${CONF}rtcpAddress: :8003\n"
CONF="${CONF}paths:\n"
-CONF="${CONF} all:\n"
+CONF="${CONF} all_others:\n"
echo -e "$CONF" > /source.conf
/mediamtx /source.conf &
@@ -40,7 +40,7 @@ CONF="${CONF}paths:\n"
for i in $(seq 1 $PROXY_COUNT); do
CONF="${CONF} proxy$i:\n"
CONF="${CONF} source: rtsp://localhost:8555/source\n"
- CONF="${CONF} sourceProtocol: $PROXY_PROTOCOL\n"
+ CONF="${CONF} rtspTransport: $PROXY_PROTOCOL\n"
done
echo -e "$CONF" > /proxy.conf
diff --git a/bench/publish/start.sh b/bench/publish/start.sh
index 0e1619d7fac..834cd76c301 100644
--- a/bench/publish/start.sh
+++ b/bench/publish/start.sh
@@ -9,7 +9,7 @@ PUBLISHER_PROTOCOL=tcp
CONF=""
CONF="${CONF}pprof: yes\n"
CONF="${CONF}paths:\n"
-CONF="${CONF} all:\n"
+CONF="${CONF} all_others:\n"
echo -e "$CONF" > /source.conf
/mediamtx /source.conf &
diff --git a/bench/read/start.sh b/bench/read/start.sh
index 9261bd3e6a9..514058231a0 100644
--- a/bench/read/start.sh
+++ b/bench/read/start.sh
@@ -9,7 +9,7 @@ READER_PROTOCOL=tcp
CONF=""
CONF="${CONF}pprof: yes\n"
CONF="${CONF}paths:\n"
-CONF="${CONF} all:\n"
+CONF="${CONF} all_others:\n"
echo -e "$CONF" > /source.conf
/mediamtx /source.conf &
diff --git a/go.mod b/go.mod
index 978256acee0..910e5ff5005 100644
--- a/go.mod
+++ b/go.mod
@@ -1,32 +1,33 @@
module github.com/bluenviron/mediamtx
-go 1.20
+go 1.21
require (
code.cloudfoundry.org/bytefmt v0.0.0
- github.com/abema/go-mp4 v1.1.0
- github.com/alecthomas/kong v0.8.0
- github.com/aler9/writerseeker v1.1.0
- github.com/bluenviron/gohlslib v1.0.3
- github.com/bluenviron/gortsplib/v4 v4.2.0
- github.com/bluenviron/mediacommon v1.4.1-0.20230924203439-7ac007e2ac2d
- github.com/datarhei/gosrt v0.5.4
- github.com/fsnotify/fsnotify v1.6.0
+ github.com/abema/go-mp4 v1.2.0
+ github.com/alecthomas/kong v0.8.1
+ github.com/bluenviron/gohlslib v1.2.1
+ github.com/bluenviron/gortsplib/v4 v4.7.1
+ github.com/bluenviron/mediacommon v1.9.0
+ github.com/datarhei/gosrt v0.5.7
+ github.com/fsnotify/fsnotify v1.7.0
github.com/gin-gonic/gin v1.9.1
- github.com/google/uuid v1.3.1
+ github.com/google/uuid v1.5.0
github.com/gookit/color v1.5.4
- github.com/gorilla/websocket v1.5.0
+ github.com/gorilla/websocket v1.5.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
+ github.com/matthewhartstonge/argon2 v1.0.0
github.com/notedit/rtmp v0.0.2
github.com/pion/ice/v2 v2.3.11
- github.com/pion/interceptor v0.1.20
- github.com/pion/rtcp v1.2.10
- github.com/pion/rtp v1.8.2
+ github.com/pion/interceptor v0.1.25
+ github.com/pion/logging v0.2.2
+ github.com/pion/rtcp v1.2.13
+ github.com/pion/rtp v1.8.3
github.com/pion/sdp/v3 v3.0.6
- github.com/pion/webrtc/v3 v3.2.21
+ github.com/pion/webrtc/v3 v3.2.22
github.com/stretchr/testify v1.8.4
- golang.org/x/crypto v0.13.0
- golang.org/x/term v0.12.0
+ golang.org/x/crypto v0.18.0
+ golang.org/x/term v0.16.0
gopkg.in/yaml.v2 v2.4.0
)
@@ -52,11 +53,10 @@ require (
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
- github.com/pion/logging v0.2.2 // indirect
- github.com/pion/mdns v0.0.8 // indirect
+ github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.8 // indirect
- github.com/pion/srtp/v2 v2.0.17 // 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/turn/v2 v2.1.3 // indirect
@@ -65,11 +65,17 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/arch v0.3.0 // indirect
- golang.org/x/net v0.15.0 // indirect
- golang.org/x/sys v0.12.0 // indirect
- golang.org/x/text v0.13.0 // indirect
+ golang.org/x/net v0.20.0 // indirect
+ golang.org/x/sys v0.16.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5
+
+replace github.com/pion/sdp/v3 => github.com/aler9/sdp/v3 v3.0.0-20231022165400-33437e07f326
+
+replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1
+
+replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6
diff --git a/go.sum b/go.sum
index df46e2e0cde..51e690b74ef 100644
--- a/go.sum
+++ b/go.sum
@@ -1,23 +1,29 @@
-github.com/abema/go-mp4 v1.1.0 h1:wr2uc6ENLtYNw/nRmjIYkAc6ZuAUe88FAUYAeZhcAgE=
-github.com/abema/go-mp4 v1.1.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
+github.com/abema/go-mp4 v1.2.0 h1:gi4X8xg/m179N/J15Fn5ugywN9vtI6PLk6iLldHGLAk=
+github.com/abema/go-mp4 v1.2.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
-github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s=
-github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
+github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
+github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
+github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
-github.com/aler9/writerseeker v1.1.0 h1:t+Sm3tjp8scNlqyoa8obpeqwciMNOvdvsxjxEb3Sx3g=
-github.com/aler9/writerseeker v1.1.0/go.mod h1:QNCcjSKnLsYoTfMmXkEEfgbz6nNXWxKSaBY+hGJGWDA=
+github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
+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/sdp/v3 v3.0.0-20231022165400-33437e07f326 h1:HA7u47vkcxFiHtiOjm8srh1JRgC0ZPYefPtpDCaTtS0=
+github.com/aler9/sdp/v3 v3.0.0-20231022165400-33437e07f326/go.mod h1:I40uD/ZSmK2peI6AdJga5fd55d4bFK0oWOgLS9Q8sVc=
+github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 h1:wMd3D1mLghoYYh31STig8Kwm2qi8QyQKUy09qUUZrVw=
+github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
-github.com/bluenviron/gohlslib v1.0.3 h1:FMHevlIrrZ67uzCXmlTSGflsfYREEtHb8L9BDyf7lJc=
-github.com/bluenviron/gohlslib v1.0.3/go.mod h1:R/aIsSxLI61N0CVMjtcHqJouK6+Ddd5YIihcCr7IFIw=
-github.com/bluenviron/gortsplib/v4 v4.2.0 h1:EbIMqkFxFo/iG5Hkld+Flew9R8ORKnuxlgUyFdpd5Rk=
-github.com/bluenviron/gortsplib/v4 v4.2.0/go.mod h1:wz9d4Tn2qS/mexc+BnvNeWzlNOpyaHzNK6SXxtg4mfM=
-github.com/bluenviron/mediacommon v1.4.1-0.20230924203439-7ac007e2ac2d h1:VbzIg0t5HKfyLbuzWeNU64JdOtTUp981Fx9ljdMRGpM=
-github.com/bluenviron/mediacommon v1.4.1-0.20230924203439-7ac007e2ac2d/go.mod h1:/vlOVSebDwzdRtQONOKLua0fOSJg1tUDHpP+h9a0uqM=
+github.com/bluenviron/gohlslib v1.2.1 h1:IuXuPOyMWjUFTpfC5GDZkmatFDnq/zlFcgSe1tEx5wQ=
+github.com/bluenviron/gohlslib v1.2.1/go.mod h1:TEVMn5iOPrS//mB/H9e08QkTvR6uS5c4LVeXFrfqSE4=
+github.com/bluenviron/gortsplib/v4 v4.7.1 h1:ZiPHjnIsdPDfPGZgfBr2n2xCFZlvmc/5zEqdoJUa1vU=
+github.com/bluenviron/gortsplib/v4 v4.7.1/go.mod h1:3+IYh85PgIPLHr4D5z7GnRvpu/ogSHMDhsYW/CjrD8E=
+github.com/bluenviron/mediacommon v1.9.0 h1:0I7PuwaDD6uOeQlV3WOlC/7FFESDa4dllYylj1YcnI4=
+github.com/bluenviron/mediacommon v1.9.0/go.mod h1:lt8V+wMyPw8C69HAqDWV5tsAwzN9u2Z+ca8B6C//+n0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -27,15 +33,15 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5 h1:xB7KkA98BcUdzVcwyZxb5R0FGIHxNPHgZOzkjPEY5gM=
github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5/go.mod h1:v4VVB6oBMz/c9fRY6vZrwr5xKRWOH5NPDjQZlPk0Gbs=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
-github.com/datarhei/gosrt v0.5.4 h1:dE3mmSB+n1GeviGM8xQAW3+UD3mKeFmd84iefDul5Vs=
-github.com/datarhei/gosrt v0.5.4/go.mod h1:MiUCwCG+LzFMzLM/kTA+3wiTtlnkVvGbW/F0XzyhtG8=
+github.com/datarhei/gosrt v0.5.7 h1:1COeDgF0D0v0poWu0yKDC72d29x16Ma6VFR1icx+3Xc=
+github.com/datarhei/gosrt v0.5.7/go.mod h1:ZicbsY9T2rXtWgQVBTR9ilnEkSYVSIb36hG9Lj7XCKM=
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=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -43,6 +49,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -68,13 +75,15 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
-github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.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.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -91,6 +100,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/matthewhartstonge/argon2 v1.0.0 h1:e65fkae6O8Na6YTy2HAccUbXR+GQHOnpQxeWGqWCRIw=
+github.com/matthewhartstonge/argon2 v1.0.0/go.mod h1:Fm4FHZxdxCM6hg21Jkz3YZVKnU7VnTlqDQ3ghS/Myok=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -121,43 +132,40 @@ 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/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
-github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
-github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
-github.com/pion/interceptor v0.1.20 h1:gORAnvlXu1f4Bx+TcXe8UJ37Jqb/tkNQ6E83NNqYZh0=
-github.com/pion/interceptor v0.1.20/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
+github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
+github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
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.8 h1:HhicWIg7OX5PVilyBO6plhMetInbzkVJAhbdJiAeVaI=
-github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
+github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
+github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
+github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
-github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
-github.com/pion/rtp v1.8.2 h1:oKMM0K1/QYQ5b5qH+ikqDSZRipP5mIxPJcgcvw5sH0w=
+github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
+github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
+github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
+github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
+github.com/pion/rtp v1.8.3/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 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
-github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
-github.com/pion/srtp/v2 v2.0.17 h1:ECuOk+7uIpY6HUlTb0nXhfvu4REG2hjtC4ronYFCZE4=
-github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
+github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
+github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
+github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
-github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
+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/pion/webrtc/v3 v3.2.21 h1:c8fy5JcqJkAQBwwy3Sk9huQLTBUSqaggyRlv9Lnh2zY=
-github.com/pion/webrtc/v3 v3.2.21/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
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=
@@ -194,10 +202,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
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 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
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=
@@ -210,14 +218,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.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 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
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=
@@ -239,41 +248,43 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/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 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.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 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
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 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
diff --git a/internal/api/api.go b/internal/api/api.go
new file mode 100644
index 00000000000..ee47c122f48
--- /dev/null
+++ b/internal/api/api.go
@@ -0,0 +1,1058 @@
+// Package api contains the API server.
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "reflect"
+ "sort"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/google/uuid"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
+ "github.com/bluenviron/mediamtx/internal/restrictnetwork"
+ "github.com/bluenviron/mediamtx/internal/servers/hls"
+ "github.com/bluenviron/mediamtx/internal/servers/rtmp"
+ "github.com/bluenviron/mediamtx/internal/servers/rtsp"
+ "github.com/bluenviron/mediamtx/internal/servers/srt"
+ "github.com/bluenviron/mediamtx/internal/servers/webrtc"
+)
+
+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
+ for name := range paths {
+ ret[i] = name
+ i++
+ }
+ sort.Strings(ret)
+ return ret
+}
+
+func paramName(ctx *gin.Context) (string, bool) {
+ name := ctx.Param("name")
+
+ if len(name) < 2 || name[0] != '/' {
+ return "", false
+ }
+
+ return name[1:], true
+}
+
+// PathManager contains methods used by the API and Metrics server.
+type PathManager interface {
+ APIPathsList() (*defs.APIPathList, error)
+ APIPathsGet(string) (*defs.APIPath, error)
+}
+
+// HLSServer contains methods used by the API and Metrics server.
+type HLSServer interface {
+ APIMuxersList() (*defs.APIHLSMuxerList, error)
+ APIMuxersGet(string) (*defs.APIHLSMuxer, error)
+}
+
+// RTSPServer contains methods used by the API and Metrics server.
+type RTSPServer interface {
+ APIConnsList() (*defs.APIRTSPConnsList, error)
+ APIConnsGet(uuid.UUID) (*defs.APIRTSPConn, error)
+ APISessionsList() (*defs.APIRTSPSessionList, error)
+ APISessionsGet(uuid.UUID) (*defs.APIRTSPSession, error)
+ APISessionsKick(uuid.UUID) error
+}
+
+// RTMPServer contains methods used by the API and Metrics server.
+type RTMPServer interface {
+ APIConnsList() (*defs.APIRTMPConnList, error)
+ APIConnsGet(uuid.UUID) (*defs.APIRTMPConn, error)
+ APIConnsKick(uuid.UUID) error
+}
+
+// SRTServer contains methods used by the API and Metrics server.
+type SRTServer interface {
+ APIConnsList() (*defs.APISRTConnList, error)
+ APIConnsGet(uuid.UUID) (*defs.APISRTConn, error)
+ APIConnsKick(uuid.UUID) error
+}
+
+// WebRTCServer contains methods used by the API and Metrics server.
+type WebRTCServer interface {
+ APISessionsList() (*defs.APIWebRTCSessionList, error)
+ APISessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error)
+ APISessionsKick(uuid.UUID) error
+}
+
+type apiParent interface {
+ logger.Writer
+ APIConfigSet(conf *conf.Conf)
+}
+
+// API is an API server.
+type API struct {
+ Address string
+ ReadTimeout conf.StringDuration
+ Conf *conf.Conf
+ PathManager PathManager
+ RTSPServer RTSPServer
+ RTSPSServer RTSPServer
+ RTMPServer RTMPServer
+ RTMPSServer RTMPServer
+ HLSServer HLSServer
+ WebRTCServer WebRTCServer
+ SRTServer SRTServer
+ Parent apiParent
+
+ httpServer *httpserv.WrappedServer
+ mutex sync.Mutex
+}
+
+// Initialize initializes API.
+func (a *API) Initialize() error {
+ router := gin.New()
+ router.SetTrustedProxies(nil) //nolint:errcheck
+
+ group := router.Group("/")
+
+ group.GET("/v3/config/global/get", a.onConfigGlobalGet)
+ group.PATCH("/v3/config/global/patch", a.onConfigGlobalPatch)
+
+ group.GET("/v3/config/pathdefaults/get", a.onConfigPathDefaultsGet)
+ group.PATCH("/v3/config/pathdefaults/patch", a.onConfigPathDefaultsPatch)
+
+ group.GET("/v3/config/paths/list", a.onConfigPathsList)
+ group.GET("/v3/config/paths/get/*name", a.onConfigPathsGet)
+ group.POST("/v3/config/paths/add/*name", a.onConfigPathsAdd)
+ group.PATCH("/v3/config/paths/patch/*name", a.onConfigPathsPatch)
+ group.POST("/v3/config/paths/replace/*name", a.onConfigPathsReplace)
+ group.DELETE("/v3/config/paths/delete/*name", a.onConfigPathsDelete)
+
+ group.GET("/v3/paths/list", a.onPathsList)
+ group.GET("/v3/paths/get/*name", a.onPathsGet)
+
+ if !interfaceIsEmpty(a.HLSServer) {
+ group.GET("/v3/hlsmuxers/list", a.onHLSMuxersList)
+ group.GET("/v3/hlsmuxers/get/*name", a.onHLSMuxersGet)
+ }
+
+ if !interfaceIsEmpty(a.RTSPServer) {
+ group.GET("/v3/rtspconns/list", a.onRTSPConnsList)
+ group.GET("/v3/rtspconns/get/:id", a.onRTSPConnsGet)
+ group.GET("/v3/rtspsessions/list", a.onRTSPSessionsList)
+ group.GET("/v3/rtspsessions/get/:id", a.onRTSPSessionsGet)
+ group.POST("/v3/rtspsessions/kick/:id", a.onRTSPSessionsKick)
+ }
+
+ if !interfaceIsEmpty(a.RTSPSServer) {
+ group.GET("/v3/rtspsconns/list", a.onRTSPSConnsList)
+ group.GET("/v3/rtspsconns/get/:id", a.onRTSPSConnsGet)
+ group.GET("/v3/rtspssessions/list", a.onRTSPSSessionsList)
+ group.GET("/v3/rtspssessions/get/:id", a.onRTSPSSessionsGet)
+ group.POST("/v3/rtspssessions/kick/:id", a.onRTSPSSessionsKick)
+ }
+
+ if !interfaceIsEmpty(a.RTMPServer) {
+ group.GET("/v3/rtmpconns/list", a.onRTMPConnsList)
+ group.GET("/v3/rtmpconns/get/:id", a.onRTMPConnsGet)
+ group.POST("/v3/rtmpconns/kick/:id", a.onRTMPConnsKick)
+ }
+
+ if !interfaceIsEmpty(a.RTMPSServer) {
+ group.GET("/v3/rtmpsconns/list", a.onRTMPSConnsList)
+ group.GET("/v3/rtmpsconns/get/:id", a.onRTMPSConnsGet)
+ group.POST("/v3/rtmpsconns/kick/:id", a.onRTMPSConnsKick)
+ }
+
+ if !interfaceIsEmpty(a.WebRTCServer) {
+ group.GET("/v3/webrtcsessions/list", a.onWebRTCSessionsList)
+ group.GET("/v3/webrtcsessions/get/:id", a.onWebRTCSessionsGet)
+ group.POST("/v3/webrtcsessions/kick/:id", a.onWebRTCSessionsKick)
+ }
+
+ if !interfaceIsEmpty(a.SRTServer) {
+ group.GET("/v3/srtconns/list", a.onSRTConnsList)
+ group.GET("/v3/srtconns/get/:id", a.onSRTConnsGet)
+ group.POST("/v3/srtconns/kick/:id", a.onSRTConnsKick)
+ }
+
+ network, address := restrictnetwork.Restrict("tcp", a.Address)
+
+ var err error
+ a.httpServer, err = httpserv.NewWrappedServer(
+ network,
+ address,
+ time.Duration(a.ReadTimeout),
+ "",
+ "",
+ router,
+ a,
+ )
+ if err != nil {
+ return err
+ }
+
+ a.Log(logger.Info, "listener opened on "+address)
+
+ return nil
+}
+
+// Close closes the API.
+func (a *API) Close() {
+ a.Log(logger.Info, "listener is closing")
+ a.httpServer.Close()
+}
+
+// Log implements logger.Writer.
+func (a *API) Log(level logger.Level, format string, args ...interface{}) {
+ a.Parent.Log(level, "[API] "+format, args...)
+}
+
+func (a *API) writeError(ctx *gin.Context, status int, err error) {
+ // show error in logs
+ a.Log(logger.Error, err.Error())
+
+ // add error to response
+ ctx.JSON(status, &defs.APIError{
+ Error: err.Error(),
+ })
+}
+
+func (a *API) onConfigGlobalGet(ctx *gin.Context) {
+ a.mutex.Lock()
+ c := a.Conf
+ a.mutex.Unlock()
+
+ ctx.JSON(http.StatusOK, c.Global())
+}
+
+func (a *API) onConfigGlobalPatch(ctx *gin.Context) {
+ var c conf.OptionalGlobal
+ err := json.NewDecoder(ctx.Request.Body).Decode(&c)
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ newConf := a.Conf.Clone()
+
+ newConf.PatchGlobal(&c)
+
+ err = newConf.Validate()
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.Conf = newConf
+
+ // since reloading the configuration can cause the shutdown of the API,
+ // call it in a goroutine
+ go a.Parent.APIConfigSet(newConf)
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onConfigPathDefaultsGet(ctx *gin.Context) {
+ a.mutex.Lock()
+ c := a.Conf
+ a.mutex.Unlock()
+
+ ctx.JSON(http.StatusOK, c.PathDefaults)
+}
+
+func (a *API) onConfigPathDefaultsPatch(ctx *gin.Context) {
+ var p conf.OptionalPath
+ err := json.NewDecoder(ctx.Request.Body).Decode(&p)
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ newConf := a.Conf.Clone()
+
+ newConf.PatchPathDefaults(&p)
+
+ err = newConf.Validate()
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.Conf = newConf
+ a.Parent.APIConfigSet(newConf)
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onConfigPathsList(ctx *gin.Context) {
+ a.mutex.Lock()
+ c := a.Conf
+ a.mutex.Unlock()
+
+ data := &defs.APIPathConfList{
+ Items: make([]*conf.Path, len(c.Paths)),
+ }
+
+ for i, key := range sortedKeys(c.Paths) {
+ data.Items[i] = c.Paths[key]
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onConfigPathsGet(ctx *gin.Context) {
+ name, ok := paramName(ctx)
+ if !ok {
+ a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
+ return
+ }
+
+ a.mutex.Lock()
+ c := a.Conf
+ a.mutex.Unlock()
+
+ p, ok := c.Paths[name]
+ if !ok {
+ a.writeError(ctx, http.StatusNotFound, fmt.Errorf("path configuration not found"))
+ return
+ }
+
+ ctx.JSON(http.StatusOK, p)
+}
+
+func (a *API) onConfigPathsAdd(ctx *gin.Context) { //nolint:dupl
+ name, ok := paramName(ctx)
+ if !ok {
+ a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
+ return
+ }
+
+ var p conf.OptionalPath
+ err := json.NewDecoder(ctx.Request.Body).Decode(&p)
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ newConf := a.Conf.Clone()
+
+ err = newConf.AddPath(name, &p)
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ err = newConf.Validate()
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.Conf = newConf
+ a.Parent.APIConfigSet(newConf)
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onConfigPathsPatch(ctx *gin.Context) { //nolint:dupl
+ name, ok := paramName(ctx)
+ if !ok {
+ a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
+ return
+ }
+
+ var p conf.OptionalPath
+ err := json.NewDecoder(ctx.Request.Body).Decode(&p)
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ newConf := a.Conf.Clone()
+
+ err = newConf.PatchPath(name, &p)
+ if err != nil {
+ if errors.Is(err, conf.ErrPathNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ }
+ return
+ }
+
+ err = newConf.Validate()
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.Conf = newConf
+ a.Parent.APIConfigSet(newConf)
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onConfigPathsReplace(ctx *gin.Context) { //nolint:dupl
+ name, ok := paramName(ctx)
+ if !ok {
+ a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
+ return
+ }
+
+ var p conf.OptionalPath
+ err := json.NewDecoder(ctx.Request.Body).Decode(&p)
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ newConf := a.Conf.Clone()
+
+ err = newConf.ReplacePath(name, &p)
+ if err != nil {
+ if errors.Is(err, conf.ErrPathNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ }
+ return
+ }
+
+ err = newConf.Validate()
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.Conf = newConf
+ a.Parent.APIConfigSet(newConf)
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onConfigPathsDelete(ctx *gin.Context) {
+ name, ok := paramName(ctx)
+ if !ok {
+ a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
+ return
+ }
+
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+
+ newConf := a.Conf.Clone()
+
+ err := newConf.RemovePath(name)
+ if err != nil {
+ if errors.Is(err, conf.ErrPathNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ }
+ return
+ }
+
+ err = newConf.Validate()
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ a.Conf = newConf
+ a.Parent.APIConfigSet(newConf)
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onPathsList(ctx *gin.Context) {
+ data, err := a.PathManager.APIPathsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onPathsGet(ctx *gin.Context) {
+ name, ok := paramName(ctx)
+ if !ok {
+ a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
+ return
+ }
+
+ data, err := a.PathManager.APIPathsGet(name)
+ if err != nil {
+ if errors.Is(err, conf.ErrPathNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPConnsList(ctx *gin.Context) {
+ data, err := a.RTSPServer.APIConnsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPConnsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.RTSPServer.APIConnsGet(uuid)
+ if err != nil {
+ if errors.Is(err, rtsp.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPSessionsList(ctx *gin.Context) {
+ data, err := a.RTSPServer.APISessionsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPSessionsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.RTSPServer.APISessionsGet(uuid)
+ if err != nil {
+ if errors.Is(err, rtsp.ErrSessionNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPSessionsKick(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ err = a.RTSPServer.APISessionsKick(uuid)
+ if err != nil {
+ if errors.Is(err, rtsp.ErrSessionNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onRTSPSConnsList(ctx *gin.Context) {
+ data, err := a.RTSPSServer.APIConnsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPSConnsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.RTSPSServer.APIConnsGet(uuid)
+ if err != nil {
+ if errors.Is(err, rtsp.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPSSessionsList(ctx *gin.Context) {
+ data, err := a.RTSPSServer.APISessionsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPSSessionsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.RTSPSServer.APISessionsGet(uuid)
+ if err != nil {
+ if errors.Is(err, rtsp.ErrSessionNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTSPSSessionsKick(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ err = a.RTSPSServer.APISessionsKick(uuid)
+ if err != nil {
+ if errors.Is(err, rtsp.ErrSessionNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onRTMPConnsList(ctx *gin.Context) {
+ data, err := a.RTMPServer.APIConnsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTMPConnsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.RTMPServer.APIConnsGet(uuid)
+ if err != nil {
+ if errors.Is(err, rtmp.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTMPConnsKick(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ err = a.RTMPServer.APIConnsKick(uuid)
+ if err != nil {
+ if errors.Is(err, rtmp.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onRTMPSConnsList(ctx *gin.Context) {
+ data, err := a.RTMPSServer.APIConnsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTMPSConnsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.RTMPSServer.APIConnsGet(uuid)
+ if err != nil {
+ if errors.Is(err, rtmp.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onRTMPSConnsKick(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ err = a.RTMPSServer.APIConnsKick(uuid)
+ if err != nil {
+ if errors.Is(err, rtmp.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onHLSMuxersList(ctx *gin.Context) {
+ data, err := a.HLSServer.APIMuxersList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onHLSMuxersGet(ctx *gin.Context) {
+ name, ok := paramName(ctx)
+ if !ok {
+ a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
+ return
+ }
+
+ data, err := a.HLSServer.APIMuxersGet(name)
+ if err != nil {
+ if errors.Is(err, hls.ErrMuxerNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onWebRTCSessionsList(ctx *gin.Context) {
+ data, err := a.WebRTCServer.APISessionsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onWebRTCSessionsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.WebRTCServer.APISessionsGet(uuid)
+ if err != nil {
+ if errors.Is(err, webrtc.ErrSessionNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onWebRTCSessionsKick(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ err = a.WebRTCServer.APISessionsKick(uuid)
+ if err != nil {
+ if errors.Is(err, webrtc.ErrSessionNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+func (a *API) onSRTConnsList(ctx *gin.Context) {
+ data, err := a.SRTServer.APIConnsList()
+ if err != nil {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ data.ItemCount = len(data.Items)
+ pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ data.PageCount = pageCount
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onSRTConnsGet(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ data, err := a.SRTServer.APIConnsGet(uuid)
+ if err != nil {
+ if errors.Is(err, srt.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, data)
+}
+
+func (a *API) onSRTConnsKick(ctx *gin.Context) {
+ uuid, err := uuid.Parse(ctx.Param("id"))
+ if err != nil {
+ a.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ err = a.SRTServer.APIConnsKick(uuid)
+ if err != nil {
+ if errors.Is(err, srt.ErrConnNotFound) {
+ a.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ a.writeError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+// ReloadConf is called by core.
+func (a *API) ReloadConf(conf *conf.Conf) {
+ a.mutex.Lock()
+ defer a.mutex.Unlock()
+ a.Conf = conf
+}
diff --git a/internal/api/api_test.go b/internal/api/api_test.go
new file mode 100644
index 00000000000..ddc656f3d00
--- /dev/null
+++ b/internal/api/api_test.go
@@ -0,0 +1,39 @@
+package api
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestPaginate(t *testing.T) {
+ 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)
+
+ 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)
+
+ 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)
+}
diff --git a/internal/conf/auth_method.go b/internal/conf/auth_method.go
index 353764d0931..e759fc4d7f8 100644
--- a/internal/conf/auth_method.go
+++ b/internal/conf/auth_method.go
@@ -59,8 +59,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *AuthMethods) UnmarshalEnv(s string) error {
- byts, _ := json.Marshal(strings.Split(s, ","))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *AuthMethods) UnmarshalEnv(_ string, v string) error {
+ byts, _ := json.Marshal(strings.Split(v, ","))
return d.UnmarshalJSON(byts)
}
diff --git a/internal/conf/conf.go b/internal/conf/conf.go
index 6691df6210e..30e3df54fab 100644
--- a/internal/conf/conf.go
+++ b/internal/conf/conf.go
@@ -4,8 +4,10 @@ package conf
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"os"
+ "reflect"
"sort"
"strings"
"time"
@@ -20,7 +22,10 @@ import (
"github.com/bluenviron/mediamtx/internal/logger"
)
-func getSortedKeys(paths map[string]*PathConf) []string {
+// ErrPathNotFound is returned when a path is not found.
+var ErrPathNotFound = errors.New("path not found")
+
+func sortedKeys(paths map[string]*OptionalPath) []string {
ret := make([]string, len(paths))
i := 0
for name := range paths {
@@ -41,45 +46,6 @@ func firstThatExists(paths []string) string {
return ""
}
-func loadFromFile(fpath string, defaultConfPaths []string, conf *Conf) (string, error) {
- if fpath == "" {
- fpath = firstThatExists(defaultConfPaths)
-
- // when the configuration file is not explicitly set,
- // it is optional. Load defaults.
- if fpath == "" {
- conf.UnmarshalJSON(nil) //nolint:errcheck
- return "", nil
- }
- }
-
- byts, err := os.ReadFile(fpath)
- if err != nil {
- return "", err
- }
-
- if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format
- byts, err = decrypt.Decrypt(key, byts)
- if err != nil {
- return "", err
- }
- }
-
- if key, ok := os.LookupEnv("MTX_CONFKEY"); ok {
- byts, err = decrypt.Decrypt(key, byts)
- if err != nil {
- return "", err
- }
- }
-
- err = yaml.Load(byts, conf)
- if err != nil {
- return "", err
- }
-
- return fpath, nil
-}
-
func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
for _, i := range list {
if i == item {
@@ -89,6 +55,33 @@ func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
return false
}
+func copyStructFields(dest interface{}, source interface{}) {
+ rvsource := reflect.ValueOf(source).Elem()
+ rvdest := reflect.ValueOf(dest)
+ nf := rvsource.NumField()
+ var zero reflect.Value
+
+ for i := 0; i < nf; i++ {
+ fnew := rvsource.Field(i)
+ f := rvdest.Elem().FieldByName(rvsource.Type().Field(i).Name)
+ if f == zero {
+ continue
+ }
+
+ if fnew.Kind() == reflect.Pointer {
+ if !fnew.IsNil() {
+ if f.Kind() == reflect.Ptr {
+ f.Set(fnew)
+ } else {
+ f.Set(fnew.Elem())
+ }
+ }
+ } else {
+ f.Set(fnew)
+ }
+ }
+}
+
// Conf is a configuration.
type Conf struct {
// General
@@ -97,12 +90,10 @@ type Conf struct {
LogFile string `json:"logFile"`
ReadTimeout StringDuration `json:"readTimeout"`
WriteTimeout StringDuration `json:"writeTimeout"`
- ReadBufferCount int `json:"readBufferCount"` // deprecated
+ ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated
WriteQueueSize int `json:"writeQueueSize"`
UDPMaxPayloadSize int `json:"udpMaxPayloadSize"`
ExternalAuthenticationURL string `json:"externalAuthenticationURL"`
- API bool `json:"api"`
- APIAddress string `json:"apiAddress"`
Metrics bool `json:"metrics"`
MetricsAddress string `json:"metricsAddress"`
PPROF bool `json:"pprof"`
@@ -111,9 +102,17 @@ type Conf struct {
RunOnConnectRestart bool `json:"runOnConnectRestart"`
RunOnDisconnect string `json:"runOnDisconnect"`
- // RTSP
+ // API
+ API bool `json:"api"`
+ APIAddress string `json:"apiAddress"`
+
+ // Playback
+ Playback bool `json:"playback"`
+ PlaybackAddress string `json:"playbackAddress"`
+
+ // RTSP server
RTSP bool `json:"rtsp"`
- RTSPDisable bool `json:"rtspDisable"` // deprecated
+ RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated
Protocols Protocols `json:"protocols"`
Encryption Encryption `json:"encryption"`
RTSPAddress string `json:"rtspAddress"`
@@ -127,18 +126,18 @@ type Conf struct {
ServerCert string `json:"serverCert"`
AuthMethods AuthMethods `json:"authMethods"`
- // RTMP
+ // RTMP server
RTMP bool `json:"rtmp"`
- RTMPDisable bool `json:"rtmpDisable"` // deprecated
+ RTMPDisable *bool `json:"rtmpDisable,omitempty"` // deprecated
RTMPAddress string `json:"rtmpAddress"`
RTMPEncryption Encryption `json:"rtmpEncryption"`
RTMPSAddress string `json:"rtmpsAddress"`
RTMPServerKey string `json:"rtmpServerKey"`
RTMPServerCert string `json:"rtmpServerCert"`
- // HLS
+ // HLS server
HLS bool `json:"hls"`
- HLSDisable bool `json:"hlsDisable"` // depreacted
+ HLSDisable *bool `json:"hlsDisable,omitempty"` // depreacted
HLSAddress string `json:"hlsAddress"`
HLSEncryption bool `json:"hlsEncryption"`
HLSServerKey string `json:"hlsServerKey"`
@@ -153,42 +152,125 @@ type Conf struct {
HLSTrustedProxies IPsOrCIDRs `json:"hlsTrustedProxies"`
HLSDirectory string `json:"hlsDirectory"`
- // WebRTC
- WebRTC bool `json:"webrtc"`
- WebRTCDisable bool `json:"webrtcDisable"` // deprecated
- WebRTCAddress string `json:"webrtcAddress"`
- WebRTCEncryption bool `json:"webrtcEncryption"`
- WebRTCServerKey string `json:"webrtcServerKey"`
- WebRTCServerCert string `json:"webrtcServerCert"`
- WebRTCAllowOrigin string `json:"webrtcAllowOrigin"`
- WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"`
- WebRTCICEServers []string `json:"webrtcICEServers"` // deprecated
- WebRTCICEServers2 []WebRTCICEServer `json:"webrtcICEServers2"`
- WebRTCICEHostNAT1To1IPs []string `json:"webrtcICEHostNAT1To1IPs"`
- WebRTCICEUDPMuxAddress string `json:"webrtcICEUDPMuxAddress"`
- WebRTCICETCPMuxAddress string `json:"webrtcICETCPMuxAddress"`
-
- // SRT
+ // WebRTC server
+ WebRTC bool `json:"webrtc"`
+ WebRTCDisable *bool `json:"webrtcDisable,omitempty"` // deprecated
+ WebRTCAddress string `json:"webrtcAddress"`
+ WebRTCEncryption bool `json:"webrtcEncryption"`
+ WebRTCServerKey string `json:"webrtcServerKey"`
+ WebRTCServerCert string `json:"webrtcServerCert"`
+ WebRTCAllowOrigin string `json:"webrtcAllowOrigin"`
+ WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"`
+ WebRTCLocalUDPAddress string `json:"webrtcLocalUDPAddress"`
+ WebRTCLocalTCPAddress string `json:"webrtcLocalTCPAddress"`
+ WebRTCIPsFromInterfaces bool `json:"webrtcIPsFromInterfaces"`
+ WebRTCIPsFromInterfacesList []string `json:"webrtcIPsFromInterfacesList"`
+ WebRTCAdditionalHosts []string `json:"webrtcAdditionalHosts"`
+ WebRTCICEServers2 []WebRTCICEServer `json:"webrtcICEServers2"`
+ WebRTCICEUDPMuxAddress *string `json:"webrtcICEUDPMuxAddress,omitempty"` // deprecated
+ WebRTCICETCPMuxAddress *string `json:"webrtcICETCPMuxAddress,omitempty"` // deprecated
+ WebRTCICEHostNAT1To1IPs *[]string `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated
+ WebRTCICEServers *[]string `json:"webrtcICEServers,omitempty"` // deprecated
+
+ // SRT server
SRT bool `json:"srt"`
SRTAddress string `json:"srtAddress"`
- // Record
- Record bool `json:"record"`
- RecordPath string `json:"recordPath"`
- RecordFormat string `json:"recordFormat"`
- RecordPartDuration StringDuration `json:"recordPartDuration"`
- RecordSegmentDuration StringDuration `json:"recordSegmentDuration"`
- RecordDeleteAfter StringDuration `json:"recordDeleteAfter"`
+ // Record (deprecated)
+ Record *bool `json:"record,omitempty"` // deprecated
+ RecordPath *string `json:"recordPath,omitempty"` // deprecated
+ RecordFormat *RecordFormat `json:"recordFormat,omitempty"` // deprecated
+ RecordPartDuration *StringDuration `json:"recordPartDuration,omitempty"` // deprecated
+ RecordSegmentDuration *StringDuration `json:"recordSegmentDuration,omitempty"` // deprecated
+ RecordDeleteAfter *StringDuration `json:"recordDeleteAfter,omitempty"` // deprecated
+
+ // Path defaults
+ PathDefaults Path `json:"pathDefaults"`
// Paths
- Paths map[string]*PathConf `json:"paths"`
+ OptionalPaths map[string]*OptionalPath `json:"paths"`
+ Paths map[string]*Path `json:"-"` // filled by Check()
+}
+
+func (conf *Conf) setDefaults() {
+ // General
+ conf.LogLevel = LogLevel(logger.Info)
+ conf.LogDestinations = LogDestinations{logger.DestinationStdout}
+ conf.LogFile = "mediamtx.log"
+ conf.ReadTimeout = 10 * StringDuration(time.Second)
+ conf.WriteTimeout = 10 * StringDuration(time.Second)
+ conf.WriteQueueSize = 512
+ conf.UDPMaxPayloadSize = 1472
+ conf.MetricsAddress = "127.0.0.1:9998"
+ conf.PPROFAddress = "127.0.0.1:9999"
+
+ // API
+ conf.APIAddress = "127.0.0.1:9997"
+
+ // Playback server
+ conf.PlaybackAddress = ":9996"
+
+ // RTSP server
+ conf.RTSP = true
+ conf.Protocols = Protocols{
+ Protocol(gortsplib.TransportUDP): {},
+ Protocol(gortsplib.TransportUDPMulticast): {},
+ Protocol(gortsplib.TransportTCP): {},
+ }
+ conf.RTSPAddress = ":8554"
+ conf.RTSPSAddress = ":8322"
+ conf.RTPAddress = ":8000"
+ conf.RTCPAddress = ":8001"
+ conf.MulticastIPRange = "224.1.0.0/16"
+ conf.MulticastRTPPort = 8002
+ conf.MulticastRTCPPort = 8003
+ conf.ServerKey = "server.key"
+ conf.ServerCert = "server.crt"
+ conf.AuthMethods = AuthMethods{headers.AuthBasic}
+
+ // RTMP server
+ conf.RTMP = true
+ conf.RTMPAddress = ":1935"
+ conf.RTMPSAddress = ":1936"
+ conf.RTMPServerKey = "server.key"
+ conf.RTMPServerCert = "server.crt"
+
+ // HLS
+ conf.HLS = true
+ conf.HLSAddress = ":8888"
+ conf.HLSServerKey = "server.key"
+ conf.HLSServerCert = "server.crt"
+ conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency)
+ conf.HLSSegmentCount = 7
+ conf.HLSSegmentDuration = 1 * StringDuration(time.Second)
+ conf.HLSPartDuration = 200 * StringDuration(time.Millisecond)
+ conf.HLSSegmentMaxSize = 50 * 1024 * 1024
+ conf.HLSAllowOrigin = "*"
+
+ // WebRTC server
+ conf.WebRTC = true
+ conf.WebRTCAddress = ":8889"
+ conf.WebRTCServerKey = "server.key"
+ conf.WebRTCServerCert = "server.crt"
+ conf.WebRTCAllowOrigin = "*"
+ conf.WebRTCLocalUDPAddress = ":8189"
+ conf.WebRTCIPsFromInterfaces = true
+ conf.WebRTCIPsFromInterfacesList = []string{}
+ conf.WebRTCAdditionalHosts = []string{}
+ conf.WebRTCICEServers2 = []WebRTCICEServer{}
+
+ // SRT server
+ conf.SRT = true
+ conf.SRTAddress = ":8890"
+
+ conf.PathDefaults.setDefaults()
}
// Load loads a Conf.
func Load(fpath string, defaultConfPaths []string) (*Conf, string, error) {
conf := &Conf{}
- fpath, err := loadFromFile(fpath, defaultConfPaths, conf)
+ fpath, err := conf.loadFromFile(fpath, defaultConfPaths)
if err != nil {
return nil, "", err
}
@@ -203,7 +285,7 @@ func Load(fpath string, defaultConfPaths []string) (*Conf, string, error) {
return nil, "", err
}
- err = conf.Check()
+ err = conf.Validate()
if err != nil {
return nil, "", err
}
@@ -211,6 +293,45 @@ func Load(fpath string, defaultConfPaths []string) (*Conf, string, error) {
return conf, fpath, nil
}
+func (conf *Conf) loadFromFile(fpath string, defaultConfPaths []string) (string, error) {
+ if fpath == "" {
+ fpath = firstThatExists(defaultConfPaths)
+
+ // when the configuration file is not explicitly set,
+ // it is optional.
+ if fpath == "" {
+ conf.setDefaults()
+ return "", nil
+ }
+ }
+
+ byts, err := os.ReadFile(fpath)
+ if err != nil {
+ return "", err
+ }
+
+ if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format
+ byts, err = decrypt.Decrypt(key, byts)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ if key, ok := os.LookupEnv("MTX_CONFKEY"); ok {
+ byts, err = decrypt.Decrypt(key, byts)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ err = yaml.Load(byts, conf)
+ if err != nil {
+ return "", err
+ }
+
+ return fpath, nil
+}
+
// Clone clones the configuration.
func (conf Conf) Clone() *Conf {
enc, err := json.Marshal(conf)
@@ -227,12 +348,12 @@ func (conf Conf) Clone() *Conf {
return &dest
}
-// Check checks the configuration for errors.
-func (conf *Conf) Check() error {
+// Validate checks the configuration for errors.
+func (conf *Conf) Validate() error {
// General
- if conf.ReadBufferCount != 0 {
- conf.WriteQueueSize = conf.ReadBufferCount
+ if conf.ReadBufferCount != nil {
+ conf.WriteQueueSize = *conf.ReadBufferCount
}
if (conf.WriteQueueSize & (conf.WriteQueueSize - 1)) != 0 {
return fmt.Errorf("'writeQueueSize' must be a power of two")
@@ -253,8 +374,8 @@ func (conf *Conf) Check() error {
// RTSP
- if conf.RTSPDisable {
- conf.RTSP = false
+ if conf.RTSPDisable != nil {
+ conf.RTSP = !*conf.RTSPDisable
}
if conf.Encryption == EncryptionStrict {
if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok {
@@ -267,36 +388,46 @@ func (conf *Conf) Check() error {
// RTMP
- if conf.RTMPDisable {
- conf.RTMP = false
+ if conf.RTMPDisable != nil {
+ conf.RTMP = !*conf.RTMPDisable
}
// HLS
- if conf.HLSDisable {
- conf.HLS = false
+ if conf.HLSDisable != nil {
+ conf.HLS = !*conf.HLSDisable
}
// WebRTC
- if conf.WebRTCDisable {
- conf.WebRTC = false
- }
- for _, server := range conf.WebRTCICEServers {
- parts := strings.Split(server, ":")
- if len(parts) == 5 {
- conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
- URL: parts[0] + ":" + parts[3] + ":" + parts[4],
- Username: parts[1],
- Password: parts[2],
- })
- } else {
- conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
- URL: server,
- })
+ if conf.WebRTCDisable != nil {
+ conf.WebRTC = !*conf.WebRTCDisable
+ }
+ if conf.WebRTCICEUDPMuxAddress != nil {
+ conf.WebRTCLocalUDPAddress = *conf.WebRTCICEUDPMuxAddress
+ }
+ if conf.WebRTCICETCPMuxAddress != nil {
+ conf.WebRTCLocalTCPAddress = *conf.WebRTCICETCPMuxAddress
+ }
+ if conf.WebRTCICEHostNAT1To1IPs != nil {
+ conf.WebRTCAdditionalHosts = *conf.WebRTCICEHostNAT1To1IPs
+ }
+ if conf.WebRTCICEServers != nil {
+ for _, server := range *conf.WebRTCICEServers {
+ parts := strings.Split(server, ":")
+ if len(parts) == 5 {
+ conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
+ URL: parts[0] + ":" + parts[3] + ":" + parts[4],
+ Username: parts[1],
+ Password: parts[2],
+ })
+ } else {
+ conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
+ URL: server,
+ })
+ }
}
}
- conf.WebRTCICEServers = nil
for _, server := range conf.WebRTCICEServers2 {
if !strings.HasPrefix(server.URL, "stun:") &&
!strings.HasPrefix(server.URL, "turn:") &&
@@ -304,29 +435,62 @@ func (conf *Conf) Check() error {
return fmt.Errorf("invalid ICE server: '%s'", server.URL)
}
}
+ if conf.WebRTCLocalUDPAddress == "" &&
+ conf.WebRTCLocalTCPAddress == "" &&
+ len(conf.WebRTCICEServers2) == 0 {
+ return fmt.Errorf("at least one between 'webrtcLocalUDPAddress'," +
+ " 'webrtcLocalTCPAddress' or 'webrtcICEServers2' must be filled")
+ }
+ if conf.WebRTCLocalUDPAddress != "" || conf.WebRTCLocalTCPAddress != "" {
+ if !conf.WebRTCIPsFromInterfaces && len(conf.WebRTCAdditionalHosts) == 0 {
+ return fmt.Errorf("at least one between 'webrtcIPsFromInterfaces' or 'webrtcAdditionalHosts' must be filled")
+ }
+ }
- // Record
-
- if conf.RecordFormat != "fmp4" {
- return fmt.Errorf("unsupported record format '%s'", conf.RecordFormat)
+ // Record (deprecated)
+ if conf.Record != nil {
+ conf.PathDefaults.Record = *conf.Record
+ }
+ if conf.RecordPath != nil {
+ conf.PathDefaults.RecordPath = *conf.RecordPath
+ }
+ if conf.RecordFormat != nil {
+ conf.PathDefaults.RecordFormat = *conf.RecordFormat
+ }
+ if conf.RecordPartDuration != nil {
+ conf.PathDefaults.RecordPartDuration = *conf.RecordPartDuration
+ }
+ if conf.RecordSegmentDuration != nil {
+ conf.PathDefaults.RecordSegmentDuration = *conf.RecordSegmentDuration
+ }
+ if conf.RecordDeleteAfter != nil {
+ conf.PathDefaults.RecordDeleteAfter = *conf.RecordDeleteAfter
}
- // do not add automatically "all", since user may want to
- // initialize all paths through API or hot reloading.
- if conf.Paths == nil {
- conf.Paths = make(map[string]*PathConf)
+ hasAllOthers := false
+ for name := range conf.OptionalPaths {
+ if name == "all" || name == "all_others" || name == "~^.*$" {
+ if hasAllOthers {
+ return fmt.Errorf("all_others, all and '~^.*$' are aliases")
+ }
+ hasAllOthers = true
+ }
}
- for _, name := range getSortedKeys(conf.Paths) {
- pconf := conf.Paths[name]
- if pconf == nil {
- pconf = &PathConf{}
- // load defaults
- pconf.UnmarshalJSON(nil) //nolint:errcheck
- conf.Paths[name] = pconf
+ conf.Paths = make(map[string]*Path)
+
+ for _, name := range sortedKeys(conf.OptionalPaths) {
+ optional := conf.OptionalPaths[name]
+ if optional == nil {
+ optional = &OptionalPath{
+ Values: newOptionalPathValues(),
+ }
}
- err := pconf.check(conf, name)
+ pconf := newPath(&conf.PathDefaults, optional)
+ conf.Paths[name] = pconf
+
+ err := pconf.validate(conf, name)
if err != nil {
return err
}
@@ -335,81 +499,77 @@ func (conf *Conf) Check() error {
return nil
}
-// UnmarshalJSON implements json.Unmarshaler. It is used to:
-// - force DisallowUnknownFields
-// - set default values
+// UnmarshalJSON implements json.Unmarshaler.
func (conf *Conf) UnmarshalJSON(b []byte) error {
- // general
- conf.LogLevel = LogLevel(logger.Info)
- conf.LogDestinations = LogDestinations{logger.DestinationStdout}
- conf.LogFile = "mediamtx.log"
- conf.ReadTimeout = 10 * StringDuration(time.Second)
- conf.WriteTimeout = 10 * StringDuration(time.Second)
- conf.WriteQueueSize = 512
- conf.UDPMaxPayloadSize = 1472
- conf.APIAddress = "127.0.0.1:9997"
- conf.MetricsAddress = "127.0.0.1:9998"
- conf.PPROFAddress = "127.0.0.1:9999"
+ conf.setDefaults()
- // RTSP
- conf.RTSP = true
- conf.Protocols = Protocols{
- Protocol(gortsplib.TransportUDP): {},
- Protocol(gortsplib.TransportUDPMulticast): {},
- Protocol(gortsplib.TransportTCP): {},
+ type alias Conf
+ d := json.NewDecoder(bytes.NewReader(b))
+ d.DisallowUnknownFields()
+ return d.Decode((*alias)(conf))
+}
+
+// Global returns the global part of Conf.
+func (conf *Conf) Global() *Global {
+ g := &Global{
+ Values: newGlobalValues(),
}
- conf.RTSPAddress = ":8554"
- conf.RTSPSAddress = ":8322"
- conf.RTPAddress = ":8000"
- conf.RTCPAddress = ":8001"
- conf.MulticastIPRange = "224.1.0.0/16"
- conf.MulticastRTPPort = 8002
- conf.MulticastRTCPPort = 8003
- conf.ServerKey = "server.key"
- conf.ServerCert = "server.crt"
- conf.AuthMethods = AuthMethods{headers.AuthBasic}
+ copyStructFields(g.Values, conf)
+ return g
+}
- // RTMP
- conf.RTMP = true
- conf.RTMPAddress = ":1935"
- conf.RTMPSAddress = ":1936"
- conf.RTMPServerKey = "server.key"
- conf.RTMPServerCert = "server.crt"
+// PatchGlobal patches the global configuration.
+func (conf *Conf) PatchGlobal(optional *OptionalGlobal) {
+ copyStructFields(conf, optional.Values)
+}
- // HLS
- conf.HLS = true
- conf.HLSAddress = ":8888"
- conf.HLSServerKey = "server.key"
- conf.HLSServerCert = "server.crt"
- conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency)
- conf.HLSSegmentCount = 7
- conf.HLSSegmentDuration = 1 * StringDuration(time.Second)
- conf.HLSPartDuration = 200 * StringDuration(time.Millisecond)
- conf.HLSSegmentMaxSize = 50 * 1024 * 1024
- conf.HLSAllowOrigin = "*"
+// PatchPathDefaults patches path default settings.
+func (conf *Conf) PatchPathDefaults(optional *OptionalPath) {
+ copyStructFields(&conf.PathDefaults, optional.Values)
+}
- // WebRTC
- conf.WebRTC = true
- conf.WebRTCAddress = ":8889"
- conf.WebRTCServerKey = "server.key"
- conf.WebRTCServerCert = "server.crt"
- conf.WebRTCAllowOrigin = "*"
- conf.WebRTCICEServers2 = []WebRTCICEServer{{URL: "stun:stun.l.google.com:19302"}}
- conf.WebRTCICEHostNAT1To1IPs = []string{}
+// AddPath adds a path.
+func (conf *Conf) AddPath(name string, p *OptionalPath) error {
+ if _, ok := conf.OptionalPaths[name]; ok {
+ return fmt.Errorf("path already exists")
+ }
- // SRT
- conf.SRT = true
- conf.SRTAddress = ":8890"
+ if conf.OptionalPaths == nil {
+ conf.OptionalPaths = make(map[string]*OptionalPath)
+ }
- // Record
- conf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
- conf.RecordFormat = "fmp4"
- conf.RecordPartDuration = 100 * StringDuration(time.Millisecond)
- conf.RecordSegmentDuration = 3600 * StringDuration(time.Second)
- conf.RecordDeleteAfter = 24 * 3600 * StringDuration(time.Second)
+ conf.OptionalPaths[name] = p
+ return nil
+}
- type alias Conf
- d := json.NewDecoder(bytes.NewReader(b))
- d.DisallowUnknownFields()
- return d.Decode((*alias)(conf))
+// PatchPath patches a path.
+func (conf *Conf) PatchPath(name string, optional2 *OptionalPath) error {
+ optional, ok := conf.OptionalPaths[name]
+ if !ok {
+ return ErrPathNotFound
+ }
+
+ copyStructFields(optional.Values, optional2.Values)
+ return nil
+}
+
+// ReplacePath replaces a path.
+func (conf *Conf) ReplacePath(name string, optional2 *OptionalPath) error {
+ _, ok := conf.OptionalPaths[name]
+ if !ok {
+ return ErrPathNotFound
+ }
+
+ conf.OptionalPaths[name] = optional2
+ return nil
+}
+
+// RemovePath removes a path.
+func (conf *Conf) RemovePath(name string) error {
+ if _, ok := conf.OptionalPaths[name]; !ok {
+ return ErrPathNotFound
+ }
+
+ delete(conf.OptionalPaths, name)
+ return nil
}
diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go
index b6b6057dd05..6575c545c33 100644
--- a/internal/conf/conf_test.go
+++ b/internal/conf/conf_test.go
@@ -47,11 +47,17 @@ func TestConfFromFile(t *testing.T) {
pa, ok := conf.Paths["cam1"]
require.Equal(t, true, ok)
- require.Equal(t, &PathConf{
+ require.Equal(t, &Path{
+ Name: "cam1",
Source: "publisher",
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
- Record: true,
+ Playback: true,
+ RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
+ RecordFormat: RecordFormatFMP4,
+ RecordPartDuration: 100000000,
+ RecordSegmentDuration: 3600000000000,
+ RecordDeleteAfter: 86400000000000,
OverridePublisher: true,
RPICameraWidth: 1920,
RPICameraHeight: 1080,
@@ -67,7 +73,7 @@ func TestConfFromFile(t *testing.T) {
RPICameraBitrate: 1000000,
RPICameraProfile: "main",
RPICameraLevel: "4.1",
- RPICameraAfMode: "auto",
+ RPICameraAfMode: "continuous",
RPICameraAfRange: "normal",
RPICameraAfSpeed: "normal",
RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX",
@@ -107,11 +113,17 @@ func TestConfFromFile(t *testing.T) {
}
func TestConfFromFileAndEnv(t *testing.T) {
- os.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing")
- defer os.Unsetenv("MTX_PATHS_CAM1_SOURCE")
+ // global parameter
+ t.Setenv("RTSP_PROTOCOLS", "tcp")
- os.Setenv("RTSP_PROTOCOLS", "tcp")
- defer os.Unsetenv("RTSP_PROTOCOLS")
+ // path parameter
+ t.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing")
+
+ // deprecated global parameter
+ t.Setenv("MTX_RTMPDISABLE", "yes")
+
+ // deprecated path parameter
+ t.Setenv("MTX_PATHS_CAM2_DISABLEPUBLISHEROVERRIDE", "yes")
tmpf, err := writeTempFile([]byte("{}"))
require.NoError(t, err)
@@ -122,41 +134,19 @@ func TestConfFromFileAndEnv(t *testing.T) {
require.Equal(t, tmpf, confPath)
require.Equal(t, Protocols{Protocol(gortsplib.TransportTCP): {}}, conf.Protocols)
+ require.Equal(t, false, conf.RTMP)
pa, ok := conf.Paths["cam1"]
require.Equal(t, true, ok)
- require.Equal(t, &PathConf{
- Source: "rtsp://testing",
- SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
- SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
- Record: true,
- OverridePublisher: true,
- RPICameraWidth: 1920,
- RPICameraHeight: 1080,
- RPICameraContrast: 1,
- RPICameraSaturation: 1,
- RPICameraSharpness: 1,
- RPICameraExposure: "normal",
- RPICameraAWB: "auto",
- RPICameraDenoise: "off",
- RPICameraMetering: "centre",
- RPICameraFPS: 30,
- RPICameraIDRPeriod: 60,
- RPICameraBitrate: 1000000,
- RPICameraProfile: "main",
- RPICameraLevel: "4.1",
- RPICameraAfMode: "auto",
- RPICameraAfRange: "normal",
- RPICameraAfSpeed: "normal",
- RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX",
- RunOnDemandStartTimeout: 10 * StringDuration(time.Second),
- RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
- }, pa)
+ require.Equal(t, "rtsp://testing", pa.Source)
+
+ pa, ok = conf.Paths["cam2"]
+ require.Equal(t, true, ok)
+ require.Equal(t, false, pa.OverridePublisher)
}
func TestConfFromEnvOnly(t *testing.T) {
- os.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing")
- defer os.Unsetenv("MTX_PATHS_CAM1_SOURCE")
+ t.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing")
conf, confPath, err := Load("", nil)
require.NoError(t, err)
@@ -164,33 +154,7 @@ func TestConfFromEnvOnly(t *testing.T) {
pa, ok := conf.Paths["cam1"]
require.Equal(t, true, ok)
- require.Equal(t, &PathConf{
- Source: "rtsp://testing",
- SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
- SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
- Record: true,
- OverridePublisher: true,
- RPICameraWidth: 1920,
- RPICameraHeight: 1080,
- RPICameraContrast: 1,
- RPICameraSaturation: 1,
- RPICameraSharpness: 1,
- RPICameraExposure: "normal",
- RPICameraAWB: "auto",
- RPICameraDenoise: "off",
- RPICameraMetering: "centre",
- RPICameraFPS: 30,
- RPICameraIDRPeriod: 60,
- RPICameraBitrate: 1000000,
- RPICameraProfile: "main",
- RPICameraLevel: "4.1",
- RPICameraAfMode: "auto",
- RPICameraAfRange: "normal",
- RPICameraAfSpeed: "normal",
- RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX",
- RunOnDemandStartTimeout: 10 * StringDuration(time.Second),
- RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
- }, pa)
+ require.Equal(t, "rtsp://testing", pa.Source)
}
func TestConfEncryption(t *testing.T) {
@@ -211,8 +175,7 @@ func TestConfEncryption(t *testing.T) {
return base64.StdEncoding.EncodeToString(encrypted)
}()
- os.Setenv("RTSP_CONFKEY", key)
- defer os.Unsetenv("RTSP_CONFKEY")
+ t.Setenv("RTSP_CONFKEY", key)
tmpf, err := writeTempFile([]byte(encryptedConf))
require.NoError(t, err)
@@ -299,7 +262,7 @@ func TestConfErrors(t *testing.T) {
" source: rpiCamera\n" +
" cam2:\n" +
" source: rpiCamera\n",
- "'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam1' and 'cam2'",
+ "'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam2' and 'cam1'",
},
{
"invalid srt publish passphrase",
@@ -315,6 +278,20 @@ func TestConfErrors(t *testing.T) {
" srtReadPassphrase: a\n",
`invalid 'readRTPassphrase': must be between 10 and 79 characters`,
},
+ {
+ "all_others aliases",
+ "paths:\n" +
+ " all:\n" +
+ " all_others:\n",
+ `all_others, all and '~^.*$' are aliases`,
+ },
+ {
+ "all_others aliases",
+ "paths:\n" +
+ " all_others:\n" +
+ " ~^.*$:\n",
+ `all_others, all and '~^.*$' are aliases`,
+ },
} {
t.Run(ca.name, func(t *testing.T) {
tmpf, err := writeTempFile([]byte(ca.conf))
@@ -332,7 +309,8 @@ func TestSampleConfFile(t *testing.T) {
conf1, confPath1, err := Load("../../mediamtx.yml", nil)
require.NoError(t, err)
require.Equal(t, "../../mediamtx.yml", confPath1)
- delete(conf1.Paths, "all")
+ conf1.Paths = make(map[string]*Path)
+ conf1.OptionalPaths = nil
conf2, confPath2, err := Load("", nil)
require.NoError(t, err)
@@ -346,7 +324,7 @@ func TestSampleConfFile(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "../../mediamtx.yml", confPath1)
- tmpf, err := writeTempFile([]byte("paths:\n all:"))
+ tmpf, err := writeTempFile([]byte("paths:\n all_others:"))
require.NoError(t, err)
defer os.Remove(tmpf)
diff --git a/internal/conf/credential.go b/internal/conf/credential.go
index 2c5abe97233..7b6d47399fe 100644
--- a/internal/conf/credential.go
+++ b/internal/conf/credential.go
@@ -1,22 +1,31 @@
package conf
import (
+ "crypto/sha256"
+ "encoding/base64"
"encoding/json"
"fmt"
"regexp"
"strings"
+
+ "github.com/matthewhartstonge/argon2"
)
-var reCredential = regexp.MustCompile(`^[a-zA-Z0-9!\$\(\)\*\+\.;<=>\[\]\^_\-\{\}@#&]+$`)
+var (
+ rePlainCredential = regexp.MustCompile(`^[a-zA-Z0-9!\$\(\)\*\+\.;<=>\[\]\^_\-\{\}@#&]+$`)
+ reBase64 = regexp.MustCompile(`^sha256:[a-zA-Z0-9\+/=]+$`)
+)
-const credentialSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,\",\",@,#,&"
+const plainCredentialSupportedChars = "A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,\",\",@,#,&"
// Credential is a parameter that is used as username or password.
-type Credential string
+type Credential struct {
+ value string
+}
// MarshalJSON implements json.Marshaler.
func (d Credential) MarshalJSON() ([]byte, error) {
- return json.Marshal(string(d))
+ return json.Marshal(d.value)
}
// UnmarshalJSON implements json.Unmarshaler.
@@ -26,17 +35,87 @@ func (d *Credential) UnmarshalJSON(b []byte) error {
return err
}
- if in != "" &&
- !strings.HasPrefix(in, "sha256:") &&
- !reCredential.MatchString(in) {
- return fmt.Errorf("credential contains unsupported characters. Supported are: %s", credentialSupportedChars)
+ *d = Credential{
+ value: in,
}
- *d = Credential(in)
- return nil
+ return d.validate()
+}
+
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *Credential) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
+}
+
+// GetValue returns the value of the credential.
+func (d *Credential) GetValue() string {
+ return d.value
+}
+
+// IsEmpty returns true if the credential is not configured.
+func (d *Credential) IsEmpty() bool {
+ return d.value == ""
+}
+
+// IsSha256 returns true if the credential is a sha256 hash.
+func (d *Credential) IsSha256() bool {
+ return d.value != "" && strings.HasPrefix(d.value, "sha256:")
+}
+
+// IsArgon2 returns true if the credential is an argon2 hash.
+func (d *Credential) IsArgon2() bool {
+ return d.value != "" && strings.HasPrefix(d.value, "argon2:")
+}
+
+// IsHashed returns true if the credential is a sha256 or argon2 hash.
+func (d *Credential) IsHashed() bool {
+ return d.IsSha256() || d.IsArgon2()
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *Credential) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+func sha256Base64(in string) string {
+ h := sha256.New()
+ h.Write([]byte(in))
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}
+
+// Check returns true if the given value matches the credential.
+func (d *Credential) Check(guess string) bool {
+ if d.IsSha256() {
+ return d.value[len("sha256:"):] == sha256Base64(guess)
+ }
+ if d.IsArgon2() {
+ // TODO: remove matthewhartstonge/argon2 when this PR gets merged into mainline Go:
+ // https://go-review.googlesource.com/c/crypto/+/502515
+ ok, err := argon2.VerifyEncoded([]byte(guess), []byte(d.value[len("argon2:"):]))
+ return ok && err == nil
+ }
+ if d.IsEmpty() {
+ // when no credential is set, any value is valid
+ return true
+ }
+
+ return d.value == guess
+}
+
+func (d *Credential) validate() error {
+ if !d.IsEmpty() {
+ switch {
+ case d.IsSha256():
+ if !reBase64.MatchString(d.value) {
+ return fmt.Errorf("credential contains unsupported characters, sha256 hash must be base64 encoded")
+ }
+ case d.IsArgon2():
+ // TODO: remove matthewhartstonge/argon2 when this PR gets merged into mainline Go:
+ // https://go-review.googlesource.com/c/crypto/+/502515
+ _, err := argon2.Decode([]byte(d.value[len("argon2:"):]))
+ if err != nil {
+ return fmt.Errorf("invalid argon2 hash: %w", err)
+ }
+ default:
+ if !rePlainCredential.MatchString(d.value) {
+ return fmt.Errorf("credential contains unsupported characters. Supported are: %s", plainCredentialSupportedChars)
+ }
+ }
+ }
+ return nil
}
diff --git a/internal/conf/credential_test.go b/internal/conf/credential_test.go
new file mode 100644
index 00000000000..ef325672b91
--- /dev/null
+++ b/internal/conf/credential_test.go
@@ -0,0 +1,167 @@
+package conf
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCredential(t *testing.T) {
+ t.Run("MarshalJSON", func(t *testing.T) {
+ cred := Credential{value: "password"}
+ expectedJSON := []byte(`"password"`)
+ actualJSON, err := cred.MarshalJSON()
+ assert.NoError(t, err)
+ assert.Equal(t, expectedJSON, actualJSON)
+ })
+
+ t.Run("UnmarshalJSON", func(t *testing.T) {
+ expectedCred := Credential{value: "password"}
+ jsonData := []byte(`"password"`)
+ var actualCred Credential
+ err := actualCred.UnmarshalJSON(jsonData)
+ assert.NoError(t, err)
+ assert.Equal(t, expectedCred, actualCred)
+ })
+
+ t.Run("UnmarshalEnv", func(t *testing.T) {
+ cred := Credential{}
+ err := cred.UnmarshalEnv("", "password")
+ assert.NoError(t, err)
+ assert.Equal(t, "password", cred.value)
+ })
+
+ t.Run("GetValue", func(t *testing.T) {
+ cred := Credential{value: "password"}
+ actualValue := cred.GetValue()
+ assert.Equal(t, "password", actualValue)
+ })
+
+ t.Run("IsEmpty", func(t *testing.T) {
+ cred := Credential{}
+ assert.True(t, cred.IsEmpty())
+ assert.False(t, cred.IsHashed())
+
+ cred.value = "password"
+ assert.False(t, cred.IsEmpty())
+ assert.False(t, cred.IsHashed())
+ })
+
+ t.Run("IsSha256", func(t *testing.T) {
+ cred := Credential{}
+ assert.False(t, cred.IsSha256())
+ assert.False(t, cred.IsHashed())
+
+ cred.value = "sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo="
+ assert.True(t, cred.IsSha256())
+ assert.True(t, cred.IsHashed())
+
+ cred.value = "argon2:$argon2id$v=19$m=65536,t=1," +
+ "p=4$WXJGqwIB2qd+pRmxMOw9Dg$X4gvR0ZB2DtQoN8vOnJPR2SeFdUhH9TyVzfV98sfWeE"
+ assert.False(t, cred.IsSha256())
+ assert.True(t, cred.IsHashed())
+ })
+
+ t.Run("IsArgon2", func(t *testing.T) {
+ cred := Credential{}
+ assert.False(t, cred.IsArgon2())
+ assert.False(t, cred.IsHashed())
+
+ cred.value = "sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo="
+ assert.False(t, cred.IsArgon2())
+ assert.True(t, cred.IsHashed())
+
+ cred.value = "argon2:$argon2id$v=19$m=65536,t=1," +
+ "p=4$WXJGqwIB2qd+pRmxMOw9Dg$X4gvR0ZB2DtQoN8vOnJPR2SeFdUhH9TyVzfV98sfWeE"
+ assert.True(t, cred.IsArgon2())
+ assert.True(t, cred.IsHashed())
+ })
+
+ t.Run("Check-plain", func(t *testing.T) {
+ cred := Credential{value: "password"}
+ assert.True(t, cred.Check("password"))
+ assert.False(t, cred.Check("wrongpassword"))
+ })
+
+ t.Run("Check-sha256", func(t *testing.T) {
+ cred := Credential{value: "password"}
+ assert.True(t, cred.Check("password"))
+ assert.False(t, cred.Check("wrongpassword"))
+ })
+
+ t.Run("Check-sha256", func(t *testing.T) {
+ cred := Credential{value: "sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ="}
+ assert.True(t, cred.Check("testuser"))
+ assert.False(t, cred.Check("notestuser"))
+ })
+
+ t.Run("Check-argon2", func(t *testing.T) {
+ cred := Credential{value: "argon2:$argon2id$v=19$m=4096,t=3," +
+ "p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58"}
+ assert.True(t, cred.Check("testuser"))
+ assert.False(t, cred.Check("notestuser"))
+ })
+
+ t.Run("validate", func(t *testing.T) {
+ tests := []struct {
+ name string
+ cred *Credential
+ wantErr bool
+ }{
+ {
+ name: "Empty credential",
+ cred: &Credential{value: ""},
+ wantErr: false,
+ },
+ {
+ name: "Valid plain credential",
+ cred: &Credential{value: "validPlain123"},
+ wantErr: false,
+ },
+ {
+ name: "Invalid plain credential",
+ cred: &Credential{value: "invalid/Plain"},
+ wantErr: true,
+ },
+ {
+ name: "Valid sha256 credential",
+ cred: &Credential{value: "sha256:validBase64EncodedHash=="},
+ wantErr: false,
+ },
+ {
+ name: "Invalid sha256 credential",
+ cred: &Credential{value: "sha256:inval*idBase64"},
+ wantErr: true,
+ },
+ {
+ name: "Valid Argon2 credential",
+ cred: &Credential{value: "argon2:$argon2id$v=19$m=4096," +
+ "t=3,p=1$MTIzNDU2Nzg$zarsL19s86GzUWlAkvwt4gJBFuU/A9CVuCjNI4fksow"},
+ wantErr: false,
+ },
+ {
+ name: "Invalid Argon2 credential",
+ cred: &Credential{value: "argon2:invalid"},
+ wantErr: true,
+ },
+ {
+ name: "Invalid Argon2 credential",
+ // testing argon2d errors, because it's not supported
+ cred: &Credential{value: "$argon2d$v=19$m=4096,t=3," +
+ "p=1$MTIzNDU2Nzg$Xqyd4R7LzXvvAEHaVU12+Nzf5OkHoYcwIEIIYJUDpz0"},
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.cred.validate()
+ if tt.wantErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+ })
+}
diff --git a/internal/conf/encryption.go b/internal/conf/encryption.go
index c9e0feabe7d..d7ff417766a 100644
--- a/internal/conf/encryption.go
+++ b/internal/conf/encryption.go
@@ -8,7 +8,7 @@ import (
// Encryption is the encryption parameter.
type Encryption int
-// supported encryption policies.
+// values.
const (
EncryptionNo Encryption = iota
EncryptionOptional
@@ -60,7 +60,7 @@ func (d *Encryption) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *Encryption) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *Encryption) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
}
diff --git a/internal/conf/env/env.go b/internal/conf/env/env.go
index bd8b2a29c19..7c60ca78111 100644
--- a/internal/conf/env/env.go
+++ b/internal/conf/env/env.go
@@ -2,7 +2,6 @@
package env
import (
- "encoding/json"
"fmt"
"os"
"reflect"
@@ -10,8 +9,9 @@ import (
"strings"
)
-type envUnmarshaler interface {
- UnmarshalEnv(string) error
+// Unmarshaler can be implemented to override the unmarshaling process.
+type Unmarshaler interface {
+ UnmarshalEnv(prefix string, v string) error
}
func envHasAtLeastAKeyWithPrefix(env map[string]string, prefix string) bool {
@@ -23,14 +23,27 @@ func envHasAtLeastAKeyWithPrefix(env map[string]string, prefix string) bool {
return false
}
-func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) error {
- rt := rv.Type()
+func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) error {
+ if prv.Kind() != reflect.Pointer {
+ return loadEnvInternal(env, prefix, prv.Addr())
+ }
+
+ rt := prv.Type().Elem()
- if i, ok := rv.Addr().Interface().(envUnmarshaler); ok {
+ if i, ok := prv.Interface().(Unmarshaler); ok {
if ev, ok := env[prefix]; ok {
- err := i.UnmarshalEnv(ev)
+ if prv.IsNil() {
+ prv.Set(reflect.New(rt))
+ i = prv.Interface().(Unmarshaler)
+ }
+ err := i.UnmarshalEnv(prefix, ev)
if err != nil {
- return fmt.Errorf("%s: %s", prefix, err)
+ return fmt.Errorf("%s: %w", prefix, err)
+ }
+ } else if envHasAtLeastAKeyWithPrefix(env, prefix) {
+ err := i.UnmarshalEnv(prefix, "")
+ if err != nil {
+ return fmt.Errorf("%s: %w", prefix, err)
}
}
return nil
@@ -39,48 +52,63 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
switch rt {
case reflect.TypeOf(""):
if ev, ok := env[prefix]; ok {
- rv.SetString(ev)
+ if prv.IsNil() {
+ prv.Set(reflect.New(rt))
+ }
+ prv.Elem().SetString(ev)
}
return nil
case reflect.TypeOf(int(0)):
if ev, ok := env[prefix]; ok {
+ if prv.IsNil() {
+ prv.Set(reflect.New(rt))
+ }
iv, err := strconv.ParseInt(ev, 10, 32)
if err != nil {
- return fmt.Errorf("%s: %s", prefix, err)
+ return fmt.Errorf("%s: %w", prefix, err)
}
- rv.SetInt(iv)
+ prv.Elem().SetInt(iv)
}
return nil
case reflect.TypeOf(uint64(0)):
if ev, ok := env[prefix]; ok {
+ if prv.IsNil() {
+ prv.Set(reflect.New(rt))
+ }
iv, err := strconv.ParseUint(ev, 10, 32)
if err != nil {
- return fmt.Errorf("%s: %s", prefix, err)
+ return fmt.Errorf("%s: %w", prefix, err)
}
- rv.SetUint(iv)
+ prv.Elem().SetUint(iv)
}
return nil
case reflect.TypeOf(float64(0)):
if ev, ok := env[prefix]; ok {
+ if prv.IsNil() {
+ prv.Set(reflect.New(rt))
+ }
iv, err := strconv.ParseFloat(ev, 64)
if err != nil {
- return fmt.Errorf("%s: %s", prefix, err)
+ return fmt.Errorf("%s: %w", prefix, err)
}
- rv.SetFloat(iv)
+ prv.Elem().SetFloat(iv)
}
return nil
case reflect.TypeOf(bool(false)):
if ev, ok := env[prefix]; ok {
+ if prv.IsNil() {
+ prv.Set(reflect.New(rt))
+ }
switch strings.ToLower(ev) {
case "yes", "true":
- rv.SetBool(true)
+ prv.Elem().SetBool(true)
case "no", "false":
- rv.SetBool(false)
+ prv.Elem().SetBool(false)
default:
return fmt.Errorf("%s: invalid value '%s'", prefix, ev)
@@ -107,20 +135,16 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
}
// initialize only if there's at least one key
- if rv.IsNil() {
- rv.Set(reflect.MakeMap(rt))
+ if prv.Elem().IsNil() {
+ prv.Elem().Set(reflect.MakeMap(rt))
}
mapKeyLower := strings.ToLower(mapKey)
- nv := rv.MapIndex(reflect.ValueOf(mapKeyLower))
+ nv := prv.Elem().MapIndex(reflect.ValueOf(mapKeyLower))
zero := reflect.Value{}
if nv == zero {
nv = reflect.New(rt.Elem().Elem())
- if unm, ok := nv.Interface().(json.Unmarshaler); ok {
- // load defaults
- unm.UnmarshalJSON(nil) //nolint:errcheck
- }
- rv.SetMapIndex(reflect.ValueOf(mapKeyLower), nv)
+ prv.Elem().SetMapIndex(reflect.ValueOf(mapKeyLower), nv)
}
err := loadEnvInternal(env, prefix+"_"+mapKey, nv.Elem())
@@ -134,13 +158,15 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
flen := rt.NumField()
for i := 0; i < flen; i++ {
f := rt.Field(i)
+ jsonTag := f.Tag.Get("json")
// load only public fields
- if f.Tag.Get("json") == "-" {
+ if jsonTag == "-" {
continue
}
- err := loadEnvInternal(env, prefix+"_"+strings.ToUpper(f.Name), rv.Field(i))
+ err := loadEnvInternal(env, prefix+"_"+
+ strings.ToUpper(strings.TrimSuffix(jsonTag, ",omitempty")), prv.Elem().Field(i))
if err != nil {
return err
}
@@ -148,20 +174,23 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
return nil
case reflect.Slice:
- if rt.Elem() == reflect.TypeOf("") {
+ switch {
+ case rt.Elem() == reflect.TypeOf(""):
if ev, ok := env[prefix]; ok {
if ev == "" {
- rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
+ prv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))
} else {
- rv.Set(reflect.ValueOf(strings.Split(ev, ",")))
+ if prv.IsNil() {
+ prv.Set(reflect.New(rt))
+ }
+ prv.Elem().Set(reflect.ValueOf(strings.Split(ev, ",")))
}
}
return nil
- }
- if rt.Elem().Kind() == reflect.Struct {
+ case rt.Elem().Kind() == reflect.Struct:
if ev, ok := env[prefix]; ok && ev == "" { // special case: empty list
- rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
+ prv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))
} else {
for i := 0; ; i++ {
itemPrefix := prefix + "_" + strconv.FormatInt(int64(i), 10)
@@ -175,7 +204,7 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
return err
}
- rv.Set(reflect.Append(rv, elem.Elem()))
+ prv.Elem().Set(reflect.Append(prv.Elem(), elem.Elem()))
}
}
return nil
@@ -185,13 +214,20 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
return fmt.Errorf("unsupported type: %v", rt)
}
-// Load loads the configuration from the environment.
-func Load(prefix string, v interface{}) error {
+func loadWithEnv(env map[string]string, prefix string, v interface{}) error {
+ return loadEnvInternal(env, prefix, reflect.ValueOf(v).Elem())
+}
+
+func envToMap() map[string]string {
env := make(map[string]string)
for _, kv := range os.Environ() {
tmp := strings.SplitN(kv, "=", 2)
env[tmp[0]] = tmp[1]
}
+ return env
+}
- return loadEnvInternal(env, prefix, reflect.ValueOf(v).Elem())
+// Load loads the configuration from the environment.
+func Load(prefix string, v interface{}) error {
+ return loadWithEnv(envToMap(), prefix, v)
}
diff --git a/internal/conf/env/env_test.go b/internal/conf/env/env_test.go
index b1f64fc64e1..6fc057dec48 100644
--- a/internal/conf/env/env_test.go
+++ b/internal/conf/env/env_test.go
@@ -2,20 +2,34 @@ package env
import (
"encoding/json"
- "os"
"testing"
"time"
"github.com/stretchr/testify/require"
)
-type subStruct struct {
- MyParam int
+func stringPtr(v string) *string {
+ return &v
}
-type mapEntry struct {
- MyValue string
- MyStruct subStruct
+func intPtr(v int) *int {
+ return &v
+}
+
+func uint64Ptr(v uint64) *uint64 {
+ return &v
+}
+
+func boolPtr(v bool) *bool {
+ return &v
+}
+
+func float64Ptr(v float64) *float64 {
+ return &v
+}
+
+func durationPtr(v time.Duration) *time.Duration {
+ return &v
}
type myDuration time.Duration
@@ -35,111 +49,154 @@ func (d *myDuration) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *myDuration) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *myDuration) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
+}
+
+type subStruct struct {
+ MyParam int `json:"myParam"`
+}
+
+type mapEntry struct {
+ MyValue string `json:"myValue"`
+ MyStruct subStruct `json:"myStruct"`
}
type mySubStruct struct {
- URL string
- Username string
- Password string
+ URL string `json:"url"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+ MyInt2 int `json:"myInt2"`
}
type testStruct struct {
- MyString string
- MyInt int
- MyFloat float64
- MyBool bool
- MyDuration myDuration
- MyMap map[string]*mapEntry
- MySlice []string
- MySliceEmpty []string
- MySliceSubStruct []mySubStruct
- MySliceSubStructEmpty []mySubStruct
+ MyString string `json:"myString"`
+ MyStringOpt *string `json:"myStringOpt"`
+ MyInt int `json:"myInt"`
+ MyIntOpt *int `json:"myIntOpt"`
+ MyUint uint64 `json:"myUint"`
+ MyUintOpt *uint64 `json:"myUintOpt"`
+ MyFloat float64 `json:"myFloat"`
+ MyFloatOpt *float64 `json:"myFloatOpt"`
+ MyBool bool `json:"myBool"`
+ MyBoolOpt *bool `json:"myBoolOpt"`
+ MyDuration myDuration `json:"myDuration"`
+ MyDurationOpt *myDuration `json:"myDurationOpt"`
+ MyDurationOptUnset *myDuration `json:"myDurationOptUnset"`
+ MyMap map[string]*mapEntry `json:"myMap"`
+ MySliceString []string `json:"mySliceString"`
+ MySliceStringEmpty []string `json:"mySliceStringEmpty"`
+ MySliceStringOpt *[]string `json:"mySliceStringOpt"`
+ MySliceStringOptUnset *[]string `json:"mySliceStringOptUnset"`
+ MySliceSubStruct []mySubStruct `json:"mySliceSubStruct"`
+ MySliceSubStructEmpty []mySubStruct `json:"mySliceSubStructEmpty"`
+ MySliceSubStructOpt *[]mySubStruct `json:"mySliceSubStructOpt"`
+ MySliceSubStructOptUnset *[]mySubStruct `json:"mySliceSubStructOptUnset"`
+ Unset *bool `json:"unset"`
}
func TestLoad(t *testing.T) {
- os.Setenv("MYPREFIX_MYSTRING", "testcontent")
- defer os.Unsetenv("MYPREFIX_MYSTRING")
-
- os.Setenv("MYPREFIX_MYINT", "123")
- defer os.Unsetenv("MYPREFIX_MYINT")
-
- os.Setenv("MYPREFIX_MYFLOAT", "15.2")
- defer os.Unsetenv("MYPREFIX_MYFLOAT")
-
- os.Setenv("MYPREFIX_MYBOOL", "yes")
- defer os.Unsetenv("MYPREFIX_MYBOOL")
-
- os.Setenv("MYPREFIX_MYDURATION", "22s")
- defer os.Unsetenv("MYPREFIX_MYDURATION")
-
- os.Setenv("MYPREFIX_MYMAP_MYKEY", "")
- defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY")
-
- os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYVALUE", "asd")
- defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYVALUE")
-
- os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM", "456")
- defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM")
-
- os.Setenv("MYPREFIX_MYSLICE", "val1,val2")
- defer os.Unsetenv("MYPREFIX_MYSLICE")
-
- os.Setenv("MYPREFIX_MYSLICEEMPTY", "")
- defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY")
-
- os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL", "url1")
- defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL")
-
- os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME", "user1")
- defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME")
-
- os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD", "pass1")
- defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD")
-
- os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL", "url2")
- defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL")
-
- os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD", "pass2")
- defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD")
+ env := map[string]string{
+ "MYPREFIX_MYSTRING": "testcontent",
+ "MYPREFIX_MYSTRINGOPT": "testcontent2",
+ "MYPREFIX_MYINT": "123",
+ "MYPREFIX_MYINTOPT": "456",
+ "MYPREFIX_MYUINT": "8910",
+ "MYPREFIX_MYUINTOPT": "112313",
+ "MYPREFIX_MYFLOAT": "15.2",
+ "MYPREFIX_MYFLOATOPT": "16.2",
+ "MYPREFIX_MYBOOL": "yes",
+ "MYPREFIX_MYBOOLOPT": "false",
+ "MYPREFIX_MYDURATION": "22s",
+ "MYPREFIX_MYDURATIONOPT": "30s",
+ "MYPREFIX_MYMAP_MYKEY": "",
+ "MYPREFIX_MYMAP_MYKEY2_MYVALUE": "asd",
+ "MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM": "456",
+ "MYPREFIX_MYSLICESTRING": "val1,val2",
+ "MYPREFIX_MYSLICESTRINGEMPTY": "",
+ "MYPREFIX_MYSLICESTRINGOPT": "aa",
+ "MYPREFIX_MYSLICESUBSTRUCT_0_URL": "url1",
+ "MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME": "user1",
+ "MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD": "pass1",
+ "MYPREFIX_MYSLICESUBSTRUCT_1_URL": "url2",
+ "MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD": "pass2",
+ "MYPREFIX_MYSLICESUBSTRUCTEMPTY": "",
+ "MYPREFIX_MYSLICESUBSTRUCTOPT_1_PASSWORD": "pwd",
+ }
- os.Setenv("MYPREFIX_MYSLICESUBSTRUCTEMPTY", "")
- defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCTEMPTY")
+ for key, val := range env {
+ t.Setenv(key, val)
+ }
var s testStruct
err := Load("MYPREFIX", &s)
require.NoError(t, err)
- require.Equal(t, "testcontent", s.MyString)
- require.Equal(t, 123, s.MyInt)
- require.Equal(t, 15.2, s.MyFloat)
- require.Equal(t, true, s.MyBool)
- require.Equal(t, 22*myDuration(time.Second), s.MyDuration)
-
- _, ok := s.MyMap["mykey"]
- require.Equal(t, true, ok)
-
- v, ok := s.MyMap["mykey2"]
- require.Equal(t, true, ok)
- require.Equal(t, "asd", v.MyValue)
- require.Equal(t, 456, v.MyStruct.MyParam)
-
- require.Equal(t, []string{"val1", "val2"}, s.MySlice)
- require.Equal(t, []string{}, s.MySliceEmpty)
-
- require.Equal(t, []mySubStruct{
- {
- URL: "url1",
- Username: "user1",
- Password: "pass1",
+ require.Equal(t, testStruct{
+ MyString: "testcontent",
+ MyStringOpt: stringPtr("testcontent2"),
+ MyInt: 123,
+ MyIntOpt: intPtr(456),
+ MyUint: 8910,
+ MyUintOpt: uint64Ptr(112313),
+ MyFloat: 15.2,
+ MyFloatOpt: float64Ptr(16.2),
+ MyBool: true,
+ MyBoolOpt: boolPtr(false),
+ MyDuration: 22000000000,
+ MyDurationOpt: (*myDuration)(durationPtr(30000000000)),
+ MyMap: map[string]*mapEntry{
+ "mykey": {
+ MyValue: "",
+ MyStruct: subStruct{
+ MyParam: 0,
+ },
+ },
+ "mykey2": {
+ MyValue: "asd",
+ MyStruct: subStruct{
+ MyParam: 456,
+ },
+ },
},
- {
- URL: "url2",
- Password: "pass2",
+ MySliceString: []string{
+ "val1",
+ "val2",
},
- }, s.MySliceSubStruct)
+ MySliceStringEmpty: []string{},
+ MySliceStringOpt: &[]string{"aa"},
+ MySliceSubStruct: []mySubStruct{
+ {
+ URL: "url1",
+ Username: "user1",
+ Password: "pass1",
+ },
+ {
+ URL: "url2",
+ Username: "",
+ Password: "pass2",
+ },
+ },
+ MySliceSubStructEmpty: []mySubStruct{},
+ }, s)
+}
- require.Equal(t, []mySubStruct{}, s.MySliceSubStructEmpty)
+func FuzzLoad(f *testing.F) {
+ f.Add("MYPREFIX_MYINT", "a")
+ f.Add("MYPREFIX_MYUINT", "a")
+ f.Add("MYPREFIX_MYFLOAT", "a")
+ f.Add("MYPREFIX_MYBOOL", "a")
+ f.Add("MYPREFIX_MYSLICESUBSTRUCT_0_MYINT2", "a")
+ f.Add("MYPREFIX_MYDURATION", "a")
+ f.Add("MYPREFIX_MYDURATION_A", "a")
+
+ f.Fuzz(func(t *testing.T, key string, val string) {
+ env := map[string]string{
+ key: val,
+ }
+
+ var s testStruct
+ loadWithEnv(env, "MYPREFIX", &s) //nolint:errcheck
+ })
}
diff --git a/internal/conf/global.go b/internal/conf/global.go
new file mode 100644
index 00000000000..51ee02a4571
--- /dev/null
+++ b/internal/conf/global.go
@@ -0,0 +1,41 @@
+package conf
+
+import (
+ "encoding/json"
+ "reflect"
+)
+
+var globalValuesType = func() reflect.Type {
+ var fields []reflect.StructField
+ rt := reflect.TypeOf(Conf{})
+ nf := rt.NumField()
+
+ for i := 0; i < nf; i++ {
+ f := rt.Field(i)
+ j := f.Tag.Get("json")
+
+ if j != "-" && j != "pathDefaults" && j != "paths" {
+ fields = append(fields, reflect.StructField{
+ Name: f.Name,
+ Type: f.Type,
+ Tag: f.Tag,
+ })
+ }
+ }
+
+ return reflect.StructOf(fields)
+}()
+
+func newGlobalValues() interface{} {
+ return reflect.New(globalValuesType).Interface()
+}
+
+// Global is the global part of Conf.
+type Global struct {
+ Values interface{}
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p *Global) MarshalJSON() ([]byte, error) {
+ return json.Marshal(p.Values)
+}
diff --git a/internal/conf/hls_variant.go b/internal/conf/hls_variant.go
index 18182a065c4..9d0dfc05480 100644
--- a/internal/conf/hls_variant.go
+++ b/internal/conf/hls_variant.go
@@ -55,7 +55,7 @@ func (d *HLSVariant) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *HLSVariant) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *HLSVariant) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
}
diff --git a/internal/conf/ips_or_cidrs.go b/internal/conf/ips_or_cidrs.go
index bc637630b5a..59d56d32d9d 100644
--- a/internal/conf/ips_or_cidrs.go
+++ b/internal/conf/ips_or_cidrs.go
@@ -50,9 +50,9 @@ func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *IPsOrCIDRs) UnmarshalEnv(s string) error {
- byts, _ := json.Marshal(strings.Split(s, ","))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *IPsOrCIDRs) UnmarshalEnv(_ string, v string) error {
+ byts, _ := json.Marshal(strings.Split(v, ","))
return d.UnmarshalJSON(byts)
}
diff --git a/internal/conf/log_destination.go b/internal/conf/log_destination.go
index c9493d7baa8..cb369f00ed7 100644
--- a/internal/conf/log_destination.go
+++ b/internal/conf/log_destination.go
@@ -84,8 +84,8 @@ func (d *LogDestinations) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *LogDestinations) UnmarshalEnv(s string) error {
- byts, _ := json.Marshal(strings.Split(s, ","))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *LogDestinations) UnmarshalEnv(_ string, v string) error {
+ byts, _ := json.Marshal(strings.Split(v, ","))
return d.UnmarshalJSON(byts)
}
diff --git a/internal/conf/log_level.go b/internal/conf/log_level.go
index 4c2f5b8d9b6..2231067c7cd 100644
--- a/internal/conf/log_level.go
+++ b/internal/conf/log_level.go
@@ -61,7 +61,7 @@ func (d *LogLevel) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *LogLevel) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *LogLevel) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
}
diff --git a/internal/conf/optional_global.go b/internal/conf/optional_global.go
new file mode 100644
index 00000000000..afaeb158c0a
--- /dev/null
+++ b/internal/conf/optional_global.go
@@ -0,0 +1,60 @@
+package conf
+
+import (
+ "bytes"
+ "encoding/json"
+ "reflect"
+ "strings"
+)
+
+var optionalGlobalValuesType = func() reflect.Type {
+ var fields []reflect.StructField
+ rt := reflect.TypeOf(Conf{})
+ nf := rt.NumField()
+
+ for i := 0; i < nf; i++ {
+ f := rt.Field(i)
+ j := f.Tag.Get("json")
+
+ if j != "-" && j != "pathDefaults" && j != "paths" {
+ if !strings.Contains(j, ",omitempty") {
+ j += ",omitempty"
+ }
+
+ typ := f.Type
+ if typ.Kind() != reflect.Pointer {
+ typ = reflect.PtrTo(typ)
+ }
+
+ fields = append(fields, reflect.StructField{
+ Name: f.Name,
+ Type: typ,
+ Tag: reflect.StructTag(`json:"` + j + `"`),
+ })
+ }
+ }
+
+ return reflect.StructOf(fields)
+}()
+
+func newOptionalGlobalValues() interface{} {
+ return reflect.New(optionalGlobalValuesType).Interface()
+}
+
+// OptionalGlobal is a Conf whose values can all be optional.
+type OptionalGlobal struct {
+ Values interface{}
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (p *OptionalGlobal) UnmarshalJSON(b []byte) error {
+ p.Values = newOptionalGlobalValues()
+ d := json.NewDecoder(bytes.NewReader(b))
+ d.DisallowUnknownFields()
+ return d.Decode(p.Values)
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p *OptionalGlobal) MarshalJSON() ([]byte, error) {
+ return json.Marshal(p.Values)
+}
diff --git a/internal/conf/optional_path.go b/internal/conf/optional_path.go
new file mode 100644
index 00000000000..47eff598852
--- /dev/null
+++ b/internal/conf/optional_path.go
@@ -0,0 +1,70 @@
+package conf
+
+import (
+ "bytes"
+ "encoding/json"
+ "reflect"
+ "strings"
+
+ "github.com/bluenviron/mediamtx/internal/conf/env"
+)
+
+var optionalPathValuesType = func() reflect.Type {
+ var fields []reflect.StructField
+ rt := reflect.TypeOf(Path{})
+ nf := rt.NumField()
+
+ for i := 0; i < nf; i++ {
+ f := rt.Field(i)
+ j := f.Tag.Get("json")
+
+ if j != "-" {
+ if !strings.Contains(j, ",omitempty") {
+ j += ",omitempty"
+ }
+
+ typ := f.Type
+ if typ.Kind() != reflect.Pointer {
+ typ = reflect.PtrTo(typ)
+ }
+
+ fields = append(fields, reflect.StructField{
+ Name: f.Name,
+ Type: typ,
+ Tag: reflect.StructTag(`json:"` + j + `"`),
+ })
+ }
+ }
+
+ return reflect.StructOf(fields)
+}()
+
+func newOptionalPathValues() interface{} {
+ return reflect.New(optionalPathValuesType).Interface()
+}
+
+// OptionalPath is a Path whose values can all be optional.
+type OptionalPath struct {
+ Values interface{}
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (p *OptionalPath) UnmarshalJSON(b []byte) error {
+ p.Values = newOptionalPathValues()
+ d := json.NewDecoder(bytes.NewReader(b))
+ d.DisallowUnknownFields()
+ return d.Decode(p.Values)
+}
+
+// UnmarshalEnv implements env.Unmarshaler.
+func (p *OptionalPath) UnmarshalEnv(prefix string, _ string) error {
+ if p.Values == nil {
+ p.Values = newOptionalPathValues()
+ }
+ return env.Load(prefix, p.Values)
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p *OptionalPath) MarshalJSON() ([]byte, error) {
+ return json.Marshal(p.Values)
+}
diff --git a/internal/conf/path.go b/internal/conf/path.go
index aecfb319d3c..a6b840a0c8b 100644
--- a/internal/conf/path.go
+++ b/internal/conf/path.go
@@ -1,7 +1,6 @@
package conf
import (
- "bytes"
"encoding/json"
"fmt"
"net"
@@ -11,14 +10,13 @@ import (
"strings"
"time"
+ "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
)
var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/\.~]+$`)
-// IsValidPathName checks if a path name is valid.
-func IsValidPathName(name string) error {
+func isValidPathName(name string) error {
if name == "" {
return fmt.Errorf("cannot be empty")
}
@@ -48,9 +46,45 @@ func srtCheckPassphrase(passphrase string) error {
}
}
-// PathConf is a path configuration.
-type PathConf struct {
- Regexp *regexp.Regexp `json:"-"`
+// FindPathConf returns the configuration corresponding to the given path name.
+func FindPathConf(pathConfs map[string]*Path, name string) (string, *Path, []string, error) {
+ err := isValidPathName(name)
+ if err != nil {
+ return "", nil, nil, fmt.Errorf("invalid path name: %w (%s)", err, name)
+ }
+
+ // normal path
+ if pathConf, ok := pathConfs[name]; ok {
+ return name, pathConf, nil, nil
+ }
+
+ // regular expression-based path
+ for pathConfName, pathConf := range pathConfs {
+ if pathConf.Regexp != nil && pathConfName != "all" && pathConfName != "all_others" {
+ m := pathConf.Regexp.FindStringSubmatch(name)
+ if m != nil {
+ return pathConfName, pathConf, m, nil
+ }
+ }
+ }
+
+ // all_others
+ for pathConfName, pathConf := range pathConfs {
+ if pathConfName == "all" || pathConfName == "all_others" {
+ m := pathConf.Regexp.FindStringSubmatch(name)
+ if m != nil {
+ return pathConfName, pathConf, m, nil
+ }
+ }
+ }
+
+ return "", nil, nil, fmt.Errorf("path '%s' is not configured", name)
+}
+
+// Path is a path configuration.
+type Path struct {
+ Regexp *regexp.Regexp `json:"-"` // filled by Check()
+ Name string `json:"name"` // filled by Check()
// General
Source string `json:"source"`
@@ -60,7 +94,16 @@ type PathConf struct {
SourceOnDemandCloseAfter StringDuration `json:"sourceOnDemandCloseAfter"`
MaxReaders int `json:"maxReaders"`
SRTReadPassphrase string `json:"srtReadPassphrase"`
- Record bool `json:"record"`
+ Fallback string `json:"fallback"`
+
+ // Record and playback
+ Record bool `json:"record"`
+ Playback bool `json:"playback"`
+ RecordPath string `json:"recordPath"`
+ RecordFormat RecordFormat `json:"recordFormat"`
+ RecordPartDuration StringDuration `json:"recordPartDuration"`
+ RecordSegmentDuration StringDuration `json:"recordSegmentDuration"`
+ RecordDeleteAfter StringDuration `json:"recordDeleteAfter"`
// Authentication
PublishUser Credential `json:"publishUser"`
@@ -70,22 +113,23 @@ type PathConf struct {
ReadPass Credential `json:"readPass"`
ReadIPs IPsOrCIDRs `json:"readIPs"`
- // Publisher
+ // Publisher source
OverridePublisher bool `json:"overridePublisher"`
- DisablePublisherOverride bool `json:"disablePublisherOverride"` // deprecated
- Fallback string `json:"fallback"`
+ DisablePublisherOverride *bool `json:"disablePublisherOverride,omitempty"` // deprecated
SRTPublishPassphrase string `json:"srtPublishPassphrase"`
- // RTSP
- SourceProtocol SourceProtocol `json:"sourceProtocol"`
- SourceAnyPortEnable bool `json:"sourceAnyPortEnable"`
+ // RTSP source
+ RTSPTransport RTSPTransport `json:"rtspTransport"`
+ RTSPAnyPort bool `json:"rtspAnyPort"`
+ SourceProtocol *RTSPTransport `json:"sourceProtocol,omitempty"` // deprecated
+ SourceAnyPortEnable *bool `json:"sourceAnyPortEnable,omitempty"` // deprecated
RTSPRangeType RTSPRangeType `json:"rtspRangeType"`
RTSPRangeStart string `json:"rtspRangeStart"`
- // Redirect
+ // Redirect source
SourceRedirect string `json:"sourceRedirect"`
- // Raspberry Pi Camera
+ // Raspberry Pi Camera source
RPICameraCamID int `json:"rpiCameraCamID"`
RPICameraWidth int `json:"rpiCameraWidth"`
RPICameraHeight int `json:"rpiCameraHeight"`
@@ -126,56 +170,124 @@ type PathConf struct {
RunOnDemandRestart bool `json:"runOnDemandRestart"`
RunOnDemandStartTimeout StringDuration `json:"runOnDemandStartTimeout"`
RunOnDemandCloseAfter StringDuration `json:"runOnDemandCloseAfter"`
+ RunOnUnDemand string `json:"runOnUnDemand"`
RunOnReady string `json:"runOnReady"`
RunOnReadyRestart bool `json:"runOnReadyRestart"`
RunOnNotReady string `json:"runOnNotReady"`
RunOnRead string `json:"runOnRead"`
RunOnReadRestart bool `json:"runOnReadRestart"`
RunOnUnread string `json:"runOnUnread"`
+ RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"`
RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"`
}
-func (pconf *PathConf) check(conf *Conf, name string) error {
+func (pconf *Path) setDefaults() {
+ // General
+ pconf.Source = "publisher"
+ pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
+ pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second)
+
+ // Record and playback
+ pconf.Playback = true
+ pconf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
+ pconf.RecordFormat = RecordFormatFMP4
+ pconf.RecordPartDuration = 100 * StringDuration(time.Millisecond)
+ pconf.RecordSegmentDuration = 3600 * StringDuration(time.Second)
+ pconf.RecordDeleteAfter = 24 * 3600 * StringDuration(time.Second)
+
+ // Publisher source
+ pconf.OverridePublisher = true
+
+ // Raspberry Pi Camera source
+ pconf.RPICameraWidth = 1920
+ pconf.RPICameraHeight = 1080
+ pconf.RPICameraContrast = 1
+ pconf.RPICameraSaturation = 1
+ pconf.RPICameraSharpness = 1
+ pconf.RPICameraExposure = "normal"
+ pconf.RPICameraAWB = "auto"
+ pconf.RPICameraDenoise = "off"
+ pconf.RPICameraMetering = "centre"
+ pconf.RPICameraFPS = 30
+ pconf.RPICameraIDRPeriod = 60
+ pconf.RPICameraBitrate = 1000000
+ pconf.RPICameraProfile = "main"
+ pconf.RPICameraLevel = "4.1"
+ pconf.RPICameraAfMode = "continuous"
+ pconf.RPICameraAfRange = "normal"
+ pconf.RPICameraAfSpeed = "normal"
+ pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX"
+
+ // Hooks
+ pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second)
+ pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second)
+}
+
+func newPath(defaults *Path, partial *OptionalPath) *Path {
+ pconf := &Path{}
+ copyStructFields(pconf, defaults)
+ copyStructFields(pconf, partial.Values)
+ return pconf
+}
+
+// Clone clones the configuration.
+func (pconf Path) Clone() *Path {
+ enc, err := json.Marshal(pconf)
+ if err != nil {
+ panic(err)
+ }
+
+ var dest Path
+ err = json.Unmarshal(enc, &dest)
+ if err != nil {
+ panic(err)
+ }
+
+ dest.Regexp = pconf.Regexp
+
+ return &dest
+}
+
+func (pconf *Path) validate(conf *Conf, name string) error {
+ pconf.Name = name
+
switch {
- case name == "all":
+ case name == "all_others", name == "all":
pconf.Regexp = regexp.MustCompile("^.*$")
case name == "" || name[0] != '~': // normal path
- err := IsValidPathName(name)
+ err := isValidPathName(name)
if err != nil {
- return fmt.Errorf("invalid path name '%s': %s", name, err)
+ return fmt.Errorf("invalid path name '%s': %w", name, err)
}
default: // regular expression-based path
- pathRegexp, err := regexp.Compile(name[1:])
+ regexp, err := regexp.Compile(name[1:])
if err != nil {
return fmt.Errorf("invalid regular expression: %s", name[1:])
}
- pconf.Regexp = pathRegexp
+ pconf.Regexp = regexp
}
// General
+ if pconf.Source != "publisher" && pconf.Source != "redirect" &&
+ pconf.Regexp != nil && !pconf.SourceOnDemand {
+ return fmt.Errorf("a path with a regular expression (or path 'all') and a static source" +
+ " must have 'sourceOnDemand' set to true")
+ }
switch {
case pconf.Source == "publisher":
case strings.HasPrefix(pconf.Source, "rtsp://") ||
strings.HasPrefix(pconf.Source, "rtsps://"):
- if pconf.Regexp != nil {
- return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTSP source. use another path")
- }
-
- _, err := url.Parse(pconf.Source)
+ _, err := base.ParseURL(pconf.Source)
if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
}
case strings.HasPrefix(pconf.Source, "rtmp://") ||
strings.HasPrefix(pconf.Source, "rtmps://"):
- if pconf.Regexp != nil {
- return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source. use another path")
- }
-
u, err := gourl.Parse(pconf.Source)
if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
@@ -192,10 +304,6 @@ func (pconf *PathConf) check(conf *Conf, name string) error {
case strings.HasPrefix(pconf.Source, "http://") ||
strings.HasPrefix(pconf.Source, "https://"):
- if pconf.Regexp != nil {
- return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a HLS source. use another path")
- }
-
u, err := gourl.Parse(pconf.Source)
if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
@@ -214,19 +322,12 @@ func (pconf *PathConf) check(conf *Conf, name string) error {
}
case strings.HasPrefix(pconf.Source, "udp://"):
- if pconf.Regexp != nil {
- return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a HLS source. use another path")
- }
-
_, _, err := net.SplitHostPort(pconf.Source[len("udp://"):])
if err != nil {
return fmt.Errorf("'%s' is not a valid UDP URL", pconf.Source)
}
case strings.HasPrefix(pconf.Source, "srt://"):
- if pconf.Regexp != nil {
- return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a SRT source. use another path")
- }
_, err := gourl.Parse(pconf.Source)
if err != nil {
@@ -235,86 +336,18 @@ func (pconf *PathConf) check(conf *Conf, name string) error {
case strings.HasPrefix(pconf.Source, "whep://") ||
strings.HasPrefix(pconf.Source, "wheps://"):
- if pconf.Regexp != nil {
- return fmt.Errorf("a path with a regular expression (or path 'all') " +
- "cannot have a WebRTC/WHEP source. use another path")
- }
-
_, err := gourl.Parse(pconf.Source)
if err != nil {
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
}
case pconf.Source == "redirect":
- if pconf.SourceRedirect == "" {
- return fmt.Errorf("source redirect must be filled")
- }
-
- _, err := url.Parse(pconf.SourceRedirect)
- if err != nil {
- return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.SourceRedirect)
- }
case pconf.Source == "rpiCamera":
- if pconf.Regexp != nil {
- return fmt.Errorf(
- "a path with a regular expression (or path 'all') cannot have 'rpiCamera' as source. use another path")
- }
-
- for otherName, otherPath := range conf.Paths {
- if otherPath != pconf && otherPath != nil &&
- otherPath.Source == "rpiCamera" && otherPath.RPICameraCamID == pconf.RPICameraCamID {
- return fmt.Errorf("'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'",
- pconf.RPICameraCamID, name, otherName)
- }
- }
-
- switch pconf.RPICameraExposure {
- case "normal", "short", "long", "custom":
- default:
- return fmt.Errorf("invalid 'rpiCameraExposure' value")
- }
-
- switch pconf.RPICameraAWB {
- case "auto", "incandescent", "tungsten", "fluorescent", "indoor", "daylight", "cloudy", "custom":
- default:
- return fmt.Errorf("invalid 'rpiCameraAWB' value")
- }
-
- switch pconf.RPICameraDenoise {
- case "off", "cdn_off", "cdn_fast", "cdn_hq":
- default:
- return fmt.Errorf("invalid 'rpiCameraDenoise' value")
- }
-
- switch pconf.RPICameraMetering {
- case "centre", "spot", "matrix", "custom":
- default:
- return fmt.Errorf("invalid 'rpiCameraMetering' value")
- }
-
- switch pconf.RPICameraAfMode {
- case "auto", "manual", "continuous":
- default:
- return fmt.Errorf("invalid 'rpiCameraAfMode' value")
- }
-
- switch pconf.RPICameraAfRange {
- case "normal", "macro", "full":
- default:
- return fmt.Errorf("invalid 'rpiCameraAfRange' value")
- }
-
- switch pconf.RPICameraAfSpeed {
- case "normal", "fast":
- default:
- return fmt.Errorf("invalid 'rpiCameraAfSpeed' value")
- }
default:
return fmt.Errorf("invalid source: '%s'", pconf.Source)
}
-
if pconf.SourceOnDemand {
if pconf.Source == "publisher" {
return fmt.Errorf("'sourceOnDemand' is useless when source is 'publisher'")
@@ -323,50 +356,30 @@ func (pconf *PathConf) check(conf *Conf, name string) error {
if pconf.SRTReadPassphrase != "" {
err := srtCheckPassphrase(pconf.SRTReadPassphrase)
if err != nil {
- return fmt.Errorf("invalid 'readRTPassphrase': %v", err)
+ return fmt.Errorf("invalid 'readRTPassphrase': %w", err)
}
}
-
- // Publisher
-
- if pconf.DisablePublisherOverride {
- pconf.OverridePublisher = true
- }
if pconf.Fallback != "" {
- if pconf.Source != "publisher" {
- return fmt.Errorf("'fallback' can only be used when source is 'publisher'")
- }
-
if strings.HasPrefix(pconf.Fallback, "/") {
- err := IsValidPathName(pconf.Fallback[1:])
+ err := isValidPathName(pconf.Fallback[1:])
if err != nil {
- return fmt.Errorf("'%s': %s", pconf.Fallback, err)
+ return fmt.Errorf("'%s': %w", pconf.Fallback, err)
}
} else {
- _, err := url.Parse(pconf.Fallback)
+ _, err := base.ParseURL(pconf.Fallback)
if err != nil {
return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.Fallback)
}
}
}
- if pconf.SRTPublishPassphrase != "" {
- if pconf.Source != "publisher" {
- return fmt.Errorf("'srtPublishPassphase' can only be used when source is 'publisher'")
- }
-
- err := srtCheckPassphrase(pconf.SRTPublishPassphrase)
- if err != nil {
- return fmt.Errorf("invalid 'srtPublishPassphrase': %v", err)
- }
- }
// Authentication
- if (pconf.PublishUser != "" && pconf.PublishPass == "") ||
- (pconf.PublishUser == "" && pconf.PublishPass != "") {
+ if (!pconf.PublishUser.IsEmpty() && pconf.PublishPass.IsEmpty()) ||
+ (pconf.PublishUser.IsEmpty() && !pconf.PublishPass.IsEmpty()) {
return fmt.Errorf("read username and password must be both filled")
}
- if pconf.PublishUser != "" && pconf.Source != "publisher" {
+ if !pconf.PublishUser.IsEmpty() && pconf.Source != "publisher" {
return fmt.Errorf("'publishUser' is useless when source is not 'publisher', since " +
"the stream is not provided by a publisher, but by a fixed source")
}
@@ -374,65 +387,132 @@ func (pconf *PathConf) check(conf *Conf, name string) error {
return fmt.Errorf("'publishIPs' is useless when source is not 'publisher', since " +
"the stream is not provided by a publisher, but by a fixed source")
}
- if (pconf.ReadUser != "" && pconf.ReadPass == "") ||
- (pconf.ReadUser == "" && pconf.ReadPass != "") {
+ if (!pconf.ReadUser.IsEmpty() && pconf.ReadPass.IsEmpty()) ||
+ (pconf.ReadUser.IsEmpty() && !pconf.ReadPass.IsEmpty()) {
return fmt.Errorf("read username and password must be both filled")
}
if contains(conf.AuthMethods, headers.AuthDigest) {
- if strings.HasPrefix(string(pconf.PublishUser), "sha256:") ||
- strings.HasPrefix(string(pconf.PublishPass), "sha256:") ||
- strings.HasPrefix(string(pconf.ReadUser), "sha256:") ||
- strings.HasPrefix(string(pconf.ReadPass), "sha256:") {
+ if pconf.PublishUser.IsHashed() ||
+ pconf.PublishPass.IsHashed() ||
+ pconf.ReadUser.IsHashed() ||
+ pconf.ReadPass.IsHashed() {
return fmt.Errorf("hashed credentials can't be used when the digest auth method is available")
}
}
if conf.ExternalAuthenticationURL != "" {
- if pconf.PublishUser != "" ||
+ if !pconf.PublishUser.IsEmpty() ||
len(pconf.PublishIPs) > 0 ||
- pconf.ReadUser != "" ||
+ !pconf.ReadUser.IsEmpty() ||
len(pconf.ReadIPs) > 0 {
return fmt.Errorf("credentials or IPs can't be used together with 'externalAuthenticationURL'")
}
}
+ // Publisher source
+
+ if pconf.DisablePublisherOverride != nil {
+ pconf.OverridePublisher = !*pconf.DisablePublisherOverride
+ }
+ if pconf.SRTPublishPassphrase != "" {
+ if pconf.Source != "publisher" {
+ return fmt.Errorf("'srtPublishPassphase' can only be used when source is 'publisher'")
+ }
+
+ err := srtCheckPassphrase(pconf.SRTPublishPassphrase)
+ if err != nil {
+ return fmt.Errorf("invalid 'srtPublishPassphrase': %w", err)
+ }
+ }
+
+ // RTSP source
+
+ if pconf.SourceProtocol != nil {
+ pconf.RTSPTransport = *pconf.SourceProtocol
+ }
+ if pconf.SourceAnyPortEnable != nil {
+ pconf.RTSPAnyPort = *pconf.SourceAnyPortEnable
+ }
+
+ // Redirect source
+
+ if pconf.Source == "redirect" {
+ if pconf.SourceRedirect == "" {
+ return fmt.Errorf("source redirect must be filled")
+ }
+
+ _, err := base.ParseURL(pconf.SourceRedirect)
+ if err != nil {
+ return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.SourceRedirect)
+ }
+ }
+
+ // Raspberry Pi Camera source
+
+ if pconf.Source == "rpiCamera" {
+ for otherName, otherPath := range conf.Paths {
+ if otherPath != pconf && otherPath != nil &&
+ otherPath.Source == "rpiCamera" && otherPath.RPICameraCamID == pconf.RPICameraCamID {
+ return fmt.Errorf("'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'",
+ pconf.RPICameraCamID, name, otherName)
+ }
+ }
+ }
+ switch pconf.RPICameraExposure {
+ case "normal", "short", "long", "custom":
+ default:
+ return fmt.Errorf("invalid 'rpiCameraExposure' value")
+ }
+ switch pconf.RPICameraAWB {
+ case "auto", "incandescent", "tungsten", "fluorescent", "indoor", "daylight", "cloudy", "custom":
+ default:
+ return fmt.Errorf("invalid 'rpiCameraAWB' value")
+ }
+ switch pconf.RPICameraDenoise {
+ case "off", "cdn_off", "cdn_fast", "cdn_hq":
+ default:
+ return fmt.Errorf("invalid 'rpiCameraDenoise' value")
+ }
+ switch pconf.RPICameraMetering {
+ case "centre", "spot", "matrix", "custom":
+ default:
+ return fmt.Errorf("invalid 'rpiCameraMetering' value")
+ }
+ switch pconf.RPICameraAfMode {
+ case "auto", "manual", "continuous":
+ default:
+ return fmt.Errorf("invalid 'rpiCameraAfMode' value")
+ }
+ switch pconf.RPICameraAfRange {
+ case "normal", "macro", "full":
+ default:
+ return fmt.Errorf("invalid 'rpiCameraAfRange' value")
+ }
+ switch pconf.RPICameraAfSpeed {
+ case "normal", "fast":
+ default:
+ return fmt.Errorf("invalid 'rpiCameraAfSpeed' value")
+ }
+
// Hooks
if pconf.RunOnInit != "" && pconf.Regexp != nil {
return fmt.Errorf("a path with a regular expression (or path 'all')" +
" does not support option 'runOnInit'; use another path")
}
- if pconf.RunOnDemand != "" && pconf.Source != "publisher" {
- return fmt.Errorf("'runOnDemand' can be used only when source is 'publisher'")
+ if (pconf.RunOnDemand != "" || pconf.RunOnUnDemand != "") && pconf.Source != "publisher" {
+ return fmt.Errorf("'runOnDemand' and 'runOnUnDemand' can be used only when source is 'publisher'")
}
return nil
}
-// Equal checks whether two PathConfs are equal.
-func (pconf *PathConf) Equal(other *PathConf) bool {
+// Equal checks whether two Paths are equal.
+func (pconf *Path) Equal(other *Path) bool {
return reflect.DeepEqual(pconf, other)
}
-// Clone clones the configuration.
-func (pconf PathConf) Clone() *PathConf {
- enc, err := json.Marshal(pconf)
- if err != nil {
- panic(err)
- }
-
- var dest PathConf
- err = json.Unmarshal(enc, &dest)
- if err != nil {
- panic(err)
- }
-
- dest.Regexp = pconf.Regexp
-
- return &dest
-}
-
// HasStaticSource checks whether the path has a static source.
-func (pconf PathConf) HasStaticSource() bool {
+func (pconf Path) HasStaticSource() bool {
return strings.HasPrefix(pconf.Source, "rtsp://") ||
strings.HasPrefix(pconf.Source, "rtsps://") ||
strings.HasPrefix(pconf.Source, "rtmp://") ||
@@ -447,54 +527,11 @@ func (pconf PathConf) HasStaticSource() bool {
}
// HasOnDemandStaticSource checks whether the path has a on demand static source.
-func (pconf PathConf) HasOnDemandStaticSource() bool {
+func (pconf Path) HasOnDemandStaticSource() bool {
return pconf.HasStaticSource() && pconf.SourceOnDemand
}
// HasOnDemandPublisher checks whether the path has a on-demand publisher.
-func (pconf PathConf) HasOnDemandPublisher() bool {
+func (pconf Path) HasOnDemandPublisher() bool {
return pconf.RunOnDemand != ""
}
-
-// UnmarshalJSON implements json.Unmarshaler. It is used to:
-// - force DisallowUnknownFields
-// - set default values
-func (pconf *PathConf) UnmarshalJSON(b []byte) error {
- // General
- pconf.Source = "publisher"
- pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
- pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second)
- pconf.Record = true
-
- // Publisher
- pconf.OverridePublisher = true
-
- // Raspberry Pi Camera
- pconf.RPICameraWidth = 1920
- pconf.RPICameraHeight = 1080
- pconf.RPICameraContrast = 1
- pconf.RPICameraSaturation = 1
- pconf.RPICameraSharpness = 1
- pconf.RPICameraExposure = "normal"
- pconf.RPICameraAWB = "auto"
- pconf.RPICameraDenoise = "off"
- pconf.RPICameraMetering = "centre"
- pconf.RPICameraFPS = 30
- pconf.RPICameraIDRPeriod = 60
- pconf.RPICameraBitrate = 1000000
- pconf.RPICameraProfile = "main"
- pconf.RPICameraLevel = "4.1"
- pconf.RPICameraAfMode = "auto"
- pconf.RPICameraAfRange = "normal"
- pconf.RPICameraAfSpeed = "normal"
- pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX"
-
- // Hooks
- pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second)
- pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second)
-
- type alias PathConf
- d := json.NewDecoder(bytes.NewReader(b))
- d.DisallowUnknownFields()
- return d.Decode((*alias)(pconf))
-}
diff --git a/internal/conf/protocol.go b/internal/conf/protocol.go
index 5c1713d55e7..e2e46c94ccb 100644
--- a/internal/conf/protocol.go
+++ b/internal/conf/protocol.go
@@ -74,8 +74,8 @@ func (d *Protocols) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *Protocols) UnmarshalEnv(s string) error {
- byts, _ := json.Marshal(strings.Split(s, ","))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *Protocols) UnmarshalEnv(_ string, v string) error {
+ byts, _ := json.Marshal(strings.Split(v, ","))
return d.UnmarshalJSON(byts)
}
diff --git a/internal/conf/record_format.go b/internal/conf/record_format.go
new file mode 100644
index 00000000000..e9e39f81dfe
--- /dev/null
+++ b/internal/conf/record_format.go
@@ -0,0 +1,56 @@
+package conf
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// RecordFormat is the recordFormat parameter.
+type RecordFormat int
+
+// supported values.
+const (
+ RecordFormatFMP4 RecordFormat = iota
+ RecordFormatMPEGTS
+)
+
+// MarshalJSON implements json.Marshaler.
+func (d RecordFormat) MarshalJSON() ([]byte, error) {
+ var out string
+
+ switch d {
+ case RecordFormatMPEGTS:
+ out = "mpegts"
+
+ default:
+ out = "fmp4"
+ }
+
+ return json.Marshal(out)
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (d *RecordFormat) UnmarshalJSON(b []byte) error {
+ var in string
+ if err := json.Unmarshal(b, &in); err != nil {
+ return err
+ }
+
+ switch in {
+ case "mpegts":
+ *d = RecordFormatMPEGTS
+
+ case "fmp4":
+ *d = RecordFormatFMP4
+
+ default:
+ return fmt.Errorf("invalid record format '%s'", in)
+ }
+
+ return nil
+}
+
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *RecordFormat) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
+}
diff --git a/internal/conf/rtsp_range_type.go b/internal/conf/rtsp_range_type.go
index 6acecd24ebc..2a6decd2bb3 100644
--- a/internal/conf/rtsp_range_type.go
+++ b/internal/conf/rtsp_range_type.go
@@ -8,7 +8,7 @@ import (
// RTSPRangeType is the type used in the Range header.
type RTSPRangeType int
-// supported rtsp range types.
+// supported values.
const (
RTSPRangeTypeUndefined RTSPRangeType = iota
RTSPRangeTypeClock
@@ -67,7 +67,7 @@ func (d *RTSPRangeType) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *RTSPRangeType) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *RTSPRangeType) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
}
diff --git a/internal/conf/source_protocol.go b/internal/conf/rtsp_transport.go
similarity index 74%
rename from internal/conf/source_protocol.go
rename to internal/conf/rtsp_transport.go
index c681f552181..cb49a11c4d0 100644
--- a/internal/conf/source_protocol.go
+++ b/internal/conf/rtsp_transport.go
@@ -7,13 +7,13 @@ import (
"github.com/bluenviron/gortsplib/v4"
)
-// SourceProtocol is the sourceProtocol parameter.
-type SourceProtocol struct {
+// RTSPTransport is the rtspTransport parameter.
+type RTSPTransport struct {
*gortsplib.Transport
}
// MarshalJSON implements json.Marshaler.
-func (d SourceProtocol) MarshalJSON() ([]byte, error) {
+func (d RTSPTransport) MarshalJSON() ([]byte, error) {
var out string
if d.Transport == nil {
@@ -38,7 +38,7 @@ func (d SourceProtocol) MarshalJSON() ([]byte, error) {
}
// UnmarshalJSON implements json.Unmarshaler.
-func (d *SourceProtocol) UnmarshalJSON(b []byte) error {
+func (d *RTSPTransport) UnmarshalJSON(b []byte) error {
var in string
if err := json.Unmarshal(b, &in); err != nil {
return err
@@ -67,7 +67,7 @@ func (d *SourceProtocol) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *SourceProtocol) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *RTSPTransport) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
}
diff --git a/internal/conf/string_duration.go b/internal/conf/string_duration.go
index a1184f2c0e7..57e6dbaae40 100644
--- a/internal/conf/string_duration.go
+++ b/internal/conf/string_duration.go
@@ -30,7 +30,7 @@ func (d *StringDuration) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (d *StringDuration) UnmarshalEnv(s string) error {
- return d.UnmarshalJSON([]byte(`"` + s + `"`))
+// UnmarshalEnv implements env.Unmarshaler.
+func (d *StringDuration) UnmarshalEnv(_ string, v string) error {
+ return d.UnmarshalJSON([]byte(`"` + v + `"`))
}
diff --git a/internal/conf/string_size.go b/internal/conf/string_size.go
index 4dba8e1c70a..e3a1dcc45d8 100644
--- a/internal/conf/string_size.go
+++ b/internal/conf/string_size.go
@@ -30,7 +30,7 @@ func (s *StringSize) UnmarshalJSON(b []byte) error {
return nil
}
-// UnmarshalEnv implements envUnmarshaler.
-func (s *StringSize) UnmarshalEnv(v string) error {
+// UnmarshalEnv implements env.Unmarshaler.
+func (s *StringSize) UnmarshalEnv(_ string, v string) error {
return s.UnmarshalJSON([]byte(`"` + v + `"`))
}
diff --git a/internal/confwatcher/confwatcher.go b/internal/confwatcher/confwatcher.go
index f721dff89f5..7abb03bf1aa 100644
--- a/internal/confwatcher/confwatcher.go
+++ b/internal/confwatcher/confwatcher.go
@@ -30,14 +30,7 @@ type ConfWatcher struct {
// New allocates a ConfWatcher.
func New(confPath string) (*ConfWatcher, error) {
if _, err := os.Stat(confPath); err != nil {
- if confPath == "mediamtx.yml" {
- confPath = "rtsp-simple-server.yml"
- if _, err := os.Stat(confPath); err != nil {
- return nil, err
- }
- } else {
- return nil, err
- }
+ return nil, err
}
inner, err := fsnotify.NewWatcher()
@@ -90,6 +83,7 @@ outer:
currentWatchedPath, _ := filepath.EvalSymlinks(w.watchedPath)
eventPath, _ := filepath.Abs(event.Name)
+ eventPath, _ = filepath.EvalSymlinks(eventPath)
if currentWatchedPath == "" {
// watched file was removed; wait for write event to trigger reload
diff --git a/internal/core/api.go b/internal/core/api.go
deleted file mode 100644
index f66f4adc749..00000000000
--- a/internal/core/api.go
+++ /dev/null
@@ -1,947 +0,0 @@
-package core
-
-import (
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "reflect"
- "strconv"
- "sync"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/httpserv"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-var errAPINotFound = errors.New("not found")
-
-func interfaceIsEmpty(i interface{}) bool {
- return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil()
-}
-
-func fillStruct(dest interface{}, source interface{}) {
- rvsource := reflect.ValueOf(source).Elem()
- rvdest := reflect.ValueOf(dest)
- nf := rvsource.NumField()
- for i := 0; i < nf; i++ {
- fnew := rvsource.Field(i)
- if !fnew.IsNil() {
- f := rvdest.Elem().FieldByName(rvsource.Type().Field(i).Name)
- if f.Kind() == reflect.Ptr {
- f.Set(fnew)
- } else {
- f.Set(fnew.Elem())
- }
- }
- }
-}
-
-func generateStructWithOptionalFields(model interface{}) interface{} {
- var fields []reflect.StructField
-
- rt := reflect.TypeOf(model)
- nf := rt.NumField()
- for i := 0; i < nf; i++ {
- f := rt.Field(i)
- j := f.Tag.Get("json")
-
- if j != "-" && j != "paths" {
- fields = append(fields, reflect.StructField{
- Name: f.Name,
- Type: reflect.PtrTo(f.Type),
- Tag: f.Tag,
- })
- }
- }
-
- return reflect.New(reflect.StructOf(fields)).Interface()
-}
-
-func loadConfData(ctx *gin.Context) (interface{}, error) {
- in := generateStructWithOptionalFields(conf.Conf{})
- d := json.NewDecoder(ctx.Request.Body)
- d.DisallowUnknownFields()
- err := d.Decode(in)
- if err != nil {
- return nil, err
- }
-
- return in, err
-}
-
-func loadConfPathData(ctx *gin.Context) (interface{}, error) {
- in := generateStructWithOptionalFields(conf.PathConf{})
- d := json.NewDecoder(ctx.Request.Body)
- d.DisallowUnknownFields()
- err := d.Decode(in)
- if err != nil {
- return nil, err
- }
-
- return in, err
-}
-
-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 - 1
- }
-
- 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 paramName(ctx *gin.Context) (string, bool) {
- name := ctx.Param("name")
-
- if len(name) < 2 || name[0] != '/' {
- return "", false
- }
-
- return name[1:], true
-}
-
-type apiPathManager interface {
- apiPathsList() (*apiPathsList, error)
- apiPathsGet(string) (*apiPath, error)
-}
-
-type apiHLSManager interface {
- apiMuxersList() (*apiHLSMuxersList, error)
- apiMuxersGet(string) (*apiHLSMuxer, error)
-}
-
-type apiRTSPServer interface {
- apiConnsList() (*apiRTSPConnsList, error)
- apiConnsGet(uuid.UUID) (*apiRTSPConn, error)
- apiSessionsList() (*apiRTSPSessionsList, error)
- apiSessionsGet(uuid.UUID) (*apiRTSPSession, error)
- apiSessionsKick(uuid.UUID) error
-}
-
-type apiRTMPServer interface {
- apiConnsList() (*apiRTMPConnsList, error)
- apiConnsGet(uuid.UUID) (*apiRTMPConn, error)
- apiConnsKick(uuid.UUID) error
-}
-
-type apiWebRTCManager interface {
- apiSessionsList() (*apiWebRTCSessionsList, error)
- apiSessionsGet(uuid.UUID) (*apiWebRTCSession, error)
- apiSessionsKick(uuid.UUID) error
-}
-
-type apiSRTServer interface {
- apiConnsList() (*apiSRTConnsList, error)
- apiConnsGet(uuid.UUID) (*apiSRTConn, error)
- apiConnsKick(uuid.UUID) error
-}
-
-type apiParent interface {
- logger.Writer
- apiConfigSet(conf *conf.Conf)
-}
-
-type api struct {
- conf *conf.Conf
- pathManager apiPathManager
- rtspServer apiRTSPServer
- rtspsServer apiRTSPServer
- rtmpServer apiRTMPServer
- rtmpsServer apiRTMPServer
- hlsManager apiHLSManager
- webRTCManager apiWebRTCManager
- srtServer apiSRTServer
- parent apiParent
-
- httpServer *httpserv.WrappedServer
- mutex sync.Mutex
-}
-
-func newAPI(
- address string,
- readTimeout conf.StringDuration,
- conf *conf.Conf,
- pathManager apiPathManager,
- rtspServer apiRTSPServer,
- rtspsServer apiRTSPServer,
- rtmpServer apiRTMPServer,
- rtmpsServer apiRTMPServer,
- hlsManager apiHLSManager,
- webRTCManager apiWebRTCManager,
- srtServer apiSRTServer,
- parent apiParent,
-) (*api, error) {
- a := &api{
- conf: conf,
- pathManager: pathManager,
- rtspServer: rtspServer,
- rtspsServer: rtspsServer,
- rtmpServer: rtmpServer,
- rtmpsServer: rtmpsServer,
- hlsManager: hlsManager,
- webRTCManager: webRTCManager,
- srtServer: srtServer,
- parent: parent,
- }
-
- router := gin.New()
- router.SetTrustedProxies(nil) //nolint:errcheck
-
- group := router.Group("/")
-
- group.GET("/v2/config/get", a.onConfigGet)
- group.POST("/v2/config/set", a.onConfigSet)
- group.POST("/v2/config/paths/add/*name", a.onConfigPathsAdd)
- group.POST("/v2/config/paths/edit/*name", a.onConfigPathsEdit)
- group.POST("/v2/config/paths/remove/*name", a.onConfigPathsDelete)
-
- if !interfaceIsEmpty(a.hlsManager) {
- group.GET("/v2/hlsmuxers/list", a.onHLSMuxersList)
- group.GET("/v2/hlsmuxers/get/*name", a.onHLSMuxersGet)
- }
-
- group.GET("/v2/paths/list", a.onPathsList)
- group.GET("/v2/paths/get/*name", a.onPathsGet)
-
- if !interfaceIsEmpty(a.rtspServer) {
- group.GET("/v2/rtspconns/list", a.onRTSPConnsList)
- group.GET("/v2/rtspconns/get/:id", a.onRTSPConnsGet)
- group.GET("/v2/rtspsessions/list", a.onRTSPSessionsList)
- group.GET("/v2/rtspsessions/get/:id", a.onRTSPSessionsGet)
- group.POST("/v2/rtspsessions/kick/:id", a.onRTSPSessionsKick)
- }
-
- if !interfaceIsEmpty(a.rtspsServer) {
- group.GET("/v2/rtspsconns/list", a.onRTSPSConnsList)
- group.GET("/v2/rtspsconns/get/:id", a.onRTSPSConnsGet)
- group.GET("/v2/rtspssessions/list", a.onRTSPSSessionsList)
- group.GET("/v2/rtspssessions/get/:id", a.onRTSPSSessionsGet)
- group.POST("/v2/rtspssessions/kick/:id", a.onRTSPSSessionsKick)
- }
-
- if !interfaceIsEmpty(a.rtmpServer) {
- group.GET("/v2/rtmpconns/list", a.onRTMPConnsList)
- group.GET("/v2/rtmpconns/get/:id", a.onRTMPConnsGet)
- group.POST("/v2/rtmpconns/kick/:id", a.onRTMPConnsKick)
- }
-
- if !interfaceIsEmpty(a.rtmpsServer) {
- group.GET("/v2/rtmpsconns/list", a.onRTMPSConnsList)
- group.GET("/v2/rtmpsconns/get/:id", a.onRTMPSConnsGet)
- group.POST("/v2/rtmpsconns/kick/:id", a.onRTMPSConnsKick)
- }
-
- if !interfaceIsEmpty(a.webRTCManager) {
- group.GET("/v2/webrtcsessions/list", a.onWebRTCSessionsList)
- group.GET("/v2/webrtcsessions/get/:id", a.onWebRTCSessionsGet)
- group.POST("/v2/webrtcsessions/kick/:id", a.onWebRTCSessionsKick)
- }
-
- if !interfaceIsEmpty(a.srtServer) {
- group.GET("/v2/srtconns/list", a.onSRTConnsList)
- group.GET("/v2/srtconns/get/:id", a.onSRTConnsGet)
- group.POST("/v2/srtconns/kick/:id", a.onSRTConnsKick)
- }
-
- network, address := restrictNetwork("tcp", address)
-
- var err error
- a.httpServer, err = httpserv.NewWrappedServer(
- network,
- address,
- time.Duration(readTimeout),
- "",
- "",
- router,
- a,
- )
- if err != nil {
- return nil, err
- }
-
- a.Log(logger.Info, "listener opened on "+address)
-
- return a, nil
-}
-
-func (a *api) close() {
- a.Log(logger.Info, "listener is closing")
- a.httpServer.Close()
-}
-
-func (a *api) Log(level logger.Level, format string, args ...interface{}) {
- a.parent.Log(level, "[API] "+format, args...)
-}
-
-// error coming from something the user inserted into the request.
-func (a *api) writeUserError(ctx *gin.Context, err error) {
- a.Log(logger.Error, err.Error())
- ctx.AbortWithStatus(http.StatusBadRequest)
-}
-
-// error coming from the server.
-func (a *api) writeServerError(ctx *gin.Context, err error) {
- a.Log(logger.Error, err.Error())
- ctx.AbortWithStatus(http.StatusInternalServerError)
-}
-
-func (a *api) writeNotFound(ctx *gin.Context) {
- ctx.AbortWithStatus(http.StatusNotFound)
-}
-
-func (a *api) writeServerErrorOrNotFound(ctx *gin.Context, err error) {
- if err == errAPINotFound {
- a.writeNotFound(ctx)
- } else {
- a.writeServerError(ctx, err)
- }
-}
-
-func (a *api) onConfigGet(ctx *gin.Context) {
- a.mutex.Lock()
- c := a.conf
- a.mutex.Unlock()
-
- ctx.JSON(http.StatusOK, c)
-}
-
-func (a *api) onConfigSet(ctx *gin.Context) {
- in, err := loadConfData(ctx)
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- a.mutex.Lock()
- defer a.mutex.Unlock()
-
- newConf := a.conf.Clone()
-
- fillStruct(newConf, in)
-
- err = newConf.Check()
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- a.conf = newConf
-
- // since reloading the configuration can cause the shutdown of the API,
- // call it in a goroutine
- go a.parent.apiConfigSet(newConf)
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onConfigPathsAdd(ctx *gin.Context) {
- name, ok := paramName(ctx)
- if !ok {
- a.writeUserError(ctx, fmt.Errorf("invalid name"))
- return
- }
-
- in, err := loadConfPathData(ctx)
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- a.mutex.Lock()
- defer a.mutex.Unlock()
-
- newConf := a.conf.Clone()
-
- if _, ok := newConf.Paths[name]; ok {
- a.writeUserError(ctx, fmt.Errorf("path already exists"))
- return
- }
-
- newConfPath := &conf.PathConf{}
-
- // load default values
- newConfPath.UnmarshalJSON([]byte("{}")) //nolint:errcheck
-
- fillStruct(newConfPath, in)
-
- newConf.Paths[name] = newConfPath
-
- err = newConf.Check()
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- a.conf = newConf
-
- // since reloading the configuration can cause the shutdown of the API,
- // call it in a goroutine
- go a.parent.apiConfigSet(newConf)
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onConfigPathsEdit(ctx *gin.Context) {
- name, ok := paramName(ctx)
- if !ok {
- a.writeUserError(ctx, fmt.Errorf("invalid name"))
- return
- }
-
- in, err := loadConfPathData(ctx)
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- a.mutex.Lock()
- defer a.mutex.Unlock()
-
- newConf := a.conf.Clone()
-
- newConfPath, ok := newConf.Paths[name]
- if !ok {
- a.writeNotFound(ctx)
- return
- }
-
- fillStruct(newConfPath, in)
-
- err = newConf.Check()
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- a.conf = newConf
-
- // since reloading the configuration can cause the shutdown of the API,
- // call it in a goroutine
- go a.parent.apiConfigSet(newConf)
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onConfigPathsDelete(ctx *gin.Context) {
- name, ok := paramName(ctx)
- if !ok {
- a.writeUserError(ctx, fmt.Errorf("invalid name"))
- return
- }
-
- a.mutex.Lock()
- defer a.mutex.Unlock()
-
- if _, ok := a.conf.Paths[name]; !ok {
- a.writeNotFound(ctx)
- return
- }
-
- newConf := a.conf.Clone()
- delete(newConf.Paths, name)
-
- err := newConf.Check()
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- a.conf = newConf
-
- // since reloading the configuration can cause the shutdown of the API,
- // call it in a goroutine
- go a.parent.apiConfigSet(newConf)
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onPathsList(ctx *gin.Context) {
- data, err := a.pathManager.apiPathsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onPathsGet(ctx *gin.Context) {
- name, ok := paramName(ctx)
- if !ok {
- a.writeUserError(ctx, fmt.Errorf("invalid name"))
- return
- }
-
- data, err := a.pathManager.apiPathsGet(name)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPConnsList(ctx *gin.Context) {
- data, err := a.rtspServer.apiConnsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPConnsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.rtspServer.apiConnsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPSessionsList(ctx *gin.Context) {
- data, err := a.rtspServer.apiSessionsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPSessionsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.rtspServer.apiSessionsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPSessionsKick(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- err = a.rtspServer.apiSessionsKick(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onRTSPSConnsList(ctx *gin.Context) {
- data, err := a.rtspsServer.apiConnsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPSConnsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.rtspsServer.apiConnsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPSSessionsList(ctx *gin.Context) {
- data, err := a.rtspsServer.apiSessionsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPSSessionsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.rtspsServer.apiSessionsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTSPSSessionsKick(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- err = a.rtspsServer.apiSessionsKick(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onRTMPConnsList(ctx *gin.Context) {
- data, err := a.rtmpServer.apiConnsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTMPConnsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.rtmpServer.apiConnsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTMPConnsKick(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- err = a.rtmpServer.apiConnsKick(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onRTMPSConnsList(ctx *gin.Context) {
- data, err := a.rtmpsServer.apiConnsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTMPSConnsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.rtmpsServer.apiConnsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onRTMPSConnsKick(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- err = a.rtmpsServer.apiConnsKick(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onHLSMuxersList(ctx *gin.Context) {
- data, err := a.hlsManager.apiMuxersList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onHLSMuxersGet(ctx *gin.Context) {
- name, ok := paramName(ctx)
- if !ok {
- a.writeUserError(ctx, fmt.Errorf("invalid name"))
- return
- }
-
- data, err := a.hlsManager.apiMuxersGet(name)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onWebRTCSessionsList(ctx *gin.Context) {
- data, err := a.webRTCManager.apiSessionsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onWebRTCSessionsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.webRTCManager.apiSessionsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onWebRTCSessionsKick(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- err = a.webRTCManager.apiSessionsKick(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.Status(http.StatusOK)
-}
-
-func (a *api) onSRTConnsList(ctx *gin.Context) {
- data, err := a.srtServer.apiConnsList()
- if err != nil {
- a.writeServerError(ctx, err)
- return
- }
-
- data.ItemCount = len(data.Items)
- pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
- data.PageCount = pageCount
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onSRTConnsGet(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- data, err := a.srtServer.apiConnsGet(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.JSON(http.StatusOK, data)
-}
-
-func (a *api) onSRTConnsKick(ctx *gin.Context) {
- uuid, err := uuid.Parse(ctx.Param("id"))
- if err != nil {
- a.writeUserError(ctx, err)
- return
- }
-
- err = a.srtServer.apiConnsKick(uuid)
- if err != nil {
- a.writeServerErrorOrNotFound(ctx, err)
- return
- }
-
- ctx.Status(http.StatusOK)
-}
-
-// confReload is called by core.
-func (a *api) confReload(conf *conf.Conf) {
- a.mutex.Lock()
- defer a.mutex.Unlock()
- a.conf = conf
-}
diff --git a/internal/core/api_defs.go b/internal/core/api_defs.go
deleted file mode 100644
index 6be6978d430..00000000000
--- a/internal/core/api_defs.go
+++ /dev/null
@@ -1,159 +0,0 @@
-package core
-
-import (
- "time"
-
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/conf"
-)
-
-type apiPathSourceOrReader struct {
- Type string `json:"type"`
- ID string `json:"id"`
-}
-
-type apiPath struct {
- Name string `json:"name"`
- ConfName string `json:"confName"`
- Conf *conf.PathConf `json:"conf"`
- Source *apiPathSourceOrReader `json:"source"`
- SourceReady bool `json:"sourceReady"` // Deprecated: renamed to Ready
- Ready bool `json:"ready"`
- ReadyTime *time.Time `json:"readyTime"`
- Tracks []string `json:"tracks"`
- BytesReceived uint64 `json:"bytesReceived"`
- Readers []apiPathSourceOrReader `json:"readers"`
-}
-
-type apiPathsList struct {
- ItemCount int `json:"itemCount"`
- PageCount int `json:"pageCount"`
- Items []*apiPath `json:"items"`
-}
-
-type apiHLSMuxer struct {
- Path string `json:"path"`
- Created time.Time `json:"created"`
- LastRequest time.Time `json:"lastRequest"`
- BytesSent uint64 `json:"bytesSent"`
-}
-
-type apiHLSMuxersList struct {
- ItemCount int `json:"itemCount"`
- PageCount int `json:"pageCount"`
- Items []*apiHLSMuxer `json:"items"`
-}
-
-type apiRTSPConn struct {
- ID uuid.UUID `json:"id"`
- Created time.Time `json:"created"`
- RemoteAddr string `json:"remoteAddr"`
- BytesReceived uint64 `json:"bytesReceived"`
- BytesSent uint64 `json:"bytesSent"`
-}
-
-type apiRTSPConnsList struct {
- ItemCount int `json:"itemCount"`
- PageCount int `json:"pageCount"`
- Items []*apiRTSPConn `json:"items"`
-}
-
-type apiRTMPConnState string
-
-const (
- apiRTMPConnStateIdle apiRTMPConnState = "idle"
- apiRTMPConnStateRead apiRTMPConnState = "read"
- apiRTMPConnStatePublish apiRTMPConnState = "publish"
-)
-
-type apiRTMPConn struct {
- ID uuid.UUID `json:"id"`
- Created time.Time `json:"created"`
- RemoteAddr string `json:"remoteAddr"`
- State apiRTMPConnState `json:"state"`
- Path string `json:"path"`
- BytesReceived uint64 `json:"bytesReceived"`
- BytesSent uint64 `json:"bytesSent"`
-}
-
-type apiRTMPConnsList struct {
- ItemCount int `json:"itemCount"`
- PageCount int `json:"pageCount"`
- Items []*apiRTMPConn `json:"items"`
-}
-
-type apiRTSPSessionState string
-
-const (
- apiRTSPSessionStateIdle apiRTSPSessionState = "idle"
- apiRTSPSessionStateRead apiRTSPSessionState = "read"
- apiRTSPSessionStatePublish apiRTSPSessionState = "publish"
-)
-
-type apiRTSPSession struct {
- ID uuid.UUID `json:"id"`
- Created time.Time `json:"created"`
- RemoteAddr string `json:"remoteAddr"`
- State apiRTSPSessionState `json:"state"`
- Path string `json:"path"`
- Transport *string `json:"transport"`
- BytesReceived uint64 `json:"bytesReceived"`
- BytesSent uint64 `json:"bytesSent"`
-}
-
-type apiRTSPSessionsList struct {
- ItemCount int `json:"itemCount"`
- PageCount int `json:"pageCount"`
- Items []*apiRTSPSession `json:"items"`
-}
-
-type apiSRTConnState string
-
-const (
- apiSRTConnStateIdle apiSRTConnState = "idle"
- apiSRTConnStateRead apiSRTConnState = "read"
- apiSRTConnStatePublish apiSRTConnState = "publish"
-)
-
-type apiSRTConn struct {
- ID uuid.UUID `json:"id"`
- Created time.Time `json:"created"`
- RemoteAddr string `json:"remoteAddr"`
- State apiSRTConnState `json:"state"`
- Path string `json:"path"`
- BytesReceived uint64 `json:"bytesReceived"`
- BytesSent uint64 `json:"bytesSent"`
-}
-
-type apiSRTConnsList struct {
- ItemCount int `json:"itemCount"`
- PageCount int `json:"pageCount"`
- Items []*apiSRTConn `json:"items"`
-}
-
-type apiWebRTCSessionState string
-
-const (
- apiWebRTCSessionStateRead apiWebRTCSessionState = "read"
- apiWebRTCSessionStatePublish apiWebRTCSessionState = "publish"
-)
-
-type apiWebRTCSession struct {
- ID uuid.UUID `json:"id"`
- Created time.Time `json:"created"`
- RemoteAddr string `json:"remoteAddr"`
- PeerConnectionEstablished bool `json:"peerConnectionEstablished"`
- LocalCandidate string `json:"localCandidate"`
- RemoteCandidate string `json:"remoteCandidate"`
- State apiWebRTCSessionState `json:"state"`
- Path string `json:"path"`
- BytesReceived uint64 `json:"bytesReceived"`
- BytesSent uint64 `json:"bytesSent"`
-}
-
-type apiWebRTCSessionsList struct {
- ItemCount int `json:"itemCount"`
- PageCount int `json:"pageCount"`
- Items []*apiWebRTCSession `json:"items"`
-}
diff --git a/internal/core/api_test.go b/internal/core/api_test.go
index d297c8491a7..f0d4f425050 100644
--- a/internal/core/api_test.go
+++ b/internal/core/api_test.go
@@ -3,6 +3,7 @@ package core
import (
"bufio"
"bytes"
+ "context"
"crypto/tls"
"encoding/json"
"fmt"
@@ -19,12 +20,13 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/pkg/formats/mpegts"
- "github.com/datarhei/gosrt"
+ srt "github.com/datarhei/gosrt"
"github.com/google/uuid"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp"
+ "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
)
var testFormatH264 = &format.H264{
@@ -58,6 +60,17 @@ var testMediaAAC = &description.Media{
}},
}
+func checkClose(t *testing.T, closeFunc func() error) {
+ require.NoError(t, closeFunc())
+}
+
+func checkError(t *testing.T, msg string, body io.Reader) {
+ var resErr map[string]interface{}
+ err := json.NewDecoder(body).Decode(&resErr)
+ require.NoError(t, err)
+ require.Equal(t, map[string]interface{}{"error": msg}, resErr)
+}
+
func httpRequest(t *testing.T, hc *http.Client, method string, ur string, in interface{}, out interface{}) {
buf := func() io.Reader {
if in == nil {
@@ -89,39 +102,7 @@ func httpRequest(t *testing.T, hc *http.Client, method string, ur string, in int
require.NoError(t, err)
}
-func TestPagination(t *testing.T) {
- 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)
-
- 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{4}, items)
-
- items = make([]int, 6)
- for i := 0; i < 6; i++ {
- items[i] = i
- }
-
- pageCount, err = paginate(&items, "3", "3")
- require.NoError(t, err)
- require.Equal(t, 2, pageCount)
- require.Equal(t, []int{5}, items)
-}
-
-func TestAPIConfigGet(t *testing.T) {
+func TestAPIConfigGlobalGet(t *testing.T) {
p, ok := newInstance("api: yes\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -129,33 +110,35 @@ func TestAPIConfigGet(t *testing.T) {
hc := &http.Client{Transport: &http.Transport{}}
var out map[string]interface{}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/global/get", nil, &out)
require.Equal(t, true, out["api"])
}
-func TestAPIConfigSet(t *testing.T) {
+func TestAPIConfigGlobalPatch(t *testing.T) {
p, ok := newInstance("api: yes\n")
require.Equal(t, true, ok)
defer p.Close()
hc := &http.Client{Transport: &http.Transport{}}
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/set", map[string]interface{}{
- "rtmp": false,
- "readTimeout": "7s",
- "protocols": []string{"tcp"},
+ httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/global/patch", map[string]interface{}{
+ "rtmp": false,
+ "readTimeout": "7s",
+ "protocols": []string{"tcp"},
+ "readBufferCount": 4096, // test setting a deprecated parameter
}, nil)
time.Sleep(500 * time.Millisecond)
var out map[string]interface{}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/global/get", nil, &out)
require.Equal(t, false, out["rtmp"])
require.Equal(t, "7s", out["readTimeout"])
require.Equal(t, []interface{}{"tcp"}, out["protocols"])
+ require.Equal(t, float64(4096), out["readBufferCount"])
}
-func TestAPIConfigSetUnknownField(t *testing.T) {
+func TestAPIConfigGlobalPatchUnknownField(t *testing.T) {
p, ok := newInstance("api: yes\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -169,37 +152,122 @@ func TestAPIConfigSetUnknownField(t *testing.T) {
hc := &http.Client{Transport: &http.Transport{}}
- req, err := http.NewRequest("POST", "http://localhost:9997/v2/config/set", bytes.NewReader(byts))
- require.NoError(t, err)
+ func() {
+ req, err := http.NewRequest(http.MethodPatch, "http://localhost:9997/v3/config/global/patch", bytes.NewReader(byts))
+ require.NoError(t, err)
- res, err := hc.Do(req)
- require.NoError(t, err)
- defer res.Body.Close()
- require.Equal(t, http.StatusBadRequest, res.StatusCode)
+ res, err := hc.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+
+ require.Equal(t, http.StatusBadRequest, res.StatusCode)
+ checkError(t, "json: unknown field \"test\"", res.Body)
+ }()
}
-func TestAPIConfigPathsAdd(t *testing.T) {
+func TestAPIConfigPathDefaultsGet(t *testing.T) {
p, ok := newInstance("api: yes\n")
require.Equal(t, true, ok)
defer p.Close()
hc := &http.Client{Transport: &http.Transport{}}
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/add/my/path", map[string]interface{}{
- "source": "rtsp://127.0.0.1:9999/mypath",
- "sourceOnDemand": true,
+ var out map[string]interface{}
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out)
+ require.Equal(t, "publisher", out["source"])
+}
+
+func TestAPIConfigPathDefaultsPatch(t *testing.T) {
+ p, ok := newInstance("api: yes\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
+
+ hc := &http.Client{Transport: &http.Transport{}}
+
+ httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch", map[string]interface{}{
+ "readUser": "myuser",
+ "readPass": "mypass",
}, nil)
- var out struct {
- Paths map[string]struct {
- Source string `json:"source"`
- SourceOnDemandStartTimeout string `json:"sourceOnDemandStartTimeout"`
- } `json:"paths"`
+ 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"])
+}
+
+func TestAPIConfigPathsList(t *testing.T) {
+ p, ok := newInstance("api: yes\n" +
+ "paths:\n" +
+ " path1:\n" +
+ " readUser: myuser1\n" +
+ " readPass: mypass1\n" +
+ " path2:\n" +
+ " readUser: myuser2\n" +
+ " readPass: mypass2\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
+
+ type pathConfig map[string]interface{}
+
+ type listRes struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []pathConfig `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out)
- require.Equal(t, "rtsp://127.0.0.1:9999/mypath", out.Paths["my/path"].Source)
- require.Equal(t, "10s", out.Paths["my/path"].SourceOnDemandStartTimeout)
+ hc := &http.Client{Transport: &http.Transport{}}
+
+ var out listRes
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/list", nil, &out)
+ require.Equal(t, 2, out.ItemCount)
+ require.Equal(t, 1, out.PageCount)
+ require.Equal(t, "path1", out.Items[0]["name"])
+ require.Equal(t, "myuser1", out.Items[0]["readUser"])
+ require.Equal(t, "mypass1", out.Items[0]["readPass"])
+ require.Equal(t, "path2", out.Items[1]["name"])
+ require.Equal(t, "myuser2", out.Items[1]["readUser"])
+ require.Equal(t, "mypass2", out.Items[1]["readPass"])
+}
+
+func TestAPIConfigPathsGet(t *testing.T) {
+ p, ok := newInstance("api: yes\n" +
+ "paths:\n" +
+ " my/path:\n" +
+ " readUser: myuser\n" +
+ " readPass: mypass\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
+
+ hc := &http.Client{Transport: &http.Transport{}}
+
+ var out map[string]interface{}
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
+ require.Equal(t, "my/path", out["name"])
+ require.Equal(t, "myuser", out["readUser"])
+}
+
+func TestAPIConfigPathsAdd(t *testing.T) {
+ p, ok := newInstance("api: yes\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
+
+ hc := &http.Client{Transport: &http.Transport{}}
+
+ httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
+ "source": "rtsp://127.0.0.1:9999/mypath",
+ "sourceOnDemand": true,
+ "disablePublisherOverride": true, // test setting a deprecated parameter
+ "rpiCameraVFlip": true,
+ }, nil)
+
+ var out map[string]interface{}
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
+ require.Equal(t, "rtsp://127.0.0.1:9999/mypath", out["source"])
+ require.Equal(t, true, out["sourceOnDemand"])
+ require.Equal(t, true, out["disablePublisherOverride"])
+ require.Equal(t, true, out["rpiCameraVFlip"])
}
func TestAPIConfigPathsAddUnknownField(t *testing.T) {
@@ -216,61 +284,99 @@ func TestAPIConfigPathsAddUnknownField(t *testing.T) {
hc := &http.Client{Transport: &http.Transport{}}
- req, err := http.NewRequest("POST", "http://localhost:9997/v2/config/paths/add/my/path", bytes.NewReader(byts))
- require.NoError(t, err)
+ func() {
+ req, err := http.NewRequest(http.MethodPost,
+ "http://localhost:9997/v3/config/paths/add/my/path", bytes.NewReader(byts))
+ require.NoError(t, err)
- res, err := hc.Do(req)
- require.NoError(t, err)
- defer res.Body.Close()
- require.Equal(t, http.StatusBadRequest, res.StatusCode)
+ res, err := hc.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+
+ require.Equal(t, http.StatusBadRequest, res.StatusCode)
+ checkError(t, "json: unknown field \"test\"", res.Body)
+ }()
}
-func TestAPIConfigPathsEdit(t *testing.T) {
+func TestAPIConfigPathsPatch(t *testing.T) { //nolint:dupl
p, ok := newInstance("api: yes\n")
require.Equal(t, true, ok)
defer p.Close()
hc := &http.Client{Transport: &http.Transport{}}
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/add/my/path", map[string]interface{}{
- "source": "rtsp://127.0.0.1:9999/mypath",
+ httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
+ "source": "rtsp://127.0.0.1:9999/mypath",
+ "sourceOnDemand": true,
+ "disablePublisherOverride": true, // test setting a deprecated parameter
+ "rpiCameraVFlip": true,
+ }, nil)
+
+ httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/my/path", map[string]interface{}{
+ "source": "rtsp://127.0.0.1:9998/mypath",
"sourceOnDemand": true,
}, nil)
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/edit/my/path", map[string]interface{}{
+ var out map[string]interface{}
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
+ require.Equal(t, "rtsp://127.0.0.1:9998/mypath", out["source"])
+ require.Equal(t, true, out["sourceOnDemand"])
+ require.Equal(t, true, out["disablePublisherOverride"])
+ require.Equal(t, true, out["rpiCameraVFlip"])
+}
+
+func TestAPIConfigPathsReplace(t *testing.T) { //nolint:dupl
+ p, ok := newInstance("api: yes\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
+
+ hc := &http.Client{Transport: &http.Transport{}}
+
+ httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
+ "source": "rtsp://127.0.0.1:9999/mypath",
+ "sourceOnDemand": true,
+ "disablePublisherOverride": true, // test setting a deprecated parameter
+ "rpiCameraVFlip": true,
+ }, nil)
+
+ httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/replace/my/path", map[string]interface{}{
"source": "rtsp://127.0.0.1:9998/mypath",
"sourceOnDemand": true,
}, nil)
- var out struct {
- Paths map[string]struct {
- Source string `json:"source"`
- } `json:"paths"`
- }
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out)
- require.Equal(t, "rtsp://127.0.0.1:9998/mypath", out.Paths["my/path"].Source)
+ var out map[string]interface{}
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out)
+ require.Equal(t, "rtsp://127.0.0.1:9998/mypath", out["source"])
+ require.Equal(t, true, out["sourceOnDemand"])
+ require.Equal(t, nil, out["disablePublisherOverride"])
+ require.Equal(t, false, out["rpiCameraVFlip"])
}
-func TestAPIConfigPathsRemove(t *testing.T) {
+func TestAPIConfigPathsDelete(t *testing.T) {
p, ok := newInstance("api: yes\n")
require.Equal(t, true, ok)
defer p.Close()
hc := &http.Client{Transport: &http.Transport{}}
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/add/my/path", map[string]interface{}{
+ httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{
"source": "rtsp://127.0.0.1:9999/mypath",
"sourceOnDemand": true,
}, nil)
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/remove/my/path", nil, nil)
+ httpRequest(t, hc, http.MethodDelete, "http://localhost:9997/v3/config/paths/delete/my/path", nil, nil)
- var out struct {
- Paths map[string]interface{} `json:"paths"`
- }
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out)
- _, ok = out.Paths["my/path"]
- require.Equal(t, false, ok)
+ func() {
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil)
+ require.NoError(t, err)
+
+ res, err := hc.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+
+ require.Equal(t, http.StatusNotFound, res.StatusCode)
+ checkError(t, "path configuration not found", res.Body)
+ }()
}
func TestAPIPathsList(t *testing.T) {
@@ -284,6 +390,7 @@ func TestAPIPathsList(t *testing.T) {
Ready bool `json:"ready"`
Tracks []string `json:"tracks"`
BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
}
type pathList struct {
@@ -323,7 +430,7 @@ func TestAPIPathsList(t *testing.T) {
require.NoError(t, err)
var out pathList
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out)
require.Equal(t, pathList{
ItemCount: 1,
PageCount: 1,
@@ -369,7 +476,7 @@ func TestAPIPathsList(t *testing.T) {
defer source.Close()
var out pathList
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out)
require.Equal(t, pathList{
ItemCount: 1,
PageCount: 1,
@@ -396,7 +503,7 @@ func TestAPIPathsList(t *testing.T) {
hc := &http.Client{Transport: &http.Transport{}}
var out pathList
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out)
require.Equal(t, pathList{
ItemCount: 1,
PageCount: 1,
@@ -423,7 +530,7 @@ func TestAPIPathsList(t *testing.T) {
hc := &http.Client{Transport: &http.Transport{}}
var out pathList
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out)
require.Equal(t, pathList{
ItemCount: 1,
PageCount: 1,
@@ -450,7 +557,7 @@ func TestAPIPathsList(t *testing.T) {
hc := &http.Client{Transport: &http.Transport{}}
var out pathList
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out)
require.Equal(t, pathList{
ItemCount: 1,
PageCount: 1,
@@ -469,7 +576,7 @@ func TestAPIPathsList(t *testing.T) {
func TestAPIPathsGet(t *testing.T) {
p, ok := newInstance("api: yes\n" +
"paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -487,6 +594,7 @@ func TestAPIPathsGet(t *testing.T) {
Ready bool `json:"Ready"`
Tracks []string `json:"tracks"`
BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
}
var pathName string
@@ -508,7 +616,7 @@ func TestAPIPathsGet(t *testing.T) {
defer source.Close()
var out path
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/get/"+pathName, nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/get/"+pathName, nil, &out)
require.Equal(t, path{
Name: pathName,
Source: pathSource{
@@ -518,10 +626,12 @@ func TestAPIPathsGet(t *testing.T) {
Tracks: []string{"H264"},
}, out)
} else {
- res, err := hc.Get("http://localhost:9997/v2/paths/get/" + pathName)
+ res, err := hc.Get("http://localhost:9997/v3/paths/get/" + pathName)
require.NoError(t, err)
defer res.Body.Close()
- require.Equal(t, 404, res.StatusCode)
+
+ require.Equal(t, http.StatusNotFound, res.StatusCode)
+ checkError(t, "path not found", res.Body)
}
})
}
@@ -564,7 +674,7 @@ func TestAPIProtocolList(t *testing.T) {
}
conf += "paths:\n" +
- " all:\n"
+ " all_others:\n"
p, ok := newInstance(conf)
require.Equal(t, true, ok)
@@ -578,7 +688,7 @@ func TestAPIProtocolList(t *testing.T) {
case "rtsp conns", "rtsp sessions":
source := gortsplib.Client{}
- err := source.StartRecording("rtsp://localhost:8554/mypath",
+ err := source.StartRecording("rtsp://localhost:8554/mypath?key=val",
&description.Session{Medias: []*description.Media{medi}})
require.NoError(t, err)
defer source.Close()
@@ -588,7 +698,7 @@ func TestAPIProtocolList(t *testing.T) {
TLSConfig: &tls.Config{InsecureSkipVerify: true},
}
- err := source.StartRecording("rtsps://localhost:8322/mypath",
+ err := source.StartRecording("rtsps://localhost:8322/mypath?key=val",
&description.Session{Medias: []*description.Media{medi}})
require.NoError(t, err)
defer source.Close()
@@ -601,7 +711,7 @@ func TestAPIProtocolList(t *testing.T) {
port = "1936"
}
- u, err := url.Parse("rtmp://127.0.0.1:" + port + "/mypath")
+ u, err := url.Parse("rtmp://127.0.0.1:" + port + "/mypath?key=val")
require.NoError(t, err)
nconn, err := func() (net.Conn, error) {
@@ -653,7 +763,7 @@ func TestAPIProtocolList(t *testing.T) {
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,
},*/
- err = source.WritePacketRTP(medi, &rtp.Packet{
+ err := source.WritePacketRTP(medi, &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
@@ -685,29 +795,38 @@ func TestAPIProtocolList(t *testing.T) {
require.NoError(t, err)
defer source.Close()
- c := newWebRTCTestClient(t, hc, "http://localhost:8889/mypath/whep", false)
- defer c.close()
+ u, err := url.Parse("http://localhost:8889/mypath/whep?key=val")
+ require.NoError(t, err)
- time.Sleep(500 * time.Millisecond)
+ go func() {
+ time.Sleep(500 * time.Millisecond)
- err = source.WritePacketRTP(medi, &rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: 123,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{5, 1, 2, 3, 4},
- })
- require.NoError(t, err)
+ err := source.WritePacketRTP(medi, &rtp.Packet{
+ Header: rtp.Header{
+ Version: 2,
+ Marker: true,
+ PayloadType: 96,
+ SequenceNumber: 123,
+ Timestamp: 45343,
+ SSRC: 563423,
+ },
+ Payload: []byte{5, 1, 2, 3, 4},
+ })
+ require.NoError(t, err)
+ }()
+
+ c := &webrtc.WHIPClient{
+ HTTPClient: hc,
+ URL: u,
+ }
- <-c.incomingTrack
+ _, err = c.Read(context.Background())
+ require.NoError(t, err)
+ defer checkClose(t, c.Close)
case "srt":
conf := srt.DefaultConfig()
- conf.StreamId = "publish:mypath"
+ conf.StreamId = "publish:mypath:::key=val"
conn, err := srt.Dial("srt", "localhost:8890", conf)
require.NoError(t, err)
@@ -759,18 +878,20 @@ func TestAPIProtocolList(t *testing.T) {
type item struct {
State string `json:"state"`
Path string `json:"path"`
+ Query string `json:"query"`
}
var out struct {
ItemCount int `json:"itemCount"`
Items []item `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out)
if ca != "rtsp conns" && ca != "rtsps conns" {
require.Equal(t, item{
State: "publish",
Path: "mypath",
+ Query: "key=val",
}, out.Items[0])
}
@@ -784,7 +905,7 @@ func TestAPIProtocolList(t *testing.T) {
ItemCount int `json:"itemCount"`
Items []item `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/hlsmuxers/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/hlsmuxers/list", nil, &out)
s := fmt.Sprintf("^%d-", time.Now().Year())
require.Regexp(t, s, out.Items[0].Created)
@@ -795,18 +916,20 @@ func TestAPIProtocolList(t *testing.T) {
PeerConnectionEstablished bool `json:"peerConnectionEstablished"`
State string `json:"state"`
Path string `json:"path"`
+ Query string `json:"query"`
}
var out struct {
ItemCount int `json:"itemCount"`
Items []item `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/webrtcsessions/list", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/webrtcsessions/list", nil, &out)
require.Equal(t, item{
PeerConnectionEstablished: true,
State: "read",
Path: "mypath",
+ Query: "key=val",
}, out.Items[0])
}
})
@@ -850,7 +973,7 @@ func TestAPIProtocolGet(t *testing.T) {
}
conf += "paths:\n" +
- " all:\n"
+ " all_others:\n"
p, ok := newInstance(conf)
require.Equal(t, true, ok)
@@ -971,25 +1094,34 @@ func TestAPIProtocolGet(t *testing.T) {
require.NoError(t, err)
defer source.Close()
- c := newWebRTCTestClient(t, hc, "http://localhost:8889/mypath/whep", false)
- defer c.close()
+ u, err := url.Parse("http://localhost:8889/mypath/whep")
+ require.NoError(t, err)
- time.Sleep(500 * time.Millisecond)
+ go func() {
+ time.Sleep(500 * time.Millisecond)
- err = source.WritePacketRTP(medi, &rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: 123,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{5, 1, 2, 3, 4},
- })
- require.NoError(t, err)
+ err := source.WritePacketRTP(medi, &rtp.Packet{
+ Header: rtp.Header{
+ Version: 2,
+ Marker: true,
+ PayloadType: 96,
+ SequenceNumber: 123,
+ Timestamp: 45343,
+ SSRC: 563423,
+ },
+ Payload: []byte{5, 1, 2, 3, 4},
+ })
+ require.NoError(t, err)
+ }()
+
+ c := &webrtc.WHIPClient{
+ HTTPClient: hc,
+ URL: u,
+ }
- <-c.incomingTrack
+ _, err = c.Read(context.Background())
+ require.NoError(t, err)
+ defer checkClose(t, c.Close)
case "srt":
conf := srt.DefaultConfig()
@@ -1050,14 +1182,14 @@ func TestAPIProtocolGet(t *testing.T) {
var out1 struct {
Items []item `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out1)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out1)
if ca != "rtsp conns" && ca != "rtsps conns" {
require.Equal(t, "publish", out1.Items[0].State)
}
var out2 item
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/get/"+out1.Items[0].ID, nil, &out2)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/get/"+out1.Items[0].ID, nil, &out2)
if ca != "rtsp conns" && ca != "rtsps conns" {
require.Equal(t, "publish", out2.State)
@@ -1070,7 +1202,7 @@ func TestAPIProtocolGet(t *testing.T) {
}
var out item
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/hlsmuxers/get/mypath", nil, &out)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/hlsmuxers/get/mypath", nil, &out)
s := fmt.Sprintf("^%d-", time.Now().Year())
require.Regexp(t, s, out.Created)
@@ -1091,10 +1223,10 @@ func TestAPIProtocolGet(t *testing.T) {
var out1 struct {
Items []item `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/webrtcsessions/list", nil, &out1)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/webrtcsessions/list", nil, &out1)
var out2 item
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/webrtcsessions/get/"+out1.Items[0].ID, nil, &out2)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/webrtcsessions/get/"+out1.Items[0].ID, nil, &out2)
require.Equal(t, true, out2.PeerConnectionEstablished)
}
@@ -1139,7 +1271,7 @@ func TestAPIProtocolGetNotFound(t *testing.T) {
}
conf += "paths:\n" +
- " all:\n"
+ " all_others:\n"
p, ok := newInstance(conf)
require.Equal(t, true, ok)
@@ -1178,13 +1310,25 @@ func TestAPIProtocolGetNotFound(t *testing.T) {
}
func() {
- req, err := http.NewRequest("GET", "http://localhost:9997/v2/"+pa+"/get/"+uuid.New().String(), nil)
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:9997/v3/"+pa+"/get/"+uuid.New().String(), nil)
require.NoError(t, err)
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
+
require.Equal(t, http.StatusNotFound, res.StatusCode)
+
+ switch ca {
+ case "rtsp conns", "rtsps conns", "rtmp", "rtmps", "srt":
+ checkError(t, "connection not found", res.Body)
+
+ case "rtsp sessions", "rtsps sessions", "webrtc":
+ checkError(t, "session not found", res.Body)
+
+ case "hls":
+ checkError(t, "muxer not found", res.Body)
+ }
}()
})
}
@@ -1217,7 +1361,7 @@ func TestAPIProtocolKick(t *testing.T) {
}
conf += "paths:\n" +
- " all:\n"
+ " all_others:\n"
p, ok := newInstance(conf)
require.Equal(t, true, ok)
@@ -1261,8 +1405,19 @@ func TestAPIProtocolKick(t *testing.T) {
require.NoError(t, err)
case "webrtc":
- c := newWebRTCTestClient(t, hc, "http://localhost:8889/mypath/whip", true)
- defer c.close()
+ u, err := url.Parse("http://localhost:8889/mypath/whip")
+ require.NoError(t, err)
+
+ c := &webrtc.WHIPClient{
+ HTTPClient: hc,
+ URL: u,
+ }
+
+ _, err = c.Publish(context.Background(), medi.Formats[0], nil)
+ require.NoError(t, err)
+ defer func() {
+ require.Error(t, c.Close())
+ }()
case "srt":
conf := srt.DefaultConfig()
@@ -1310,16 +1465,16 @@ func TestAPIProtocolKick(t *testing.T) {
ID string `json:"id"`
} `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out1)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out1)
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/"+pa+"/kick/"+out1.Items[0].ID, nil, nil)
+ httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/"+pa+"/kick/"+out1.Items[0].ID, nil, nil)
var out2 struct {
Items []struct {
ID string `json:"id"`
} `json:"items"`
}
- httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out2)
+ httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out2)
require.Equal(t, 0, len(out2.Items))
})
}
@@ -1352,7 +1507,7 @@ func TestAPIProtocolKickNotFound(t *testing.T) {
}
conf += "paths:\n" +
- " all:\n"
+ " all_others:\n"
p, ok := newInstance(conf)
require.Equal(t, true, ok)
@@ -1379,13 +1534,25 @@ func TestAPIProtocolKickNotFound(t *testing.T) {
}
func() {
- req, err := http.NewRequest("GET", "http://localhost:9997/v2/"+pa+"/kick/"+uuid.New().String(), nil)
+ req, err := http.NewRequest(http.MethodPost, "http://localhost:9997/v3/"+pa+"/kick/"+uuid.New().String(), nil)
require.NoError(t, err)
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
+
require.Equal(t, http.StatusNotFound, res.StatusCode)
+
+ switch ca {
+ case "rtsp conns", "rtsps conns", "rtmp", "rtmps", "srt":
+ checkError(t, "connection not found", res.Body)
+
+ case "rtsp sessions", "rtsps sessions", "webrtc":
+ checkError(t, "session not found", res.Body)
+
+ case "hls":
+ checkError(t, "muxer not found", res.Body)
+ }
}()
})
}
diff --git a/internal/core/auth.go b/internal/core/auth.go
new file mode 100644
index 00000000000..dbe2abf04af
--- /dev/null
+++ b/internal/core/auth.go
@@ -0,0 +1,126 @@
+package core
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/auth"
+ "github.com/bluenviron/gortsplib/v4/pkg/headers"
+ "github.com/google/uuid"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/defs"
+)
+
+func doExternalAuthentication(
+ ur string,
+ accessRequest defs.PathAccessRequest,
+) error {
+ enc, _ := json.Marshal(struct {
+ IP string `json:"ip"`
+ User string `json:"user"`
+ Password string `json:"password"`
+ Path string `json:"path"`
+ Protocol string `json:"protocol"`
+ ID *uuid.UUID `json:"id"`
+ Action string `json:"action"`
+ Query string `json:"query"`
+ }{
+ IP: accessRequest.IP.String(),
+ User: accessRequest.User,
+ Password: accessRequest.Pass,
+ Path: accessRequest.Name,
+ Protocol: string(accessRequest.Proto),
+ ID: accessRequest.ID,
+ Action: func() string {
+ if accessRequest.Publish {
+ return "publish"
+ }
+ return "read"
+ }(),
+ Query: accessRequest.Query,
+ })
+ res, err := http.Post(ur, "application/json", bytes.NewReader(enc))
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode < 200 || res.StatusCode > 299 {
+ if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 {
+ return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody))
+ }
+ return fmt.Errorf("server replied with code %d", res.StatusCode)
+ }
+
+ return nil
+}
+
+func doAuthentication(
+ externalAuthenticationURL string,
+ rtspAuthMethods conf.AuthMethods,
+ pathConf *conf.Path,
+ accessRequest defs.PathAccessRequest,
+) error {
+ var rtspAuth headers.Authorization
+ if accessRequest.RTSPRequest != nil {
+ err := rtspAuth.Unmarshal(accessRequest.RTSPRequest.Header["Authorization"])
+ if err == nil && rtspAuth.Method == headers.AuthBasic {
+ accessRequest.User = rtspAuth.BasicUser
+ accessRequest.Pass = rtspAuth.BasicPass
+ }
+ }
+
+ if externalAuthenticationURL != "" {
+ err := doExternalAuthentication(
+ externalAuthenticationURL,
+ accessRequest,
+ )
+ if err != nil {
+ return defs.AuthenticationError{Message: fmt.Sprintf("external authentication failed: %s", err)}
+ }
+ }
+
+ var pathIPs conf.IPsOrCIDRs
+ var pathUser conf.Credential
+ var pathPass conf.Credential
+
+ if accessRequest.Publish {
+ pathIPs = pathConf.PublishIPs
+ pathUser = pathConf.PublishUser
+ pathPass = pathConf.PublishPass
+ } else {
+ pathIPs = pathConf.ReadIPs
+ pathUser = pathConf.ReadUser
+ pathPass = pathConf.ReadPass
+ }
+
+ if pathIPs != nil {
+ if !ipEqualOrInRange(accessRequest.IP, pathIPs) {
+ return defs.AuthenticationError{Message: fmt.Sprintf("IP %s not allowed", accessRequest.IP)}
+ }
+ }
+
+ if !pathUser.IsEmpty() {
+ if accessRequest.RTSPRequest != nil && rtspAuth.Method == headers.AuthDigest {
+ err := auth.Validate(
+ accessRequest.RTSPRequest,
+ pathUser.GetValue(),
+ pathPass.GetValue(),
+ accessRequest.RTSPBaseURL,
+ rtspAuthMethods,
+ "IPCAM",
+ accessRequest.RTSPNonce)
+ if err != nil {
+ return defs.AuthenticationError{Message: err.Error()}
+ }
+ } else if !pathUser.Check(accessRequest.User) || !pathPass.Check(accessRequest.Pass) {
+ return defs.AuthenticationError{Message: "invalid credentials"}
+ }
+ }
+
+ return nil
+}
diff --git a/internal/core/authentication.go b/internal/core/authentication.go
deleted file mode 100644
index 61aaf25bcf3..00000000000
--- a/internal/core/authentication.go
+++ /dev/null
@@ -1,183 +0,0 @@
-package core
-
-import (
- "bytes"
- "crypto/sha256"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "strings"
-
- "github.com/bluenviron/gortsplib/v4/pkg/auth"
- "github.com/bluenviron/gortsplib/v4/pkg/base"
- "github.com/bluenviron/gortsplib/v4/pkg/headers"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/conf"
-)
-
-func sha256Base64(in string) string {
- h := sha256.New()
- h.Write([]byte(in))
- return base64.StdEncoding.EncodeToString(h.Sum(nil))
-}
-
-func checkCredential(right string, guess string) bool {
- if strings.HasPrefix(right, "sha256:") {
- return right[len("sha256:"):] == sha256Base64(guess)
- }
-
- return right == guess
-}
-
-type errAuthentication struct {
- message string
-}
-
-// Error implements the error interface.
-func (e *errAuthentication) Error() string {
- return "authentication failed: " + e.message
-}
-
-type authProtocol string
-
-const (
- authProtocolRTSP authProtocol = "rtsp"
- authProtocolRTMP authProtocol = "rtmp"
- authProtocolHLS authProtocol = "hls"
- authProtocolWebRTC authProtocol = "webrtc"
- authProtocolSRT authProtocol = "srt"
-)
-
-type authCredentials struct {
- query string
- ip net.IP
- user string
- pass string
- proto authProtocol
- id *uuid.UUID
- rtspRequest *base.Request
- rtspBaseURL *url.URL
- rtspNonce string
-}
-
-func doExternalAuthentication(
- ur string,
- path string,
- publish bool,
- credentials authCredentials,
-) error {
- enc, _ := json.Marshal(struct {
- IP string `json:"ip"`
- User string `json:"user"`
- Password string `json:"password"`
- Path string `json:"path"`
- Protocol string `json:"protocol"`
- ID *uuid.UUID `json:"id"`
- Action string `json:"action"`
- Query string `json:"query"`
- }{
- IP: credentials.ip.String(),
- User: credentials.user,
- Password: credentials.pass,
- Path: path,
- Protocol: string(credentials.proto),
- ID: credentials.id,
- Action: func() string {
- if publish {
- return "publish"
- }
- return "read"
- }(),
- Query: credentials.query,
- })
- res, err := http.Post(ur, "application/json", bytes.NewReader(enc))
- if err != nil {
- return err
- }
- defer res.Body.Close()
-
- if res.StatusCode < 200 || res.StatusCode > 299 {
- if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 {
- return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody))
- }
- return fmt.Errorf("server replied with code %d", res.StatusCode)
- }
-
- return nil
-}
-
-func doAuthentication(
- externalAuthenticationURL string,
- rtspAuthMethods conf.AuthMethods,
- pathName string,
- pathConf *conf.PathConf,
- publish bool,
- credentials authCredentials,
-) error {
- var rtspAuth headers.Authorization
- if credentials.rtspRequest != nil {
- err := rtspAuth.Unmarshal(credentials.rtspRequest.Header["Authorization"])
- if err == nil && rtspAuth.Method == headers.AuthBasic {
- credentials.user = rtspAuth.BasicUser
- credentials.pass = rtspAuth.BasicPass
- }
- }
-
- if externalAuthenticationURL != "" {
- err := doExternalAuthentication(
- externalAuthenticationURL,
- pathName,
- publish,
- credentials,
- )
- if err != nil {
- return &errAuthentication{message: fmt.Sprintf("external authentication failed: %s", err)}
- }
- }
-
- var pathIPs conf.IPsOrCIDRs
- var pathUser string
- var pathPass string
-
- if publish {
- pathIPs = pathConf.PublishIPs
- pathUser = string(pathConf.PublishUser)
- pathPass = string(pathConf.PublishPass)
- } else {
- pathIPs = pathConf.ReadIPs
- pathUser = string(pathConf.ReadUser)
- pathPass = string(pathConf.ReadPass)
- }
-
- if pathIPs != nil {
- if !ipEqualOrInRange(credentials.ip, pathIPs) {
- return &errAuthentication{message: fmt.Sprintf("IP %s not allowed", credentials.ip)}
- }
- }
-
- if pathUser != "" {
- if credentials.rtspRequest != nil && rtspAuth.Method == headers.AuthDigest {
- err := auth.Validate(
- credentials.rtspRequest,
- pathUser,
- pathPass,
- credentials.rtspBaseURL,
- rtspAuthMethods,
- "IPCAM",
- credentials.rtspNonce)
- if err != nil {
- return &errAuthentication{message: err.Error()}
- }
- } else if !checkCredential(pathUser, credentials.user) ||
- !checkCredential(pathPass, credentials.pass) {
- return &errAuthentication{message: "invalid credentials"}
- }
- }
-
- return nil
-}
diff --git a/internal/core/conn.go b/internal/core/conn.go
deleted file mode 100644
index 8f8a5c2c1dc..00000000000
--- a/internal/core/conn.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package core
-
-import (
- "net"
-
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-type conn struct {
- rtspAddress string
- runOnConnect string
- runOnConnectRestart bool
- runOnDisconnect string
- externalCmdPool *externalcmd.Pool
- logger logger.Writer
-
- onConnectCmd *externalcmd.Cmd
-}
-
-func newConn(
- rtspAddress string,
- runOnConnect string,
- runOnConnectRestart bool,
- runOnDisconnect string,
- externalCmdPool *externalcmd.Pool,
- logger logger.Writer,
-) *conn {
- return &conn{
- rtspAddress: rtspAddress,
- runOnConnect: runOnConnect,
- runOnConnectRestart: runOnConnectRestart,
- runOnDisconnect: runOnDisconnect,
- externalCmdPool: externalCmdPool,
- logger: logger,
- }
-}
-
-func (c *conn) open(desc apiPathSourceOrReader) {
- if c.runOnConnect != "" {
- c.logger.Log(logger.Info, "runOnConnect command started")
-
- _, port, _ := net.SplitHostPort(c.rtspAddress)
- env := externalcmd.Environment{
- "RTSP_PORT": port,
- "MTX_CONN_TYPE": desc.Type,
- "MTX_CONN_ID": desc.ID,
- }
-
- c.onConnectCmd = externalcmd.NewCmd(
- c.externalCmdPool,
- c.runOnConnect,
- c.runOnConnectRestart,
- env,
- func(err error) {
- c.logger.Log(logger.Info, "runOnConnect command exited: %v", err)
- })
- }
-}
-
-func (c *conn) close(desc apiPathSourceOrReader) {
- if c.onConnectCmd != nil {
- c.onConnectCmd.Close()
- c.logger.Log(logger.Info, "runOnConnect command stopped")
- }
-
- if c.runOnDisconnect != "" {
- c.logger.Log(logger.Info, "runOnDisconnect command launched")
-
- _, port, _ := net.SplitHostPort(c.rtspAddress)
- env := externalcmd.Environment{
- "RTSP_PORT": port,
- "MTX_CONN_TYPE": desc.Type,
- "MTX_CONN_ID": desc.ID,
- }
-
- externalcmd.NewCmd(
- c.externalCmdPool,
- c.runOnDisconnect,
- false,
- env,
- nil)
- }
-}
diff --git a/internal/core/core.go b/internal/core/core.go
index 4182b8351d4..4cb1c333d74 100644
--- a/internal/core/core.go
+++ b/internal/core/core.go
@@ -8,6 +8,7 @@ import (
"os/signal"
"path/filepath"
"reflect"
+ "sort"
"strings"
"time"
@@ -15,12 +16,21 @@ import (
"github.com/bluenviron/gortsplib/v4"
"github.com/gin-gonic/gin"
+ "github.com/bluenviron/mediamtx/internal/api"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/confwatcher"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/bluenviron/mediamtx/internal/metrics"
+ "github.com/bluenviron/mediamtx/internal/playback"
+ "github.com/bluenviron/mediamtx/internal/pprof"
"github.com/bluenviron/mediamtx/internal/record"
"github.com/bluenviron/mediamtx/internal/rlimit"
+ "github.com/bluenviron/mediamtx/internal/servers/hls"
+ "github.com/bluenviron/mediamtx/internal/servers/rtmp"
+ "github.com/bluenviron/mediamtx/internal/servers/rtsp"
+ "github.com/bluenviron/mediamtx/internal/servers/srt"
+ "github.com/bluenviron/mediamtx/internal/servers/webrtc"
)
var version = "v0.0.0"
@@ -33,12 +43,44 @@ var defaultConfPaths = []string{
"/etc/mediamtx/mediamtx.yml",
}
+func gatherCleanerEntries(paths map[string]*conf.Path) []record.CleanerEntry {
+ out := make(map[record.CleanerEntry]struct{})
+
+ for _, pa := range paths {
+ if pa.Record && pa.RecordDeleteAfter != 0 {
+ entry := record.CleanerEntry{
+ Path: pa.RecordPath,
+ Format: pa.RecordFormat,
+ DeleteAfter: time.Duration(pa.RecordDeleteAfter),
+ }
+ out[entry] = struct{}{}
+ }
+ }
+
+ out2 := make([]record.CleanerEntry, len(out))
+ i := 0
+
+ for v := range out {
+ out2[i] = v
+ i++
+ }
+
+ sort.Slice(out2, func(i, j int) bool {
+ if out2[i].Path != out2[j].Path {
+ return out2[i].Path < out2[j].Path
+ }
+ return out2[i].DeleteAfter < out2[j].DeleteAfter
+ })
+
+ return out2
+}
+
var cli struct {
Version bool `help:"print version"`
Confpath string `arg:"" default:""`
}
-// Core is an instance of mediamtx.
+// Core is an instance of MediaMTX.
type Core struct {
ctx context.Context
ctxCancel func()
@@ -46,18 +88,19 @@ type Core struct {
conf *conf.Conf
logger *logger.Logger
externalCmdPool *externalcmd.Pool
- metrics *metrics
- pprof *pprof
+ metrics *metrics.Metrics
+ pprof *pprof.PPROF
recordCleaner *record.Cleaner
+ playbackServer *playback.Server
pathManager *pathManager
- rtspServer *rtspServer
- rtspsServer *rtspServer
- rtmpServer *rtmpServer
- rtmpsServer *rtmpServer
- hlsManager *hlsManager
- webRTCManager *webRTCManager
- srtServer *srtServer
- api *api
+ rtspServer *rtsp.Server
+ rtspsServer *rtsp.Server
+ rtmpServer *rtmp.Server
+ rtmpsServer *rtmp.Server
+ hlsServer *hls.Server
+ webRTCServer *webrtc.Server
+ srtServer *srt.Server
+ api *api.API
confWatcher *confwatcher.ConfWatcher
// in
@@ -67,7 +110,7 @@ type Core struct {
done chan struct{}
}
-// New allocates a core.
+// New allocates a Core.
func New(args []string) (*Core, bool) {
parser, err := kong.New(&cli,
kong.Description("MediaMTX "+version),
@@ -135,7 +178,7 @@ func (p *Core) Wait() {
<-p.done
}
-// Log is the main logging function.
+// Log implements logger.Writer.
func (p *Core) Log(level logger.Level, format string, args ...interface{}) {
p.logger.Log(level, format, args...)
}
@@ -237,11 +280,12 @@ func (p *Core) createResources(initial bool) error {
if p.conf.Metrics &&
p.metrics == nil {
- p.metrics, err = newMetrics(
- p.conf.MetricsAddress,
- p.conf.ReadTimeout,
- p,
- )
+ p.metrics = &metrics.Metrics{
+ Address: p.conf.MetricsAddress,
+ ReadTimeout: p.conf.ReadTimeout,
+ Parent: p,
+ }
+ err := p.metrics.Initialize()
if err != nil {
return err
}
@@ -249,44 +293,60 @@ func (p *Core) createResources(initial bool) error {
if p.conf.PPROF &&
p.pprof == nil {
- p.pprof, err = newPPROF(
- p.conf.PPROFAddress,
- p.conf.ReadTimeout,
- p,
- )
+ p.pprof = &pprof.PPROF{
+ Address: p.conf.PPROFAddress,
+ ReadTimeout: p.conf.ReadTimeout,
+ Parent: p,
+ }
+ err := p.pprof.Initialize()
if err != nil {
return err
}
}
- if p.conf.Record &&
- p.conf.RecordDeleteAfter != 0 &&
+ cleanerEntries := gatherCleanerEntries(p.conf.Paths)
+ if len(cleanerEntries) != 0 &&
p.recordCleaner == nil {
- p.recordCleaner = record.NewCleaner(
- p.conf.RecordPath,
- time.Duration(p.conf.RecordDeleteAfter),
- p,
- )
+ p.recordCleaner = &record.Cleaner{
+ Entries: cleanerEntries,
+ Parent: p,
+ }
+ p.recordCleaner.Initialize()
+ }
+
+ if p.conf.Playback &&
+ p.playbackServer == nil {
+ p.playbackServer = &playback.Server{
+ Address: p.conf.PlaybackAddress,
+ ReadTimeout: p.conf.ReadTimeout,
+ PathConfs: p.conf.Paths,
+ Parent: p,
+ }
+ err := p.playbackServer.Initialize()
+ if err != nil {
+ return err
+ }
}
if p.pathManager == nil {
- p.pathManager = newPathManager(
- p.conf.ExternalAuthenticationURL,
- p.conf.RTSPAddress,
- p.conf.AuthMethods,
- p.conf.ReadTimeout,
- p.conf.WriteTimeout,
- p.conf.WriteQueueSize,
- p.conf.UDPMaxPayloadSize,
- p.conf.Record,
- p.conf.RecordPath,
- p.conf.RecordPartDuration,
- p.conf.RecordSegmentDuration,
- p.conf.Paths,
- p.externalCmdPool,
- p.metrics,
- p,
- )
+ p.pathManager = &pathManager{
+ logLevel: p.conf.LogLevel,
+ externalAuthenticationURL: p.conf.ExternalAuthenticationURL,
+ rtspAddress: p.conf.RTSPAddress,
+ authMethods: p.conf.AuthMethods,
+ readTimeout: p.conf.ReadTimeout,
+ writeTimeout: p.conf.WriteTimeout,
+ writeQueueSize: p.conf.WriteQueueSize,
+ udpMaxPayloadSize: p.conf.UDPMaxPayloadSize,
+ pathConfs: p.conf.Paths,
+ externalCmdPool: p.externalCmdPool,
+ parent: p,
+ }
+ p.pathManager.initialize()
+
+ if p.metrics != nil {
+ p.metrics.SetPathManager(p.pathManager)
+ }
}
if p.conf.RTSP &&
@@ -296,214 +356,249 @@ func (p *Core) createResources(initial bool) error {
_, useUDP := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDP)]
_, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)]
- p.rtspServer, err = newRTSPServer(
- p.conf.RTSPAddress,
- p.conf.AuthMethods,
- p.conf.ReadTimeout,
- p.conf.WriteTimeout,
- p.conf.WriteQueueSize,
- useUDP,
- useMulticast,
- p.conf.RTPAddress,
- p.conf.RTCPAddress,
- p.conf.MulticastIPRange,
- p.conf.MulticastRTPPort,
- p.conf.MulticastRTCPPort,
- false,
- "",
- "",
- p.conf.RTSPAddress,
- p.conf.Protocols,
- p.conf.RunOnConnect,
- p.conf.RunOnConnectRestart,
- p.conf.RunOnDisconnect,
- p.externalCmdPool,
- p.metrics,
- p.pathManager,
- p,
- )
+ p.rtspServer = &rtsp.Server{
+ Address: p.conf.RTSPAddress,
+ AuthMethods: p.conf.AuthMethods,
+ ReadTimeout: p.conf.ReadTimeout,
+ WriteTimeout: p.conf.WriteTimeout,
+ WriteQueueSize: p.conf.WriteQueueSize,
+ UseUDP: useUDP,
+ UseMulticast: useMulticast,
+ RTPAddress: p.conf.RTPAddress,
+ RTCPAddress: p.conf.RTCPAddress,
+ MulticastIPRange: p.conf.MulticastIPRange,
+ MulticastRTPPort: p.conf.MulticastRTPPort,
+ MulticastRTCPPort: p.conf.MulticastRTCPPort,
+ IsTLS: false,
+ ServerCert: "",
+ ServerKey: "",
+ RTSPAddress: p.conf.RTSPAddress,
+ Protocols: p.conf.Protocols,
+ RunOnConnect: p.conf.RunOnConnect,
+ RunOnConnectRestart: p.conf.RunOnConnectRestart,
+ RunOnDisconnect: p.conf.RunOnDisconnect,
+ ExternalCmdPool: p.externalCmdPool,
+ PathManager: p.pathManager,
+ Parent: p,
+ }
+ err := p.rtspServer.Initialize()
if err != nil {
return err
}
+
+ if p.metrics != nil {
+ p.metrics.SetRTSPServer(p.rtspServer)
+ }
}
if p.conf.RTSP &&
(p.conf.Encryption == conf.EncryptionStrict ||
p.conf.Encryption == conf.EncryptionOptional) &&
p.rtspsServer == nil {
- p.rtspsServer, err = newRTSPServer(
- p.conf.RTSPSAddress,
- p.conf.AuthMethods,
- p.conf.ReadTimeout,
- p.conf.WriteTimeout,
- p.conf.WriteQueueSize,
- false,
- false,
- "",
- "",
- "",
- 0,
- 0,
- true,
- p.conf.ServerCert,
- p.conf.ServerKey,
- p.conf.RTSPAddress,
- p.conf.Protocols,
- p.conf.RunOnConnect,
- p.conf.RunOnConnectRestart,
- p.conf.RunOnDisconnect,
- p.externalCmdPool,
- p.metrics,
- p.pathManager,
- p,
- )
+ p.rtspsServer = &rtsp.Server{
+ Address: p.conf.RTSPSAddress,
+ AuthMethods: p.conf.AuthMethods,
+ ReadTimeout: p.conf.ReadTimeout,
+ WriteTimeout: p.conf.WriteTimeout,
+ WriteQueueSize: p.conf.WriteQueueSize,
+ UseUDP: false,
+ UseMulticast: false,
+ RTPAddress: "",
+ RTCPAddress: "",
+ MulticastIPRange: "",
+ MulticastRTPPort: 0,
+ MulticastRTCPPort: 0,
+ IsTLS: true,
+ ServerCert: p.conf.ServerCert,
+ ServerKey: p.conf.ServerKey,
+ RTSPAddress: p.conf.RTSPAddress,
+ Protocols: p.conf.Protocols,
+ RunOnConnect: p.conf.RunOnConnect,
+ RunOnConnectRestart: p.conf.RunOnConnectRestart,
+ RunOnDisconnect: p.conf.RunOnDisconnect,
+ ExternalCmdPool: p.externalCmdPool,
+ PathManager: p.pathManager,
+ Parent: p,
+ }
+ err := p.rtspsServer.Initialize()
if err != nil {
return err
}
+
+ if p.metrics != nil {
+ p.metrics.SetRTSPSServer(p.rtspsServer)
+ }
}
if p.conf.RTMP &&
(p.conf.RTMPEncryption == conf.EncryptionNo ||
p.conf.RTMPEncryption == conf.EncryptionOptional) &&
p.rtmpServer == nil {
- p.rtmpServer, err = newRTMPServer(
- p.conf.RTMPAddress,
- p.conf.ReadTimeout,
- p.conf.WriteTimeout,
- p.conf.WriteQueueSize,
- false,
- "",
- "",
- p.conf.RTSPAddress,
- p.conf.RunOnConnect,
- p.conf.RunOnConnectRestart,
- p.conf.RunOnDisconnect,
- p.externalCmdPool,
- p.metrics,
- p.pathManager,
- p,
- )
+ p.rtmpServer = &rtmp.Server{
+ Address: p.conf.RTMPAddress,
+ ReadTimeout: p.conf.ReadTimeout,
+ WriteTimeout: p.conf.WriteTimeout,
+ WriteQueueSize: p.conf.WriteQueueSize,
+ IsTLS: false,
+ ServerCert: "",
+ ServerKey: "",
+ RTSPAddress: p.conf.RTSPAddress,
+ RunOnConnect: p.conf.RunOnConnect,
+ RunOnConnectRestart: p.conf.RunOnConnectRestart,
+ RunOnDisconnect: p.conf.RunOnDisconnect,
+ ExternalCmdPool: p.externalCmdPool,
+ PathManager: p.pathManager,
+ Parent: p,
+ }
+ err := p.rtmpServer.Initialize()
if err != nil {
return err
}
+
+ if p.metrics != nil {
+ p.metrics.SetRTMPServer(p.rtmpServer)
+ }
}
if p.conf.RTMP &&
(p.conf.RTMPEncryption == conf.EncryptionStrict ||
p.conf.RTMPEncryption == conf.EncryptionOptional) &&
p.rtmpsServer == nil {
- p.rtmpsServer, err = newRTMPServer(
- p.conf.RTMPSAddress,
- p.conf.ReadTimeout,
- p.conf.WriteTimeout,
- p.conf.WriteQueueSize,
- true,
- p.conf.RTMPServerCert,
- p.conf.RTMPServerKey,
- p.conf.RTSPAddress,
- p.conf.RunOnConnect,
- p.conf.RunOnConnectRestart,
- p.conf.RunOnDisconnect,
- p.externalCmdPool,
- p.metrics,
- p.pathManager,
- p,
- )
+ p.rtmpsServer = &rtmp.Server{
+ Address: p.conf.RTMPSAddress,
+ ReadTimeout: p.conf.ReadTimeout,
+ WriteTimeout: p.conf.WriteTimeout,
+ WriteQueueSize: p.conf.WriteQueueSize,
+ IsTLS: true,
+ ServerCert: p.conf.RTMPServerCert,
+ ServerKey: p.conf.RTMPServerKey,
+ RTSPAddress: p.conf.RTSPAddress,
+ RunOnConnect: p.conf.RunOnConnect,
+ RunOnConnectRestart: p.conf.RunOnConnectRestart,
+ RunOnDisconnect: p.conf.RunOnDisconnect,
+ ExternalCmdPool: p.externalCmdPool,
+ PathManager: p.pathManager,
+ Parent: p,
+ }
+ err := p.rtmpsServer.Initialize()
if err != nil {
return err
}
+
+ if p.metrics != nil {
+ p.metrics.SetRTMPSServer(p.rtmpsServer)
+ }
}
if p.conf.HLS &&
- p.hlsManager == nil {
- p.hlsManager, err = newHLSManager(
- p.conf.HLSAddress,
- p.conf.HLSEncryption,
- p.conf.HLSServerKey,
- p.conf.HLSServerCert,
- p.conf.ExternalAuthenticationURL,
- p.conf.HLSAlwaysRemux,
- p.conf.HLSVariant,
- p.conf.HLSSegmentCount,
- p.conf.HLSSegmentDuration,
- p.conf.HLSPartDuration,
- p.conf.HLSSegmentMaxSize,
- p.conf.HLSAllowOrigin,
- p.conf.HLSTrustedProxies,
- p.conf.HLSDirectory,
- p.conf.ReadTimeout,
- p.conf.WriteQueueSize,
- p.pathManager,
- p.metrics,
- p,
- )
+ p.hlsServer == nil {
+ p.hlsServer = &hls.Server{
+ Address: p.conf.HLSAddress,
+ Encryption: p.conf.HLSEncryption,
+ ServerKey: p.conf.HLSServerKey,
+ ServerCert: p.conf.HLSServerCert,
+ ExternalAuthenticationURL: p.conf.ExternalAuthenticationURL,
+ AlwaysRemux: p.conf.HLSAlwaysRemux,
+ Variant: p.conf.HLSVariant,
+ SegmentCount: p.conf.HLSSegmentCount,
+ SegmentDuration: p.conf.HLSSegmentDuration,
+ PartDuration: p.conf.HLSPartDuration,
+ SegmentMaxSize: p.conf.HLSSegmentMaxSize,
+ AllowOrigin: p.conf.HLSAllowOrigin,
+ TrustedProxies: p.conf.HLSTrustedProxies,
+ Directory: p.conf.HLSDirectory,
+ ReadTimeout: p.conf.ReadTimeout,
+ WriteQueueSize: p.conf.WriteQueueSize,
+ PathManager: p.pathManager,
+ Parent: p,
+ }
+ err := p.hlsServer.Initialize()
if err != nil {
return err
}
+
+ p.pathManager.setHLSServer(p.hlsServer)
+
+ if p.metrics != nil {
+ p.metrics.SetHLSServer(p.hlsServer)
+ }
}
if p.conf.WebRTC &&
- p.webRTCManager == nil {
- p.webRTCManager, err = newWebRTCManager(
- p.conf.WebRTCAddress,
- p.conf.WebRTCEncryption,
- p.conf.WebRTCServerKey,
- p.conf.WebRTCServerCert,
- p.conf.WebRTCAllowOrigin,
- p.conf.WebRTCTrustedProxies,
- p.conf.WebRTCICEServers2,
- p.conf.ReadTimeout,
- p.conf.WriteQueueSize,
- p.conf.WebRTCICEHostNAT1To1IPs,
- p.conf.WebRTCICEUDPMuxAddress,
- p.conf.WebRTCICETCPMuxAddress,
- p.externalCmdPool,
- p.pathManager,
- p.metrics,
- p,
- )
+ p.webRTCServer == nil {
+ p.webRTCServer = &webrtc.Server{
+ Address: p.conf.WebRTCAddress,
+ Encryption: p.conf.WebRTCEncryption,
+ ServerKey: p.conf.WebRTCServerKey,
+ ServerCert: p.conf.WebRTCServerCert,
+ AllowOrigin: p.conf.WebRTCAllowOrigin,
+ TrustedProxies: p.conf.WebRTCTrustedProxies,
+ ReadTimeout: p.conf.ReadTimeout,
+ WriteQueueSize: p.conf.WriteQueueSize,
+ LocalUDPAddress: p.conf.WebRTCLocalUDPAddress,
+ LocalTCPAddress: p.conf.WebRTCLocalTCPAddress,
+ IPsFromInterfaces: p.conf.WebRTCIPsFromInterfaces,
+ IPsFromInterfacesList: p.conf.WebRTCIPsFromInterfacesList,
+ AdditionalHosts: p.conf.WebRTCAdditionalHosts,
+ ICEServers: p.conf.WebRTCICEServers2,
+ ExternalCmdPool: p.externalCmdPool,
+ PathManager: p.pathManager,
+ Parent: p,
+ }
+ err := p.webRTCServer.Initialize()
if err != nil {
+ p.webRTCServer = nil
return err
}
+
+ if p.metrics != nil {
+ p.metrics.SetWebRTCServer(p.webRTCServer)
+ }
}
if p.conf.SRT &&
p.srtServer == nil {
- p.srtServer, err = newSRTServer(
- p.conf.SRTAddress,
- p.conf.RTSPAddress,
- p.conf.ReadTimeout,
- p.conf.WriteTimeout,
- p.conf.WriteQueueSize,
- p.conf.UDPMaxPayloadSize,
- p.conf.RunOnConnect,
- p.conf.RunOnConnectRestart,
- p.conf.RunOnDisconnect,
- p.externalCmdPool,
- p.pathManager,
- p,
- )
+ p.srtServer = &srt.Server{
+ Address: p.conf.SRTAddress,
+ RTSPAddress: p.conf.RTSPAddress,
+ ReadTimeout: p.conf.ReadTimeout,
+ WriteTimeout: p.conf.WriteTimeout,
+ WriteQueueSize: p.conf.WriteQueueSize,
+ UDPMaxPayloadSize: p.conf.UDPMaxPayloadSize,
+ RunOnConnect: p.conf.RunOnConnect,
+ RunOnConnectRestart: p.conf.RunOnConnectRestart,
+ RunOnDisconnect: p.conf.RunOnDisconnect,
+ ExternalCmdPool: p.externalCmdPool,
+ PathManager: p.pathManager,
+ Parent: p,
+ }
+ err := p.srtServer.Initialize()
if err != nil {
return err
}
+
+ if p.metrics != nil {
+ p.metrics.SetSRTServer(p.srtServer)
+ }
}
if p.conf.API &&
p.api == nil {
- p.api, err = newAPI(
- p.conf.APIAddress,
- p.conf.ReadTimeout,
- p.conf,
- p.pathManager,
- p.rtspServer,
- p.rtspsServer,
- p.rtmpServer,
- p.rtmpsServer,
- p.hlsManager,
- p.webRTCManager,
- p.srtServer,
- p,
- )
+ p.api = &api.API{
+ Address: p.conf.APIAddress,
+ ReadTimeout: p.conf.ReadTimeout,
+ Conf: p.conf,
+ PathManager: p.pathManager,
+ RTSPServer: p.rtspServer,
+ RTSPSServer: p.rtspsServer,
+ RTMPServer: p.rtmpServer,
+ RTMPSServer: p.rtmpsServer,
+ HLSServer: p.hlsServer,
+ WebRTCServer: p.webRTCServer,
+ SRTServer: p.srtServer,
+ Parent: p,
+ }
+ err := p.api.Initialize()
if err != nil {
return err
}
@@ -538,11 +633,20 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeLogger
closeRecorderCleaner := newConf == nil ||
- newConf.Record != p.conf.Record ||
- newConf.RecordPath != p.conf.RecordPath ||
- newConf.RecordDeleteAfter != p.conf.RecordDeleteAfter
+ !reflect.DeepEqual(gatherCleanerEntries(newConf.Paths), gatherCleanerEntries(p.conf.Paths)) ||
+ closeLogger
+
+ closePlaybackServer := newConf == nil ||
+ newConf.Playback != p.conf.Playback ||
+ newConf.PlaybackAddress != p.conf.PlaybackAddress ||
+ newConf.ReadTimeout != p.conf.ReadTimeout ||
+ closeLogger
+ if !closePlaybackServer && p.playbackServer != nil && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
+ p.playbackServer.ReloadPathConfs(newConf.Paths)
+ }
closePathManager := newConf == nil ||
+ newConf.LogLevel != p.conf.LogLevel ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
newConf.RTSPAddress != p.conf.RTSPAddress ||
!reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) ||
@@ -550,14 +654,10 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.WriteQueueSize != p.conf.WriteQueueSize ||
newConf.UDPMaxPayloadSize != p.conf.UDPMaxPayloadSize ||
- newConf.Record != p.conf.Record ||
- newConf.RecordPath != p.conf.RecordPath ||
- newConf.RecordPartDuration != p.conf.RecordPartDuration ||
- newConf.RecordSegmentDuration != p.conf.RecordSegmentDuration ||
closeMetrics ||
closeLogger
if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
- p.pathManager.confReload(newConf.Paths)
+ p.pathManager.ReloadPathConfs(newConf.Paths)
}
closeRTSPServer := newConf == nil ||
@@ -634,7 +734,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closePathManager ||
closeLogger
- closeHLSManager := newConf == nil ||
+ closeHLSServer := newConf == nil ||
newConf.HLS != p.conf.HLS ||
newConf.HLSAddress != p.conf.HLSAddress ||
newConf.HLSEncryption != p.conf.HLSEncryption ||
@@ -656,7 +756,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeMetrics ||
closeLogger
- closeWebRTCManager := newConf == nil ||
+ closeWebRTCServer := newConf == nil ||
newConf.WebRTC != p.conf.WebRTC ||
newConf.WebRTCAddress != p.conf.WebRTCAddress ||
newConf.WebRTCEncryption != p.conf.WebRTCEncryption ||
@@ -664,12 +764,14 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.WebRTCServerCert != p.conf.WebRTCServerCert ||
newConf.WebRTCAllowOrigin != p.conf.WebRTCAllowOrigin ||
!reflect.DeepEqual(newConf.WebRTCTrustedProxies, p.conf.WebRTCTrustedProxies) ||
- !reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.WriteQueueSize != p.conf.WriteQueueSize ||
- !reflect.DeepEqual(newConf.WebRTCICEHostNAT1To1IPs, p.conf.WebRTCICEHostNAT1To1IPs) ||
- newConf.WebRTCICEUDPMuxAddress != p.conf.WebRTCICEUDPMuxAddress ||
- newConf.WebRTCICETCPMuxAddress != p.conf.WebRTCICETCPMuxAddress ||
+ newConf.WebRTCLocalUDPAddress != p.conf.WebRTCLocalUDPAddress ||
+ newConf.WebRTCLocalTCPAddress != p.conf.WebRTCLocalTCPAddress ||
+ newConf.WebRTCIPsFromInterfaces != p.conf.WebRTCIPsFromInterfaces ||
+ !reflect.DeepEqual(newConf.WebRTCIPsFromInterfacesList, p.conf.WebRTCIPsFromInterfacesList) ||
+ !reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) ||
+ !reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
closeMetrics ||
closePathManager ||
closeLogger
@@ -696,8 +798,8 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeRTSPServer ||
closeRTSPSServer ||
closeRTMPServer ||
- closeHLSManager ||
- closeWebRTCManager ||
+ closeHLSServer ||
+ closeWebRTCServer ||
closeSRTServer ||
closeLogger
@@ -708,65 +810,104 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
if p.api != nil {
if closeAPI {
- p.api.close()
+ p.api.Close()
p.api = nil
} else if !calledByAPI { // avoid a loop
- p.api.confReload(newConf)
+ p.api.ReloadConf(newConf)
}
}
if closeSRTServer && p.srtServer != nil {
- p.srtServer.close()
+ if p.metrics != nil {
+ p.metrics.SetSRTServer(nil)
+ }
+
+ p.srtServer.Close()
p.srtServer = nil
}
- if closeWebRTCManager && p.webRTCManager != nil {
- p.webRTCManager.close()
- p.webRTCManager = nil
+ if closeWebRTCServer && p.webRTCServer != nil {
+ if p.metrics != nil {
+ p.metrics.SetWebRTCServer(nil)
+ }
+
+ p.webRTCServer.Close()
+ p.webRTCServer = nil
}
- if closeHLSManager && p.hlsManager != nil {
- p.hlsManager.close()
- p.hlsManager = nil
+ if closeHLSServer && p.hlsServer != nil {
+ if p.metrics != nil {
+ p.metrics.SetHLSServer(nil)
+ }
+
+ p.pathManager.setHLSServer(nil)
+
+ p.hlsServer.Close()
+ p.hlsServer = nil
}
if closeRTMPSServer && p.rtmpsServer != nil {
- p.rtmpsServer.close()
+ if p.metrics != nil {
+ p.metrics.SetRTMPSServer(nil)
+ }
+
+ p.rtmpsServer.Close()
p.rtmpsServer = nil
}
if closeRTMPServer && p.rtmpServer != nil {
- p.rtmpServer.close()
+ if p.metrics != nil {
+ p.metrics.SetRTMPServer(nil)
+ }
+
+ p.rtmpServer.Close()
p.rtmpServer = nil
}
if closeRTSPSServer && p.rtspsServer != nil {
- p.rtspsServer.close()
+ if p.metrics != nil {
+ p.metrics.SetRTSPSServer(nil)
+ }
+
+ p.rtspsServer.Close()
p.rtspsServer = nil
}
if closeRTSPServer && p.rtspServer != nil {
- p.rtspServer.close()
+ if p.metrics != nil {
+ p.metrics.SetRTSPServer(nil)
+ }
+
+ p.rtspServer.Close()
p.rtspServer = nil
}
if closePathManager && p.pathManager != nil {
+ if p.metrics != nil {
+ p.metrics.SetPathManager(nil)
+ }
+
p.pathManager.close()
p.pathManager = nil
}
+ if closePlaybackServer && p.playbackServer != nil {
+ p.playbackServer.Close()
+ p.playbackServer = nil
+ }
+
if closeRecorderCleaner && p.recordCleaner != nil {
p.recordCleaner.Close()
p.recordCleaner = nil
}
if closePPROF && p.pprof != nil {
- p.pprof.close()
+ p.pprof.Close()
p.pprof = nil
}
if closeMetrics && p.metrics != nil {
- p.metrics.close()
+ p.metrics.Close()
p.metrics = nil
}
@@ -787,8 +928,8 @@ func (p *Core) reloadConf(newConf *conf.Conf, calledByAPI bool) error {
return p.createResources(false)
}
-// apiConfigSet is called by api.
-func (p *Core) apiConfigSet(conf *conf.Conf) {
+// APIConfigSet is called by api.
+func (p *Core) APIConfigSet(conf *conf.Conf) {
select {
case p.chAPIConfigSet <- conf:
case <-p.ctx.Done():
diff --git a/internal/core/hls.min.js b/internal/core/hls.min.js
deleted file mode 100644
index a8dbeb2be2b..00000000000
--- a/internal/core/hls.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function t(e){var r,i;r=this,i=function(){"use strict";function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,i)}return r}function i(t){for(var e=1;et.length)&&(e=t.length);for(var r=0,i=new Array(e);r=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function m(t){var e=function(t,e){if("object"!=typeof t||null===t)return t;var r=t[Symbol.toPrimitive];if(void 0!==r){var i=r.call(t,e||"default");if("object"!=typeof i)return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}(t,"string");return"symbol"==typeof e?e:String(e)}function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var y={exports:{}};!function(t,e){var r,i,n,a,s;r=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,i=/^(?=([^\/?#]*))\1([^]*)$/,n=/(?:\/|^)\.(?=\/)/g,a=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,s={buildAbsoluteURL:function(t,e,r){if(r=r||{},t=t.trim(),!(e=e.trim())){if(!r.alwaysNormalize)return t;var n=s.parseURL(t);if(!n)throw new Error("Error trying to parse base URL.");return n.path=s.normalizePath(n.path),s.buildURLFromParts(n)}var a=s.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return r.alwaysNormalize?(a.path=s.normalizePath(a.path),s.buildURLFromParts(a)):e;var o=s.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=i.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var u={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(u.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var h=o.path,d=h.substring(0,h.lastIndexOf("/")+1)+a.path;u.path=s.normalizePath(d)}else u.path=o.path,a.params||(u.params=o.params,a.query||(u.query=o.query));return null===u.path&&(u.path=r.alwaysNormalize?s.normalizePath(a.path):a.path),s.buildURLFromParts(u)},parseURL:function(t){var e=r.exec(t);return e?{scheme:e[1]||"",netLoc:e[2]||"",path:e[3]||"",params:e[4]||"",query:e[5]||"",fragment:e[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(n,"");t.length!==(t=t.replace(a,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}},t.exports=s}(y);var T=y.exports,E=Number.isFinite||function(t){return"number"==typeof t&&isFinite(t)},S=function(t){return t.MEDIA_ATTACHING="hlsMediaAttaching",t.MEDIA_ATTACHED="hlsMediaAttached",t.MEDIA_DETACHING="hlsMediaDetaching",t.MEDIA_DETACHED="hlsMediaDetached",t.BUFFER_RESET="hlsBufferReset",t.BUFFER_CODECS="hlsBufferCodecs",t.BUFFER_CREATED="hlsBufferCreated",t.BUFFER_APPENDING="hlsBufferAppending",t.BUFFER_APPENDED="hlsBufferAppended",t.BUFFER_EOS="hlsBufferEos",t.BUFFER_FLUSHING="hlsBufferFlushing",t.BUFFER_FLUSHED="hlsBufferFlushed",t.MANIFEST_LOADING="hlsManifestLoading",t.MANIFEST_LOADED="hlsManifestLoaded",t.MANIFEST_PARSED="hlsManifestParsed",t.LEVEL_SWITCHING="hlsLevelSwitching",t.LEVEL_SWITCHED="hlsLevelSwitched",t.LEVEL_LOADING="hlsLevelLoading",t.LEVEL_LOADED="hlsLevelLoaded",t.LEVEL_UPDATED="hlsLevelUpdated",t.LEVEL_PTS_UPDATED="hlsLevelPtsUpdated",t.LEVELS_UPDATED="hlsLevelsUpdated",t.AUDIO_TRACKS_UPDATED="hlsAudioTracksUpdated",t.AUDIO_TRACK_SWITCHING="hlsAudioTrackSwitching",t.AUDIO_TRACK_SWITCHED="hlsAudioTrackSwitched",t.AUDIO_TRACK_LOADING="hlsAudioTrackLoading",t.AUDIO_TRACK_LOADED="hlsAudioTrackLoaded",t.SUBTITLE_TRACKS_UPDATED="hlsSubtitleTracksUpdated",t.SUBTITLE_TRACKS_CLEARED="hlsSubtitleTracksCleared",t.SUBTITLE_TRACK_SWITCH="hlsSubtitleTrackSwitch",t.SUBTITLE_TRACK_LOADING="hlsSubtitleTrackLoading",t.SUBTITLE_TRACK_LOADED="hlsSubtitleTrackLoaded",t.SUBTITLE_FRAG_PROCESSED="hlsSubtitleFragProcessed",t.CUES_PARSED="hlsCuesParsed",t.NON_NATIVE_TEXT_TRACKS_FOUND="hlsNonNativeTextTracksFound",t.INIT_PTS_FOUND="hlsInitPtsFound",t.FRAG_LOADING="hlsFragLoading",t.FRAG_LOAD_EMERGENCY_ABORTED="hlsFragLoadEmergencyAborted",t.FRAG_LOADED="hlsFragLoaded",t.FRAG_DECRYPTED="hlsFragDecrypted",t.FRAG_PARSING_INIT_SEGMENT="hlsFragParsingInitSegment",t.FRAG_PARSING_USERDATA="hlsFragParsingUserdata",t.FRAG_PARSING_METADATA="hlsFragParsingMetadata",t.FRAG_PARSED="hlsFragParsed",t.FRAG_BUFFERED="hlsFragBuffered",t.FRAG_CHANGED="hlsFragChanged",t.FPS_DROP="hlsFpsDrop",t.FPS_DROP_LEVEL_CAPPING="hlsFpsDropLevelCapping",t.ERROR="hlsError",t.DESTROYING="hlsDestroying",t.KEY_LOADING="hlsKeyLoading",t.KEY_LOADED="hlsKeyLoaded",t.LIVE_BACK_BUFFER_REACHED="hlsLiveBackBufferReached",t.BACK_BUFFER_REACHED="hlsBackBufferReached",t}({}),L=function(t){return t.NETWORK_ERROR="networkError",t.MEDIA_ERROR="mediaError",t.KEY_SYSTEM_ERROR="keySystemError",t.MUX_ERROR="muxError",t.OTHER_ERROR="otherError",t}({}),R=function(t){return t.KEY_SYSTEM_NO_KEYS="keySystemNoKeys",t.KEY_SYSTEM_NO_ACCESS="keySystemNoAccess",t.KEY_SYSTEM_NO_SESSION="keySystemNoSession",t.KEY_SYSTEM_NO_CONFIGURED_LICENSE="keySystemNoConfiguredLicense",t.KEY_SYSTEM_LICENSE_REQUEST_FAILED="keySystemLicenseRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED="keySystemServerCertificateRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED="keySystemServerCertificateUpdateFailed",t.KEY_SYSTEM_SESSION_UPDATE_FAILED="keySystemSessionUpdateFailed",t.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED="keySystemStatusOutputRestricted",t.KEY_SYSTEM_STATUS_INTERNAL_ERROR="keySystemStatusInternalError",t.MANIFEST_LOAD_ERROR="manifestLoadError",t.MANIFEST_LOAD_TIMEOUT="manifestLoadTimeOut",t.MANIFEST_PARSING_ERROR="manifestParsingError",t.MANIFEST_INCOMPATIBLE_CODECS_ERROR="manifestIncompatibleCodecsError",t.LEVEL_EMPTY_ERROR="levelEmptyError",t.LEVEL_LOAD_ERROR="levelLoadError",t.LEVEL_LOAD_TIMEOUT="levelLoadTimeOut",t.LEVEL_PARSING_ERROR="levelParsingError",t.LEVEL_SWITCH_ERROR="levelSwitchError",t.AUDIO_TRACK_LOAD_ERROR="audioTrackLoadError",t.AUDIO_TRACK_LOAD_TIMEOUT="audioTrackLoadTimeOut",t.SUBTITLE_LOAD_ERROR="subtitleTrackLoadError",t.SUBTITLE_TRACK_LOAD_TIMEOUT="subtitleTrackLoadTimeOut",t.FRAG_LOAD_ERROR="fragLoadError",t.FRAG_LOAD_TIMEOUT="fragLoadTimeOut",t.FRAG_DECRYPT_ERROR="fragDecryptError",t.FRAG_PARSING_ERROR="fragParsingError",t.FRAG_GAP="fragGap",t.REMUX_ALLOC_ERROR="remuxAllocError",t.KEY_LOAD_ERROR="keyLoadError",t.KEY_LOAD_TIMEOUT="keyLoadTimeOut",t.BUFFER_ADD_CODEC_ERROR="bufferAddCodecError",t.BUFFER_INCOMPATIBLE_CODECS_ERROR="bufferIncompatibleCodecsError",t.BUFFER_APPEND_ERROR="bufferAppendError",t.BUFFER_APPENDING_ERROR="bufferAppendingError",t.BUFFER_STALLED_ERROR="bufferStalledError",t.BUFFER_FULL_ERROR="bufferFullError",t.BUFFER_SEEK_OVER_HOLE="bufferSeekOverHole",t.BUFFER_NUDGE_ON_STALL="bufferNudgeOnStall",t.INTERNAL_EXCEPTION="internalException",t.INTERNAL_ABORTED="aborted",t.UNKNOWN="unknown",t}({}),A=function(){},k={trace:A,debug:A,log:A,warn:A,info:A,error:A},b=k;function D(t){var e=self.console[t];return e?e.bind(self.console,"["+t+"] >"):A}function I(t,e){if(self.console&&!0===t||"object"==typeof t){!function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;iNumber.MAX_SAFE_INTEGER?1/0:e},e.hexadecimalInteger=function(t){if(this[t]){var e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;for(var r=new Uint8Array(e.length/2),i=0;iNumber.MAX_SAFE_INTEGER?1/0:e},e.decimalFloatingPoint=function(t){return parseFloat(this[t])},e.optionalFloat=function(t,e){var r=this[t];return r?parseFloat(r):e},e.enumeratedString=function(t){return this[t]},e.bool=function(t){return"YES"===this[t]},e.decimalResolution=function(t){var e=C.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}},t.parseAttrList=function(t){var e,r={};for(_.lastIndex=0;null!==(e=_.exec(t));){var i=e[2];0===i.indexOf('"')&&i.lastIndexOf('"')===i.length-1&&(i=i.slice(1,-1)),r[e[1].trim()]=i}return r},t}();function x(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}var F=function(){function t(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){var r=e.attr;for(var i in r)if(Object.prototype.hasOwnProperty.call(t,i)&&t[i]!==r[i]){w.warn('DATERANGE tag attribute: "'+i+'" does not match for tags with ID: "'+t.ID+'"'),this._badValueForSameId=i;break}t=o(new P({}),r,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){var n=new Date(this.attr["END-DATE"]);E(n.getTime())&&(this._endDate=n)}}return a(t,[{key:"id",get:function(){return this.attr.ID}},{key:"class",get:function(){return this.attr.CLASS}},{key:"startDate",get:function(){return this._startDate}},{key:"endDate",get:function(){if(this._endDate)return this._endDate;var t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}},{key:"duration",get:function(){if("DURATION"in this.attr){var t=this.attr.decimalFloatingPoint("DURATION");if(E(t))return t}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}},{key:"plannedDuration",get:function(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}},{key:"endOnNext",get:function(){return this.attr.bool("END-ON-NEXT")}},{key:"isValid",get:function(){return!!this.id&&!this._badValueForSameId&&E(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}]),t}(),M=function(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}},O="audio",N="video",U="audiovideo",B=function(){function t(t){var e;this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams=((e={})[O]=null,e[N]=null,e[U]=null,e),this.baseurl=t}return t.prototype.setByteRange=function(t,e){var r=t.split("@",2),i=[];1===r.length?i[0]=e?e.byteRangeEndOffset:0:i[0]=parseInt(r[1]),i[1]=parseInt(r[0])+i[0],this._byteRange=i},a(t,[{key:"byteRange",get:function(){return this._byteRange?this._byteRange:[]}},{key:"byteRangeStartOffset",get:function(){return this.byteRange[0]}},{key:"byteRangeEndOffset",get:function(){return this.byteRange[1]}},{key:"url",get:function(){return!this._url&&this.baseurl&&this.relurl&&(this._url=T.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""},set:function(t){this._url=t}}]),t}(),G=function(t){function e(e,r){var i;return(i=t.call(this,r)||this)._decryptdata=null,i.rawProgramDateTime=null,i.programDateTime=null,i.tagList=[],i.duration=0,i.sn=0,i.levelkeys=void 0,i.type=void 0,i.loader=null,i.keyLoader=null,i.level=-1,i.cc=0,i.startPTS=void 0,i.endPTS=void 0,i.startDTS=void 0,i.endDTS=void 0,i.start=0,i.deltaPTS=void 0,i.maxStartPTS=void 0,i.minEndPTS=void 0,i.stats=new M,i.urlId=0,i.data=void 0,i.bitrateTest=!1,i.title=null,i.initSegment=null,i.endList=void 0,i.gap=void 0,i.type=e,i}l(e,t);var r=e.prototype;return r.setKeyFormat=function(t){if(this.levelkeys){var e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}},r.abortRequests=function(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()},r.setElementaryStreamInfo=function(t,e,r,i,n,a){void 0===a&&(a=!1);var s=this.elementaryStreams,o=s[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,r),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,n)):s[t]={startPTS:e,endPTS:r,startDTS:i,endDTS:n,partial:a}},r.clearElementaryStreamInfo=function(){var t=this.elementaryStreams;t[O]=null,t[N]=null,t[U]=null},a(e,[{key:"decryptdata",get:function(){if(!this.levelkeys&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){var t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{var e=Object.keys(this.levelkeys);if(1===e.length)return this._decryptdata=this.levelkeys[e[0]].getDecryptData(this.sn)}}return this._decryptdata}},{key:"end",get:function(){return this.start+this.duration}},{key:"endProgramDateTime",get:function(){if(null===this.programDateTime)return null;if(!E(this.programDateTime))return null;var t=E(this.duration)?this.duration:0;return this.programDateTime+1e3*t}},{key:"encrypted",get:function(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){var e=Object.keys(this.levelkeys),r=e.length;if(r>1||1===r&&this.levelkeys[e[0]].encrypted)return!0}return!1}}]),e}(B),K=function(t){function e(e,r,i,n,a){var s;(s=t.call(this,i)||this).fragOffset=0,s.duration=0,s.gap=!1,s.independent=!1,s.relurl=void 0,s.fragment=void 0,s.index=void 0,s.stats=new M,s.duration=e.decimalFloatingPoint("DURATION"),s.gap=e.bool("GAP"),s.independent=e.bool("INDEPENDENT"),s.relurl=e.enumeratedString("URI"),s.fragment=r,s.index=n;var o=e.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,a),a&&(s.fragOffset=a.fragOffset+a.duration),s}return l(e,t),a(e,[{key:"start",get:function(){return this.fragment.start+this.fragOffset}},{key:"end",get:function(){return this.start+this.duration}},{key:"loaded",get:function(){var t=this.elementaryStreams;return!!(t.audio||t.video||t.audiovideo)}}]),e}(B),H=function(){function t(t){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}return t.prototype.reloaded=function(t){if(!t)return this.advanced=!0,void(this.updated=!0);var e=this.lastPartSn-t.lastPartSn,r=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!r||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&r>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay},a(t,[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&E(this.fragments[this.fragments.length-1].programDateTime)}},{key:"levelTargetDuration",get:function(){return this.averagetargetduration||this.targetduration||10}},{key:"drift",get:function(){var t=this.driftEndTime-this.driftStartTime;return t>0?1e3*(this.driftEnd-this.driftStart)/t:1}},{key:"edge",get:function(){return this.partEnd||this.fragmentEnd}},{key:"partEnd",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}},{key:"age",get:function(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}},{key:"lastPartIndex",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}},{key:"lastPartSn",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}]),t}();function V(t){return Uint8Array.from(atob(t),(function(t){return t.charCodeAt(0)}))}function Y(t){var e,r,i=t.split(":"),n=null;if("data"===i[0]&&2===i.length){var a=i[1].split(";"),s=a[a.length-1].split(",");if(2===s.length){var o="base64"===s[0],l=s[1];o?(a.splice(-1,1),n=V(l)):(e=W(l).subarray(0,16),(r=new Uint8Array(16)).set(e,16-e.length),n=r)}}return n}function W(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(function(t){return t.charCodeAt(0)}))}var j={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},q="org.w3.clearkey",X="com.apple.streamingkeydelivery",z="com.microsoft.playready",Q="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function $(t){switch(t){case X:return j.FAIRPLAY;case z:return j.PLAYREADY;case Q:return j.WIDEVINE;case q:return j.CLEARKEY}}var J="edef8ba979d64acea3c827dcd51d21ed";function Z(t){switch(t){case j.FAIRPLAY:return X;case j.PLAYREADY:return z;case j.WIDEVINE:return Q;case j.CLEARKEY:return q}}function tt(t){var e=t.drmSystems,r=t.widevineLicenseUrl,i=e?[j.FAIRPLAY,j.WIDEVINE,j.PLAYREADY,j.CLEARKEY].filter((function(t){return!!e[t]})):[];return!i[j.WIDEVINE]&&r&&i.push(j.WIDEVINE),i}var et="undefined"!=typeof self&&self.navigator&&self.navigator.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function rt(t,e,r){return Uint8Array.prototype.slice?t.slice(e,r):new Uint8Array(Array.prototype.slice.call(t,e,r))}var it,nt=function(t,e){return e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},at=function(t,e){return e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},st=function(t,e){for(var r=e,i=0;nt(t,e);)i+=10,i+=ot(t,e+6),at(t,e+10)&&(i+=10),e+=i;if(i>0)return t.subarray(r,r+i)},ot=function(t,e){var r=0;return r=(127&t[e])<<21,r|=(127&t[e+1])<<14,r|=(127&t[e+2])<<7,r|=127&t[e+3]},lt=function(t,e){return nt(t,e)&&ot(t,e+6)+10<=t.length-e},ut=function(t){return t&&"PRIV"===t.key&&"com.apple.streaming.transportStreamTimestamp"===t.info},ht=function(t){var e=String.fromCharCode(t[0],t[1],t[2],t[3]),r=ot(t,4);return{type:e,size:r,data:t.subarray(10,10+r)}},dt=function(t){for(var e=0,r=[];nt(t,e);){for(var i=ot(t,e+6),n=(e+=10)+i;e+8>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:u+=String.fromCharCode(a);break;case 12:case 13:s=t[h++],u+=String.fromCharCode((31&a)<<6|63&s);break;case 14:s=t[h++],o=t[h++],u+=String.fromCharCode((15&a)<<12|(63&s)<<6|(63&o)<<0)}}return u};function yt(){return it||void 0===self.TextDecoder||(it=new self.TextDecoder("utf-8")),it}var Tt=function(t){for(var e="",r=0;r>24,t[e+1]=r>>16&255,t[e+2]=r>>8&255,t[e+3]=255&r}function It(t,e){var r=[];if(!e.length)return r;for(var i=t.byteLength,n=0;n1?n+a:i;if(Rt(t.subarray(n+4,n+8))===e[0])if(1===e.length)r.push(t.subarray(n+8,s));else{var o=It(t.subarray(n+8,s),e.slice(1));o.length&&St.apply(r,o)}n=s}return r}function wt(t){var e=[],r=t[0],i=8,n=kt(t,i);i+=4,i+=0===r?8:16,i+=2;var a=t.length+0,s=At(t,i);i+=2;for(var o=0;o>>31)return w.warn("SIDX has hierarchical references (not supported)"),null;var d=kt(t,l);l+=4,e.push({referenceSize:h,subsegmentDuration:d,info:{duration:d/n,start:a,end:a+h-1}}),a+=h,i=l+=4}return{earliestPresentationTime:0,timescale:n,version:r,referencesCount:s,references:e}}function Ct(t){for(var e=[],r=It(t,["moov","trak"]),i=0;i>1&63;return 39===r||40===r}return 6==(31&e)}function Ot(t,e,r,i){var n=Nt(t),a=0;a+=e;for(var s=0,o=0,l=!1,u=0;a=n.length)break;s+=u=n[a++]}while(255===u);o=0;do{if(a>=n.length)break;o+=u=n[a++]}while(255===u);var h=n.length-a;if(!l&&4===s&&a16){for(var T=[],E=0;E<16;E++){var S=n[a++].toString(16);T.push(1==S.length?"0"+S:S),3!==E&&5!==E&&7!==E&&9!==E||T.push("-")}for(var L=o-16,R=new Uint8Array(L),A=0;Ah)break}}function Nt(t){for(var e=t.byteLength,r=[],i=1;i0?(a=new Uint8Array(4),e.length>0&&new DataView(a.buffer).setUint32(0,e.length,!1)):a=new Uint8Array;var l=new Uint8Array(4);return r&&r.byteLength>0&&new DataView(l.buffer).setUint32(0,r.byteLength,!1),function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i>24&255,o[1]=a>>16&255,o[2]=a>>8&255,o[3]=255&a,o.set(t,4),s=0,a=8;s>8*(15-r)&255;return e}(e);return new t(this.method,this.uri,"identity",this.keyFormatVersions,r)}var i=Y(this.uri);if(i)switch(this.keyFormat){case Q:this.pssh=i,i.length>=22&&(this.keyId=i.subarray(i.length-22,i.length-6));break;case z:var n=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=Ut(n,null,i);var a=new Uint16Array(i.buffer,i.byteOffset,i.byteLength/2),s=String.fromCharCode.apply(null,Array.from(a)),o=s.substring(s.indexOf("<"),s.length),l=(new DOMParser).parseFromString(o,"text/xml").getElementsByTagName("KID")[0];if(l){var u=l.childNodes[0]?l.childNodes[0].nodeValue:l.getAttribute("VALUE");if(u){var h=V(u).subarray(0,16);!function(t){var e=function(t,e,r){var i=t[e];t[e]=t[r],t[r]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(h),this.keyId=h}}break;default:var d=i.subarray(0,16);if(16!==d.length){var c=new Uint8Array(16);c.set(d,16-d.length),d=c}this.keyId=d}if(!this.keyId||16!==this.keyId.byteLength){var f=Bt[this.uri];if(!f){var g=Object.keys(Bt).length%Number.MAX_SAFE_INTEGER;f=new Uint8Array(16),new DataView(f.buffer,12,4).setUint32(0,g),Bt[this.uri]=f}this.keyId=f}return this},t}(),Kt=/\{\$([a-zA-Z0-9-_]+)\}/g;function Ht(t){return Kt.test(t)}function Vt(t,e,r){if(null!==t.variableList||t.hasVariableRefs)for(var i=r.length;i--;){var n=r[i],a=e[n];a&&(e[n]=Yt(t,a))}}function Yt(t,e){if(null!==t.variableList||t.hasVariableRefs){var r=t.variableList;return e.replace(Kt,(function(e){var i=e.substring(2,e.length-1),n=null==r?void 0:r[i];return void 0===n?(t.playlistParsingError||(t.playlistParsingError=new Error('Missing preceding EXT-X-DEFINE tag for Variable Reference: "'+i+'"')),e):n}))}return e}function Wt(t,e,r){var i,n,a=t.variableList;if(a||(t.variableList=a={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{var s=new self.URL(r).searchParams;if(!s.has(i))throw new Error('"'+i+'" does not match any query parameter in URI: "'+r+'"');n=s.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error("EXT-X-DEFINE QUERYPARAM: "+e.message))}}else i=e.NAME,n=e.VALUE;i in a?t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE duplicate Variable Name declarations: "'+i+'"')):a[i]=n||""}function jt(t,e,r){var i=e.IMPORT;if(r&&i in r){var n=t.variableList;n||(t.variableList=n={}),n[i]=r[i]}else t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "'+i+'"'))}function qt(){if("undefined"!=typeof self)return self.MediaSource||self.WebKitMediaSource}var Xt={audio:{a3ds:!0,"ac-3":!0,"ac-4":!0,alac:!0,alaw:!0,dra1:!0,"dts+":!0,"dts-":!0,dtsc:!0,dtse:!0,dtsh:!0,"ec-3":!0,enca:!0,g719:!0,g726:!0,m4ae:!0,mha1:!0,mha2:!0,mhm1:!0,mhm2:!0,mlpa:!0,mp4a:!0,"raw ":!0,Opus:!0,opus:!0,samr:!0,sawb:!0,sawp:!0,sevc:!0,sqcp:!0,ssmv:!0,twos:!0,ulaw:!0},video:{avc1:!0,avc2:!0,avc3:!0,avc4:!0,avcp:!0,av01:!0,drac:!0,dva1:!0,dvav:!0,dvh1:!0,dvhe:!0,encv:!0,hev1:!0,hvc1:!0,mjp2:!0,mp4v:!0,mvc1:!0,mvc2:!0,mvc3:!0,mvc4:!0,resv:!0,rv60:!0,s263:!0,svc1:!0,svc2:!0,"vc-1":!0,vp08:!0,vp09:!0},text:{stpp:!0,wvtt:!0}},zt=qt();function Qt(t,e){var r;return null!=(r=null==zt?void 0:zt.isTypeSupported((e||"video")+'/mp4;codecs="'+t+'"'))&&r}var $t=/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g,Jt=/#EXT-X-MEDIA:(.*)/g,Zt=/^#EXT(?:INF|-X-TARGETDURATION):/m,te=new RegExp([/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source,/(?!#) *(\S[\S ]*)/.source,/#EXT-X-BYTERANGE:*(.+)/.source,/#EXT-X-PROGRAM-DATE-TIME:(.+)/.source,/#.*/.source].join("|"),"g"),ee=new RegExp([/#(EXTM3U)/.source,/#EXT-X-(DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/.source,/#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/.source,/#EXT-X-(DISCONTINUITY|ENDLIST|GAP)/.source,/(#)([^:]*):(.*)/.source,/(#)(.*)(?:.*)\r?\n?/.source].join("|")),re=function(){function t(){}return t.findGroup=function(t,e){for(var r=0;r2){var r=e.shift()+".";return r+=parseInt(e.shift()).toString(16),r+=("000"+parseInt(e.shift()).toString(16)).slice(-4)}return t},t.resolve=function(t,e){return T.buildAbsoluteURL(e,t,{alwaysNormalize:!0})},t.isMediaPlaylist=function(t){return Zt.test(t)},t.parseMasterPlaylist=function(e,r){var i,n={contentSteering:null,levels:[],playlistParsingError:null,sessionData:null,sessionKeys:null,startTimeOffset:null,variableList:null,hasVariableRefs:Ht(e)},a=[];for($t.lastIndex=0;null!=(i=$t.exec(e));)if(i[1]){var s,o=new P(i[1]);Vt(n,o,["CODECS","SUPPLEMENTAL-CODECS","ALLOWED-CPC","PATHWAY-ID","STABLE-VARIANT-ID","AUDIO","VIDEO","SUBTITLES","CLOSED-CAPTIONS","NAME"]);var l=Yt(n,i[2]),u={attrs:o,bitrate:o.decimalInteger("AVERAGE-BANDWIDTH")||o.decimalInteger("BANDWIDTH"),name:o.NAME,url:t.resolve(l,r)},h=o.decimalResolution("RESOLUTION");h&&(u.width=h.width,u.height=h.height),ae((o.CODECS||"").split(/[ ,]+/).filter((function(t){return t})),u),u.videoCodec&&-1!==u.videoCodec.indexOf("avc1")&&(u.videoCodec=t.convertAVC1ToAVCOTI(u.videoCodec)),null!=(s=u.unknownCodecs)&&s.length||a.push(u),n.levels.push(u)}else if(i[3]){var d=i[3],c=i[4];switch(d){case"SESSION-DATA":var f=new P(c);Vt(n,f,["DATA-ID","LANGUAGE","VALUE","URI"]);var g=f["DATA-ID"];g&&(null===n.sessionData&&(n.sessionData={}),n.sessionData[g]=f);break;case"SESSION-KEY":var v=ie(c,r,n);v.encrypted&&v.isSupported()?(null===n.sessionKeys&&(n.sessionKeys=[]),n.sessionKeys.push(v)):w.warn('[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "'+c+'"');break;case"DEFINE":var m=new P(c);Vt(n,m,["NAME","VALUE","QUERYPARAM"]),Wt(n,m,r);break;case"CONTENT-STEERING":var p=new P(c);Vt(n,p,["SERVER-URI","PATHWAY-ID"]),n.contentSteering={uri:t.resolve(p["SERVER-URI"],r),pathwayId:p["PATHWAY-ID"]||"."};break;case"START":n.startTimeOffset=ne(c)}}var y=a.length>0&&a.length0&&W.bool("CAN-SKIP-DATERANGES"),h.partHoldBack=W.optionalFloat("PART-HOLD-BACK",0),h.holdBack=W.optionalFloat("HOLD-BACK",0);break;case"PART-INF":var j=new P(D);h.partTarget=j.decimalFloatingPoint("PART-TARGET");break;case"PART":var q=h.partList;q||(q=h.partList=[]);var X=g>0?q[q.length-1]:void 0,z=g++,Q=new P(D);Vt(h,Q,["BYTERANGE","URI"]);var $=new K(Q,y,e,z,X);q.push($),y.duration+=$.duration;break;case"PRELOAD-HINT":var J=new P(D);Vt(h,J,["URI"]),h.preloadHint=J;break;case"RENDITION-REPORT":var Z=new P(D);Vt(h,Z,["URI"]),h.renditionReports=h.renditionReports||[],h.renditionReports.push(Z);break;default:w.warn("line parsed but not handled: "+s)}}}p&&!p.relurl?(d.pop(),v-=p.duration,h.partList&&(h.fragmentHint=p)):h.partList&&(oe(y,p),y.cc=m,h.fragmentHint=y,u&&ue(y,u,h));var tt=d.length,et=d[0],rt=d[tt-1];if((v+=h.skippedSegments*h.targetduration)>0&&tt&&rt){h.averagetargetduration=v/tt;var it=rt.sn;h.endSN="initSegment"!==it?it:0,h.live||(rt.endList=!0),et&&(h.startCC=et.cc)}else h.endSN=0,h.startCC=0;return h.fragmentHint&&(v+=h.fragmentHint.duration),h.totalduration=v,h.endCC=m,T>0&&function(t,e){for(var r=t[e],i=e;i--;){var n=t[i];if(!n)return;n.programDateTime=r.programDateTime-1e3*n.duration,r=n}}(d,T),h},t}();function ie(t,e,r){var i,n,a=new P(t);Vt(r,a,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);var s=null!=(i=a.METHOD)?i:"",o=a.URI,l=a.hexadecimalInteger("IV"),u=a.KEYFORMATVERSIONS,h=null!=(n=a.KEYFORMAT)?n:"identity";o&&a.IV&&!l&&w.error("Invalid IV: "+a.IV);var d=o?re.resolve(o,e):"",c=(u||"1").split("/").map(Number).filter(Number.isFinite);return new Gt(s,d,h,c,l)}function ne(t){var e=new P(t).decimalFloatingPoint("TIME-OFFSET");return E(e)?e:null}function ae(t,e){["video","audio","text"].forEach((function(r){var i=t.filter((function(t){return function(t,e){var r=Xt[e];return!!r&&!0===r[t.slice(0,4)]}(t,r)}));if(i.length){var n=i.filter((function(t){return 0===t.lastIndexOf("avc1",0)||0===t.lastIndexOf("mp4a",0)}));e[r+"Codec"]=n.length>0?n[0]:i[0],t=t.filter((function(t){return-1===i.indexOf(t)}))}})),e.unknownCodecs=t}function se(t,e,r){var i=e[r];i&&(t[r]=i)}function oe(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),E(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function le(t,e,r,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=r,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function ue(t,e,r){t.levelkeys=e;var i=r.encryptedFragments;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((function(t){return e[t].isCommonEncryption}))||i.push(t)}var he="manifest",de="level",ce="audioTrack",fe="subtitleTrack",ge="main",ve="audio",me="subtitle";function pe(t){switch(t.type){case ce:return ve;case fe:return me;default:return ge}}function ye(t,e){var r=t.url;return void 0!==r&&0!==r.indexOf("data:")||(r=e.url),r}var Te=function(){function t(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}var e=t.prototype;return e.startLoad=function(t){},e.stopLoad=function(){this.destroyInternalLoaders()},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_LOADING,this.onLevelLoading,this),t.on(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_LOADING,this.onLevelLoading,this),t.off(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.createInternalLoader=function(t){var e=this.hls.config,r=e.pLoader,i=e.loader,n=new(r||i)(e);return this.loaders[t.type]=n,n},e.getInternalLoader=function(t){return this.loaders[t.type]},e.resetInternalLoader=function(t){this.loaders[t]&&delete this.loaders[t]},e.destroyInternalLoaders=function(){for(var t in this.loaders){var e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}},e.destroy=function(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()},e.onManifestLoading=function(t,e){var r=e.url;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:he,url:r,deliveryDirectives:null})},e.onLevelLoading=function(t,e){var r=e.id,i=e.level,n=e.url,a=e.deliveryDirectives;this.load({id:r,level:i,responseType:"text",type:de,url:n,deliveryDirectives:a})},e.onAudioTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:ce,url:n,deliveryDirectives:a})},e.onSubtitleTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:fe,url:n,deliveryDirectives:a})},e.load=function(t){var e,r,i,n=this,a=this.hls.config,s=this.getInternalLoader(t);if(s){var l=s.context;if(l&&l.url===t.url)return void w.trace("[playlist-loader]: playlist request ongoing");w.log("[playlist-loader]: aborting previous loader for type: "+t.type),s.abort()}if(r=t.type===he?a.manifestLoadPolicy.default:o({},a.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),s=this.createInternalLoader(t),null!=(e=t.deliveryDirectives)&&e.part&&(t.type===de&&null!==t.level?i=this.hls.levels[t.level].details:t.type===ce&&null!==t.id?i=this.hls.audioTracks[t.id].details:t.type===fe&&null!==t.id&&(i=this.hls.subtitleTracks[t.id].details),i)){var u=i.partTarget,h=i.targetduration;if(u&&h){var d=1e3*Math.max(3*u,.8*h);r=o({},r,{maxTimeToFirstByteMs:Math.min(d,r.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(d,r.maxTimeToFirstByteMs)})}}var c=r.errorRetry||r.timeoutRetry||{},f={loadPolicy:r,timeout:r.maxLoadTimeMs,maxRetry:c.maxNumRetry||0,retryDelay:c.retryDelayMs||0,maxRetryDelay:c.maxRetryDelayMs||0},g={onSuccess:function(t,e,r,i){var a=n.getInternalLoader(r);n.resetInternalLoader(r.type);var s=t.data;0===s.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),re.isMediaPlaylist(s)?n.handleTrackOrLevelPlaylist(t,e,r,i||null,a):n.handleMasterPlaylist(t,e,r,i)):n.handleManifestParsingError(t,r,new Error("no EXTM3U delimiter"),i||null,e)},onError:function(t,e,r,i){n.handleNetworkError(e,r,!1,t,i)},onTimeout:function(t,e,r){n.handleNetworkError(e,r,!0,void 0,t)}};s.load(t,f,g)},e.handleMasterPlaylist=function(t,e,r,i){var n=this.hls,a=t.data,s=ye(t,r),o=re.parseMasterPlaylist(a,s);if(o.playlistParsingError)this.handleManifestParsingError(t,r,o.playlistParsingError,i,e);else{var l=o.contentSteering,u=o.levels,h=o.sessionData,d=o.sessionKeys,c=o.startTimeOffset,f=o.variableList;this.variableList=f;var g=re.parseMasterPlaylistMedia(a,s,o),v=g.AUDIO,m=void 0===v?[]:v,p=g.SUBTITLES,y=g["CLOSED-CAPTIONS"];m.length&&(m.some((function(t){return!t.url}))||!u[0].audioCodec||u[0].attrs.AUDIO||(w.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),m.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new P({}),bitrate:0,url:""}))),n.trigger(S.MANIFEST_LOADED,{levels:u,audioTracks:m,subtitles:p,captions:y,contentSteering:l,url:s,stats:e,networkDetails:i,sessionData:h,sessionKeys:d,startTimeOffset:c,variableList:f})}},e.handleTrackOrLevelPlaylist=function(t,e,r,i,n){var a=this.hls,s=r.id,o=r.level,l=r.type,u=ye(t,r),h=E(s)?s:0,d=E(o)?o:h,c=pe(r),f=re.parseLevelPlaylist(t.data,u,d,c,h,this.variableList);if(l===he){var g={attrs:new P({}),bitrate:0,details:f,name:"",url:u};a.trigger(S.MANIFEST_LOADED,{levels:[g],audioTracks:[],url:u,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),r.levelDetails=f,this.handlePlaylistLoaded(f,t,e,r,i,n)},e.handleManifestParsingError=function(t,e,r,i,n){this.hls.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:R.MANIFEST_PARSING_ERROR,fatal:e.type===he,url:t.url,err:r,error:r,reason:r.message,response:t,context:e,networkDetails:i,stats:n})},e.handleNetworkError=function(t,e,r,n,a){void 0===r&&(r=!1);var s="A network "+(r?"timeout":"error"+(n?" (status "+n.code+")":""))+" occurred while loading "+t.type;t.type===de?s+=": "+t.level+" id: "+t.id:t.type!==ce&&t.type!==fe||(s+=" id: "+t.id+' group-id: "'+t.groupId+'"');var o=new Error(s);w.warn("[playlist-loader]: "+s);var l=R.UNKNOWN,u=!1,h=this.getInternalLoader(t);switch(t.type){case he:l=r?R.MANIFEST_LOAD_TIMEOUT:R.MANIFEST_LOAD_ERROR,u=!0;break;case de:l=r?R.LEVEL_LOAD_TIMEOUT:R.LEVEL_LOAD_ERROR,u=!1;break;case ce:l=r?R.AUDIO_TRACK_LOAD_TIMEOUT:R.AUDIO_TRACK_LOAD_ERROR,u=!1;break;case fe:l=r?R.SUBTITLE_TRACK_LOAD_TIMEOUT:R.SUBTITLE_LOAD_ERROR,u=!1}h&&this.resetInternalLoader(t.type);var d={type:L.NETWORK_ERROR,details:l,fatal:u,url:t.url,loader:h,context:t,error:o,networkDetails:e,stats:a};if(n){var c=(null==e?void 0:e.url)||t.url;d.response=i({url:c,data:void 0},n)}this.hls.trigger(S.ERROR,d)},e.handlePlaylistLoaded=function(t,e,r,i,n,a){var s=this.hls,o=i.type,l=i.level,u=i.id,h=i.groupId,d=i.deliveryDirectives,c=ye(e,i),f=pe(i),g="number"==typeof i.level&&f===ge?l:void 0;if(t.fragments.length){t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));var v=t.playlistParsingError;if(v)s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:R.LEVEL_PARSING_ERROR,fatal:!1,url:c,error:v,reason:v.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r});else switch(t.live&&a&&(a.getCacheAge&&(t.ageHeader=a.getCacheAge()||0),a.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case he:case de:s.trigger(S.LEVEL_LOADED,{details:t,level:g||0,id:u||0,stats:r,networkDetails:n,deliveryDirectives:d});break;case ce:s.trigger(S.AUDIO_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d});break;case fe:s.trigger(S.SUBTITLE_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d})}}else{var m=new Error("No Segments found in Playlist");s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:R.LEVEL_EMPTY_ERROR,fatal:!1,url:c,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r})}},t}();function Ee(t,e){var r;try{r=new Event("addtrack")}catch(t){(r=document.createEvent("Event")).initEvent("addtrack",!1,!1)}r.track=t,e.dispatchEvent(r)}function Se(t,e){var r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error("addCue is failed for: "+e)}catch(r){w.debug("[texttrack-utils]: "+r);try{var i=new self.TextTrackCue(e.startTime,e.endTime,e.text);i.id=e.id,t.addCue(i)}catch(t){w.debug("[texttrack-utils]: Legacy TextTrackCue fallback failed: "+t)}}"disabled"===r&&(t.mode=r)}function Le(t){var e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(var r=t.cues.length;r--;)t.removeCue(t.cues[r]);"disabled"===e&&(t.mode=e)}function Re(t,e,r,i){var n=t.mode;if("disabled"===n&&(t.mode="hidden"),t.cues&&t.cues.length>0)for(var a=function(t,e,r){var i=[],n=function(t,e){if(et[r].endTime)return-1;for(var i=0,n=r;i<=n;){var a=Math.floor((n+i)/2);if(et[a].startTime&&i-1)for(var a=n,s=t.length;a=e&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(t.cues,e,r),s=0;sIe&&(d=Ie),d-h<=0&&(d=h+.25);for(var c=0;ce.startDate&&t.push(i),t}),[]).sort((function(t,e){return t.startDate.getTime()-e.startDate.getTime()}))[0];g&&(h=we(g.startDate,c),l=!0)}for(var m,p,y=Object.keys(e.attr),T=0;T.05&&this.forwardBufferLength>1){var u=Math.min(2,Math.max(1,a)),h=Math.round(2/(1+Math.exp(-.75*o-this.edgeStalled))*20)/20;t.playbackRate=Math.min(u,Math.max(1,h))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}}}}},e.estimateLiveEdge=function(){var t=this.levelDetails;return null===t?null:t.edge+t.age},e.computeLatency=function(){var t=this.estimateLiveEdge();return null===t?null:t-this.currentTime},a(t,[{key:"latency",get:function(){return this._latency||0}},{key:"maxLatency",get:function(){var t=this.config,e=this.levelDetails;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}},{key:"targetLatency",get:function(){var t=this.levelDetails;if(null===t)return null;var e=t.holdBack,r=t.partHoldBack,i=t.targetduration,n=this.config,a=n.liveSyncDuration,s=n.liveSyncDurationCount,o=n.lowLatencyMode,l=this.hls.userConfig,u=o&&r||e;(l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==a?a:s*i);var h=i;return u+Math.min(1*this.stallCount,h)}},{key:"liveSyncPosition",get:function(){var t=this.estimateLiveEdge(),e=this.targetLatency,r=this.levelDetails;if(null===t||null===e||null===r)return null;var i=r.edge,n=t-e-this.edgeStalled,a=i-r.totalduration,s=i-(this.config.lowLatencyMode&&r.partTarget||r.targetduration);return Math.min(Math.max(a,n),s)}},{key:"drift",get:function(){var t=this.levelDetails;return null===t?1:t.drift}},{key:"edgeStalled",get:function(){var t=this.levelDetails;if(null===t)return 0;var e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}},{key:"forwardBufferLength",get:function(){var t=this.media,e=this.levelDetails;if(!t||!e)return 0;var r=t.buffered.length;return(r?t.buffered.end(r-1):e.edge)-this.currentTime}}]),t}(),Pe=["NONE","TYPE-0","TYPE-1",null],xe="",Fe="YES",Me="v2",Oe=function(){function t(t,e,r){this.msn=void 0,this.part=void 0,this.skip=void 0,this.msn=t,this.part=e,this.skip=r}return t.prototype.addDirectives=function(t){var e=new self.URL(t);return void 0!==this.msn&&e.searchParams.set("_HLS_msn",this.msn.toString()),void 0!==this.part&&e.searchParams.set("_HLS_part",this.part.toString()),this.skip&&e.searchParams.set("_HLS_skip",this.skip),e.href},t}(),Ne=function(){function t(t){this._attrs=void 0,this.audioCodec=void 0,this.bitrate=void 0,this.codecSet=void 0,this.height=void 0,this.id=void 0,this.name=void 0,this.videoCodec=void 0,this.width=void 0,this.unknownCodecs=void 0,this.audioGroupIds=void 0,this.details=void 0,this.fragmentError=0,this.loadError=0,this.loaded=void 0,this.realBitrate=0,this.textGroupIds=void 0,this.url=void 0,this._urlId=0,this.url=[t.url],this._attrs=[t.attrs],this.bitrate=t.bitrate,t.details&&(this.details=t.details),this.id=t.id||0,this.name=t.name,this.width=t.width||0,this.height=t.height||0,this.audioCodec=t.audioCodec,this.videoCodec=t.videoCodec,this.unknownCodecs=t.unknownCodecs,this.codecSet=[t.videoCodec,t.audioCodec].filter((function(t){return t})).join(",").replace(/\.[^.,]+/g,"")}return t.prototype.addFallback=function(t){this.url.push(t.url),this._attrs.push(t.attrs)},a(t,[{key:"maxBitrate",get:function(){return Math.max(this.realBitrate,this.bitrate)}},{key:"attrs",get:function(){return this._attrs[this._urlId]}},{key:"pathwayId",get:function(){return this.attrs["PATHWAY-ID"]||"."}},{key:"uri",get:function(){return this.url[this._urlId]||""}},{key:"urlId",get:function(){return this._urlId},set:function(t){var e=t%this.url.length;this._urlId!==e&&(this.fragmentError=0,this.loadError=0,this.details=void 0,this._urlId=e)}},{key:"audioGroupId",get:function(){var t;return null==(t=this.audioGroupIds)?void 0:t[this.urlId]}},{key:"textGroupId",get:function(){var t;return null==(t=this.textGroupIds)?void 0:t[this.urlId]}}]),t}();function Ue(t,e){var r=e.startPTS;if(E(r)){var i,n=0;e.sn>t.sn?(n=r-t.start,i=t):(n=t.start-r,i=e),i.duration!==n&&(i.duration=n)}else e.sn>t.sn?t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration:e.start=Math.max(t.start-e.duration,0)}function Be(t,e,r,i,n,a){i-r<=0&&(w.warn("Fragment should have a positive duration",e),i=r+e.duration,a=n+e.duration);var s=r,o=i,l=e.startPTS,u=e.endPTS;if(E(l)){var h=Math.abs(l-r);E(e.deltaPTS)?e.deltaPTS=Math.max(h,e.deltaPTS):e.deltaPTS=h,s=Math.max(r,l),r=Math.min(r,l),n=Math.min(n,e.startDTS),o=Math.min(i,u),i=Math.max(i,u),a=Math.max(a,e.endDTS)}var d=r-e.start;0!==e.start&&(e.start=r),e.duration=i-e.start,e.startPTS=r,e.maxStartPTS=s,e.startDTS=n,e.endPTS=i,e.minEndPTS=o,e.endDTS=a;var c,f=e.sn;if(!t||ft.endSN)return 0;var g=f-t.startSN,v=t.fragments;for(v[g]=e,c=g;c>0;c--)Ue(v[c],v[c-1]);for(c=g;c=0;n--){var a=i[n].initSegment;if(a){r=a;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;var s,l,u,h,d,c=0;if(function(t,e,r){for(var i=e.skippedSegments,n=Math.max(t.startSN,e.startSN)-e.startSN,a=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,s=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments,u=n;u<=a;u++){var h=l[s+u],d=o[u];i&&!d&&u=i.length||He(e,i[r].start)}function He(t,e){if(e){for(var r=t.fragments,i=t.skippedSegments;i499)}(i)||!!r)}var Qe=function(t,e){for(var r=0,i=t.length-1,n=null,a=null;r<=i;){var s=e(a=t[n=(r+i)/2|0]);if(s>0)r=n+1;else{if(!(s<0))return a;i=n-1}}return null};function $e(t,e,r,i){void 0===r&&(r=0),void 0===i&&(i=0);var n=null;if(t?n=e[t.sn-e[0].sn+1]||null:0===r&&0===e[0].start&&(n=e[0]),n&&0===Je(r,i,n))return n;var a=Qe(e,Je.bind(null,r,i));return!a||a===t&&n?n:a}function Je(t,e,r){if(void 0===t&&(t=0),void 0===e&&(e=0),r.start<=t&&r.start+r.duration>t)return 0;var i=Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return r.start+r.duration-i<=t?1:r.start-i>t&&r.start?-1:0}function Ze(t,e,r){var i=1e3*Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>t}var tr,er=3e5,rr=0,ir=2,nr=5,ar=0,sr=1,or=2,lr=function(){function t(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=w.log.bind(w,"[info]:"),this.warn=w.warn.bind(w,"[warning]:"),this.error=w.error.bind(w,"[error]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.ERROR,this.onError,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.ERROR,this.onError,this),t.off(S.ERROR,this.onErrorOut,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this))},e.destroy=function(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}},e.startLoad=function(t){this.playlistError=0},e.stopLoad=function(){},e.getVariantLevelIndex=function(t){return(null==t?void 0:t.type)===ge?t.level:this.hls.loadLevel},e.onManifestLoading=function(){this.playlistError=0,this.penalizedRenditions={}},e.onLevelUpdated=function(){this.playlistError=0},e.onError=function(t,e){var r,i;if(!e.fatal){var n=this.hls,a=e.context;switch(e.details){case R.FRAG_LOAD_ERROR:case R.FRAG_LOAD_TIMEOUT:case R.KEY_LOAD_ERROR:case R.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case R.FRAG_PARSING_ERROR:if(null!=(r=e.frag)&&r.gap)return void(e.errorAction={action:rr,flags:ar});case R.FRAG_GAP:case R.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=ir);case R.LEVEL_EMPTY_ERROR:case R.LEVEL_PARSING_ERROR:var s,o,l=e.parent===ge?e.level:n.loadLevel;return void(e.details===R.LEVEL_EMPTY_ERROR&&null!=(s=e.context)&&null!=(o=s.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,l):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,l)));case R.LEVEL_LOAD_ERROR:case R.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==a?void 0:a.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,a.level)));case R.AUDIO_TRACK_LOAD_ERROR:case R.AUDIO_TRACK_LOAD_TIMEOUT:case R.SUBTITLE_LOAD_ERROR:case R.SUBTITLE_TRACK_LOAD_TIMEOUT:if(a){var u=n.levels[n.loadLevel];if(u&&(a.type===ce&&a.groupId===u.audioGroupId||a.type===fe&&a.groupId===u.textGroupId))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.loadLevel),e.errorAction.action=ir,void(e.errorAction.flags=sr)}return;case R.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:var h=n.levels[n.loadLevel],d=null==h?void 0:h.attrs["HDCP-LEVEL"];return void(d&&(e.errorAction={action:ir,flags:or,hdcpLevel:d}));case R.BUFFER_ADD_CODEC_ERROR:case R.REMUX_ALLOC_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:n.loadLevel));case R.INTERNAL_EXCEPTION:case R.BUFFER_APPENDING_ERROR:case R.BUFFER_APPEND_ERROR:case R.BUFFER_FULL_ERROR:case R.LEVEL_SWITCH_ERROR:case R.BUFFER_STALLED_ERROR:case R.BUFFER_SEEK_OVER_HOLE:case R.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:rr,flags:ar})}if(e.type===L.KEY_SYSTEM_ERROR){var c=this.getVariantLevelIndex(e.frag);return e.levelRetry=!1,void(e.errorAction=this.getLevelSwitchAction(e,c))}}},e.getPlaylistRetryOrSwitchAction=function(t,e){var r,i=je(this.hls.config.playlistLoadPolicy,t),n=this.playlistError++,a=null==(r=t.response)?void 0:r.code;if(ze(i,n,We(t),a))return{action:nr,flags:ar,retryConfig:i,retryCount:n};var s=this.getLevelSwitchAction(t,e);return i&&(s.retryConfig=i,s.retryCount=n),s},e.getFragRetryOrSwitchAction=function(t){var e=this.hls,r=this.getVariantLevelIndex(t.frag),i=e.levels[r],n=e.config,a=n.fragLoadPolicy,s=n.keyLoadPolicy,o=je(t.details.startsWith("key")?s:a,t),l=e.levels.reduce((function(t,e){return t+e.fragmentError}),0);if(i){var u;t.details!==R.FRAG_GAP&&i.fragmentError++;var h=null==(u=t.response)?void 0:u.code;if(ze(o,l,We(t),h))return{action:nr,flags:ar,retryConfig:o,retryCount:l}}var d=this.getLevelSwitchAction(t,r);return o&&(d.retryConfig=o,d.retryCount=l),d},e.getLevelSwitchAction=function(t,e){var r=this.hls;null==e&&(e=r.loadLevel);var i=this.hls.levels[e];if(i&&(i.loadError++,r.autoLevelEnabled)){for(var n,a,s=-1,o=r.levels,l=r.loadLevel,u=r.minAutoLevel,h=r.maxAutoLevel,d=null==(n=t.frag)?void 0:n.type,c=null!=(a=t.context)?a:{},f=c.type,g=c.groupId,v=o.length;v--;){var m=(v+l)%o.length;if(m!==l&&m>=u&&m<=h&&0===o[m].loadError){var p=o[m];if(t.details===R.FRAG_GAP&&t.frag){var y=o[m].details;if(y){var T=$e(t.frag,y.fragments,t.frag.start);if(null!=T&&T.gap)continue}}else{if(f===ce&&g===p.audioGroupId||f===fe&&g===p.textGroupId)continue;if(d===ve&&i.audioGroupId===p.audioGroupId||d===me&&i.textGroupId===p.textGroupId)continue}s=m;break}}if(s>-1&&r.loadLevel!==s)return t.levelRetry=!0,this.playlistError=0,{action:ir,flags:ar,nextAutoLevel:s}}return{action:ir,flags:sr}},e.onErrorOut=function(t,e){var r;switch(null==(r=e.errorAction)?void 0:r.action){case rr:break;case ir:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===R.FRAG_GAP||(e.fatal=!0)}e.fatal&&this.hls.stopLoad()},e.sendAlternateToPenaltyBox=function(t){var e=this.hls,r=t.errorAction;if(r){var i=r.flags,n=r.hdcpLevel,a=r.nextAutoLevel;switch(i){case ar:this.switchLevel(t,a);break;case sr:r.resolved||(r.resolved=this.redundantFailover(t));break;case or:n&&(e.maxHdcpLevel=Pe[Pe.indexOf(n)-1],r.resolved=!0),this.warn('Restricting playback to HDCP-LEVEL of "'+e.maxHdcpLevel+'" or lower')}r.resolved||this.switchLevel(t,a)}},e.switchLevel=function(t,e){void 0!==e&&t.errorAction&&(this.warn("switching to level "+e+" after "+t.details),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)},e.redundantFailover=function(t){var e=this,r=this.hls,i=this.penalizedRenditions,n=t.parent===ge?t.level:r.loadLevel,a=r.levels[n],s=a.url.length,o=t.frag?t.frag.urlId:a.urlId;a.urlId!==o||t.frag&&!a.details||this.penalizeRendition(a,t);for(var l=function(){var l=(o+u)%s,h=i[l];if(!h||function(t,e,r){if(performance.now()-t.lastErrorPerfMs>er)return!0;var i=t.details;if(e.details===R.FRAG_GAP&&i&&e.frag){var n=e.frag.start,a=$e(null,i.fragments,n);if(a&&!a.gap)return!0}if(r&&t.errors.length3*i.targetduration)return!0}return!1}(h,t,i[o]))return e.warn("Switching to Redundant Stream "+(l+1)+"/"+s+': "'+a.url[l]+'" after '+t.details),e.playlistError=0,r.levels.forEach((function(t){t.urlId=l})),r.nextLoadLevel=n,{v:!0}},u=1;u=0&&h>e.partTarget&&(u+=1)}return new Oe(l,u>=0?u:void 0,xe)}}},e.loadPlaylist=function(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())},e.shouldLoadPlaylist=function(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)},e.shouldReloadPlaylist=function(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)},e.playlistLoaded=function(t,e,r){var i=this,n=e.details,a=e.stats,s=self.performance.now(),o=a.loading.first?Math.max(0,s-a.loading.first):0;if(n.advancedDateTime=Date.now()-o,n.live||null!=r&&r.live){if(n.reloaded(r),r&&this.log("live playlist "+t+" "+(n.advanced?"REFRESHED "+n.lastPartSn+"-"+n.lastPartIndex:n.updated?"UPDATED":"MISSED")),r&&n.fragments.length>0&&Ge(r,n),!this.canLoad||!n.live)return;var l,u=void 0,h=void 0;if(n.canBlockReload&&n.endSN&&n.advanced){var d=this.hls.config.lowLatencyMode,c=n.lastPartSn,f=n.endSN,g=n.lastPartIndex,v=c===f;-1!==g?(u=v?f+1:c,h=v?d?0:g:g+1):u=f+1;var m=n.age,p=m+n.ageHeader,y=Math.min(p-n.partTarget,1.5*n.targetduration);if(y>0){if(r&&y>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+y+" with playlist age: "+n.age),y=0;else{var T=Math.floor(y/n.targetduration);u+=T,void 0!==h&&(h+=Math.round(y%n.targetduration/n.partTarget)),this.log("CDN Tune-in age: "+n.ageHeader+"s last advanced "+m.toFixed(2)+"s goal: "+y+" skip sn "+T+" to part "+h)}n.tuneInGoal=y}if(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h),d||!v)return void this.loadPlaylist(l)}else(n.canBlockReload||n.canSkipUntil)&&(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h));var E=this.hls.mainForwardBufferInfo,S=E?E.end-E.len:0,L=function(t,e){void 0===e&&(e=1/0);var r=1e3*t.targetduration;if(t.updated){var i=t.fragments;if(i.length&&4*r>e){var n=1e3*i[i.length-1].duration;nthis.requestScheduled+L&&(this.requestScheduled=a.loading.start),void 0!==u&&n.canBlockReload?this.requestScheduled=a.loading.first+L-(1e3*n.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+L=u.maxNumRetry)return!1;if(i&&null!=(d=t.context)&&d.deliveryDirectives)this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" without delivery-directives'),this.loadPlaylist();else{var c=qe(u,l);this.timer=self.setTimeout((function(){return e.loadPlaylist()}),c),this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" in '+c+"ms")}t.levelRetry=!0,n.resolved=!0}return h},t}(),hr=function(t){function e(e,r){var i;return(i=t.call(this,e,"[level-controller]")||this)._levels=[],i._firstLevel=-1,i._startLevel=void 0,i.currentLevel=null,i.currentLevelIndex=-1,i.manualLevelIndex=-1,i.steering=void 0,i.onParsedComplete=void 0,i.steering=r,i._registerListeners(),i}l(e,t);var r=e.prototype;return r._registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.AUDIO_TRACK_SWITCHED,this.onAudioTrackSwitched,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.ERROR,this.onError,this)},r._unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.AUDIO_TRACK_SWITCHED,this.onAudioTrackSwitched,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.ERROR,this.onError,this)},r.destroy=function(){this._unregisterListeners(),this.steering=null,this.resetLevels(),t.prototype.destroy.call(this)},r.startLoad=function(){this._levels.forEach((function(t){t.loadError=0,t.fragmentError=0})),t.prototype.startLoad.call(this)},r.resetLevels=function(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[]},r.onManifestLoading=function(t,e){this.resetLevels()},r.onManifestLoaded=function(t,e){var r,i=[],n={};e.levels.forEach((function(t){var e,a=t.attrs;-1!==(null==(e=t.audioCodec)?void 0:e.indexOf("mp4a.40.34"))&&(tr||(tr=/chrome|firefox/i.test(navigator.userAgent)),tr&&(t.audioCodec=void 0));var s=a.AUDIO,o=a.CODECS,l=a["FRAME-RATE"],u=a["PATHWAY-ID"],h=a.RESOLUTION,d=a.SUBTITLES,c=(u||".")+"-"+t.bitrate+"-"+h+"-"+l+"-"+o;(r=n[c])?r.addFallback(t):(r=new Ne(t),n[c]=r,i.push(r)),dr(r,"audio",s),dr(r,"text",d)})),this.filterAndSortMediaOptions(i,e)},r.filterAndSortMediaOptions=function(t,e){var r=this,i=[],n=[],a=!1,s=!1,o=!1,l=t.filter((function(t){var e=t.audioCodec,r=t.videoCodec,i=t.width,n=t.height,l=t.unknownCodecs;return a||(a=!(!i||!n)),s||(s=!!r),o||(o=!!e),!(null!=l&&l.length)&&(!e||Qt(e,"audio"))&&(!r||Qt(r,"video"))}));if((a||s)&&o&&(l=l.filter((function(t){var e=t.videoCodec,r=t.width,i=t.height;return!!e||!(!r||!i)}))),0!==l.length){e.audioTracks&&cr(i=e.audioTracks.filter((function(t){return!t.audioCodec||Qt(t.audioCodec,"audio")}))),e.subtitles&&cr(n=e.subtitles);var u=l.slice(0);l.sort((function(t,e){return t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"]?(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1:t.bitrate!==e.bitrate?t.bitrate-e.bitrate:t.attrs["FRAME-RATE"]!==e.attrs["FRAME-RATE"]?t.attrs.decimalFloatingPoint("FRAME-RATE")-e.attrs.decimalFloatingPoint("FRAME-RATE"):t.attrs.SCORE!==e.attrs.SCORE?t.attrs.decimalFloatingPoint("SCORE")-e.attrs.decimalFloatingPoint("SCORE"):a&&t.height!==e.height?t.height-e.height:0}));var h=u[0];if(this.steering&&(l=this.steering.filterParsedLevels(l)).length!==u.length)for(var d=0;d1&&void 0!==e?(n.url=n.url.filter(i),n.audioGroupIds&&(n.audioGroupIds=n.audioGroupIds.filter(i)),n.textGroupIds&&(n.textGroupIds=n.textGroupIds.filter(i)),n.urlId=0,!0):(r.steering&&r.steering.removeLevel(n),!1))}));this.hls.trigger(S.LEVELS_UPDATED,{levels:n})},r.onLevelsUpdated=function(t,e){var r=e.levels;r.forEach((function(t,e){var r=t.details;null!=r&&r.fragments&&r.fragments.forEach((function(t){t.level=e}))})),this._levels=r},a(e,[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"level",get:function(){return this.currentLevelIndex},set:function(t){var e=this._levels;if(0!==e.length){if(t<0||t>=e.length){var r=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:R.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:r,reason:r.message}),i)return;t=Math.min(t,e.length-1)}var n=this.currentLevelIndex,a=this.currentLevel,s=a?a.attrs["PATHWAY-ID"]:void 0,l=e[t],u=l.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=l,n!==t||!l.details||!a||s!==u){this.log("Switching to level "+t+(u?" with Pathway "+u:"")+" from level "+n+(s?" with Pathway "+s:""));var h=o({},l,{level:t,maxBitrate:l.maxBitrate,attrs:l.attrs,uri:l.uri,urlId:l.urlId});delete h._attrs,delete h._urlId,this.hls.trigger(S.LEVEL_SWITCHING,h);var d=l.details;if(!d||d.live){var c=this.switchParams(l.uri,null==a?void 0:a.details);this.loadPlaylist(c)}}}}},{key:"manualLevel",get:function(){return this.manualLevelIndex},set:function(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}},{key:"firstLevel",get:function(){return this._firstLevel},set:function(t){this._firstLevel=t}},{key:"startLevel",get:function(){if(void 0===this._startLevel){var t=this.hls.config.startLevel;return void 0!==t?t:this._firstLevel}return this._startLevel},set:function(t){this._startLevel=t}},{key:"nextLoadLevel",get:function(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel},set:function(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}}]),e}(ur);function dr(t,e,r){r&&("audio"===e?(t.audioGroupIds||(t.audioGroupIds=[]),t.audioGroupIds[t.url.length-1]=r):"text"===e&&(t.textGroupIds||(t.textGroupIds=[]),t.textGroupIds[t.url.length-1]=r))}function cr(t){var e={};t.forEach((function(t){var r=t.groupId||"";t.id=e[r]=e[r]||0,e[r]++}))}var fr="NOT_LOADED",gr="APPENDING",vr="PARTIAL",mr="OK",pr=function(){function t(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){var t=this.hls;t.on(S.BUFFER_APPENDED,this.onBufferAppended,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this)},e._unregisterListeners=function(){var t=this.hls;t.off(S.BUFFER_APPENDED,this.onBufferAppended,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this)},e.destroy=function(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null},e.getAppendedFrag=function(t,e){var r=this.activePartLists[e];if(r)for(var i=r.length;i--;){var n=r[i];if(!n)break;var a=n.end;if(n.start<=t&&null!==a&&t<=a)return n}return this.getBufferedFrag(t,e)},e.getBufferedFrag=function(t,e){for(var r=this.fragments,i=Object.keys(r),n=i.length;n--;){var a=r[i[n]];if((null==a?void 0:a.body.type)===e&&a.buffered){var s=a.body;if(s.start<=t&&t<=s.end)return s}}return null},e.detectEvictedFragments=function(t,e,r,i){var n=this;this.timeRanges&&(this.timeRanges[t]=e);var a=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((function(i){var s=n.fragments[i];if(s&&!(a>=s.body.sn))if(s.buffered||s.loaded){var o=s.range[t];o&&o.time.some((function(t){var r=!n.isTimeBuffered(t.startPTS,t.endPTS,e);return r&&n.removeFragment(s.body),r}))}else s.body.type===r&&n.removeFragment(s.body)}))},e.detectPartialFragments=function(t){var e=this,r=this.timeRanges,i=t.frag,n=t.part;if(r&&"initSegment"!==i.sn){var a=Tr(i),s=this.fragments[a];if(!(!s||s.buffered&&i.gap)){var o=!i.relurl;Object.keys(r).forEach((function(t){var a=i.elementaryStreams[t];if(a){var l=r[t],u=o||!0===a.partial;s.range[t]=e.getBufferedTimes(i,n,u,l)}})),s.loaded=null,Object.keys(s.range).length?(s.buffered=!0,(s.body.endList=i.endList||s.body.endList)&&(this.endListFragments[s.body.type]=s),yr(s)||this.removeParts(i.sn-1,i.type)):this.removeFragment(s.body)}}},e.removeParts=function(t,e){var r=this.activePartLists[e];r&&(this.activePartLists[e]=r.filter((function(e){return e.fragment.sn>=t})))},e.fragBuffered=function(t,e){var r=Tr(t),i=this.fragments[r];!i&&e&&(i=this.fragments[r]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)},e.getBufferedTimes=function(t,e,r,i){for(var n={time:[],partial:r},a=t.start,s=t.end,o=t.minEndPTS||s,l=t.maxStartPTS||a,u=0;u=h&&o<=d){n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(ah)n.partial=!0,n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});else if(s<=h)break}return n},e.getPartialFragment=function(t){var e,r,i,n=null,a=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&yr(u)&&(r=u.body.start-s,i=u.body.end+s,t>=r&&t<=i&&(e=Math.min(t-r,i-t),a<=e&&(n=u.body,a=e)))})),n},e.isEndListAppended=function(t){var e=this.endListFragments[t];return void 0!==e&&(e.buffered||yr(e))},e.getState=function(t){var e=Tr(t),r=this.fragments[e];return r?r.buffered?yr(r)?vr:mr:gr:fr},e.isTimeBuffered=function(t,e,r){for(var i,n,a=0;a=i&&e<=n)return!0;if(e<=i)return!1}return!1},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part;if("initSegment"!==r.sn&&!r.bitrateTest){var n=i?null:e,a=Tr(r);this.fragments[a]={body:r,appendedPTS:null,loaded:n,buffered:!1,range:Object.create(null)}}},e.onBufferAppended=function(t,e){var r=this,i=e.frag,n=e.part,a=e.timeRanges;if("initSegment"!==i.sn){var s=i.type;if(n){var o=this.activePartLists[s];o||(this.activePartLists[s]=o=[]),o.push(n)}this.timeRanges=a,Object.keys(a).forEach((function(t){var e=a[t];r.detectEvictedFragments(t,e,s,n)}))}},e.onFragBuffered=function(t,e){this.detectPartialFragments(e)},e.hasFragment=function(t){var e=Tr(t);return!!this.fragments[e]},e.hasParts=function(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)},e.removeFragmentsInRange=function(t,e,r,i,n){var a=this;i&&!this.hasGaps||Object.keys(this.fragments).forEach((function(s){var o=a.fragments[s];if(o){var l=o.body;l.type!==r||i&&!l.gap||l.startt&&(o.buffered||n)&&a.removeFragment(l)}}))},e.removeFragment=function(t){var e=Tr(t);t.stats.loaded=0,t.clearElementaryStreamInfo();var r=this.activePartLists[t.type];if(r){var i=t.sn;this.activePartLists[t.type]=r.filter((function(t){return t.fragment.sn!==i}))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]},e.removeAllFragments=function(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1},t}();function yr(t){var e,r,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(r=t.range.audio)?void 0:r.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function Tr(t){return t.type+"_"+t.level+"_"+t.urlId+"_"+t.sn}var Er=Math.pow(2,17),Sr=function(){function t(t){this.config=void 0,this.loader=null,this.partLoadTimeout=-1,this.config=t}var e=t.prototype;return e.destroy=function(){this.loader&&(this.loader.destroy(),this.loader=null)},e.abort=function(){this.loader&&this.loader.abort()},e.load=function(t,e){var r=this,n=t.url;if(!n)return Promise.reject(new Ar({type:L.NETWORK_ERROR,details:R.FRAG_LOAD_ERROR,fatal:!1,frag:t,error:new Error("Fragment does not have a "+(n?"part list":"url")),networkDetails:null}));this.abort();var a=this.config,s=a.fLoader,o=a.loader;return new Promise((function(l,u){if(r.loader&&r.loader.destroy(),t.gap){if(t.tagList.some((function(t){return"GAP"===t[0]})))return void u(Rr(t));t.gap=!1}var h=r.loader=t.loader=s?new s(a):new o(a),d=Lr(t),c=Xe(a.fragLoadPolicy.default),f={loadPolicy:c,timeout:c.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0,highWaterMark:"initSegment"===t.sn?1/0:Er};t.stats=h.stats,h.load(d,f,{onSuccess:function(e,i,n,a){r.resetLoader(t,h);var s=e.data;n.resetIV&&t.decryptdata&&(t.decryptdata.iv=new Uint8Array(s.slice(0,16)),s=s.slice(16)),l({frag:t,part:null,payload:s,networkDetails:a})},onError:function(e,a,s,o){r.resetLoader(t,h),u(new Ar({type:L.NETWORK_ERROR,details:R.FRAG_LOAD_ERROR,fatal:!1,frag:t,response:i({url:n,data:void 0},e),error:new Error("HTTP Error "+e.code+" "+e.text),networkDetails:s,stats:o}))},onAbort:function(e,i,n){r.resetLoader(t,h),u(new Ar({type:L.NETWORK_ERROR,details:R.INTERNAL_ABORTED,fatal:!1,frag:t,error:new Error("Aborted"),networkDetails:n,stats:e}))},onTimeout:function(e,i,n){r.resetLoader(t,h),u(new Ar({type:L.NETWORK_ERROR,details:R.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,error:new Error("Timeout after "+f.timeout+"ms"),networkDetails:n,stats:e}))},onProgress:function(r,i,n,a){e&&e({frag:t,part:null,payload:n,networkDetails:a})}})}))},e.loadPart=function(t,e,r){var n=this;this.abort();var a=this.config,s=a.fLoader,o=a.loader;return new Promise((function(l,u){if(n.loader&&n.loader.destroy(),t.gap||e.gap)u(Rr(t,e));else{var h=n.loader=t.loader=s?new s(a):new o(a),d=Lr(t,e),c=Xe(a.fragLoadPolicy.default),f={loadPolicy:c,timeout:c.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0,highWaterMark:Er};e.stats=h.stats,h.load(d,f,{onSuccess:function(i,a,s,o){n.resetLoader(t,h),n.updateStatsFromPart(t,e);var u={frag:t,part:e,payload:i.data,networkDetails:o};r(u),l(u)},onError:function(r,a,s,o){n.resetLoader(t,h),u(new Ar({type:L.NETWORK_ERROR,details:R.FRAG_LOAD_ERROR,fatal:!1,frag:t,part:e,response:i({url:d.url,data:void 0},r),error:new Error("HTTP Error "+r.code+" "+r.text),networkDetails:s,stats:o}))},onAbort:function(r,i,a){t.stats.aborted=e.stats.aborted,n.resetLoader(t,h),u(new Ar({type:L.NETWORK_ERROR,details:R.INTERNAL_ABORTED,fatal:!1,frag:t,part:e,error:new Error("Aborted"),networkDetails:a,stats:r}))},onTimeout:function(r,i,a){n.resetLoader(t,h),u(new Ar({type:L.NETWORK_ERROR,details:R.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,part:e,error:new Error("Timeout after "+f.timeout+"ms"),networkDetails:a,stats:r}))}})}}))},e.updateStatsFromPart=function(t,e){var r=t.stats,i=e.stats,n=i.total;if(r.loaded+=i.loaded,n){var a=Math.round(t.duration/e.duration),s=Math.min(Math.round(r.loaded/n),a),o=(a-s)*Math.round(r.loaded/s);r.total=r.loaded+o}else r.total=Math.max(r.loaded,r.total);var l=r.loading,u=i.loading;l.start?l.first+=u.first-u.start:(l.start=u.start,l.first=u.first),l.end=u.end},e.resetLoader=function(t,e){t.loader=null,this.loader===e&&(self.clearTimeout(this.partLoadTimeout),this.loader=null),e.destroy()},t}();function Lr(t,e){void 0===e&&(e=null);var r=e||t,i={frag:t,part:e,responseType:"arraybuffer",url:r.url,headers:{},rangeStart:0,rangeEnd:0},n=r.byteRangeStartOffset,a=r.byteRangeEndOffset;if(E(n)&&E(a)){var s,o=n,l=a;if("initSegment"===t.sn&&"AES-128"===(null==(s=t.decryptdata)?void 0:s.method)){var u=a-n;u%16&&(l=a+(16-u%16)),0!==n&&(i.resetIV=!0,o=n-16)}i.rangeStart=o,i.rangeEnd=l}return i}function Rr(t,e){var r=new Error("GAP "+(t.gap?"tag":"attribute")+" found"),i={type:L.MEDIA_ERROR,details:R.FRAG_GAP,fatal:!1,frag:t,error:r,networkDetails:null};return e&&(i.part=e),(e||t).stats.aborted=!0,new Ar(i)}var Ar=function(t){function e(e){var r;return(r=t.call(this,e.error.message)||this).data=void 0,r.data=e,r}return l(e,t),e}(f(Error)),kr=function(){function t(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}var e=t.prototype;return e.abort=function(t){for(var e in this.keyUriToKeyInfo){var r=this.keyUriToKeyInfo[e].loader;if(r){if(t&&t!==r.context.frag.type)return;r.abort()}}},e.detach=function(){for(var t in this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}},e.destroy=function(){for(var t in this.detach(),this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}},e.createKeyLoadError=function(t,e,r,i,n){return void 0===e&&(e=R.KEY_LOAD_ERROR),new Ar({type:L.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:n,error:r,networkDetails:i})},e.loadClear=function(t,e){var r=this;if(this.emeController&&this.config.emeEnabled)for(var i=t.sn,n=t.cc,a=function(){var t=e[s];if(n<=t.cc&&("initSegment"===i||"initSegment"===t.sn||i1&&this.tickImmediate(),this._tickCallCount=0)},e.tickImmediate=function(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)},e.doTick=function(){},t}(),Dr={length:0,start:function(){return 0},end:function(){return 0}},Ir=function(){function t(){}return t.isBuffered=function(e,r){try{if(e)for(var i=t.getBuffered(e),n=0;n=i.start(n)&&r<=i.end(n))return!0}catch(t){}return!1},t.bufferInfo=function(e,r,i){try{if(e){var n,a=t.getBuffered(e),s=[];for(n=0;ns&&(i[a-1].end=t[n].end):i.push(t[n])}else i.push(t[n])}else i=t;for(var o,l=0,u=e,h=e,d=0;d=c&&er.startCC||t&&t.cc>>8^255&m^99,t[f]=m,e[m]=f;var p=c[f],y=c[p],T=c[y],E=257*c[m]^16843008*m;i[f]=E<<24|E>>>8,n[f]=E<<16|E>>>16,a[f]=E<<8|E>>>24,s[f]=E,E=16843009*T^65537*y^257*p^16843008*f,l[m]=E<<24|E>>>8,u[m]=E<<16|E>>>16,h[m]=E<<8|E>>>24,d[m]=E,f?(f=p^c[c[c[T^p]]],g^=c[c[g]]):f=g=1}},e.expandKey=function(t){for(var e=this.uint8ArrayToUint32Array_(t),r=!0,i=0;is.end){var h=a>u;(a0&&a&&a.key&&a.iv&&"AES-128"===a.method){var s=self.performance.now();return r.decrypter.decrypt(new Uint8Array(n),a.key.buffer,a.iv.buffer).catch((function(e){throw i.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((function(r){var n=self.performance.now();return i.trigger(S.FRAG_DECRYPTED,{frag:t,payload:r,stats:{tstart:s,tdecrypt:n}}),e.payload=r,e}))}return e})).then((function(i){var n=r.fragCurrent,a=r.hls;if(!r.levels)throw new Error("init load aborted, missing levels");var s=t.stats;r.state=Kr,e.fragmentError=0,t.data=new Uint8Array(i.payload),s.parsing.start=s.buffering.start=self.performance.now(),s.parsing.end=s.buffering.end=self.performance.now(),i.frag===n&&a.trigger(S.FRAG_BUFFERED,{stats:s,frag:n,part:null,id:t.type}),r.tick()})).catch((function(e){r.state!==Gr&&r.state!==zr&&(r.warn(e),r.resetFragmentLoading(t))}))},r.fragContextChanged=function(t){var e=this.fragCurrent;return!t||!e||t.level!==e.level||t.sn!==e.sn||t.urlId!==e.urlId},r.fragBufferedComplete=function(t,e){var r,i,n,a,s=this.mediaBuffer?this.mediaBuffer:this.media;this.log("Buffered "+t.type+" sn: "+t.sn+(e?" part: "+e.index:"")+" of "+(this.playlistType===ge?"level":"track")+" "+t.level+" (frag:["+(null!=(r=t.startPTS)?r:NaN).toFixed(3)+"-"+(null!=(i=t.endPTS)?i:NaN).toFixed(3)+"] > buffer:"+(s?Br(Ir.getBuffered(s)):"(detached)")+")"),this.state=Kr,s&&(!this.loadedmetadata&&t.type==ge&&s.buffered.length&&(null==(n=this.fragCurrent)?void 0:n.sn)===(null==(a=this.fragPrevious)?void 0:a.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())},r.seekToStartPos=function(){},r._handleFragmentLoadComplete=function(t){var e=this.transmuxer;if(e){var r=t.frag,i=t.part,n=t.partsLoaded,a=!n||0===n.length||n.some((function(t){return!t})),s=new wr(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!a);e.flush(s)}},r._handleFragmentLoadProgress=function(t){},r._doFragLoad=function(t,e,r,i){var n,a=this;void 0===r&&(r=null);var s=null==e?void 0:e.details;if(!this.levels||!s)throw new Error("frag load aborted, missing level"+(s?"":" detail")+"s");var o=null;if(!t.encrypted||null!=(n=t.decryptdata)&&n.key?!t.encrypted&&s.encryptedFragments.length&&this.keyLoader.loadClear(t,s.encryptedFragments):(this.log("Loading key for "+t.sn+" of ["+s.startSN+"-"+s.endSN+"], "+("[stream-controller]"===this.logPrefix?"level":"track")+" "+t.level),this.state=Hr,this.fragCurrent=t,o=this.keyLoader.load(t).then((function(t){if(!a.fragContextChanged(t.frag))return a.hls.trigger(S.KEY_LOADED,t),a.state===Hr&&(a.state=Kr),t})),this.hls.trigger(S.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(o=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),r=Math.max(t.start,r||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){var l=s.partList;if(l&&i){r>t.end&&s.fragmentHint&&(t=s.fragmentHint);var u=this.getNextPart(l,t,r);if(u>-1){var h,d=l[u];return this.log("Loading part sn: "+t.sn+" p: "+d.index+" cc: "+t.cc+" of playlist ["+s.startSN+"-"+s.endSN+"] parts [0-"+u+"-"+(l.length-1)+"] "+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=d.start+d.duration,this.state=Vr,h=o?o.then((function(r){return!r||a.fragContextChanged(r.frag)?null:a.doFragPartsLoad(t,d,e,i)})).catch((function(t){return a.handleFragLoadError(t)})):this.doFragPartsLoad(t,d,e,i).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,part:d,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(l,r))return Promise.resolve(null)}}this.log("Loading fragment "+t.sn+" cc: "+t.cc+" "+(s?"of ["+s.startSN+"-"+s.endSN+"] ":"")+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),E(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=Vr;var c,f=this.config.progressive;return c=f&&o?o.then((function(e){return!e||a.fragContextChanged(null==e?void 0:e.frag)?null:a.fragmentLoader.load(t,i)})).catch((function(t){return a.handleFragLoadError(t)})):Promise.all([this.fragmentLoader.load(t,f?i:void 0),o]).then((function(t){var e=t[0];return!f&&e&&i&&i(e),e})).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):c},r.doFragPartsLoad=function(t,e,r,i){var n=this;return new Promise((function(a,s){var o,l=[],u=null==(o=r.details)?void 0:o.partList;!function e(o){n.fragmentLoader.loadPart(t,o,i).then((function(i){l[o.index]=i;var s=i.part;n.hls.trigger(S.FRAG_LOADED,i);var h=Ve(r,t.sn,o.index+1)||Ye(u,t.sn,o.index+1);if(!h)return a({frag:t,part:s,partsLoaded:l});e(h)})).catch(s)}(e)}))},r.handleFragLoadError=function(t){if("data"in t){var e=t.data;t.data&&e.details===R.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(S.ERROR,e)}else this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:R.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null},r._handleTransmuxerFlush=function(t){var e=this.getCurrentContext(t);if(e&&this.state===jr){var r=e.frag,i=e.part,n=e.level,a=self.performance.now();r.stats.parsing.end=a,i&&(i.stats.parsing.end=a),this.updateLevelTiming(r,i,n,t.partial)}else this.fragCurrent||this.state===Gr||this.state===zr||(this.state=Kr)},r.getCurrentContext=function(t){var e=this.levels,r=this.fragCurrent,i=t.level,n=t.sn,a=t.part;if(null==e||!e[i])return this.warn("Levels object was unset while buffering fragment "+n+" of level "+i+". The current chunk will not be buffered."),null;var s=e[i],o=a>-1?Ve(s,n,a):null,l=o?o.fragment:function(t,e,r){if(null==t||!t.details)return null;var i=t.details,n=i.fragments[e-i.startSN];return n||((n=i.fragmentHint)&&n.sn===e?n:ea&&this.flushMainBuffer(s,t.start)}else this.flushMainBuffer(0,t.start)},r.getFwdBufferInfo=function(t,e){var r=this.getLoadPosition();return E(r)?this.getFwdBufferInfoAtPos(t,r,e):null},r.getFwdBufferInfoAtPos=function(t,e,r){var i=this.config.maxBufferHole,n=Ir.bufferInfo(t,e,i);if(0===n.len&&void 0!==n.nextStart){var a=this.fragmentTracker.getBufferedFrag(e,r);if(a&&n.nextStart=r&&(e.maxMaxBufferLength/=2,this.warn("Reduce max buffer length to "+e.maxMaxBufferLength+"s"),!0)},r.getAppendedFrag=function(t,e){var r=this.fragmentTracker.getAppendedFrag(t,ge);return r&&"fragment"in r?r.fragment:r},r.getNextFragment=function(t,e){var r=e.fragments,i=r.length;if(!i)return null;var n,a=this.config,s=r[0].start;if(e.live){var o=a.initialLiveManifestSize;if(ie},r.getNextFragmentLoopLoading=function(t,e,r,i,n){var a=t.gap,s=this.getNextFragment(this.nextLoadPosition,e);if(null===s)return s;if(t=s,a&&t&&!t.gap&&r.nextStart){var o=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,r.nextStart,i);if(null!==o&&r.len+o.len>=n)return this.log('buffer full after gaps in "'+i+'" playlist starting at sn: '+t.sn),null}return t},r.mapToInitFragWhenRequired=function(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment},r.getNextPart=function(t,e,r){for(var i=-1,n=!1,a=!0,s=0,o=t.length;s-1&&rr.start&&r.loaded},r.getInitialLiveFragment=function(t,e){var r=this.fragPrevious,i=null;if(r){if(t.hasProgramDateTime&&(this.log("Live playlist, switching playlist, load frag with same PDT: "+r.programDateTime),i=function(t,e,r){if(null===e||!Array.isArray(t)||!t.length||!E(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;r=r||0;for(var i=0;i=t.startSN&&n<=t.endSN){var a=e[n-t.startSN];r.cc===a.cc&&(i=a,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=function(t,e){return Qe(t,(function(t){return t.cce?-1:0}))}(e,r.cc),i&&this.log("Live playlist, switching playlist, load frag with same CC: "+i.sn))}}else{var s=this.hls.liveSyncPosition;null!==s&&(i=this.getFragmentAtPosition(s,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i},r.getFragmentAtPosition=function(t,e,r){var i,n=this.config,a=this.fragPrevious,s=r.fragments,o=r.endSN,l=r.fragmentHint,u=n.maxFragLookUpTolerance,h=r.partList,d=!!(n.lowLatencyMode&&null!=h&&h.length&&l);if(d&&l&&!this.bitrateTest&&(s=s.concat(l),o=l.sn),i=te-u?0:u):s[s.length-1]){var c=i.sn-r.startSN,f=this.fragmentTracker.getState(i);if((f===mr||f===vr&&i.gap)&&(a=i),a&&i.sn===a.sn&&(!d||h[0].fragment.sn>i.sn)&&a&&i.level===a.level){var g=s[c+1];i=i.sn=a-e.maxFragLookUpTolerance&&n<=s;if(null!==i&&r.duration>i&&(n"+t.startSN+" prev-sn: "+(n?n.sn:"na")+" fragments: "+s),h}return o},r.waitForCdnTuneIn=function(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)},r.setStartPosition=function(t,e){var r=this.startPosition;if(r "+(null==(n=this.fragCurrent)?void 0:n.url))}else{var a=e.details===R.FRAG_GAP;a&&this.fragmentTracker.fragBuffered(i,!0);var s=e.errorAction,o=s||{},l=o.action,u=o.retryCount,h=void 0===u?0:u,d=o.retryConfig;if(s&&l===nr&&d){var c;this.resetStartWhenNotLoaded(null!=(c=this.levelLastLoaded)?c:i.level);var f=qe(d,h);this.warn("Fragment "+i.sn+" of "+t+" "+i.level+" errored with "+e.details+", retrying loading "+(h+1)+"/"+d.maxNumRetry+" in "+f+"ms"),s.resolved=!0,this.retryDate=self.performance.now()+f,this.state=Yr}else d&&s?(this.resetFragmentErrors(t),h.5;i&&this.reduceMaxBufferLength(r.len);var n=!i;return n&&this.warn("Buffer full error while media.currentTime is not buffered, flush "+e+" buffer"),t.frag&&(this.fragmentTracker.removeFragment(t.frag),this.nextLoadPosition=t.frag.start),this.resetLoadingState(),n}return!1},r.resetFragmentErrors=function(t){t===ve&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==Gr&&(this.state=Kr)},r.afterBufferFlushed=function(t,e,r){if(t){var i=Ir.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,r),this.state===Xr&&this.resetLoadingState()}},r.resetLoadingState=function(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=Kr},r.resetStartWhenNotLoaded=function(t){if(!this.loadedmetadata){this.startFragRequested=!1;var e=this.levels?this.levels[t].details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}},r.resetWhenMissingContext=function(t){var e;this.warn("The loading context changed while buffering fragment "+t.sn+" of level "+t.level+". This chunk will not be buffered."),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(null!=(e=this.levelLastLoaded)?e:t.level),this.resetLoadingState()},r.removeUnbufferedFrags=function(t){void 0===t&&(t=0),this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)},r.updateLevelTiming=function(t,e,r,i){var n,a=this,s=r.details;if(s){if(Object.keys(t.elementaryStreams).reduce((function(e,n){var o=t.elementaryStreams[n];if(o){var l=o.endPTS-o.startPTS;if(l<=0)return a.warn("Could not parse fragment "+t.sn+" "+n+" duration reliably ("+l+")"),e||!1;var u=i?0:Be(s,t,o.startPTS,o.endPTS,o.startDTS,o.endDTS);return a.hls.trigger(S.LEVEL_PTS_UPDATED,{details:s,level:r,drift:u,type:n,frag:t,start:o.startPTS,end:o.endPTS}),!0}return e}),!1))r.fragmentError=0;else if(null===(null==(n=this.transmuxer)?void 0:n.error)){var o=new Error("Found no media in fragment "+t.sn+" of level "+t.level+" resetting transmuxer to fallback to playlist timing");if(0===r.fragmentError&&(r.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(o.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.FRAG_PARSING_ERROR,fatal:!1,error:o,frag:t,reason:"Found no media in msn "+t.sn+' of level "'+r.url+'"'}),!this.hls)return;this.resetTransmuxer()}this.state=qr,this.hls.trigger(S.FRAG_PARSED,{frag:t,part:e})}else this.warn("level.details undefined")},r.resetTransmuxer=function(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)},r.recoverWorkerError=function(t){var e,r,i;"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(null!=(e=null!=(r=this.levelLastLoaded)?r:null==(i=this.fragCurrent)?void 0:i.level)?e:0),this.resetLoadingState())},a(e,[{key:"state",get:function(){return this._state},set:function(t){var e=this._state;e!==t&&(this._state=t,this.log(e+"->"+t))}}]),e}(br);function Zr(){return self.SourceBuffer||self.WebKitSourceBuffer}function ti(t,e){return void 0===t&&(t=""),void 0===e&&(e=9e4),{type:t,id:-1,pid:-1,inputTimeScale:e,sequenceNumber:-1,samples:[],dropped:0}}var ei=function(){function t(){this._audioTrack=void 0,this._id3Track=void 0,this.frameIndex=0,this.cachedData=null,this.basePTS=null,this.initPTS=null,this.lastPTS=null}var e=t.prototype;return e.resetInitSegment=function(t,e,r,i){this._id3Track={type:"id3",id:3,pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0}},e.resetTimeStamp=function(t){this.initPTS=t,this.resetContiguity()},e.resetContiguity=function(){this.basePTS=null,this.lastPTS=null,this.frameIndex=0},e.canParse=function(t,e){return!1},e.appendFrame=function(t,e,r){},e.demux=function(t,e){this.cachedData&&(t=xt(this.cachedData,t),this.cachedData=null);var r,i=st(t,0),n=i?i.length:0,a=this._audioTrack,s=this._id3Track,o=i?function(t){for(var e=dt(t),r=0;r0&&s.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:Ae,duration:Number.POSITIVE_INFINITY});n>>5}function si(t,e){return e+1=t.length)return!1;var i=ai(t,e);if(i<=r)return!1;var n=e+i;return n===t.length||si(t,n)}return!1}function li(t,e,r,i,n){if(!t.samplerate){var a=function(t,e,r,i){var n,a,s,o,l=navigator.userAgent.toLowerCase(),u=i,h=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];n=1+((192&e[r+2])>>>6);var d=(60&e[r+2])>>>2;if(!(d>h.length-1))return s=(1&e[r+2])<<2,s|=(192&e[r+3])>>>6,w.log("manifest codec:"+i+", ADTS type:"+n+", samplingIndex:"+d),/firefox/i.test(l)?d>=6?(n=5,o=new Array(4),a=d-3):(n=2,o=new Array(2),a=d):-1!==l.indexOf("android")?(n=2,o=new Array(2),a=d):(n=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&d>=6?a=d-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(d>=6&&1===s||/vivaldi/i.test(l))||!i&&1===s)&&(n=2,o=new Array(2)),a=d)),o[0]=n<<3,o[0]|=(14&d)>>1,o[1]|=(1&d)<<7,o[1]|=s<<3,5===n&&(o[1]|=(14&a)>>1,o[2]=(1&a)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:h[d],channelCount:s,codec:"mp4a.40."+n,manifestCodec:u};t.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.FRAG_PARSING_ERROR,fatal:!0,reason:"invalid ADTS sampling index:"+d})}(e,r,i,n);if(!a)return;t.config=a.config,t.samplerate=a.samplerate,t.channelCount=a.channelCount,t.codec=a.codec,t.manifestCodec=a.manifestCodec,w.log("parsed codec:"+t.codec+", rate:"+a.samplerate+", channels:"+a.channelCount)}}function ui(t){return 9216e4/t}function hi(t,e,r,i,n){var a,s=i+n*ui(t.samplerate),o=function(t,e){var r=ni(t,e);if(e+r<=t.length){var i=ai(t,e)-r;if(i>0)return{headerLength:r,frameLength:i}}}(e,r);if(o){var l=o.frameLength,u=o.headerLength,h=u+l,d=Math.max(0,r+h-e.length);d?(a=new Uint8Array(h-u)).set(e.subarray(r+u,e.length),0):a=e.subarray(r+u,r+h);var c={unit:a,pts:s};return d||t.samples.push(c),{sample:c,length:h,missing:d}}var f=e.length-r;return(a=new Uint8Array(f)).set(e.subarray(r,e.length),0),{sample:{unit:a,pts:s},length:f,missing:-1}}var di=function(t){function e(e,r){var i;return(i=t.call(this)||this).observer=void 0,i.config=void 0,i.observer=e,i.config=r,i}l(e,t);var r=e.prototype;return r.resetInitSegment=function(e,r,i,n){t.prototype.resetInitSegment.call(this,e,r,i,n),this._audioTrack={container:"audio/adts",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"aac",samples:[],manifestCodec:r,duration:n,inputTimeScale:9e4,dropped:0}},e.probe=function(t){if(!t)return!1;for(var e=(st(t,0)||[]).length,r=t.length;e16384?t.subarray(0,16384):t,["moof"]).length>0},e.demux=function(t,e){this.timeOffset=e;var r=t,i=this.videoTrack,n=this.txtTrack;if(this.config.progressive){this.remainderData&&(r=xt(this.remainderData,t));var a=function(t){var e={valid:null,remainder:null},r=It(t,["moof"]);if(!r)return e;if(r.length<2)return e.remainder=t,e;var i=r[r.length-1];return e.valid=rt(t,0,i.byteOffset-8),e.remainder=rt(t,i.byteOffset-8),e}(r);this.remainderData=a.remainder,i.samples=a.valid||new Uint8Array}else i.samples=r;var s=this.extractID3Track(i,e);return n.samples=Ft(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:s,textTrack:this.txtTrack}},e.flush=function(){var t=this.timeOffset,e=this.videoTrack,r=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;var i=this.extractID3Track(e,this.timeOffset);return r.samples=Ft(t,e),{videoTrack:e,audioTrack:ti(),id3Track:i,textTrack:ti()}},e.extractID3Track=function(t,e){var r=this.id3Track;if(t.samples.length){var i=It(t.samples,["emsg"]);i&&i.forEach((function(t){var i=function(t){var e=t[0],r="",i="",n=0,a=0,s=0,o=0,l=0,u=0;if(0===e){for(;"\0"!==Rt(t.subarray(u,u+1));)r+=Rt(t.subarray(u,u+1)),u+=1;for(r+=Rt(t.subarray(u,u+1)),u+=1;"\0"!==Rt(t.subarray(u,u+1));)i+=Rt(t.subarray(u,u+1)),u+=1;i+=Rt(t.subarray(u,u+1)),u+=1,n=kt(t,12),a=kt(t,16),o=kt(t,20),l=kt(t,24),u=28}else if(1===e){n=kt(t,u+=4);var h=kt(t,u+=4),d=kt(t,u+=4);for(u+=4,s=Math.pow(2,32)*h+d,Number.isSafeInteger(s)||(s=Number.MAX_SAFE_INTEGER,w.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=kt(t,u),l=kt(t,u+=4),u+=4;"\0"!==Rt(t.subarray(u,u+1));)r+=Rt(t.subarray(u,u+1)),u+=1;for(r+=Rt(t.subarray(u,u+1)),u+=1;"\0"!==Rt(t.subarray(u,u+1));)i+=Rt(t.subarray(u,u+1)),u+=1;i+=Rt(t.subarray(u,u+1)),u+=1}return{schemeIdUri:r,value:i,timeScale:n,presentationTime:s,presentationTimeDelta:a,eventDuration:o,id:l,payload:t.subarray(u,t.byteLength)}}(t);if(ci.test(i.schemeIdUri)){var n=E(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale,a=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;a<=.001&&(a=Number.POSITIVE_INFINITY);var s=i.payload;r.samples.push({data:s,len:s.byteLength,dts:n,pts:n,type:be,duration:a})}}))}return r},e.demuxSampleAes=function(t,e,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},e.destroy=function(){},t}(),gi=null,vi=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],mi=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],pi=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],yi=[0,1,1,4];function Ti(t,e,r,i,n){if(!(r+24>e.length)){var a=Ei(e,r);if(a&&r+a.frameLength<=e.length){var s=i+n*(9e4*a.samplesPerFrame/a.sampleRate),o={unit:e.subarray(r,r+a.frameLength),pts:s,dts:s};return t.config=[],t.channelCount=a.channelCount,t.samplerate=a.sampleRate,t.samples.push(o),{sample:o,length:a.frameLength,missing:0}}}}function Ei(t,e){var r=t[e+1]>>3&3,i=t[e+1]>>1&3,n=t[e+2]>>4&15,a=t[e+2]>>2&3;if(1!==r&&0!==n&&15!==n&&3!==a){var s=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*vi[14*(3===r?3-i:3===i?3:4)+n-1],u=mi[3*(3===r?0:2===r?1:2)+a],h=3===o?1:2,d=pi[r][i],c=yi[i],f=8*d*c,g=Math.floor(d*l/u+s)*c;if(null===gi){var v=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);gi=v?parseInt(v[1]):0}return!!gi&&gi<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:u,channelCount:h,frameLength:g,samplesPerFrame:f}}}function Si(t,e){return 255===t[e]&&224==(224&t[e+1])&&0!=(6&t[e+1])}function Li(t,e){return e+1t?(this.word<<=t,this.bitsAvailable-=t):(t-=this.bitsAvailable,t-=(e=t>>3)<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)},e.readBits=function(t){var e=Math.min(this.bitsAvailable,t),r=this.word>>>32-e;if(t>32&&w.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return(e=t-e)>0&&this.bitsAvailable?r<>>t))return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()},e.skipUEG=function(){this.skipBits(1+this.skipLZ())},e.skipEG=function(){this.skipBits(1+this.skipLZ())},e.readUEG=function(){var t=this.skipLZ();return this.readBits(t+1)-1},e.readEG=function(){var t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)},e.readBoolean=function(){return 1===this.readBits(1)},e.readUByte=function(){return this.readBits(8)},e.readUShort=function(){return this.readBits(16)},e.readUInt=function(){return this.readBits(32)},e.skipScalingList=function(t){for(var e=8,r=8,i=0;i=t.length)return void r();if(!(t[e].unit.length<32||(this.decryptAacSample(t,e,r),this.decrypter.isSync())))return}},e.getAvcEncryptedData=function(t){for(var e=16*Math.floor((t.length-48)/160)+16,r=new Int8Array(e),i=0,n=32;n=t.length)return void i();for(var n=t[e].units;!(r>=n.length);r++){var a=n[r];if(!(a.data.length<=48||1!==a.type&&5!==a.type||(this.decryptAvcSample(t,e,r,i,a),this.decrypter.isSync())))return}}},t}(),bi=188,Di=function(){function t(t,e,r){this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._duration=0,this._pmtId=-1,this._avcTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.avcSample=null,this.remainderData=null,this.observer=t,this.config=e,this.typeSupported=r}t.probe=function(e){var r=t.syncOffset(e);return r>0&&w.warn("MPEG2-TS detected but first sync word found @ offset "+r),-1!==r},t.syncOffset=function(t){for(var e=t.length,r=Math.min(940,t.length-bi)+1,i=0;i1&&(0===a&&s>2||o+bi>r))return a}i++}return-1},t.createTrack=function(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:Lt[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}};var e=t.prototype;return e.resetInitSegment=function(e,r,i,n){this.pmtParsed=!1,this._pmtId=-1,this._avcTrack=t.createTrack("video"),this._audioTrack=t.createTrack("audio",n),this._id3Track=t.createTrack("id3"),this._txtTrack=t.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.avcSample=null,this.remainderData=null,this.audioCodec=r,this.videoCodec=i,this._duration=n},e.resetTimeStamp=function(){},e.resetContiguity=function(){var t=this._audioTrack,e=this._avcTrack,r=this._id3Track;t&&(t.pesData=null),e&&(e.pesData=null),r&&(r.pesData=null),this.aacOverFlow=null,this.avcSample=null,this.remainderData=null},e.demux=function(e,r,i,n){var a;void 0===i&&(i=!1),void 0===n&&(n=!1),i||(this.sampleAes=null);var s=this._avcTrack,o=this._audioTrack,l=this._id3Track,u=this._txtTrack,h=s.pid,d=s.pesData,c=o.pid,f=l.pid,g=o.pesData,v=l.pesData,m=null,p=this.pmtParsed,y=this._pmtId,T=e.length;if(this.remainderData&&(T=(e=xt(this.remainderData,e)).length,this.remainderData=null),T>4>1){if((I=k+5+e[k+4])===k+bi)continue}else I=k+4;switch(D){case h:b&&(d&&(a=Pi(d))&&this.parseAVCPES(s,u,a,!1),d={data:[],size:0}),d&&(d.data.push(e.subarray(I,k+bi)),d.size+=k+bi-I);break;case c:if(b){if(g&&(a=Pi(g)))switch(o.segmentCodec){case"aac":this.parseAACPES(o,a);break;case"mp3":this.parseMPEGPES(o,a)}g={data:[],size:0}}g&&(g.data.push(e.subarray(I,k+bi)),g.size+=k+bi-I);break;case f:b&&(v&&(a=Pi(v))&&this.parseID3PES(l,a),v={data:[],size:0}),v&&(v.data.push(e.subarray(I,k+bi)),v.size+=k+bi-I);break;case 0:b&&(I+=e[I]+1),y=this._pmtId=Ci(e,I);break;case y:b&&(I+=e[I]+1);var C=_i(e,I,this.typeSupported,i);(h=C.avc)>0&&(s.pid=h),(c=C.audio)>0&&(o.pid=c,o.segmentCodec=C.segmentCodec),(f=C.id3)>0&&(l.pid=f),null===m||p||(w.warn("MPEG-TS PMT found at "+k+" after unknown PID '"+m+"'. Backtracking to sync byte @"+E+" to parse all TS packets."),m=null,k=E-188),p=this.pmtParsed=!0;break;case 17:case 8191:break;default:m=D}}else A++;if(A>0){var _=new Error("Found "+A+" TS packet/s that do not start with 0x47");this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:R.FRAG_PARSING_ERROR,fatal:!1,error:_,reason:_.message})}s.pesData=d,o.pesData=g,l.pesData=v;var P={audioTrack:o,videoTrack:s,id3Track:l,textTrack:u};return n&&this.extractRemainingSamples(P),P},e.flush=function(){var t,e=this.remainderData;return this.remainderData=null,t=e?this.demux(e,-1,!1,!0):{videoTrack:this._avcTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(t),this.sampleAes?this.decrypt(t,this.sampleAes):t},e.extractRemainingSamples=function(t){var e,r=t.audioTrack,i=t.videoTrack,n=t.id3Track,a=t.textTrack,s=i.pesData,o=r.pesData,l=n.pesData;if(s&&(e=Pi(s))?(this.parseAVCPES(i,a,e,!0),i.pesData=null):i.pesData=s,o&&(e=Pi(o))){switch(r.segmentCodec){case"aac":this.parseAACPES(r,e);break;case"mp3":this.parseMPEGPES(r,e)}r.pesData=null}else null!=o&&o.size&&w.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=o;l&&(e=Pi(l))?(this.parseID3PES(n,e),n.pesData=null):n.pesData=l},e.demuxSampleAes=function(t,e,r){var i=this.demux(t,r,!0,!this.config.progressive),n=this.sampleAes=new ki(this.observer,this.config,e);return this.decrypt(i,n)},e.decrypt=function(t,e){return new Promise((function(r){var i=t.audioTrack,n=t.videoTrack;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(function(){n.samples?e.decryptAvcSamples(n.samples,0,0,(function(){r(t)})):r(t)})):n.samples&&e.decryptAvcSamples(n.samples,0,0,(function(){r(t)}))}))},e.destroy=function(){this._duration=0},e.parseAVCPES=function(t,e,r,i){var n,a=this,s=this.parseAVCNALu(t,r.data),o=this.avcSample,l=!1;r.data=null,o&&s.length&&!t.audFound&&(xi(o,t),o=this.avcSample=Ii(!1,r.pts,r.dts,"")),s.forEach((function(i){var s;switch(i.type){case 1:var u=!1;n=!0;var h,d=i.data;if(l&&d.length>4){var c=new Ai(d).readSliceType();2!==c&&4!==c&&7!==c&&9!==c||(u=!0)}u&&null!=(h=o)&&h.frame&&!o.key&&(xi(o,t),o=a.avcSample=null),o||(o=a.avcSample=Ii(!0,r.pts,r.dts,"")),o.frame=!0,o.key=u;break;case 5:n=!0,null!=(s=o)&&s.frame&&!o.key&&(xi(o,t),o=a.avcSample=null),o||(o=a.avcSample=Ii(!0,r.pts,r.dts,"")),o.key=!0,o.frame=!0;break;case 6:n=!0,Ot(i.data,1,r.pts,e.samples);break;case 7:if(n=!0,l=!0,!t.sps){var f=i.data,g=new Ai(f).readSPS();t.width=g.width,t.height=g.height,t.pixelRatio=g.pixelRatio,t.sps=[f],t.duration=a._duration;for(var v=f.subarray(1,4),m="avc1.",p=0;p<3;p++){var y=v[p].toString(16);y.length<2&&(y="0"+y),m+=y}t.codec=m}break;case 8:n=!0,t.pps||(t.pps=[i.data]);break;case 9:n=!1,t.audFound=!0,o&&xi(o,t),o=a.avcSample=Ii(!1,r.pts,r.dts,"");break;case 12:n=!0;break;default:n=!1,o&&(o.debug+="unknown NAL "+i.type+" ")}o&&n&&o.units.push(i)})),i&&o&&(xi(o,t),this.avcSample=null)},e.getLastNalUnit=function(t){var e,r,i=this.avcSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){var n=i.units;r=n[n.length-1]}return r},e.parseAVCNALu=function(t,e){var r,i,n=e.byteLength,a=t.naluState||0,s=a,o=[],l=0,u=-1,h=0;for(-1===a&&(u=0,h=31&e[0],a=0,l=1);l=0){var d={data:e.subarray(u,l-a-1),type:h};o.push(d)}else{var c=this.getLastNalUnit(t.samples);if(c&&(s&&l<=4-s&&c.state&&(c.data=c.data.subarray(0,c.data.byteLength-s)),(i=l-a-1)>0)){var f=new Uint8Array(c.data.byteLength+i);f.set(c.data,0),f.set(e.subarray(0,i),c.data.byteLength),c.data=f,c.state=0}}l=0&&a>=0){var g={data:e.subarray(u,n),type:h,state:a};o.push(g)}if(0===o.length){var v=this.getLastNalUnit(t.samples);if(v){var m=new Uint8Array(v.data.byteLength+e.byteLength);m.set(v.data,0),m.set(e,v.data.byteLength),v.data=m}}return t.naluState=a,o},e.parseAACPES=function(t,e){var r,i,n,a=0,s=this.aacOverFlow,o=e.data;if(s){this.aacOverFlow=null;var l=s.missing,u=s.sample.unit.byteLength;if(-1===l){var h=new Uint8Array(u+o.byteLength);h.set(s.sample.unit,0),h.set(o,u),o=h}else{var d=u-l;s.sample.unit.set(o.subarray(0,l),d),t.samples.push(s.sample),a=s.missing}}for(r=a,i=o.length;r1;){var l=new Uint8Array(o[0].length+o[1].length);l.set(o[0]),l.set(o[1],o[0].length),o[0]=l,o.splice(1,1)}if(1===((e=o[0])[0]<<16)+(e[1]<<8)+e[2]){if((r=(e[4]<<8)+e[5])&&r>t.size-6)return null;var u=e[7];192&u&&(n=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&u?n-(a=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2)>54e5&&(w.warn(Math.round((n-a)/9e4)+"s delta between PTS and DTS, align them"),n=a):a=n);var h=(i=e[8])+9;if(t.size<=h)return null;t.size-=h;for(var d=new Uint8Array(t.size),c=0,f=o.length;cg){h-=g;continue}e=e.subarray(h),g-=h,h=0}d.set(e,s),s+=g}return r&&(r-=i+3),{data:d,pts:n,dts:a,len:r}}return null}function xi(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){var r=e.samples,i=r.length;if(!i)return void e.dropped++;var n=r[i-1];t.pts=n.pts,t.dts=n.dts}e.samples.push(t)}t.debug.length&&w.log(t.pts+"/"+t.dts+":"+t.debug)}var Fi=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var r=e.prototype;return r.resetInitSegment=function(e,r,i,n){t.prototype.resetInitSegment.call(this,e,r,i,n),this._audioTrack={container:"audio/mpeg",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"mp3",samples:[],manifestCodec:r,duration:n,inputTimeScale:9e4,dropped:0}},e.probe=function(t){if(!t)return!1;for(var e=(st(t,0)||[]).length,r=t.length;e1?r-1:0),n=1;n>24&255,o[1]=e>>16&255,o[2]=e>>8&255,o[3]=255&e,o.set(t,4),a=0,e=8;a>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))},t.mdia=function(e){return t.box(t.types.mdia,t.mdhd(e.timescale,e.duration),t.hdlr(e.type),t.minf(e))},t.mfhd=function(e){return t.box(t.types.mfhd,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e]))},t.minf=function(e){return"audio"===e.type?t.box(t.types.minf,t.box(t.types.smhd,t.SMHD),t.DINF,t.stbl(e)):t.box(t.types.minf,t.box(t.types.vmhd,t.VMHD),t.DINF,t.stbl(e))},t.moof=function(e,r,i){return t.box(t.types.moof,t.mfhd(e),t.traf(i,r))},t.moov=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trak(e[r]);return t.box.apply(null,[t.types.moov,t.mvhd(e[0].timescale,e[0].duration)].concat(i).concat(t.mvex(e)))},t.mvex=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trex(e[r]);return t.box.apply(null,[t.types.mvex].concat(i))},t.mvhd=function(e,r){r*=e;var i=Math.floor(r/(Oi+1)),n=Math.floor(r%(Oi+1)),a=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return t.box(t.types.mvhd,a)},t.sdtp=function(e){var r,i,n=e.samples||[],a=new Uint8Array(4+n.length);for(r=0;r>>8&255),a.push(255&n),a=a.concat(Array.prototype.slice.call(i));for(r=0;r>>8&255),s.push(255&n),s=s.concat(Array.prototype.slice.call(i));var o=t.box(t.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|e.sps.length].concat(a).concat([e.pps.length]).concat(s))),l=e.width,u=e.height,h=e.pixelRatio[0],d=e.pixelRatio[1];return t.box(t.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,l>>8&255,255&l,u>>8&255,255&u,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),o,t.box(t.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),t.box(t.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))},t.esds=function(t){var e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))},t.mp4a=function(e){var r=e.samplerate;return t.box(t.types.mp4a,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,e.channelCount,0,16,0,0,0,0,r>>8&255,255&r,0,0]),t.box(t.types.esds,t.esds(e)))},t.mp3=function(e){var r=e.samplerate;return t.box(t.types[".mp3"],new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,e.channelCount,0,16,0,0,0,0,r>>8&255,255&r,0,0]))},t.stsd=function(e){return"audio"===e.type?"mp3"===e.segmentCodec&&"mp3"===e.codec?t.box(t.types.stsd,t.STSD,t.mp3(e)):t.box(t.types.stsd,t.STSD,t.mp4a(e)):t.box(t.types.stsd,t.STSD,t.avc1(e))},t.tkhd=function(e){var r=e.id,i=e.duration*e.timescale,n=e.width,a=e.height,s=Math.floor(i/(Oi+1)),o=Math.floor(i%(Oi+1));return t.box(t.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,r>>24&255,r>>16&255,r>>8&255,255&r,0,0,0,0,s>>24,s>>16&255,s>>8&255,255&s,o>>24,o>>16&255,o>>8&255,255&o,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,n>>8&255,255&n,0,0,a>>8&255,255&a,0,0]))},t.traf=function(e,r){var i=t.sdtp(e),n=e.id,a=Math.floor(r/(Oi+1)),s=Math.floor(r%(Oi+1));return t.box(t.types.traf,t.box(t.types.tfhd,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),t.box(t.types.tfdt,new Uint8Array([1,0,0,0,a>>24,a>>16&255,a>>8&255,255&a,s>>24,s>>16&255,s>>8&255,255&s])),t.trun(e,i.length+16+20+8+16+8+8),i)},t.trak=function(e){return e.duration=e.duration||4294967295,t.box(t.types.trak,t.tkhd(e),t.mdia(e))},t.trex=function(e){var r=e.id;return t.box(t.types.trex,new Uint8Array([0,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))},t.trun=function(e,r){var i,n,a,s,o,l,u=e.samples||[],h=u.length,d=12+16*h,c=new Uint8Array(d);for(r+=8+d,c.set(["video"===e.type?1:0,0,15,1,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r],0),i=0;i>>24&255,a>>>16&255,a>>>8&255,255&a,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o.isLeading<<2|o.dependsOn,o.isDependedOn<<6|o.hasRedundancy<<4|o.paddingValue<<1|o.isNonSync,61440&o.degradPrio,15&o.degradPrio,l>>>24&255,l>>>16&255,l>>>8&255,255&l],12+16*i);return t.box(t.types.trun,c)},t.initSegment=function(e){t.types||t.init();var r=t.moov(e),i=new Uint8Array(t.FTYP.byteLength+r.byteLength);return i.set(t.FTYP),i.set(r,t.FTYP.byteLength),i},t}();function Ui(t,e,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var n=t*e*r;return i?Math.round(n):n}function Bi(t,e){return void 0===e&&(e=!1),Ui(t,1e3,1/9e4,e)}Ni.types=void 0,Ni.HDLR_TYPES=void 0,Ni.STTS=void 0,Ni.STSC=void 0,Ni.STCO=void 0,Ni.STSZ=void 0,Ni.VMHD=void 0,Ni.SMHD=void 0,Ni.STSD=void 0,Ni.FTYP=void 0,Ni.DINF=void 0;var Gi=null,Ki=null,Hi=function(){function t(t,e,r,i){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.observer=t,this.config=e,this.typeSupported=r,this.ISGenerated=!1,null===Gi){var n=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Gi=n?parseInt(n[1]):0}if(null===Ki){var a=navigator.userAgent.match(/Safari\/(\d+)/i);Ki=a?parseInt(a[1]):0}}var e=t.prototype;return e.destroy=function(){},e.resetTimeStamp=function(t){w.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t},e.resetNextTimestamp=function(){w.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1},e.resetInitSegment=function(){w.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1},e.getVideoStartPts=function(t){var e=!1,r=t.reduce((function(t,r){var i=r.pts-t;return i<-4294967296?(e=!0,Vi(t,r.pts)):i>0?t:r.pts}),t[0].pts);return e&&w.debug("PTS rollover detected"),r},e.remux=function(t,e,r,i,n,a,s,o){var l,u,h,d,c,f,g=n,v=n,m=t.pid>-1,p=e.pid>-1,y=e.samples.length,T=t.samples.length>0,E=s&&y>0||y>1;if((!m||T)&&(!p||E)||this.ISGenerated||s){this.ISGenerated||(h=this.generateIS(t,e,n,a));var S,L=this.isVideoContiguous,R=-1;if(E&&(R=function(t){for(var e=0;e0){w.warn("[mp4-remuxer]: Dropped "+R+" out of "+y+" video samples due to a missing keyframe");var A=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(R),e.dropped+=R,S=v+=(e.samples[0].pts-A)/e.inputTimeScale}else-1===R&&(w.warn("[mp4-remuxer]: No keyframe found out of "+y+" video samples"),f=!1);if(this.ISGenerated){if(T&&E){var k=this.getVideoStartPts(e.samples),b=(Vi(t.samples[0].pts,k)-k)/e.inputTimeScale;g+=Math.max(0,b),v+=Math.max(0,-b)}if(T){if(t.samplerate||(w.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),h=this.generateIS(t,e,n,a)),u=this.remuxAudio(t,g,this.isAudioContiguous,a,p||E||o===ve?v:void 0),E){var D=u?u.endPTS-u.startPTS:0;e.inputTimeScale||(w.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),h=this.generateIS(t,e,n,a)),l=this.remuxVideo(e,v,L,D)}}else E&&(l=this.remuxVideo(e,v,L,0));l&&(l.firstKeyFrame=R,l.independent=-1!==R,l.firstKeyFramePTS=S)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(r.samples.length&&(c=Yi(r,n,this._initPTS,this._initDTS)),i.samples.length&&(d=Wi(i,n,this._initPTS))),{audio:u,video:l,initSegment:h,independent:f,text:d,id3:c}},e.generateIS=function(t,e,r,i){var n,a,s,o=t.samples,l=e.samples,u=this.typeSupported,h={},d=this._initPTS,c=!d||i,f="audio/mp4";if(c&&(n=a=1/0),t.config&&o.length&&(t.timescale=t.samplerate,"mp3"===t.segmentCodec&&(u.mpeg?(f="audio/mpeg",t.codec=""):u.mp3&&(t.codec="mp3")),h.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&u.mpeg?new Uint8Array(0):Ni.initSegment([t]),metadata:{channelCount:t.channelCount}},c&&(s=t.inputTimeScale,d&&s===d.timescale?c=!1:n=a=o[0].pts-Math.round(s*r))),e.sps&&e.pps&&l.length&&(e.timescale=e.inputTimeScale,h.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:Ni.initSegment([e]),metadata:{width:e.width,height:e.height}},c))if(s=e.inputTimeScale,d&&s===d.timescale)c=!1;else{var g=this.getVideoStartPts(l),v=Math.round(s*r);a=Math.min(a,Vi(l[0].dts,g)-v),n=Math.min(n,g-v)}if(Object.keys(h).length)return this.ISGenerated=!0,c?(this._initPTS={baseTime:n,timescale:s},this._initDTS={baseTime:a,timescale:s}):n=s=void 0,{tracks:h,initPTS:n,timescale:s}},e.remuxVideo=function(t,e,r,i){var n,a,s=t.inputTimeScale,l=t.samples,u=[],h=l.length,d=this._initPTS,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,v=Number.POSITIVE_INFINITY,m=Number.NEGATIVE_INFINITY,p=!1;r&&null!==c||(c=e*s-(l[0].pts-Vi(l[0].dts,l[0].pts)));for(var y=d.baseTime*s/d.timescale,T=0;T0?T-1:T].dts&&(p=!0)}p&&l.sort((function(t,e){var r=t.dts-e.dts,i=t.pts-e.pts;return r||i})),n=l[0].dts;var A=(a=l[l.length-1].dts)-n,k=A?Math.round(A/(h-1)):g||t.inputTimeScale/30;if(r){var b=n-c,D=b>k,I=b<-1;if((D||I)&&(D?w.warn("AVC: "+Bi(b,!0)+" ms ("+b+"dts) hole between fragments detected, filling it"):w.warn("AVC: "+Bi(-b,!0)+" ms ("+b+"dts) overlapping between fragments detected"),!I||c>=l[0].pts)){n=c;var C=l[0].pts-b;l[0].dts=n,l[0].pts=C,w.log("Video: First PTS/DTS adjusted: "+Bi(C,!0)+"/"+Bi(n,!0)+", delta: "+Bi(b,!0)+" ms")}}n=Math.max(0,n);for(var _=0,P=0,x=0;x0?X.dts-l[q-1].dts:k;if(rt=q>0?X.pts-l[q-1].pts:k,it.stretchShortVideoTrack&&null!==this.nextAudioPts){var at=Math.floor(it.maxBufferHole*s),st=(i?v+i*s:this.nextAudioPts)-X.pts;st>at?((g=st-nt)<0?g=nt:H=!0,w.log("[mp4-remuxer]: It is approximately "+st/90+" ms to the next segment; using duration "+g/90+" ms for the last video frame.")):g=nt}else g=nt}var ot=Math.round(X.pts-X.dts);V=Math.min(V,g),W=Math.max(W,g),Y=Math.min(Y,rt),j=Math.max(j,rt),u.push(new qi(X.key,g,Q,ot))}if(u.length)if(Gi){if(Gi<70){var lt=u[0].flags;lt.dependsOn=2,lt.isNonSync=0}}else if(Ki&&j-Y0&&(i&&Math.abs(p-m)<9e3||Math.abs(Vi(g[0].pts-y,p)-m)<20*u),g.forEach((function(t){t.pts=Vi(t.pts-y,p)})),!r||m<0){if(g=g.filter((function(t){return t.pts>=0})),!g.length)return;m=0===n?0:i&&!f?Math.max(0,p):g[0].pts}if("aac"===t.segmentCodec)for(var T=this.config.maxAudioFramesDrift,E=0,A=m;E=T*u&&I<1e4&&f){var C=Math.round(D/u);(A=b-C*u)<0&&(C--,A+=u),0===E&&(this.nextAudioPts=m=A),w.warn("[mp4-remuxer]: Injecting "+C+" audio frame @ "+(A/a).toFixed(3)+"s due to "+Math.round(1e3*D/a)+" ms gap.");for(var _=0;_0))return;N+=v;try{F=new Uint8Array(N)}catch(t){return void this.observer.emit(S.ERROR,S.ERROR,{type:L.MUX_ERROR,details:R.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:N,reason:"fail allocating audio mdat "+N})}d||(new DataView(F.buffer).setUint32(0,N),F.set(Ni.types.mdat,4))}F.set(H,v);var Y=H.byteLength;v+=Y,c.push(new qi(!0,l,Y,0)),O=V}var W=c.length;if(W){var j=c[c.length-1];this.nextAudioPts=m=O+s*j.duration;var q=d?new Uint8Array(0):Ni.moof(t.sequenceNumber++,M/s,o({},t,{samples:c}));t.samples=[];var X=M/a,z=m/a,Q={data1:q,data2:F,startPTS:X,endPTS:z,startDTS:X,endDTS:z,type:"audio",hasAudio:!0,hasVideo:!1,nb:W};return this.isAudioContiguous=!0,Q}},e.remuxEmptyAudio=function(t,e,r,i){var n=t.inputTimeScale,a=n/(t.samplerate?t.samplerate:n),s=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,u=(null!==s?s:i.startDTS*n)+l,h=i.endDTS*n+l,d=1024*a,c=Math.ceil((h-u)/d),f=Mi.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(w.warn("[mp4-remuxer]: remux empty Audio"),f){for(var g=[],v=0;v4294967296;)t+=r;return t}function Yi(t,e,r,i){var n=t.samples.length;if(n){for(var a=t.inputTimeScale,s=0;s0;n||(i=It(e,["encv"])),i.forEach((function(t){It(n?t.subarray(28):t.subarray(78),["sinf"]).forEach((function(t){var e=_t(t);if(e){var i=e.subarray(8,24);i.some((function(t){return 0!==t}))||(w.log("[eme] Patching keyId in 'enc"+(n?"a":"v")+">sinf>>tenc' box: "+Tt(i)+" -> "+Tt(r)),e.set(r,8))}}))}))})),t}(t,i)),this.emitInitSegment=!0},e.generateInitSegment=function(t){var e=this.audioCodec,r=this.videoCodec;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);var i=this.initData=Ct(t);e||(e=Qi(i.audio,O)),r||(r=Qi(i.video,N));var n={};i.audio&&i.video?n.audiovideo={container:"video/mp4",codec:e+","+r,initSegment:t,id:"main"}:i.audio?n.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?n.video={container:"video/mp4",codec:r,initSegment:t,id:"main"}:w.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=n},e.remux=function(t,e,r,i,n,a){var s,o,l=this.initPTS,u=this.lastEndTime,h={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};E(u)||(u=this.lastEndTime=n||0);var d=e.samples;if(null==d||!d.length)return h;var c={initPTS:void 0,timescale:1},f=this.initData;if(null!=(s=f)&&s.length||(this.generateInitSegment(d),f=this.initData),null==(o=f)||!o.length)return w.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),h;this.emitInitSegment&&(c.tracks=this.initTracks,this.emitInitSegment=!1);var g=function(t,e){for(var r=0,i=0,n=0,a=It(t,["moof","traf"]),s=0;sn}(l,m,n,g)||c.timescale!==l.timescale&&a)&&(c.initPTS=m-n,l&&1===l.timescale&&w.warn("Adjusting initPTS by "+(c.initPTS-l.baseTime)),this.initPTS=l={baseTime:c.initPTS,timescale:1});var p=t?m-l.baseTime/l.timescale:u,y=p+g;!function(t,e,r){It(e,["moof","traf"]).forEach((function(e){It(e,["tfhd"]).forEach((function(i){var n=kt(i,4),a=t[n];if(a){var s=a.timescale||9e4;It(e,["tfdt"]).forEach((function(t){var e=t[0],i=kt(t,4);if(0===e)i-=r*s,Dt(t,4,i=Math.max(i,0));else{i*=Math.pow(2,32),i+=kt(t,8),i-=r*s,i=Math.max(i,0);var n=Math.floor(i/(Et+1)),a=Math.floor(i%(Et+1));Dt(t,4,n),Dt(t,8,a)}}))}}))}))}(f,d,l.baseTime/l.timescale),g>0?this.lastEndTime=y:(w.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var T=!!f.audio,S=!!f.video,L="";T&&(L+="audio"),S&&(L+="video");var R={data1:d,startPTS:p,startDTS:p,endPTS:y,endDTS:y,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return h.audio="audio"===R.type?R:void 0,h.video="audio"!==R.type?R:void 0,h.initSegment=c,h.id3=Yi(r,n,l,l),i.samples.length&&(h.text=Wi(i,n,l)),h},t}();function Qi(t,e){var r=null==t?void 0:t.codec;return r&&r.length>4?r:"hvc1"===r||"hev1"===r?"hvc1.1.6.L120.90":"av01"===r?"av01.0.04M.08":"avc1"===r||e===N?"avc1.42e01e":"mp4a.40.5"}try{ji=self.performance.now.bind(self.performance)}catch(t){w.debug("Unable to use Performance API on this environment"),ji="undefined"!=typeof self&&self.Date.now}var $i=[{demux:fi,remux:zi},{demux:Di,remux:Hi},{demux:di,remux:Hi},{demux:Fi,remux:Hi}],Ji=function(){function t(t,e,r,i,n){this.async=!1,this.observer=void 0,this.typeSupported=void 0,this.config=void 0,this.vendor=void 0,this.id=void 0,this.demuxer=void 0,this.remuxer=void 0,this.decrypter=void 0,this.probe=void 0,this.decryptionPromise=null,this.transmuxConfig=void 0,this.currentTransmuxState=void 0,this.observer=t,this.typeSupported=e,this.config=r,this.vendor=i,this.id=n}var e=t.prototype;return e.configure=function(t){this.transmuxConfig=t,this.decrypter&&this.decrypter.reset()},e.push=function(t,e,r,i){var n=this,a=r.transmuxing;a.executeStart=ji();var s=new Uint8Array(t),o=this.currentTransmuxState,l=this.transmuxConfig;i&&(this.currentTransmuxState=i);var u=i||o,h=u.contiguous,d=u.discontinuity,c=u.trackSwitch,f=u.accurateTimeOffset,g=u.timeOffset,v=u.initSegmentChange,m=l.audioCodec,p=l.videoCodec,y=l.defaultInitPts,T=l.duration,E=l.initSegmentData,A=function(t,e){var r=null;return t.byteLength>0&&null!=e&&null!=e.key&&null!==e.iv&&null!=e.method&&(r=e),r}(s,e);if(A&&"AES-128"===A.method){var k=this.getDecrypter();if(!k.isSync())return this.decryptionPromise=k.webCryptoDecrypt(s,A.key.buffer,A.iv.buffer).then((function(t){var e=n.push(t,null,r);return n.decryptionPromise=null,e})),this.decryptionPromise;var b=k.softwareDecrypt(s,A.key.buffer,A.iv.buffer);if(r.part>-1&&(b=k.flush()),!b)return a.executeEnd=ji(),Zi(r);s=new Uint8Array(b)}var D=this.needsProbing(d,c);if(D){var I=this.configureTransmuxer(s);if(I)return w.warn("[transmuxer] "+I.message),this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:R.FRAG_PARSING_ERROR,fatal:!1,error:I,reason:I.message}),a.executeEnd=ji(),Zi(r)}(d||c||v||D)&&this.resetInitSegment(E,m,p,T,e),(d||v||D)&&this.resetInitialTimestamp(y),h||this.resetContiguity();var C=this.transmux(s,A,g,f,r),_=this.currentTransmuxState;return _.contiguous=!0,_.discontinuity=!1,_.trackSwitch=!1,a.executeEnd=ji(),C},e.flush=function(t){var e=this,r=t.transmuxing;r.executeStart=ji();var i=this.decrypter,n=this.currentTransmuxState,a=this.decryptionPromise;if(a)return a.then((function(){return e.flush(t)}));var s=[],o=n.timeOffset;if(i){var l=i.flush();l&&s.push(this.push(l,null,t))}var u=this.demuxer,h=this.remuxer;if(!u||!h)return r.executeEnd=ji(),[Zi(t)];var d=u.flush(o);return tn(d)?d.then((function(r){return e.flushRemux(s,r,t),s})):(this.flushRemux(s,d,t),s)},e.flushRemux=function(t,e,r){var i=e.audioTrack,n=e.videoTrack,a=e.id3Track,s=e.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;w.log("[transmuxer.ts]: Flushed fragment "+r.sn+(r.part>-1?" p: "+r.part:"")+" of level "+r.level);var h=this.remuxer.remux(i,n,a,s,u,l,!0,this.id);t.push({remuxResult:h,chunkMeta:r}),r.transmuxing.executeEnd=ji()},e.resetInitialTimestamp=function(t){var e=this.demuxer,r=this.remuxer;e&&r&&(e.resetTimeStamp(t),r.resetTimeStamp(t))},e.resetContiguity=function(){var t=this.demuxer,e=this.remuxer;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())},e.resetInitSegment=function(t,e,r,i,n){var a=this.demuxer,s=this.remuxer;a&&s&&(a.resetInitSegment(t,e,r,i),s.resetInitSegment(t,e,r,n))},e.destroy=function(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)},e.transmux=function(t,e,r,i,n){return e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,r,i,n):this.transmuxUnencrypted(t,r,i,n)},e.transmuxUnencrypted=function(t,e,r,i){var n=this.demuxer.demux(t,e,!1,!this.config.progressive),a=n.audioTrack,s=n.videoTrack,o=n.id3Track,l=n.textTrack;return{remuxResult:this.remuxer.remux(a,s,o,l,e,r,!1,this.id),chunkMeta:i}},e.transmuxSampleAes=function(t,e,r,i,n){var a=this;return this.demuxer.demuxSampleAes(t,e,r).then((function(t){return{remuxResult:a.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,r,i,!1,a.id),chunkMeta:n}}))},e.configureTransmuxer=function(t){for(var e,r=this.config,i=this.observer,n=this.typeSupported,a=this.vendor,s=0,o=$i.length;s1&&l.id===(null==m?void 0:m.stats.chunkCount),L=!y&&(1===T||0===T&&(1===E||S&&E<=0)),R=self.performance.now();(y||T||0===n.stats.parsing.start)&&(n.stats.parsing.start=R),!a||!E&&L||(a.stats.parsing.start=R);var A=!(m&&(null==(h=n.initSegment)?void 0:h.url)===(null==(d=m.initSegment)?void 0:d.url)),k=new rn(p,L,o,y,g,A);if(!L||p||A){w.log("[transmuxer-interface, "+n.type+"]: Starting new transmux session for sn: "+l.sn+" p: "+l.part+" level: "+l.level+" id: "+l.id+"\n discontinuity: "+p+"\n trackSwitch: "+y+"\n contiguous: "+L+"\n accurateTimeOffset: "+o+"\n timeOffset: "+g+"\n initSegmentChange: "+A);var b=new en(r,i,e,s,u);this.configureTransmuxer(b)}if(this.frag=n,this.part=a,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:v,chunkMeta:l,state:k},t instanceof ArrayBuffer?[t]:[]);else if(f){var D=f.push(t,v,l,k);tn(D)?(f.async=!0,D.then((function(t){c.handleTransmuxComplete(t)})).catch((function(t){c.transmuxerError(t,l,"transmuxer-interface push error")}))):(f.async=!1,this.handleTransmuxComplete(D))}},r.flush=function(t){var e=this;t.transmuxing.start=self.performance.now();var r=this.transmuxer;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(r){var i=r.flush(t);tn(i)||r.async?(tn(i)||(i=Promise.resolve(i)),i.then((function(r){e.handleFlushResult(r,t)})).catch((function(r){e.transmuxerError(r,t,"transmuxer-interface flush error")}))):this.handleFlushResult(i,t)}},r.transmuxerError=function(t,e,r){this.hls&&(this.error=t,this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.FRAG_PARSING_ERROR,chunkMeta:e,fatal:!1,error:t,err:t,reason:r}))},r.handleFlushResult=function(t,e){var r=this;t.forEach((function(t){r.handleTransmuxComplete(t)})),this.onFlush(e)},r.onWorkerMessage=function(t){var e=t.data,r=this.hls;switch(e.event){case"init":var i,n=null==(i=this.workerContext)?void 0:i.objectURL;n&&self.URL.revokeObjectURL(n);break;case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":w[e.data.logType]&&w[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,r.trigger(e.event,e.data)}},r.configureTransmuxer=function(t){var e=this.transmuxer;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)},r.handleTransmuxComplete=function(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)},e}(),dn=function(){function t(t,e,r,i){this.config=void 0,this.media=null,this.fragmentTracker=void 0,this.hls=void 0,this.nudgeRetry=0,this.stallReported=!1,this.stalled=null,this.moved=!1,this.seeking=!1,this.config=t,this.media=e,this.fragmentTracker=r,this.hls=i}var e=t.prototype;return e.destroy=function(){this.media=null,this.hls=this.fragmentTracker=null},e.poll=function(t,e){var r=this.config,i=this.media,n=this.stalled;if(null!==i){var a=i.currentTime,s=i.seeking,o=this.seeking&&!s,l=!this.seeking&&s;if(this.seeking=s,a===t){if(l||o)this.stalled=null;else if(!(i.paused&&!s||i.ended||0===i.playbackRate)&&Ir.getBuffered(i).length){var u=Ir.bufferInfo(i,a,0),h=u.len>0,d=u.nextStart||0;if(h||d){if(s){var c=u.len>2,f=!d||e&&e.start<=a||d-a>2&&!this.fragmentTracker.getPartialFragment(a);if(c||f)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var g,v=Math.max(d,u.start||0)-a,m=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,p=(null==m||null==(g=m.details)?void 0:g.live)?2*m.details.targetduration:2,y=this.fragmentTracker.getPartialFragment(a);if(v>0&&(v<=p||y))return void this._trySkipBufferHole(y)}var T=self.performance.now();if(null!==n){var E=T-n;if(s||!(E>=250)||(this._reportStall(u),this.media)){var S=Ir.bufferInfo(i,a,r.maxBufferHole);this._tryFixBufferStall(S,E)}}else this.stalled=T}}}else if(this.moved=!0,null!==n){if(this.stallReported){var L=self.performance.now()-n;w.warn("playback not stuck anymore @"+a+", after "+Math.round(L)+"ms"),this.stallReported=!1}this.stalled=null,this.nudgeRetry=0}}},e._tryFixBufferStall=function(t,e){var r=this.config,i=this.fragmentTracker,n=this.media;if(null!==n){var a=n.currentTime,s=i.getPartialFragment(a);if(s&&(this._trySkipBufferHole(s)||!this.media))return;(t.len>r.maxBufferHole||t.nextStart&&t.nextStart-a1e3*r.highBufferWatchdogPeriod&&(w.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())}},e._reportStall=function(t){var e=this.hls,r=this.media;if(!this.stallReported&&r){this.stallReported=!0;var i=new Error("Playback stalling at @"+r.currentTime+" due to low buffer ("+JSON.stringify(t)+")");w.warn(i.message),e.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.BUFFER_STALLED_ERROR,fatal:!1,error:i,buffer:t.len})}},e._trySkipBufferHole=function(t){var e=this.config,r=this.hls,i=this.media;if(null===i)return 0;var n=i.currentTime,a=Ir.bufferInfo(i,n,0),s=n0&&a.len<1&&i.readyState<3,u=s-n;if(u>0&&(o||l)){if(u>e.maxBufferHole){var h=this.fragmentTracker,d=!1;if(0===n){var c=h.getAppendedFrag(0,ge);c&&s1?(i=0,this.bitrateTest=!0):i=r.nextAutoLevel),this.level=r.nextLoadLevel=i,this.loadedmetadata=!1}e>0&&-1===t&&(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e),this.state=Kr,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=Gr},r.stopLoad=function(){this._forceStartLoad=!1,t.prototype.stopLoad.call(this)},r.doTick=function(){switch(this.state){case $r:var t,e=this.levels,r=this.level,i=null==e||null==(t=e[r])?void 0:t.details;if(i&&(!i.live||this.levelLastLoaded===this.level)){if(this.waitForCdnTuneIn(i))break;this.state=Kr;break}if(this.hls.nextLoadLevel!==this.level){this.state=Kr;break}break;case Yr:var n,a=self.performance.now(),s=this.retryDate;(!s||a>=s||null!=(n=this.media)&&n.seeking)&&(this.resetStartWhenNotLoaded(this.level),this.state=Kr)}this.state===Kr&&this.doTickIdle(),this.onTickEnd()},r.onTickEnd=function(){t.prototype.onTickEnd.call(this),this.checkBuffer(),this.checkFragmentChanged()},r.doTickIdle=function(){var t=this.hls,e=this.levelLastLoaded,r=this.levels,i=this.media,n=t.config,a=t.nextLoadLevel;if(null!==e&&(i||!this.startFragRequested&&n.startFragPrefetch)&&(!this.altAudio||!this.audioOnly)&&null!=r&&r[a]){var s=r[a],o=this.getMainFwdBufferInfo();if(null!==o){var l=this.getLevelDetails();if(l&&this._streamEnded(o,l)){var u={};return this.altAudio&&(u.type="video"),this.hls.trigger(S.BUFFER_EOS,u),void(this.state=Xr)}t.loadLevel!==a&&-1===t.manualLevel&&this.log("Adapting to level "+a+" from level "+this.level),this.level=t.nextLoadLevel=a;var h=s.details;if(!h||this.state===$r||h.live&&this.levelLastLoaded!==a)return this.level=a,void(this.state=$r);var d=o.len,c=this.getMaxBufferLength(s.maxBitrate);if(!(d>=c)){this.backtrackFragment&&this.backtrackFragment.start>o.end&&(this.backtrackFragment=null);var f=this.backtrackFragment?this.backtrackFragment.start:o.end,g=this.getNextFragment(f,h);if(this.couldBacktrack&&!this.fragPrevious&&g&&"initSegment"!==g.sn&&this.fragmentTracker.getState(g)!==mr){var v,m=(null!=(v=this.backtrackFragment)?v:g).sn-h.startSN,p=h.fragments[m-1];p&&g.cc===p.cc&&(g=p,this.fragmentTracker.removeFragment(p))}else this.backtrackFragment&&o.len&&(this.backtrackFragment=null);if(g&&this.isLoopLoading(g,f)){if(!g.gap){var y=this.audioOnly&&!this.altAudio?O:N,T=(y===N?this.videoBuffer:this.mediaBuffer)||this.media;T&&this.afterBufferFlushed(T,y,ge)}g=this.getNextFragmentLoopLoading(g,h,o,ge,c)}g&&(!g.initSegment||g.initSegment.data||this.bitrateTest||(g=g.initSegment),this.loadFragment(g,s,f))}}}},r.loadFragment=function(e,r,i){var n=this.fragmentTracker.getState(e);this.fragCurrent=e,n===fr||n===vr?"initSegment"===e.sn?this._loadInitSegment(e,r):this.bitrateTest?(this.log("Fragment "+e.sn+" of level "+e.level+" is being downloaded to test bitrate and will not be buffered"),this._loadBitrateTestFrag(e,r)):(this.startFragRequested=!0,t.prototype.loadFragment.call(this,e,r,i)):this.clearTrackerIfNeeded(e)},r.getBufferedFrag=function(t){return this.fragmentTracker.getBufferedFrag(t,ge)},r.followingBufferedFrag=function(t){return t?this.getBufferedFrag(t.end+.5):null},r.immediateLevelSwitch=function(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)},r.nextLevelSwitch=function(){var t=this.levels,e=this.media;if(null!=e&&e.readyState){var r,i=this.getAppendedFrag(e.currentTime);i&&i.start>1&&this.flushMainBuffer(0,i.start-1);var n=this.getLevelDetails();if(null!=n&&n.live){var a=this.getMainFwdBufferInfo();if(!a||a.len<2*n.targetduration)return}if(!e.paused&&t){var s=t[this.hls.nextLoadLevel],o=this.fragLastKbps;r=o&&this.fragCurrent?this.fragCurrent.duration*s.maxBitrate/(1e3*o)+1:0}else r=0;var l=this.getBufferedFrag(e.currentTime+r);if(l){var u=this.followingBufferedFrag(l);if(u){this.abortCurrentFrag();var h=u.maxStartPTS?u.maxStartPTS:u.start,d=u.duration,c=Math.max(l.end,h+Math.min(Math.max(d-this.config.maxFragLookUpTolerance,.5*d),.75*d));this.flushMainBuffer(c,Number.POSITIVE_INFINITY)}}}},r.abortCurrentFrag=function(){var t=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,t&&(t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.state){case Hr:case Vr:case Yr:case jr:case qr:this.state=Kr}this.nextLoadPosition=this.getLoadPosition()},r.flushMainBuffer=function(e,r){t.prototype.flushMainBuffer.call(this,e,r,this.altAudio?"video":null)},r.onMediaAttached=function(e,r){t.prototype.onMediaAttached.call(this,e,r);var i=r.media;this.onvplaying=this.onMediaPlaying.bind(this),this.onvseeked=this.onMediaSeeked.bind(this),i.addEventListener("playing",this.onvplaying),i.addEventListener("seeked",this.onvseeked),this.gapController=new dn(this.config,i,this.fragmentTracker,this.hls)},r.onMediaDetaching=function(){var e=this.media;e&&this.onvplaying&&this.onvseeked&&(e.removeEventListener("playing",this.onvplaying),e.removeEventListener("seeked",this.onvseeked),this.onvplaying=this.onvseeked=null,this.videoBuffer=null),this.fragPlaying=null,this.gapController&&(this.gapController.destroy(),this.gapController=null),t.prototype.onMediaDetaching.call(this)},r.onMediaPlaying=function(){this.tick()},r.onMediaSeeked=function(){var t=this.media,e=t?t.currentTime:null;E(e)&&this.log("Media seeked to "+e.toFixed(3));var r=this.getMainFwdBufferInfo();null!==r&&0!==r.len?this.tick():this.warn('Main forward buffer length on "seeked" event '+(r?r.len:"empty")+")")},r.onManifestLoading=function(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(S.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=!1,this.startPosition=this.lastCurrentTime=0,this.levels=this.fragPlaying=this.backtrackFragment=null,this.altAudio=this.audioOnly=!1},r.onManifestParsed=function(t,e){var r,i,n,a=!1,s=!1;e.levels.forEach((function(t){(r=t.audioCodec)&&(-1!==r.indexOf("mp4a.40.2")&&(a=!0),-1!==r.indexOf("mp4a.40.5")&&(s=!0))})),this.audioCodecSwitch=a&&s&&!("function"==typeof(null==(n=Zr())||null==(i=n.prototype)?void 0:i.changeType)),this.audioCodecSwitch&&this.log("Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC"),this.levels=e.levels,this.startFragRequested=!1},r.onLevelLoading=function(t,e){var r=this.levels;if(r&&this.state===Kr){var i=r[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==e.level||this.waitForCdnTuneIn(i.details))&&(this.state=$r)}},r.onLevelLoaded=function(t,e){var r,i=this.levels,n=e.level,a=e.details,s=a.totalduration;if(i){this.log("Level "+n+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+", cc ["+a.startCC+", "+a.endCC+"] duration:"+s);var o=i[n],l=this.fragCurrent;!l||this.state!==Vr&&this.state!==Yr||l.level===e.level&&l.urlId===o.urlId||!l.loader||this.abortCurrentFrag();var u=0;if(a.live||null!=(r=o.details)&&r.live){if(this.checkLiveUpdate(a),a.deltaUpdateFailed)return;u=this.alignPlaylists(a,o.details)}if(o.details=a,this.levelLastLoaded=n,this.hls.trigger(S.LEVEL_UPDATED,{details:a,level:n}),this.state===$r){if(this.waitForCdnTuneIn(a))return;this.state=Kr}this.startFragRequested?a.live&&this.synchronizeToLiveEdge(a):this.setStartPosition(a,u),this.tick()}else this.warn("Levels were reset while loading level "+n)},r._handleFragmentLoadProgress=function(t){var e,r=t.frag,i=t.part,n=t.payload,a=this.levels;if(a){var s=a[r.level],o=s.details;if(!o)return this.warn("Dropping fragment "+r.sn+" of level "+r.level+" after level details were reset"),void this.fragmentTracker.removeFragment(r);var l=s.videoCodec,u=o.PTSKnown||!o.live,h=null==(e=r.initSegment)?void 0:e.data,d=this._getAudioCodec(s),c=this.transmuxer=this.transmuxer||new hn(this.hls,ge,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),f=i?i.index:-1,g=-1!==f,v=new wr(r.level,r.sn,r.stats.chunkCount,n.byteLength,f,g),m=this.initPTS[r.cc];c.push(n,h,d,l,r,i,o.totalduration,u,v,m)}else this.warn("Levels were reset while fragment load was in progress. Fragment "+r.sn+" of level "+r.level+" will not be buffered")},r.onAudioTrackSwitching=function(t,e){var r=this.altAudio;if(!e.url){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;var i=this.fragCurrent;i&&(this.log("Switching to main audio track, cancel main fragment load"),i.abortRequests(),this.fragmentTracker.removeFragment(i)),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();var n=this.hls;r&&(n.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:null}),this.fragmentTracker.removeAllFragments()),n.trigger(S.AUDIO_TRACK_SWITCHED,e)}},r.onAudioTrackSwitched=function(t,e){var r=e.id,i=!!this.hls.audioTracks[r].url;if(i){var n=this.videoBuffer;n&&this.mediaBuffer!==n&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=n)}this.altAudio=i,this.tick()},r.onBufferCreated=function(t,e){var r,i,n=e.tracks,a=!1;for(var s in n){var o=n[s];if("main"===o.id){if(i=s,r=o,"video"===s){var l=n[s];l&&(this.videoBuffer=l.buffer)}}else a=!0}a&&r?(this.log("Alternate track found, use "+i+".buffered to schedule main fragment loading"),this.mediaBuffer=r.buffer):this.mediaBuffer=this.media},r.onFragBuffered=function(t,e){var r=e.frag,i=e.part;if(!r||r.type===ge){if(this.fragContextChanged(r))return this.warn("Fragment "+r.sn+(i?" p: "+i.index:"")+" of level "+r.level+" finished buffering, but was aborted. state: "+this.state),void(this.state===qr&&(this.state=Kr));var n=i?i.stats:r.stats;this.fragLastKbps=Math.round(8*n.total/(n.buffering.end-n.loading.first)),"initSegment"!==r.sn&&(this.fragPrevious=r),this.fragBufferedComplete(r,i)}},r.onError=function(t,e){var r;if(e.fatal)this.state=zr;else switch(e.details){case R.FRAG_GAP:case R.FRAG_PARSING_ERROR:case R.FRAG_DECRYPT_ERROR:case R.FRAG_LOAD_ERROR:case R.FRAG_LOAD_TIMEOUT:case R.KEY_LOAD_ERROR:case R.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(ge,e);break;case R.LEVEL_LOAD_ERROR:case R.LEVEL_LOAD_TIMEOUT:case R.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==$r||(null==(r=e.context)?void 0:r.type)!==de||(this.state=Kr);break;case R.BUFFER_FULL_ERROR:if(!e.parent||"main"!==e.parent)return;this.reduceLengthAndFlushBuffer(e)&&this.flushMainBuffer(0,Number.POSITIVE_INFINITY);break;case R.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}},r.checkBuffer=function(){var t=this.media,e=this.gapController;if(t&&e&&t.readyState){if(this.loadedmetadata||!Ir.getBuffered(t).length){var r=this.state!==Kr?this.fragCurrent:null;e.poll(this.lastCurrentTime,r)}this.lastCurrentTime=t.currentTime}},r.onFragLoadEmergencyAborted=function(){this.state=Kr,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()},r.onBufferFlushed=function(t,e){var r=e.type;if(r!==O||this.audioOnly&&!this.altAudio){var i=(r===N?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(i,r,ge)}},r.onLevelsUpdated=function(t,e){this.levels=e.levels},r.swapAudioCodec=function(){this.audioCodecSwap=!this.audioCodecSwap},r.seekToStartPos=function(){var t=this.media;if(t){var e=t.currentTime,r=this.startPosition;if(r>=0&&e0&&(nT.cc;if(!1!==n.independent){var A=h.startPTS,k=h.endPTS,b=h.startDTS,D=h.endDTS;if(l)l.elementaryStreams[h.type]={startPTS:A,endPTS:k,startDTS:b,endDTS:D};else if(h.firstKeyFrame&&h.independent&&1===a.id&&!R&&(this.couldBacktrack=!0),h.dropped&&h.independent){var I=this.getMainFwdBufferInfo(),w=(I?I.end:this.getLoadPosition())+this.config.maxBufferHole,C=h.firstKeyFramePTS?h.firstKeyFramePTS:A;if(!L&&w1&&!1===t.seeking){var r=t.currentTime;if(Ir.isBuffered(t,r)?e=this.getAppendedFrag(r):Ir.isBuffered(t,r+.1)&&(e=this.getAppendedFrag(r+.1)),e){this.backtrackFragment=null;var i=this.fragPlaying,n=e.level;i&&e.sn===i.sn&&i.level===n&&e.urlId===i.urlId||(this.fragPlaying=e,this.hls.trigger(S.FRAG_CHANGED,{frag:e}),i&&i.level===n||this.hls.trigger(S.LEVEL_SWITCHED,{level:n}))}}},a(e,[{key:"nextLevel",get:function(){var t=this.nextBufferedFrag;return t?t.level:-1}},{key:"currentFrag",get:function(){var t=this.media;return t?this.fragPlaying||this.getAppendedFrag(t.currentTime):null}},{key:"currentProgramDateTime",get:function(){var t=this.media;if(t){var e=t.currentTime,r=this.currentFrag;if(r&&E(e)&&E(r.programDateTime)){var i=r.programDateTime+1e3*(e-r.start);return new Date(i)}}return null}},{key:"currentLevel",get:function(){var t=this.currentFrag;return t?t.level:-1}},{key:"nextBufferedFrag",get:function(){var t=this.currentFrag;return t?this.followingBufferedFrag(t):null}},{key:"forceStartLoad",get:function(){return this._forceStartLoad}}]),e}(Jr),fn=function(){function t(t,e,r){void 0===e&&(e=0),void 0===r&&(r=0),this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=r}var e=t.prototype;return e.sample=function(t,e){var r=Math.pow(this.alpha_,t);this.estimate_=e*(1-r)+r*this.estimate_,this.totalWeight_+=t},e.getTotalWeight=function(){return this.totalWeight_},e.getEstimate=function(){if(this.alpha_){var t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_},t}(),gn=function(){function t(t,e,r,i){void 0===i&&(i=100),this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=r,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new fn(t),this.fast_=new fn(e),this.defaultTTFB_=i,this.ttfb_=new fn(t)}var e=t.prototype;return e.update=function(t,e){var r=this.slow_,i=this.fast_,n=this.ttfb_;r.halfLife!==t&&(this.slow_=new fn(t,r.getEstimate(),r.getTotalWeight())),i.halfLife!==e&&(this.fast_=new fn(e,i.getEstimate(),i.getTotalWeight())),n.halfLife!==t&&(this.ttfb_=new fn(t,n.getEstimate(),n.getTotalWeight()))},e.sample=function(t,e){var r=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/r;this.fast_.sample(r,i),this.slow_.sample(r,i)},e.sampleTTFB=function(t){var e=t/1e3,r=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(r,Math.max(t,5))},e.canEstimate=function(){return this.fast_.getTotalWeight()>=this.minWeight_},e.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},e.getEstimateTTFB=function(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_},e.destroy=function(){},t}(),vn=function(){function t(t){this.hls=void 0,this.lastLevelLoadSec=0,this.lastLoadedFragLevel=0,this._nextAutoLevel=-1,this.timer=-1,this.onCheck=this._abandonRulesCheck.bind(this),this.fragCurrent=null,this.partCurrent=null,this.bitrateTestDelay=0,this.bwEstimator=void 0,this.hls=t;var e=t.config;this.bwEstimator=new gn(e.abrEwmaSlowVoD,e.abrEwmaFastVoD,e.abrEwmaDefaultEstimate),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this)},e.destroy=function(){this.unregisterListeners(),this.clearTimer(),this.hls=this.onCheck=null,this.fragCurrent=this.partCurrent=null},e.onFragLoading=function(t,e){var r,i=e.frag;this.ignoreFragment(i)||(this.fragCurrent=i,this.partCurrent=null!=(r=e.part)?r:null,this.clearTimer(),this.timer=self.setInterval(this.onCheck,100))},e.onLevelSwitching=function(t,e){this.clearTimer()},e.getTimeToLoadFrag=function(t,e,r,i){return t+r/e+(i?this.lastLevelLoadSec:0)},e.onLevelLoaded=function(t,e){var r=this.hls.config,i=e.stats,n=i.total,a=i.bwEstimate;E(n)&&E(a)&&(this.lastLevelLoadSec=8*n/a),e.details.live?this.bwEstimator.update(r.abrEwmaSlowLive,r.abrEwmaFastLive):this.bwEstimator.update(r.abrEwmaSlowVoD,r.abrEwmaFastVoD)},e._abandonRulesCheck=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.autoLevelEnabled,n=r.media;if(t&&n){var a=performance.now(),s=e?e.stats:t.stats,o=e?e.duration:t.duration,l=a-s.loading.start;if(s.aborted||s.loaded&&s.loaded===s.total||0===t.level)return this.clearTimer(),void(this._nextAutoLevel=-1);if(i&&!n.paused&&n.playbackRate&&n.readyState){var u=r.mainForwardBufferInfo;if(null!==u){var h=this.bwEstimator.getEstimateTTFB(),d=Math.abs(n.playbackRate);if(!(l<=Math.max(h,o/(2*d)*1e3))){var c=u.len/d;if(!(c>=2*o/d)){var f=s.loading.first?s.loading.first-s.loading.start:-1,g=s.loaded&&f>-1,v=this.bwEstimator.getEstimate(),m=r.levels,p=r.minAutoLevel,y=m[t.level],T=s.total||Math.max(s.loaded,Math.round(o*y.maxBitrate/8)),L=l-f;L<1&&g&&(L=Math.min(l,8*s.loaded/v));var R=g?1e3*s.loaded/L:0,A=R?(T-s.loaded)/R:8*T/v+h/1e3;if(!(A<=c)){var k,b=R?8*R:v,D=Number.POSITIVE_INFINITY;for(k=t.level-1;k>p;k--){var I=m[k].maxBitrate;if((D=this.getTimeToLoadFrag(h/1e3,b,o*I,!m[k].details))=A||D>10*o||(r.nextLoadLevel=k,g?this.bwEstimator.sample(l-Math.min(h,f),s.loaded):this.bwEstimator.sampleTTFB(l),this.clearTimer(),w.warn("[abr] Fragment "+t.sn+(e?" part "+e.index:"")+" of level "+t.level+" is loading too slowly;\n Time to underbuffer: "+c.toFixed(3)+" s\n Estimated load time for current fragment: "+A.toFixed(3)+" s\n Estimated load time for down switch fragment: "+D.toFixed(3)+" s\n TTFB estimate: "+f+"\n Current BW estimate: "+(E(v)?(v/1024).toFixed(3):"Unknown")+" Kb/s\n New BW estimate: "+(this.bwEstimator.getEstimate()/1024).toFixed(3)+" Kb/s\n Aborting and switching to level "+k),t.loader&&(this.fragCurrent=this.partCurrent=null,t.abortRequests()),r.trigger(S.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:e,stats:s}))}}}}}}},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part,n=i?i.stats:r.stats;if(r.type===ge&&this.bwEstimator.sampleTTFB(n.loading.first-n.loading.start),!this.ignoreFragment(r)){if(this.clearTimer(),this.lastLoadedFragLevel=r.level,this._nextAutoLevel=-1,this.hls.config.abrMaxWithRealBitrate){var a=i?i.duration:r.duration,s=this.hls.levels[r.level],o=(s.loaded?s.loaded.bytes:0)+n.loaded,l=(s.loaded?s.loaded.duration:0)+a;s.loaded={bytes:o,duration:l},s.realBitrate=Math.round(8*o/l)}if(r.bitrateTest){var u={stats:n,frag:r,part:i,id:r.type};this.onFragBuffered(S.FRAG_BUFFERED,u),r.bitrateTest=!1}}},e.onFragBuffered=function(t,e){var r=e.frag,i=e.part,n=null!=i&&i.stats.loaded?i.stats:r.stats;if(!n.aborted&&!this.ignoreFragment(r)){var a=n.parsing.end-n.loading.start-Math.min(n.loading.first-n.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(a,n.loaded),n.bwEstimate=this.bwEstimator.getEstimate(),r.bitrateTest?this.bitrateTestDelay=a/1e3:this.bitrateTestDelay=0}},e.ignoreFragment=function(t){return t.type!==ge||"initSegment"===t.sn},e.clearTimer=function(){self.clearInterval(this.timer)},e.getNextABRAutoLevel=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.maxAutoLevel,n=r.config,a=r.minAutoLevel,s=r.media,o=e?e.duration:t?t.duration:0,l=s&&0!==s.playbackRate?Math.abs(s.playbackRate):1,u=this.bwEstimator?this.bwEstimator.getEstimate():n.abrEwmaDefaultEstimate,h=r.mainForwardBufferInfo,d=(h?h.len:0)/l,c=this.findBestLevel(u,a,i,d,n.abrBandWidthFactor,n.abrBandWidthUpFactor);if(c>=0)return c;w.trace("[abr] "+(d?"rebuffering expected":"buffer is empty")+", finding optimal quality level");var f=o?Math.min(o,n.maxStarvationDelay):n.maxStarvationDelay,g=n.abrBandWidthFactor,v=n.abrBandWidthUpFactor;if(!d){var m=this.bitrateTestDelay;m&&(f=(o?Math.min(o,n.maxLoadingDelay):n.maxLoadingDelay)-m,w.trace("[abr] bitrate test took "+Math.round(1e3*m)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*f)+" ms"),g=v=1)}return c=this.findBestLevel(u,a,i,d+f,g,v),Math.max(c,0)},e.findBestLevel=function(t,e,r,i,n,a){for(var s,o=this.fragCurrent,l=this.partCurrent,u=this.lastLoadedFragLevel,h=this.hls.levels,d=h[u],c=!(null==d||null==(s=d.details)||!s.live),f=null==d?void 0:d.codecSet,g=l?l.duration:o?o.duration:0,v=this.bwEstimator.getEstimateTTFB()/1e3,m=e,p=-1,y=r;y>=e;y--){var T=h[y];if(!T||f&&T.codecSet!==f)T&&(m=Math.min(y,m),p=Math.max(y,p));else{-1!==p&&w.trace("[abr] Skipped level(s) "+m+"-"+p+' with CODECS:"'+h[p].attrs.CODECS+'"; not compatible with "'+d.attrs.CODECS+'"');var S=T.details,L=(l?null==S?void 0:S.partTarget:null==S?void 0:S.averagetargetduration)||g,R=void 0;R=y<=u?n*t:a*t;var A=h[y].maxBitrate,k=this.getTimeToLoadFrag(v,R,A*L,void 0===S);if(w.trace("[abr] level:"+y+" adjustedbw-bitrate:"+Math.round(R-A)+" avgDuration:"+L.toFixed(1)+" maxFetchDuration:"+i.toFixed(1)+" fetchDuration:"+k.toFixed(1)),R>A&&(0===k||!E(k)||c&&!this.bitrateTestDelay||kMath.max(t,r)&&i[t].loadError<=i[r].loadError)return t}return-1!==t&&(r=Math.min(t,r)),r},set:function(t){this._nextAutoLevel=t}}]),t}(),mn=function(){function t(){this.chunks=[],this.dataLength=0}var e=t.prototype;return e.push=function(t){this.chunks.push(t),this.dataLength+=t.length},e.flush=function(){var t,e=this.chunks,r=this.dataLength;return e.length?(t=1===e.length?e[0]:function(t,e){for(var r=new Uint8Array(e),i=0,n=0;n0&&-1===t?(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e,this.state=Kr):(this.loadedmetadata=!1,this.state=Wr),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()},r.doTick=function(){switch(this.state){case Kr:this.doTickIdle();break;case Wr:var e,r=this.levels,i=this.trackId,n=null==r||null==(e=r[i])?void 0:e.details;if(n){if(this.waitForCdnTuneIn(n))break;this.state=Qr}break;case Yr:var a,s=performance.now(),o=this.retryDate;(!o||s>=o||null!=(a=this.media)&&a.seeking)&&(this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded(this.trackId),this.state=Kr);break;case Qr:var l=this.waitingData;if(l){var u=l.frag,h=l.part,d=l.cache,c=l.complete;if(void 0!==this.initPTS[u.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=Vr;var f={frag:u,part:h,payload:d.flush(),networkDetails:null};this._handleFragmentLoadProgress(f),c&&t.prototype._handleFragmentLoadComplete.call(this,f)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log("Waiting fragment cc ("+u.cc+") cancelled because video is at cc "+this.videoTrackCC),this.clearWaitingFragment();else{var g=this.getLoadPosition(),v=Ir.bufferInfo(this.mediaBuffer,g,this.config.maxBufferHole);Je(v.end,this.config.maxFragLookUpTolerance,u)<0&&(this.log("Waiting fragment cc ("+u.cc+") @ "+u.start+" cancelled because another fragment at "+v.end+" is needed"),this.clearWaitingFragment())}}else this.state=Kr}this.onTickEnd()},r.clearWaitingFragment=function(){var t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=Kr)},r.resetLoadingState=function(){this.clearWaitingFragment(),t.prototype.resetLoadingState.call(this)},r.onTickEnd=function(){var t=this.media;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)},r.doTickIdle=function(){var t=this.hls,e=this.levels,r=this.media,i=this.trackId,n=t.config;if(null!=e&&e[i]&&(r||!this.startFragRequested&&n.startFragPrefetch)){var a=e[i],s=a.details;if(!s||s.live&&this.levelLastLoaded!==i||this.waitForCdnTuneIn(s))this.state=Wr;else{var o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,O,ve));var l=this.getFwdBufferInfo(o,ve);if(null!==l){var u=this.bufferedTrack,h=this.switchingTrack;if(!h&&this._streamEnded(l,s))return t.trigger(S.BUFFER_EOS,{type:"audio"}),void(this.state=Xr);var d=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,ge),c=l.len,f=this.getMaxBufferLength(null==d?void 0:d.len);if(!(c>=f)||h){var g=s.fragments[0].start,v=l.end;if(h&&r){var m=this.getLoadPosition();u&&h.attrs!==u.attrs&&(v=m),s.PTSKnown&&mg||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),r.currentTime=g+.05)}var p=this.getNextFragment(v,s),y=!1;if(p&&this.isLoopLoading(p,v)&&(y=!!p.gap,p=this.getNextFragmentLoopLoading(p,s,l,ge,f)),p){var T=d&&p.start>d.end+s.targetduration;if(T||(null==d||!d.len)&&l.len){var E=this.getAppendedFrag(p.start,ge);if(null===E)return;if(y||(y=!!E.gap||!!T&&0===d.len),T&&!y||y&&l.nextStart&&l.nextStart=e.length)this.warn("Invalid id passed to audio-track controller");else{this.clearTimer();var r=this.currentTrack;e[this.trackId];var n=e[t],a=n.groupId,s=n.name;if(this.log("Switching to audio-track "+t+' "'+s+'" lang:'+n.lang+" group:"+a),this.trackId=t,this.currentTrack=n,this.selectDefaultTrack=!1,this.hls.trigger(S.AUDIO_TRACK_SWITCHING,i({},n)),!n.details||n.details.live){var o=this.switchParams(n.url,null==r?void 0:r.details);this.loadPlaylist(o)}}},r.selectInitialTrack=function(){var t=this.tracksInGroup,e=this.findTrackId(this.currentTrack)|this.findTrackId(null);if(-1!==e)this.setAudioTrack(e);else{var r=new Error("No track found for running audio group-ID: "+this.groupId+" track count: "+t.length);this.warn(r.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:r})}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=0;r=n[o].start&&s<=n[o].end){a=n[o];break}var l=r.start+r.duration;a?a.end=l:(a={start:s,end:l},n.push(a)),this.fragmentTracker.fragBuffered(r)}}},r.onBufferFlushing=function(t,e){var r=e.startOffset,i=e.endOffset;if(0===r&&i!==Number.POSITIVE_INFINITY){var n=i-1;if(n<=0)return;e.endOffsetSubtitles=Math.max(0,n),this.tracksBuffered.forEach((function(t){for(var e=0;e=s.length||n!==a)&&o){this.mediaBuffer=this.mediaBufferTimeRanges;var l=0;if(i.live||null!=(r=o.details)&&r.live){var u=this.mainDetails;if(i.deltaUpdateFailed||!u)return;var h=u.fragments[0];o.details?0===(l=this.alignPlaylists(i,o.details))&&h&&He(i,l=h.start):i.hasProgramDateTime&&u.hasProgramDateTime?(Fr(i,u),l=i.fragments[0].start):h&&He(i,l=h.start)}o.details=i,this.levelLastLoaded=n,this.startFragRequested||!this.mainDetails&&i.live||this.setStartPosition(o.details,l),this.tick(),i.live&&!this.fragCurrent&&this.media&&this.state===Kr&&($e(null,i.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0))}}},r._handleFragmentLoadComplete=function(t){var e=this,r=t.frag,i=t.payload,n=r.decryptdata,a=this.hls;if(!this.fragContextChanged(r)&&i&&i.byteLength>0&&n&&n.key&&n.iv&&"AES-128"===n.method){var s=performance.now();this.decrypter.decrypt(new Uint8Array(i),n.key.buffer,n.iv.buffer).catch((function(t){throw a.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:r}),t})).then((function(t){var e=performance.now();a.trigger(S.FRAG_DECRYPTED,{frag:r,payload:t,stats:{tstart:s,tdecrypt:e}})})).catch((function(t){e.warn(t.name+": "+t.message),e.state=Kr}))}},r.doTick=function(){if(this.media){if(this.state===Kr){var t=this.currentTrackId,e=this.levels,r=e[t];if(!e.length||!r||!r.details)return;var i=this.config,n=this.getLoadPosition(),a=Ir.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],n,i.maxBufferHole),s=a.end,o=a.len,l=this.getFwdBufferInfo(this.media,ge),u=r.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+u.levelTargetDuration)return;var h=u.fragments,d=h.length,c=u.edge,f=null,g=this.fragPrevious;if(sc-v?0:v;!(f=$e(g,h,Math.max(h[0].start,s),m))&&g&&g.start>>=0)>i-1)throw new DOMException("Failed to execute '"+e+"' on 'TimeRanges': The index provided ("+r+") is greater than the maximum bound ("+i+")");return t[r][e]};this.buffered={get length(){return t.length},end:function(r){return e("end",r,t.length)},start:function(r){return e("start",r,t.length)}}},Rn=function(t){function e(e){var r;return(r=t.call(this,e,"[subtitle-track-controller]")||this).media=null,r.tracks=[],r.groupId=null,r.tracksInGroup=[],r.trackId=-1,r.selectDefaultTrack=!0,r.queuedDefaultTrack=-1,r.trackChangeListener=function(){return r.onTextTracksChanged()},r.asyncPollTrackChange=function(){return r.pollTrackChange(0)},r.useTextTrackPolling=!1,r.subtitlePollingInterval=-1,r._subtitleDisplay=!0,r.registerListeners(),r}l(e,t);var r=e.prototype;return r.destroy=function(){this.unregisterListeners(),this.tracks.length=0,this.tracksInGroup.length=0,this.trackChangeListener=this.asyncPollTrackChange=null,t.prototype.destroy.call(this)},r.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.LEVEL_LOADING,this.onLevelLoading,this),t.on(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(S.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.on(S.ERROR,this.onError,this)},r.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.LEVEL_LOADING,this.onLevelLoading,this),t.off(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(S.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.off(S.ERROR,this.onError,this)},r.onMediaAttached=function(t,e){this.media=e.media,this.media&&(this.queuedDefaultTrack>-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))},r.pollTrackChange=function(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.trackChangeListener,t)},r.onMediaDetaching=function(){this.media&&(self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId),An(this.media.textTracks).forEach((function(t){Le(t)})),this.subtitleTrack=-1,this.media=null)},r.onManifestLoading=function(){this.tracks=[],this.groupId=null,this.tracksInGroup=[],this.trackId=-1,this.selectDefaultTrack=!0},r.onManifestParsed=function(t,e){this.tracks=e.subtitleTracks},r.onSubtitleTrackLoaded=function(t,e){var r=e.id,i=e.details,n=this.trackId,a=this.tracksInGroup[n];if(a){var s=a.details;a.details=e.details,this.log("subtitle track "+r+" loaded ["+i.startSN+"-"+i.endSN+"]"),r===this.trackId&&this.playlistLoaded(r,e,s)}else this.warn("Invalid subtitle track id "+r)},r.onLevelLoading=function(t,e){this.switchLevel(e.level)},r.onLevelSwitching=function(t,e){this.switchLevel(e.level)},r.switchLevel=function(t){var e=this.hls.levels[t];if(null!=e&&e.textGroupIds){var r=e.textGroupIds[e.urlId],i=this.tracksInGroup?this.tracksInGroup[this.trackId]:void 0;if(this.groupId!==r){var n=this.tracks.filter((function(t){return!r||t.groupId===r}));this.tracksInGroup=n;var a=this.findTrackId(null==i?void 0:i.name)||this.findTrackId();this.groupId=r||null;var s={subtitleTracks:n};this.log("Updating subtitle tracks, "+n.length+' track(s) found in "'+r+'" group-id'),this.hls.trigger(S.SUBTITLE_TRACKS_UPDATED,s),-1!==a&&this.setSubtitleTrack(a,i)}else this.shouldReloadPlaylist(i)&&this.setSubtitleTrack(this.trackId,i)}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=0;r=i.length)){this.clearTimer();var n=i[t];if(this.log("Switching to subtitle-track "+t+(n?' "'+n.name+'" lang:'+n.lang+" group:"+n.groupId:"")),this.trackId=t,n){var a=n.id,s=n.groupId,o=void 0===s?"":s,l=n.name,u=n.type,h=n.url;this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:a,groupId:o,name:l,type:u,url:h});var d=this.switchParams(n.url,null==e?void 0:e.details);this.loadPlaylist(d)}else this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:t})}}else this.queuedDefaultTrack=t},r.onTextTracksChanged=function(){if(this.useTextTrackPolling||self.clearInterval(this.subtitlePollingInterval),this.media&&this.hls.config.renderTextTracksNatively){for(var t=-1,e=An(this.media.textTracks),r=0;r-1&&this.toggleTrackModes(this.trackId)}},{key:"subtitleTracks",get:function(){return this.tracksInGroup}},{key:"subtitleTrack",get:function(){return this.trackId},set:function(t){this.selectDefaultTrack=!1;var e=this.tracksInGroup?this.tracksInGroup[this.trackId]:void 0;this.setSubtitleTrack(t,e)}}]),e}(ur);function An(t){for(var e=[],r=0;r "+t.src+")")},this.hls=t,this._initSourceBuffer(),this.registerListeners()}var e=t.prototype;return e.hasSourceTypes=function(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0},e.destroy=function(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null},e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.BUFFER_RESET,this.onBufferReset,this),t.on(S.BUFFER_APPENDING,this.onBufferAppending,this),t.on(S.BUFFER_CODECS,this.onBufferCodecs,this),t.on(S.BUFFER_EOS,this.onBufferEos,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(S.FRAG_PARSED,this.onFragParsed,this),t.on(S.FRAG_CHANGED,this.onFragChanged,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.BUFFER_RESET,this.onBufferReset,this),t.off(S.BUFFER_APPENDING,this.onBufferAppending,this),t.off(S.BUFFER_CODECS,this.onBufferCodecs,this),t.off(S.BUFFER_EOS,this.onBufferEos,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(S.FRAG_PARSED,this.onFragParsed,this),t.off(S.FRAG_CHANGED,this.onFragChanged,this)},e._initSourceBuffer=function(){this.sourceBuffer={},this.operationQueue=new kn(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.lastMpegAudioChunk=null},e.onManifestLoading=function(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null},e.onManifestParsed=function(t,e){var r=2;(e.audio&&!e.video||!e.altAudio)&&(r=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=r,w.log(this.bufferCodecEventsExpected+" bufferCodec event(s) expected")},e.onMediaAttaching=function(t,e){var r=this.media=e.media;if(r&&bn){var i=this.mediaSource=new bn;i.addEventListener("sourceopen",this._onMediaSourceOpen),i.addEventListener("sourceended",this._onMediaSourceEnded),i.addEventListener("sourceclose",this._onMediaSourceClose),r.src=self.URL.createObjectURL(i),this._objectUrl=r.src,r.addEventListener("emptied",this._onMediaEmptied)}},e.onMediaDetaching=function(){var t=this.media,e=this.mediaSource,r=this._objectUrl;if(e){if(w.log("[buffer-controller]: media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){w.warn("[buffer-controller]: onMediaDetaching: "+t.message+" while calling endOfStream")}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),t&&(t.removeEventListener("emptied",this._onMediaEmptied),r&&self.URL.revokeObjectURL(r),t.src===r?(t.removeAttribute("src"),t.load()):w.warn("[buffer-controller]: media.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(S.MEDIA_DETACHED,void 0)},e.onBufferReset=function(){var t=this;this.getSourceBufferTypes().forEach((function(e){var r=t.sourceBuffer[e];try{r&&(t.removeBufferListeners(e),t.mediaSource&&t.mediaSource.removeSourceBuffer(r),t.sourceBuffer[e]=void 0)}catch(t){w.warn("[buffer-controller]: Failed to reset the "+e+" buffer",t)}})),this._initSourceBuffer()},e.onBufferCodecs=function(t,e){var r=this,i=this.getSourceBufferTypes().length;Object.keys(e).forEach((function(t){if(i){var n=r.tracks[t];if(n&&"function"==typeof n.buffer.changeType){var a=e[t],s=a.id,o=a.codec,l=a.levelCodec,u=a.container,h=a.metadata,d=(n.levelCodec||n.codec).replace(Dn,"$1"),c=(l||o).replace(Dn,"$1");if(d!==c){var f=u+";codecs="+(l||o);r.appendChangeType(t,f),w.log("[buffer-controller]: switching codec "+d+" to "+c),r.tracks[t]={buffer:n.buffer,codec:o,container:u,levelCodec:l,metadata:h,id:s}}}}else r.pendingTracks[t]=e[t]})),i||(this.bufferCodecEventsExpected=Math.max(this.bufferCodecEventsExpected-1,0),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks())},e.appendChangeType=function(t,e){var r=this,i=this.operationQueue,n={execute:function(){var n=r.sourceBuffer[t];n&&(w.log("[buffer-controller]: changing "+t+" sourceBuffer type to "+e),n.changeType(e)),i.shiftAndExecuteNext(t)},onStart:function(){},onComplete:function(){},onError:function(e){w.warn("[buffer-controller]: Failed to change "+t+" SourceBuffer type",e)}};i.append(n,t)},e.onBufferAppending=function(t,e){var r=this,i=this.hls,n=this.operationQueue,a=this.tracks,s=e.data,o=e.type,l=e.frag,u=e.part,h=e.chunkMeta,d=h.buffering[o],c=self.performance.now();d.start=c;var f=l.stats.buffering,g=u?u.stats.buffering:null;0===f.start&&(f.start=c),g&&0===g.start&&(g.start=c);var v=a.audio,m=!1;"audio"===o&&"audio/mpeg"===(null==v?void 0:v.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);var p=l.start,y={execute:function(){if(d.executeStart=self.performance.now(),m){var t=r.sourceBuffer[o];if(t){var e=p-t.timestampOffset;Math.abs(e)>=.1&&(w.log("[buffer-controller]: Updating audio SourceBuffer timestampOffset to "+p+" (delta: "+e+") sn: "+l.sn+")"),t.timestampOffset=p)}}r.appendExecutor(s,o)},onStart:function(){},onComplete:function(){var t=self.performance.now();d.executeEnd=d.end=t,0===f.first&&(f.first=t),g&&0===g.first&&(g.first=t);var e=r.sourceBuffer,i={};for(var n in e)i[n]=Ir.getBuffered(e[n]);r.appendError=0,r.hls.trigger(S.BUFFER_APPENDED,{type:o,frag:l,part:u,chunkMeta:h,parent:l.type,timeRanges:i})},onError:function(t){w.error("[buffer-controller]: Error encountered while trying to append to the "+o+" SourceBuffer",t);var e={type:L.MEDIA_ERROR,parent:l.type,details:R.BUFFER_APPEND_ERROR,frag:l,part:u,chunkMeta:h,error:t,err:t,fatal:!1};t.code===DOMException.QUOTA_EXCEEDED_ERR?e.details=R.BUFFER_FULL_ERROR:(r.appendError++,e.details=R.BUFFER_APPEND_ERROR,r.appendError>i.config.appendErrorMaxRetry&&(w.error("[buffer-controller]: Failed "+i.config.appendErrorMaxRetry+" times to append segment in sourceBuffer"),e.fatal=!0)),i.trigger(S.ERROR,e)}};n.append(y,o)},e.onBufferFlushing=function(t,e){var r=this,i=this.operationQueue,n=function(t){return{execute:r.removeExecutor.bind(r,t,e.startOffset,e.endOffset),onStart:function(){},onComplete:function(){r.hls.trigger(S.BUFFER_FLUSHED,{type:t})},onError:function(e){w.warn("[buffer-controller]: Failed to remove from "+t+" SourceBuffer",e)}}};e.type?i.append(n(e.type),e.type):this.getSourceBufferTypes().forEach((function(t){i.append(n(t),t)}))},e.onFragParsed=function(t,e){var r=this,i=e.frag,n=e.part,a=[],s=n?n.elementaryStreams:i.elementaryStreams;s[U]?a.push("audiovideo"):(s[O]&&a.push("audio"),s[N]&&a.push("video")),0===a.length&&w.warn("Fragments must have at least one ElementaryStreamType set. type: "+i.type+" level: "+i.level+" sn: "+i.sn),this.blockBuffers((function(){var t=self.performance.now();i.stats.buffering.end=t,n&&(n.stats.buffering.end=t);var e=n?n.stats:i.stats;r.hls.trigger(S.FRAG_BUFFERED,{frag:i,part:n,stats:e,id:i.type})}),a)},e.onFragChanged=function(t,e){this.flushBackBuffer()},e.onBufferEos=function(t,e){var r=this;this.getSourceBufferTypes().reduce((function(t,i){var n=r.sourceBuffer[i];return!n||e.type&&e.type!==i||(n.ending=!0,n.ended||(n.ended=!0,w.log("[buffer-controller]: "+i+" sourceBuffer now EOS"))),t&&!(n&&!n.ended)}),!0)&&(w.log("[buffer-controller]: Queueing mediaSource.endOfStream()"),this.blockBuffers((function(){r.getSourceBufferTypes().forEach((function(t){var e=r.sourceBuffer[t];e&&(e.ending=!1)}));var t=r.mediaSource;t&&"open"===t.readyState?(w.log("[buffer-controller]: Calling mediaSource.endOfStream()"),t.endOfStream()):t&&w.info("[buffer-controller]: Could not call mediaSource.endOfStream(). mediaSource.readyState: "+t.readyState)})))},e.onLevelUpdated=function(t,e){var r=e.details;r.fragments.length&&(this.details=r,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())},e.flushBackBuffer=function(){var t=this.hls,e=this.details,r=this.media,i=this.sourceBuffer;if(r&&null!==e){var n=this.getSourceBufferTypes();if(n.length){var a=e.live&&null!==t.config.liveBackBufferLength?t.config.liveBackBufferLength:t.config.backBufferLength;if(E(a)&&!(a<0)){var s=r.currentTime,o=e.levelTargetDuration,l=Math.max(a,o),u=Math.floor(s/o)*o-l;n.forEach((function(r){var n=i[r];if(n){var a=Ir.getBuffered(n);if(a.length>0&&u>a.start(0)){if(t.trigger(S.BACK_BUFFER_REACHED,{bufferEnd:u}),e.live)t.trigger(S.LIVE_BACK_BUFFER_REACHED,{bufferEnd:u});else if(n.ended&&a.end(a.length-1)-s<2*o)return void w.info("[buffer-controller]: Cannot flush "+r+" back buffer while SourceBuffer is in ended state");t.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:u,type:r})}}}))}}}},e.updateMediaElementDuration=function(){if(this.details&&this.media&&this.mediaSource&&"open"===this.mediaSource.readyState){var t=this.details,e=this.hls,r=this.media,i=this.mediaSource,n=t.fragments[0].start+t.totalduration,a=r.duration,s=E(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(w.log("[buffer-controller]: Media Source duration is set to Infinity"),i.duration=1/0,this.updateSeekableRange(t)):(n>s&&n>a||!E(a))&&(w.log("[buffer-controller]: Updating Media Source duration to "+n.toFixed(3)),i.duration=n)}},e.updateSeekableRange=function(t){var e=this.mediaSource,r=t.fragments;if(r.length&&t.live&&null!=e&&e.setLiveSeekableRange){var i=Math.max(0,r[0].start),n=Math.max(i,i+t.totalduration);e.setLiveSeekableRange(i,n)}},e.checkPendingTracks=function(){var t=this.bufferCodecEventsExpected,e=this.operationQueue,r=this.pendingTracks,i=Object.keys(r).length;if(i&&!t||2===i){this.createSourceBuffers(r),this.pendingTracks={};var n=this.getSourceBufferTypes();if(n.length)this.hls.trigger(S.BUFFER_CREATED,{tracks:this.tracks}),n.forEach((function(t){e.executeNext(t)}));else{var a=new Error("could not create source buffer for media codec(s)");this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:a,reason:a.message})}}},e.createSourceBuffers=function(t){var e=this.sourceBuffer,r=this.mediaSource;if(!r)throw Error("createSourceBuffers called when mediaSource was null");for(var i in t)if(!e[i]){var n=t[i];if(!n)throw Error("source buffer exists for track "+i+", however track does not");var a=n.levelCodec||n.codec,s=n.container+";codecs="+a;w.log("[buffer-controller]: creating sourceBuffer("+s+")");try{var o=e[i]=r.addSourceBuffer(s),l=i;this.addBufferListener(l,"updatestart",this._onSBUpdateStart),this.addBufferListener(l,"updateend",this._onSBUpdateEnd),this.addBufferListener(l,"error",this._onSBUpdateError),this.tracks[i]={buffer:o,codec:a,container:n.container,levelCodec:n.levelCodec,metadata:n.metadata,id:n.id}}catch(t){w.error("[buffer-controller]: error while trying to add sourceBuffer: "+t.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,mimeType:s})}}},e._onSBUpdateStart=function(t){this.operationQueue.current(t).onStart()},e._onSBUpdateEnd=function(t){var e=this.operationQueue;e.current(t).onComplete(),e.shiftAndExecuteNext(t)},e._onSBUpdateError=function(t,e){var r=new Error(t+" SourceBuffer error");w.error("[buffer-controller]: "+r,e),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:R.BUFFER_APPENDING_ERROR,error:r,fatal:!1});var i=this.operationQueue.current(t);i&&i.onError(e)},e.removeExecutor=function(t,e,r){var i=this.media,n=this.mediaSource,a=this.operationQueue,s=this.sourceBuffer[t];if(!i||!n||!s)return w.warn("[buffer-controller]: Attempting to remove from the "+t+" SourceBuffer, but it does not exist"),void a.shiftAndExecuteNext(t);var o=E(i.duration)?i.duration:1/0,l=E(n.duration)?n.duration:1/0,u=Math.max(0,e),h=Math.min(r,o,l);h>u&&!s.ending?(s.ended=!1,w.log("[buffer-controller]: Removing ["+u+","+h+"] from the "+t+" SourceBuffer"),s.remove(u,h)):a.shiftAndExecuteNext(t)},e.appendExecutor=function(t,e){var r=this.operationQueue,i=this.sourceBuffer[e];if(!i)return w.warn("[buffer-controller]: Attempting to append to the "+e+" SourceBuffer, but it does not exist"),void r.shiftAndExecuteNext(e);i.ended=!1,i.appendBuffer(t)},e.blockBuffers=function(t,e){var r=this;if(void 0===e&&(e=this.getSourceBufferTypes()),!e.length)return w.log("[buffer-controller]: Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);var i=this.operationQueue,n=e.map((function(t){return i.appendBlocker(t)}));Promise.all(n).then((function(){t(),e.forEach((function(t){var e=r.sourceBuffer[t];null!=e&&e.updating||i.shiftAndExecuteNext(t)}))}))},e.getSourceBufferTypes=function(){return Object.keys(this.sourceBuffer)},e.addBufferListener=function(t,e,r){var i=this.sourceBuffer[t];if(i){var n=r.bind(this,t);this.listeners[t].push({event:e,listener:n}),i.addEventListener(e,n)}},e.removeBufferListeners=function(t){var e=this.sourceBuffer[t];e&&this.listeners[t].forEach((function(t){e.removeEventListener(t.event,t.listener)}))},t}(),wn={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},Cn=function(t){var e=t;return wn.hasOwnProperty(t)&&(e=wn[t]),String.fromCharCode(e)},_n=15,Pn=100,xn={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},Fn={17:2,18:4,21:6,22:8,23:10,19:13,20:15},Mn={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},On={25:2,26:4,29:6,30:8,31:10,27:13,28:15},Nn=["white","green","blue","cyan","red","yellow","magenta","black","transparent"],Un=function(){function t(){this.time=null,this.verboseLevel=0}return t.prototype.log=function(t,e){if(this.verboseLevel>=t){var r="function"==typeof e?e():e;w.log(this.time+" ["+t+"] "+r)}},t}(),Bn=function(t){for(var e=[],r=0;rPn&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=Pn)},e.moveCursor=function(t){var e=this.pos+t;if(t>1)for(var r=this.pos+1;r=144&&this.backSpace();var r=Cn(t);this.pos>=Pn?this.logger.log(0,(function(){return"Cannot insert "+t.toString(16)+" ("+r+") at position "+e.pos+". Skipping it!"})):(this.chars[this.pos].setChar(r,this.currPenState),this.moveCursor(1))},e.clearFromPos=function(t){var e;for(e=t;e0&&(r=t?"["+e.join(" | ")+"]":e.join("\n")),r},e.getTextAndFormat=function(){return this.rows},t}(),Yn=function(){function t(t,e,r){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new Vn(r),this.nonDisplayedMemory=new Vn(r),this.lastOutputScreen=new Vn(r),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=r}var e=t.prototype;return e.reset=function(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null},e.getHandler=function(){return this.outputFilter},e.setHandler=function(t){this.outputFilter=t},e.setPAC=function(t){this.writeScreen.setPAC(t)},e.setBkgData=function(t){this.writeScreen.setBkgData(t)},e.setMode=function(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(function(){return"MODE="+t})),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=t)},e.insertChars=function(t){for(var e=this,r=0;r=46,e.italics)e.foreground="white";else{var r=Math.floor(t/2)-16;e.foreground=["white","green","blue","cyan","red","yellow","magenta"][r]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)},e.outputDataUpdate=function(t){void 0===t&&(t=!1);var e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))},e.cueSplitAtTime=function(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))},t}(),Wn=function(){function t(t,e,r){this.channels=void 0,this.currentChannel=0,this.cmdHistory=void 0,this.logger=void 0;var i=new Un;this.channels=[null,new Yn(t,e,i),new Yn(t+1,r,i)],this.cmdHistory={a:null,b:null},this.logger=i}var e=t.prototype;return e.getHandler=function(t){return this.channels[t].getHandler()},e.setHandler=function(t,e){this.channels[t].setHandler(e)},e.addData=function(t,e){var r,i,n,a=!1;this.logger.time=t;for(var s=0;s ("+Bn([i,n])+")"),(r=this.parseCmd(i,n))||(r=this.parseMidrow(i,n)),r||(r=this.parsePAC(i,n)),r||(r=this.parseBackgroundAttributes(i,n)),!r&&(a=this.parseChars(i,n))){var o=this.currentChannel;o&&o>0?this.channels[o].insertChars(a):this.logger.log(2,"No channel found yet. TEXT-MODE?")}r||a||this.logger.log(2,"Couldn't parse cleaned data "+Bn([i,n])+" orig: "+Bn([e[s],e[s+1]]))}},e.parseCmd=function(t,e){var r=this.cmdHistory;if(!((20===t||28===t||21===t||29===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=33&&e<=35))return!1;if(qn(t,e,r))return jn(null,null,r),this.logger.log(3,"Repeated command ("+Bn([t,e])+") is dropped"),!0;var i=20===t||21===t||23===t?1:2,n=this.channels[i];return 20===t||21===t||28===t||29===t?32===e?n.ccRCL():33===e?n.ccBS():34===e?n.ccAOF():35===e?n.ccAON():36===e?n.ccDER():37===e?n.ccRU(2):38===e?n.ccRU(3):39===e?n.ccRU(4):40===e?n.ccFON():41===e?n.ccRDC():42===e?n.ccTR():43===e?n.ccRTD():44===e?n.ccEDM():45===e?n.ccCR():46===e?n.ccENM():47===e&&n.ccEOC():n.ccTO(e-32),jn(t,e,r),this.currentChannel=i,!0},e.parseMidrow=function(t,e){var r=0;if((17===t||25===t)&&e>=32&&e<=47){if((r=17===t?1:2)!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;var i=this.channels[r];return!!i&&(i.ccMIDROW(e),this.logger.log(3,"MIDROW ("+Bn([t,e])+")"),!0)}return!1},e.parsePAC=function(t,e){var r,i=this.cmdHistory;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127||(16===t||24===t)&&e>=64&&e<=95))return!1;if(qn(t,e,i))return jn(null,null,i),!0;var n=t<=23?1:2;r=e>=64&&e<=95?1===n?xn[t]:Mn[t]:1===n?Fn[t]:On[t];var a=this.channels[n];return!!a&&(a.setPAC(this.interpretPAC(r,e)),jn(t,e,i),this.currentChannel=n,!0)},e.interpretPAC=function(t,e){var r,i={color:null,italics:!1,indent:null,underline:!1,row:t};return r=e>95?e-96:e-64,i.underline=1==(1&r),r<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(r/2)]:r<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((r-16)/2),i},e.parseChars=function(t,e){var r,i,n=null,a=null;if(t>=25?(r=2,a=t-8):(r=1,a=t),a>=17&&a<=19?(i=17===a?e+80:18===a?e+112:e+144,this.logger.log(2,"Special char '"+Cn(i)+"' in channel "+r),n=[i]):t>=32&&t<=127&&(n=0===e?[t]:[t,e]),n){var s=Bn(n);this.logger.log(3,"Char codes = "+s.join(",")),jn(t,e,this.cmdHistory)}return n},e.parseBackgroundAttributes=function(t,e){var r;if(!((16===t||24===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=45&&e<=47))return!1;var i={};16===t||24===t?(r=Math.floor((e-32)/2),i.background=Nn[r],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));var n=t<=23?1:2;return this.channels[n].setBkgData(i),jn(t,e,this.cmdHistory),!0},e.reset=function(){for(var t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=r,this.timelineController.createCaptionsTrack(this.trackName)},e.reset=function(){this.cueRanges=[],this.startTime=null},t}(),zn=function(){if("undefined"!=typeof self&&self.VTTCue)return self.VTTCue;var t=["","lr","rl"],e=["start","middle","end","left","right"];function r(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;var r=e.toLowerCase();return!!~t.indexOf(r)&&r}function i(t){return r(e,t)}function n(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i100)throw new Error("Position must be between 0 and 100.");T=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",n({},l,{get:function(){return E},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");E=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",n({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",n({},l,{get:function(){return L},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return a.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},a}(),Qn=function(){function t(){}return t.prototype.decode=function(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))},t}();function $n(t){function e(t,e,r,i){return 3600*(0|t)+60*(0|e)+(0|r)+parseFloat(i||0)}var r=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return r?parseFloat(r[2])>59?e(r[2],r[3],0,r[4]):e(r[1],r[2],r[3],r[4]):null}var Jn=function(){function t(){this.values=Object.create(null)}var e=t.prototype;return e.set=function(t,e){this.get(t)||""===e||(this.values[t]=e)},e.get=function(t,e,r){return r?this.has(t)?this.values[t]:e[r]:this.has(t)?this.values[t]:e},e.has=function(t){return t in this.values},e.alt=function(t,e,r){for(var i=0;i=0&&r<=100)return this.set(t,r),!0}return!1},t}();function Zn(t,e,r,i){var n=i?t.split(i):[t];for(var a in n)if("string"==typeof n[a]){var s=n[a].split(r);2===s.length&&e(s[0],s[1])}}var ta=new zn(0,0,""),ea="middle"===ta.align?"middle":"center";function ra(t,e,r){var i=t;function n(){var e=$n(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function a(){t=t.replace(/^\s+/,"")}if(a(),e.startTime=n(),a(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),a(),e.endTime=n(),a(),function(t,e){var i=new Jn;Zn(t,(function(t,e){var n;switch(t){case"region":for(var a=r.length-1;a>=0;a--)if(r[a].id===e){i.set(t,r[a].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":n=e.split(","),i.integer(t,n[0]),i.percent(t,n[0])&&i.set("snapToLines",!1),i.alt(t,n[0],["auto"]),2===n.length&&i.alt("lineAlign",n[1],["start",ea,"end"]);break;case"position":n=e.split(","),i.percent(t,n[0]),2===n.length&&i.alt("positionAlign",n[1],["start",ea,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",ea,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");var n=i.get("line","auto");"auto"===n&&-1===ta.line&&(n=-1),e.line=n,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",ea);var a=i.get("position","auto");"auto"===a&&50===ta.position&&(a="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=a}(t,e)}function ia(t){return t.replace(/ /gi,"\n")}var na=function(){function t(){this.state="INITIAL",this.buffer="",this.decoder=new Qn,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}var e=t.prototype;return e.parse=function(t){var e=this;function r(){var t=e.buffer,r=0;for(t=ia(t);r>>0).toString()};function la(t,e,r){return oa(t.toString())+oa(e.toString())+oa(r)}function ua(t,e,r,i,n,a,s){var o,l,u,h=new na,d=pt(new Uint8Array(t)).trim().replace(aa,"\n").split("\n"),c=[],f=e?(o=e.baseTime,void 0===(l=e.timescale)&&(l=1),Ui(o,9e4,1/l)):0,g="00:00.000",v=0,m=0,p=!0;h.oncue=function(t){var a=r[i],s=r.ccOffset,o=(v-f)/9e4;if(null!=a&&a.new&&(void 0!==m?s=r.ccOffset=a.start:function(t,e,r){var i=t[e],n=t[i.prevCC];if(!n||!n.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(a=n)&&a.new;){var a;t.ccOffset+=i.start-n.start,i.new=!1,n=t[(i=n).prevCC]}t.presentationOffset=r}(r,i,o)),o){if(!e)return void(u=new Error("Missing initPTS for VTT MPEGTS"));s=o-r.presentationOffset}var l=t.endTime-t.startTime,h=Vi(9e4*(t.startTime+s-m),9e4*n)/9e4;t.startTime=Math.max(h,0),t.endTime=Math.max(h+l,0);var d=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(d)),t.id||(t.id=la(t.startTime,t.endTime,d)),t.endTime>0&&c.push(t)},h.onparsingerror=function(t){u=t},h.onflush=function(){u?s(u):a(c)},d.forEach((function(t){if(p){if(sa(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((function(t){sa(t,"LOCAL:")?g=t.slice(6):sa(t,"MPEGTS:")&&(v=parseInt(t.slice(7)))}));try{m=function(t){var e=parseInt(t.slice(-3)),r=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),n=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(E(e)&&E(r)&&E(i)&&E(n)))throw Error("Malformed X-TIMESTAMP-MAP: Local:"+t);return e+=1e3*r,(e+=6e4*i)+36e5*n}(g)/1e3}catch(t){u=t}return}""===t&&(p=!1)}h.parse(t+"\n")})),h.flush()}var ha="stpp.ttml.im1t",da=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,ca=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,fa={left:"start",center:"center",right:"end",start:"start",end:"end"};function ga(t,e,r,i){var n=It(new Uint8Array(t),["mdat"]);if(0!==n.length){var a,s,l,u,h=n.map((function(t){return pt(t)})),d=(a=e.baseTime,s=1,void 0===(l=e.timescale)&&(l=1),void 0===u&&(u=!1),Ui(a,s,1/l,u));try{h.forEach((function(t){return r(function(t,e){var r=(new DOMParser).parseFromString(t,"text/xml").getElementsByTagName("tt")[0];if(!r)throw new Error("Invalid ttml");var i={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},n=Object.keys(i).reduce((function(t,e){return t[e]=r.getAttribute("ttp:"+e)||i[e],t}),{}),a="preserve"!==r.getAttribute("xml:space"),s=ma(va(r,"styling","style")),l=ma(va(r,"layout","region")),u=va(r,"body","[begin]");return[].map.call(u,(function(t){var r=pa(t,a);if(!r||!t.hasAttribute("begin"))return null;var i=Ea(t.getAttribute("begin"),n),u=Ea(t.getAttribute("dur"),n),h=Ea(t.getAttribute("end"),n);if(null===i)throw Ta(t);if(null===h){if(null===u)throw Ta(t);h=i+u}var d=new zn(i-e,h-e,r);d.id=la(d.startTime,d.endTime,d.text);var c=function(t,e,r){var i="http://www.w3.org/ns/ttml#styling",n=null,a=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],s=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;return s&&r.hasOwnProperty(s)&&(n=r[s]),a.reduce((function(r,a){var s=ya(e,i,a)||ya(t,i,a)||ya(n,i,a);return s&&(r[a]=s),r}),{})}(l[t.getAttribute("region")],s[t.getAttribute("style")],s),f=c.textAlign;if(f){var g=fa[f];g&&(d.lineAlign=g),d.align=f}return o(d,c),d})).filter((function(t){return null!==t}))}(t,d))}))}catch(t){i(t)}}else i(new Error("Could not parse IMSC1 mdat"))}function va(t,e,r){var i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(r)):[]}function ma(t){return t.reduce((function(t,e){var r=e.getAttribute("xml:id");return r&&(t[r]=e),t}),{})}function pa(t,e){return[].slice.call(t.childNodes).reduce((function(t,r,i){var n;return"br"===r.nodeName&&i?t+"\n":null!=(n=r.childNodes)&&n.length?pa(r,e):e?t+r.textContent.trim().replace(/\s+/g," "):t+r.textContent}),"")}function ya(t,e,r){return t&&t.hasAttributeNS(e,r)?t.getAttributeNS(e,r):null}function Ta(t){return new Error("Could not parse ttml timestamp "+t)}function Ea(t,e){if(!t)return null;var r=$n(t);return null===r&&(da.test(t)?r=function(t,e){var r=da.exec(t),i=(0|r[4])+(0|r[5])/e.subFrameRate;return 3600*(0|r[1])+60*(0|r[2])+(0|r[3])+i/e.frameRate}(t,e):ca.test(t)&&(r=function(t,e){var r=ca.exec(t),i=Number(r[1]);switch(r[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),r}var Sa=function(){function t(t){if(this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=t,this.config=t.config,this.Cues=t.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},this.config.enableCEA708Captions){var e=new Xn(this,"textTrack1"),r=new Xn(this,"textTrack2"),i=new Xn(this,"textTrack3"),n=new Xn(this,"textTrack4");this.cea608Parser1=new Wn(1,e,r),this.cea608Parser2=new Wn(3,i,n)}t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this)}var e=t.prototype;return e.destroy=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=this.cea608Parser1=this.cea608Parser2=null},e.addCues=function(t,e,r,i,n){for(var a,s,o,l,u=!1,h=n.length;h--;){var d=n[h],c=(a=d[0],s=d[1],o=e,l=r,Math.min(s,l)-Math.max(a,o));if(c>=0&&(d[0]=Math.min(d[0],e),d[1]=Math.max(d[1],r),u=!0,c/(r-e)>.5))return}if(u||n.push([e,r]),this.config.renderTextTracksNatively){var f=this.captionsTracks[t];this.Cues.newCue(f,e,r,i)}else{var g=this.Cues.newCue(null,e,r,i);this.hls.trigger(S.CUES_PARSED,{type:"captions",cues:g,track:t})}},e.onInitPtsFound=function(t,e){var r=this,i=e.frag,n=e.id,a=e.initPTS,s=e.timescale,o=this.unparsedVttFrags;"main"===n&&(this.initPTS[i.cc]={baseTime:a,timescale:s}),o.length&&(this.unparsedVttFrags=[],o.forEach((function(t){r.onFragLoaded(S.FRAG_LOADED,t)})))},e.getExistingTrack=function(t){var e=this.media;if(e)for(var r=0;ri.cc||l.trigger(S.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:i,error:e})}))}else s.push(t)},e._fallbackToIMSC1=function(t,e){var r=this,i=this.tracks[t.level];i.textCodec||ga(e,this.initPTS[t.cc],(function(){i.textCodec=ha,r._parseIMSC1(t,e)}),(function(){i.textCodec="wvtt"}))},e._appendCues=function(t,e){var r=this.hls;if(this.config.renderTextTracksNatively){var i=this.textTracks[e];if(!i||"disabled"===i.mode)return;t.forEach((function(t){return Se(i,t)}))}else{var n=this.tracks[e];if(!n)return;var a=n.default?"default":"subtitles"+e;r.trigger(S.CUES_PARSED,{type:"subtitles",cues:t,track:a})}},e.onFragDecrypted=function(t,e){e.frag.type===me&&this.onFragLoaded(S.FRAG_LOADED,e)},e.onSubtitleTracksCleared=function(){this.tracks=[],this.captionsTracks={}},e.onFragParsingUserdata=function(t,e){var r=this.cea608Parser1,i=this.cea608Parser2;if(this.enabled&&r&&i){var n=e.frag,a=e.samples;if(n.type!==ge||"NONE"!==this.closedCaptionsForLevel(n))for(var s=0;s0&&this.mediaWidth>0){var t=this.hls.levels;if(t.length){var e=this.hls;e.autoLevelCapping=this.getMaxLevel(t.length-1),e.autoLevelCapping>this.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}},e.getMaxLevel=function(e){var r=this,i=this.hls.levels;if(!i.length)return-1;var n=i.filter((function(t,i){return r.isLevelAllowed(t)&&i<=e}));return this.clientRect=null,t.getMaxLevelByMediaSize(n,this.mediaWidth,this.mediaHeight)},e.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,this.hls.firstLevel=this.getMaxLevel(this.firstLevel),self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},e.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},e.getDimensions=function(){if(this.clientRect)return this.clientRect;var t=this.media,e={width:0,height:0};if(t){var r=t.getBoundingClientRect();e.width=r.width,e.height=r.height,e.width||e.height||(e.width=r.right-r.left||t.width||0,e.height=r.bottom-r.top||t.height||0)}return this.clientRect=e,e},e.isLevelAllowed=function(t){return!this.restrictedLevels.some((function(e){return t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height}))},t.getMaxLevelByMediaSize=function(t,e,r){if(null==t||!t.length)return-1;for(var i,n,a=t.length-1,s=0;s=e||o.height>=r)&&(i=o,!(n=t[s+1])||i.width!==n.width||i.height!==n.height)){a=s;break}}return a},a(t,[{key:"mediaWidth",get:function(){return this.getDimensions().width*this.contentScaleFactor}},{key:"mediaHeight",get:function(){return this.getDimensions().height*this.contentScaleFactor}},{key:"contentScaleFactor",get:function(){var t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}}]),t}(),Aa=function(){function t(t){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=t,this.registerListeners()}var e=t.prototype;return e.setStreamController=function(t){this.streamController=t},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.destroy=function(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null},e.onMediaAttaching=function(t,e){var r=this.hls.config;if(r.capLevelOnFPSDrop){var i=e.media instanceof self.HTMLVideoElement?e.media:null;this.media=i,i&&"function"==typeof i.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),r.fpsDroppedMonitoringPeriod)}},e.checkFPS=function(t,e,r){var i=performance.now();if(e){if(this.lastTime){var n=i-this.lastTime,a=r-this.lastDroppedFrames,s=e-this.lastDecodedFrames,o=1e3*a/n,l=this.hls;if(l.trigger(S.FPS_DROP,{currentDropped:a,currentDecoded:s,totalDroppedFrames:r}),o>0&&a>l.config.fpsDroppedMonitoringThreshold*s){var u=l.currentLevel;w.warn("drop FPS ratio greater than max allowed value for currentLevel: "+u),u>0&&(-1===l.autoLevelCapping||l.autoLevelCapping>=u)&&(u-=1,l.trigger(S.FPS_DROP_LEVEL_CAPPING,{level:u,droppedLevel:l.currentLevel}),l.autoLevelCapping=u,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=r,this.lastDecodedFrames=e}},e.checkFPSInterval=function(){var t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){var e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)},t}(),ka="[eme]",ba=function(){function t(e){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=t.CDMCleanupPromise?[t.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=w.debug.bind(w,ka),this.log=w.log.bind(w,ka),this.warn=w.warn.bind(w,ka),this.error=w.error.bind(w,ka),this.hls=e,this.config=e.config,this.registerListeners()}var e=t.prototype;return e.destroy=function(){this.unregisterListeners(),this.onMediaDetached();var t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.getLicenseServerUrl=function(t){var e=this.config,r=e.drmSystems,i=e.widevineLicenseUrl,n=r[t];if(n)return n.licenseUrl;if(t===j.WIDEVINE&&i)return i;throw new Error('no license server URL configured for key-system "'+t+'"')},e.getServerCertificateUrl=function(t){var e=this.config.drmSystems[t];if(e)return e.serverCertificateUrl;this.log('No Server Certificate in config.drmSystems["'+t+'"]')},e.attemptKeySystemAccess=function(t){var e=this,r=this.hls.levels,i=function(t,e,r){return!!t&&r.indexOf(t)===e},n=r.map((function(t){return t.audioCodec})).filter(i),a=r.map((function(t){return t.videoCodec})).filter(i);return n.length+a.length===0&&a.push("avc1.42e01e"),new Promise((function(r,i){!function t(s){var o=s.shift();e.getMediaKeysPromise(o,n,a).then((function(t){return r({keySystem:o,mediaKeys:t})})).catch((function(e){s.length?t(s):i(e instanceof Da?e:new Da({type:L.KEY_SYSTEM_ERROR,details:R.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))}(t)}))},e.requestMediaKeySystemAccess=function(t,e){var r=this.config.requestMediaKeySystemAccessFunc;if("function"!=typeof r){var i="Configured requestMediaKeySystemAccess is not a function "+r;return null===et&&"http:"===self.location.protocol&&(i="navigator.requestMediaKeySystemAccess is not available over insecure protocol "+location.protocol),Promise.reject(new Error(i))}return r(t,e)},e.getMediaKeysPromise=function(t,e,r){var i=this,n=function(t,e,r,i){var n;switch(t){case j.FAIRPLAY:n=["cenc","sinf"];break;case j.WIDEVINE:case j.PLAYREADY:n=["cenc"];break;case j.CLEARKEY:n=["cenc","keyids"];break;default:throw new Error("Unknown key-system: "+t)}return function(t,e,r,i){return[{initDataTypes:t,persistentState:i.persistentState||"not-allowed",distinctiveIdentifier:i.distinctiveIdentifier||"not-allowed",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((function(t){return{contentType:'audio/mp4; codecs="'+t+'"',robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}})),videoCapabilities:r.map((function(t){return{contentType:'video/mp4; codecs="'+t+'"',robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null}}))}]}(n,e,r,i)}(t,e,r,this.config.drmSystemOptions),a=this.keySystemAccessPromises[t],s=null==a?void 0:a.keySystemAccess;if(!s){this.log('Requesting encrypted media "'+t+'" key-system access with config: '+JSON.stringify(n)),s=this.requestMediaKeySystemAccess(t,n);var o=this.keySystemAccessPromises[t]={keySystemAccess:s};return s.catch((function(e){i.log('Failed to obtain access to key-system "'+t+'": '+e)})),s.then((function(e){i.log('Access for key-system "'+e.keySystem+'" obtained');var r=i.fetchServerCertificate(t);return i.log('Create media-keys for "'+t+'"'),o.mediaKeys=e.createMediaKeys().then((function(e){return i.log('Media-keys created for "'+t+'"'),r.then((function(r){return r?i.setMediaKeysServerCertificate(e,t,r):e}))})),o.mediaKeys.catch((function(e){i.error('Failed to create media-keys for "'+t+'"}: '+e)})),o.mediaKeys}))}return s.then((function(){return a.mediaKeys}))},e.createMediaKeySessionContext=function(t){var e=t.decryptdata,r=t.keySystem,i=t.mediaKeys;this.log('Creating key-system session "'+r+'" keyId: '+Tt(e.keyId||[]));var n=i.createSession(),a={decryptdata:e,keySystem:r,mediaKeys:i,mediaKeysSession:n,keyStatus:"status-pending"};return this.mediaKeySessions.push(a),a},e.renewKeySession=function(t){var e=t.decryptdata;if(e.pssh){var r=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e);this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(r,"cenc",e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)},e.getKeyIdString=function(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return Tt(t.keyId)},e.updateKeySession=function(t,e){var r,i=t.mediaKeysSession;return this.log('Updating key-session "'+i.sessionId+'" for keyID '+Tt((null==(r=t.decryptdata)?void 0:r.keyId)||[])+"\n } (data length: "+(e?e.byteLength:e)+")"),i.update(e)},e.selectKeySystemFormat=function(t){var e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log("Selecting key-system from fragment (sn: "+t.sn+" "+t.type+": "+t.level+") key formats "+e.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise},e.getKeyFormatPromise=function(t){var e=this;return new Promise((function(r,i){var n=tt(e.config),a=t.map($).filter((function(t){return!!t&&-1!==n.indexOf(t)}));return e.getKeySystemSelectionPromise(a).then((function(t){var e=t.keySystem,n=Z(e);n?r(n):i(new Error('Unable to find format for key-system "'+e+'"'))})).catch(i)}))},e.loadKey=function(t){var e=this,r=t.keyInfo.decryptdata,i=this.getKeyIdString(r),n="(keyId: "+i+' format: "'+r.keyFormat+'" method: '+r.method+" uri: "+r.uri+")";this.log("Starting session for key "+n);var a=this.keyIdToKeySessionPromise[i];return a||(a=this.keyIdToKeySessionPromise[i]=this.getKeySystemForKeyPromise(r).then((function(i){var a=i.keySystem,s=i.mediaKeys;return e.throwIfDestroyed(),e.log("Handle encrypted media sn: "+t.frag.sn+" "+t.frag.type+": "+t.frag.level+" using key "+n),e.attemptSetMediaKeys(a,s).then((function(){e.throwIfDestroyed();var t=e.createMediaKeySessionContext({keySystem:a,mediaKeys:s,decryptdata:r});return e.generateRequestWithPreferredKeySession(t,"cenc",r.pssh,"playlist-key")}))}))).catch((function(t){return e.handleError(t)})),a},e.throwIfDestroyed=function(t){if(!this.hls)throw new Error("invalid state")},e.handleError=function(t){this.hls&&(this.error(t.message),t instanceof Da?this.hls.trigger(S.ERROR,t.data):this.hls.trigger(S.ERROR,{type:L.KEY_SYSTEM_ERROR,details:R.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))},e.getKeySystemForKeyPromise=function(t){var e=this.getKeyIdString(t),r=this.keyIdToKeySessionPromise[e];if(!r){var i=$(t.keyFormat),n=i?[i]:tt(this.config);return this.attemptKeySystemAccess(n)}return r},e.getKeySystemSelectionPromise=function(t){if(t.length||(t=tt(this.config)),0===t.length)throw new Da({type:L.KEY_SYSTEM_ERROR,details:R.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},"Missing key-system license configuration options "+JSON.stringify({drmSystems:this.config.drmSystems}));return this.attemptKeySystemAccess(t)},e._onMediaEncrypted=function(t){var e=this,r=t.initDataType,i=t.initData;if(this.debug('"'+t.type+'" event: init data type: "'+r+'"'),null!==i){var n,a;if("sinf"===r&&this.config.drmSystems[j.FAIRPLAY]){var s=Rt(new Uint8Array(i));try{var o=V(JSON.parse(s).sinf),l=_t(new Uint8Array(o));if(!l)return;n=l.subarray(8,24),a=j.FAIRPLAY}catch(t){return void this.warn('Failed to parse sinf "encrypted" event message initData')}}else{var u=function(t){if(!(t instanceof ArrayBuffer)||t.byteLength<32)return null;var e={version:0,systemId:"",kids:null,data:null},r=new DataView(t),i=r.getUint32(0);if(t.byteLength!==i&&i>44)return null;if(1886614376!==r.getUint32(4))return null;if(e.version=r.getUint32(8)>>>24,e.version>1)return null;e.systemId=Tt(new Uint8Array(t,12,16));var n=r.getUint32(28);if(0===e.version){if(i-32d||o.status>=400&&o.status<500)a(new Da({type:L.KEY_SYSTEM_ERROR,details:R.KEY_SYSTEM_LICENSE_REQUEST_FAILED,fatal:!0,networkDetails:o,response:{url:s,data:void 0,code:o.status,text:o.statusText}},"License Request XHR failed ("+s+"). Status: "+o.status+" ("+o.statusText+")"));else{var c=d-r._requestLicenseFailureCount+1;r.warn("Retrying license request, "+c+" attempts left"),r.requestLicense(t,e).then(n,a)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=o,r.setupLicenseXHR(o,s,t,e).then((function(t){var e=t.xhr,r=t.licenseChallenge;e.send(r)}))}))},e.onMediaAttached=function(t,e){if(this.config.emeEnabled){var r=e.media;this.media=r,r.addEventListener("encrypted",this.onMediaEncrypted),r.addEventListener("waitingforkey",this.onWaitingForKey)}},e.onMediaDetached=function(){var e=this,r=this.media,i=this.mediaKeySessions;r&&(r.removeEventListener("encrypted",this.onMediaEncrypted),r.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},Gt.clearKeyUriToKeyIdMap();var n=i.length;t.CDMCleanupPromise=Promise.all(i.map((function(t){return e.removeSession(t)})).concat(null==r?void 0:r.setMediaKeys(null).catch((function(t){e.log("Could not clear media keys: "+t+". media.src: "+(null==r?void 0:r.src))})))).then((function(){n&&(e.log("finished closing key sessions and clearing media keys"),i.length=0)})).catch((function(t){e.log("Could not close sessions and clear media keys: "+t+". media.src: "+(null==r?void 0:r.src))}))},e.onManifestLoading=function(){this.keyFormatPromise=null},e.onManifestLoaded=function(t,e){var r=e.sessionKeys;if(r&&this.config.emeEnabled&&!this.keyFormatPromise){var i=r.reduce((function(t,e){return-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t}),[]);this.log("Selecting key-system from session-keys "+i.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(i)}},e.removeSession=function(t){var e=this,r=t.mediaKeysSession,i=t.licenseXhr;if(r){this.log("Remove licenses and keys and close session "+r.sessionId),r.onmessage=null,r.onkeystatuseschange=null,i&&i.readyState!==XMLHttpRequest.DONE&&i.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;var n=this.mediaKeySessions.indexOf(t);return n>-1&&this.mediaKeySessions.splice(n,1),r.remove().catch((function(t){e.log("Could not remove session: "+t)})).then((function(){return r.close()})).catch((function(t){e.log("Could not close session: "+t)}))}},t}();ba.CDMCleanupPromise=void 0;var Da=function(t){function e(e,r){var i;return(i=t.call(this,r)||this).data=void 0,e.error||(e.error=new Error(r)),i.data=e,e.err=e.error,i}return l(e,t),e}(f(Error)),Ia="m",wa="a",Ca="v",_a="av",Pa="i",xa="tt",Fa=function(){function t(e){var r=this;this.hls=void 0,this.config=void 0,this.media=void 0,this.sid=void 0,this.cid=void 0,this.useHeaders=!1,this.initialized=!1,this.starved=!1,this.buffering=!0,this.audioBuffer=void 0,this.videoBuffer=void 0,this.onWaiting=function(){r.initialized&&(r.starved=!0),r.buffering=!0},this.onPlaying=function(){r.initialized||(r.initialized=!0),r.buffering=!1},this.applyPlaylistData=function(t){try{r.apply(t,{ot:Ia,su:!r.initialized})}catch(t){w.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=function(t){try{var e=t.frag,i=r.hls.levels[e.level],n=r.getObjectType(e),a={d:1e3*e.duration,ot:n};n!==Ca&&n!==wa&&n!=_a||(a.br=i.bitrate/1e3,a.tb=r.getTopBandwidth(n)/1e3,a.bl=r.getBufferLength(n)),r.apply(t,a)}catch(t){w.warn("Could not generate segment CMCD data.",t)}},this.hls=e;var i=this.config=e.config,n=i.cmcd;null!=n&&(i.pLoader=this.createPlaylistLoader(),i.fLoader=this.createFragmentLoader(),this.sid=n.sessionId||t.uuid(),this.cid=n.contentId,this.useHeaders=!0===n.useHeaders,this.registerListeners())}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(S.MEDIA_DETACHED,this.onMediaDetached,this),t.on(S.BUFFER_CREATED,this.onBufferCreated,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(S.MEDIA_DETACHED,this.onMediaDetached,this),t.off(S.BUFFER_CREATED,this.onBufferCreated,this)},e.destroy=function(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null},e.onMediaAttached=function(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)},e.onMediaDetached=function(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)},e.onBufferCreated=function(t,e){var r,i;this.audioBuffer=null==(r=e.tracks.audio)?void 0:r.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer},e.createData=function(){var t;return{v:1,sf:"h",sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}},e.apply=function(e,r){void 0===r&&(r={}),o(r,this.createData());var i=r.ot===Pa||r.ot===Ca||r.ot===_a;if(this.starved&&i&&(r.bs=!0,r.su=!0,this.starved=!1),null==r.su&&(r.su=this.buffering),this.useHeaders){var n=t.toHeaders(r);if(!Object.keys(n).length)return;e.headers||(e.headers={}),o(e.headers,n)}else{var a=t.toQuery(r);if(!a)return;e.url=t.appendQueryToUri(e.url,a)}},e.getObjectType=function(t){var e=t.type;return"subtitle"===e?xa:"initSegment"===t.sn?Pa:"audio"===e?wa:"main"===e?this.hls.audioTracks.length?Ca:_a:void 0},e.getTopBandwidth=function(t){var e,r=0,i=this.hls;if(t===wa)e=i.audioTracks;else{var n=i.maxAutoLevel,a=n>-1?n+1:i.levels.length;e=i.levels.slice(0,a)}for(var s,o=v(e);!(s=o()).done;){var l=s.value;l.bitrate>r&&(r=l.bitrate)}return r>0?r:NaN},e.getBufferLength=function(t){var e=this.hls.media,r=t===wa?this.audioBuffer:this.videoBuffer;return r&&e?1e3*Ir.bufferInfo(r,e.currentTime,this.config.maxBufferHole).len:NaN},e.createPlaylistLoader=function(){var t=this.config.pLoader,e=this.applyPlaylistData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},a(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},e.createFragmentLoader=function(){var t=this.config.fLoader,e=this.applyFragmentData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},a(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},t.uuid=function(){var t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)},t.serialize=function(t){for(var e,r=[],i=function(t){return!Number.isNaN(t)&&null!=t&&""!==t&&!1!==t},n=function(t){return Math.round(t)},a=function(t){return 100*n(t/100)},s={br:n,d:n,bl:a,dl:a,mtp:a,nor:function(t){return encodeURIComponent(t)},rtp:a,tb:n},o=v(Object.keys(t||{}).sort());!(e=o()).done;){var l=e.value,u=t[l];if(i(u)&&!("v"===l&&1===u||"pr"==l&&1===u)){var h=s[l];h&&(u=h(u));var d=typeof u,c=void 0;c="ot"===l||"sf"===l||"st"===l?l+"="+u:"boolean"===d?l:"number"===d?l+"="+u:l+"="+JSON.stringify(u),r.push(c)}}return r.join(",")},t.toHeaders=function(e){for(var r={},i=["Object","Request","Session","Status"],n=[{},{},{},{}],a={br:0,d:0,ot:0,tb:0,bl:1,dl:1,mtp:1,nor:1,nrr:1,su:1,cid:2,pr:2,sf:2,sid:2,st:2,v:2,bs:3,rtp:3},s=0,o=Object.keys(e);s1&&(this.updatePathwayPriority(i),r.resolved=this.pathwayId!==n)}},e.filterParsedLevels=function(t){this.levels=t;var e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){var r=t[0].pathwayId;this.log("No levels found in Pathway "+this.pathwayId+'. Setting initial Pathway to "'+r+'"'),e=this.getLevelsForPathway(r),this.pathwayId=r}return e.length!==t.length?(this.log("Found "+e.length+"/"+t.length+' levels in Pathway "'+this.pathwayId+'"'),e):t},e.getLevelsForPathway=function(t){return null===this.levels?[]:this.levels.filter((function(e){return t===e.pathwayId}))},e.updatePathwayPriority=function(t){var e;this.pathwayPriority=t;var r=this.penalizedPathways,i=performance.now();Object.keys(r).forEach((function(t){i-r[t]>3e5&&delete r[t]}));for(var n=0;n0){this.log('Setting Pathway to "'+a+'"'),this.pathwayId=a,this.hls.trigger(S.LEVELS_UPDATED,{levels:e});var l=this.hls.levels[s];o&&l&&this.levels&&(l.attrs["STABLE-VARIANT-ID"]!==o.attrs["STABLE-VARIANT-ID"]&&l.bitrate!==o.bitrate&&this.log("Unstable Pathways change from bitrate "+o.bitrate+" to "+l.bitrate),this.hls.nextLoadLevel=s);break}}}},e.clonePathways=function(t){var e=this,r=this.levels;if(r){var i={},n={};t.forEach((function(t){var a=t.ID,s=t["BASE-ID"],l=t["URI-REPLACEMENT"];if(!r.some((function(t){return t.pathwayId===a}))){var u=e.getLevelsForPathway(s).map((function(t){var e=o({},t);e.details=void 0,e.url=Na(t.uri,t.attrs["STABLE-VARIANT-ID"],"PER-VARIANT-URIS",l);var r=new P(t.attrs);r["PATHWAY-ID"]=a;var s=r.AUDIO&&r.AUDIO+"_clone_"+a,u=r.SUBTITLES&&r.SUBTITLES+"_clone_"+a;s&&(i[r.AUDIO]=s,r.AUDIO=s),u&&(n[r.SUBTITLES]=u,r.SUBTITLES=u),e.attrs=r;var h=new Ne(e);return dr(h,"audio",s),dr(h,"text",u),h}));r.push.apply(r,u),Oa(e.audioTracks,i,l,a),Oa(e.subtitleTracks,n,l,a)}}))}},e.loadSteeringManifest=function(t){var e,r=this,i=this.hls.config,n=i.loader;this.loader&&this.loader.destroy(),this.loader=new n(i);try{e=new self.URL(t)}catch(e){return this.enabled=!1,void this.log("Failed to parse Steering Manifest URI: "+t)}if("data:"!==e.protocol){var a=0|(this.hls.bandwidthEstimate||i.abrEwmaDefaultEstimate);e.searchParams.set("_HLS_pathway",this.pathwayId),e.searchParams.set("_HLS_throughput",""+a)}var s={responseType:"json",url:e.href},o=i.steeringManifestLoadPolicy.default,l=o.errorRetry||o.timeoutRetry||{},u={loadPolicy:o,timeout:o.maxLoadTimeMs,maxRetry:l.maxNumRetry||0,retryDelay:l.retryDelayMs||0,maxRetryDelay:l.maxRetryDelayMs||0},h={onSuccess:function(t,i,n,a){r.log('Loaded steering manifest: "'+e+'"');var s=t.data;if(1===s.VERSION){r.updated=performance.now(),r.timeToLoad=s.TTL;var o=s["RELOAD-URI"],l=s["PATHWAY-CLONES"],u=s["PATHWAY-PRIORITY"];if(o)try{r.uri=new self.URL(o,e).href}catch(t){return r.enabled=!1,void r.log("Failed to parse Steering Manifest RELOAD-URI: "+o)}r.scheduleRefresh(r.uri||n.url),l&&r.clonePathways(l),u&&r.updatePathwayPriority(u)}else r.log("Steering VERSION "+s.VERSION+" not supported!")},onError:function(t,e,i,n){if(r.log("Error loading steering manifest: "+t.code+" "+t.text+" ("+e.url+")"),r.stopLoad(),410===t.code)return r.enabled=!1,void r.log("Steering manifest "+e.url+" no longer available");var a=1e3*r.timeToLoad;if(429!==t.code)r.scheduleRefresh(r.uri||e.url,a);else{var s=r.loader;if("function"==typeof(null==s?void 0:s.getResponseHeader)){var o=s.getResponseHeader("Retry-After");o&&(a=1e3*parseFloat(o))}r.log("Steering manifest "+e.url+" rate limited")}},onTimeout:function(t,e,i){r.log("Timeout loading steering manifest ("+e.url+")"),r.scheduleRefresh(r.uri||e.url)}};this.log("Requesting steering manifest: "+e),this.loader.load(s,u,h)},e.scheduleRefresh=function(t,e){var r=this;void 0===e&&(e=1e3*this.timeToLoad),self.clearTimeout(this.reloadTimer),this.reloadTimer=self.setTimeout((function(){r.loadSteeringManifest(t)}),e)},t}();function Oa(t,e,r,i){t&&Object.keys(e).forEach((function(n){var a=t.filter((function(t){return t.groupId===n})).map((function(t){var a=o({},t);return a.details=void 0,a.attrs=new P(a.attrs),a.url=a.attrs.URI=Na(t.url,t.attrs["STABLE-RENDITION-ID"],"PER-RENDITION-URIS",r),a.groupId=a.attrs["GROUP-ID"]=e[n],a.attrs["PATHWAY-ID"]=i,a}));t.push.apply(t,a)}))}function Na(t,e,r,i){var n,a=i.HOST,s=i.PARAMS,o=i[r];e&&(n=null==o?void 0:o[e])&&(t=n);var l=new self.URL(t);return a&&!n&&(l.host=a),s&&Object.keys(s).sort().forEach((function(t){t&&l.searchParams.set(t,s[t])})),l.href}var Ua=/^age:\s*[\d.]+\s*$/im,Ba=function(){function t(t){this.xhrSetup=void 0,this.requestTimeout=void 0,this.retryTimeout=void 0,this.retryDelay=void 0,this.config=null,this.callbacks=null,this.context=void 0,this.loader=null,this.stats=void 0,this.xhrSetup=t&&t.xhrSetup||null,this.stats=new M,this.retryDelay=0}var e=t.prototype;return e.destroy=function(){this.callbacks=null,this.abortInternal(),this.loader=null,this.config=null},e.abortInternal=function(){var t=this.loader;self.clearTimeout(this.requestTimeout),self.clearTimeout(this.retryTimeout),t&&(t.onreadystatechange=null,t.onprogress=null,4!==t.readyState&&(this.stats.aborted=!0,t.abort()))},e.abort=function(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.loader)},e.load=function(t,e,r){if(this.stats.loading.start)throw new Error("Loader can only be used once.");this.stats.loading.start=self.performance.now(),this.context=t,this.config=e,this.callbacks=r,this.loadInternal()},e.loadInternal=function(){var t=this,e=this.config,r=this.context;if(e){var i=this.loader=new self.XMLHttpRequest,n=this.stats;n.loading.first=0,n.loaded=0,n.aborted=!1;var a=this.xhrSetup;a?Promise.resolve().then((function(){if(!t.stats.aborted)return a(i,r.url)})).catch((function(t){return i.open("GET",r.url,!0),a(i,r.url)})).then((function(){t.stats.aborted||t.openAndSendXhr(i,r,e)})).catch((function(e){t.callbacks.onError({code:i.status,text:e.message},r,i,n)})):this.openAndSendXhr(i,r,e)}},e.openAndSendXhr=function(t,e,r){t.readyState||t.open("GET",e.url,!0);var i=this.context.headers,n=r.loadPolicy,a=n.maxTimeToFirstByteMs,s=n.maxLoadTimeMs;if(i)for(var o in i)t.setRequestHeader(o,i[o]);e.rangeEnd&&t.setRequestHeader("Range","bytes="+e.rangeStart+"-"+(e.rangeEnd-1)),t.onreadystatechange=this.readystatechange.bind(this),t.onprogress=this.loadprogress.bind(this),t.responseType=e.responseType,self.clearTimeout(this.requestTimeout),r.timeout=a&&E(a)?a:s,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),r.timeout),t.send()},e.readystatechange=function(){var t=this.context,e=this.loader,r=this.stats;if(t&&e){var i=e.readyState,n=this.config;if(!r.aborted&&i>=2&&(0===r.loading.first&&(r.loading.first=Math.max(self.performance.now(),r.loading.start),n.timeout!==n.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),n.timeout=n.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),n.loadPolicy.maxLoadTimeMs-(r.loading.first-r.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),e.onreadystatechange=null,e.onprogress=null;var a=e.status,s="text"!==e.responseType;if(a>=200&&a<300&&(s&&e.response||null!==e.responseText)){r.loading.end=Math.max(self.performance.now(),r.loading.first);var o=s?e.response:e.responseText,l="arraybuffer"===e.responseType?o.byteLength:o.length;if(r.loaded=r.total=l,r.bwEstimate=8e3*r.total/(r.loading.end-r.loading.first),!this.callbacks)return;var u=this.callbacks.onProgress;if(u&&u(r,t,o,e),!this.callbacks)return;var h={url:e.responseURL,data:o,code:a};this.callbacks.onSuccess(h,r,t,e)}else{var d=n.loadPolicy.errorRetry;ze(d,r.retry,!1,a)?this.retry(d):(w.error(a+" while loading "+t.url),this.callbacks.onError({code:a,text:e.statusText},t,e,r))}}}},e.loadtimeout=function(){var t,e=null==(t=this.config)?void 0:t.loadPolicy.timeoutRetry;if(ze(e,this.stats.retry,!0))this.retry(e);else{w.warn("timeout while loading "+this.context.url);var r=this.callbacks;r&&(this.abortInternal(),r.onTimeout(this.stats,this.context,this.loader))}},e.retry=function(t){var e=this.context,r=this.stats;this.retryDelay=qe(t,r.retry),r.retry++,w.warn((status?"HTTP Status "+status:"Timeout")+" while loading "+e.url+", retrying "+r.retry+"/"+t.maxNumRetry+" in "+this.retryDelay+"ms"),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)},e.loadprogress=function(t){var e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)},e.getCacheAge=function(){var t=null;if(this.loader&&Ua.test(this.loader.getAllResponseHeaders())){var e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.loader&&new RegExp("^"+t+":\\s*[\\d.]+\\s*$","im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null},t}(),Ga=/(\d+)-(\d+)\/(\d+)/,Ka=function(){function t(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=void 0,this.response=void 0,this.controller=void 0,this.context=void 0,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||Ha,this.controller=new self.AbortController,this.stats=new M}var e=t.prototype;return e.destroy=function(){this.loader=this.callbacks=null,this.abortInternal()},e.abortInternal=function(){var t=this.response;null!=t&&t.ok||(this.stats.aborted=!0,this.controller.abort())},e.abort=function(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)},e.load=function(t,e,r){var i=this,n=this.stats;if(n.loading.start)throw new Error("Loader can only be used once.");n.loading.start=self.performance.now();var a=function(t,e){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(o({},t.headers))};return t.rangeEnd&&r.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1)),r}(t,this.controller.signal),s=r.onProgress,l="arraybuffer"===t.responseType,u=l?"byteLength":"length",h=e.loadPolicy,d=h.maxTimeToFirstByteMs,c=h.maxLoadTimeMs;this.context=t,this.config=e,this.callbacks=r,this.request=this.fetchSetup(t,a),self.clearTimeout(this.requestTimeout),e.timeout=d&&E(d)?d:c,this.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),e.timeout),self.fetch(this.request).then((function(a){i.response=i.loader=a;var o=Math.max(self.performance.now(),n.loading.start);if(self.clearTimeout(i.requestTimeout),e.timeout=c,i.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),c-(o-n.loading.start)),!a.ok){var u=a.status,h=a.statusText;throw new Va(h||"fetch, bad network response",u,a)}return n.loading.first=o,n.total=function(t){var e=t.get("Content-Range");if(e){var r=function(t){var e=Ga.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(E(r))return r}var i=t.get("Content-Length");if(i)return parseInt(i)}(a.headers)||n.total,s&&E(e.highWaterMark)?i.loadProgressively(a,n,t,e.highWaterMark,s):l?a.arrayBuffer():"json"===t.responseType?a.json():a.text()})).then((function(a){var o=i.response;self.clearTimeout(i.requestTimeout),n.loading.end=Math.max(self.performance.now(),n.loading.first);var l=a[u];l&&(n.loaded=n.total=l);var h={url:o.url,data:a,code:o.status};s&&!E(e.highWaterMark)&&s(n,t,a,o),r.onSuccess(h,n,t,o)})).catch((function(e){if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=e&&e.code||0,s=e?e.message:null;r.onError({code:a,text:s},t,e?e.details:null,n)}}))},e.getCacheAge=function(){var t=null;if(this.response){var e=this.response.headers.get("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.response?this.response.headers.get(t):null},e.loadProgressively=function(t,e,r,i,n){void 0===i&&(i=0);var a=new mn,s=t.body.getReader();return function o(){return s.read().then((function(s){if(s.done)return a.dataLength&&n(e,r,a.flush(),t),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return e.loaded+=u,u=i&&n(e,r,a.flush(),t)):n(e,r,l,t),o()})).catch((function(){return Promise.reject()}))}()},t}();function Ha(t,e){return new self.Request(t.url,e)}var Va=function(t){function e(e,r,i){var n;return(n=t.call(this,e)||this).code=void 0,n.details=void 0,n.code=r,n.details=i,n}return l(e,t),e}(f(Error)),Ya=/\s/,Wa=i(i({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,loader:Ba,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:vn,bufferController:In,capLevelController:Ra,errorController:lr,fpsController:Aa,stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:et,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:{newCue:function(t,e,r,i){for(var n,a,s,o,l,u=[],h=self.VTTCue||self.TextTrackCue,d=0;d=16?o--:o++;var g=ia(l.trim()),v=la(e,r,g);null!=t&&null!=(c=t.cues)&&c.getCueById(v)||((a=new h(e,r,g)).id=v,a.line=d+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*o/32)),u.push(a))}return t&&u.length&&(u.sort((function(t,e){return"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line})),u.forEach((function(e){return Se(t,e)}))),u}},enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:Sn,subtitleTrackController:Rn,timelineController:Sa,audioStreamController:pn,audioTrackController:yn,emeController:ba,cmcdController:Fa,contentSteeringController:Ma});function ja(t){return t&&"object"==typeof t?Array.isArray(t)?t.map(ja):Object.keys(t).reduce((function(e,r){return e[r]=ja(t[r]),e}),{}):t}function qa(t){var e=t.loader;e!==Ka&&e!==Ba?(w.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1):function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1}()&&(t.loader=Ka,t.progressive=!0,t.enableSoftwareAES=!0,w.log("[config]: Progressive streaming enabled, using FetchLoader"))}var Xa=function(){function t(e){void 0===e&&(e={}),this.config=void 0,this.userConfig=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this._emitter=new an,this._autoLevelCapping=void 0,this._maxHdcpLevel=null,this.abrController=void 0,this.bufferController=void 0,this.capLevelController=void 0,this.latencyController=void 0,this.levelController=void 0,this.streamController=void 0,this.audioTrackController=void 0,this.subtitleTrackController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this.url=null,I(e.debug||!1,"Hls instance");var r=this.config=function(t,e){if((e.liveSyncDurationCount||e.liveMaxLatencyDurationCount)&&(e.liveSyncDuration||e.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==e.liveMaxLatencyDurationCount&&(void 0===e.liveSyncDurationCount||e.liveMaxLatencyDurationCount<=e.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==e.liveMaxLatencyDuration&&(void 0===e.liveSyncDuration||e.liveMaxLatencyDuration<=e.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');var r=ja(t),n=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((function(t){var i=("level"===t?"playlist":t)+"LoadPolicy",a=void 0===e[i],s=[];n.forEach((function(n){var o=t+"Loading"+n,l=e[o];if(void 0!==l&&a){s.push(o);var u=r[i].default;switch(e[i]={default:u},n){case"TimeOut":u.maxLoadTimeMs=l,u.maxTimeToFirstByteMs=l;break;case"MaxRetry":u.errorRetry.maxNumRetry=l,u.timeoutRetry.maxNumRetry=l;break;case"RetryDelay":u.errorRetry.retryDelayMs=l,u.timeoutRetry.retryDelayMs=l;break;case"MaxRetryTimeout":u.errorRetry.maxRetryDelayMs=l,u.timeoutRetry.maxRetryDelayMs=l}}})),s.length&&w.warn('hls.js config: "'+s.join('", "')+'" setting(s) are deprecated, use "'+i+'": '+JSON.stringify(e[i]))})),i(i({},r),e)}(t.DefaultConfig,e);this.userConfig=e,this._autoLevelCapping=-1,r.progressive&&qa(r);var n=r.abrController,a=r.bufferController,s=r.capLevelController,o=r.errorController,l=r.fpsController,u=new o(this),h=this.abrController=new n(this),d=this.bufferController=new a(this),c=this.capLevelController=new s(this),f=new l(this),g=new Te(this),v=new Ce(this),m=r.contentSteeringController,p=m?new m(this):null,y=this.levelController=new hr(this,p),T=new pr(this),E=new kr(this.config),L=this.streamController=new cn(this,T,E);c.setStreamController(L),f.setStreamController(L);var R=[g,y,L];p&&R.splice(1,0,p),this.networkControllers=R;var A=[h,d,c,f,v,T];this.audioTrackController=this.createController(r.audioTrackController,R);var k=r.audioStreamController;k&&R.push(new k(this,T,E)),this.subtitleTrackController=this.createController(r.subtitleTrackController,R);var b=r.subtitleStreamController;b&&R.push(new b(this,T,E)),this.createController(r.timelineController,A),E.emeController=this.emeController=this.createController(r.emeController,A),this.cmcdController=this.createController(r.cmcdController,A),this.latencyController=this.createController(_e,A),this.coreComponents=A,R.push(u);var D=u.onErrorOut;"function"==typeof D&&this.on(S.ERROR,D,u)}t.isSupported=function(){return function(){var t=qt();if(!t)return!1;var e=Zr(),r=t&&"function"==typeof t.isTypeSupported&&t.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"'),i=!e||e.prototype&&"function"==typeof e.prototype.appendBuffer&&"function"==typeof e.prototype.remove;return!!r&&!!i}()};var e=t.prototype;return e.createController=function(t,e){if(t){var r=new t(this);return e&&e.push(r),r}return null},e.on=function(t,e,r){void 0===r&&(r=this),this._emitter.on(t,e,r)},e.once=function(t,e,r){void 0===r&&(r=this),this._emitter.once(t,e,r)},e.removeAllListeners=function(t){this._emitter.removeAllListeners(t)},e.off=function(t,e,r,i){void 0===r&&(r=this),this._emitter.off(t,e,r,i)},e.listeners=function(t){return this._emitter.listeners(t)},e.emit=function(t,e,r){return this._emitter.emit(t,e,r)},e.trigger=function(t,e){if(this.config.debug)return this.emit(t,t,e);try{return this.emit(t,t,e)}catch(e){w.error("An internal error happened while handling event "+t+'. Error message: "'+e.message+'". Here is a stacktrace:',e),this.trigger(S.ERROR,{type:L.OTHER_ERROR,details:R.INTERNAL_EXCEPTION,fatal:!1,event:t,error:e})}return!1},e.listenerCount=function(t){return this._emitter.listenerCount(t)},e.destroy=function(){w.log("destroy"),this.trigger(S.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this.url=null,this.networkControllers.forEach((function(t){return t.destroy()})),this.networkControllers.length=0,this.coreComponents.forEach((function(t){return t.destroy()})),this.coreComponents.length=0;var t=this.config;t.xhrSetup=t.fetchSetup=void 0,this.userConfig=null},e.attachMedia=function(t){w.log("attachMedia"),this._media=t,this.trigger(S.MEDIA_ATTACHING,{media:t})},e.detachMedia=function(){w.log("detachMedia"),this.trigger(S.MEDIA_DETACHING,void 0),this._media=null},e.loadSource=function(t){this.stopLoad();var e=this.media,r=this.url,i=this.url=T.buildAbsoluteURL(self.location.href,t,{alwaysNormalize:!0});w.log("loadSource:"+i),e&&r&&(r!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(e)),this.trigger(S.MANIFEST_LOADING,{url:t})},e.startLoad=function(t){void 0===t&&(t=-1),w.log("startLoad("+t+")"),this.networkControllers.forEach((function(e){e.startLoad(t)}))},e.stopLoad=function(){w.log("stopLoad"),this.networkControllers.forEach((function(t){t.stopLoad()}))},e.swapAudioCodec=function(){w.log("swapAudioCodec"),this.streamController.swapAudioCodec()},e.recoverMediaError=function(){w.log("recoverMediaError");var t=this._media;this.detachMedia(),t&&this.attachMedia(t)},e.removeLevel=function(t,e){void 0===e&&(e=0),this.levelController.removeLevel(t,e)},a(t,[{key:"levels",get:function(){var t=this.levelController.levels;return t||[]}},{key:"currentLevel",get:function(){return this.streamController.currentLevel},set:function(t){w.log("set currentLevel:"+t),this.loadLevel=t,this.abrController.clearTimer(),this.streamController.immediateLevelSwitch()}},{key:"nextLevel",get:function(){return this.streamController.nextLevel},set:function(t){w.log("set nextLevel:"+t),this.levelController.manualLevel=t,this.streamController.nextLevelSwitch()}},{key:"loadLevel",get:function(){return this.levelController.level},set:function(t){w.log("set loadLevel:"+t),this.levelController.manualLevel=t}},{key:"nextLoadLevel",get:function(){return this.levelController.nextLoadLevel},set:function(t){this.levelController.nextLoadLevel=t}},{key:"firstLevel",get:function(){return Math.max(this.levelController.firstLevel,this.minAutoLevel)},set:function(t){w.log("set firstLevel:"+t),this.levelController.firstLevel=t}},{key:"startLevel",get:function(){return this.levelController.startLevel},set:function(t){w.log("set startLevel:"+t),-1!==t&&(t=Math.max(t,this.minAutoLevel)),this.levelController.startLevel=t}},{key:"capLevelToPlayerSize",get:function(){return this.config.capLevelToPlayerSize},set:function(t){var e=!!t;e!==this.config.capLevelToPlayerSize&&(e?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=e)}},{key:"autoLevelCapping",get:function(){return this._autoLevelCapping},set:function(t){this._autoLevelCapping!==t&&(w.log("set autoLevelCapping:"+t),this._autoLevelCapping=t)}},{key:"bandwidthEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimate():NaN}},{key:"ttfbEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimateTTFB():NaN}},{key:"maxHdcpLevel",get:function(){return this._maxHdcpLevel},set:function(t){Pe.indexOf(t)>-1&&(this._maxHdcpLevel=t)}},{key:"autoLevelEnabled",get:function(){return-1===this.levelController.manualLevel}},{key:"manualLevel",get:function(){return this.levelController.manualLevel}},{key:"minAutoLevel",get:function(){var t=this.levels,e=this.config.minAutoBitrate;if(!t)return 0;for(var r=t.length,i=0;i=e)return i;return 0}},{key:"maxAutoLevel",get:function(){var t,e=this.levels,r=this.autoLevelCapping,i=this.maxHdcpLevel;if(t=-1===r&&e&&e.length?e.length-1:r,i)for(var n=t;n--;){var a=e[n].attrs["HDCP-LEVEL"];if(a&&a<=i)return n}return t}},{key:"nextAutoLevel",get:function(){return Math.min(Math.max(this.abrController.nextAutoLevel,this.minAutoLevel),this.maxAutoLevel)},set:function(t){this.abrController.nextAutoLevel=Math.max(this.minAutoLevel,t)}},{key:"playingDate",get:function(){return this.streamController.currentProgramDateTime}},{key:"mainForwardBufferInfo",get:function(){return this.streamController.getMainFwdBufferInfo()}},{key:"audioTracks",get:function(){var t=this.audioTrackController;return t?t.audioTracks:[]}},{key:"audioTrack",get:function(){var t=this.audioTrackController;return t?t.audioTrack:-1},set:function(t){var e=this.audioTrackController;e&&(e.audioTrack=t)}},{key:"subtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTracks:[]}},{key:"subtitleTrack",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTrack:-1},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleTrack=t)}},{key:"media",get:function(){return this._media}},{key:"subtitleDisplay",get:function(){var t=this.subtitleTrackController;return!!t&&t.subtitleDisplay},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleDisplay=t)}},{key:"lowLatencyMode",get:function(){return this.config.lowLatencyMode},set:function(t){this.config.lowLatencyMode=t}},{key:"liveSyncPosition",get:function(){return this.latencyController.liveSyncPosition}},{key:"latency",get:function(){return this.latencyController.latency}},{key:"maxLatency",get:function(){return this.latencyController.maxLatency}},{key:"targetLatency",get:function(){return this.latencyController.targetLatency}},{key:"drift",get:function(){return this.latencyController.drift}},{key:"forceStartLoad",get:function(){return this.streamController.forceStartLoad}}],[{key:"version",get:function(){return"1.4.12"}},{key:"Events",get:function(){return S}},{key:"ErrorTypes",get:function(){return L}},{key:"ErrorDetails",get:function(){return R}},{key:"DefaultConfig",get:function(){return t.defaultConfig?t.defaultConfig:Wa},set:function(e){t.defaultConfig=e}}]),t}();return Xa.defaultConfig=void 0,Xa},"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(r="undefined"!=typeof globalThis?globalThis:r||self).Hls=i()}(!1);
-//# sourceMappingURL=hls.min.js.map
diff --git a/internal/core/hls_manager.go b/internal/core/hls_manager.go
deleted file mode 100644
index 2f43ccaed29..00000000000
--- a/internal/core/hls_manager.go
+++ /dev/null
@@ -1,323 +0,0 @@
-package core
-
-import (
- "context"
- "fmt"
- "sort"
- "sync"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-type hlsManagerAPIMuxersListRes struct {
- data *apiHLSMuxersList
- err error
-}
-
-type hlsManagerAPIMuxersListReq struct {
- res chan hlsManagerAPIMuxersListRes
-}
-
-type hlsManagerAPIMuxersGetRes struct {
- data *apiHLSMuxer
- err error
-}
-
-type hlsManagerAPIMuxersGetReq struct {
- name string
- res chan hlsManagerAPIMuxersGetRes
-}
-
-type hlsManagerParent interface {
- logger.Writer
-}
-
-type hlsManager struct {
- externalAuthenticationURL string
- alwaysRemux bool
- variant conf.HLSVariant
- segmentCount int
- segmentDuration conf.StringDuration
- partDuration conf.StringDuration
- segmentMaxSize conf.StringSize
- directory string
- writeQueueSize int
- pathManager *pathManager
- metrics *metrics
- parent hlsManagerParent
-
- ctx context.Context
- ctxCancel func()
- wg sync.WaitGroup
- httpServer *hlsHTTPServer
- muxers map[string]*hlsMuxer
-
- // in
- chPathReady chan *path
- chPathNotReady chan *path
- chHandleRequest chan hlsMuxerHandleRequestReq
- chCloseMuxer chan *hlsMuxer
- chAPIMuxerList chan hlsManagerAPIMuxersListReq
- chAPIMuxerGet chan hlsManagerAPIMuxersGetReq
-}
-
-func newHLSManager(
- address string,
- encryption bool,
- serverKey string,
- serverCert string,
- externalAuthenticationURL string,
- alwaysRemux bool,
- variant conf.HLSVariant,
- segmentCount int,
- segmentDuration conf.StringDuration,
- partDuration conf.StringDuration,
- segmentMaxSize conf.StringSize,
- allowOrigin string,
- trustedProxies conf.IPsOrCIDRs,
- directory string,
- readTimeout conf.StringDuration,
- writeQueueSize int,
- pathManager *pathManager,
- metrics *metrics,
- parent hlsManagerParent,
-) (*hlsManager, error) {
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- m := &hlsManager{
- externalAuthenticationURL: externalAuthenticationURL,
- alwaysRemux: alwaysRemux,
- variant: variant,
- segmentCount: segmentCount,
- segmentDuration: segmentDuration,
- partDuration: partDuration,
- segmentMaxSize: segmentMaxSize,
- directory: directory,
- writeQueueSize: writeQueueSize,
- pathManager: pathManager,
- parent: parent,
- metrics: metrics,
- ctx: ctx,
- ctxCancel: ctxCancel,
- muxers: make(map[string]*hlsMuxer),
- chPathReady: make(chan *path),
- chPathNotReady: make(chan *path),
- chHandleRequest: make(chan hlsMuxerHandleRequestReq),
- chCloseMuxer: make(chan *hlsMuxer),
- chAPIMuxerList: make(chan hlsManagerAPIMuxersListReq),
- chAPIMuxerGet: make(chan hlsManagerAPIMuxersGetReq),
- }
-
- var err error
- m.httpServer, err = newHLSHTTPServer(
- address,
- encryption,
- serverKey,
- serverCert,
- allowOrigin,
- trustedProxies,
- readTimeout,
- m.pathManager,
- m,
- )
- if err != nil {
- ctxCancel()
- return nil, err
- }
-
- m.Log(logger.Info, "listener opened on "+address)
-
- m.pathManager.setHLSManager(m)
-
- if m.metrics != nil {
- m.metrics.setHLSManager(m)
- }
-
- m.wg.Add(1)
- go m.run()
-
- return m, nil
-}
-
-// Log is the main logging function.
-func (m *hlsManager) Log(level logger.Level, format string, args ...interface{}) {
- m.parent.Log(level, "[HLS] "+format, args...)
-}
-
-func (m *hlsManager) close() {
- m.Log(logger.Info, "listener is closing")
- m.ctxCancel()
- m.wg.Wait()
-}
-
-func (m *hlsManager) run() {
- defer m.wg.Done()
-
-outer:
- for {
- select {
- case pa := <-m.chPathReady:
- if m.alwaysRemux && !pa.conf.SourceOnDemand {
- if _, ok := m.muxers[pa.name]; !ok {
- m.createMuxer(pa.name, "")
- }
- }
-
- case pa := <-m.chPathNotReady:
- c, ok := m.muxers[pa.name]
- if ok && c.remoteAddr == "" { // created with "always remux"
- c.close()
- delete(m.muxers, pa.name)
- }
-
- case req := <-m.chHandleRequest:
- r, ok := m.muxers[req.path]
- switch {
- case ok:
- r.processRequest(&req)
-
- default:
- r := m.createMuxer(req.path, req.ctx.ClientIP())
- r.processRequest(&req)
- }
-
- case c := <-m.chCloseMuxer:
- if c2, ok := m.muxers[c.PathName()]; !ok || c2 != c {
- continue
- }
- delete(m.muxers, c.PathName())
-
- case req := <-m.chAPIMuxerList:
- data := &apiHLSMuxersList{
- Items: []*apiHLSMuxer{},
- }
-
- for _, muxer := range m.muxers {
- data.Items = append(data.Items, muxer.apiItem())
- }
-
- sort.Slice(data.Items, func(i, j int) bool {
- return data.Items[i].Created.Before(data.Items[j].Created)
- })
-
- req.res <- hlsManagerAPIMuxersListRes{
- data: data,
- }
-
- case req := <-m.chAPIMuxerGet:
- muxer, ok := m.muxers[req.name]
- if !ok {
- req.res <- hlsManagerAPIMuxersGetRes{err: errAPINotFound}
- continue
- }
-
- req.res <- hlsManagerAPIMuxersGetRes{data: muxer.apiItem()}
-
- case <-m.ctx.Done():
- break outer
- }
- }
-
- m.ctxCancel()
-
- m.httpServer.close()
-
- m.pathManager.setHLSManager(nil)
-
- if m.metrics != nil {
- m.metrics.setHLSManager(nil)
- }
-}
-
-func (m *hlsManager) createMuxer(pathName string, remoteAddr string) *hlsMuxer {
- r := newHLSMuxer(
- m.ctx,
- remoteAddr,
- m.externalAuthenticationURL,
- m.variant,
- m.segmentCount,
- m.segmentDuration,
- m.partDuration,
- m.segmentMaxSize,
- m.directory,
- m.writeQueueSize,
- &m.wg,
- pathName,
- m.pathManager,
- m)
- m.muxers[pathName] = r
- return r
-}
-
-// closeMuxer is called by hlsMuxer.
-func (m *hlsManager) closeMuxer(c *hlsMuxer) {
- select {
- case m.chCloseMuxer <- c:
- case <-m.ctx.Done():
- }
-}
-
-// pathReady is called by pathManager.
-func (m *hlsManager) pathReady(pa *path) {
- select {
- case m.chPathReady <- pa:
- case <-m.ctx.Done():
- }
-}
-
-// pathNotReady is called by pathManager.
-func (m *hlsManager) pathNotReady(pa *path) {
- select {
- case m.chPathNotReady <- pa:
- case <-m.ctx.Done():
- }
-}
-
-// apiMuxersList is called by api.
-func (m *hlsManager) apiMuxersList() (*apiHLSMuxersList, error) {
- req := hlsManagerAPIMuxersListReq{
- res: make(chan hlsManagerAPIMuxersListRes),
- }
-
- select {
- case m.chAPIMuxerList <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-m.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-// apiMuxersGet is called by api.
-func (m *hlsManager) apiMuxersGet(name string) (*apiHLSMuxer, error) {
- req := hlsManagerAPIMuxersGetReq{
- name: name,
- res: make(chan hlsManagerAPIMuxersGetRes),
- }
-
- select {
- case m.chAPIMuxerGet <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-m.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-func (m *hlsManager) handleRequest(req hlsMuxerHandleRequestReq) {
- req.res = make(chan *hlsMuxer)
-
- select {
- case m.chHandleRequest <- req:
- muxer := <-req.res
- if muxer != nil {
- req.ctx.Request.URL.Path = req.file
- muxer.handleRequest(req.ctx)
- }
-
- case <-m.ctx.Done():
- }
-}
diff --git a/internal/core/hls_manager_test.go b/internal/core/hls_server_test.go
similarity index 97%
rename from internal/core/hls_manager_test.go
rename to internal/core/hls_server_test.go
index 29382405a31..71cbf6a6487 100644
--- a/internal/core/hls_manager_test.go
+++ b/internal/core/hls_server_test.go
@@ -114,7 +114,7 @@ func TestHLSReadNotFound(t *testing.T) {
func TestHLSRead(t *testing.T) {
p, ok := newInstance("hlsAlwaysRemux: yes\n" +
"paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -168,7 +168,7 @@ func TestHLSRead(t *testing.T) {
"#EXT-X-INDEPENDENT-SEGMENTS\n"+
"\n"+
"#EXT-X-STREAM-INF:BANDWIDTH=1192,AVERAGE-BANDWIDTH=1192,"+
- "CODECS=\"avc1.42c028\",RESOLUTION=1920x1084,FRAME-RATE=30.000\n"+
+ "CODECS=\"avc1.42c028\",RESOLUTION=1920x1080,FRAME-RATE=30.000\n"+
"stream.m3u8\n", string(cnt))
cnt = httpPullFile(t, hc, "http://localhost:8888/stream/stream.m3u8")
@@ -197,7 +197,7 @@ func TestHLSRead(t *testing.T) {
"#EXT-X-GAP\n"+
"#EXTINF:1\\.00000,\n"+
"gap.mp4\n"+
- "#EXT-X-PROGRAM-DATE-TIME:.+?Z\n"+
+ "#EXT-X-PROGRAM-DATE-TIME:.+?\n"+
"#EXT-X-PART:DURATION=1\\.00000,URI=\".*?_part0.mp4\",INDEPENDENT=YES\n"+
"#EXTINF:1\\.00000,\n"+
".*?_seg7.mp4\n"+
diff --git a/internal/core/hls_source_test.go b/internal/core/hls_source_test.go
deleted file mode 100644
index 44e4cb62c58..00000000000
--- a/internal/core/hls_source_test.go
+++ /dev/null
@@ -1,208 +0,0 @@
-package core
-
-import (
- "bytes"
- "context"
- "io"
- "net"
- "net/http"
- "testing"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
- "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
- "github.com/gin-gonic/gin"
- "github.com/pion/rtp"
- "github.com/stretchr/testify/require"
-)
-
-var track1 = &mpegts.Track{
- Codec: &mpegts.CodecH264{},
-}
-
-var track2 = &mpegts.Track{
- Codec: &mpegts.CodecMPEG4Audio{
- Config: mpeg4audio.Config{
- Type: 2,
- SampleRate: 44100,
- ChannelCount: 2,
- },
- },
-}
-
-type testHLSManager struct {
- s *http.Server
-
- clientConnected chan struct{}
-}
-
-func newTestHLSManager() (*testHLSManager, error) {
- ln, err := net.Listen("tcp", "localhost:5780")
- if err != nil {
- return nil, err
- }
-
- ts := &testHLSManager{
- clientConnected: make(chan struct{}),
- }
-
- gin.SetMode(gin.ReleaseMode)
- router := gin.New()
- router.GET("/stream.m3u8", ts.onPlaylist)
- router.GET("/segment1.ts", ts.onSegment1)
- router.GET("/segment2.ts", ts.onSegment2)
-
- ts.s = &http.Server{Handler: router}
- go ts.s.Serve(ln)
-
- return ts, nil
-}
-
-func (ts *testHLSManager) close() {
- ts.s.Shutdown(context.Background())
-}
-
-func (ts *testHLSManager) onPlaylist(ctx *gin.Context) {
- cnt := `#EXTM3U
-#EXT-X-VERSION:3
-#EXT-X-ALLOW-CACHE:NO
-#EXT-X-TARGETDURATION:2
-#EXT-X-MEDIA-SEQUENCE:0
-#EXTINF:2,
-segment1.ts
-#EXTINF:2,
-segment2.ts
-#EXT-X-ENDLIST
-`
-
- ctx.Writer.Header().Set("Content-Type", `application/vnd.apple.mpegurl`)
- io.Copy(ctx.Writer, bytes.NewReader([]byte(cnt)))
-}
-
-func (ts *testHLSManager) onSegment1(ctx *gin.Context) {
- ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
-
- w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2})
-
- w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}}) //nolint:errcheck
-}
-
-func (ts *testHLSManager) onSegment2(ctx *gin.Context) {
- <-ts.clientConnected
-
- ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
-
- w := mpegts.NewWriter(ctx.Writer, []*mpegts.Track{track1, track2})
-
- w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{ //nolint:errcheck
- {7, 1, 2, 3}, // SPS
- {8}, // PPS
- })
-
- w.WriteMPEG4Audio(track2, 2*90000, [][]byte{{1, 2, 3, 4}}) //nolint:errcheck
-
- w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{ //nolint:errcheck
- {5}, // IDR
- })
-}
-
-func TestHLSSource(t *testing.T) {
- ts, err := newTestHLSManager()
- require.NoError(t, err)
- defer ts.close()
-
- p, ok := newInstance("rtmp: no\n" +
- "hls: no\n" +
- "webrtc: no\n" +
- "paths:\n" +
- " proxied:\n" +
- " source: http://localhost:5780/stream.m3u8\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
-
- frameRecv := make(chan struct{})
-
- c := gortsplib.Client{}
-
- u, err := url.Parse("rtsp://localhost:8554/proxied")
- require.NoError(t, err)
-
- err = c.Start(u.Scheme, u.Host)
- require.NoError(t, err)
- defer c.Close()
-
- desc, _, err := c.Describe(u)
- require.NoError(t, err)
-
- require.Equal(t, []*description.Media{
- {
- Type: description.MediaTypeVideo,
- Control: desc.Medias[0].Control,
- Formats: []format.Format{
- &format.H264{
- PayloadTyp: 96,
- PacketizationMode: 1,
- },
- },
- },
- {
- Type: description.MediaTypeAudio,
- Control: desc.Medias[1].Control,
- Formats: []format.Format{
- &format.MPEG4Audio{
- PayloadTyp: 96,
- ProfileLevelID: 1,
- Config: &mpeg4audio.Config{
- Type: 2,
- SampleRate: 44100,
- ChannelCount: 2,
- },
- SizeLength: 13,
- IndexLength: 3,
- IndexDeltaLength: 3,
- },
- },
- },
- }, desc.Medias)
-
- var forma *format.H264
- medi := desc.FindFormat(&forma)
-
- _, err = c.Setup(desc.BaseURL, medi, 0, 0)
- require.NoError(t, err)
-
- c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
- require.Equal(t, &rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: pkt.SequenceNumber,
- Timestamp: pkt.Timestamp,
- SSRC: pkt.SSRC,
- CSRC: []uint32{},
- },
- Payload: []byte{
- 0x18,
- 0x00, 0x04,
- 0x07, 0x01, 0x02, 0x03, // SPS
- 0x00, 0x01,
- 0x08, // PPS
- 0x00, 0x01,
- 0x05, // IDR
- },
- }, pkt)
- close(frameRecv)
- })
-
- _, err = c.Play(nil)
- require.NoError(t, err)
-
- close(ts.clientConnected)
-
- <-frameRecv
-}
diff --git a/internal/core/metrics_test.go b/internal/core/metrics_test.go
index dc335bf6b63..4c5bc08d00b 100644
--- a/internal/core/metrics_test.go
+++ b/internal/core/metrics_test.go
@@ -1,20 +1,27 @@
package core
import (
+ "bufio"
+ "context"
"crypto/tls"
"net"
"net/http"
"net/url"
"os"
+ "sync"
"testing"
"time"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
+ srt "github.com/datarhei/gosrt"
+ "github.com/pion/rtp"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp"
+ "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
)
func TestMetrics(t *testing.T) {
@@ -26,23 +33,28 @@ func TestMetrics(t *testing.T) {
require.NoError(t, err)
defer os.Remove(serverKeyFpath)
- p, ok := newInstance("hlsAlwaysRemux: yes\n" +
+ p, ok := newInstance("api: yes\n" +
+ "hlsAlwaysRemux: yes\n" +
"metrics: yes\n" +
"webrtcServerCert: " + serverCertFpath + "\n" +
"webrtcServerKey: " + serverKeyFpath + "\n" +
"encryption: optional\n" +
"serverCert: " + serverCertFpath + "\n" +
"serverKey: " + serverKeyFpath + "\n" +
+ "rtmpEncryption: optional\n" +
+ "rtmpServerCert: " + serverCertFpath + "\n" +
+ "rtmpServerKey: " + serverKeyFpath + "\n" +
"paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
hc := &http.Client{Transport: &http.Transport{}}
- bo := httpPullFile(t, hc, "http://localhost:9998/metrics")
+ t.Run("initial", func(t *testing.T) {
+ bo := httpPullFile(t, hc, "http://localhost:9998/metrics")
- require.Equal(t, `paths 0
+ require.Equal(t, `paths 0
hls_muxers 0
hls_muxers_bytes_sent 0
rtsp_conns 0
@@ -60,84 +72,235 @@ rtsps_sessions_bytes_sent 0
rtmp_conns 0
rtmp_conns_bytes_received 0
rtmp_conns_bytes_sent 0
+rtmps_conns 0
+rtmps_conns_bytes_received 0
+rtmps_conns_bytes_sent 0
+srt_conns 0
+srt_conns_bytes_received 0
+srt_conns_bytes_sent 0
webrtc_sessions 0
webrtc_sessions_bytes_received 0
webrtc_sessions_bytes_sent 0
`, string(bo))
+ })
- medi := testMediaH264
+ t.Run("with data", func(t *testing.T) {
+ terminate := make(chan struct{})
+ var wg sync.WaitGroup
+ wg.Add(6)
- source := gortsplib.Client{}
- err = source.StartRecording("rtsp://localhost:8554/rtsp_path",
- &description.Session{Medias: []*description.Media{medi}})
- require.NoError(t, err)
- defer source.Close()
+ go func() {
+ defer wg.Done()
+ source := gortsplib.Client{}
+ err := source.StartRecording("rtsp://localhost:8554/rtsp_path",
+ &description.Session{Medias: []*description.Media{{
+ Type: description.MediaTypeVideo,
+ Formats: []format.Format{testFormatH264},
+ }}})
+ require.NoError(t, err)
+ defer source.Close()
+ <-terminate
+ }()
- source2 := gortsplib.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}}
- err = source2.StartRecording("rtsps://localhost:8322/rtsps_path",
- &description.Session{Medias: []*description.Media{medi}})
- require.NoError(t, err)
- defer source2.Close()
+ go func() {
+ defer wg.Done()
+ source2 := gortsplib.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}}
+ err := source2.StartRecording("rtsps://localhost:8322/rtsps_path",
+ &description.Session{Medias: []*description.Media{{
+ Type: description.MediaTypeVideo,
+ Formats: []format.Format{testFormatH264},
+ }}})
+ require.NoError(t, err)
+ defer source2.Close()
+ <-terminate
+ }()
- u, err := url.Parse("rtmp://localhost:1935/rtmp_path")
- require.NoError(t, err)
+ go func() {
+ defer wg.Done()
+ u, err := url.Parse("rtmp://localhost:1935/rtmp_path")
+ require.NoError(t, err)
- nconn, err := net.Dial("tcp", u.Host)
- require.NoError(t, err)
- defer nconn.Close()
+ nconn, err := net.Dial("tcp", u.Host)
+ require.NoError(t, err)
+ defer nconn.Close()
- conn, err := rtmp.NewClientConn(nconn, u, true)
- require.NoError(t, err)
+ conn, err := rtmp.NewClientConn(nconn, u, true)
+ require.NoError(t, err)
- videoTrack := &format.H264{
- PayloadTyp: 96,
- SPS: []byte{ // 1920x1080 baseline
- 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,
- },
- PPS: []byte{0x08, 0x06, 0x07, 0x08},
- PacketizationMode: 1,
- }
-
- _, err = rtmp.NewWriter(conn, videoTrack, nil)
- require.NoError(t, err)
+ _, err = rtmp.NewWriter(conn, testFormatH264, nil)
+ require.NoError(t, err)
+ <-terminate
+ }()
+
+ go func() {
+ defer wg.Done()
+ u, err := url.Parse("rtmp://localhost:1936/rtmps_path")
+ require.NoError(t, err)
+
+ nconn, err := tls.Dial("tcp", u.Host, &tls.Config{InsecureSkipVerify: true})
+ require.NoError(t, err)
+ defer nconn.Close() //nolint:errcheck
+
+ conn, err := rtmp.NewClientConn(nconn, u, true)
+ require.NoError(t, err)
+
+ _, err = rtmp.NewWriter(conn, testFormatH264, nil)
+ require.NoError(t, err)
+ <-terminate
+ }()
+
+ go func() {
+ defer wg.Done()
+
+ su, err := url.Parse("http://localhost:8889/webrtc_path/whip")
+ require.NoError(t, err)
+
+ s := &webrtc.WHIPClient{
+ HTTPClient: &http.Client{Transport: &http.Transport{}},
+ URL: su,
+ }
+
+ tracks, err := s.Publish(context.Background(), testMediaH264.Formats[0], nil)
+ require.NoError(t, err)
+ defer checkClose(t, s.Close)
+
+ err = tracks[0].WriteRTP(&rtp.Packet{
+ Header: rtp.Header{
+ Version: 2,
+ Marker: true,
+ PayloadType: 96,
+ SequenceNumber: 123,
+ Timestamp: 45343,
+ SSRC: 563423,
+ },
+ Payload: []byte{1},
+ })
+ require.NoError(t, err)
+ <-terminate
+ }()
+
+ go func() {
+ defer wg.Done()
+
+ srtConf := srt.DefaultConfig()
+ address, err := srtConf.UnmarshalURL("srt://localhost:8890?streamid=publish:srt_path")
+ require.NoError(t, err)
+
+ err = srtConf.Validate()
+ require.NoError(t, err)
+
+ publisher, err := srt.Dial("srt", address, srtConf)
+ require.NoError(t, err)
+ defer publisher.Close()
+
+ track := &mpegts.Track{
+ Codec: &mpegts.CodecH264{},
+ }
+
+ bw := bufio.NewWriter(publisher)
+ w := mpegts.NewWriter(bw, []*mpegts.Track{track})
+ require.NoError(t, err)
+
+ err = w.WriteH26x(track, 0, 0, true, [][]byte{
+ { // SPS
+ 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,
+ },
+ { // PPS
+ 0x08, 0x06, 0x07, 0x08,
+ },
+ { // IDR
+ 0x05, 1,
+ },
+ })
+ require.NoError(t, err)
+
+ err = bw.Flush()
+ require.NoError(t, err)
+ <-terminate
+ }()
+
+ time.Sleep(500 * time.Millisecond)
+
+ bo := httpPullFile(t, hc, "http://localhost:9998/metrics")
+
+ require.Regexp(t,
+ `^paths\{name=".*?",state="ready"\} 1`+"\n"+
+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths\{name=".*?",state="ready"\} 1`+"\n"+
+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths\{name=".*?",state="ready"\} 1`+"\n"+
+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths\{name=".*?",state="ready"\} 1`+"\n"+
+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths\{name=".*?",state="ready"\} 1`+"\n"+
+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths\{name=".*?",state="ready"\} 1`+"\n"+
+ `paths_bytes_received\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `paths_bytes_sent\{name=".*?",state="ready"\} [0-9]+`+"\n"+
+ `hls_muxers\{name=".*?"\} 1`+"\n"+
+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+
+ `hls_muxers\{name=".*?"\} 1`+"\n"+
+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+
+ `hls_muxers\{name=".*?"\} 1`+"\n"+
+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+
+ `hls_muxers\{name=".*?"\} 1`+"\n"+
+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+
+ `hls_muxers\{name=".*?"\} 1`+"\n"+
+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+
+ `hls_muxers\{name=".*?"\} 1`+"\n"+
+ `hls_muxers_bytes_sent\{name=".*?"\} 0`+"\n"+
+ `rtsp_conns\{id=".*?"\} 1`+"\n"+
+ `rtsp_conns_bytes_received\{id=".*?"\} [0-9]+`+"\n"+
+ `rtsp_conns_bytes_sent\{id=".*?"\} [0-9]+`+"\n"+
+ `rtsp_sessions\{id=".*?",state="publish"\} 1`+"\n"+
+ `rtsp_sessions_bytes_received\{id=".*?",state="publish"\} 0`+"\n"+
+ `rtsp_sessions_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `rtsps_conns\{id=".*?"\} 1`+"\n"+
+ `rtsps_conns_bytes_received\{id=".*?"\} [0-9]+`+"\n"+
+ `rtsps_conns_bytes_sent\{id=".*?"\} [0-9]+`+"\n"+
+ `rtsps_sessions\{id=".*?",state="publish"\} 1`+"\n"+
+ `rtsps_sessions_bytes_received\{id=".*?",state="publish"\} 0`+"\n"+
+ `rtsps_sessions_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `rtmp_conns\{id=".*?",state="publish"\} 1`+"\n"+
+ `rtmp_conns_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `rtmp_conns_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `rtmps_conns\{id=".*?",state="publish"\} 1`+"\n"+
+ `rtmps_conns_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `rtmps_conns_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `srt_conns\{id=".*?",state="publish"\} 1`+"\n"+
+ `srt_conns_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `srt_conns_bytes_sent\{id=".*?",state="publish"\} 0`+"\n"+
+ `webrtc_sessions\{id=".*?",state="publish"\} 1`+"\n"+
+ `webrtc_sessions_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ `webrtc_sessions_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
+ "$",
+ string(bo))
+
+ close(terminate)
+ wg.Wait()
+ })
+
+ t.Run("servers deleted", func(t *testing.T) {
+ httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/global/patch", map[string]interface{}{
+ "rtsp": false,
+ "rtmp": false,
+ "srt": false,
+ "hls": false,
+ "webrtc": false,
+ }, nil)
+
+ time.Sleep(500 * time.Millisecond)
+
+ bo := httpPullFile(t, hc, "http://localhost:9998/metrics")
- time.Sleep(500 * time.Millisecond)
-
- bo = httpPullFile(t, hc, "http://localhost:9998/metrics")
-
- require.Regexp(t,
- `^paths\{name=".*?",state="ready"\} 1`+"\n"+
- `paths_bytes_received\{name=".*?",state="ready"\} 0`+"\n"+
- `paths\{name=".*?",state="ready"\} 1`+"\n"+
- `paths_bytes_received\{name=".*?",state="ready"\} 0`+"\n"+
- `paths\{name=".*?",state="ready"\} 1`+"\n"+
- `paths_bytes_received\{name=".*?",state="ready"\} 0`+"\n"+
- `hls_muxers\{name=".*?"\} 1`+"\n"+
- `hls_muxers_bytes_sent\{name=".*?"\} [0-9]+`+"\n"+
- `hls_muxers\{name=".*?"\} 1`+"\n"+
- `hls_muxers_bytes_sent\{name=".*?"\} [0-9]+`+"\n"+
- `hls_muxers\{name=".*?"\} 1`+"\n"+
- `hls_muxers_bytes_sent\{name=".*?"\} [0-9]+`+"\n"+
- `rtsp_conns\{id=".*?"\} 1`+"\n"+
- `rtsp_conns_bytes_received\{id=".*?"\} [0-9]+`+"\n"+
- `rtsp_conns_bytes_sent\{id=".*?"\} [0-9]+`+"\n"+
- `rtsp_sessions\{id=".*?",state="publish"\} 1`+"\n"+
- `rtsp_sessions_bytes_received\{id=".*?",state="publish"\} 0`+"\n"+
- `rtsp_sessions_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
- `rtsps_conns\{id=".*?"\} 1`+"\n"+
- `rtsps_conns_bytes_received\{id=".*?"\} [0-9]+`+"\n"+
- `rtsps_conns_bytes_sent\{id=".*?"\} [0-9]+`+"\n"+
- `rtsps_sessions\{id=".*?",state="publish"\} 1`+"\n"+
- `rtsps_sessions_bytes_received\{id=".*?",state="publish"\} 0`+"\n"+
- `rtsps_sessions_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
- `rtmp_conns\{id=".*?",state="publish"\} 1`+"\n"+
- `rtmp_conns_bytes_received\{id=".*?",state="publish"\} [0-9]+`+"\n"+
- `rtmp_conns_bytes_sent\{id=".*?",state="publish"\} [0-9]+`+"\n"+
- `webrtc_sessions 0`+"\n"+
- `webrtc_sessions_bytes_received 0`+"\n"+
- `webrtc_sessions_bytes_sent 0`+"\n"+
- "$",
- string(bo))
+ require.Equal(t, "paths 0\n", string(bo))
+ })
}
diff --git a/internal/core/path.go b/internal/core/path.go
index 951b2cf2fd3..a88cbc8e22f 100644
--- a/internal/core/path.go
+++ b/internal/core/path.go
@@ -9,11 +9,13 @@ import (
"sync"
"time"
+ "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
"github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
+ "github.com/bluenviron/mediamtx/internal/hooks"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/record"
"github.com/bluenviron/mediamtx/internal/stream"
@@ -25,15 +27,6 @@ func newEmptyTimer() *time.Timer {
return t
}
-type errPathNoOnePublishing struct {
- pathName string
-}
-
-// Error implements the error interface.
-func (e errPathNoOnePublishing) Error() string {
- return fmt.Sprintf("no one is publishing to path '%s'", e.pathName)
-}
-
type pathParent interface {
logger.Writer
pathReady(*path)
@@ -50,103 +43,8 @@ const (
pathOnDemandStateClosing
)
-type pathSourceStaticSetReadyRes struct {
- stream *stream.Stream
- err error
-}
-
-type pathSourceStaticSetReadyReq struct {
- desc *description.Session
- generateRTPPackets bool
- res chan pathSourceStaticSetReadyRes
-}
-
-type pathSourceStaticSetNotReadyReq struct {
- res chan struct{}
-}
-
-type pathRemoveReaderReq struct {
- author reader
- res chan struct{}
-}
-
-type pathRemovePublisherReq struct {
- author publisher
- res chan struct{}
-}
-
-type pathGetConfForPathRes struct {
- conf *conf.PathConf
- err error
-}
-
-type pathGetConfForPathReq struct {
- name string
- publish bool
- credentials authCredentials
- res chan pathGetConfForPathRes
-}
-
-type pathDescribeRes struct {
- path *path
- stream *stream.Stream
- redirect string
- err error
-}
-
-type pathDescribeReq struct {
- pathName string
- url *url.URL
- credentials authCredentials
- res chan pathDescribeRes
-}
-
-type pathAddReaderRes struct {
- path *path
- stream *stream.Stream
- err error
-}
-
-type pathAddReaderReq struct {
- author reader
- pathName string
- skipAuth bool
- credentials authCredentials
- res chan pathAddReaderRes
-}
-
-type pathAddPublisherRes struct {
- path *path
- err error
-}
-
-type pathAddPublisherReq struct {
- author publisher
- pathName string
- skipAuth bool
- credentials authCredentials
- res chan pathAddPublisherRes
-}
-
-type pathStartPublisherRes struct {
- stream *stream.Stream
- err error
-}
-
-type pathStartPublisherReq struct {
- author publisher
- desc *description.Session
- generateRTPPackets bool
- res chan pathStartPublisherRes
-}
-
-type pathStopPublisherReq struct {
- author publisher
- res chan struct{}
-}
-
type pathAPIPathsListRes struct {
- data *apiPathsList
+ data *defs.APIPathList
paths map[string]*path
}
@@ -156,7 +54,7 @@ type pathAPIPathsListReq struct {
type pathAPIPathsGetRes struct {
path *path
- data *apiPath
+ data *defs.APIPath
err error
}
@@ -166,35 +64,34 @@ type pathAPIPathsGetReq struct {
}
type path struct {
- rtspAddress string
- readTimeout conf.StringDuration
- writeTimeout conf.StringDuration
- writeQueueSize int
- udpMaxPayloadSize int
- record bool
- recordPath string
- recordPartDuration conf.StringDuration
- recordSegmentDuration conf.StringDuration
- confName string
- conf *conf.PathConf
- name string
- matches []string
- wg *sync.WaitGroup
- externalCmdPool *externalcmd.Pool
- parent pathParent
+ parentCtx context.Context
+ logLevel conf.LogLevel
+ rtspAddress string
+ readTimeout conf.StringDuration
+ writeTimeout conf.StringDuration
+ writeQueueSize int
+ udpMaxPayloadSize int
+ confName string
+ conf *conf.Path
+ name string
+ matches []string
+ wg *sync.WaitGroup
+ externalCmdPool *externalcmd.Pool
+ parent pathParent
ctx context.Context
ctxCancel func()
confMutex sync.RWMutex
- source source
+ source defs.Source
+ publisherQuery string
stream *stream.Stream
recordAgent *record.Agent
readyTime time.Time
- readers map[reader]struct{}
- describeRequestsOnHold []pathDescribeReq
- readerAddRequestsOnHold []pathAddReaderReq
- onDemandCmd *externalcmd.Cmd
- onReadyCmd *externalcmd.Cmd
+ onUnDemandHook func(string)
+ onNotReadyHook func()
+ readers map[defs.Reader]struct{}
+ describeRequestsOnHold []defs.PathDescribeReq
+ readerAddRequestsOnHold []defs.PathAddReaderReq
onDemandStaticSourceState pathOnDemandState
onDemandStaticSourceReadyTimer *time.Timer
onDemandStaticSourceCloseTimer *time.Timer
@@ -203,87 +100,49 @@ type path struct {
onDemandPublisherCloseTimer *time.Timer
// in
- chReloadConf chan *conf.PathConf
- chSourceStaticSetReady chan pathSourceStaticSetReadyReq
- chSourceStaticSetNotReady chan pathSourceStaticSetNotReadyReq
- chDescribe chan pathDescribeReq
- chRemovePublisher chan pathRemovePublisherReq
- chAddPublisher chan pathAddPublisherReq
- chStartPublisher chan pathStartPublisherReq
- chStopPublisher chan pathStopPublisherReq
- chAddReader chan pathAddReaderReq
- chRemoveReader chan pathRemoveReaderReq
+ chReloadConf chan *conf.Path
+ chStaticSourceSetReady chan defs.PathSourceStaticSetReadyReq
+ chStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq
+ chDescribe chan defs.PathDescribeReq
+ chAddPublisher chan defs.PathAddPublisherReq
+ chRemovePublisher chan defs.PathRemovePublisherReq
+ chStartPublisher chan defs.PathStartPublisherReq
+ chStopPublisher chan defs.PathStopPublisherReq
+ chAddReader chan defs.PathAddReaderReq
+ chRemoveReader chan defs.PathRemoveReaderReq
chAPIPathsGet chan pathAPIPathsGetReq
// out
done chan struct{}
}
-func newPath(
- parentCtx context.Context,
- rtspAddress string,
- readTimeout conf.StringDuration,
- writeTimeout conf.StringDuration,
- writeQueueSize int,
- udpMaxPayloadSize int,
- record bool,
- recordPath string,
- recordPartDuration conf.StringDuration,
- recordSegmentDuration conf.StringDuration,
- confName string,
- cnf *conf.PathConf,
- name string,
- matches []string,
- wg *sync.WaitGroup,
- externalCmdPool *externalcmd.Pool,
- parent pathParent,
-) *path {
- ctx, ctxCancel := context.WithCancel(parentCtx)
-
- pa := &path{
- rtspAddress: rtspAddress,
- readTimeout: readTimeout,
- writeTimeout: writeTimeout,
- writeQueueSize: writeQueueSize,
- udpMaxPayloadSize: udpMaxPayloadSize,
- record: record,
- recordPath: recordPath,
- recordPartDuration: recordPartDuration,
- recordSegmentDuration: recordSegmentDuration,
- confName: confName,
- conf: cnf,
- name: name,
- matches: matches,
- wg: wg,
- externalCmdPool: externalCmdPool,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- readers: make(map[reader]struct{}),
- onDemandStaticSourceReadyTimer: newEmptyTimer(),
- onDemandStaticSourceCloseTimer: newEmptyTimer(),
- onDemandPublisherReadyTimer: newEmptyTimer(),
- onDemandPublisherCloseTimer: newEmptyTimer(),
- chReloadConf: make(chan *conf.PathConf),
- chSourceStaticSetReady: make(chan pathSourceStaticSetReadyReq),
- chSourceStaticSetNotReady: make(chan pathSourceStaticSetNotReadyReq),
- chDescribe: make(chan pathDescribeReq),
- chRemovePublisher: make(chan pathRemovePublisherReq),
- chAddPublisher: make(chan pathAddPublisherReq),
- chStartPublisher: make(chan pathStartPublisherReq),
- chStopPublisher: make(chan pathStopPublisherReq),
- chAddReader: make(chan pathAddReaderReq),
- chRemoveReader: make(chan pathRemoveReaderReq),
- chAPIPathsGet: make(chan pathAPIPathsGetReq),
- done: make(chan struct{}),
- }
+func (pa *path) initialize() {
+ ctx, ctxCancel := context.WithCancel(pa.parentCtx)
+
+ pa.ctx = ctx
+ pa.ctxCancel = ctxCancel
+ pa.readers = make(map[defs.Reader]struct{})
+ pa.onDemandStaticSourceReadyTimer = newEmptyTimer()
+ pa.onDemandStaticSourceCloseTimer = newEmptyTimer()
+ pa.onDemandPublisherReadyTimer = newEmptyTimer()
+ pa.onDemandPublisherCloseTimer = newEmptyTimer()
+ pa.chReloadConf = make(chan *conf.Path)
+ pa.chStaticSourceSetReady = make(chan defs.PathSourceStaticSetReadyReq)
+ pa.chStaticSourceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
+ pa.chDescribe = make(chan defs.PathDescribeReq)
+ pa.chAddPublisher = make(chan defs.PathAddPublisherReq)
+ pa.chRemovePublisher = make(chan defs.PathRemovePublisherReq)
+ pa.chStartPublisher = make(chan defs.PathStartPublisherReq)
+ pa.chStopPublisher = make(chan defs.PathStopPublisherReq)
+ pa.chAddReader = make(chan defs.PathAddReaderReq)
+ pa.chRemoveReader = make(chan defs.PathRemoveReaderReq)
+ pa.chAPIPathsGet = make(chan pathAPIPathsGetReq)
+ pa.done = make(chan struct{})
pa.Log(logger.Debug, "created")
pa.wg.Add(1)
go pa.run()
-
- return pa
}
func (pa *path) close() {
@@ -294,11 +153,15 @@ func (pa *path) wait() {
<-pa.done
}
-// Log is the main logging function.
+// Log implements logger.Writer.
func (pa *path) Log(level logger.Level, format string, args ...interface{}) {
pa.parent.Log(level, "[path "+pa.name+"] "+format, args...)
}
+func (pa *path) Name() string {
+ return pa.name
+}
+
func (pa *path) run() {
defer close(pa.done)
defer pa.wg.Done()
@@ -306,30 +169,35 @@ func (pa *path) run() {
if pa.conf.Source == "redirect" {
pa.source = &sourceRedirect{}
} else if pa.conf.HasStaticSource() {
- pa.source = newSourceStatic(
- pa.conf,
- pa.readTimeout,
- pa.writeTimeout,
- pa.writeQueueSize,
- pa)
+ 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,
+ parent: pa,
+ }
+ pa.source.(*staticSourceHandler).initialize()
if !pa.conf.SourceOnDemand {
- pa.source.(*sourceStatic).start(false)
+ pa.source.(*staticSourceHandler).start(false)
}
}
- var onInitCmd *externalcmd.Cmd
- if pa.conf.RunOnInit != "" {
- pa.Log(logger.Info, "runOnInit command started")
- onInitCmd = externalcmd.NewCmd(
- pa.externalCmdPool,
- pa.conf.RunOnInit,
- pa.conf.RunOnInitRestart,
- pa.externalCmdEnv(),
- func(err error) {
- pa.Log(logger.Info, "runOnInit command exited: %v", err)
- })
- }
+ onUnInitHook := hooks.OnInit(hooks.OnInitParams{
+ Logger: pa,
+ ExternalCmdPool: pa.externalCmdPool,
+ Conf: pa.conf,
+ ExternalCmdEnv: pa.ExternalCmdEnv(),
+ })
err := pa.runInner()
@@ -343,17 +211,14 @@ func (pa *path) run() {
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherCloseTimer.Stop()
- if onInitCmd != nil {
- onInitCmd.Close()
- pa.Log(logger.Info, "runOnInit command stopped")
- }
+ onUnInitHook()
for _, req := range pa.describeRequestsOnHold {
- req.res <- pathDescribeRes{err: fmt.Errorf("terminated")}
+ req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
}
for _, req := range pa.readerAddRequestsOnHold {
- req.res <- pathAddReaderRes{err: fmt.Errorf("terminated")}
+ req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("terminated")}
}
if pa.stream != nil {
@@ -361,18 +226,17 @@ func (pa *path) run() {
}
if pa.source != nil {
- if source, ok := pa.source.(*sourceStatic); ok {
+ if source, ok := pa.source.(*staticSourceHandler); ok {
if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial {
source.close("path is closing")
}
- } else if source, ok := pa.source.(publisher); ok {
- source.close()
+ } else if source, ok := pa.source.(defs.Publisher); ok {
+ source.Close()
}
}
- if pa.onDemandCmd != nil {
- pa.onDemandCmd.Close()
- pa.Log(logger.Info, "runOnDemand command stopped")
+ if pa.onUnDemandHook != nil {
+ pa.onUnDemandHook("path destroyed")
}
pa.Log(logger.Debug, "destroyed: %v", err)
@@ -405,17 +269,13 @@ func (pa *path) runInner() error {
case <-pa.onDemandPublisherCloseTimer.C:
pa.doOnDemandPublisherCloseTimer()
- if pa.shouldClose() {
- return fmt.Errorf("not in use")
- }
-
case newConf := <-pa.chReloadConf:
pa.doReloadConf(newConf)
- case req := <-pa.chSourceStaticSetReady:
+ case req := <-pa.chStaticSourceSetReady:
pa.doSourceStaticSetReady(req)
- case req := <-pa.chSourceStaticSetNotReady:
+ case req := <-pa.chStaticSourceSetNotReady:
pa.doSourceStaticSetNotReady(req)
if pa.shouldClose() {
@@ -429,6 +289,9 @@ func (pa *path) runInner() error {
return fmt.Errorf("not in use")
}
+ case req := <-pa.chAddPublisher:
+ pa.doAddPublisher(req)
+
case req := <-pa.chRemovePublisher:
pa.doRemovePublisher(req)
@@ -436,9 +299,6 @@ func (pa *path) runInner() error {
return fmt.Errorf("not in use")
}
- case req := <-pa.chAddPublisher:
- pa.doAddPublisher(req)
-
case req := <-pa.chStartPublisher:
pa.doStartPublisher(req)
@@ -470,12 +330,12 @@ func (pa *path) runInner() error {
func (pa *path) doOnDemandStaticSourceReadyTimer() {
for _, req := range pa.describeRequestsOnHold {
- req.res <- pathDescribeRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
+ req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.describeRequestsOnHold = nil
for _, req := range pa.readerAddRequestsOnHold {
- req.res <- pathAddReaderRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
+ req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.readerAddRequestsOnHold = nil
@@ -489,12 +349,12 @@ func (pa *path) doOnDemandStaticSourceCloseTimer() {
func (pa *path) doOnDemandPublisherReadyTimer() {
for _, req := range pa.describeRequestsOnHold {
- req.res <- pathDescribeRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
+ req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.describeRequestsOnHold = nil
for _, req := range pa.readerAddRequestsOnHold {
- req.res <- pathAddReaderRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
+ req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.readerAddRequestsOnHold = nil
@@ -505,16 +365,16 @@ func (pa *path) doOnDemandPublisherCloseTimer() {
pa.onDemandPublisherStop("not needed by anyone")
}
-func (pa *path) doReloadConf(newConf *conf.PathConf) {
+func (pa *path) doReloadConf(newConf *conf.Path) {
pa.confMutex.Lock()
pa.conf = newConf
pa.confMutex.Unlock()
if pa.conf.HasStaticSource() {
- go pa.source.(*sourceStatic).reloadConf(newConf)
+ go pa.source.(*staticSourceHandler).reloadConf(newConf)
}
- if pa.recordingEnabled() {
+ if pa.conf.Record {
if pa.stream != nil && pa.recordAgent == nil {
pa.startRecording()
}
@@ -524,58 +384,47 @@ func (pa *path) doReloadConf(newConf *conf.PathConf) {
}
}
-func (pa *path) doSourceStaticSetReady(req pathSourceStaticSetReadyReq) {
- err := pa.setReady(req.desc, req.generateRTPPackets)
+func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {
+ err := pa.setReady(req.Desc, req.GenerateRTPPackets)
if err != nil {
- req.res <- pathSourceStaticSetReadyRes{err: err}
+ req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
return
}
if pa.conf.HasOnDemandStaticSource() {
pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceReadyTimer = newEmptyTimer()
-
pa.onDemandStaticSourceScheduleClose()
-
- for _, req := range pa.describeRequestsOnHold {
- req.res <- pathDescribeRes{
- stream: pa.stream,
- }
- }
- pa.describeRequestsOnHold = nil
-
- for _, req := range pa.readerAddRequestsOnHold {
- pa.addReaderPost(req)
- }
- pa.readerAddRequestsOnHold = nil
}
- req.res <- pathSourceStaticSetReadyRes{stream: pa.stream}
+ pa.consumeOnHoldRequests()
+
+ req.Res <- defs.PathSourceStaticSetReadyRes{Stream: pa.stream}
}
-func (pa *path) doSourceStaticSetNotReady(req pathSourceStaticSetNotReadyReq) {
+func (pa *path) doSourceStaticSetNotReady(req defs.PathSourceStaticSetNotReadyReq) {
pa.setNotReady()
// send response before calling onDemandStaticSourceStop()
- // in order to avoid a deadlock due to sourceStatic.stop()
- close(req.res)
+ // in order to avoid a deadlock due to staticSourceHandler.stop()
+ close(req.Res)
if pa.conf.HasOnDemandStaticSource() && pa.onDemandStaticSourceState != pathOnDemandStateInitial {
pa.onDemandStaticSourceStop("an error occurred")
}
}
-func (pa *path) doDescribe(req pathDescribeReq) {
+func (pa *path) doDescribe(req defs.PathDescribeReq) {
if _, ok := pa.source.(*sourceRedirect); ok {
- req.res <- pathDescribeRes{
- redirect: pa.conf.SourceRedirect,
+ req.Res <- defs.PathDescribeRes{
+ Redirect: pa.conf.SourceRedirect,
}
return
}
if pa.stream != nil {
- req.res <- pathDescribeRes{
- stream: pa.stream,
+ req.Res <- defs.PathDescribeRes{
+ Stream: pa.stream,
}
return
}
@@ -590,7 +439,7 @@ func (pa *path) doDescribe(req pathDescribeReq) {
if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateInitial {
- pa.onDemandPublisherStart()
+ pa.onDemandPublisherStart(req.AccessRequest.Query)
}
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)
return
@@ -599,100 +448,90 @@ func (pa *path) doDescribe(req pathDescribeReq) {
if pa.conf.Fallback != "" {
fallbackURL := func() string {
if strings.HasPrefix(pa.conf.Fallback, "/") {
- ur := url.URL{
- Scheme: req.url.Scheme,
- User: req.url.User,
- Host: req.url.Host,
+ ur := base.URL{
+ Scheme: req.AccessRequest.RTSPRequest.URL.Scheme,
+ User: req.AccessRequest.RTSPRequest.URL.User,
+ Host: req.AccessRequest.RTSPRequest.URL.Host,
Path: pa.conf.Fallback,
}
return ur.String()
}
return pa.conf.Fallback
}()
- req.res <- pathDescribeRes{redirect: fallbackURL}
+ req.Res <- defs.PathDescribeRes{Redirect: fallbackURL}
return
}
- req.res <- pathDescribeRes{err: errPathNoOnePublishing{pathName: pa.name}}
+ req.Res <- defs.PathDescribeRes{Err: defs.PathNoOnePublishingError{PathName: pa.name}}
}
-func (pa *path) doRemovePublisher(req pathRemovePublisherReq) {
- if pa.source == req.author {
+func (pa *path) doRemovePublisher(req defs.PathRemovePublisherReq) {
+ if pa.source == req.Author {
pa.executeRemovePublisher()
}
- close(req.res)
+ close(req.Res)
}
-func (pa *path) doAddPublisher(req pathAddPublisherReq) {
+func (pa *path) doAddPublisher(req defs.PathAddPublisherReq) {
if pa.conf.Source != "publisher" {
- req.res <- pathAddPublisherRes{
- err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name),
+ req.Res <- defs.PathAddPublisherRes{
+ Err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name),
}
return
}
if pa.source != nil {
if !pa.conf.OverridePublisher {
- req.res <- pathAddPublisherRes{err: fmt.Errorf("someone is already publishing to path '%s'", pa.name)}
+ req.Res <- defs.PathAddPublisherRes{Err: fmt.Errorf("someone is already publishing to path '%s'", pa.name)}
return
}
pa.Log(logger.Info, "closing existing publisher")
- pa.source.(publisher).close()
+ pa.source.(defs.Publisher).Close()
pa.executeRemovePublisher()
}
- pa.source = req.author
+ pa.source = req.Author
+ pa.publisherQuery = req.AccessRequest.Query
- req.res <- pathAddPublisherRes{path: pa}
+ req.Res <- defs.PathAddPublisherRes{Path: pa}
}
-func (pa *path) doStartPublisher(req pathStartPublisherReq) {
- if pa.source != req.author {
- req.res <- pathStartPublisherRes{err: fmt.Errorf("publisher is not assigned to this path anymore")}
+func (pa *path) doStartPublisher(req defs.PathStartPublisherReq) {
+ if pa.source != req.Author {
+ req.Res <- defs.PathStartPublisherRes{Err: fmt.Errorf("publisher is not assigned to this path anymore")}
return
}
- err := pa.setReady(req.desc, req.generateRTPPackets)
+ err := pa.setReady(req.Desc, req.GenerateRTPPackets)
if err != nil {
- req.res <- pathStartPublisherRes{err: err}
+ req.Res <- defs.PathStartPublisherRes{Err: err}
return
}
- req.author.Log(logger.Info, "is publishing to path '%s', %s",
+ req.Author.Log(logger.Info, "is publishing to path '%s', %s",
pa.name,
- sourceMediaInfo(req.desc.Medias))
+ defs.MediasInfo(req.Desc.Medias))
- if pa.conf.HasOnDemandPublisher() {
+ if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial {
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherReadyTimer = newEmptyTimer()
-
pa.onDemandPublisherScheduleClose()
-
- for _, req := range pa.describeRequestsOnHold {
- req.res <- pathDescribeRes{
- stream: pa.stream,
- }
- }
- pa.describeRequestsOnHold = nil
-
- for _, req := range pa.readerAddRequestsOnHold {
- pa.addReaderPost(req)
- }
- pa.readerAddRequestsOnHold = nil
}
- req.res <- pathStartPublisherRes{stream: pa.stream}
+ pa.consumeOnHoldRequests()
+
+ req.Res <- defs.PathStartPublisherRes{Stream: pa.stream}
}
-func (pa *path) doStopPublisher(req pathStopPublisherReq) {
- if req.author == pa.source && pa.stream != nil {
+func (pa *path) doStopPublisher(req defs.PathStopPublisherReq) {
+ if req.Author == pa.source && pa.stream != nil {
pa.setNotReady()
}
- close(req.res)
+ close(req.Res)
}
-func (pa *path) doAddReader(req pathAddReaderReq) {
+func (pa *path) doAddReader(req defs.PathAddReaderReq) {
if pa.stream != nil {
pa.addReaderPost(req)
return
@@ -708,20 +547,20 @@ func (pa *path) doAddReader(req pathAddReaderReq) {
if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateInitial {
- pa.onDemandPublisherStart()
+ pa.onDemandPublisherStart(req.AccessRequest.Query)
}
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)
return
}
- req.res <- pathAddReaderRes{err: errPathNoOnePublishing{pathName: pa.name}}
+ req.Res <- defs.PathAddReaderRes{Err: defs.PathNoOnePublishingError{PathName: pa.name}}
}
-func (pa *path) doRemoveReader(req pathRemoveReaderReq) {
- if _, ok := pa.readers[req.author]; ok {
- pa.executeRemoveReader(req.author)
+func (pa *path) doRemoveReader(req defs.PathRemoveReaderReq) {
+ if _, ok := pa.readers[req.Author]; ok {
+ pa.executeRemoveReader(req.Author)
}
- close(req.res)
+ close(req.Res)
if len(pa.readers) == 0 {
if pa.conf.HasOnDemandStaticSource() {
@@ -738,19 +577,17 @@ func (pa *path) doRemoveReader(req pathRemoveReaderReq) {
func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
req.res <- pathAPIPathsGetRes{
- data: &apiPath{
+ data: &defs.APIPath{
Name: pa.name,
ConfName: pa.confName,
- Conf: pa.conf,
- Source: func() *apiPathSourceOrReader {
+ Source: func() *defs.APIPathSourceOrReader {
if pa.source == nil {
return nil
}
- v := pa.source.apiSourceDescribe()
+ v := pa.source.APISourceDescribe()
return &v
}(),
- SourceReady: pa.stream != nil,
- Ready: pa.stream != nil,
+ Ready: pa.stream != nil,
ReadyTime: func() *time.Time {
if pa.stream == nil {
return nil
@@ -762,7 +599,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
if pa.stream == nil {
return []string{}
}
- return mediasDescription(pa.stream.Desc().Medias)
+ return defs.MediasToCodecs(pa.stream.Desc().Medias)
}(),
BytesReceived: func() uint64 {
if pa.stream == nil {
@@ -770,10 +607,16 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
}
return pa.stream.BytesReceived()
}(),
- Readers: func() []apiPathSourceOrReader {
- ret := []apiPathSourceOrReader{}
+ BytesSent: func() uint64 {
+ if pa.stream == nil {
+ return 0
+ }
+ return pa.stream.BytesSent()
+ }(),
+ Readers: func() []defs.APIPathSourceOrReader {
+ ret := []defs.APIPathSourceOrReader{}
for r := range pa.readers {
- ret = append(ret, r.apiReaderDescribe())
+ ret = append(ret, r.APIReaderDescribe())
}
return ret
}(),
@@ -781,25 +624,13 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
}
}
-func (pa *path) safeConf() *conf.PathConf {
+func (pa *path) SafeConf() *conf.Path {
pa.confMutex.RLock()
defer pa.confMutex.RUnlock()
return pa.conf
}
-func (pa *path) shouldClose() bool {
- return pa.conf.Regexp != nil &&
- pa.source == nil &&
- len(pa.readers) == 0 &&
- len(pa.describeRequestsOnHold) == 0 &&
- len(pa.readerAddRequestsOnHold) == 0
-}
-
-func (pa *path) recordingEnabled() bool {
- return pa.record && pa.conf.Record
-}
-
-func (pa *path) externalCmdEnv() externalcmd.Environment {
+func (pa *path) ExternalCmdEnv() externalcmd.Environment {
_, port, _ := net.SplitHostPort(pa.rtspAddress)
env := externalcmd.Environment{
"MTX_PATH": pa.name,
@@ -816,8 +647,16 @@ func (pa *path) externalCmdEnv() externalcmd.Environment {
return env
}
+func (pa *path) shouldClose() bool {
+ return pa.conf.Regexp != nil &&
+ pa.source == nil &&
+ len(pa.readers) == 0 &&
+ len(pa.describeRequestsOnHold) == 0 &&
+ len(pa.readerAddRequestsOnHold) == 0
+}
+
func (pa *path) onDemandStaticSourceStart() {
- pa.source.(*sourceStatic).start(true)
+ pa.source.(*staticSourceHandler).start(true)
pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))
@@ -840,19 +679,17 @@ func (pa *path) onDemandStaticSourceStop(reason string) {
pa.onDemandStaticSourceState = pathOnDemandStateInitial
- pa.source.(*sourceStatic).stop(reason)
+ pa.source.(*staticSourceHandler).stop(reason)
}
-func (pa *path) onDemandPublisherStart() {
- pa.Log(logger.Info, "runOnDemand command started")
- pa.onDemandCmd = externalcmd.NewCmd(
- pa.externalCmdPool,
- pa.conf.RunOnDemand,
- pa.conf.RunOnDemandRestart,
- pa.externalCmdEnv(),
- func(err error) {
- pa.Log(logger.Info, "runOnDemand command exited: %v", err)
- })
+func (pa *path) onDemandPublisherStart(query string) {
+ pa.onUnDemandHook = hooks.OnDemand(hooks.OnDemandParams{
+ Logger: pa,
+ ExternalCmdPool: pa.externalCmdPool,
+ Conf: pa.conf,
+ ExternalCmdEnv: pa.ExternalCmdEnv(),
+ Query: query,
+ })
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherReadyTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandStartTimeout))
@@ -868,23 +705,15 @@ func (pa *path) onDemandPublisherScheduleClose() {
}
func (pa *path) onDemandPublisherStop(reason string) {
- if pa.source != nil {
- pa.source.(publisher).close()
- pa.executeRemovePublisher()
- }
-
if pa.onDemandPublisherState == pathOnDemandStateClosing {
pa.onDemandPublisherCloseTimer.Stop()
pa.onDemandPublisherCloseTimer = newEmptyTimer()
}
- pa.onDemandPublisherState = pathOnDemandStateInitial
+ pa.onUnDemandHook(reason)
+ pa.onUnDemandHook = nil
- if pa.onDemandCmd != nil {
- pa.onDemandCmd.Close()
- pa.onDemandCmd = nil
- pa.Log(logger.Info, "runOnDemand command stopped: %s", reason)
- }
+ pa.onDemandPublisherState = pathOnDemandStateInitial
}
func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error {
@@ -899,62 +728,49 @@ func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error
return err
}
- if pa.recordingEnabled() {
+ if pa.conf.Record {
pa.startRecording()
}
pa.readyTime = time.Now()
- if pa.conf.RunOnReady != "" {
- env := pa.externalCmdEnv()
- desc := pa.source.apiSourceDescribe()
- env["MTX_SOURCE_TYPE"] = desc.Type
- env["MTX_SOURCE_ID"] = desc.ID
-
- pa.Log(logger.Info, "runOnReady command started")
- pa.onReadyCmd = externalcmd.NewCmd(
- pa.externalCmdPool,
- pa.conf.RunOnReady,
- pa.conf.RunOnReadyRestart,
- env,
- func(err error) {
- pa.Log(logger.Info, "runOnReady command exited: %v", err)
- })
- }
+ pa.onNotReadyHook = hooks.OnReady(hooks.OnReadyParams{
+ Logger: pa,
+ ExternalCmdPool: pa.externalCmdPool,
+ Conf: pa.conf,
+ ExternalCmdEnv: pa.ExternalCmdEnv(),
+ Desc: pa.source.APISourceDescribe(),
+ Query: pa.publisherQuery,
+ })
pa.parent.pathReady(pa)
return nil
}
+func (pa *path) consumeOnHoldRequests() {
+ for _, req := range pa.describeRequestsOnHold {
+ req.Res <- defs.PathDescribeRes{
+ Stream: pa.stream,
+ }
+ }
+ pa.describeRequestsOnHold = nil
+
+ for _, req := range pa.readerAddRequestsOnHold {
+ pa.addReaderPost(req)
+ }
+ pa.readerAddRequestsOnHold = nil
+}
+
func (pa *path) setNotReady() {
pa.parent.pathNotReady(pa)
for r := range pa.readers {
pa.executeRemoveReader(r)
- r.close()
- }
-
- if pa.onReadyCmd != nil {
- pa.onReadyCmd.Close()
- pa.onReadyCmd = nil
- pa.Log(logger.Info, "runOnReady command stopped")
+ r.Close()
}
- if pa.conf.RunOnNotReady != "" {
- env := pa.externalCmdEnv()
- desc := pa.source.apiSourceDescribe()
- env["MTX_SOURCE_TYPE"] = desc.Type
- env["MTX_SOURCE_ID"] = desc.ID
-
- pa.Log(logger.Info, "runOnNotReady command launched")
- externalcmd.NewCmd(
- pa.externalCmdPool,
- pa.conf.RunOnNotReady,
- false,
- env,
- nil)
- }
+ pa.onNotReadyHook()
if pa.recordAgent != nil {
pa.recordAgent.Close()
@@ -968,16 +784,31 @@ func (pa *path) setNotReady() {
}
func (pa *path) startRecording() {
- pa.recordAgent = record.NewAgent(
- pa.writeQueueSize,
- pa.recordPath,
- time.Duration(pa.recordPartDuration),
- time.Duration(pa.recordSegmentDuration),
- pa.name,
- pa.stream,
- func(segmentPath string) {
+ pa.recordAgent = &record.Agent{
+ WriteQueueSize: pa.writeQueueSize,
+ PathFormat: pa.conf.RecordPath,
+ Format: pa.conf.RecordFormat,
+ PartDuration: time.Duration(pa.conf.RecordPartDuration),
+ SegmentDuration: time.Duration(pa.conf.RecordSegmentDuration),
+ PathName: pa.name,
+ Stream: pa.stream,
+ OnSegmentCreate: func(segmentPath string) {
+ if pa.conf.RunOnRecordSegmentCreate != "" {
+ env := pa.ExternalCmdEnv()
+ env["MTX_SEGMENT_PATH"] = segmentPath
+
+ pa.Log(logger.Info, "runOnRecordSegmentCreate command launched")
+ externalcmd.NewCmd(
+ pa.externalCmdPool,
+ pa.conf.RunOnRecordSegmentCreate,
+ false,
+ env,
+ nil)
+ }
+ },
+ OnSegmentComplete: func(segmentPath string) {
if pa.conf.RunOnRecordSegmentComplete != "" {
- env := pa.externalCmdEnv()
+ env := pa.ExternalCmdEnv()
env["MTX_SEGMENT_PATH"] = segmentPath
pa.Log(logger.Info, "runOnRecordSegmentComplete command launched")
@@ -989,11 +820,12 @@ func (pa *path) startRecording() {
nil)
}
},
- pa,
- )
+ Parent: pa,
+ }
+ pa.recordAgent.Initialize()
}
-func (pa *path) executeRemoveReader(r reader) {
+func (pa *path) executeRemoveReader(r defs.Reader) {
delete(pa.readers, r)
}
@@ -1005,23 +837,21 @@ func (pa *path) executeRemovePublisher() {
pa.source = nil
}
-func (pa *path) addReaderPost(req pathAddReaderReq) {
- if _, ok := pa.readers[req.author]; ok {
- req.res <- pathAddReaderRes{
- path: pa,
- stream: pa.stream,
+func (pa *path) addReaderPost(req defs.PathAddReaderReq) {
+ if _, ok := pa.readers[req.Author]; ok {
+ req.Res <- defs.PathAddReaderRes{
+ Path: pa,
+ Stream: pa.stream,
}
return
}
if pa.conf.MaxReaders != 0 && len(pa.readers) >= pa.conf.MaxReaders {
- req.res <- pathAddReaderRes{
- err: fmt.Errorf("maximum reader count reached"),
- }
+ req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("maximum reader count reached")}
return
}
- pa.readers[req.author] = struct{}{}
+ pa.readers[req.Author] = struct{}{}
if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateClosing {
@@ -1037,125 +867,129 @@ func (pa *path) addReaderPost(req pathAddReaderReq) {
}
}
- req.res <- pathAddReaderRes{
- path: pa,
- stream: pa.stream,
+ req.Res <- defs.PathAddReaderRes{
+ Path: pa,
+ Stream: pa.stream,
}
}
// reloadConf is called by pathManager.
-func (pa *path) reloadConf(newConf *conf.PathConf) {
+func (pa *path) reloadConf(newConf *conf.Path) {
select {
case pa.chReloadConf <- newConf:
case <-pa.ctx.Done():
}
}
-// sourceStaticSetReady is called by sourceStatic.
-func (pa *path) sourceStaticSetReady(sourceStaticCtx context.Context, req pathSourceStaticSetReadyReq) {
+// staticSourceHandlerSetReady is called by staticSourceHandler.
+func (pa *path) staticSourceHandlerSetReady(
+ staticSourceHandlerCtx context.Context, req defs.PathSourceStaticSetReadyReq,
+) {
select {
- case pa.chSourceStaticSetReady <- req:
+ case pa.chStaticSourceSetReady <- req:
case <-pa.ctx.Done():
- req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")}
+ req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
// this avoids:
// - invalid requests sent after the source has been terminated
// - deadlocks caused by <-done inside stop()
- case <-sourceStaticCtx.Done():
- req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")}
+ case <-staticSourceHandlerCtx.Done():
+ req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
}
}
-// sourceStaticSetNotReady is called by sourceStatic.
-func (pa *path) sourceStaticSetNotReady(sourceStaticCtx context.Context, req pathSourceStaticSetNotReadyReq) {
+// staticSourceHandlerSetNotReady is called by staticSourceHandler.
+func (pa *path) staticSourceHandlerSetNotReady(
+ staticSourceHandlerCtx context.Context, req defs.PathSourceStaticSetNotReadyReq,
+) {
select {
- case pa.chSourceStaticSetNotReady <- req:
+ case pa.chStaticSourceSetNotReady <- req:
case <-pa.ctx.Done():
- close(req.res)
+ close(req.Res)
// this avoids:
// - invalid requests sent after the source has been terminated
// - deadlocks caused by <-done inside stop()
- case <-sourceStaticCtx.Done():
- close(req.res)
+ case <-staticSourceHandlerCtx.Done():
+ close(req.Res)
}
}
// describe is called by a reader or publisher through pathManager.
-func (pa *path) describe(req pathDescribeReq) pathDescribeRes {
+func (pa *path) describe(req defs.PathDescribeReq) defs.PathDescribeRes {
select {
case pa.chDescribe <- req:
- return <-req.res
+ return <-req.Res
case <-pa.ctx.Done():
- return pathDescribeRes{err: fmt.Errorf("terminated")}
+ return defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
}
}
-// removePublisher is called by a publisher.
-func (pa *path) removePublisher(req pathRemovePublisherReq) {
- req.res = make(chan struct{})
+// addPublisher is called by a publisher through pathManager.
+func (pa *path) addPublisher(req defs.PathAddPublisherReq) defs.PathAddPublisherRes {
select {
- case pa.chRemovePublisher <- req:
- <-req.res
+ case pa.chAddPublisher <- req:
+ return <-req.Res
case <-pa.ctx.Done():
+ return defs.PathAddPublisherRes{Err: fmt.Errorf("terminated")}
}
}
-// addPublisher is called by a publisher through pathManager.
-func (pa *path) addPublisher(req pathAddPublisherReq) pathAddPublisherRes {
+// RemovePublisher is called by a publisher.
+func (pa *path) RemovePublisher(req defs.PathRemovePublisherReq) {
+ req.Res = make(chan struct{})
select {
- case pa.chAddPublisher <- req:
- return <-req.res
+ case pa.chRemovePublisher <- req:
+ <-req.Res
case <-pa.ctx.Done():
- return pathAddPublisherRes{err: fmt.Errorf("terminated")}
}
}
-// startPublisher is called by a publisher.
-func (pa *path) startPublisher(req pathStartPublisherReq) pathStartPublisherRes {
- req.res = make(chan pathStartPublisherRes)
+// StartPublisher is called by a publisher.
+func (pa *path) StartPublisher(req defs.PathStartPublisherReq) defs.PathStartPublisherRes {
+ req.Res = make(chan defs.PathStartPublisherRes)
select {
case pa.chStartPublisher <- req:
- return <-req.res
+ return <-req.Res
case <-pa.ctx.Done():
- return pathStartPublisherRes{err: fmt.Errorf("terminated")}
+ return defs.PathStartPublisherRes{Err: fmt.Errorf("terminated")}
}
}
-// stopPublisher is called by a publisher.
-func (pa *path) stopPublisher(req pathStopPublisherReq) {
- req.res = make(chan struct{})
+// StopPublisher is called by a publisher.
+func (pa *path) StopPublisher(req defs.PathStopPublisherReq) {
+ req.Res = make(chan struct{})
select {
case pa.chStopPublisher <- req:
- <-req.res
+ <-req.Res
case <-pa.ctx.Done():
}
}
// addReader is called by a reader through pathManager.
-func (pa *path) addReader(req pathAddReaderReq) pathAddReaderRes {
+func (pa *path) addReader(req defs.PathAddReaderReq) defs.PathAddReaderRes {
select {
case pa.chAddReader <- req:
- return <-req.res
+ return <-req.Res
case <-pa.ctx.Done():
- return pathAddReaderRes{err: fmt.Errorf("terminated")}
+ return defs.PathAddReaderRes{Err: fmt.Errorf("terminated")}
}
}
-// removeReader is called by a reader.
-func (pa *path) removeReader(req pathRemoveReaderReq) {
- req.res = make(chan struct{})
+// RemoveReader is called by a reader.
+func (pa *path) RemoveReader(req defs.PathRemoveReaderReq) {
+ req.Res = make(chan struct{})
select {
case pa.chRemoveReader <- req:
- <-req.res
+ <-req.Res
case <-pa.ctx.Done():
}
}
-// apiPathsGet is called by api.
-func (pa *path) apiPathsGet(req pathAPIPathsGetReq) (*apiPath, error) {
+// APIPathsGet is called by api.
+func (pa *path) APIPathsGet(req pathAPIPathsGetReq) (*defs.APIPath, error) {
req.res = make(chan pathAPIPathsGetRes)
select {
case pa.chAPIPathsGet <- req:
diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go
index 5ae81adb0ef..c4a0ff48a1f 100644
--- a/internal/core/path_manager.go
+++ b/internal/core/path_manager.go
@@ -7,11 +7,12 @@ import (
"sync"
"github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
)
-func pathConfCanBeUpdated(oldPathConf *conf.PathConf, newPathConf *conf.PathConf) bool {
+func pathConfCanBeUpdated(oldPathConf *conf.Path, newPathConf *conf.Path) bool {
clone := oldPathConf.Clone()
clone.Record = newPathConf.Record
@@ -28,37 +29,15 @@ func pathConfCanBeUpdated(oldPathConf *conf.PathConf, newPathConf *conf.PathConf
clone.RPICameraGain = newPathConf.RPICameraGain
clone.RPICameraEV = newPathConf.RPICameraEV
clone.RPICameraFPS = newPathConf.RPICameraFPS
+ clone.RPICameraIDRPeriod = newPathConf.RPICameraIDRPeriod
+ clone.RPICameraBitrate = newPathConf.RPICameraBitrate
return newPathConf.Equal(clone)
}
-func getConfForPath(pathConfs map[string]*conf.PathConf, name string) (string, *conf.PathConf, []string, error) {
- err := conf.IsValidPathName(name)
- if err != nil {
- return "", nil, nil, fmt.Errorf("invalid path name: %s (%s)", err, name)
- }
-
- // normal path
- if pathConf, ok := pathConfs[name]; ok {
- return name, pathConf, nil, nil
- }
-
- // regular expression-based path
- for pathConfName, pathConf := range pathConfs {
- if pathConf.Regexp != nil {
- m := pathConf.Regexp.FindStringSubmatch(name)
- if m != nil {
- return pathConfName, pathConf, m, nil
- }
- }
- }
-
- return "", nil, nil, fmt.Errorf("path '%s' is not configured", name)
-}
-
-type pathManagerHLSManager interface {
- pathReady(*path)
- pathNotReady(*path)
+type pathManagerHLSServer interface {
+ PathReady(defs.Path)
+ PathNotReady(defs.Path)
}
type pathManagerParent interface {
@@ -66,6 +45,7 @@ type pathManagerParent interface {
}
type pathManager struct {
+ logLevel conf.LogLevel
externalAuthenticationURL string
rtspAddress string
authMethods conf.AuthMethods
@@ -73,87 +53,49 @@ type pathManager struct {
writeTimeout conf.StringDuration
writeQueueSize int
udpMaxPayloadSize int
- record bool
- recordPath string
- recordPartDuration conf.StringDuration
- recordSegmentDuration conf.StringDuration
- pathConfs map[string]*conf.PathConf
+ pathConfs map[string]*conf.Path
externalCmdPool *externalcmd.Pool
- metrics *metrics
parent pathManagerParent
ctx context.Context
ctxCancel func()
wg sync.WaitGroup
- hlsManager pathManagerHLSManager
+ hlsManager pathManagerHLSServer
paths map[string]*path
pathsByConf map[string]map[*path]struct{}
// in
- chReloadConf chan map[string]*conf.PathConf
- chSetHLSManager chan pathManagerHLSManager
- chClosePath chan *path
- chPathReady chan *path
- chPathNotReady chan *path
- chGetConfForPath chan pathGetConfForPathReq
- chDescribe chan pathDescribeReq
- chAddReader chan pathAddReaderReq
- chAddPublisher chan pathAddPublisherReq
- chAPIPathsList chan pathAPIPathsListReq
- chAPIPathsGet chan pathAPIPathsGetReq
-}
-
-func newPathManager(
- externalAuthenticationURL string,
- rtspAddress string,
- authMethods conf.AuthMethods,
- readTimeout conf.StringDuration,
- writeTimeout conf.StringDuration,
- writeQueueSize int,
- udpMaxPayloadSize int,
- record bool,
- recordPath string,
- recordPartDuration conf.StringDuration,
- recordSegmentDuration conf.StringDuration,
- pathConfs map[string]*conf.PathConf,
- externalCmdPool *externalcmd.Pool,
- metrics *metrics,
- parent pathManagerParent,
-) *pathManager {
+ chReloadConf chan map[string]*conf.Path
+ chSetHLSServer chan pathManagerHLSServer
+ chClosePath chan *path
+ chPathReady chan *path
+ chPathNotReady chan *path
+ chFindPathConf chan defs.PathFindPathConfReq
+ chDescribe chan defs.PathDescribeReq
+ chAddReader chan defs.PathAddReaderReq
+ chAddPublisher chan defs.PathAddPublisherReq
+ chAPIPathsList chan pathAPIPathsListReq
+ chAPIPathsGet chan pathAPIPathsGetReq
+}
+
+func (pm *pathManager) initialize() {
ctx, ctxCancel := context.WithCancel(context.Background())
- pm := &pathManager{
- externalAuthenticationURL: externalAuthenticationURL,
- rtspAddress: rtspAddress,
- authMethods: authMethods,
- readTimeout: readTimeout,
- writeTimeout: writeTimeout,
- writeQueueSize: writeQueueSize,
- udpMaxPayloadSize: udpMaxPayloadSize,
- record: record,
- recordPath: recordPath,
- recordPartDuration: recordPartDuration,
- recordSegmentDuration: recordSegmentDuration,
- pathConfs: pathConfs,
- externalCmdPool: externalCmdPool,
- metrics: metrics,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- paths: make(map[string]*path),
- pathsByConf: make(map[string]map[*path]struct{}),
- chReloadConf: make(chan map[string]*conf.PathConf),
- chSetHLSManager: make(chan pathManagerHLSManager),
- chClosePath: make(chan *path),
- chPathReady: make(chan *path),
- chPathNotReady: make(chan *path),
- chGetConfForPath: make(chan pathGetConfForPathReq),
- chDescribe: make(chan pathDescribeReq),
- chAddReader: make(chan pathAddReaderReq),
- chAddPublisher: make(chan pathAddPublisherReq),
- chAPIPathsList: make(chan pathAPIPathsListReq),
- chAPIPathsGet: make(chan pathAPIPathsGetReq),
- }
+ pm.ctx = ctx
+ pm.ctxCancel = ctxCancel
+ pm.paths = make(map[string]*path)
+ pm.pathsByConf = make(map[string]map[*path]struct{})
+ pm.chReloadConf = make(chan map[string]*conf.Path)
+ pm.chSetHLSServer = make(chan pathManagerHLSServer)
+ pm.chClosePath = make(chan *path)
+ pm.chPathReady = make(chan *path)
+ pm.chPathNotReady = make(chan *path)
+ pm.chFindPathConf = make(chan defs.PathFindPathConfReq)
+ pm.chDescribe = make(chan defs.PathDescribeReq)
+ pm.chAddReader = make(chan defs.PathAddReaderReq)
+ pm.chAddPublisher = make(chan defs.PathAddPublisherReq)
+ pm.chAPIPathsList = make(chan pathAPIPathsListReq)
+ pm.chAPIPathsGet = make(chan pathAPIPathsGetReq)
for pathConfName, pathConf := range pm.pathConfs {
if pathConf.Regexp == nil {
@@ -161,16 +103,10 @@ func newPathManager(
}
}
- if pm.metrics != nil {
- pm.metrics.pathManagerSet(pm)
- }
-
pm.Log(logger.Debug, "path manager created")
pm.wg.Add(1)
go pm.run()
-
- return pm
}
func (pm *pathManager) close() {
@@ -179,7 +115,7 @@ func (pm *pathManager) close() {
pm.wg.Wait()
}
-// Log is the main logging function.
+// Log implements logger.Writer.
func (pm *pathManager) Log(level logger.Level, format string, args ...interface{}) {
pm.parent.Log(level, format, args...)
}
@@ -190,11 +126,11 @@ func (pm *pathManager) run() {
outer:
for {
select {
- case newPathConfs := <-pm.chReloadConf:
- pm.doReloadConf(newPathConfs)
+ case newPaths := <-pm.chReloadConf:
+ pm.doReloadConf(newPaths)
- case m := <-pm.chSetHLSManager:
- pm.doSetHLSManager(m)
+ case m := <-pm.chSetHLSServer:
+ pm.doSetHLSServer(m)
case pa := <-pm.chClosePath:
pm.doClosePath(pa)
@@ -205,8 +141,8 @@ outer:
case pa := <-pm.chPathNotReady:
pm.doPathNotReady(pa)
- case req := <-pm.chGetConfForPath:
- pm.doGetConfForPath(req)
+ case req := <-pm.chFindPathConf:
+ pm.doFindPathConf(req)
case req := <-pm.chDescribe:
pm.doDescribe(req)
@@ -229,20 +165,16 @@ outer:
}
pm.ctxCancel()
-
- if pm.metrics != nil {
- pm.metrics.pathManagerSet(nil)
- }
}
-func (pm *pathManager) doReloadConf(newPathConfs map[string]*conf.PathConf) {
+func (pm *pathManager) doReloadConf(newPaths map[string]*conf.Path) {
for confName, pathConf := range pm.pathConfs {
- if newPathConf, ok := newPathConfs[confName]; ok {
+ if newPath, ok := newPaths[confName]; ok {
// configuration has changed
- if !newPathConf.Equal(pathConf) {
- if pathConfCanBeUpdated(pathConf, newPathConf) { // paths associated with the configuration can be updated
+ if !newPath.Equal(pathConf) {
+ if pathConfCanBeUpdated(pathConf, newPath) { // paths associated with the configuration can be updated
for pa := range pm.pathsByConf[confName] {
- go pa.reloadConf(newPathConf)
+ go pa.reloadConf(newPath)
}
} else { // paths associated with the configuration must be recreated
for pa := range pm.pathsByConf[confName] {
@@ -262,7 +194,7 @@ func (pm *pathManager) doReloadConf(newPathConfs map[string]*conf.PathConf) {
}
}
- pm.pathConfs = newPathConfs
+ pm.pathConfs = newPaths
// add new paths
for pathConfName, pathConf := range pm.pathConfs {
@@ -272,7 +204,7 @@ func (pm *pathManager) doReloadConf(newPathConfs map[string]*conf.PathConf) {
}
}
-func (pm *pathManager) doSetHLSManager(m pathManagerHLSManager) {
+func (pm *pathManager) doSetHLSServer(m pathManagerHLSServer) {
pm.hlsManager = m
}
@@ -285,98 +217,101 @@ func (pm *pathManager) doClosePath(pa *path) {
func (pm *pathManager) doPathReady(pa *path) {
if pm.hlsManager != nil {
- pm.hlsManager.pathReady(pa)
+ pm.hlsManager.PathReady(pa)
}
}
func (pm *pathManager) doPathNotReady(pa *path) {
if pm.hlsManager != nil {
- pm.hlsManager.pathNotReady(pa)
+ pm.hlsManager.PathNotReady(pa)
}
}
-func (pm *pathManager) doGetConfForPath(req pathGetConfForPathReq) {
- _, pathConf, _, err := getConfForPath(pm.pathConfs, req.name)
+func (pm *pathManager) doFindPathConf(req defs.PathFindPathConfReq) {
+ _, pathConf, _, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
if err != nil {
- req.res <- pathGetConfForPathRes{err: err}
+ req.Res <- defs.PathFindPathConfRes{Err: err}
return
}
err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
- req.name, pathConf, req.publish, req.credentials)
+ pathConf, req.AccessRequest)
if err != nil {
- req.res <- pathGetConfForPathRes{err: err}
+ req.Res <- defs.PathFindPathConfRes{Err: err}
return
}
- req.res <- pathGetConfForPathRes{conf: pathConf}
+ req.Res <- defs.PathFindPathConfRes{Conf: pathConf}
}
-func (pm *pathManager) doDescribe(req pathDescribeReq) {
- pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.pathName)
+func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
+ pathConfName, pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
if err != nil {
- req.res <- pathDescribeRes{err: err}
+ req.Res <- defs.PathDescribeRes{Err: err}
return
}
- err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials)
+ err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
+ pathConf, req.AccessRequest)
if err != nil {
- req.res <- pathDescribeRes{err: err}
+ req.Res <- defs.PathDescribeRes{Err: err}
return
}
// create path if it doesn't exist
- if _, ok := pm.paths[req.pathName]; !ok {
- pm.createPath(pathConfName, pathConf, req.pathName, pathMatches)
+ if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
+ pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches)
}
- req.res <- pathDescribeRes{path: pm.paths[req.pathName]}
+ req.Res <- defs.PathDescribeRes{Path: pm.paths[req.AccessRequest.Name]}
}
-func (pm *pathManager) doAddReader(req pathAddReaderReq) {
- pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.pathName)
+func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) {
+ pathConfName, pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
if err != nil {
- req.res <- pathAddReaderRes{err: err}
+ req.Res <- defs.PathAddReaderRes{Err: err}
return
}
- if !req.skipAuth {
- err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials)
+ if !req.AccessRequest.SkipAuth {
+ err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
+ pathConf, req.AccessRequest)
if err != nil {
- req.res <- pathAddReaderRes{err: err}
+ req.Res <- defs.PathAddReaderRes{Err: err}
return
}
}
// create path if it doesn't exist
- if _, ok := pm.paths[req.pathName]; !ok {
- pm.createPath(pathConfName, pathConf, req.pathName, pathMatches)
+ if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
+ pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches)
}
- req.res <- pathAddReaderRes{path: pm.paths[req.pathName]}
+ req.Res <- defs.PathAddReaderRes{Path: pm.paths[req.AccessRequest.Name]}
}
-func (pm *pathManager) doAddPublisher(req pathAddPublisherReq) {
- pathConfName, pathConf, pathMatches, err := getConfForPath(pm.pathConfs, req.pathName)
+func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) {
+ pathConfName, pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
if err != nil {
- req.res <- pathAddPublisherRes{err: err}
+ req.Res <- defs.PathAddPublisherRes{Err: err}
return
}
- if !req.skipAuth {
- err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, true, req.credentials)
+ if !req.AccessRequest.SkipAuth {
+ err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods,
+ pathConf, req.AccessRequest)
if err != nil {
- req.res <- pathAddPublisherRes{err: err}
+ req.Res <- defs.PathAddPublisherRes{Err: err}
return
}
}
// create path if it doesn't exist
- if _, ok := pm.paths[req.pathName]; !ok {
- pm.createPath(pathConfName, pathConf, req.pathName, pathMatches)
+ if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
+ pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches)
}
- req.res <- pathAddPublisherRes{path: pm.paths[req.pathName]}
+ req.Res <- defs.PathAddPublisherRes{Path: pm.paths[req.AccessRequest.Name]}
}
func (pm *pathManager) doAPIPathsList(req pathAPIPathsListReq) {
@@ -392,7 +327,7 @@ func (pm *pathManager) doAPIPathsList(req pathAPIPathsListReq) {
func (pm *pathManager) doAPIPathsGet(req pathAPIPathsGetReq) {
path, ok := pm.paths[req.name]
if !ok {
- req.res <- pathAPIPathsGetRes{err: errAPINotFound}
+ req.res <- pathAPIPathsGetRes{err: conf.ErrPathNotFound}
return
}
@@ -401,28 +336,27 @@ func (pm *pathManager) doAPIPathsGet(req pathAPIPathsGetReq) {
func (pm *pathManager) createPath(
pathConfName string,
- pathConf *conf.PathConf,
+ pathConf *conf.Path,
name string,
matches []string,
) {
- pa := newPath(
- pm.ctx,
- pm.rtspAddress,
- pm.readTimeout,
- pm.writeTimeout,
- pm.writeQueueSize,
- pm.udpMaxPayloadSize,
- pm.record,
- pm.recordPath,
- pm.recordPartDuration,
- pm.recordSegmentDuration,
- pathConfName,
- pathConf,
- name,
- matches,
- &pm.wg,
- pm.externalCmdPool,
- pm)
+ pa := &path{
+ parentCtx: pm.ctx,
+ logLevel: pm.logLevel,
+ rtspAddress: pm.rtspAddress,
+ readTimeout: pm.readTimeout,
+ writeTimeout: pm.writeTimeout,
+ writeQueueSize: pm.writeQueueSize,
+ udpMaxPayloadSize: pm.udpMaxPayloadSize,
+ confName: pathConfName,
+ conf: pathConf,
+ name: name,
+ matches: matches,
+ wg: &pm.wg,
+ externalCmdPool: pm.externalCmdPool,
+ parent: pm,
+ }
+ pa.initialize()
pm.paths[name] = pa
@@ -440,8 +374,8 @@ func (pm *pathManager) removePath(pa *path) {
delete(pm.paths, pa.name)
}
-// confReload is called by core.
-func (pm *pathManager) confReload(pathConfs map[string]*conf.PathConf) {
+// ReloadPathConfs is called by core.
+func (pm *pathManager) ReloadPathConfs(pathConfs map[string]*conf.Path) {
select {
case pm.chReloadConf <- pathConfs:
case <-pm.ctx.Done():
@@ -475,85 +409,85 @@ func (pm *pathManager) closePath(pa *path) {
}
}
-// getConfForPath is called by a reader or publisher.
-func (pm *pathManager) getConfForPath(req pathGetConfForPathReq) pathGetConfForPathRes {
- req.res = make(chan pathGetConfForPathRes)
+// GetConfForPath is called by a reader or publisher.
+func (pm *pathManager) FindPathConf(req defs.PathFindPathConfReq) defs.PathFindPathConfRes {
+ req.Res = make(chan defs.PathFindPathConfRes)
select {
- case pm.chGetConfForPath <- req:
- return <-req.res
+ case pm.chFindPathConf <- req:
+ return <-req.Res
case <-pm.ctx.Done():
- return pathGetConfForPathRes{err: fmt.Errorf("terminated")}
+ return defs.PathFindPathConfRes{Err: fmt.Errorf("terminated")}
}
}
-// describe is called by a reader or publisher.
-func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes {
- req.res = make(chan pathDescribeRes)
+// Describe is called by a reader or publisher.
+func (pm *pathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {
+ req.Res = make(chan defs.PathDescribeRes)
select {
case pm.chDescribe <- req:
- res1 := <-req.res
- if res1.err != nil {
+ res1 := <-req.Res
+ if res1.Err != nil {
return res1
}
- res2 := res1.path.describe(req)
- if res2.err != nil {
+ res2 := res1.Path.(*path).describe(req)
+ if res2.Err != nil {
return res2
}
- res2.path = res1.path
+ res2.Path = res1.Path
return res2
case <-pm.ctx.Done():
- return pathDescribeRes{err: fmt.Errorf("terminated")}
+ return defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
}
}
-// addPublisher is called by a publisher.
-func (pm *pathManager) addPublisher(req pathAddPublisherReq) pathAddPublisherRes {
- req.res = make(chan pathAddPublisherRes)
+// AddPublisher is called by a publisher.
+func (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) defs.PathAddPublisherRes {
+ req.Res = make(chan defs.PathAddPublisherRes)
select {
case pm.chAddPublisher <- req:
- res := <-req.res
- if res.err != nil {
+ res := <-req.Res
+ if res.Err != nil {
return res
}
- return res.path.addPublisher(req)
+ return res.Path.(*path).addPublisher(req)
case <-pm.ctx.Done():
- return pathAddPublisherRes{err: fmt.Errorf("terminated")}
+ return defs.PathAddPublisherRes{Err: fmt.Errorf("terminated")}
}
}
-// addReader is called by a reader.
-func (pm *pathManager) addReader(req pathAddReaderReq) pathAddReaderRes {
- req.res = make(chan pathAddReaderRes)
+// AddReader is called by a reader.
+func (pm *pathManager) AddReader(req defs.PathAddReaderReq) defs.PathAddReaderRes {
+ req.Res = make(chan defs.PathAddReaderRes)
select {
case pm.chAddReader <- req:
- res := <-req.res
- if res.err != nil {
+ res := <-req.Res
+ if res.Err != nil {
return res
}
- return res.path.addReader(req)
+ return res.Path.(*path).addReader(req)
case <-pm.ctx.Done():
- return pathAddReaderRes{err: fmt.Errorf("terminated")}
+ return defs.PathAddReaderRes{Err: fmt.Errorf("terminated")}
}
}
-// setHLSManager is called by hlsManager.
-func (pm *pathManager) setHLSManager(s pathManagerHLSManager) {
+// setHLSServer is called by hlsManager.
+func (pm *pathManager) setHLSServer(s pathManagerHLSServer) {
select {
- case pm.chSetHLSManager <- s:
+ case pm.chSetHLSServer <- s:
case <-pm.ctx.Done():
}
}
-// apiPathsList is called by api.
-func (pm *pathManager) apiPathsList() (*apiPathsList, error) {
+// APIPathsList is called by api.
+func (pm *pathManager) APIPathsList() (*defs.APIPathList, error) {
req := pathAPIPathsListReq{
res: make(chan pathAPIPathsListRes),
}
@@ -562,12 +496,12 @@ func (pm *pathManager) apiPathsList() (*apiPathsList, error) {
case pm.chAPIPathsList <- req:
res := <-req.res
- res.data = &apiPathsList{
- Items: []*apiPath{},
+ res.data = &defs.APIPathList{
+ Items: []*defs.APIPath{},
}
for _, pa := range res.paths {
- item, err := pa.apiPathsGet(pathAPIPathsGetReq{})
+ item, err := pa.APIPathsGet(pathAPIPathsGetReq{})
if err == nil {
res.data.Items = append(res.data.Items, item)
}
@@ -584,8 +518,8 @@ func (pm *pathManager) apiPathsList() (*apiPathsList, error) {
}
}
-// apiPathsGet is called by api.
-func (pm *pathManager) apiPathsGet(name string) (*apiPath, error) {
+// APIPathsGet is called by api.
+func (pm *pathManager) APIPathsGet(name string) (*defs.APIPath, error) {
req := pathAPIPathsGetReq{
name: name,
res: make(chan pathAPIPathsGetRes),
@@ -598,7 +532,7 @@ func (pm *pathManager) apiPathsGet(name string) (*apiPath, error) {
return nil, res.err
}
- data, err := res.path.apiPathsGet(req)
+ data, err := res.path.APIPathsGet(req)
return data, err
case <-pm.ctx.Done():
diff --git a/internal/core/path_manager_test.go b/internal/core/path_manager_test.go
index 7c567e8d90f..85d14c8db8b 100644
--- a/internal/core/path_manager_test.go
+++ b/internal/core/path_manager_test.go
@@ -7,15 +7,17 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
+ "github.com/bluenviron/mediamtx/internal/defs"
"github.com/stretchr/testify/require"
)
+var _ defs.PathManager = &pathManager{}
+
func TestPathAutoDeletion(t *testing.T) {
for _, ca := range []string{"describe", "setup"} {
t.Run(ca, func(t *testing.T) {
p, ok := newInstance("paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -26,7 +28,7 @@ func TestPathAutoDeletion(t *testing.T) {
br := bufio.NewReader(conn)
if ca == "describe" {
- u, err := url.Parse("rtsp://localhost:8554/mypath")
+ u, err := base.ParseURL("rtsp://localhost:8554/mypath")
require.NoError(t, err)
byts, _ := base.Request{
@@ -44,7 +46,7 @@ func TestPathAutoDeletion(t *testing.T) {
require.NoError(t, err)
require.Equal(t, base.StatusNotFound, res.StatusCode)
} else {
- u, err := url.Parse("rtsp://localhost:8554/mypath/trackID=0")
+ u, err := base.ParseURL("rtsp://localhost:8554/mypath/trackID=0")
require.NoError(t, err)
byts, _ := base.Request{
@@ -76,7 +78,7 @@ func TestPathAutoDeletion(t *testing.T) {
}
}()
- data, err := p.pathManager.apiPathsList()
+ data, err := p.pathManager.APIPathsList()
require.NoError(t, err)
require.Equal(t, 0, len(data.Items))
diff --git a/internal/core/path_test.go b/internal/core/path_test.go
index 60e5fb537eb..f33b3e9ece5 100644
--- a/internal/core/path_test.go
+++ b/internal/core/path_test.go
@@ -2,6 +2,7 @@ package core
import (
"bufio"
+ "context"
"fmt"
"net"
"net/http"
@@ -9,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strings"
"testing"
"time"
@@ -17,19 +19,16 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
"github.com/bluenviron/gortsplib/v4/pkg/sdp"
- rtspurl "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/datarhei/gosrt"
+ srt "github.com/datarhei/gosrt"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp"
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp"
+ "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
)
-func TestPathRunOnDemand(t *testing.T) {
- onDemandFile := filepath.Join(os.TempDir(), "ondemand")
-
- srcFile := filepath.Join(os.TempDir(), "ondemand.go")
- err := os.WriteFile(srcFile, []byte(`
+var runOnDemandSampleScript = `
package main
import (
@@ -42,7 +41,9 @@ import (
)
func main() {
- if os.Getenv("G1") != "on" {
+ if os.Getenv("MTX_PATH") != "ondemand" ||
+ os.Getenv("MTX_QUERY") != "param=value" ||
+ os.Getenv("G1") != "on" {
panic("environment not set")
}
@@ -74,12 +75,41 @@ func main() {
signal.Notify(c, syscall.SIGINT)
<-c
- err = os.WriteFile("`+onDemandFile+`", []byte(""), 0644)
+ err = os.WriteFile("ON_DEMAND_FILE", []byte(""), 0644)
if err != nil {
panic(err)
}
}
-`), 0o644)
+`
+
+type testServer struct {
+ onDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error)
+ onSetup func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error)
+ onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error)
+}
+
+func (sh *testServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
+) (*base.Response, *gortsplib.ServerStream, error) {
+ return sh.onDescribe(ctx)
+}
+
+func (sh *testServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
+ return sh.onSetup(ctx)
+}
+
+func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
+ return sh.onPlay(ctx)
+}
+
+var _ defs.Path = &path{}
+
+func TestPathRunOnDemand(t *testing.T) {
+ onDemandFile := filepath.Join(os.TempDir(), "ondemand")
+ onUnDemandFile := filepath.Join(os.TempDir(), "onundemand")
+
+ srcFile := filepath.Join(os.TempDir(), "ondemand.go")
+ err := os.WriteFile(srcFile,
+ []byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemandFile)), 0o644)
require.NoError(t, err)
execFile := filepath.Join(os.TempDir(), "ondemand_cmd")
@@ -95,6 +125,7 @@ func main() {
for _, ca := range []string{"describe", "setup", "describe and setup"} {
t.Run(ca, func(t *testing.T) {
defer os.Remove(onDemandFile)
+ defer os.Remove(onUnDemandFile)
p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
"hls: no\n"+
@@ -102,7 +133,8 @@ func main() {
"paths:\n"+
" '~^(on)demand$':\n"+
" runOnDemand: %s\n"+
- " runOnDemandCloseAfter: 1s\n", execFile))
+ " runOnDemandCloseAfter: 1s\n"+
+ " runOnUnDemand: touch %s\n", execFile, onUnDemandFile))
require.Equal(t, true, ok)
defer p1.Close()
@@ -115,7 +147,7 @@ func main() {
br := bufio.NewReader(conn)
if ca == "describe" || ca == "describe and setup" {
- u, err := rtspurl.Parse("rtsp://localhost:8554/ondemand")
+ u, err := base.ParseURL("rtsp://localhost:8554/ondemand?param=value")
require.NoError(t, err)
byts, _ := base.Request{
@@ -138,11 +170,11 @@ func main() {
require.NoError(t, err)
control, _ = desc.MediaDescriptions[0].Attribute("control")
} else {
- control = "rtsp://localhost:8554/ondemand/"
+ control = "rtsp://localhost:8554/ondemand?param=value/"
}
if ca == "setup" || ca == "describe and setup" {
- u, err := rtspurl.Parse(control)
+ u, err := base.ParseURL(control)
require.NoError(t, err)
byts, _ := base.Request{
@@ -171,12 +203,15 @@ func main() {
}()
for {
- _, err := os.Stat(onDemandFile)
+ _, err := os.Stat(onUnDemandFile)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
+
+ _, err := os.Stat(onDemandFile)
+ require.NoError(t, err)
})
}
}
@@ -259,15 +294,15 @@ func TestPathRunOnReady(t *testing.T) {
"webrtc: no\n"+
"paths:\n"+
" test:\n"+
- " runOnReady: touch %s\n"+
- " runOnNotReady: touch %s\n",
+ " runOnReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
+ " runOnNotReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
onReadyFile, onNotReadyFile))
require.Equal(t, true, ok)
defer p.Close()
c := gortsplib.Client{}
err := c.StartRecording(
- "rtsp://localhost:8554/test",
+ "rtsp://localhost:8554/test?query=value",
&description.Session{Medias: []*description.Media{testMediaH264}})
require.NoError(t, err)
defer c.Close()
@@ -275,11 +310,13 @@ func TestPathRunOnReady(t *testing.T) {
time.Sleep(500 * time.Millisecond)
}()
- _, err := os.Stat(onReadyFile)
+ byts, err := os.ReadFile(onReadyFile)
require.NoError(t, err)
+ require.Equal(t, "test query=value\n", string(byts))
- _, err = os.Stat(onNotReadyFile)
+ byts, err = os.ReadFile(onNotReadyFile)
require.NoError(t, err)
+ require.Equal(t, "test query=value\n", string(byts))
}
func TestPathRunOnRead(t *testing.T) {
@@ -295,8 +332,8 @@ func TestPathRunOnRead(t *testing.T) {
p, ok := newInstance(fmt.Sprintf(
"paths:\n"+
" test:\n"+
- " runOnRead: touch %s\n"+
- " runOnUnread: touch %s\n",
+ " runOnRead: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+
+ " runOnUnread: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n",
onReadFile, onUnreadFile))
require.Equal(t, true, ok)
defer p.Close()
@@ -312,7 +349,7 @@ func TestPathRunOnRead(t *testing.T) {
case "rtsp":
reader := gortsplib.Client{}
- u, err := rtspurl.Parse("rtsp://127.0.0.1:8554/test")
+ u, err := base.ParseURL("rtsp://127.0.0.1:8554/test?query=value")
require.NoError(t, err)
err = reader.Start(u.Scheme, u.Host)
@@ -329,7 +366,7 @@ func TestPathRunOnRead(t *testing.T) {
require.NoError(t, err)
case "rtmp":
- u, err := url.Parse("rtmp://127.0.0.1:1935/test")
+ u, err := url.Parse("rtmp://127.0.0.1:1935/test?query=value")
require.NoError(t, err)
nconn, err := net.Dial("tcp", u.Host)
@@ -344,7 +381,7 @@ func TestPathRunOnRead(t *testing.T) {
case "srt":
conf := srt.DefaultConfig()
- address, err := conf.UnmarshalURL("srt://localhost:8890?streamid=read:test")
+ address, err := conf.UnmarshalURL("srt://localhost:8890?streamid=read:test:query=value")
require.NoError(t, err)
err = conf.Validate()
@@ -356,25 +393,37 @@ func TestPathRunOnRead(t *testing.T) {
case "webrtc":
hc := &http.Client{Transport: &http.Transport{}}
- c := newWebRTCTestClient(t, hc, "http://localhost:8889/test/whep", false)
- defer c.close()
+
+ u, err := url.Parse("http://localhost:8889/test/whep?query=value")
+ require.NoError(t, err)
+
+ c := &webrtc.WHIPClient{
+ HTTPClient: hc,
+ URL: u,
+ }
+
+ _, err = c.Read(context.Background())
+ require.NoError(t, err)
+ defer checkClose(t, c.Close)
}
time.Sleep(500 * time.Millisecond)
}()
- _, err := os.Stat(onReadFile)
+ byts, err := os.ReadFile(onReadFile)
require.NoError(t, err)
+ require.Equal(t, "test query=value\n", string(byts))
- _, err = os.Stat(onUnreadFile)
+ byts, err = os.ReadFile(onUnreadFile)
require.NoError(t, err)
+ require.Equal(t, "test query=value\n", string(byts))
})
}
}
func TestPathMaxReaders(t *testing.T) {
p, ok := newInstance("paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" maxReaders: 1\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -392,7 +441,7 @@ func TestPathMaxReaders(t *testing.T) {
for i := 0; i < 2; i++ {
reader := gortsplib.Client{}
- u, err := rtspurl.Parse("rtsp://127.0.0.1:8554/mystream")
+ u, err := base.ParseURL("rtsp://127.0.0.1:8554/mystream")
require.NoError(t, err)
err = reader.Start(u.Scheme, u.Host)
@@ -420,7 +469,7 @@ func TestPathRecord(t *testing.T) {
"record: yes\n" +
"recordPath: " + filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f") + "\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" record: yes\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -455,13 +504,13 @@ func TestPathRecord(t *testing.T) {
hc := &http.Client{Transport: &http.Transport{}}
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/edit/all", map[string]interface{}{
+ httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/all_others", map[string]interface{}{
"record": false,
}, nil)
time.Sleep(500 * time.Millisecond)
- httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/edit/all", map[string]interface{}{
+ httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/all_others", map[string]interface{}{
"record": true,
}, nil)
@@ -488,3 +537,113 @@ func TestPathRecord(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2, len(files))
}
+
+func TestPathFallback(t *testing.T) {
+ for _, ca := range []string{
+ "absolute",
+ "relative",
+ "source",
+ } {
+ t.Run(ca, func(t *testing.T) {
+ var conf string
+
+ switch ca {
+ case "absolute":
+ conf = "paths:\n" +
+ " path1:\n" +
+ " fallback: rtsp://localhost:8554/path2\n" +
+ " path2:\n"
+
+ case "relative":
+ conf = "paths:\n" +
+ " path1:\n" +
+ " fallback: /path2\n" +
+ " path2:\n"
+
+ case "source":
+ conf = "paths:\n" +
+ " path1:\n" +
+ " fallback: /path2\n" +
+ " source: rtsp://localhost:3333/nonexistent\n" +
+ " path2:\n"
+ }
+
+ p1, ok := newInstance(conf)
+ require.Equal(t, true, ok)
+ defer p1.Close()
+
+ source := gortsplib.Client{}
+ err := source.StartRecording("rtsp://localhost:8554/path2",
+ &description.Session{Medias: []*description.Media{testMediaH264}})
+ require.NoError(t, err)
+ defer source.Close()
+
+ u, err := base.ParseURL("rtsp://localhost:8554/path1")
+ require.NoError(t, err)
+
+ dest := gortsplib.Client{}
+ err = dest.Start(u.Scheme, u.Host)
+ require.NoError(t, err)
+ defer dest.Close()
+
+ desc, _, err := dest.Describe(u)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(desc.Medias))
+ })
+ }
+}
+
+func TestPathSourceRegexp(t *testing.T) {
+ var stream *gortsplib.ServerStream
+
+ s := gortsplib.Server{
+ Handler: &testServer{
+ onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
+ ) (*base.Response, *gortsplib.ServerStream, error) {
+ require.Equal(t, "/a", ctx.Path)
+ return &base.Response{
+ StatusCode: base.StatusOK,
+ }, stream, nil
+ },
+ onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
+ return &base.Response{
+ StatusCode: base.StatusOK,
+ }, stream, nil
+ },
+ onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
+ return &base.Response{
+ StatusCode: base.StatusOK,
+ }, nil
+ },
+ },
+ RTSPAddress: "127.0.0.1:8555",
+ }
+
+ err := s.Start()
+ require.NoError(t, err)
+ defer s.Close()
+
+ stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
+ defer stream.Close()
+
+ p, ok := newInstance(
+ "paths:\n" +
+ " '~^test_(.+)$':\n" +
+ " source: rtsp://127.0.0.1:8555/$G1\n" +
+ " sourceOnDemand: yes\n" +
+ " 'all':\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
+
+ reader := gortsplib.Client{}
+
+ u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a")
+ require.NoError(t, err)
+
+ err = reader.Start(u.Scheme, u.Host)
+ require.NoError(t, err)
+ defer reader.Close()
+
+ _, _, err = reader.Describe(u)
+ require.NoError(t, err)
+}
diff --git a/internal/core/pprof.go b/internal/core/pprof.go
deleted file mode 100644
index 9eb87656f04..00000000000
--- a/internal/core/pprof.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package core
-
-import (
- "net/http"
- "time"
-
- // start pprof
- _ "net/http/pprof"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/httpserv"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-type pprofParent interface {
- logger.Writer
-}
-
-type pprof struct {
- parent pprofParent
-
- httpServer *httpserv.WrappedServer
-}
-
-func newPPROF(
- address string,
- readTimeout conf.StringDuration,
- parent pprofParent,
-) (*pprof, error) {
- pp := &pprof{
- parent: parent,
- }
-
- network, address := restrictNetwork("tcp", address)
-
- var err error
- pp.httpServer, err = httpserv.NewWrappedServer(
- network,
- address,
- time.Duration(readTimeout),
- "",
- "",
- http.DefaultServeMux,
- pp,
- )
- if err != nil {
- return nil, err
- }
-
- pp.Log(logger.Info, "listener opened on "+address)
-
- return pp, nil
-}
-
-func (pp *pprof) close() {
- pp.Log(logger.Info, "listener is closing")
- pp.httpServer.Close()
-}
-
-func (pp *pprof) Log(level logger.Level, format string, args ...interface{}) {
- pp.parent.Log(level, "[pprof] "+format, args...)
-}
diff --git a/internal/core/publisher.go b/internal/core/publisher.go
deleted file mode 100644
index c76f8d0f34c..00000000000
--- a/internal/core/publisher.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package core
-
-// publisher is an entity that can publish a stream.
-type publisher interface {
- source
- close()
-}
diff --git a/internal/core/reader.go b/internal/core/reader.go
deleted file mode 100644
index f03bcb0bfdb..00000000000
--- a/internal/core/reader.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package core
-
-// reader is an entity that can read a stream.
-type reader interface {
- close()
- apiReaderDescribe() apiPathSourceOrReader
-}
diff --git a/internal/core/restrict_network.go b/internal/core/restrict_network.go
deleted file mode 100644
index 0316eb4dd5d..00000000000
--- a/internal/core/restrict_network.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package core
-
-import (
- "net"
-)
-
-// do not listen on IPv6 when address is 0.0.0.0.
-func restrictNetwork(network string, address string) (string, string) {
- host, _, err := net.SplitHostPort(address)
- if err == nil {
- if host == "0.0.0.0" {
- return network + "4", address
- }
- }
-
- return network, address
-}
diff --git a/internal/core/rtmp_listener.go b/internal/core/rtmp_listener.go
deleted file mode 100644
index 7c058a4b756..00000000000
--- a/internal/core/rtmp_listener.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package core
-
-import (
- "net"
- "sync"
-)
-
-type rtmpListener struct {
- ln net.Listener
- wg *sync.WaitGroup
- parent *rtmpServer
-}
-
-func newRTMPListener(
- ln net.Listener,
- wg *sync.WaitGroup,
- parent *rtmpServer,
-) *rtmpListener {
- l := &rtmpListener{
- ln: ln,
- wg: wg,
- parent: parent,
- }
-
- l.wg.Add(1)
- go l.run()
-
- return l
-}
-
-func (l *rtmpListener) run() {
- defer l.wg.Done()
-
- err := l.runInner()
-
- l.parent.acceptError(err)
-}
-
-func (l *rtmpListener) runInner() error {
- for {
- conn, err := l.ln.Accept()
- if err != nil {
- return err
- }
-
- l.parent.newConn(conn)
- }
-}
diff --git a/internal/core/rtmp_server.go b/internal/core/rtmp_server.go
deleted file mode 100644
index 3553195efdf..00000000000
--- a/internal/core/rtmp_server.go
+++ /dev/null
@@ -1,336 +0,0 @@
-package core
-
-import (
- "context"
- "crypto/tls"
- "fmt"
- "net"
- "sort"
- "sync"
-
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-type rtmpServerAPIConnsListRes struct {
- data *apiRTMPConnsList
- err error
-}
-
-type rtmpServerAPIConnsListReq struct {
- res chan rtmpServerAPIConnsListRes
-}
-
-type rtmpServerAPIConnsGetRes struct {
- data *apiRTMPConn
- err error
-}
-
-type rtmpServerAPIConnsGetReq struct {
- uuid uuid.UUID
- res chan rtmpServerAPIConnsGetRes
-}
-
-type rtmpServerAPIConnsKickRes struct {
- err error
-}
-
-type rtmpServerAPIConnsKickReq struct {
- uuid uuid.UUID
- res chan rtmpServerAPIConnsKickRes
-}
-
-type rtmpServerParent interface {
- logger.Writer
-}
-
-type rtmpServer struct {
- readTimeout conf.StringDuration
- writeTimeout conf.StringDuration
- writeQueueSize int
- isTLS bool
- rtspAddress string
- runOnConnect string
- runOnConnectRestart bool
- runOnDisconnect string
- externalCmdPool *externalcmd.Pool
- metrics *metrics
- pathManager *pathManager
- parent rtmpServerParent
-
- ctx context.Context
- ctxCancel func()
- wg sync.WaitGroup
- ln net.Listener
- conns map[*rtmpConn]struct{}
-
- // in
- chNewConn chan net.Conn
- chAcceptErr chan error
- chCloseConn chan *rtmpConn
- chAPIConnsList chan rtmpServerAPIConnsListReq
- chAPIConnsGet chan rtmpServerAPIConnsGetReq
- chAPIConnsKick chan rtmpServerAPIConnsKickReq
-}
-
-func newRTMPServer(
- address string,
- readTimeout conf.StringDuration,
- writeTimeout conf.StringDuration,
- writeQueueSize int,
- isTLS bool,
- serverCert string,
- serverKey string,
- rtspAddress string,
- runOnConnect string,
- runOnConnectRestart bool,
- runOnDisconnect string,
- externalCmdPool *externalcmd.Pool,
- metrics *metrics,
- pathManager *pathManager,
- parent rtmpServerParent,
-) (*rtmpServer, error) {
- ln, err := func() (net.Listener, error) {
- if !isTLS {
- return net.Listen(restrictNetwork("tcp", address))
- }
-
- cert, err := tls.LoadX509KeyPair(serverCert, serverKey)
- if err != nil {
- return nil, err
- }
-
- network, address := restrictNetwork("tcp", address)
- return tls.Listen(network, address, &tls.Config{Certificates: []tls.Certificate{cert}})
- }()
- if err != nil {
- return nil, err
- }
-
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- s := &rtmpServer{
- readTimeout: readTimeout,
- writeTimeout: writeTimeout,
- writeQueueSize: writeQueueSize,
- rtspAddress: rtspAddress,
- runOnConnect: runOnConnect,
- runOnConnectRestart: runOnConnectRestart,
- runOnDisconnect: runOnDisconnect,
- isTLS: isTLS,
- externalCmdPool: externalCmdPool,
- metrics: metrics,
- pathManager: pathManager,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- ln: ln,
- conns: make(map[*rtmpConn]struct{}),
- chNewConn: make(chan net.Conn),
- chAcceptErr: make(chan error),
- chCloseConn: make(chan *rtmpConn),
- chAPIConnsList: make(chan rtmpServerAPIConnsListReq),
- chAPIConnsGet: make(chan rtmpServerAPIConnsGetReq),
- chAPIConnsKick: make(chan rtmpServerAPIConnsKickReq),
- }
-
- s.Log(logger.Info, "listener opened on %s", address)
-
- if s.metrics != nil {
- s.metrics.rtmpServerSet(s)
- }
-
- newRTMPListener(
- s.ln,
- &s.wg,
- s,
- )
-
- s.wg.Add(1)
- go s.run()
-
- return s, nil
-}
-
-func (s *rtmpServer) Log(level logger.Level, format string, args ...interface{}) {
- label := func() string {
- if s.isTLS {
- return "RTMPS"
- }
- return "RTMP"
- }()
- s.parent.Log(level, "[%s] "+format, append([]interface{}{label}, args...)...)
-}
-
-func (s *rtmpServer) close() {
- s.Log(logger.Info, "listener is closing")
- s.ctxCancel()
- s.wg.Wait()
-}
-
-func (s *rtmpServer) run() {
- defer s.wg.Done()
-
-outer:
- for {
- select {
- case err := <-s.chAcceptErr:
- s.Log(logger.Error, "%s", err)
- break outer
-
- case nconn := <-s.chNewConn:
- c := newRTMPConn(
- s.ctx,
- s.isTLS,
- s.rtspAddress,
- s.readTimeout,
- s.writeTimeout,
- s.writeQueueSize,
- s.runOnConnect,
- s.runOnConnectRestart,
- s.runOnDisconnect,
- &s.wg,
- nconn,
- s.externalCmdPool,
- s.pathManager,
- s)
- s.conns[c] = struct{}{}
-
- case c := <-s.chCloseConn:
- delete(s.conns, c)
-
- case req := <-s.chAPIConnsList:
- data := &apiRTMPConnsList{
- Items: []*apiRTMPConn{},
- }
-
- for c := range s.conns {
- data.Items = append(data.Items, c.apiItem())
- }
-
- sort.Slice(data.Items, func(i, j int) bool {
- return data.Items[i].Created.Before(data.Items[j].Created)
- })
-
- req.res <- rtmpServerAPIConnsListRes{data: data}
-
- case req := <-s.chAPIConnsGet:
- c := s.findConnByUUID(req.uuid)
- if c == nil {
- req.res <- rtmpServerAPIConnsGetRes{err: errAPINotFound}
- continue
- }
-
- req.res <- rtmpServerAPIConnsGetRes{data: c.apiItem()}
-
- case req := <-s.chAPIConnsKick:
- c := s.findConnByUUID(req.uuid)
- if c == nil {
- req.res <- rtmpServerAPIConnsKickRes{err: errAPINotFound}
- continue
- }
-
- delete(s.conns, c)
- c.close()
- req.res <- rtmpServerAPIConnsKickRes{}
-
- case <-s.ctx.Done():
- break outer
- }
- }
-
- s.ctxCancel()
-
- s.ln.Close()
-
- if s.metrics != nil {
- s.metrics.rtmpServerSet(s)
- }
-}
-
-func (s *rtmpServer) findConnByUUID(uuid uuid.UUID) *rtmpConn {
- for c := range s.conns {
- if c.uuid == uuid {
- return c
- }
- }
- return nil
-}
-
-// newConn is called by rtmpListener.
-func (s *rtmpServer) newConn(conn net.Conn) {
- select {
- case s.chNewConn <- conn:
- case <-s.ctx.Done():
- conn.Close()
- }
-}
-
-// acceptError is called by rtmpListener.
-func (s *rtmpServer) acceptError(err error) {
- select {
- case s.chAcceptErr <- err:
- case <-s.ctx.Done():
- }
-}
-
-// closeConn is called by rtmpConn.
-func (s *rtmpServer) closeConn(c *rtmpConn) {
- select {
- case s.chCloseConn <- c:
- case <-s.ctx.Done():
- }
-}
-
-// apiConnsList is called by api.
-func (s *rtmpServer) apiConnsList() (*apiRTMPConnsList, error) {
- req := rtmpServerAPIConnsListReq{
- res: make(chan rtmpServerAPIConnsListRes),
- }
-
- select {
- case s.chAPIConnsList <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-// apiConnsGet is called by api.
-func (s *rtmpServer) apiConnsGet(uuid uuid.UUID) (*apiRTMPConn, error) {
- req := rtmpServerAPIConnsGetReq{
- uuid: uuid,
- res: make(chan rtmpServerAPIConnsGetRes),
- }
-
- select {
- case s.chAPIConnsGet <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-// apiConnsKick is called by api.
-func (s *rtmpServer) apiConnsKick(uuid uuid.UUID) error {
- req := rtmpServerAPIConnsKickReq{
- uuid: uuid,
- res: make(chan rtmpServerAPIConnsKickRes),
- }
-
- select {
- case s.chAPIConnsKick <- req:
- res := <-req.res
- return res.err
-
- case <-s.ctx.Done():
- return fmt.Errorf("terminated")
- }
-}
diff --git a/internal/core/rtmp_server_test.go b/internal/core/rtmp_server_test.go
index 8f9966052a6..f7c81a7bed8 100644
--- a/internal/core/rtmp_server_test.go
+++ b/internal/core/rtmp_server_test.go
@@ -12,7 +12,7 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp"
)
func TestRTMPServer(t *testing.T) {
@@ -56,11 +56,11 @@ func TestRTMPServer(t *testing.T) {
switch auth {
case "none":
conf += "paths:\n" +
- " all:\n"
+ " all_others:\n"
case "internal":
conf += "paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" publishUser: testpublisher\n" +
" publishPass: testpass\n" +
" publishIPs: [127.0.0.0/16]\n" +
@@ -71,7 +71,7 @@ func TestRTMPServer(t *testing.T) {
case "external":
conf += "externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
- " all:\n"
+ " all_others:\n"
}
p, ok := newInstance(conf)
@@ -193,7 +193,7 @@ func TestRTMPServerAuthFail(t *testing.T) {
"hls: no\n" +
"webrtc: no\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" publishUser: testuser2\n" +
" publishPass: testpass\n")
require.Equal(t, true, ok)
@@ -244,7 +244,7 @@ func TestRTMPServerAuthFail(t *testing.T) {
t.Run("publish_external", func(t *testing.T) {
p, ok := newInstance("externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -298,7 +298,7 @@ func TestRTMPServerAuthFail(t *testing.T) {
"hls: no\n" +
"webrtc: no\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" readUser: testuser2\n" +
" readPass: testpass\n")
require.Equal(t, true, ok)
diff --git a/internal/core/rtmp_source_test.go b/internal/core/rtmp_source_test.go
deleted file mode 100644
index 480eaf8e50d..00000000000
--- a/internal/core/rtmp_source_test.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package core
-
-import (
- "crypto/tls"
- "net"
- "os"
- "testing"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
- "github.com/pion/rtp"
- "github.com/stretchr/testify/require"
-
- "github.com/bluenviron/mediamtx/internal/rtmp"
-)
-
-func TestRTMPSource(t *testing.T) {
- for _, ca := range []string{
- "plain",
- "tls",
- } {
- t.Run(ca, func(t *testing.T) {
- ln, err := func() (net.Listener, error) {
- if ca == "plain" {
- return net.Listen("tcp", "127.0.0.1:1937")
- }
-
- serverCertFpath, err := writeTempFile(serverCert)
- require.NoError(t, err)
- defer os.Remove(serverCertFpath)
-
- serverKeyFpath, err := writeTempFile(serverKey)
- require.NoError(t, err)
- defer os.Remove(serverKeyFpath)
-
- var cert tls.Certificate
- cert, err = tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath)
- require.NoError(t, err)
-
- return tls.Listen("tcp", "127.0.0.1:1937", &tls.Config{Certificates: []tls.Certificate{cert}})
- }()
- require.NoError(t, err)
- defer ln.Close()
-
- connected := make(chan struct{})
- received := make(chan struct{})
- done := make(chan struct{})
-
- go func() {
- nconn, err := ln.Accept()
- require.NoError(t, err)
- defer nconn.Close()
-
- conn, _, _, err := rtmp.NewServerConn(nconn)
- require.NoError(t, err)
-
- videoTrack := &format.H264{
- PayloadTyp: 96,
- SPS: []byte{ // 1920x1080 baseline
- 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,
- },
- PPS: []byte{0x08, 0x06, 0x07, 0x08},
- PacketizationMode: 1,
- }
-
- audioTrack := &format.MPEG4Audio{
- PayloadTyp: 96,
- Config: &mpeg4audio.Config{
- Type: 2,
- SampleRate: 44100,
- ChannelCount: 2,
- },
- SizeLength: 13,
- IndexLength: 3,
- IndexDeltaLength: 3,
- }
-
- w, err := rtmp.NewWriter(conn, videoTrack, audioTrack)
- require.NoError(t, err)
-
- <-connected
-
- err = w.WriteH264(0, 0, true, [][]byte{{0x05, 0x02, 0x03, 0x04}})
- require.NoError(t, err)
-
- <-done
- }()
-
- if ca == "plain" {
- p, ok := newInstance("paths:\n" +
- " proxied:\n" +
- " source: rtmp://localhost:1937/teststream\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
- } else {
- p, ok := newInstance("paths:\n" +
- " proxied:\n" +
- " source: rtmps://localhost:1937/teststream\n" +
- " sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
- }
-
- c := gortsplib.Client{}
-
- u, err := url.Parse("rtsp://127.0.0.1:8554/proxied")
- require.NoError(t, err)
-
- err = c.Start(u.Scheme, u.Host)
- require.NoError(t, err)
- defer c.Close()
-
- desc, _, err := c.Describe(u)
- require.NoError(t, err)
-
- var forma *format.H264
- medi := desc.FindFormat(&forma)
-
- _, err = c.Setup(desc.BaseURL, medi, 0, 0)
- require.NoError(t, err)
-
- c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
- require.Equal(t, []byte{
- 0x18, 0x0, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,
- 0x0, 0x78, 0x2, 0x27, 0xe5, 0x84, 0x0, 0x0,
- 0x3, 0x0, 0x4, 0x0, 0x0, 0x3, 0x0, 0xf0,
- 0x3c, 0x60, 0xc9, 0x20, 0x0, 0x4, 0x8, 0x6,
- 0x7, 0x8, 0x0, 0x4, 0x5, 0x2, 0x3, 0x4,
- }, pkt.Payload)
- close(received)
- })
-
- _, err = c.Play(nil)
- require.NoError(t, err)
-
- close(connected)
- <-received
- close(done)
- })
- }
-}
diff --git a/internal/core/rtsp_conn.go b/internal/core/rtsp_conn.go
deleted file mode 100644
index 45e0c44d032..00000000000
--- a/internal/core/rtsp_conn.go
+++ /dev/null
@@ -1,243 +0,0 @@
-package core
-
-import (
- "fmt"
- "net"
- "time"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/auth"
- "github.com/bluenviron/gortsplib/v4/pkg/base"
- "github.com/bluenviron/gortsplib/v4/pkg/headers"
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-const (
- rtspPauseAfterAuthError = 2 * time.Second
-)
-
-type rtspConnParent interface {
- logger.Writer
- getISTLS() bool
- getServer() *gortsplib.Server
-}
-
-type rtspConn struct {
- *conn
-
- isTLS bool
- rtspAddress string
- authMethods []headers.AuthMethod
- readTimeout conf.StringDuration
- pathManager *pathManager
- rconn *gortsplib.ServerConn
- parent rtspConnParent
-
- uuid uuid.UUID
- created time.Time
- authNonce string
- authFailures int
-}
-
-func newRTSPConn(
- isTLS bool,
- rtspAddress string,
- authMethods []headers.AuthMethod,
- readTimeout conf.StringDuration,
- runOnConnect string,
- runOnConnectRestart bool,
- runOnDisconnect string,
- externalCmdPool *externalcmd.Pool,
- pathManager *pathManager,
- conn *gortsplib.ServerConn,
- parent rtspConnParent,
-) *rtspConn {
- c := &rtspConn{
- isTLS: isTLS,
- rtspAddress: rtspAddress,
- authMethods: authMethods,
- readTimeout: readTimeout,
- pathManager: pathManager,
- rconn: conn,
- parent: parent,
- uuid: uuid.New(),
- created: time.Now(),
- }
-
- c.conn = newConn(
- rtspAddress,
- runOnConnect,
- runOnConnectRestart,
- runOnDisconnect,
- externalCmdPool,
- c,
- )
-
- c.Log(logger.Info, "opened")
-
- c.conn.open(apiPathSourceOrReader{
- Type: func() string {
- if isTLS {
- return "rtspsConn"
- }
- return "rtspConn"
- }(),
- ID: c.uuid.String(),
- })
-
- return c
-}
-
-func (c *rtspConn) Log(level logger.Level, format string, args ...interface{}) {
- c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.rconn.NetConn().RemoteAddr()}, args...)...)
-}
-
-// Conn returns the RTSP connection.
-func (c *rtspConn) Conn() *gortsplib.ServerConn {
- return c.rconn
-}
-
-func (c *rtspConn) remoteAddr() net.Addr {
- return c.rconn.NetConn().RemoteAddr()
-}
-
-func (c *rtspConn) ip() net.IP {
- return c.rconn.NetConn().RemoteAddr().(*net.TCPAddr).IP
-}
-
-// onClose is called by rtspServer.
-func (c *rtspConn) onClose(err error) {
- c.Log(logger.Info, "closed: %v", err)
-
- c.conn.close(apiPathSourceOrReader{
- Type: func() string {
- if c.isTLS {
- return "rtspsConn"
- }
- return "rtspConn"
- }(),
- ID: c.uuid.String(),
- })
-}
-
-// onRequest is called by rtspServer.
-func (c *rtspConn) onRequest(req *base.Request) {
- c.Log(logger.Debug, "[c->s] %v", req)
-}
-
-// OnResponse is called by rtspServer.
-func (c *rtspConn) OnResponse(res *base.Response) {
- c.Log(logger.Debug, "[s->c] %v", res)
-}
-
-// onDescribe is called by rtspServer.
-func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
-) (*base.Response, *gortsplib.ServerStream, error) {
- if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
- return &base.Response{
- StatusCode: base.StatusBadRequest,
- }, nil, fmt.Errorf("invalid path")
- }
- ctx.Path = ctx.Path[1:]
-
- if c.authNonce == "" {
- var err error
- c.authNonce, err = auth.GenerateNonce()
- if err != nil {
- return &base.Response{
- StatusCode: base.StatusInternalServerError,
- }, nil, err
- }
- }
-
- res := c.pathManager.describe(pathDescribeReq{
- pathName: ctx.Path,
- url: ctx.Request.URL,
- credentials: authCredentials{
- query: ctx.Query,
- ip: c.ip(),
- proto: authProtocolRTSP,
- id: &c.uuid,
- rtspRequest: ctx.Request,
- rtspNonce: c.authNonce,
- },
- })
-
- if res.err != nil {
- switch terr := res.err.(type) {
- case *errAuthentication:
- res, err := c.handleAuthError(terr)
- return res, nil, err
-
- case errPathNoOnePublishing:
- return &base.Response{
- StatusCode: base.StatusNotFound,
- }, nil, res.err
-
- default:
- return &base.Response{
- StatusCode: base.StatusBadRequest,
- }, nil, res.err
- }
- }
-
- if res.redirect != "" {
- return &base.Response{
- StatusCode: base.StatusMovedPermanently,
- Header: base.Header{
- "Location": base.HeaderValue{res.redirect},
- },
- }, nil, nil
- }
-
- var stream *gortsplib.ServerStream
- if !c.parent.getISTLS() {
- stream = res.stream.RTSPStream(c.parent.getServer())
- } else {
- stream = res.stream.RTSPSStream(c.parent.getServer())
- }
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
-}
-
-func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) {
- c.authFailures++
-
- // VLC with login prompt sends 4 requests:
- // 1) without credentials
- // 2) with password but without username
- // 3) without credentials
- // 4) with password and username
- // therefore we must allow up to 3 failures
- if c.authFailures <= 3 {
- return &base.Response{
- StatusCode: base.StatusUnauthorized,
- Header: base.Header{
- "WWW-Authenticate": auth.GenerateWWWAuthenticate(c.authMethods, "IPCAM", c.authNonce),
- },
- }, nil
- }
-
- // wait some seconds to stop brute force attacks
- <-time.After(rtspPauseAfterAuthError)
-
- return &base.Response{
- StatusCode: base.StatusUnauthorized,
- }, authErr
-}
-
-func (c *rtspConn) apiItem() *apiRTSPConn {
- return &apiRTSPConn{
- ID: c.uuid,
- Created: c.created,
- RemoteAddr: c.remoteAddr().String(),
- BytesReceived: c.rconn.BytesReceived(),
- BytesSent: c.rconn.BytesSent(),
- }
-}
diff --git a/internal/core/rtsp_server.go b/internal/core/rtsp_server.go
deleted file mode 100644
index bcab3d11f9d..00000000000
--- a/internal/core/rtsp_server.go
+++ /dev/null
@@ -1,472 +0,0 @@
-package core
-
-import (
- "context"
- "crypto/tls"
- "fmt"
- "sort"
- "strings"
- "sync"
- "time"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/base"
- "github.com/bluenviron/gortsplib/v4/pkg/headers"
- "github.com/bluenviron/gortsplib/v4/pkg/liberrors"
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-type rtspServerParent interface {
- logger.Writer
-}
-
-func printAddresses(srv *gortsplib.Server) string {
- var ret []string
-
- ret = append(ret, fmt.Sprintf("%s (TCP)", srv.RTSPAddress))
-
- if srv.UDPRTPAddress != "" {
- ret = append(ret, fmt.Sprintf("%s (UDP/RTP)", srv.UDPRTPAddress))
- }
-
- if srv.UDPRTCPAddress != "" {
- ret = append(ret, fmt.Sprintf("%s (UDP/RTCP)", srv.UDPRTCPAddress))
- }
-
- return strings.Join(ret, ", ")
-}
-
-type rtspServer struct {
- authMethods []headers.AuthMethod
- readTimeout conf.StringDuration
- isTLS bool
- rtspAddress string
- protocols map[conf.Protocol]struct{}
- runOnConnect string
- runOnConnectRestart bool
- runOnDisconnect string
- externalCmdPool *externalcmd.Pool
- metrics *metrics
- pathManager *pathManager
- parent rtspServerParent
-
- ctx context.Context
- ctxCancel func()
- wg sync.WaitGroup
- srv *gortsplib.Server
- mutex sync.RWMutex
- conns map[*gortsplib.ServerConn]*rtspConn
- sessions map[*gortsplib.ServerSession]*rtspSession
-}
-
-func newRTSPServer(
- address string,
- authMethods []headers.AuthMethod,
- readTimeout conf.StringDuration,
- writeTimeout conf.StringDuration,
- writeQueueSize int,
- useUDP bool,
- useMulticast bool,
- rtpAddress string,
- rtcpAddress string,
- multicastIPRange string,
- multicastRTPPort int,
- multicastRTCPPort int,
- isTLS bool,
- serverCert string,
- serverKey string,
- rtspAddress string,
- protocols map[conf.Protocol]struct{},
- runOnConnect string,
- runOnConnectRestart bool,
- runOnDisconnect string,
- externalCmdPool *externalcmd.Pool,
- metrics *metrics,
- pathManager *pathManager,
- parent rtspServerParent,
-) (*rtspServer, error) {
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- s := &rtspServer{
- authMethods: authMethods,
- readTimeout: readTimeout,
- isTLS: isTLS,
- rtspAddress: rtspAddress,
- protocols: protocols,
- runOnConnect: runOnConnect,
- runOnConnectRestart: runOnConnectRestart,
- runOnDisconnect: runOnDisconnect,
- externalCmdPool: externalCmdPool,
- metrics: metrics,
- pathManager: pathManager,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- conns: make(map[*gortsplib.ServerConn]*rtspConn),
- sessions: make(map[*gortsplib.ServerSession]*rtspSession),
- }
-
- s.srv = &gortsplib.Server{
- Handler: s,
- ReadTimeout: time.Duration(readTimeout),
- WriteTimeout: time.Duration(writeTimeout),
- WriteQueueSize: writeQueueSize,
- RTSPAddress: address,
- }
-
- if useUDP {
- s.srv.UDPRTPAddress = rtpAddress
- s.srv.UDPRTCPAddress = rtcpAddress
- }
-
- if useMulticast {
- s.srv.MulticastIPRange = multicastIPRange
- s.srv.MulticastRTPPort = multicastRTPPort
- s.srv.MulticastRTCPPort = multicastRTCPPort
- }
-
- if isTLS {
- cert, err := tls.LoadX509KeyPair(serverCert, serverKey)
- if err != nil {
- return nil, err
- }
-
- s.srv.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
- }
-
- err := s.srv.Start()
- if err != nil {
- return nil, err
- }
-
- s.Log(logger.Info, "listener opened on %s", printAddresses(s.srv))
-
- if metrics != nil {
- if !isTLS {
- metrics.setRTSPServer(s)
- } else {
- metrics.setRTSPSServer(s)
- }
- }
-
- s.wg.Add(1)
- go s.run()
-
- return s, nil
-}
-
-func (s *rtspServer) Log(level logger.Level, format string, args ...interface{}) {
- label := func() string {
- if s.isTLS {
- return "RTSPS"
- }
- return "RTSP"
- }()
- s.parent.Log(level, "[%s] "+format, append([]interface{}{label}, args...)...)
-}
-
-func (s *rtspServer) getISTLS() bool {
- return s.isTLS
-}
-
-func (s *rtspServer) getServer() *gortsplib.Server {
- return s.srv
-}
-
-func (s *rtspServer) close() {
- s.Log(logger.Info, "listener is closing")
- s.ctxCancel()
- s.wg.Wait()
-}
-
-func (s *rtspServer) run() {
- defer s.wg.Done()
-
- serverErr := make(chan error)
- go func() {
- serverErr <- s.srv.Wait()
- }()
-
-outer:
- select {
- case err := <-serverErr:
- s.Log(logger.Error, "%s", err)
- break outer
-
- case <-s.ctx.Done():
- s.srv.Close()
- <-serverErr
- break outer
- }
-
- s.ctxCancel()
-
- if s.metrics != nil {
- if !s.isTLS {
- s.metrics.setRTSPServer(nil)
- } else {
- s.metrics.setRTSPSServer(nil)
- }
- }
-}
-
-// OnConnOpen implements gortsplib.ServerHandlerOnConnOpen.
-func (s *rtspServer) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) {
- c := newRTSPConn(
- s.isTLS,
- s.rtspAddress,
- s.authMethods,
- s.readTimeout,
- s.runOnConnect,
- s.runOnConnectRestart,
- s.runOnDisconnect,
- s.externalCmdPool,
- s.pathManager,
- ctx.Conn,
- s)
- s.mutex.Lock()
- s.conns[ctx.Conn] = c
- s.mutex.Unlock()
-
- ctx.Conn.SetUserData(c)
-}
-
-// OnConnClose implements gortsplib.ServerHandlerOnConnClose.
-func (s *rtspServer) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) {
- s.mutex.Lock()
- c := s.conns[ctx.Conn]
- delete(s.conns, ctx.Conn)
- s.mutex.Unlock()
- c.onClose(ctx.Error)
-}
-
-// OnRequest implements gortsplib.ServerHandlerOnRequest.
-func (s *rtspServer) OnRequest(sc *gortsplib.ServerConn, req *base.Request) {
- c := sc.UserData().(*rtspConn)
- c.onRequest(req)
-}
-
-// OnResponse implements gortsplib.ServerHandlerOnResponse.
-func (s *rtspServer) OnResponse(sc *gortsplib.ServerConn, res *base.Response) {
- c := sc.UserData().(*rtspConn)
- c.OnResponse(res)
-}
-
-// OnSessionOpen implements gortsplib.ServerHandlerOnSessionOpen.
-func (s *rtspServer) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx) {
- se := newRTSPSession(
- s.isTLS,
- s.protocols,
- ctx.Session,
- ctx.Conn,
- s.externalCmdPool,
- s.pathManager,
- s)
- s.mutex.Lock()
- s.sessions[ctx.Session] = se
- s.mutex.Unlock()
- ctx.Session.SetUserData(se)
-}
-
-// OnSessionClose implements gortsplib.ServerHandlerOnSessionClose.
-func (s *rtspServer) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) {
- s.mutex.Lock()
- se := s.sessions[ctx.Session]
- delete(s.sessions, ctx.Session)
- s.mutex.Unlock()
-
- if se != nil {
- se.onClose(ctx.Error)
- }
-}
-
-// OnDescribe implements gortsplib.ServerHandlerOnDescribe.
-func (s *rtspServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
-) (*base.Response, *gortsplib.ServerStream, error) {
- c := ctx.Conn.UserData().(*rtspConn)
- return c.onDescribe(ctx)
-}
-
-// OnAnnounce implements gortsplib.ServerHandlerOnAnnounce.
-func (s *rtspServer) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {
- c := ctx.Conn.UserData().(*rtspConn)
- se := ctx.Session.UserData().(*rtspSession)
- return se.onAnnounce(c, ctx)
-}
-
-// OnSetup implements gortsplib.ServerHandlerOnSetup.
-func (s *rtspServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
- c := ctx.Conn.UserData().(*rtspConn)
- se := ctx.Session.UserData().(*rtspSession)
- return se.onSetup(c, ctx)
-}
-
-// OnPlay implements gortsplib.ServerHandlerOnPlay.
-func (s *rtspServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
- se := ctx.Session.UserData().(*rtspSession)
- return se.onPlay(ctx)
-}
-
-// OnRecord implements gortsplib.ServerHandlerOnRecord.
-func (s *rtspServer) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
- se := ctx.Session.UserData().(*rtspSession)
- return se.onRecord(ctx)
-}
-
-// OnPause implements gortsplib.ServerHandlerOnPause.
-func (s *rtspServer) OnPause(ctx *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) {
- se := ctx.Session.UserData().(*rtspSession)
- return se.onPause(ctx)
-}
-
-// OnPacketLost implements gortsplib.ServerHandlerOnDecodeError.
-func (s *rtspServer) OnPacketLost(ctx *gortsplib.ServerHandlerOnPacketLostCtx) {
- se := ctx.Session.UserData().(*rtspSession)
- se.onPacketLost(ctx)
-}
-
-// OnDecodeError implements gortsplib.ServerHandlerOnDecodeError.
-func (s *rtspServer) OnDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) {
- se := ctx.Session.UserData().(*rtspSession)
- se.onDecodeError(ctx)
-}
-
-// OnDecodeError implements gortsplib.ServerHandlerOnStreamWriteError.
-func (s *rtspServer) OnStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) {
- se := ctx.Session.UserData().(*rtspSession)
- se.onStreamWriteError(ctx)
-}
-
-func (s *rtspServer) findConnByUUID(uuid uuid.UUID) *rtspConn {
- for _, c := range s.conns {
- if c.uuid == uuid {
- return c
- }
- }
- return nil
-}
-
-func (s *rtspServer) findSessionByUUID(uuid uuid.UUID) (*gortsplib.ServerSession, *rtspSession) {
- for key, sx := range s.sessions {
- if sx.uuid == uuid {
- return key, sx
- }
- }
- return nil, nil
-}
-
-// apiConnsList is called by api and metrics.
-func (s *rtspServer) apiConnsList() (*apiRTSPConnsList, error) {
- select {
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- default:
- }
-
- s.mutex.RLock()
- defer s.mutex.RUnlock()
-
- data := &apiRTSPConnsList{
- Items: []*apiRTSPConn{},
- }
-
- for _, c := range s.conns {
- data.Items = append(data.Items, c.apiItem())
- }
-
- sort.Slice(data.Items, func(i, j int) bool {
- return data.Items[i].Created.Before(data.Items[j].Created)
- })
-
- return data, nil
-}
-
-// apiConnsGet is called by api.
-func (s *rtspServer) apiConnsGet(uuid uuid.UUID) (*apiRTSPConn, error) {
- select {
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- default:
- }
-
- s.mutex.RLock()
- defer s.mutex.RUnlock()
-
- conn := s.findConnByUUID(uuid)
- if conn == nil {
- return nil, errAPINotFound
- }
-
- return conn.apiItem(), nil
-}
-
-// apiSessionsList is called by api and metrics.
-func (s *rtspServer) apiSessionsList() (*apiRTSPSessionsList, error) {
- select {
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- default:
- }
-
- s.mutex.RLock()
- defer s.mutex.RUnlock()
-
- data := &apiRTSPSessionsList{
- Items: []*apiRTSPSession{},
- }
-
- for _, s := range s.sessions {
- data.Items = append(data.Items, s.apiItem())
- }
-
- sort.Slice(data.Items, func(i, j int) bool {
- return data.Items[i].Created.Before(data.Items[j].Created)
- })
-
- return data, nil
-}
-
-// apiSessionsGet is called by api.
-func (s *rtspServer) apiSessionsGet(uuid uuid.UUID) (*apiRTSPSession, error) {
- select {
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- default:
- }
-
- s.mutex.RLock()
- defer s.mutex.RUnlock()
-
- _, sx := s.findSessionByUUID(uuid)
- if sx == nil {
- return nil, errAPINotFound
- }
-
- return sx.apiItem(), nil
-}
-
-// apiSessionsKick is called by api.
-func (s *rtspServer) apiSessionsKick(uuid uuid.UUID) error {
- select {
- case <-s.ctx.Done():
- return fmt.Errorf("terminated")
- default:
- }
-
- s.mutex.RLock()
- defer s.mutex.RUnlock()
-
- key, sx := s.findSessionByUUID(uuid)
- if sx == nil {
- return errAPINotFound
- }
-
- sx.close()
- delete(s.sessions, key)
- sx.onClose(liberrors.ErrServerTerminated{})
- return nil
-}
diff --git a/internal/core/rtsp_server_test.go b/internal/core/rtsp_server_test.go
index 446c9314f99..672d5172677 100644
--- a/internal/core/rtsp_server_test.go
+++ b/internal/core/rtsp_server_test.go
@@ -4,8 +4,8 @@ import (
"testing"
"github.com/bluenviron/gortsplib/v4"
+ "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
)
@@ -22,14 +22,14 @@ func TestRTSPServer(t *testing.T) {
switch auth {
case "none":
conf = "paths:\n" +
- " all:\n"
+ " all_others:\n"
case "internal":
conf = "rtmp: no\n" +
"hls: no\n" +
"webrtc: no\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" publishUser: testpublisher\n" +
" publishPass: testpass\n" +
" publishIPs: [127.0.0.0/16]\n" +
@@ -40,7 +40,7 @@ func TestRTSPServer(t *testing.T) {
case "external":
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
- " all:\n"
+ " all_others:\n"
}
p, ok := newInstance(conf)
@@ -70,7 +70,7 @@ func TestRTSPServer(t *testing.T) {
reader := gortsplib.Client{}
- u, err := url.Parse("rtsp://testreader:testpass@127.0.0.1:8554/teststream?param=value")
+ u, err := base.ParseURL("rtsp://testreader:testpass@127.0.0.1:8554/teststream?param=value")
require.NoError(t, err)
err = reader.Start(u.Scheme, u.Host)
@@ -89,13 +89,13 @@ func TestRTSPServer(t *testing.T) {
}
}
-func TestRTSPServerAuthHashed(t *testing.T) {
+func TestRTSPServerAuthHashedSHA256(t *testing.T) {
p, ok := newInstance(
"rtmp: no\n" +
"hls: no\n" +
"webrtc: no\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" publishUser: sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\n" +
" publishPass: sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=\n")
require.Equal(t, true, ok)
@@ -112,6 +112,29 @@ func TestRTSPServerAuthHashed(t *testing.T) {
defer source.Close()
}
+func TestRTSPServerAuthHashedArgon2(t *testing.T) {
+ p, ok := newInstance(
+ "rtmp: no\n" +
+ "hls: no\n" +
+ "webrtc: no\n" +
+ "paths:\n" +
+ " all_others:\n" +
+ " publishUser: argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58\n" +
+ " publishPass: argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
+
+ medi := testMediaH264
+
+ source := gortsplib.Client{}
+
+ err := source.StartRecording(
+ "rtsp://testuser:testpass@127.0.0.1:8554/test/stream",
+ &description.Session{Medias: []*description.Media{medi}})
+ require.NoError(t, err)
+ defer source.Close()
+}
+
func TestRTSPServerAuthFail(t *testing.T) {
for _, ca := range []struct {
name string
@@ -139,7 +162,7 @@ func TestRTSPServerAuthFail(t *testing.T) {
"hls: no\n" +
"webrtc: no\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" publishUser: testuser\n" +
" publishPass: testpass\n")
require.Equal(t, true, ok)
@@ -183,7 +206,7 @@ func TestRTSPServerAuthFail(t *testing.T) {
"hls: no\n" +
"webrtc: no\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" readUser: testuser\n" +
" readPass: testpass\n")
require.Equal(t, true, ok)
@@ -191,7 +214,7 @@ func TestRTSPServerAuthFail(t *testing.T) {
c := gortsplib.Client{}
- u, err := url.Parse("rtsp://" + ca.user + ":" + ca.pass + "@localhost:8554/test/stream")
+ u, err := base.ParseURL("rtsp://" + ca.user + ":" + ca.pass + "@localhost:8554/test/stream")
require.NoError(t, err)
err = c.Start(u.Scheme, u.Host)
@@ -208,7 +231,7 @@ func TestRTSPServerAuthFail(t *testing.T) {
"hls: no\n" +
"webrtc: no\n" +
"paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" publishIPs: [128.0.0.1/32]\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -227,7 +250,7 @@ func TestRTSPServerAuthFail(t *testing.T) {
t.Run("external", func(t *testing.T) {
p, ok := newInstance("externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -254,7 +277,7 @@ func TestRTSPServerPublisherOverride(t *testing.T) {
t.Run(ca, func(t *testing.T) {
conf := "rtmp: no\n" +
"paths:\n" +
- " all:\n"
+ " all_others:\n"
if ca == "disabled" {
conf += " overridePublisher: no\n"
@@ -288,7 +311,7 @@ func TestRTSPServerPublisherOverride(t *testing.T) {
c := gortsplib.Client{}
- u, err := url.Parse("rtsp://localhost:8554/teststream")
+ u, err := base.ParseURL("rtsp://localhost:8554/teststream")
require.NoError(t, err)
err = c.Start(u.Scheme, u.Host)
@@ -348,47 +371,3 @@ func TestRTSPServerPublisherOverride(t *testing.T) {
})
}
}
-
-func TestRTSPServerFallback(t *testing.T) {
- for _, ca := range []string{
- "absolute",
- "relative",
- } {
- t.Run(ca, func(t *testing.T) {
- val := func() string {
- if ca == "absolute" {
- return "rtsp://localhost:8554/path2"
- }
- return "/path2"
- }()
-
- p1, ok := newInstance("rtmp: no\n" +
- "hls: no\n" +
- "webrtc: no\n" +
- "paths:\n" +
- " path1:\n" +
- " fallback: " + val + "\n" +
- " path2:\n")
- require.Equal(t, true, ok)
- defer p1.Close()
-
- source := gortsplib.Client{}
- err := source.StartRecording("rtsp://localhost:8554/path2",
- &description.Session{Medias: []*description.Media{testMediaH264}})
- require.NoError(t, err)
- defer source.Close()
-
- u, err := url.Parse("rtsp://localhost:8554/path1")
- require.NoError(t, err)
-
- dest := gortsplib.Client{}
- err = dest.Start(u.Scheme, u.Host)
- require.NoError(t, err)
- defer dest.Close()
-
- desc, _, err := dest.Describe(u)
- require.NoError(t, err)
- require.Equal(t, 1, len(desc.Medias))
- })
- }
-}
diff --git a/internal/core/rtsp_session.go b/internal/core/rtsp_session.go
deleted file mode 100644
index 28b0fe9b1b7..00000000000
--- a/internal/core/rtsp_session.go
+++ /dev/null
@@ -1,472 +0,0 @@
-package core
-
-import (
- "encoding/hex"
- "fmt"
- "net"
- "sync"
- "time"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/auth"
- "github.com/bluenviron/gortsplib/v4/pkg/base"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/google/uuid"
- "github.com/pion/rtp"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
- "github.com/bluenviron/mediamtx/internal/stream"
-)
-
-type rtspSessionPathManager interface {
- addPublisher(req pathAddPublisherReq) pathAddPublisherRes
- addReader(req pathAddReaderReq) pathAddReaderRes
-}
-
-type rtspSessionParent interface {
- logger.Writer
- getISTLS() bool
- getServer() *gortsplib.Server
-}
-
-type rtspSession struct {
- isTLS bool
- protocols map[conf.Protocol]struct{}
- session *gortsplib.ServerSession
- author *gortsplib.ServerConn
- externalCmdPool *externalcmd.Pool
- pathManager rtspSessionPathManager
- parent rtspSessionParent
-
- uuid uuid.UUID
- created time.Time
- path *path
- stream *stream.Stream
- onReadCmd *externalcmd.Cmd // read
- mutex sync.Mutex
- state gortsplib.ServerSessionState
- transport *gortsplib.Transport
- pathName string
- decodeErrLogger logger.Writer
- writeErrLogger logger.Writer
-}
-
-func newRTSPSession(
- isTLS bool,
- protocols map[conf.Protocol]struct{},
- session *gortsplib.ServerSession,
- sc *gortsplib.ServerConn,
- externalCmdPool *externalcmd.Pool,
- pathManager rtspSessionPathManager,
- parent rtspSessionParent,
-) *rtspSession {
- s := &rtspSession{
- isTLS: isTLS,
- protocols: protocols,
- session: session,
- author: sc,
- externalCmdPool: externalCmdPool,
- pathManager: pathManager,
- parent: parent,
- uuid: uuid.New(),
- created: time.Now(),
- }
-
- s.decodeErrLogger = logger.NewLimitedLogger(s)
- s.writeErrLogger = logger.NewLimitedLogger(s)
-
- s.Log(logger.Info, "created by %v", s.author.NetConn().RemoteAddr())
-
- return s
-}
-
-// Close closes a Session.
-func (s *rtspSession) close() {
- s.session.Close()
-}
-
-func (s *rtspSession) remoteAddr() net.Addr {
- return s.author.NetConn().RemoteAddr()
-}
-
-func (s *rtspSession) Log(level logger.Level, format string, args ...interface{}) {
- id := hex.EncodeToString(s.uuid[:4])
- s.parent.Log(level, "[session %s] "+format, append([]interface{}{id}, args...)...)
-}
-
-func (s *rtspSession) onUnread() {
- if s.onReadCmd != nil {
- s.Log(logger.Info, "runOnRead command stopped")
- s.onReadCmd.Close()
- }
-
- if s.path.conf.RunOnUnread != "" {
- env := s.path.externalCmdEnv()
- desc := s.apiReaderDescribe()
- env["MTX_READER_TYPE"] = desc.Type
- env["MTX_READER_ID"] = desc.ID
-
- s.Log(logger.Info, "runOnUnread command launched")
- externalcmd.NewCmd(
- s.externalCmdPool,
- s.path.conf.RunOnUnread,
- false,
- env,
- nil)
- }
-}
-
-// onClose is called by rtspServer.
-func (s *rtspSession) onClose(err error) {
- if s.session.State() == gortsplib.ServerSessionStatePlay {
- s.onUnread()
- }
-
- switch s.session.State() {
- case gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay:
- s.path.removeReader(pathRemoveReaderReq{author: s})
-
- case gortsplib.ServerSessionStatePreRecord, gortsplib.ServerSessionStateRecord:
- s.path.removePublisher(pathRemovePublisherReq{author: s})
- }
-
- s.path = nil
- s.stream = nil
-
- s.Log(logger.Info, "destroyed: %v", err)
-}
-
-// onAnnounce is called by rtspServer.
-func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {
- if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
- return &base.Response{
- StatusCode: base.StatusBadRequest,
- }, fmt.Errorf("invalid path")
- }
- ctx.Path = ctx.Path[1:]
-
- if c.authNonce == "" {
- var err error
- c.authNonce, err = auth.GenerateNonce()
- if err != nil {
- return &base.Response{
- StatusCode: base.StatusInternalServerError,
- }, err
- }
- }
-
- res := s.pathManager.addPublisher(pathAddPublisherReq{
- author: s,
- pathName: ctx.Path,
- credentials: authCredentials{
- query: ctx.Query,
- ip: c.ip(),
- proto: authProtocolRTSP,
- id: &c.uuid,
- rtspRequest: ctx.Request,
- rtspBaseURL: nil,
- rtspNonce: c.authNonce,
- },
- })
-
- if res.err != nil {
- switch terr := res.err.(type) {
- case *errAuthentication:
- return c.handleAuthError(terr)
-
- default:
- return &base.Response{
- StatusCode: base.StatusBadRequest,
- }, res.err
- }
- }
-
- s.path = res.path
-
- s.mutex.Lock()
- s.state = gortsplib.ServerSessionStatePreRecord
- s.pathName = ctx.Path
- s.mutex.Unlock()
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, nil
-}
-
-// onSetup is called by rtspServer.
-func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCtx,
-) (*base.Response, *gortsplib.ServerStream, error) {
- if len(ctx.Path) == 0 || ctx.Path[0] != '/' {
- return &base.Response{
- StatusCode: base.StatusBadRequest,
- }, nil, fmt.Errorf("invalid path")
- }
- ctx.Path = ctx.Path[1:]
-
- // in case the client is setupping a stream with UDP or UDP-multicast, and these
- // transport protocols are disabled, gortsplib already blocks the request.
- // we have only to handle the case in which the transport protocol is TCP
- // and it is disabled.
- if ctx.Transport == gortsplib.TransportTCP {
- if _, ok := s.protocols[conf.Protocol(gortsplib.TransportTCP)]; !ok {
- return &base.Response{
- StatusCode: base.StatusUnsupportedTransport,
- }, nil, nil
- }
- }
-
- switch s.session.State() {
- case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play
- baseURL := &url.URL{
- Scheme: ctx.Request.URL.Scheme,
- Host: ctx.Request.URL.Host,
- Path: ctx.Path,
- RawQuery: ctx.Query,
- }
-
- if ctx.Query != "" {
- baseURL.RawQuery += "/"
- } else {
- baseURL.Path += "/"
- }
-
- if c.authNonce == "" {
- var err error
- c.authNonce, err = auth.GenerateNonce()
- if err != nil {
- return &base.Response{
- StatusCode: base.StatusInternalServerError,
- }, nil, err
- }
- }
-
- res := s.pathManager.addReader(pathAddReaderReq{
- author: s,
- pathName: ctx.Path,
- credentials: authCredentials{
- query: ctx.Query,
- ip: c.ip(),
- proto: authProtocolRTSP,
- id: &c.uuid,
- rtspRequest: ctx.Request,
- rtspBaseURL: baseURL,
- rtspNonce: c.authNonce,
- },
- })
-
- if res.err != nil {
- switch terr := res.err.(type) {
- case *errAuthentication:
- res, err := c.handleAuthError(terr)
- return res, nil, err
-
- case errPathNoOnePublishing:
- return &base.Response{
- StatusCode: base.StatusNotFound,
- }, nil, res.err
-
- default:
- return &base.Response{
- StatusCode: base.StatusBadRequest,
- }, nil, res.err
- }
- }
-
- s.path = res.path
- s.stream = res.stream
-
- s.mutex.Lock()
- s.state = gortsplib.ServerSessionStatePrePlay
- s.pathName = ctx.Path
- s.mutex.Unlock()
-
- var stream *gortsplib.ServerStream
- if !s.parent.getISTLS() {
- stream = res.stream.RTSPStream(s.parent.getServer())
- } else {
- stream = res.stream.RTSPSStream(s.parent.getServer())
- }
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
-
- default: // record
- return &base.Response{
- StatusCode: base.StatusOK,
- }, nil, nil
- }
-}
-
-// onPlay is called by rtspServer.
-func (s *rtspSession) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
- h := make(base.Header)
-
- if s.session.State() == gortsplib.ServerSessionStatePrePlay {
- s.Log(logger.Info, "is reading from path '%s', with %s, %s",
- s.path.name,
- s.session.SetuppedTransport(),
- sourceMediaInfo(s.session.SetuppedMedias()))
-
- pathConf := s.path.safeConf()
-
- if pathConf.RunOnRead != "" {
- env := s.path.externalCmdEnv()
- desc := s.apiReaderDescribe()
- env["MTX_READER_TYPE"] = desc.Type
- env["MTX_READER_ID"] = desc.ID
-
- s.Log(logger.Info, "runOnRead command started")
- s.onReadCmd = externalcmd.NewCmd(
- s.externalCmdPool,
- pathConf.RunOnRead,
- pathConf.RunOnReadRestart,
- env,
- func(err error) {
- s.Log(logger.Info, "runOnRead command exited: %v", err)
- })
- }
-
- s.mutex.Lock()
- s.state = gortsplib.ServerSessionStatePlay
- s.transport = s.session.SetuppedTransport()
- s.mutex.Unlock()
- }
-
- return &base.Response{
- StatusCode: base.StatusOK,
- Header: h,
- }, nil
-}
-
-// onRecord is called by rtspServer.
-func (s *rtspSession) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
- res := s.path.startPublisher(pathStartPublisherReq{
- author: s,
- desc: s.session.AnnouncedDescription(),
- generateRTPPackets: false,
- })
- if res.err != nil {
- return &base.Response{
- StatusCode: base.StatusBadRequest,
- }, res.err
- }
-
- s.stream = res.stream
-
- for _, medi := range s.session.AnnouncedDescription().Medias {
- for _, forma := range medi.Formats {
- cmedi := medi
- cforma := forma
-
- s.session.OnPacketRTP(cmedi, cforma, func(pkt *rtp.Packet) {
- pts, ok := s.session.PacketPTS(cmedi, pkt)
- if !ok {
- return
- }
-
- res.stream.WriteRTPPacket(cmedi, cforma, pkt, time.Now(), pts)
- })
- }
- }
-
- s.mutex.Lock()
- s.state = gortsplib.ServerSessionStateRecord
- s.transport = s.session.SetuppedTransport()
- s.mutex.Unlock()
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, nil
-}
-
-// onPause is called by rtspServer.
-func (s *rtspSession) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) {
- switch s.session.State() {
- case gortsplib.ServerSessionStatePlay:
- s.onUnread()
-
- s.mutex.Lock()
- s.state = gortsplib.ServerSessionStatePrePlay
- s.mutex.Unlock()
-
- case gortsplib.ServerSessionStateRecord:
- s.path.stopPublisher(pathStopPublisherReq{author: s})
-
- s.mutex.Lock()
- s.state = gortsplib.ServerSessionStatePreRecord
- s.mutex.Unlock()
- }
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, nil
-}
-
-// apiReaderDescribe implements reader.
-func (s *rtspSession) apiReaderDescribe() apiPathSourceOrReader {
- return apiPathSourceOrReader{
- Type: func() string {
- if s.isTLS {
- return "rtspsSession"
- }
- return "rtspSession"
- }(),
- ID: s.uuid.String(),
- }
-}
-
-// apiSourceDescribe implements source.
-func (s *rtspSession) apiSourceDescribe() apiPathSourceOrReader {
- return s.apiReaderDescribe()
-}
-
-// onPacketLost is called by rtspServer.
-func (s *rtspSession) onPacketLost(ctx *gortsplib.ServerHandlerOnPacketLostCtx) {
- s.decodeErrLogger.Log(logger.Warn, ctx.Error.Error())
-}
-
-// onDecodeError is called by rtspServer.
-func (s *rtspSession) onDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) {
- s.decodeErrLogger.Log(logger.Warn, ctx.Error.Error())
-}
-
-// onStreamWriteError is called by rtspServer.
-func (s *rtspSession) onStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) {
- s.writeErrLogger.Log(logger.Warn, ctx.Error.Error())
-}
-
-func (s *rtspSession) apiItem() *apiRTSPSession {
- s.mutex.Lock()
- defer s.mutex.Unlock()
-
- return &apiRTSPSession{
- ID: s.uuid,
- Created: s.created,
- RemoteAddr: s.remoteAddr().String(),
- State: func() apiRTSPSessionState {
- switch s.state {
- case gortsplib.ServerSessionStatePrePlay,
- gortsplib.ServerSessionStatePlay:
- return apiRTSPSessionStateRead
-
- case gortsplib.ServerSessionStatePreRecord,
- gortsplib.ServerSessionStateRecord:
- return apiRTSPSessionStatePublish
- }
- return apiRTSPSessionStateIdle
- }(),
- Path: s.pathName,
- Transport: func() *string {
- if s.transport == nil {
- return nil
- }
- v := s.transport.String()
- return &v
- }(),
- BytesReceived: s.session.BytesReceived(),
- BytesSent: s.session.BytesSent(),
- }
-}
diff --git a/internal/core/rtsp_source_test.go b/internal/core/rtsp_source_test.go
deleted file mode 100644
index 6e203cfb95d..00000000000
--- a/internal/core/rtsp_source_test.go
+++ /dev/null
@@ -1,312 +0,0 @@
-package core
-
-import (
- "crypto/tls"
- "os"
- "testing"
- "time"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/auth"
- "github.com/bluenviron/gortsplib/v4/pkg/base"
- "github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/pion/rtp"
- "github.com/stretchr/testify/require"
-)
-
-type testServer struct {
- onDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error)
- onSetup func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error)
- onPlay func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error)
-}
-
-func (sh *testServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
-) (*base.Response, *gortsplib.ServerStream, error) {
- return sh.onDescribe(ctx)
-}
-
-func (sh *testServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
- return sh.onSetup(ctx)
-}
-
-func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
- return sh.onPlay(ctx)
-}
-
-func TestRTSPSource(t *testing.T) {
- for _, source := range []string{
- "udp",
- "tcp",
- "tls",
- } {
- t.Run(source, func(t *testing.T) {
- serverMedia := testMediaH264
- var stream *gortsplib.ServerStream
-
- nonce, err := auth.GenerateNonce()
- require.NoError(t, err)
-
- s := gortsplib.Server{
- Handler: &testServer{
- onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,
- ) (*base.Response, *gortsplib.ServerStream, error) {
- err := auth.Validate(ctx.Request, "testuser", "testpass", nil, nil, "IPCAM", nonce)
- if err != nil {
- return &base.Response{ //nolint:nilerr
- StatusCode: base.StatusUnauthorized,
- Header: base.Header{
- "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
- },
- }, nil, nil
- }
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
- },
- onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
- },
- onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
- go func() {
- time.Sleep(1 * time.Second)
- err := stream.WritePacketRTP(serverMedia, &rtp.Packet{
- Header: rtp.Header{
- Version: 0x02,
- PayloadType: 96,
- SequenceNumber: 57899,
- Timestamp: 345234345,
- SSRC: 978651231,
- Marker: true,
- },
- Payload: []byte{5, 1, 2, 3, 4},
- })
- require.NoError(t, err)
- }()
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, nil
- },
- },
- RTSPAddress: "127.0.0.1:8555",
- }
-
- switch source {
- case "udp":
- s.UDPRTPAddress = "127.0.0.1:8002"
- s.UDPRTCPAddress = "127.0.0.1:8003"
-
- case "tls":
- serverCertFpath, err := writeTempFile(serverCert)
- require.NoError(t, err)
- defer os.Remove(serverCertFpath)
-
- serverKeyFpath, err := writeTempFile(serverKey)
- require.NoError(t, err)
- defer os.Remove(serverKeyFpath)
-
- cert, err := tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath)
- require.NoError(t, err)
-
- s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
- }
-
- err = s.Start()
- require.NoError(t, err)
- defer s.Wait() //nolint:errcheck
- defer s.Close()
-
- stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{serverMedia}})
- defer stream.Close()
-
- if source == "udp" || source == "tcp" {
- p, ok := newInstance("paths:\n" +
- " proxied:\n" +
- " source: rtsp://testuser:testpass@localhost:8555/teststream\n" +
- " sourceProtocol: " + source + "\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
- } else {
- p, ok := newInstance("paths:\n" +
- " proxied:\n" +
- " source: rtsps://testuser:testpass@localhost:8555/teststream\n" +
- " sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
- }
-
- received := make(chan struct{})
-
- c := gortsplib.Client{}
-
- u, err := url.Parse("rtsp://127.0.0.1:8554/proxied")
- require.NoError(t, err)
-
- err = c.Start(u.Scheme, u.Host)
- require.NoError(t, err)
- defer c.Close()
-
- desc, _, err := c.Describe(u)
- require.NoError(t, err)
-
- var forma *format.H264
- medi := desc.FindFormat(&forma)
-
- _, err = c.Setup(desc.BaseURL, medi, 0, 0)
- require.NoError(t, err)
-
- c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
- require.Equal(t, []byte{5, 1, 2, 3, 4}, pkt.Payload)
- close(received)
- })
-
- _, err = c.Play(nil)
- require.NoError(t, err)
-
- <-received
- })
- }
-}
-
-func TestRTSPSourceNoPassword(t *testing.T) {
- var stream *gortsplib.ServerStream
-
- nonce, err := auth.GenerateNonce()
- require.NoError(t, err)
-
- done := make(chan struct{})
-
- s := gortsplib.Server{
- Handler: &testServer{
- onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
- err := auth.Validate(ctx.Request, "testuser", "", nil, nil, "IPCAM", nonce)
- if err != nil {
- return &base.Response{ //nolint:nilerr
- StatusCode: base.StatusUnauthorized,
- Header: base.Header{
- "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
- },
- }, nil, nil
- }
-
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
- },
- onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
- close(done)
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
- },
- onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
- return &base.Response{
- StatusCode: base.StatusOK,
- }, nil
- },
- },
- RTSPAddress: "127.0.0.1:8555",
- }
-
- err = s.Start()
- require.NoError(t, err)
- defer s.Wait() //nolint:errcheck
- defer s.Close()
-
- stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
- defer stream.Close()
-
- p, ok := newInstance("rtmp: no\n" +
- "hls: no\n" +
- "webrtc: no\n" +
- "paths:\n" +
- " proxied:\n" +
- " source: rtsp://testuser:@127.0.0.1:8555/teststream\n" +
- " sourceProtocol: tcp\n")
- require.Equal(t, true, ok)
- defer p.Close()
-
- <-done
-}
-
-func TestRTSPSourceRange(t *testing.T) {
- for _, ca := range []string{"clock", "npt", "smpte"} {
- t.Run(ca, func(t *testing.T) {
- var stream *gortsplib.ServerStream
- done := make(chan struct{})
-
- s := gortsplib.Server{
- Handler: &testServer{
- onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
- },
- onSetup: func(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {
- return &base.Response{
- StatusCode: base.StatusOK,
- }, stream, nil
- },
- onPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {
- switch ca {
- case "clock":
- require.Equal(t, base.HeaderValue{"clock=20230812T120000Z-"}, ctx.Request.Header["Range"])
-
- case "npt":
- require.Equal(t, base.HeaderValue{"npt=0.35-"}, ctx.Request.Header["Range"])
-
- case "smpte":
- require.Equal(t, base.HeaderValue{"smpte=0:02:10-"}, ctx.Request.Header["Range"])
- }
-
- close(done)
- return &base.Response{
- StatusCode: base.StatusOK,
- }, nil
- },
- },
- RTSPAddress: "127.0.0.1:8555",
- }
-
- err := s.Start()
- require.NoError(t, err)
- defer s.Wait() //nolint:errcheck
- defer s.Close()
-
- stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}})
- defer stream.Close()
-
- var addConf string
- switch ca {
- case "clock":
- addConf += " rtspRangeType: clock\n" +
- " rtspRangeStart: 20230812T120000Z\n"
-
- case "npt":
- addConf += " rtspRangeType: npt\n" +
- " rtspRangeStart: 350ms\n"
-
- case "smpte":
- addConf += " rtspRangeType: smpte\n" +
- " rtspRangeStart: 130s\n"
- }
- p, ok := newInstance("rtmp: no\n" +
- "hls: no\n" +
- "webrtc: no\n" +
- "paths:\n" +
- " proxied:\n" +
- " source: rtsp://testuser:@127.0.0.1:8555/teststream\n" + addConf)
- require.Equal(t, true, ok)
- defer p.Close()
-
- <-done
- })
- }
-}
diff --git a/internal/core/source.go b/internal/core/source.go
deleted file mode 100644
index 03137761298..00000000000
--- a/internal/core/source.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package core
-
-import (
- "fmt"
- "strings"
-
- "github.com/bluenviron/gortsplib/v4/pkg/description"
-
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-// source is an entity that can provide a stream.
-// it can be:
-// - a publisher
-// - sourceStatic
-// - sourceRedirect
-type source interface {
- logger.Writer
- apiSourceDescribe() apiPathSourceOrReader
-}
-
-func mediaDescription(media *description.Media) string {
- ret := make([]string, len(media.Formats))
- for i, forma := range media.Formats {
- ret[i] = forma.Codec()
- }
- return strings.Join(ret, "/")
-}
-
-func mediasDescription(medias []*description.Media) []string {
- ret := make([]string, len(medias))
- for i, media := range medias {
- ret[i] = mediaDescription(media)
- }
- return ret
-}
-
-func sourceMediaInfo(medias []*description.Media) string {
- return fmt.Sprintf("%d %s (%s)",
- len(medias),
- func() string {
- if len(medias) == 1 {
- return "track"
- }
- return "tracks"
- }(),
- strings.Join(mediasDescription(medias), ", "))
-}
diff --git a/internal/core/source_redirect.go b/internal/core/source_redirect.go
index 4931b20d027..9428430d870 100644
--- a/internal/core/source_redirect.go
+++ b/internal/core/source_redirect.go
@@ -1,6 +1,7 @@
package core
import (
+ "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger"
)
@@ -10,9 +11,9 @@ type sourceRedirect struct{}
func (*sourceRedirect) Log(logger.Level, string, ...interface{}) {
}
-// apiSourceDescribe implements source.
-func (*sourceRedirect) apiSourceDescribe() apiPathSourceOrReader {
- return apiPathSourceOrReader{
+// APISourceDescribe implements source.
+func (*sourceRedirect) APISourceDescribe() defs.APIPathSourceOrReader {
+ return defs.APIPathSourceOrReader{
Type: "redirect",
ID: "",
}
diff --git a/internal/core/source_static.go b/internal/core/source_static.go
deleted file mode 100644
index 246fc6be938..00000000000
--- a/internal/core/source_static.go
+++ /dev/null
@@ -1,249 +0,0 @@
-package core
-
-import (
- "context"
- "fmt"
- "strings"
- "time"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-const (
- sourceStaticRetryPause = 5 * time.Second
-)
-
-type sourceStaticImpl interface {
- logger.Writer
- run(context.Context, *conf.PathConf, chan *conf.PathConf) error
- apiSourceDescribe() apiPathSourceOrReader
-}
-
-type sourceStaticParent interface {
- logger.Writer
- sourceStaticSetReady(context.Context, pathSourceStaticSetReadyReq)
- sourceStaticSetNotReady(context.Context, pathSourceStaticSetNotReadyReq)
-}
-
-// sourceStatic is a static source.
-type sourceStatic struct {
- conf *conf.PathConf
- parent sourceStaticParent
-
- ctx context.Context
- ctxCancel func()
- impl sourceStaticImpl
- running bool
-
- // in
- chReloadConf chan *conf.PathConf
- chSourceStaticImplSetReady chan pathSourceStaticSetReadyReq
- chSourceStaticImplSetNotReady chan pathSourceStaticSetNotReadyReq
-
- // out
- done chan struct{}
-}
-
-func newSourceStatic(
- cnf *conf.PathConf,
- readTimeout conf.StringDuration,
- writeTimeout conf.StringDuration,
- writeQueueSize int,
- parent sourceStaticParent,
-) *sourceStatic {
- s := &sourceStatic{
- conf: cnf,
- parent: parent,
- chReloadConf: make(chan *conf.PathConf),
- chSourceStaticImplSetReady: make(chan pathSourceStaticSetReadyReq),
- chSourceStaticImplSetNotReady: make(chan pathSourceStaticSetNotReadyReq),
- }
-
- switch {
- case strings.HasPrefix(cnf.Source, "rtsp://") ||
- strings.HasPrefix(cnf.Source, "rtsps://"):
- s.impl = newRTSPSource(
- readTimeout,
- writeTimeout,
- writeQueueSize,
- s)
-
- case strings.HasPrefix(cnf.Source, "rtmp://") ||
- strings.HasPrefix(cnf.Source, "rtmps://"):
- s.impl = newRTMPSource(
- readTimeout,
- writeTimeout,
- s)
-
- case strings.HasPrefix(cnf.Source, "http://") ||
- strings.HasPrefix(cnf.Source, "https://"):
- s.impl = newHLSSource(
- s)
-
- case strings.HasPrefix(cnf.Source, "udp://"):
- s.impl = newUDPSource(
- readTimeout,
- s)
-
- case strings.HasPrefix(cnf.Source, "srt://"):
- s.impl = newSRTSource(
- readTimeout,
- s)
-
- case strings.HasPrefix(cnf.Source, "whep://") ||
- strings.HasPrefix(cnf.Source, "wheps://"):
- s.impl = newWebRTCSource(
- readTimeout,
- s)
-
- case cnf.Source == "rpiCamera":
- s.impl = newRPICameraSource(
- s)
- }
-
- return s
-}
-
-func (s *sourceStatic) close(reason string) {
- s.stop(reason)
-}
-
-func (s *sourceStatic) start(onDemand bool) {
- if s.running {
- panic("should not happen")
- }
-
- s.running = true
- s.impl.Log(logger.Info, "started%s",
- func() string {
- if onDemand {
- return " on demand"
- }
- return ""
- }())
-
- s.ctx, s.ctxCancel = context.WithCancel(context.Background())
- s.done = make(chan struct{})
-
- go s.run()
-}
-
-func (s *sourceStatic) stop(reason string) {
- if !s.running {
- panic("should not happen")
- }
-
- s.running = false
- s.impl.Log(logger.Info, "stopped: %s", reason)
-
- s.ctxCancel()
-
- // we must wait since s.ctx is not thread safe
- <-s.done
-}
-
-func (s *sourceStatic) Log(level logger.Level, format string, args ...interface{}) {
- s.parent.Log(level, format, args...)
-}
-
-func (s *sourceStatic) run() {
- defer close(s.done)
-
- var innerCtx context.Context
- var innerCtxCancel func()
- implErr := make(chan error)
- innerReloadConf := make(chan *conf.PathConf)
-
- recreate := func() {
- innerCtx, innerCtxCancel = context.WithCancel(context.Background())
- go func() {
- implErr <- s.impl.run(innerCtx, s.conf, innerReloadConf)
- }()
- }
-
- recreate()
-
- recreating := false
- recreateTimer := newEmptyTimer()
-
- for {
- select {
- case err := <-implErr:
- innerCtxCancel()
- s.impl.Log(logger.Error, err.Error())
- recreating = true
- recreateTimer = time.NewTimer(sourceStaticRetryPause)
-
- case newConf := <-s.chReloadConf:
- s.conf = newConf
- if !recreating {
- cReloadConf := innerReloadConf
- cInnerCtx := innerCtx
- go func() {
- select {
- case cReloadConf <- newConf:
- case <-cInnerCtx.Done():
- }
- }()
- }
-
- case req := <-s.chSourceStaticImplSetReady:
- s.parent.sourceStaticSetReady(s.ctx, req)
-
- case req := <-s.chSourceStaticImplSetNotReady:
- s.parent.sourceStaticSetNotReady(s.ctx, req)
-
- case <-recreateTimer.C:
- recreate()
- recreating = false
-
- case <-s.ctx.Done():
- if !recreating {
- innerCtxCancel()
- <-implErr
- }
- return
- }
- }
-}
-
-func (s *sourceStatic) reloadConf(newConf *conf.PathConf) {
- select {
- case s.chReloadConf <- newConf:
- case <-s.ctx.Done():
- }
-}
-
-// apiSourceDescribe implements source.
-func (s *sourceStatic) apiSourceDescribe() apiPathSourceOrReader {
- return s.impl.apiSourceDescribe()
-}
-
-// setReady is called by a sourceStaticImpl.
-func (s *sourceStatic) setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes {
- req.res = make(chan pathSourceStaticSetReadyRes)
- select {
- case s.chSourceStaticImplSetReady <- req:
- res := <-req.res
-
- if res.err == nil {
- s.impl.Log(logger.Info, "ready: %s", sourceMediaInfo(req.desc.Medias))
- }
-
- return res
-
- case <-s.ctx.Done():
- return pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")}
- }
-}
-
-// setNotReady is called by a sourceStaticImpl.
-func (s *sourceStatic) setNotReady(req pathSourceStaticSetNotReadyReq) {
- req.res = make(chan struct{})
- select {
- case s.chSourceStaticImplSetNotReady <- req:
- <-req.res
- case <-s.ctx.Done():
- }
-}
diff --git a/internal/core/srt_conn.go b/internal/core/srt_conn.go
deleted file mode 100644
index 860c994df5b..00000000000
--- a/internal/core/srt_conn.go
+++ /dev/null
@@ -1,727 +0,0 @@
-package core
-
-import (
- "bufio"
- "context"
- "errors"
- "fmt"
- "net"
- "strings"
- "sync"
- "time"
-
- "github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/mediacommon/pkg/codecs/ac3"
- "github.com/bluenviron/mediacommon/pkg/codecs/h264"
- "github.com/bluenviron/mediacommon/pkg/codecs/h265"
- "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
- "github.com/datarhei/gosrt"
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/asyncwriter"
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
- "github.com/bluenviron/mediamtx/internal/stream"
- "github.com/bluenviron/mediamtx/internal/unit"
-)
-
-func durationGoToMPEGTS(v time.Duration) int64 {
- return int64(v.Seconds() * 90000)
-}
-
-func srtCheckPassphrase(connReq srt.ConnRequest, passphrase string) error {
- if passphrase == "" {
- return nil
- }
-
- if !connReq.IsEncrypted() {
- return fmt.Errorf("connection is encrypted, but not passphrase is defined in configuration")
- }
-
- err := connReq.SetPassphrase(passphrase)
- if err != nil {
- return fmt.Errorf("invalid passphrase")
- }
-
- return nil
-}
-
-type srtConnState int
-
-const (
- srtConnStateRead srtConnState = iota + 1
- srtConnStatePublish
-)
-
-type srtConnPathManager interface {
- addReader(req pathAddReaderReq) pathAddReaderRes
- addPublisher(req pathAddPublisherReq) pathAddPublisherRes
-}
-
-type srtConnParent interface {
- logger.Writer
- closeConn(*srtConn)
-}
-
-type srtConn struct {
- *conn
-
- rtspAddress string
- readTimeout conf.StringDuration
- writeTimeout conf.StringDuration
- writeQueueSize int
- udpMaxPayloadSize int
- connReq srt.ConnRequest
- wg *sync.WaitGroup
- externalCmdPool *externalcmd.Pool
- pathManager srtConnPathManager
- parent srtConnParent
-
- ctx context.Context
- ctxCancel func()
- created time.Time
- uuid uuid.UUID
- mutex sync.RWMutex
- state srtConnState
- pathName string
- sconn srt.Conn
-
- chNew chan srtNewConnReq
- chSetConn chan srt.Conn
-}
-
-func newSRTConn(
- parentCtx context.Context,
- rtspAddress string,
- readTimeout conf.StringDuration,
- writeTimeout conf.StringDuration,
- writeQueueSize int,
- udpMaxPayloadSize int,
- connReq srt.ConnRequest,
- runOnConnect string,
- runOnConnectRestart bool,
- runOnDisconnect string,
- wg *sync.WaitGroup,
- externalCmdPool *externalcmd.Pool,
- pathManager srtConnPathManager,
- parent srtConnParent,
-) *srtConn {
- ctx, ctxCancel := context.WithCancel(parentCtx)
-
- c := &srtConn{
- rtspAddress: rtspAddress,
- readTimeout: readTimeout,
- writeTimeout: writeTimeout,
- writeQueueSize: writeQueueSize,
- udpMaxPayloadSize: udpMaxPayloadSize,
- connReq: connReq,
- wg: wg,
- externalCmdPool: externalCmdPool,
- pathManager: pathManager,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- created: time.Now(),
- uuid: uuid.New(),
- chNew: make(chan srtNewConnReq),
- chSetConn: make(chan srt.Conn),
- }
-
- c.conn = newConn(
- rtspAddress,
- runOnConnect,
- runOnConnectRestart,
- runOnDisconnect,
- externalCmdPool,
- c,
- )
-
- c.Log(logger.Info, "opened")
-
- c.wg.Add(1)
- go c.run()
-
- return c
-}
-
-func (c *srtConn) close() {
- c.ctxCancel()
-}
-
-func (c *srtConn) Log(level logger.Level, format string, args ...interface{}) {
- c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.connReq.RemoteAddr()}, args...)...)
-}
-
-func (c *srtConn) ip() net.IP {
- return c.connReq.RemoteAddr().(*net.UDPAddr).IP
-}
-
-func (c *srtConn) run() { //nolint:dupl
- defer c.wg.Done()
-
- desc := c.apiReaderDescribe()
- c.conn.open(desc)
- defer c.conn.close(desc)
-
- err := c.runInner()
-
- c.ctxCancel()
-
- c.parent.closeConn(c)
-
- c.Log(logger.Info, "closed: %v", err)
-}
-
-func (c *srtConn) 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 *srtConn) runInner2(req srtNewConnReq) (bool, error) {
- parts := strings.Split(req.connReq.StreamId(), ":")
- if (len(parts) != 2 && len(parts) != 4) || (parts[0] != "read" && parts[0] != "publish") {
- return false, fmt.Errorf("invalid streamid '%s':"+
- " it must be 'action:pathname' or 'action:pathname:user:pass', "+
- "where action is either read or publish, pathname is the path name, user and pass are the credentials",
- req.connReq.StreamId())
- }
-
- pathName := parts[1]
- user := ""
- pass := ""
-
- if len(parts) == 4 {
- user, pass = parts[2], parts[3]
- }
-
- if parts[0] == "publish" {
- return c.runPublish(req, pathName, user, pass)
- }
- return c.runRead(req, pathName, user, pass)
-}
-
-func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pass string) (bool, error) {
- res := c.pathManager.addPublisher(pathAddPublisherReq{
- author: c,
- pathName: pathName,
- credentials: authCredentials{
- ip: c.ip(),
- user: user,
- pass: pass,
- proto: authProtocolSRT,
- id: &c.uuid,
- },
- })
-
- if res.err != nil {
- if terr, ok := res.err.(*errAuthentication); ok {
- // TODO: re-enable. Currently this freezes the listener.
- // wait some seconds to stop brute force attacks
- // <-time.After(srtPauseAfterAuthError)
- return false, terr
- }
- return false, res.err
- }
-
- defer res.path.removePublisher(pathRemovePublisherReq{author: c})
-
- err := srtCheckPassphrase(req.connReq, res.path.conf.SRTPublishPassphrase)
- if err != nil {
- return false, err
- }
-
- sconn, err := c.exchangeRequestWithConn(req)
- if err != nil {
- return true, err
- }
-
- c.mutex.Lock()
- c.state = srtConnStatePublish
- c.pathName = pathName
- c.sconn = sconn
- c.mutex.Unlock()
-
- readerErr := make(chan error)
- go func() {
- readerErr <- c.runPublishReader(sconn, res.path)
- }()
-
- select {
- case err := <-readerErr:
- sconn.Close()
- return true, err
-
- case <-c.ctx.Done():
- sconn.Close()
- <-readerErr
- return true, errors.New("terminated")
- }
-}
-
-func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error {
- sconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))
- r, err := mpegts.NewReader(mpegts.NewBufferedReader(sconn))
- if err != nil {
- return err
- }
-
- decodeErrLogger := logger.NewLimitedLogger(c)
-
- r.OnDecodeError(func(err error) {
- decodeErrLogger.Log(logger.Warn, err.Error())
- })
-
- var stream *stream.Stream
-
- medias, err := mpegtsSetupTracks(r, &stream)
- if err != nil {
- return err
- }
-
- rres := path.startPublisher(pathStartPublisherReq{
- author: c,
- desc: &description.Session{Medias: medias},
- generateRTPPackets: true,
- })
- if rres.err != nil {
- return rres.err
- }
-
- stream = rres.stream
-
- for {
- err := r.Read()
- if err != nil {
- return err
- }
- }
-}
-
-func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass string) (bool, error) {
- res := c.pathManager.addReader(pathAddReaderReq{
- author: c,
- pathName: pathName,
- credentials: authCredentials{
- ip: c.ip(),
- user: user,
- pass: pass,
- proto: authProtocolSRT,
- id: &c.uuid,
- },
- })
-
- if res.err != nil {
- if terr, ok := res.err.(*errAuthentication); ok {
- // TODO: re-enable. Currently this freezes the listener.
- // wait some seconds to stop brute force attacks
- // <-time.After(srtPauseAfterAuthError)
- return false, terr
- }
- return false, res.err
- }
-
- defer res.path.removeReader(pathRemoveReaderReq{author: c})
-
- err := srtCheckPassphrase(req.connReq, res.path.conf.SRTReadPassphrase)
- if err != nil {
- return false, err
- }
-
- sconn, err := c.exchangeRequestWithConn(req)
- if err != nil {
- return true, err
- }
- defer sconn.Close()
-
- c.mutex.Lock()
- c.state = srtConnStateRead
- c.pathName = pathName
- c.sconn = sconn
- c.mutex.Unlock()
-
- writer := asyncwriter.New(c.writeQueueSize, c)
-
- defer res.stream.RemoveReader(writer)
-
- var w *mpegts.Writer
- var tracks []*mpegts.Track
- var medias []*description.Media
- bw := bufio.NewWriterSize(sconn, srtMaxPayloadSize(c.udpMaxPayloadSize))
-
- addTrack := func(medi *description.Media, codec mpegts.Codec) *mpegts.Track {
- track := &mpegts.Track{
- Codec: codec,
- }
- tracks = append(tracks, track)
- medias = append(medias, medi)
- return track
- }
-
- for _, medi := range res.stream.Desc().Medias {
- for _, forma := range medi.Formats {
- switch forma := forma.(type) {
- case *format.H265: //nolint:dupl
- track := addTrack(medi, &mpegts.CodecH265{})
-
- var dtsExtractor *h265.DTSExtractor
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.H265)
- if tunit.AU == nil {
- return nil
- }
-
- randomAccess := h265.IsRandomAccess(tunit.AU)
-
- if dtsExtractor == nil {
- if !randomAccess {
- return nil
- }
- dtsExtractor = h265.NewDTSExtractor()
- }
-
- dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
- if err != nil {
- return err
- }
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
- if err != nil {
- return err
- }
- return bw.Flush()
- })
-
- case *format.H264: //nolint:dupl
- track := addTrack(medi, &mpegts.CodecH264{})
-
- var dtsExtractor *h264.DTSExtractor
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.H264)
- if tunit.AU == nil {
- return nil
- }
-
- idrPresent := h264.IDRPresent(tunit.AU)
-
- if dtsExtractor == nil {
- if !idrPresent {
- return nil
- }
- dtsExtractor = h264.NewDTSExtractor()
- }
-
- dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
- if err != nil {
- return err
- }
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU)
- if err != nil {
- return err
- }
- return bw.Flush()
- })
-
- case *format.MPEG4Video:
- track := addTrack(medi, &mpegts.CodecMPEG4Video{})
-
- firstReceived := false
- var lastPTS time.Duration
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG4Video)
- if tunit.Frame == nil {
- return nil
- }
-
- if !firstReceived {
- firstReceived = true
- } else if tunit.PTS < lastPTS {
- return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)")
- }
- lastPTS = tunit.PTS
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
- if err != nil {
- return err
- }
- return bw.Flush()
- })
-
- case *format.MPEG1Video:
- track := addTrack(medi, &mpegts.CodecMPEG1Video{})
-
- firstReceived := false
- var lastPTS time.Duration
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG1Video)
- if tunit.Frame == nil {
- return nil
- }
-
- if !firstReceived {
- firstReceived = true
- } else if tunit.PTS < lastPTS {
- return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
- }
- lastPTS = tunit.PTS
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
- if err != nil {
- return err
- }
- return bw.Flush()
- })
-
- case *format.MPEG4Audio:
- track := addTrack(medi, &mpegts.CodecMPEG4Audio{
- Config: *forma.GetConfig(),
- })
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG4Audio)
- if tunit.AUs == nil {
- return nil
- }
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs)
- if err != nil {
- return err
- }
- return bw.Flush()
- })
-
- case *format.Opus:
- track := addTrack(medi, &mpegts.CodecOpus{
- ChannelCount: func() int {
- if forma.IsStereo {
- return 2
- }
- return 1
- }(),
- })
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.Opus)
- if tunit.Packets == nil {
- return nil
- }
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets)
- if err != nil {
- return err
- }
- return bw.Flush()
- })
-
- case *format.MPEG1Audio:
- track := addTrack(medi, &mpegts.CodecMPEG1Audio{})
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG1Audio)
- if tunit.Frames == nil {
- return nil
- }
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames)
- if err != nil {
- return err
- }
- return bw.Flush()
- })
-
- case *format.AC3:
- track := addTrack(medi, &mpegts.CodecAC3{})
-
- sampleRate := time.Duration(forma.SampleRate)
-
- res.stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
- tunit := u.(*unit.AC3)
- if tunit.Frames == nil {
- return nil
- }
-
- for i, frame := range tunit.Frames {
- framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame*
- time.Second/sampleRate
-
- sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))
- err = w.WriteAC3(track, durationGoToMPEGTS(framePTS), frame)
- if err != nil {
- return err
- }
- }
- return bw.Flush()
- })
- }
- }
- }
-
- if len(tracks) == 0 {
- return true, fmt.Errorf(
- "the stream doesn't contain any supported codec, which are currently H265, H264, Opus, MPEG-4 Audio")
- }
-
- c.Log(logger.Info, "is reading from path '%s', %s",
- res.path.name, sourceMediaInfo(medias))
-
- pathConf := res.path.safeConf()
-
- if pathConf.RunOnRead != "" {
- env := res.path.externalCmdEnv()
- desc := c.apiReaderDescribe()
- env["MTX_READER_TYPE"] = desc.Type
- env["MTX_READER_ID"] = desc.ID
-
- c.Log(logger.Info, "runOnRead command started")
- onReadCmd := externalcmd.NewCmd(
- c.externalCmdPool,
- pathConf.RunOnRead,
- pathConf.RunOnReadRestart,
- env,
- func(err error) {
- c.Log(logger.Info, "runOnRead command exited: %v", err)
- })
- defer func() {
- onReadCmd.Close()
- c.Log(logger.Info, "runOnRead command stopped")
- }()
- }
-
- if pathConf.RunOnUnread != "" {
- defer func() {
- env := res.path.externalCmdEnv()
- desc := c.apiReaderDescribe()
- env["MTX_READER_TYPE"] = desc.Type
- env["MTX_READER_ID"] = desc.ID
-
- c.Log(logger.Info, "runOnUnread command launched")
- externalcmd.NewCmd(
- c.externalCmdPool,
- pathConf.RunOnUnread,
- false,
- env,
- nil)
- }()
- }
-
- w = mpegts.NewWriter(bw, tracks)
-
- // disable read deadline
- sconn.SetReadDeadline(time.Time{})
-
- writer.Start()
-
- select {
- case <-c.ctx.Done():
- writer.Stop()
- return true, fmt.Errorf("terminated")
-
- case err := <-writer.Error():
- return true, err
- }
-}
-
-func (c *srtConn) 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 *srtConn) new(req srtNewConnReq) *srtConn {
- select {
- case c.chNew <- req:
- return <-req.res
-
- case <-c.ctx.Done():
- return nil
- }
-}
-
-// setConn is called by srtListener .
-func (c *srtConn) setConn(sconn srt.Conn) {
- select {
- case c.chSetConn <- sconn:
- case <-c.ctx.Done():
- }
-}
-
-// apiReaderDescribe implements reader.
-func (c *srtConn) apiReaderDescribe() apiPathSourceOrReader {
- return apiPathSourceOrReader{
- Type: "srtConn",
- ID: c.uuid.String(),
- }
-}
-
-// apiSourceDescribe implements source.
-func (c *srtConn) apiSourceDescribe() apiPathSourceOrReader {
- return c.apiReaderDescribe()
-}
-
-func (c *srtConn) apiItem() *apiSRTConn {
- c.mutex.RLock()
- defer c.mutex.RUnlock()
-
- bytesReceived := uint64(0)
- bytesSent := uint64(0)
-
- if c.sconn != nil {
- var s srt.Statistics
- c.sconn.Stats(&s)
- bytesReceived = s.Accumulated.ByteRecv
- bytesSent = s.Accumulated.ByteSent
- }
-
- return &apiSRTConn{
- ID: c.uuid,
- Created: c.created,
- RemoteAddr: c.connReq.RemoteAddr().String(),
- State: func() apiSRTConnState {
- switch c.state {
- case srtConnStateRead:
- return apiSRTConnStateRead
-
- case srtConnStatePublish:
- return apiSRTConnStatePublish
-
- default:
- return apiSRTConnStateIdle
- }
- }(),
- Path: c.pathName,
- BytesReceived: bytesReceived,
- BytesSent: bytesSent,
- }
-}
diff --git a/internal/core/srt_server.go b/internal/core/srt_server.go
deleted file mode 100644
index 0edebae893b..00000000000
--- a/internal/core/srt_server.go
+++ /dev/null
@@ -1,329 +0,0 @@
-package core
-
-import (
- "context"
- "fmt"
- "sort"
- "sync"
- "time"
-
- "github.com/datarhei/gosrt"
- "github.com/google/uuid"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-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 *srtConn
-}
-
-type srtServerAPIConnsListRes struct {
- data *apiSRTConnsList
- err error
-}
-
-type srtServerAPIConnsListReq struct {
- res chan srtServerAPIConnsListRes
-}
-
-type srtServerAPIConnsGetRes struct {
- data *apiSRTConn
- err error
-}
-
-type srtServerAPIConnsGetReq struct {
- uuid uuid.UUID
- res chan srtServerAPIConnsGetRes
-}
-
-type srtServerAPIConnsKickRes struct {
- err error
-}
-
-type srtServerAPIConnsKickReq struct {
- uuid uuid.UUID
- res chan srtServerAPIConnsKickRes
-}
-
-type srtServerParent interface {
- logger.Writer
-}
-
-type srtServer struct {
- rtspAddress string
- readTimeout conf.StringDuration
- writeTimeout conf.StringDuration
- writeQueueSize int
- udpMaxPayloadSize int
- runOnConnect string
- runOnConnectRestart bool
- runOnDisconnect string
- externalCmdPool *externalcmd.Pool
- pathManager *pathManager
- parent srtServerParent
-
- ctx context.Context
- ctxCancel func()
- wg sync.WaitGroup
- ln srt.Listener
- conns map[*srtConn]struct{}
-
- // in
- chNewConnRequest chan srtNewConnReq
- chAcceptErr chan error
- chCloseConn chan *srtConn
- chAPIConnsList chan srtServerAPIConnsListReq
- chAPIConnsGet chan srtServerAPIConnsGetReq
- chAPIConnsKick chan srtServerAPIConnsKickReq
-}
-
-func newSRTServer(
- address string,
- rtspAddress string,
- readTimeout conf.StringDuration,
- writeTimeout conf.StringDuration,
- writeQueueSize int,
- udpMaxPayloadSize int,
- runOnConnect string,
- runOnConnectRestart bool,
- runOnDisconnect string,
- externalCmdPool *externalcmd.Pool,
- pathManager *pathManager,
- parent srtServerParent,
-) (*srtServer, error) {
- conf := srt.DefaultConfig()
- conf.ConnectionTimeout = time.Duration(readTimeout)
- conf.PayloadSize = uint32(srtMaxPayloadSize(udpMaxPayloadSize))
-
- ln, err := srt.Listen("srt", address, conf)
- if err != nil {
- return nil, err
- }
-
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- s := &srtServer{
- rtspAddress: rtspAddress,
- readTimeout: readTimeout,
- writeTimeout: writeTimeout,
- writeQueueSize: writeQueueSize,
- udpMaxPayloadSize: udpMaxPayloadSize,
- runOnConnect: runOnConnect,
- runOnConnectRestart: runOnConnectRestart,
- runOnDisconnect: runOnDisconnect,
- externalCmdPool: externalCmdPool,
- pathManager: pathManager,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- ln: ln,
- conns: make(map[*srtConn]struct{}),
- chNewConnRequest: make(chan srtNewConnReq),
- chAcceptErr: make(chan error),
- chCloseConn: make(chan *srtConn),
- chAPIConnsList: make(chan srtServerAPIConnsListReq),
- chAPIConnsGet: make(chan srtServerAPIConnsGetReq),
- chAPIConnsKick: make(chan srtServerAPIConnsKickReq),
- }
-
- s.Log(logger.Info, "listener opened on "+address+" (UDP)")
-
- newSRTListener(
- s.ln,
- &s.wg,
- s,
- )
-
- s.wg.Add(1)
- go s.run()
-
- return s, nil
-}
-
-// Log is the main logging function.
-func (s *srtServer) Log(level logger.Level, format string, args ...interface{}) {
- s.parent.Log(level, "[SRT] "+format, args...)
-}
-
-func (s *srtServer) close() {
- s.Log(logger.Info, "listener is closing")
- s.ctxCancel()
- s.wg.Wait()
-}
-
-func (s *srtServer) run() {
- defer s.wg.Done()
-
-outer:
- for {
- select {
- case err := <-s.chAcceptErr:
- s.Log(logger.Error, "%s", err)
- break outer
-
- case req := <-s.chNewConnRequest:
- c := newSRTConn(
- s.ctx,
- s.rtspAddress,
- s.readTimeout,
- s.writeTimeout,
- s.writeQueueSize,
- s.udpMaxPayloadSize,
- req.connReq,
- s.runOnConnect,
- s.runOnConnectRestart,
- s.runOnDisconnect,
- &s.wg,
- s.externalCmdPool,
- s.pathManager,
- s)
- s.conns[c] = struct{}{}
- req.res <- c
-
- case c := <-s.chCloseConn:
- delete(s.conns, c)
-
- case req := <-s.chAPIConnsList:
- data := &apiSRTConnsList{
- Items: []*apiSRTConn{},
- }
-
- for c := range s.conns {
- data.Items = append(data.Items, c.apiItem())
- }
-
- sort.Slice(data.Items, func(i, j int) bool {
- return data.Items[i].Created.Before(data.Items[j].Created)
- })
-
- req.res <- srtServerAPIConnsListRes{data: data}
-
- case req := <-s.chAPIConnsGet:
- c := s.findConnByUUID(req.uuid)
- if c == nil {
- req.res <- srtServerAPIConnsGetRes{err: errAPINotFound}
- continue
- }
-
- req.res <- srtServerAPIConnsGetRes{data: c.apiItem()}
-
- case req := <-s.chAPIConnsKick:
- c := s.findConnByUUID(req.uuid)
- if c == nil {
- req.res <- srtServerAPIConnsKickRes{err: errAPINotFound}
- continue
- }
-
- delete(s.conns, c)
- c.close()
- req.res <- srtServerAPIConnsKickRes{}
-
- case <-s.ctx.Done():
- break outer
- }
- }
-
- s.ctxCancel()
-
- s.ln.Close()
-}
-
-func (s *srtServer) findConnByUUID(uuid uuid.UUID) *srtConn {
- for sx := range s.conns {
- if sx.uuid == uuid {
- return sx
- }
- }
- return nil
-}
-
-// newConnRequest is called by srtListener.
-func (s *srtServer) newConnRequest(connReq srt.ConnRequest) *srtConn {
- req := srtNewConnReq{
- connReq: connReq,
- res: make(chan *srtConn),
- }
-
- select {
- case s.chNewConnRequest <- req:
- c := <-req.res
-
- return c.new(req)
-
- case <-s.ctx.Done():
- return nil
- }
-}
-
-// acceptError is called by srtListener.
-func (s *srtServer) acceptError(err error) {
- select {
- case s.chAcceptErr <- err:
- case <-s.ctx.Done():
- }
-}
-
-// closeConn is called by srtConn.
-func (s *srtServer) closeConn(c *srtConn) {
- select {
- case s.chCloseConn <- c:
- case <-s.ctx.Done():
- }
-}
-
-// apiConnsList is called by api.
-func (s *srtServer) apiConnsList() (*apiSRTConnsList, error) {
- req := srtServerAPIConnsListReq{
- res: make(chan srtServerAPIConnsListRes),
- }
-
- select {
- case s.chAPIConnsList <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-// apiConnsGet is called by api.
-func (s *srtServer) apiConnsGet(uuid uuid.UUID) (*apiSRTConn, error) {
- req := srtServerAPIConnsGetReq{
- uuid: uuid,
- res: make(chan srtServerAPIConnsGetRes),
- }
-
- select {
- case s.chAPIConnsGet <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-s.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-// apiConnsKick is called by api.
-func (s *srtServer) apiConnsKick(uuid uuid.UUID) error {
- req := srtServerAPIConnsKickReq{
- uuid: uuid,
- res: make(chan srtServerAPIConnsKickRes),
- }
-
- select {
- case s.chAPIConnsKick <- req:
- res := <-req.res
- return res.err
-
- case <-s.ctx.Done():
- return fmt.Errorf("terminated")
- }
-}
diff --git a/internal/core/srt_server_test.go b/internal/core/srt_server_test.go
index d1afc29aa13..fa55d6f7a8e 100644
--- a/internal/core/srt_server_test.go
+++ b/internal/core/srt_server_test.go
@@ -18,7 +18,7 @@ func TestSRTServer(t *testing.T) {
} {
t.Run(ca, func(t *testing.T) {
conf := "paths:\n" +
- " all:\n"
+ " all_others:\n"
switch ca {
case "publish passphrase":
diff --git a/internal/core/srt_source.go b/internal/core/srt_source.go
deleted file mode 100644
index bbe90936928..00000000000
--- a/internal/core/srt_source.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package core
-
-import (
- "context"
- "time"
-
- "github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
- "github.com/datarhei/gosrt"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/logger"
- "github.com/bluenviron/mediamtx/internal/stream"
-)
-
-type srtSourceParent interface {
- logger.Writer
- setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes
- setNotReady(req pathSourceStaticSetNotReadyReq)
-}
-
-type srtSource struct {
- readTimeout conf.StringDuration
- parent srtSourceParent
-}
-
-func newSRTSource(
- readTimeout conf.StringDuration,
- parent srtSourceParent,
-) *srtSource {
- s := &srtSource{
- readTimeout: readTimeout,
- parent: parent,
- }
-
- return s
-}
-
-func (s *srtSource) Log(level logger.Level, format string, args ...interface{}) {
- s.parent.Log(level, "[SRT source] "+format, args...)
-}
-
-// run implements sourceStaticImpl.
-func (s *srtSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error {
- s.Log(logger.Debug, "connecting")
-
- conf := srt.DefaultConfig()
- address, err := conf.UnmarshalURL(cnf.Source)
- if err != nil {
- return err
- }
-
- err = conf.Validate()
- if err != nil {
- return err
- }
-
- sconn, err := srt.Dial("srt", address, conf)
- if err != nil {
- return err
- }
-
- readDone := make(chan error)
- go func() {
- readDone <- s.runReader(sconn)
- }()
-
- for {
- select {
- case err := <-readDone:
- sconn.Close()
- return err
-
- case <-reloadConf:
-
- case <-ctx.Done():
- sconn.Close()
- <-readDone
- return nil
- }
- }
-}
-
-func (s *srtSource) runReader(sconn srt.Conn) error {
- sconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
- r, err := mpegts.NewReader(mpegts.NewBufferedReader(sconn))
- if err != nil {
- return err
- }
-
- decodeErrLogger := logger.NewLimitedLogger(s)
-
- r.OnDecodeError(func(err error) {
- decodeErrLogger.Log(logger.Warn, err.Error())
- })
-
- var stream *stream.Stream
-
- medias, err := mpegtsSetupTracks(r, &stream)
- if err != nil {
- return err
- }
-
- res := s.parent.setReady(pathSourceStaticSetReadyReq{
- desc: &description.Session{Medias: medias},
- generateRTPPackets: true,
- })
- if res.err != nil {
- return res.err
- }
-
- stream = res.stream
-
- for {
- sconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
- err := r.Read()
- if err != nil {
- return err
- }
- }
-}
-
-// apiSourceDescribe implements sourceStaticImpl.
-func (*srtSource) apiSourceDescribe() apiPathSourceOrReader {
- return apiPathSourceOrReader{
- Type: "srtSource",
- ID: "",
- }
-}
diff --git a/internal/core/srt_source_test.go b/internal/core/srt_source_test.go
deleted file mode 100644
index bb67aa256b1..00000000000
--- a/internal/core/srt_source_test.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package core
-
-import (
- "bufio"
- "testing"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
- "github.com/datarhei/gosrt"
- "github.com/pion/rtp"
- "github.com/stretchr/testify/require"
-)
-
-func TestSRTSource(t *testing.T) {
- ln, err := srt.Listen("srt", "localhost:9999", srt.DefaultConfig())
- require.NoError(t, err)
- defer ln.Close()
-
- connected := make(chan struct{})
- received := make(chan struct{})
- done := make(chan struct{})
-
- 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
- })
- require.NoError(t, err)
- require.NotNil(t, conn)
- defer conn.Close()
-
- track := &mpegts.Track{
- Codec: &mpegts.CodecH264{},
- }
-
- bw := bufio.NewWriter(conn)
- w := mpegts.NewWriter(bw, []*mpegts.Track{track})
- require.NoError(t, err)
-
- err = w.WriteH26x(track, 0, 0, true, [][]byte{
- { // IDR
- 0x05, 1,
- },
- })
- require.NoError(t, err)
-
- err = bw.Flush()
- require.NoError(t, err)
-
- <-connected
-
- err = w.WriteH26x(track, 0, 0, true, [][]byte{{5, 2}})
- require.NoError(t, err)
-
- err = bw.Flush()
- require.NoError(t, err)
-
- <-done
- }()
-
- p, ok := newInstance("paths:\n" +
- " proxied:\n" +
- " source: srt://localhost:9999?streamid=sidname&passphrase=ttest1234567\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
-
- c := gortsplib.Client{}
-
- u, err := url.Parse("rtsp://127.0.0.1:8554/proxied")
- require.NoError(t, err)
-
- err = c.Start(u.Scheme, u.Host)
- require.NoError(t, err)
- defer c.Close()
-
- desc, _, err := c.Describe(u)
- require.NoError(t, err)
-
- var forma *format.H264
- medi := desc.FindFormat(&forma)
-
- _, err = c.Setup(desc.BaseURL, medi, 0, 0)
- require.NoError(t, err)
-
- c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
- require.Equal(t, []byte{5, 1}, pkt.Payload)
- close(received)
- })
-
- _, err = c.Play(nil)
- require.NoError(t, err)
-
- close(connected)
- <-received
- close(done)
-}
diff --git a/internal/core/static_source_handler.go b/internal/core/static_source_handler.go
new file mode 100644
index 00000000000..c437d1e14c8
--- /dev/null
+++ b/internal/core/static_source_handler.go
@@ -0,0 +1,264 @@
+package core
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/logger"
+ hlssource "github.com/bluenviron/mediamtx/internal/staticsources/hls"
+ rpicamerasource "github.com/bluenviron/mediamtx/internal/staticsources/rpicamera"
+ rtmpsource "github.com/bluenviron/mediamtx/internal/staticsources/rtmp"
+ rtspsource "github.com/bluenviron/mediamtx/internal/staticsources/rtsp"
+ srtsource "github.com/bluenviron/mediamtx/internal/staticsources/srt"
+ udpsource "github.com/bluenviron/mediamtx/internal/staticsources/udp"
+ webrtcsource "github.com/bluenviron/mediamtx/internal/staticsources/webrtc"
+)
+
+const (
+ staticSourceHandlerRetryPause = 5 * time.Second
+)
+
+type staticSourceHandlerParent interface {
+ logger.Writer
+ staticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)
+ staticSourceHandlerSetNotReady(context.Context, defs.PathSourceStaticSetNotReadyReq)
+}
+
+// staticSourceHandler is a static source handler.
+type staticSourceHandler struct {
+ conf *conf.Path
+ logLevel conf.LogLevel
+ readTimeout conf.StringDuration
+ writeTimeout conf.StringDuration
+ writeQueueSize int
+ resolvedSource string
+ parent staticSourceHandlerParent
+
+ ctx context.Context
+ ctxCancel func()
+ instance defs.StaticSource
+ running bool
+
+ // in
+ chReloadConf chan *conf.Path
+ chInstanceSetReady chan defs.PathSourceStaticSetReadyReq
+ chInstanceSetNotReady chan defs.PathSourceStaticSetNotReadyReq
+
+ // out
+ done chan struct{}
+}
+
+func (s *staticSourceHandler) initialize() {
+ s.chReloadConf = make(chan *conf.Path)
+ s.chInstanceSetReady = make(chan defs.PathSourceStaticSetReadyReq)
+ s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
+
+ switch {
+ case strings.HasPrefix(s.resolvedSource, "rtsp://") ||
+ strings.HasPrefix(s.resolvedSource, "rtsps://"):
+ s.instance = &rtspsource.Source{
+ ResolvedSource: s.resolvedSource,
+ ReadTimeout: s.readTimeout,
+ WriteTimeout: s.writeTimeout,
+ WriteQueueSize: s.writeQueueSize,
+ Parent: s,
+ }
+
+ case strings.HasPrefix(s.resolvedSource, "rtmp://") ||
+ strings.HasPrefix(s.resolvedSource, "rtmps://"):
+ s.instance = &rtmpsource.Source{
+ ResolvedSource: s.resolvedSource,
+ ReadTimeout: s.readTimeout,
+ WriteTimeout: s.writeTimeout,
+ Parent: s,
+ }
+
+ case strings.HasPrefix(s.resolvedSource, "http://") ||
+ strings.HasPrefix(s.resolvedSource, "https://"):
+ s.instance = &hlssource.Source{
+ ResolvedSource: s.resolvedSource,
+ ReadTimeout: s.readTimeout,
+ Parent: s,
+ }
+
+ case strings.HasPrefix(s.resolvedSource, "udp://"):
+ s.instance = &udpsource.Source{
+ ResolvedSource: s.resolvedSource,
+ ReadTimeout: s.readTimeout,
+ Parent: s,
+ }
+
+ case strings.HasPrefix(s.resolvedSource, "srt://"):
+ s.instance = &srtsource.Source{
+ ResolvedSource: s.resolvedSource,
+ ReadTimeout: s.readTimeout,
+ Parent: s,
+ }
+
+ case strings.HasPrefix(s.resolvedSource, "whep://") ||
+ strings.HasPrefix(s.resolvedSource, "wheps://"):
+ s.instance = &webrtcsource.Source{
+ ResolvedSource: s.resolvedSource,
+ ReadTimeout: s.readTimeout,
+ Parent: s,
+ }
+
+ case s.resolvedSource == "rpiCamera":
+ s.instance = &rpicamerasource.Source{
+ LogLevel: s.logLevel,
+ Parent: s,
+ }
+ }
+}
+
+func (s *staticSourceHandler) close(reason string) {
+ s.stop(reason)
+}
+
+func (s *staticSourceHandler) start(onDemand bool) {
+ if s.running {
+ panic("should not happen")
+ }
+
+ s.running = true
+ s.instance.Log(logger.Info, "started%s",
+ func() string {
+ if onDemand {
+ return " on demand"
+ }
+ return ""
+ }())
+
+ s.ctx, s.ctxCancel = context.WithCancel(context.Background())
+ s.done = make(chan struct{})
+
+ go s.run()
+}
+
+func (s *staticSourceHandler) stop(reason string) {
+ if !s.running {
+ panic("should not happen")
+ }
+
+ s.running = false
+ s.instance.Log(logger.Info, "stopped: %s", reason)
+
+ s.ctxCancel()
+
+ // we must wait since s.ctx is not thread safe
+ <-s.done
+}
+
+// Log implements logger.Writer.
+func (s *staticSourceHandler) Log(level logger.Level, format string, args ...interface{}) {
+ s.parent.Log(level, format, args...)
+}
+
+func (s *staticSourceHandler) run() {
+ defer close(s.done)
+
+ var runCtx context.Context
+ var runCtxCancel func()
+ runErr := make(chan error)
+ runReloadConf := make(chan *conf.Path)
+
+ recreate := func() {
+ runCtx, runCtxCancel = context.WithCancel(context.Background())
+ go func() {
+ runErr <- s.instance.Run(defs.StaticSourceRunParams{
+ Context: runCtx,
+ Conf: s.conf,
+ ReloadConf: runReloadConf,
+ })
+ }()
+ }
+
+ recreate()
+
+ recreating := false
+ recreateTimer := newEmptyTimer()
+
+ for {
+ select {
+ case err := <-runErr:
+ runCtxCancel()
+ s.instance.Log(logger.Error, err.Error())
+ recreating = true
+ recreateTimer = time.NewTimer(staticSourceHandlerRetryPause)
+
+ case req := <-s.chInstanceSetReady:
+ s.parent.staticSourceHandlerSetReady(s.ctx, req)
+
+ case req := <-s.chInstanceSetNotReady:
+ s.parent.staticSourceHandlerSetNotReady(s.ctx, req)
+
+ case newConf := <-s.chReloadConf:
+ s.conf = newConf
+ if !recreating {
+ cReloadConf := runReloadConf
+ cInnerCtx := runCtx
+ go func() {
+ select {
+ case cReloadConf <- newConf:
+ case <-cInnerCtx.Done():
+ }
+ }()
+ }
+
+ case <-recreateTimer.C:
+ recreate()
+ recreating = false
+
+ case <-s.ctx.Done():
+ if !recreating {
+ runCtxCancel()
+ <-runErr
+ }
+ return
+ }
+ }
+}
+
+func (s *staticSourceHandler) reloadConf(newConf *conf.Path) {
+ select {
+ case s.chReloadConf <- newConf:
+ case <-s.ctx.Done():
+ }
+}
+
+// APISourceDescribe instanceements source.
+func (s *staticSourceHandler) APISourceDescribe() defs.APIPathSourceOrReader {
+ return s.instance.APISourceDescribe()
+}
+
+// setReady is called by a staticSource.
+func (s *staticSourceHandler) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes {
+ req.Res = make(chan defs.PathSourceStaticSetReadyRes)
+ select {
+ case s.chInstanceSetReady <- req:
+ res := <-req.Res
+
+ if res.Err == nil {
+ s.instance.Log(logger.Info, "ready: %s", defs.MediasInfo(req.Desc.Medias))
+ }
+
+ return res
+
+ case <-s.ctx.Done():
+ return defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
+ }
+}
+
+// setNotReady is called by a staticSource.
+func (s *staticSourceHandler) SetNotReady(req defs.PathSourceStaticSetNotReadyReq) {
+ req.Res = make(chan struct{})
+ select {
+ case s.chInstanceSetNotReady <- req:
+ <-req.Res
+ case <-s.ctx.Done():
+ }
+}
diff --git a/internal/core/tls_fingerprint.go b/internal/core/tls_fingerprint.go
deleted file mode 100644
index ff2f70d7bab..00000000000
--- a/internal/core/tls_fingerprint.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package core
-
-import (
- "crypto/sha256"
- "crypto/tls"
- "encoding/hex"
- "fmt"
- "strings"
-)
-
-type fingerprintValidatorFunc func(tls.ConnectionState) error
-
-func fingerprintValidator(fingerprint string) fingerprintValidatorFunc {
- fingerprintLower := strings.ToLower(fingerprint)
-
- return func(cs tls.ConnectionState) error {
- h := sha256.New()
- h.Write(cs.PeerCertificates[0].Raw)
- hstr := hex.EncodeToString(h.Sum(nil))
-
- if hstr != fingerprintLower {
- return fmt.Errorf("source fingerprint does not match: expected %s, got %s",
- fingerprintLower, hstr)
- }
-
- return nil
- }
-}
-
-func tlsConfigForFingerprint(fingerprint string) *tls.Config {
- if fingerprint == "" {
- return nil
- }
-
- return &tls.Config{
- InsecureSkipVerify: true,
- VerifyConnection: fingerprintValidator(fingerprint),
- }
-}
diff --git a/internal/core/udp_source_test.go b/internal/core/udp_source_test.go
deleted file mode 100644
index f7900c198a1..00000000000
--- a/internal/core/udp_source_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package core
-
-import (
- "bufio"
- "net"
- "testing"
- "time"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
- "github.com/pion/rtp"
- "github.com/stretchr/testify/require"
-)
-
-func TestUDPSource(t *testing.T) {
- p, ok := newInstance("paths:\n" +
- " proxied:\n" +
- " source: udp://localhost:9999\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
-
- c := gortsplib.Client{}
-
- u, err := url.Parse("rtsp://127.0.0.1:8554/proxied")
- require.NoError(t, err)
-
- err = c.Start(u.Scheme, u.Host)
- require.NoError(t, err)
- defer c.Close()
-
- connected := make(chan struct{})
- received := make(chan struct{})
-
- go func() {
- time.Sleep(200 * time.Millisecond)
-
- conn, err := net.Dial("udp", "localhost:9999")
- require.NoError(t, err)
- defer conn.Close()
-
- track := &mpegts.Track{
- Codec: &mpegts.CodecH264{},
- }
-
- bw := bufio.NewWriter(conn)
- w := mpegts.NewWriter(bw, []*mpegts.Track{track})
- require.NoError(t, err)
-
- err = w.WriteH26x(track, 0, 0, true, [][]byte{
- { // IDR
- 0x05, 1,
- },
- })
- require.NoError(t, err)
-
- err = bw.Flush()
- require.NoError(t, err)
-
- <-connected
-
- err = w.WriteH26x(track, 0, 0, true, [][]byte{{5, 2}})
- require.NoError(t, err)
-
- err = bw.Flush()
- require.NoError(t, err)
- }()
-
- desc, _, err := c.Describe(u)
- require.NoError(t, err)
-
- var forma *format.H264
- medi := desc.FindFormat(&forma)
-
- _, err = c.Setup(desc.BaseURL, medi, 0, 0)
- require.NoError(t, err)
-
- c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
- require.Equal(t, []byte{5, 1}, pkt.Payload)
- close(received)
- })
-
- _, err = c.Play(nil)
- require.NoError(t, err)
-
- close(connected)
- <-received
-}
diff --git a/internal/core/webrtc_http_server.go b/internal/core/webrtc_http_server.go
deleted file mode 100644
index e5689635b9f..00000000000
--- a/internal/core/webrtc_http_server.go
+++ /dev/null
@@ -1,309 +0,0 @@
-package core
-
-import (
- _ "embed"
- "fmt"
- "io"
- "net"
- "net/http"
- "strings"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "github.com/pion/webrtc/v3"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/httpserv"
- "github.com/bluenviron/mediamtx/internal/logger"
- "github.com/bluenviron/mediamtx/internal/whip"
-)
-
-//go:embed webrtc_publish_index.html
-var webrtcPublishIndex []byte
-
-//go:embed webrtc_read_index.html
-var webrtcReadIndex []byte
-
-type webRTCHTTPServerParent interface {
- logger.Writer
- generateICEServers() ([]webrtc.ICEServer, error)
- newSession(req webRTCNewSessionReq) webRTCNewSessionRes
- addSessionCandidates(req webRTCAddSessionCandidatesReq) webRTCAddSessionCandidatesRes
-}
-
-type webRTCHTTPServer struct {
- allowOrigin string
- pathManager *pathManager
- parent webRTCHTTPServerParent
-
- inner *httpserv.WrappedServer
-}
-
-func newWebRTCHTTPServer( //nolint:dupl
- address string,
- encryption bool,
- serverKey string,
- serverCert string,
- allowOrigin string,
- trustedProxies conf.IPsOrCIDRs,
- readTimeout conf.StringDuration,
- pathManager *pathManager,
- parent webRTCHTTPServerParent,
-) (*webRTCHTTPServer, error) {
- if encryption {
- if serverCert == "" {
- return nil, fmt.Errorf("server cert is missing")
- }
- } else {
- serverKey = ""
- serverCert = ""
- }
-
- s := &webRTCHTTPServer{
- allowOrigin: allowOrigin,
- pathManager: pathManager,
- parent: parent,
- }
-
- router := gin.New()
- router.SetTrustedProxies(trustedProxies.ToTrustedProxies()) //nolint:errcheck
- router.NoRoute(s.onRequest)
-
- network, address := restrictNetwork("tcp", address)
-
- var err error
- s.inner, err = httpserv.NewWrappedServer(
- network,
- address,
- time.Duration(readTimeout),
- serverCert,
- serverKey,
- router,
- s,
- )
- if err != nil {
- return nil, err
- }
-
- return s, nil
-}
-
-func (s *webRTCHTTPServer) Log(level logger.Level, format string, args ...interface{}) {
- s.parent.Log(level, format, args...)
-}
-
-func (s *webRTCHTTPServer) close() {
- s.inner.Close()
-}
-
-func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) {
- ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin)
- ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
-
- // remove leading prefix
- pa := ctx.Request.URL.Path[1:]
-
- isWHIPorWHEP := strings.HasSuffix(pa, "/whip") || strings.HasSuffix(pa, "/whep")
- isPreflight := ctx.Request.Method == http.MethodOptions &&
- ctx.Request.Header.Get("Access-Control-Request-Method") != ""
-
- if !isWHIPorWHEP || isPreflight {
- switch ctx.Request.Method {
- case http.MethodOptions:
- ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH")
- ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match")
- ctx.Writer.WriteHeader(http.StatusNoContent)
- return
-
- case http.MethodGet:
-
- default:
- return
- }
- }
-
- var dir string
- var fname string
- var publish bool
-
- switch {
- case pa == "", pa == "favicon.ico":
- return
-
- case strings.HasSuffix(pa, "/publish"):
- dir, fname = pa[:len(pa)-len("/publish")], "publish"
- publish = true
-
- case strings.HasSuffix(pa, "/whip"):
- dir, fname = pa[:len(pa)-len("/whip")], "whip"
- publish = true
-
- case strings.HasSuffix(pa, "/whep"):
- dir, fname = pa[:len(pa)-len("/whep")], "whep"
- publish = false
-
- default:
- dir, fname = pa, ""
- publish = false
-
- if !strings.HasSuffix(dir, "/") {
- l := "/" + dir + "/"
- if ctx.Request.URL.RawQuery != "" {
- l += "?" + ctx.Request.URL.RawQuery
- }
- ctx.Writer.Header().Set("Location", l)
- ctx.Writer.WriteHeader(http.StatusMovedPermanently)
- return
- }
- }
-
- dir = strings.TrimSuffix(dir, "/")
- if dir == "" {
- return
- }
-
- ip := ctx.ClientIP()
- _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
- remoteAddr := net.JoinHostPort(ip, port)
- user, pass, hasCredentials := ctx.Request.BasicAuth()
-
- // if request doesn't belong to a session, check authentication here
- if !isWHIPorWHEP || ctx.Request.Method == http.MethodOptions {
- res := s.pathManager.getConfForPath(pathGetConfForPathReq{
- name: dir,
- publish: publish,
- credentials: authCredentials{
- query: ctx.Request.URL.RawQuery,
- ip: net.ParseIP(ip),
- user: user,
- pass: pass,
- proto: authProtocolWebRTC,
- },
- })
- if res.err != nil {
- if terr, ok := res.err.(*errAuthentication); ok {
- if !hasCredentials {
- ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
- ctx.Writer.WriteHeader(http.StatusUnauthorized)
- return
- }
-
- s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.message)
-
- // wait some seconds to stop brute force attacks
- <-time.After(webrtcPauseAfterAuthError)
-
- ctx.Writer.WriteHeader(http.StatusUnauthorized)
- return
- }
-
- ctx.Writer.WriteHeader(http.StatusNotFound)
- return
- }
- }
-
- switch fname {
- case "":
- ctx.Writer.Header().Set("Cache-Control", "max-age=3600")
- ctx.Writer.Header().Set("Content-Type", "text/html")
- ctx.Writer.WriteHeader(http.StatusOK)
- ctx.Writer.Write(webrtcReadIndex)
-
- case "publish":
- ctx.Writer.Header().Set("Cache-Control", "max-age=3600")
- ctx.Writer.Header().Set("Content-Type", "text/html")
- ctx.Writer.WriteHeader(http.StatusOK)
- ctx.Writer.Write(webrtcPublishIndex)
-
- case "whip", "whep":
- switch ctx.Request.Method {
- case http.MethodOptions:
- servers, err := s.parent.generateICEServers()
- if err != nil {
- ctx.Writer.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH")
- ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match")
- ctx.Writer.Header()["Link"] = whip.LinkHeaderMarshal(servers)
- ctx.Writer.WriteHeader(http.StatusNoContent)
-
- case http.MethodPost:
- if ctx.Request.Header.Get("Content-Type") != "application/sdp" {
- ctx.Writer.WriteHeader(http.StatusBadRequest)
- return
- }
-
- offer, err := io.ReadAll(ctx.Request.Body)
- if err != nil {
- return
- }
-
- res := s.parent.newSession(webRTCNewSessionReq{
- pathName: dir,
- remoteAddr: remoteAddr,
- query: ctx.Request.URL.RawQuery,
- user: user,
- pass: pass,
- offer: offer,
- publish: (fname == "whip"),
- })
- if res.err != nil {
- ctx.Writer.WriteHeader(res.errStatusCode)
- return
- }
-
- servers, err := s.parent.generateICEServers()
- if err != nil {
- ctx.Writer.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- ctx.Writer.Header().Set("Content-Type", "application/sdp")
- ctx.Writer.Header().Set("Access-Control-Expose-Headers", "ETag, Accept-Patch, Link")
- ctx.Writer.Header().Set("ETag", res.sx.secret.String())
- ctx.Writer.Header().Set("ID", res.sx.uuid.String())
- ctx.Writer.Header().Set("Accept-Patch", "application/trickle-ice-sdpfrag")
- ctx.Writer.Header()["Link"] = whip.LinkHeaderMarshal(servers)
- ctx.Writer.Header().Set("Location", ctx.Request.URL.String())
- ctx.Writer.WriteHeader(http.StatusCreated)
- ctx.Writer.Write(res.answer)
-
- case http.MethodPatch:
- secret, err := uuid.Parse(ctx.Request.Header.Get("If-Match"))
- if err != nil {
- ctx.Writer.WriteHeader(http.StatusBadRequest)
- return
- }
-
- if ctx.Request.Header.Get("Content-Type") != "application/trickle-ice-sdpfrag" {
- ctx.Writer.WriteHeader(http.StatusBadRequest)
- return
- }
-
- byts, err := io.ReadAll(ctx.Request.Body)
- if err != nil {
- return
- }
-
- candidates, err := whip.ICEFragmentUnmarshal(byts)
- if err != nil {
- ctx.Writer.WriteHeader(http.StatusBadRequest)
- return
- }
-
- res := s.parent.addSessionCandidates(webRTCAddSessionCandidatesReq{
- secret: secret,
- candidates: candidates,
- })
- if res.err != nil {
- ctx.Writer.WriteHeader(http.StatusBadRequest)
- return
- }
-
- ctx.Writer.WriteHeader(http.StatusNoContent)
- }
- }
-}
diff --git a/internal/core/webrtc_manager.go b/internal/core/webrtc_manager.go
deleted file mode 100644
index 87f5da91822..00000000000
--- a/internal/core/webrtc_manager.go
+++ /dev/null
@@ -1,653 +0,0 @@
-package core
-
-import (
- "context"
- "crypto/hmac"
- "crypto/rand"
- "crypto/sha1"
- "encoding/base64"
- "fmt"
- "net"
- "net/http"
- "sort"
- "strconv"
- "sync"
- "time"
-
- "github.com/google/uuid"
- "github.com/pion/ice/v2"
- "github.com/pion/interceptor"
- "github.com/pion/webrtc/v3"
-
- "github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-const (
- webrtcPauseAfterAuthError = 2 * time.Second
- webrtcHandshakeTimeout = 10 * time.Second
- webrtcTrackGatherTimeout = 3 * time.Second
- webrtcPayloadMaxSize = 1188 // 1200 - 12 (RTP header)
- webrtcStreamID = "mediamtx"
- webrtcTurnSecretExpiration = 24 * 3600 * time.Second
-)
-
-var videoCodecs = []webrtc.RTPCodecParameters{
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeAV1,
- ClockRate: 90000,
- },
- PayloadType: 96,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeVP9,
- ClockRate: 90000,
- SDPFmtpLine: "profile-id=0",
- },
- PayloadType: 97,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeVP9,
- ClockRate: 90000,
- SDPFmtpLine: "profile-id=1",
- },
- PayloadType: 98,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeVP8,
- ClockRate: 90000,
- },
- PayloadType: 99,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeH264,
- ClockRate: 90000,
- SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
- },
- PayloadType: 100,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeH264,
- ClockRate: 90000,
- SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
- },
- PayloadType: 101,
- },
-}
-
-var audioCodecs = []webrtc.RTPCodecParameters{
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeOpus,
- ClockRate: 48000,
- Channels: 2,
- SDPFmtpLine: "minptime=10;useinbandfec=1",
- },
- PayloadType: 111,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeG722,
- ClockRate: 8000,
- },
- PayloadType: 9,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypePCMU,
- ClockRate: 8000,
- },
- PayloadType: 0,
- },
- {
- RTPCodecCapability: webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypePCMA,
- ClockRate: 8000,
- },
- PayloadType: 8,
- },
-}
-
-func randInt63() (int64, error) {
- var b [8]byte
- _, err := rand.Read(b[:])
- if err != nil {
- return 0, err
- }
-
- return int64(uint64(b[0]&0b01111111)<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 |
- uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7])), nil
-}
-
-// https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/math/rand/rand.go;l=119
-func randInt63n(n int64) (int64, error) {
- if n&(n-1) == 0 { // n is power of two, can mask
- r, err := randInt63()
- if err != nil {
- return 0, err
- }
- return r & (n - 1), nil
- }
-
- max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
-
- v, err := randInt63()
- if err != nil {
- return 0, err
- }
-
- for v > max {
- v, err = randInt63()
- if err != nil {
- return 0, err
- }
- }
-
- return v % n, nil
-}
-
-func randomTurnUser() (string, error) {
- const charset = "abcdefghijklmnopqrstuvwxyz1234567890"
- b := make([]byte, 20)
- for i := range b {
- j, err := randInt63n(int64(len(charset)))
- if err != nil {
- return "", err
- }
-
- b[i] = charset[int(j)]
- }
-
- return string(b), nil
-}
-
-func webrtcNewAPI(
- iceHostNAT1To1IPs []string,
- iceUDPMux ice.UDPMux,
- iceTCPMux ice.TCPMux,
-) (*webrtc.API, error) {
- settingsEngine := webrtc.SettingEngine{}
-
- if len(iceHostNAT1To1IPs) != 0 {
- settingsEngine.SetNAT1To1IPs(iceHostNAT1To1IPs, webrtc.ICECandidateTypeHost)
- }
-
- if iceUDPMux != nil {
- settingsEngine.SetICEUDPMux(iceUDPMux)
- }
-
- if iceTCPMux != nil {
- settingsEngine.SetICETCPMux(iceTCPMux)
- settingsEngine.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4})
- }
-
- mediaEngine := &webrtc.MediaEngine{}
-
- for _, codec := range videoCodecs {
- err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)
- if err != nil {
- return nil, err
- }
- }
-
- for _, codec := range audioCodecs {
- err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeAudio)
- if err != nil {
- return nil, err
- }
- }
-
- interceptorRegistry := &interceptor.Registry{}
- if err := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil {
- return nil, err
- }
-
- return webrtc.NewAPI(
- webrtc.WithSettingEngine(settingsEngine),
- webrtc.WithMediaEngine(mediaEngine),
- webrtc.WithInterceptorRegistry(interceptorRegistry)), nil
-}
-
-type webRTCManagerAPISessionsListRes struct {
- data *apiWebRTCSessionsList
- err error
-}
-
-type webRTCManagerAPISessionsListReq struct {
- res chan webRTCManagerAPISessionsListRes
-}
-
-type webRTCManagerAPISessionsGetRes struct {
- data *apiWebRTCSession
- err error
-}
-
-type webRTCManagerAPISessionsGetReq struct {
- uuid uuid.UUID
- res chan webRTCManagerAPISessionsGetRes
-}
-
-type webRTCManagerAPISessionsKickRes struct {
- err error
-}
-
-type webRTCManagerAPISessionsKickReq struct {
- uuid uuid.UUID
- res chan webRTCManagerAPISessionsKickRes
-}
-
-type webRTCNewSessionRes struct {
- sx *webRTCSession
- answer []byte
- err error
- errStatusCode int
-}
-
-type webRTCNewSessionReq struct {
- pathName string
- remoteAddr string
- query string
- user string
- pass string
- offer []byte
- publish bool
- res chan webRTCNewSessionRes
-}
-
-type webRTCAddSessionCandidatesRes struct {
- sx *webRTCSession
- err error
-}
-
-type webRTCAddSessionCandidatesReq struct {
- secret uuid.UUID
- candidates []*webrtc.ICECandidateInit
- res chan webRTCAddSessionCandidatesRes
-}
-
-type webRTCManagerParent interface {
- logger.Writer
-}
-
-type webRTCManager struct {
- allowOrigin string
- trustedProxies conf.IPsOrCIDRs
- iceServers []conf.WebRTCICEServer
- writeQueueSize int
- externalCmdPool *externalcmd.Pool
- pathManager *pathManager
- metrics *metrics
- parent webRTCManagerParent
-
- ctx context.Context
- ctxCancel func()
- httpServer *webRTCHTTPServer
- udpMuxLn net.PacketConn
- tcpMuxLn net.Listener
- api *webrtc.API
- sessions map[*webRTCSession]struct{}
- sessionsBySecret map[uuid.UUID]*webRTCSession
-
- // in
- chNewSession chan webRTCNewSessionReq
- chCloseSession chan *webRTCSession
- chAddSessionCandidates chan webRTCAddSessionCandidatesReq
- chAPISessionsList chan webRTCManagerAPISessionsListReq
- chAPISessionsGet chan webRTCManagerAPISessionsGetReq
- chAPIConnsKick chan webRTCManagerAPISessionsKickReq
-
- // out
- done chan struct{}
-}
-
-func newWebRTCManager(
- address string,
- encryption bool,
- serverKey string,
- serverCert string,
- allowOrigin string,
- trustedProxies conf.IPsOrCIDRs,
- iceServers []conf.WebRTCICEServer,
- readTimeout conf.StringDuration,
- writeQueueSize int,
- iceHostNAT1To1IPs []string,
- iceUDPMuxAddress string,
- iceTCPMuxAddress string,
- externalCmdPool *externalcmd.Pool,
- pathManager *pathManager,
- metrics *metrics,
- parent webRTCManagerParent,
-) (*webRTCManager, error) {
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- m := &webRTCManager{
- allowOrigin: allowOrigin,
- trustedProxies: trustedProxies,
- iceServers: iceServers,
- writeQueueSize: writeQueueSize,
- externalCmdPool: externalCmdPool,
- pathManager: pathManager,
- metrics: metrics,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- sessions: make(map[*webRTCSession]struct{}),
- sessionsBySecret: make(map[uuid.UUID]*webRTCSession),
- chNewSession: make(chan webRTCNewSessionReq),
- chCloseSession: make(chan *webRTCSession),
- chAddSessionCandidates: make(chan webRTCAddSessionCandidatesReq),
- chAPISessionsList: make(chan webRTCManagerAPISessionsListReq),
- chAPISessionsGet: make(chan webRTCManagerAPISessionsGetReq),
- chAPIConnsKick: make(chan webRTCManagerAPISessionsKickReq),
- done: make(chan struct{}),
- }
-
- var err error
- m.httpServer, err = newWebRTCHTTPServer(
- address,
- encryption,
- serverKey,
- serverCert,
- allowOrigin,
- trustedProxies,
- readTimeout,
- pathManager,
- m,
- )
- if err != nil {
- ctxCancel()
- return nil, err
- }
-
- var iceUDPMux ice.UDPMux
-
- if iceUDPMuxAddress != "" {
- m.udpMuxLn, err = net.ListenPacket(restrictNetwork("udp", iceUDPMuxAddress))
- if err != nil {
- m.httpServer.close()
- ctxCancel()
- return nil, err
- }
- iceUDPMux = webrtc.NewICEUDPMux(nil, m.udpMuxLn)
- }
-
- var iceTCPMux ice.TCPMux
-
- if iceTCPMuxAddress != "" {
- m.tcpMuxLn, err = net.Listen(restrictNetwork("tcp", iceTCPMuxAddress))
- if err != nil {
- m.udpMuxLn.Close()
- m.httpServer.close()
- ctxCancel()
- return nil, err
- }
- iceTCPMux = webrtc.NewICETCPMux(nil, m.tcpMuxLn, 8)
- }
-
- m.api, err = webrtcNewAPI(iceHostNAT1To1IPs, iceUDPMux, iceTCPMux)
- if err != nil {
- m.udpMuxLn.Close()
- m.tcpMuxLn.Close()
- m.httpServer.close()
- ctxCancel()
- return nil, err
- }
-
- str := "listener opened on " + address + " (HTTP)"
- if m.udpMuxLn != nil {
- str += ", " + iceUDPMuxAddress + " (ICE/UDP)"
- }
- if m.tcpMuxLn != nil {
- str += ", " + iceTCPMuxAddress + " (ICE/TCP)"
- }
- m.Log(logger.Info, str)
-
- if m.metrics != nil {
- m.metrics.webRTCManagerSet(m)
- }
-
- go m.run()
-
- return m, nil
-}
-
-// Log is the main logging function.
-func (m *webRTCManager) Log(level logger.Level, format string, args ...interface{}) {
- m.parent.Log(level, "[WebRTC] "+format, args...)
-}
-
-func (m *webRTCManager) close() {
- m.Log(logger.Info, "listener is closing")
- m.ctxCancel()
- <-m.done
-}
-
-func (m *webRTCManager) run() {
- defer close(m.done)
-
- var wg sync.WaitGroup
-
-outer:
- for {
- select {
- case req := <-m.chNewSession:
- sx := newWebRTCSession(
- m.ctx,
- m.writeQueueSize,
- m.api,
- req,
- &wg,
- m.externalCmdPool,
- m.pathManager,
- m,
- )
- m.sessions[sx] = struct{}{}
- m.sessionsBySecret[sx.secret] = sx
- req.res <- webRTCNewSessionRes{sx: sx}
-
- case sx := <-m.chCloseSession:
- delete(m.sessions, sx)
- delete(m.sessionsBySecret, sx.secret)
-
- case req := <-m.chAddSessionCandidates:
- sx, ok := m.sessionsBySecret[req.secret]
- if !ok {
- req.res <- webRTCAddSessionCandidatesRes{err: fmt.Errorf("session not found")}
- continue
- }
-
- req.res <- webRTCAddSessionCandidatesRes{sx: sx}
-
- case req := <-m.chAPISessionsList:
- data := &apiWebRTCSessionsList{
- Items: []*apiWebRTCSession{},
- }
-
- for sx := range m.sessions {
- data.Items = append(data.Items, sx.apiItem())
- }
-
- sort.Slice(data.Items, func(i, j int) bool {
- return data.Items[i].Created.Before(data.Items[j].Created)
- })
-
- req.res <- webRTCManagerAPISessionsListRes{data: data}
-
- case req := <-m.chAPISessionsGet:
- sx := m.findSessionByUUID(req.uuid)
- if sx == nil {
- req.res <- webRTCManagerAPISessionsGetRes{err: errAPINotFound}
- continue
- }
-
- req.res <- webRTCManagerAPISessionsGetRes{data: sx.apiItem()}
-
- case req := <-m.chAPIConnsKick:
- sx := m.findSessionByUUID(req.uuid)
- if sx == nil {
- req.res <- webRTCManagerAPISessionsKickRes{err: errAPINotFound}
- continue
- }
-
- delete(m.sessions, sx)
- delete(m.sessionsBySecret, sx.secret)
- sx.close()
- req.res <- webRTCManagerAPISessionsKickRes{}
-
- case <-m.ctx.Done():
- break outer
- }
- }
-
- m.ctxCancel()
-
- wg.Wait()
-
- m.httpServer.close()
-
- if m.udpMuxLn != nil {
- m.udpMuxLn.Close()
- }
-
- if m.tcpMuxLn != nil {
- m.tcpMuxLn.Close()
- }
-}
-
-func (m *webRTCManager) findSessionByUUID(uuid uuid.UUID) *webRTCSession {
- for sx := range m.sessions {
- if sx.uuid == uuid {
- return sx
- }
- }
- return nil
-}
-
-func (m *webRTCManager) generateICEServers() ([]webrtc.ICEServer, error) {
- ret := make([]webrtc.ICEServer, len(m.iceServers))
-
- for i, server := range m.iceServers {
- if server.Username == "AUTH_SECRET" {
- expireDate := time.Now().Add(webrtcTurnSecretExpiration).Unix()
-
- user, err := randomTurnUser()
- if err != nil {
- return nil, err
- }
-
- server.Username = strconv.FormatInt(expireDate, 10) + ":" + user
-
- h := hmac.New(sha1.New, []byte(server.Password))
- h.Write([]byte(server.Username))
-
- server.Password = base64.StdEncoding.EncodeToString(h.Sum(nil))
- }
-
- ret[i] = webrtc.ICEServer{
- URLs: []string{server.URL},
- Username: server.Username,
- Credential: server.Password,
- }
- }
-
- return ret, nil
-}
-
-// newSession is called by webRTCHTTPServer.
-func (m *webRTCManager) newSession(req webRTCNewSessionReq) webRTCNewSessionRes {
- req.res = make(chan webRTCNewSessionRes)
-
- select {
- case m.chNewSession <- req:
- res := <-req.res
-
- return res.sx.new(req)
-
- case <-m.ctx.Done():
- return webRTCNewSessionRes{err: fmt.Errorf("terminated"), errStatusCode: http.StatusInternalServerError}
- }
-}
-
-// closeSession is called by webRTCSession.
-func (m *webRTCManager) closeSession(sx *webRTCSession) {
- select {
- case m.chCloseSession <- sx:
- case <-m.ctx.Done():
- }
-}
-
-// addSessionCandidates is called by webRTCHTTPServer.
-func (m *webRTCManager) addSessionCandidates(
- req webRTCAddSessionCandidatesReq,
-) webRTCAddSessionCandidatesRes {
- req.res = make(chan webRTCAddSessionCandidatesRes)
- select {
- case m.chAddSessionCandidates <- req:
- res1 := <-req.res
- if res1.err != nil {
- return res1
- }
-
- return res1.sx.addCandidates(req)
-
- case <-m.ctx.Done():
- return webRTCAddSessionCandidatesRes{err: fmt.Errorf("terminated")}
- }
-}
-
-// apiSessionsList is called by api.
-func (m *webRTCManager) apiSessionsList() (*apiWebRTCSessionsList, error) {
- req := webRTCManagerAPISessionsListReq{
- res: make(chan webRTCManagerAPISessionsListRes),
- }
-
- select {
- case m.chAPISessionsList <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-m.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-// apiSessionsGet is called by api.
-func (m *webRTCManager) apiSessionsGet(uuid uuid.UUID) (*apiWebRTCSession, error) {
- req := webRTCManagerAPISessionsGetReq{
- uuid: uuid,
- res: make(chan webRTCManagerAPISessionsGetRes),
- }
-
- select {
- case m.chAPISessionsGet <- req:
- res := <-req.res
- return res.data, res.err
-
- case <-m.ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
-}
-
-// apiSessionsKick is called by api.
-func (m *webRTCManager) apiSessionsKick(uuid uuid.UUID) error {
- req := webRTCManagerAPISessionsKickReq{
- uuid: uuid,
- res: make(chan webRTCManagerAPISessionsKickRes),
- }
-
- select {
- case m.chAPIConnsKick <- req:
- res := <-req.res
- return res.err
-
- case <-m.ctx.Done():
- return fmt.Errorf("terminated")
- }
-}
diff --git a/internal/core/webrtc_outgoing_track.go b/internal/core/webrtc_outgoing_track.go
deleted file mode 100644
index 5338bef8d43..00000000000
--- a/internal/core/webrtc_outgoing_track.go
+++ /dev/null
@@ -1,364 +0,0 @@
-package core
-
-import (
- "fmt"
- "time"
-
- "github.com/bluenviron/gortsplib/v4/pkg/description"
- "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/rtpvp8"
- "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9"
- "github.com/pion/webrtc/v3"
-
- "github.com/bluenviron/mediamtx/internal/asyncwriter"
- "github.com/bluenviron/mediamtx/internal/stream"
- "github.com/bluenviron/mediamtx/internal/unit"
-)
-
-type webRTCOutgoingTrack struct {
- sender *webrtc.RTPSender
- media *description.Media
- format format.Format
- track *webrtc.TrackLocalStaticRTP
- cb func(unit.Unit) error
-}
-
-func newWebRTCOutgoingTrackVideo(desc *description.Session) (*webRTCOutgoingTrack, error) {
- var av1Format *format.AV1
- videoMedia := desc.FindFormat(&av1Format)
-
- if videoMedia != nil {
- webRTCTrak, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeAV1,
- ClockRate: 90000,
- },
- "av1",
- webrtcStreamID,
- )
- if err != nil {
- return nil, err
- }
-
- encoder := &rtpav1.Encoder{
- PayloadType: 105,
- PayloadMaxSize: webrtcPayloadMaxSize,
- }
- err = encoder.Init()
- if err != nil {
- return nil, err
- }
-
- return &webRTCOutgoingTrack{
- media: videoMedia,
- format: av1Format,
- track: webRTCTrak,
- cb: func(u unit.Unit) error {
- tunit := u.(*unit.AV1)
-
- if tunit.TU == nil {
- return nil
- }
-
- packets, err := encoder.Encode(tunit.TU)
- if err != nil {
- return nil //nolint:nilerr
- }
-
- for _, pkt := range packets {
- pkt.Timestamp += tunit.RTPPackets[0].Timestamp
- webRTCTrak.WriteRTP(pkt) //nolint:errcheck
- }
-
- return nil
- },
- }, nil
- }
-
- var vp9Format *format.VP9
- videoMedia = desc.FindFormat(&vp9Format)
-
- if videoMedia != nil { //nolint:dupl
- webRTCTrak, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeVP9,
- ClockRate: uint32(vp9Format.ClockRate()),
- },
- "vp9",
- webrtcStreamID,
- )
- if err != nil {
- return nil, err
- }
-
- encoder := &rtpvp9.Encoder{
- PayloadType: 96,
- PayloadMaxSize: webrtcPayloadMaxSize,
- }
- err = encoder.Init()
- if err != nil {
- return nil, err
- }
-
- return &webRTCOutgoingTrack{
- media: videoMedia,
- format: vp9Format,
- track: webRTCTrak,
- cb: func(u unit.Unit) error {
- tunit := u.(*unit.VP9)
-
- if tunit.Frame == nil {
- return nil
- }
-
- packets, err := encoder.Encode(tunit.Frame)
- if err != nil {
- return nil //nolint:nilerr
- }
-
- for _, pkt := range packets {
- pkt.Timestamp += tunit.RTPPackets[0].Timestamp
- webRTCTrak.WriteRTP(pkt) //nolint:errcheck
- }
-
- return nil
- },
- }, nil
- }
-
- var vp8Format *format.VP8
- videoMedia = desc.FindFormat(&vp8Format)
-
- if videoMedia != nil { //nolint:dupl
- webRTCTrak, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeVP8,
- ClockRate: uint32(vp8Format.ClockRate()),
- },
- "vp8",
- webrtcStreamID,
- )
- if err != nil {
- return nil, err
- }
-
- encoder := &rtpvp8.Encoder{
- PayloadType: 96,
- PayloadMaxSize: webrtcPayloadMaxSize,
- }
- err = encoder.Init()
- if err != nil {
- return nil, err
- }
-
- return &webRTCOutgoingTrack{
- media: videoMedia,
- format: vp8Format,
- track: webRTCTrak,
- cb: func(u unit.Unit) error {
- tunit := u.(*unit.VP8)
-
- if tunit.Frame == nil {
- return nil
- }
-
- packets, err := encoder.Encode(tunit.Frame)
- if err != nil {
- return nil //nolint:nilerr
- }
-
- for _, pkt := range packets {
- pkt.Timestamp += tunit.RTPPackets[0].Timestamp
- webRTCTrak.WriteRTP(pkt) //nolint:errcheck
- }
-
- return nil
- },
- }, nil
- }
-
- var h264Format *format.H264
- videoMedia = desc.FindFormat(&h264Format)
-
- if videoMedia != nil {
- webRTCTrak, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeH264,
- ClockRate: uint32(h264Format.ClockRate()),
- },
- "h264",
- webrtcStreamID,
- )
- if err != nil {
- return nil, err
- }
-
- encoder := &rtph264.Encoder{
- PayloadType: 96,
- PayloadMaxSize: webrtcPayloadMaxSize,
- }
- err = encoder.Init()
- if err != nil {
- return nil, err
- }
-
- firstReceived := false
- var lastPTS time.Duration
-
- return &webRTCOutgoingTrack{
- media: videoMedia,
- format: h264Format,
- track: webRTCTrak,
- cb: func(u unit.Unit) error {
- tunit := u.(*unit.H264)
-
- if tunit.AU == nil {
- return nil
- }
-
- if !firstReceived {
- firstReceived = true
- } else if tunit.PTS < lastPTS {
- return fmt.Errorf("WebRTC doesn't support H264 streams with B-frames")
- }
- lastPTS = tunit.PTS
-
- packets, err := encoder.Encode(tunit.AU)
- if err != nil {
- return nil //nolint:nilerr
- }
-
- for _, pkt := range packets {
- pkt.Timestamp += tunit.RTPPackets[0].Timestamp
- webRTCTrak.WriteRTP(pkt) //nolint:errcheck
- }
-
- return nil
- },
- }, nil
- }
-
- return nil, nil
-}
-
-func newWebRTCOutgoingTrackAudio(desc *description.Session) (*webRTCOutgoingTrack, error) {
- var opusFormat *format.Opus
- audioMedia := desc.FindFormat(&opusFormat)
-
- if audioMedia != nil {
- webRTCTrak, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeOpus,
- ClockRate: uint32(opusFormat.ClockRate()),
- Channels: 2,
- },
- "opus",
- webrtcStreamID,
- )
- if err != nil {
- return nil, err
- }
-
- return &webRTCOutgoingTrack{
- media: audioMedia,
- format: opusFormat,
- track: webRTCTrak,
- cb: func(u unit.Unit) error {
- for _, pkt := range u.GetRTPPackets() {
- webRTCTrak.WriteRTP(pkt) //nolint:errcheck
- }
-
- return nil
- },
- }, nil
- }
-
- var g722Format *format.G722
- audioMedia = desc.FindFormat(&g722Format)
-
- if audioMedia != nil {
- webRTCTrak, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeG722,
- ClockRate: uint32(g722Format.ClockRate()),
- },
- "g722",
- webrtcStreamID,
- )
- if err != nil {
- return nil, err
- }
-
- return &webRTCOutgoingTrack{
- media: audioMedia,
- format: g722Format,
- track: webRTCTrak,
- cb: func(u unit.Unit) error {
- for _, pkt := range u.GetRTPPackets() {
- webRTCTrak.WriteRTP(pkt) //nolint:errcheck
- }
-
- return nil
- },
- }, nil
- }
-
- var g711Format *format.G711
- audioMedia = desc.FindFormat(&g711Format)
-
- if audioMedia != nil {
- var mtyp string
- if g711Format.MULaw {
- mtyp = webrtc.MimeTypePCMU
- } else {
- mtyp = webrtc.MimeTypePCMA
- }
-
- webRTCTrak, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: mtyp,
- ClockRate: uint32(g711Format.ClockRate()),
- },
- "g711",
- webrtcStreamID,
- )
- if err != nil {
- return nil, err
- }
-
- return &webRTCOutgoingTrack{
- media: audioMedia,
- format: g711Format,
- track: webRTCTrak,
- cb: func(u unit.Unit) error {
- for _, pkt := range u.GetRTPPackets() {
- webRTCTrak.WriteRTP(pkt) //nolint:errcheck
- }
-
- return nil
- },
- }, nil
- }
-
- return nil, nil
-}
-
-func (t *webRTCOutgoingTrack) start(
- stream *stream.Stream,
- writer *asyncwriter.Writer,
-) {
- // read incoming RTCP packets to make interceptors work
- go func() {
- buf := make([]byte, 1500)
- for {
- _, _, err := t.sender.Read(buf)
- if err != nil {
- return
- }
- }
- }()
-
- stream.AddReader(writer, t.media, t.format, t.cb)
-}
diff --git a/internal/core/webrtc_manager_test.go b/internal/core/webrtc_server_test.go
similarity index 56%
rename from internal/core/webrtc_manager_test.go
rename to internal/core/webrtc_server_test.go
index ab2c69a51ff..12c73794332 100644
--- a/internal/core/webrtc_manager_test.go
+++ b/internal/core/webrtc_server_test.go
@@ -4,158 +4,41 @@ import (
"bytes"
"context"
"net/http"
+ "net/url"
"testing"
"time"
"github.com/bluenviron/gortsplib/v4"
+ "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
"github.com/pion/rtp"
- "github.com/pion/webrtc/v3"
+ pwebrtc "github.com/pion/webrtc/v3"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/logger"
- "github.com/bluenviron/mediamtx/internal/webrtcpc"
- "github.com/bluenviron/mediamtx/internal/whip"
+ "github.com/bluenviron/mediamtx/internal/protocols/webrtc"
)
-type nilLogger struct{}
-
-func (nilLogger) Log(_ logger.Level, _ string, _ ...interface{}) {
-}
-
-type webRTCTestClient struct {
- pc *webrtcpc.PeerConnection
- outgoingTrack1 *webrtc.TrackLocalStaticRTP
- outgoingTrack2 *webrtc.TrackLocalStaticRTP
- incomingTrack chan *webrtc.TrackRemote
-}
-
-func newWebRTCTestClient(
- t *testing.T,
- hc *http.Client,
- ur string,
- publish bool,
-) *webRTCTestClient {
- iceServers, err := whip.GetICEServers(context.Background(), hc, ur)
- require.NoError(t, err)
-
- c := &webRTCTestClient{}
-
- api, err := webrtcNewAPI(nil, nil, nil)
- require.NoError(t, err)
-
- pc, err := webrtcpc.New(iceServers, api, nilLogger{})
- require.NoError(t, err)
-
- var outgoingTrack1 *webrtc.TrackLocalStaticRTP
- var outgoingTrack2 *webrtc.TrackLocalStaticRTP
- var incomingTrack chan *webrtc.TrackRemote
-
- if publish {
- var err error
- outgoingTrack1, err = webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeVP8,
- ClockRate: 90000,
- },
- "vp8",
- webrtcStreamID,
- )
- require.NoError(t, err)
-
- _, err = pc.AddTrack(outgoingTrack1)
- require.NoError(t, err)
-
- outgoingTrack2, err = webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeOpus,
- ClockRate: 48000,
- Channels: 2,
- },
- "opus",
- webrtcStreamID,
- )
- require.NoError(t, err)
-
- _, err = pc.AddTrack(outgoingTrack2)
- require.NoError(t, err)
- } else {
- incomingTrack = make(chan *webrtc.TrackRemote, 1)
- pc.OnTrack(func(trak *webrtc.TrackRemote, recv *webrtc.RTPReceiver) {
- incomingTrack <- trak
- })
-
- _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
- require.NoError(t, err)
- }
-
- offer, err := pc.CreateOffer(nil)
- require.NoError(t, err)
-
- res, err := whip.PostOffer(context.Background(), hc, ur, &offer)
- require.NoError(t, err)
+func TestWebRTCPages(t *testing.T) {
+ p, ok := newInstance("paths:\n" +
+ " all:\n")
+ require.Equal(t, true, ok)
+ defer p.Close()
- err = pc.SetLocalDescription(offer)
- require.NoError(t, err)
+ hc := &http.Client{Transport: &http.Transport{}}
- // test adding additional candidates, even if it is not mandatory here
-outer:
- for {
- select {
- case c := <-pc.NewLocalCandidate():
- err := whip.PostCandidate(context.Background(), hc, ur, &offer, res.ETag, c)
+ for _, path := range []string{"/stream", "/stream/publish", "/publish"} {
+ func() {
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:8889"+path, nil)
require.NoError(t, err)
- case <-pc.GatheringDone():
- break outer
- }
- }
- err = pc.SetRemoteDescription(*res.Answer)
- require.NoError(t, err)
-
- <-pc.Connected()
-
- if publish {
- err := outgoingTrack1.WriteRTP(&rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: 123,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{1},
- })
- require.NoError(t, err)
-
- err = outgoingTrack2.WriteRTP(&rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: 1123,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{2},
- })
- require.NoError(t, err)
+ res, err := hc.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
- time.Sleep(200 * time.Millisecond)
+ require.Equal(t, http.StatusOK, res.StatusCode)
+ }()
}
-
- c.pc = pc
- c.outgoingTrack1 = outgoingTrack1
- c.outgoingTrack2 = outgoingTrack2
- c.incomingTrack = incomingTrack
- return c
-}
-
-func (c *webRTCTestClient) close() {
- c.pc.Close()
}
func TestWebRTCRead(t *testing.T) {
@@ -170,18 +53,18 @@ func TestWebRTCRead(t *testing.T) {
switch auth {
case "none":
conf = "paths:\n" +
- " all:\n"
+ " all_others:\n"
case "internal":
conf = "paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" readUser: myuser\n" +
" readPass: mypass\n"
case "external":
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
- " all:\n"
+ " all_others:\n"
}
p, ok := newInstance(conf)
@@ -238,27 +121,36 @@ func TestWebRTCRead(t *testing.T) {
}
ur += "localhost:8889/teststream/whep?param=value"
- c := newWebRTCTestClient(t, hc, ur, false)
- defer c.close()
-
- time.Sleep(500 * time.Millisecond)
+ go func() {
+ time.Sleep(500 * time.Millisecond)
+
+ err := source.WritePacketRTP(medi, &rtp.Packet{
+ Header: rtp.Header{
+ Version: 2,
+ Marker: true,
+ PayloadType: 96,
+ SequenceNumber: 123,
+ Timestamp: 45343,
+ SSRC: 563423,
+ },
+ Payload: []byte{5, 3},
+ })
+ require.NoError(t, err)
+ }()
- err = source.WritePacketRTP(medi, &rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: 123,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{5, 3},
- })
+ u, err := url.Parse(ur)
require.NoError(t, err)
- trak := <-c.incomingTrack
+ c := &webrtc.WHIPClient{
+ HTTPClient: hc,
+ URL: u,
+ }
+
+ tracks, err := c.Read(context.Background())
+ require.NoError(t, err)
+ defer checkClose(t, c.Close)
- pkt, _, err := trak.ReadRTP()
+ pkt, err := tracks[0].ReadRTP()
require.NoError(t, err)
require.Equal(t, &rtp.Packet{
Header: rtp.Header{
@@ -278,28 +170,28 @@ func TestWebRTCRead(t *testing.T) {
func TestWebRTCReadNotFound(t *testing.T) {
p, ok := newInstance("paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
hc := &http.Client{Transport: &http.Transport{}}
- iceServers, err := whip.GetICEServers(context.Background(), hc, "http://localhost:8889/stream/whep")
+ iceServers, err := webrtc.WHIPOptionsICEServers(context.Background(), hc, "http://localhost:8889/stream/whep")
require.NoError(t, err)
- pc, err := webrtc.NewPeerConnection(webrtc.Configuration{
+ pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{
ICEServers: iceServers,
})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck
- _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
+ _, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
- req, err := http.NewRequest("POST", "http://localhost:8889/stream/whep", bytes.NewReader([]byte(offer.SDP)))
+ req, err := http.NewRequest(http.MethodPost, "http://localhost:8889/stream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/sdp")
@@ -323,18 +215,18 @@ func TestWebRTCPublish(t *testing.T) {
switch auth {
case "none":
conf = "paths:\n" +
- " all:\n"
+ " all_others:\n"
case "internal":
conf = "paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" publishUser: myuser\n" +
" publishPass: mypass\n"
case "external":
conf = "externalAuthenticationURL: http://localhost:9120/auth\n" +
"paths:\n" +
- " all:\n"
+ " all_others:\n"
}
p, ok := newInstance(conf)
@@ -350,7 +242,7 @@ func TestWebRTCPublish(t *testing.T) {
// preflight requests must always work, without authentication
func() {
- req, err := http.NewRequest("OPTIONS", "http://localhost:8889/teststream/whip", nil)
+ req, err := http.NewRequest(http.MethodOptions, "http://localhost:8889/teststream/whip", nil)
require.NoError(t, err)
req.Header.Set("Access-Control-Request-Method", "OPTIONS")
@@ -386,8 +278,32 @@ func TestWebRTCPublish(t *testing.T) {
}
ur += "localhost:8889/teststream/whip?param=value"
- s := newWebRTCTestClient(t, hc, ur, true)
- defer s.close()
+ su, err := url.Parse(ur)
+ require.NoError(t, err)
+
+ s := &webrtc.WHIPClient{
+ HTTPClient: hc,
+ URL: su,
+ }
+
+ tracks, err := s.Publish(context.Background(), testMediaH264.Formats[0], nil)
+ require.NoError(t, err)
+ defer checkClose(t, s.Close)
+
+ err = tracks[0].WriteRTP(&rtp.Packet{
+ Header: rtp.Header{
+ Version: 2,
+ Marker: true,
+ PayloadType: 96,
+ SequenceNumber: 123,
+ Timestamp: 45343,
+ SSRC: 563423,
+ },
+ Payload: []byte{1},
+ })
+ require.NoError(t, err)
+
+ time.Sleep(200 * time.Millisecond)
if auth == "external" {
a.close()
@@ -401,7 +317,7 @@ func TestWebRTCPublish(t *testing.T) {
},
}
- u, err := url.Parse("rtsp://testreader:testpass@127.0.0.1:8554/teststream?param=value")
+ u, err := base.ParseURL("rtsp://testreader:testpass@127.0.0.1:8554/teststream?param=value")
require.NoError(t, err)
err = c.Start(u.Scheme, u.Host)
@@ -411,7 +327,7 @@ func TestWebRTCPublish(t *testing.T) {
desc, _, err := c.Describe(u)
require.NoError(t, err)
- var forma *format.VP8
+ var forma *format.H264
medi := desc.FindFormat(&forma)
_, err = c.Setup(desc.BaseURL, medi, 0, 0)
@@ -427,7 +343,7 @@ func TestWebRTCPublish(t *testing.T) {
_, err = c.Play(nil)
require.NoError(t, err)
- err = s.outgoingTrack1.WriteRTP(&rtp.Packet{
+ err = tracks[0].WriteRTP(&rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
diff --git a/internal/core/webrtc_session.go b/internal/core/webrtc_session.go
deleted file mode 100644
index 493a9921bb2..00000000000
--- a/internal/core/webrtc_session.go
+++ /dev/null
@@ -1,683 +0,0 @@
-package core
-
-import (
- "context"
- "encoding/hex"
- "fmt"
- "net"
- "net/http"
- "strings"
- "sync"
- "time"
-
- "github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/rtptime"
- "github.com/google/uuid"
- "github.com/pion/sdp/v3"
- "github.com/pion/webrtc/v3"
-
- "github.com/bluenviron/mediamtx/internal/asyncwriter"
- "github.com/bluenviron/mediamtx/internal/externalcmd"
- "github.com/bluenviron/mediamtx/internal/logger"
- "github.com/bluenviron/mediamtx/internal/webrtcpc"
-)
-
-type trackRecvPair struct {
- track *webrtc.TrackRemote
- receiver *webrtc.RTPReceiver
-}
-
-func webrtcMediasOfOutgoingTracks(tracks []*webRTCOutgoingTrack) []*description.Media {
- ret := make([]*description.Media, len(tracks))
- for i, track := range tracks {
- ret[i] = track.media
- }
- return ret
-}
-
-func webrtcMediasOfIncomingTracks(tracks []*webRTCIncomingTrack) []*description.Media {
- ret := make([]*description.Media, len(tracks))
- for i, track := range tracks {
- ret[i] = track.media
- }
- return ret
-}
-
-func whipOffer(body []byte) *webrtc.SessionDescription {
- return &webrtc.SessionDescription{
- Type: webrtc.SDPTypeOffer,
- SDP: string(body),
- }
-}
-
-func webrtcWaitUntilConnected(
- ctx context.Context,
- pc *webrtcpc.PeerConnection,
-) error {
- t := time.NewTimer(webrtcHandshakeTimeout)
- defer t.Stop()
-
-outer:
- for {
- select {
- case <-t.C:
- return fmt.Errorf("deadline exceeded while waiting connection")
-
- case <-pc.Connected():
- break outer
-
- case <-ctx.Done():
- return fmt.Errorf("terminated")
- }
- }
-
- return nil
-}
-
-func webrtcGatherOutgoingTracks(desc *description.Session) ([]*webRTCOutgoingTrack, error) {
- var tracks []*webRTCOutgoingTrack
-
- videoTrack, err := newWebRTCOutgoingTrackVideo(desc)
- if err != nil {
- return nil, err
- }
-
- if videoTrack != nil {
- tracks = append(tracks, videoTrack)
- }
-
- audioTrack, err := newWebRTCOutgoingTrackAudio(desc)
- if err != nil {
- return nil, err
- }
-
- if audioTrack != nil {
- tracks = append(tracks, audioTrack)
- }
-
- if tracks == nil {
- return nil, fmt.Errorf(
- "the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711")
- }
-
- return tracks, nil
-}
-
-func webrtcTrackCount(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
-}
-
-func webrtcGatherIncomingTracks(
- ctx context.Context,
- pc *webrtcpc.PeerConnection,
- trackRecv chan trackRecvPair,
- trackCount int,
-) ([]*webRTCIncomingTrack, error) {
- var tracks []*webRTCIncomingTrack
-
- t := time.NewTimer(webrtcTrackGatherTimeout)
- defer t.Stop()
-
- for {
- select {
- case <-t.C:
- if trackCount == 0 {
- return tracks, nil
- }
- return nil, fmt.Errorf("deadline exceeded while waiting tracks")
-
- case pair := <-trackRecv:
- track, err := newWebRTCIncomingTrack(pair.track, pair.receiver, pc.WriteRTCP)
- if err != nil {
- return nil, err
- }
- tracks = append(tracks, track)
-
- if len(tracks) == trackCount {
- return tracks, nil
- }
-
- case <-pc.Disconnected():
- return nil, fmt.Errorf("peer connection closed")
-
- case <-ctx.Done():
- return nil, fmt.Errorf("terminated")
- }
- }
-}
-
-type webRTCSessionPathManager interface {
- addPublisher(req pathAddPublisherReq) pathAddPublisherRes
- addReader(req pathAddReaderReq) pathAddReaderRes
-}
-
-type webRTCSession struct {
- writeQueueSize int
- api *webrtc.API
- req webRTCNewSessionReq
- wg *sync.WaitGroup
- externalCmdPool *externalcmd.Pool
- pathManager webRTCSessionPathManager
- parent *webRTCManager
-
- ctx context.Context
- ctxCancel func()
- created time.Time
- uuid uuid.UUID
- secret uuid.UUID
- mutex sync.RWMutex
- pc *webrtcpc.PeerConnection
-
- chNew chan webRTCNewSessionReq
- chAddCandidates chan webRTCAddSessionCandidatesReq
-}
-
-func newWebRTCSession(
- parentCtx context.Context,
- writeQueueSize int,
- api *webrtc.API,
- req webRTCNewSessionReq,
- wg *sync.WaitGroup,
- externalCmdPool *externalcmd.Pool,
- pathManager webRTCSessionPathManager,
- parent *webRTCManager,
-) *webRTCSession {
- ctx, ctxCancel := context.WithCancel(parentCtx)
-
- s := &webRTCSession{
- writeQueueSize: writeQueueSize,
- api: api,
- req: req,
- wg: wg,
- externalCmdPool: externalCmdPool,
- pathManager: pathManager,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- created: time.Now(),
- uuid: uuid.New(),
- secret: uuid.New(),
- chNew: make(chan webRTCNewSessionReq),
- chAddCandidates: make(chan webRTCAddSessionCandidatesReq),
- }
-
- s.Log(logger.Info, "created by %s", req.remoteAddr)
-
- wg.Add(1)
- go s.run()
-
- return s
-}
-
-func (s *webRTCSession) Log(level logger.Level, format string, args ...interface{}) {
- id := hex.EncodeToString(s.uuid[:4])
- s.parent.Log(level, "[session %v] "+format, append([]interface{}{id}, args...)...)
-}
-
-func (s *webRTCSession) close() {
- s.ctxCancel()
-}
-
-func (s *webRTCSession) run() {
- defer s.wg.Done()
-
- err := s.runInner()
-
- s.ctxCancel()
-
- s.parent.closeSession(s)
-
- s.Log(logger.Info, "closed: %v", err)
-}
-
-func (s *webRTCSession) runInner() error {
- select {
- case <-s.chNew:
- case <-s.ctx.Done():
- return fmt.Errorf("terminated")
- }
-
- errStatusCode, err := s.runInner2()
-
- if errStatusCode != 0 {
- s.req.res <- webRTCNewSessionRes{
- err: err,
- errStatusCode: errStatusCode,
- }
- }
-
- return err
-}
-
-func (s *webRTCSession) runInner2() (int, error) {
- if s.req.publish {
- return s.runPublish()
- }
- return s.runRead()
-}
-
-func (s *webRTCSession) runPublish() (int, error) {
- ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
-
- res := s.pathManager.addPublisher(pathAddPublisherReq{
- author: s,
- pathName: s.req.pathName,
- credentials: authCredentials{
- query: s.req.query,
- ip: net.ParseIP(ip),
- user: s.req.user,
- pass: s.req.pass,
- proto: authProtocolWebRTC,
- id: &s.uuid,
- },
- })
- if res.err != nil {
- if _, ok := res.err.(*errAuthentication); ok {
- // wait some seconds to stop brute force attacks
- <-time.After(webrtcPauseAfterAuthError)
-
- return http.StatusUnauthorized, res.err
- }
-
- return http.StatusBadRequest, res.err
- }
-
- defer res.path.removePublisher(pathRemovePublisherReq{author: s})
-
- servers, err := s.parent.generateICEServers()
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- pc, err := webrtcpc.New(
- servers,
- s.api,
- s)
- if err != nil {
- return http.StatusBadRequest, err
- }
- defer pc.Close()
-
- offer := whipOffer(s.req.offer)
-
- var sdp sdp.SessionDescription
- err = sdp.Unmarshal([]byte(offer.SDP))
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- trackCount, err := webrtcTrackCount(sdp.MediaDescriptions)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RtpTransceiverInit{
- Direction: webrtc.RTPTransceiverDirectionRecvonly,
- })
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RtpTransceiverInit{
- Direction: webrtc.RTPTransceiverDirectionRecvonly,
- })
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- trackRecv := make(chan trackRecvPair)
-
- pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
- select {
- case trackRecv <- trackRecvPair{track, receiver}:
- case <-s.ctx.Done():
- }
- })
-
- err = pc.SetRemoteDescription(*offer)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- answer, err := pc.CreateAnswer(nil)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- err = pc.SetLocalDescription(answer)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- err = pc.WaitGatheringDone(s.ctx)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- s.writeAnswer(pc.LocalDescription())
-
- go s.readRemoteCandidates(pc)
-
- err = webrtcWaitUntilConnected(s.ctx, pc)
- if err != nil {
- return 0, err
- }
-
- s.mutex.Lock()
- s.pc = pc
- s.mutex.Unlock()
-
- tracks, err := webrtcGatherIncomingTracks(s.ctx, pc, trackRecv, trackCount)
- if err != nil {
- return 0, err
- }
- medias := webrtcMediasOfIncomingTracks(tracks)
-
- rres := res.path.startPublisher(pathStartPublisherReq{
- author: s,
- desc: &description.Session{Medias: medias},
- generateRTPPackets: false,
- })
- if rres.err != nil {
- return 0, rres.err
- }
-
- timeDecoder := rtptime.NewGlobalDecoder()
-
- for _, track := range tracks {
- track.start(rres.stream, timeDecoder)
- }
-
- select {
- case <-pc.Disconnected():
- return 0, fmt.Errorf("peer connection closed")
-
- case <-s.ctx.Done():
- return 0, fmt.Errorf("terminated")
- }
-}
-
-func (s *webRTCSession) runRead() (int, error) {
- ip, _, _ := net.SplitHostPort(s.req.remoteAddr)
-
- res := s.pathManager.addReader(pathAddReaderReq{
- author: s,
- pathName: s.req.pathName,
- credentials: authCredentials{
- query: s.req.query,
- ip: net.ParseIP(ip),
- user: s.req.user,
- pass: s.req.pass,
- proto: authProtocolWebRTC,
- id: &s.uuid,
- },
- })
- if res.err != nil {
- if _, ok := res.err.(*errAuthentication); ok {
- // wait some seconds to stop brute force attacks
- <-time.After(webrtcPauseAfterAuthError)
-
- return http.StatusUnauthorized, res.err
- }
-
- if strings.HasPrefix(res.err.Error(), "no one is publishing") {
- return http.StatusNotFound, res.err
- }
-
- return http.StatusBadRequest, res.err
- }
-
- defer res.path.removeReader(pathRemoveReaderReq{author: s})
-
- tracks, err := webrtcGatherOutgoingTracks(res.stream.Desc())
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- servers, err := s.parent.generateICEServers()
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- pc, err := webrtcpc.New(
- servers,
- s.api,
- s)
- if err != nil {
- return http.StatusBadRequest, err
- }
- defer pc.Close()
-
- for _, track := range tracks {
- var err error
- track.sender, err = pc.AddTrack(track.track)
- if err != nil {
- return http.StatusBadRequest, err
- }
- }
-
- offer := whipOffer(s.req.offer)
-
- err = pc.SetRemoteDescription(*offer)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- answer, err := pc.CreateAnswer(nil)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- err = pc.SetLocalDescription(answer)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- err = pc.WaitGatheringDone(s.ctx)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- s.writeAnswer(pc.LocalDescription())
-
- go s.readRemoteCandidates(pc)
-
- err = webrtcWaitUntilConnected(s.ctx, pc)
- if err != nil {
- return 0, err
- }
-
- s.mutex.Lock()
- s.pc = pc
- s.mutex.Unlock()
-
- writer := asyncwriter.New(s.writeQueueSize, s)
-
- defer res.stream.RemoveReader(writer)
-
- for _, track := range tracks {
- track.start(res.stream, writer)
- }
-
- s.Log(logger.Info, "is reading from path '%s', %s",
- res.path.name, sourceMediaInfo(webrtcMediasOfOutgoingTracks(tracks)))
-
- pathConf := res.path.safeConf()
-
- if pathConf.RunOnRead != "" {
- env := res.path.externalCmdEnv()
- desc := s.apiReaderDescribe()
- env["MTX_READER_TYPE"] = desc.Type
- env["MTX_READER_ID"] = desc.ID
-
- s.Log(logger.Info, "runOnRead command started")
- onReadCmd := externalcmd.NewCmd(
- s.externalCmdPool,
- pathConf.RunOnRead,
- pathConf.RunOnReadRestart,
- env,
- func(err error) {
- s.Log(logger.Info, "runOnRead command exited: %v", err)
- })
- defer func() {
- onReadCmd.Close()
- s.Log(logger.Info, "runOnRead command stopped")
- }()
- }
-
- if pathConf.RunOnUnread != "" {
- defer func() {
- env := res.path.externalCmdEnv()
- desc := s.apiReaderDescribe()
- env["MTX_READER_TYPE"] = desc.Type
- env["MTX_READER_ID"] = desc.ID
-
- s.Log(logger.Info, "runOnUnread command launched")
- externalcmd.NewCmd(
- s.externalCmdPool,
- pathConf.RunOnUnread,
- false,
- env,
- nil)
- }()
- }
-
- writer.Start()
-
- select {
- case <-pc.Disconnected():
- writer.Stop()
- return 0, fmt.Errorf("peer connection closed")
-
- case err := <-writer.Error():
- return 0, err
-
- case <-s.ctx.Done():
- writer.Stop()
- return 0, fmt.Errorf("terminated")
- }
-}
-
-func (s *webRTCSession) writeAnswer(answer *webrtc.SessionDescription) {
- s.req.res <- webRTCNewSessionRes{
- sx: s,
- answer: []byte(answer.SDP),
- }
-}
-
-func (s *webRTCSession) readRemoteCandidates(pc *webrtcpc.PeerConnection) {
- for {
- select {
- case req := <-s.chAddCandidates:
- for _, candidate := range req.candidates {
- err := pc.AddICECandidate(*candidate)
- if err != nil {
- req.res <- webRTCAddSessionCandidatesRes{err: err}
- }
- }
- req.res <- webRTCAddSessionCandidatesRes{}
-
- case <-s.ctx.Done():
- return
- }
- }
-}
-
-// new is called by webRTCHTTPServer through webRTCManager.
-func (s *webRTCSession) new(req webRTCNewSessionReq) webRTCNewSessionRes {
- select {
- case s.chNew <- req:
- return <-req.res
-
- case <-s.ctx.Done():
- return webRTCNewSessionRes{err: fmt.Errorf("terminated"), errStatusCode: http.StatusInternalServerError}
- }
-}
-
-// addCandidates is called by webRTCHTTPServer through webRTCManager.
-func (s *webRTCSession) addCandidates(
- req webRTCAddSessionCandidatesReq,
-) webRTCAddSessionCandidatesRes {
- select {
- case s.chAddCandidates <- req:
- return <-req.res
-
- case <-s.ctx.Done():
- return webRTCAddSessionCandidatesRes{err: fmt.Errorf("terminated")}
- }
-}
-
-// apiSourceDescribe implements sourceStaticImpl.
-func (s *webRTCSession) apiSourceDescribe() apiPathSourceOrReader {
- return apiPathSourceOrReader{
- Type: "webRTCSession",
- ID: s.uuid.String(),
- }
-}
-
-// apiReaderDescribe implements reader.
-func (s *webRTCSession) apiReaderDescribe() apiPathSourceOrReader {
- return s.apiSourceDescribe()
-}
-
-func (s *webRTCSession) apiItem() *apiWebRTCSession {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
-
- peerConnectionEstablished := false
- localCandidate := ""
- remoteCandidate := ""
- bytesReceived := uint64(0)
- bytesSent := uint64(0)
-
- if s.pc != nil {
- peerConnectionEstablished = true
- localCandidate = s.pc.LocalCandidate()
- remoteCandidate = s.pc.RemoteCandidate()
- bytesReceived = s.pc.BytesReceived()
- bytesSent = s.pc.BytesSent()
- }
-
- return &apiWebRTCSession{
- ID: s.uuid,
- Created: s.created,
- RemoteAddr: s.req.remoteAddr,
- PeerConnectionEstablished: peerConnectionEstablished,
- LocalCandidate: localCandidate,
- RemoteCandidate: remoteCandidate,
- State: func() apiWebRTCSessionState {
- if s.req.publish {
- return apiWebRTCSessionStatePublish
- }
- return apiWebRTCSessionStateRead
- }(),
- Path: s.req.pathName,
- BytesReceived: bytesReceived,
- BytesSent: bytesSent,
- }
-}
diff --git a/internal/core/webrtc_source.go b/internal/core/webrtc_source.go
deleted file mode 100644
index a9bc04f0684..00000000000
--- a/internal/core/webrtc_source.go
+++ /dev/null
@@ -1,179 +0,0 @@
-package core
-
-import (
- "context"
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "time"
-
- "github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/rtptime"
- "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/webrtcpc"
- "github.com/bluenviron/mediamtx/internal/whip"
-)
-
-type webRTCSourceParent interface {
- logger.Writer
- setReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes
- setNotReady(req pathSourceStaticSetNotReadyReq)
-}
-
-type webRTCSource struct {
- readTimeout conf.StringDuration
-
- parent webRTCSourceParent
-}
-
-func newWebRTCSource(
- readTimeout conf.StringDuration,
- parent webRTCSourceParent,
-) *webRTCSource {
- s := &webRTCSource{
- readTimeout: readTimeout,
- parent: parent,
- }
-
- return s
-}
-
-func (s *webRTCSource) Log(level logger.Level, format string, args ...interface{}) {
- s.parent.Log(level, "[WebRTC source] "+format, args...)
-}
-
-// run implements sourceStaticImpl.
-func (s *webRTCSource) run(ctx context.Context, cnf *conf.PathConf, _ chan *conf.PathConf) error {
- s.Log(logger.Debug, "connecting")
-
- u, err := url.Parse(cnf.Source)
- if err != nil {
- return err
- }
-
- u.Scheme = strings.ReplaceAll(u.Scheme, "whep", "http")
-
- c := &http.Client{
- Timeout: time.Duration(s.readTimeout),
- }
-
- iceServers, err := whip.GetICEServers(ctx, c, u.String())
- if err != nil {
- return err
- }
-
- api, err := webrtcNewAPI(nil, nil, nil)
- if err != nil {
- return err
- }
-
- pc, err := webrtcpc.New(iceServers, api, s)
- if err != nil {
- return err
- }
- defer pc.Close()
-
- _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
- if err != nil {
- return err
- }
-
- _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio)
- if err != nil {
- return err
- }
-
- offer, err := pc.CreateOffer(nil)
- if err != nil {
- return err
- }
-
- err = pc.SetLocalDescription(offer)
- if err != nil {
- return err
- }
-
- err = pc.WaitGatheringDone(ctx)
- if err != nil {
- return err
- }
-
- res, err := whip.PostOffer(ctx, c, u.String(), pc.LocalDescription())
- if err != nil {
- return err
- }
-
- var sdp sdp.SessionDescription
- err = sdp.Unmarshal([]byte(res.Answer.SDP))
- if err != nil {
- return err
- }
-
- // check that there are at most two tracks
- _, err = webrtcTrackCount(sdp.MediaDescriptions)
- if err != nil {
- return err
- }
-
- trackRecv := make(chan trackRecvPair)
-
- pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
- select {
- case trackRecv <- trackRecvPair{track, receiver}:
- case <-ctx.Done():
- }
- })
-
- err = pc.SetRemoteDescription(*res.Answer)
- if err != nil {
- return err
- }
-
- err = webrtcWaitUntilConnected(ctx, pc)
- if err != nil {
- return err
- }
-
- tracks, err := webrtcGatherIncomingTracks(ctx, pc, trackRecv, 0)
- if err != nil {
- return err
- }
- medias := webrtcMediasOfIncomingTracks(tracks)
-
- rres := s.parent.setReady(pathSourceStaticSetReadyReq{
- desc: &description.Session{Medias: medias},
- generateRTPPackets: true,
- })
- if rres.err != nil {
- return rres.err
- }
-
- defer s.parent.setNotReady(pathSourceStaticSetNotReadyReq{})
-
- timeDecoder := rtptime.NewGlobalDecoder()
-
- for _, track := range tracks {
- track.start(rres.stream, timeDecoder)
- }
-
- select {
- case <-pc.Disconnected():
- return fmt.Errorf("peer connection closed")
-
- case <-ctx.Done():
- return fmt.Errorf("terminated")
- }
-}
-
-// apiSourceDescribe implements sourceStaticImpl.
-func (*webRTCSource) apiSourceDescribe() apiPathSourceOrReader {
- return apiPathSourceOrReader{
- Type: "webRTCSource",
- ID: "",
- }
-}
diff --git a/internal/core/webrtc_source_test.go b/internal/core/webrtc_source_test.go
deleted file mode 100644
index 23d63575714..00000000000
--- a/internal/core/webrtc_source_test.go
+++ /dev/null
@@ -1,188 +0,0 @@
-package core
-
-import (
- "context"
- "io"
- "net"
- "net/http"
- "testing"
-
- "github.com/bluenviron/gortsplib/v4"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/url"
- "github.com/pion/rtp"
- "github.com/pion/webrtc/v3"
- "github.com/stretchr/testify/require"
-
- "github.com/bluenviron/mediamtx/internal/webrtcpc"
-)
-
-func TestWebRTCSource(t *testing.T) {
- state := 0
-
- api, err := webrtcNewAPI(nil, nil, nil)
- require.NoError(t, err)
-
- pc, err := webrtcpc.New(nil, api, nilLogger{})
- require.NoError(t, err)
- defer pc.Close()
-
- outgoingTrack1, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeVP8,
- ClockRate: 90000,
- },
- "vp8",
- webrtcStreamID,
- )
- require.NoError(t, err)
-
- _, err = pc.AddTrack(outgoingTrack1)
- require.NoError(t, err)
-
- outgoingTrack2, err := webrtc.NewTrackLocalStaticRTP(
- webrtc.RTPCodecCapability{
- MimeType: webrtc.MimeTypeOpus,
- ClockRate: 48000,
- Channels: 2,
- },
- "opus",
- webrtcStreamID,
- )
- require.NoError(t, err)
-
- _, err = pc.AddTrack(outgoingTrack2)
- require.NoError(t, err)
-
- httpServ := &http.Server{
- Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch state {
- case 0:
- require.Equal(t, http.MethodOptions, r.Method)
- require.Equal(t, "/my/resource", r.URL.Path)
-
- w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH")
- w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match")
- w.WriteHeader(http.StatusNoContent)
-
- case 1:
- require.Equal(t, http.MethodPost, r.Method)
- require.Equal(t, "/my/resource", r.URL.Path)
- require.Equal(t, "application/sdp", r.Header.Get("Content-Type"))
-
- body, err := io.ReadAll(r.Body)
- require.NoError(t, err)
- offer := whipOffer(body)
-
- err = pc.SetRemoteDescription(*offer)
- require.NoError(t, err)
-
- answer, err := pc.CreateAnswer(nil)
- require.NoError(t, err)
-
- err = pc.SetLocalDescription(answer)
- require.NoError(t, err)
-
- err = pc.WaitGatheringDone(context.Background())
- require.NoError(t, err)
-
- w.Header().Set("Content-Type", "application/sdp")
- w.Header().Set("Accept-Patch", "application/trickle-ice-sdpfrag")
- w.Header().Set("ETag", "test_etag")
- w.Header().Set("Location", "/my/resource/sessionid")
- w.WriteHeader(http.StatusCreated)
- w.Write([]byte(pc.LocalDescription().SDP))
-
- go func() {
- <-pc.Connected()
-
- err = outgoingTrack1.WriteRTP(&rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: 123,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{5, 1},
- })
- require.NoError(t, err)
-
- err = outgoingTrack2.WriteRTP(&rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 97,
- SequenceNumber: 1123,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{5, 2},
- })
- require.NoError(t, err)
- }()
-
- default:
- t.Errorf("should not happen since there should not be additional candidates")
- }
- state++
- }),
- }
-
- ln, err := net.Listen("tcp", "localhost:5555")
- require.NoError(t, err)
-
- go httpServ.Serve(ln)
- defer httpServ.Shutdown(context.Background())
-
- p, ok := newInstance("paths:\n" +
- " proxied:\n" +
- " source: whep://localhost:5555/my/resource\n" +
- " sourceOnDemand: yes\n")
- require.Equal(t, true, ok)
- defer p.Close()
-
- c := gortsplib.Client{}
-
- u, err := url.Parse("rtsp://127.0.0.1:8554/proxied")
- require.NoError(t, err)
-
- err = c.Start(u.Scheme, u.Host)
- require.NoError(t, err)
- defer c.Close()
-
- desc, _, err := c.Describe(u)
- require.NoError(t, err)
-
- var forma *format.VP8
- medi := desc.FindFormat(&forma)
-
- _, err = c.Setup(desc.BaseURL, medi, 0, 0)
- require.NoError(t, err)
-
- received := make(chan struct{})
-
- c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
- require.Equal(t, []byte{5, 3}, pkt.Payload)
- close(received)
- })
-
- _, err = c.Play(nil)
- require.NoError(t, err)
-
- err = outgoingTrack1.WriteRTP(&rtp.Packet{
- Header: rtp.Header{
- Version: 2,
- Marker: true,
- PayloadType: 96,
- SequenceNumber: 124,
- Timestamp: 45343,
- SSRC: 563423,
- },
- Payload: []byte{5, 3},
- })
- require.NoError(t, err)
-
- <-received
-}
diff --git a/internal/defs/api.go b/internal/defs/api.go
new file mode 100644
index 00000000000..feaea0e6976
--- /dev/null
+++ b/internal/defs/api.go
@@ -0,0 +1,197 @@
+package defs
+
+import (
+ "time"
+
+ "github.com/google/uuid"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+)
+
+// APIError is a generic error.
+type APIError struct {
+ Error string `json:"error"`
+}
+
+// APIPathConfList is a list of path configurations.
+type APIPathConfList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*conf.Path `json:"items"`
+}
+
+// APIPathSourceOrReader is a source or a reader.
+type APIPathSourceOrReader struct {
+ Type string `json:"type"`
+ ID string `json:"id"`
+}
+
+// APIPath is a path.
+type APIPath struct {
+ Name string `json:"name"`
+ ConfName string `json:"confName"`
+ Source *APIPathSourceOrReader `json:"source"`
+ Ready bool `json:"ready"`
+ ReadyTime *time.Time `json:"readyTime"`
+ Tracks []string `json:"tracks"`
+ BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
+ Readers []APIPathSourceOrReader `json:"readers"`
+}
+
+// APIPathList is a list of paths.
+type APIPathList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*APIPath `json:"items"`
+}
+
+// APIHLSMuxer is an HLS muxer.
+type APIHLSMuxer struct {
+ Path string `json:"path"`
+ Created time.Time `json:"created"`
+ LastRequest time.Time `json:"lastRequest"`
+ BytesSent uint64 `json:"bytesSent"`
+}
+
+// APIHLSMuxerList is a list of HLS muxers.
+type APIHLSMuxerList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*APIHLSMuxer `json:"items"`
+}
+
+// APIRTMPConnState is the state of a RTMP connection.
+type APIRTMPConnState string
+
+// states.
+const (
+ APIRTMPConnStateIdle APIRTMPConnState = "idle"
+ APIRTMPConnStateRead APIRTMPConnState = "read"
+ APIRTMPConnStatePublish APIRTMPConnState = "publish"
+)
+
+// APIRTMPConn is a RTMP connection.
+type APIRTMPConn struct {
+ ID uuid.UUID `json:"id"`
+ Created time.Time `json:"created"`
+ RemoteAddr string `json:"remoteAddr"`
+ State APIRTMPConnState `json:"state"`
+ Path string `json:"path"`
+ Query string `json:"query"`
+ BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
+}
+
+// APIRTMPConnList is a list of RTMP connections.
+type APIRTMPConnList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*APIRTMPConn `json:"items"`
+}
+
+// APIRTSPConn is a RTSP connection.
+type APIRTSPConn struct {
+ ID uuid.UUID `json:"id"`
+ Created time.Time `json:"created"`
+ RemoteAddr string `json:"remoteAddr"`
+ BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
+}
+
+// APIRTSPConnsList is a list of RTSP connections.
+type APIRTSPConnsList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*APIRTSPConn `json:"items"`
+}
+
+// APIRTSPSessionState is the state of a RTSP session.
+type APIRTSPSessionState string
+
+// states.
+const (
+ APIRTSPSessionStateIdle APIRTSPSessionState = "idle"
+ APIRTSPSessionStateRead APIRTSPSessionState = "read"
+ APIRTSPSessionStatePublish APIRTSPSessionState = "publish"
+)
+
+// APIRTSPSession is a RTSP session.
+type APIRTSPSession struct {
+ ID uuid.UUID `json:"id"`
+ Created time.Time `json:"created"`
+ RemoteAddr string `json:"remoteAddr"`
+ State APIRTSPSessionState `json:"state"`
+ Path string `json:"path"`
+ Query string `json:"query"`
+ Transport *string `json:"transport"`
+ BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
+}
+
+// APIRTSPSessionList is a list of RTSP sessions.
+type APIRTSPSessionList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*APIRTSPSession `json:"items"`
+}
+
+// APISRTConnState is the state of a SRT connection.
+type APISRTConnState string
+
+// states.
+const (
+ APISRTConnStateIdle APISRTConnState = "idle"
+ APISRTConnStateRead APISRTConnState = "read"
+ APISRTConnStatePublish APISRTConnState = "publish"
+)
+
+// APISRTConn is a SRT connection.
+type APISRTConn struct {
+ ID uuid.UUID `json:"id"`
+ Created time.Time `json:"created"`
+ RemoteAddr string `json:"remoteAddr"`
+ State APISRTConnState `json:"state"`
+ Path string `json:"path"`
+ Query string `json:"query"`
+ BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
+}
+
+// APISRTConnList is a list of SRT connections.
+type APISRTConnList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*APISRTConn `json:"items"`
+}
+
+// APIWebRTCSessionState is the state of a WebRTC connection.
+type APIWebRTCSessionState string
+
+// states.
+const (
+ APIWebRTCSessionStateRead APIWebRTCSessionState = "read"
+ APIWebRTCSessionStatePublish APIWebRTCSessionState = "publish"
+)
+
+// APIWebRTCSession is a WebRTC session.
+type APIWebRTCSession struct {
+ ID uuid.UUID `json:"id"`
+ Created time.Time `json:"created"`
+ RemoteAddr string `json:"remoteAddr"`
+ PeerConnectionEstablished bool `json:"peerConnectionEstablished"`
+ LocalCandidate string `json:"localCandidate"`
+ RemoteCandidate string `json:"remoteCandidate"`
+ State APIWebRTCSessionState `json:"state"`
+ Path string `json:"path"`
+ Query string `json:"query"`
+ BytesReceived uint64 `json:"bytesReceived"`
+ BytesSent uint64 `json:"bytesSent"`
+}
+
+// APIWebRTCSessionList is a list of WebRTC sessions.
+type APIWebRTCSessionList struct {
+ ItemCount int `json:"itemCount"`
+ PageCount int `json:"pageCount"`
+ Items []*APIWebRTCSession `json:"items"`
+}
diff --git a/internal/defs/auth.go b/internal/defs/auth.go
new file mode 100644
index 00000000000..a0d708f9b4f
--- /dev/null
+++ b/internal/defs/auth.go
@@ -0,0 +1,23 @@
+package defs
+
+// AuthProtocol is a authentication protocol.
+type AuthProtocol string
+
+// authentication protocols.
+const (
+ AuthProtocolRTSP AuthProtocol = "rtsp"
+ AuthProtocolRTMP AuthProtocol = "rtmp"
+ AuthProtocolHLS AuthProtocol = "hls"
+ AuthProtocolWebRTC AuthProtocol = "webrtc"
+ AuthProtocolSRT AuthProtocol = "srt"
+)
+
+// AuthenticationError is a authentication error.
+type AuthenticationError struct {
+ Message string
+}
+
+// Error implements the error interface.
+func (e AuthenticationError) Error() string {
+ return "authentication failed: " + e.Message
+}
diff --git a/internal/defs/defs.go b/internal/defs/defs.go
new file mode 100644
index 00000000000..6049823f336
--- /dev/null
+++ b/internal/defs/defs.go
@@ -0,0 +1,2 @@
+// Package defs contains shared definitions.
+package defs
diff --git a/internal/defs/path.go b/internal/defs/path.go
new file mode 100644
index 00000000000..fb5be3686fa
--- /dev/null
+++ b/internal/defs/path.go
@@ -0,0 +1,156 @@
+package defs
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/base"
+ "github.com/bluenviron/gortsplib/v4/pkg/description"
+ "github.com/google/uuid"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/externalcmd"
+ "github.com/bluenviron/mediamtx/internal/stream"
+)
+
+// PathNoOnePublishingError is returned when no one is publishing.
+type PathNoOnePublishingError struct {
+ PathName string
+}
+
+// Error implements the error interface.
+func (e PathNoOnePublishingError) Error() string {
+ return fmt.Sprintf("no one is publishing to path '%s'", e.PathName)
+}
+
+// Path is a path.
+type Path interface {
+ Name() string
+ SafeConf() *conf.Path
+ ExternalCmdEnv() externalcmd.Environment
+ StartPublisher(req PathStartPublisherReq) PathStartPublisherRes
+ StopPublisher(req PathStopPublisherReq)
+ RemovePublisher(req PathRemovePublisherReq)
+ RemoveReader(req PathRemoveReaderReq)
+}
+
+// PathAccessRequest is an access request.
+type PathAccessRequest struct {
+ Name string
+ Query string
+ Publish bool
+ SkipAuth bool
+
+ // only if skipAuth = false
+ IP net.IP
+ User string
+ Pass string
+ Proto AuthProtocol
+ ID *uuid.UUID
+ RTSPRequest *base.Request
+ RTSPBaseURL *base.URL
+ RTSPNonce string
+}
+
+// PathFindPathConfRes contains the response of FindPathConf().
+type PathFindPathConfRes struct {
+ Conf *conf.Path
+ Err error
+}
+
+// PathFindPathConfReq contains arguments of FindPathConf().
+type PathFindPathConfReq struct {
+ AccessRequest PathAccessRequest
+ Res chan PathFindPathConfRes
+}
+
+// PathDescribeRes contains the response of Describe().
+type PathDescribeRes struct {
+ Path Path
+ Stream *stream.Stream
+ Redirect string
+ Err error
+}
+
+// PathDescribeReq contains arguments of Describe().
+type PathDescribeReq struct {
+ AccessRequest PathAccessRequest
+ Res chan PathDescribeRes
+}
+
+// PathAddPublisherRes contains the response of AddPublisher().
+type PathAddPublisherRes struct {
+ Path Path
+ Err error
+}
+
+// PathAddPublisherReq contains arguments of AddPublisher().
+type PathAddPublisherReq struct {
+ Author Publisher
+ AccessRequest PathAccessRequest
+ Res chan PathAddPublisherRes
+}
+
+// PathRemovePublisherReq contains arguments of RemovePublisher().
+type PathRemovePublisherReq struct {
+ Author Publisher
+ Res chan struct{}
+}
+
+// PathStartPublisherRes contains the response of StartPublisher().
+type PathStartPublisherRes struct {
+ Stream *stream.Stream
+ Err error
+}
+
+// PathStartPublisherReq contains arguments of StartPublisher().
+type PathStartPublisherReq struct {
+ Author Publisher
+ Desc *description.Session
+ GenerateRTPPackets bool
+ Res chan PathStartPublisherRes
+}
+
+// PathStopPublisherReq contains arguments of StopPublisher().
+type PathStopPublisherReq struct {
+ Author Publisher
+ Res chan struct{}
+}
+
+// PathAddReaderRes contains the response of AddReader().
+type PathAddReaderRes struct {
+ Path Path
+ Stream *stream.Stream
+ Err error
+}
+
+// PathAddReaderReq contains arguments of AddReader().
+type PathAddReaderReq struct {
+ Author Reader
+ AccessRequest PathAccessRequest
+ Res chan PathAddReaderRes
+}
+
+// PathRemoveReaderReq contains arguments of RemoveReader().
+type PathRemoveReaderReq struct {
+ Author Reader
+ Res chan struct{}
+}
+
+// PathSourceStaticSetReadyRes contains the response of SetReadu().
+type PathSourceStaticSetReadyRes struct {
+ Stream *stream.Stream
+ Err error
+}
+
+// PathSourceStaticSetReadyReq contains arguments of SetReady().
+type PathSourceStaticSetReadyReq struct {
+ Desc *description.Session
+ GenerateRTPPackets bool
+ Res chan PathSourceStaticSetReadyRes
+}
+
+// PathSourceStaticSetNotReadyReq contains arguments of SetNotReady().
+type PathSourceStaticSetNotReadyReq struct {
+ Res chan struct{}
+}
diff --git a/internal/defs/path_manager.go b/internal/defs/path_manager.go
new file mode 100644
index 00000000000..98cd1ca3a29
--- /dev/null
+++ b/internal/defs/path_manager.go
@@ -0,0 +1,9 @@
+package defs
+
+// PathManager is a path manager.
+type PathManager interface {
+ FindPathConf(req PathFindPathConfReq) PathFindPathConfRes
+ Describe(req PathDescribeReq) PathDescribeRes
+ AddPublisher(req PathAddPublisherReq) PathAddPublisherRes
+ AddReader(req PathAddReaderReq) PathAddReaderRes
+}
diff --git a/internal/defs/publisher.go b/internal/defs/publisher.go
new file mode 100644
index 00000000000..72ffc74d46e
--- /dev/null
+++ b/internal/defs/publisher.go
@@ -0,0 +1,7 @@
+package defs
+
+// Publisher is an entity that can publish a stream.
+type Publisher interface {
+ Source
+ Close()
+}
diff --git a/internal/defs/reader.go b/internal/defs/reader.go
new file mode 100644
index 00000000000..c6f9f6403a0
--- /dev/null
+++ b/internal/defs/reader.go
@@ -0,0 +1,7 @@
+package defs
+
+// Reader is an entity that can read a stream.
+type Reader interface {
+ Close()
+ APIReaderDescribe() APIPathSourceOrReader
+}
diff --git a/internal/defs/source.go b/internal/defs/source.go
new file mode 100644
index 00000000000..e1c3171c24d
--- /dev/null
+++ b/internal/defs/source.go
@@ -0,0 +1,63 @@
+package defs
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/description"
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// Source is an entity that can provide a stream.
+// it can be:
+// - publisher
+// - staticSourceHandler
+// - redirectSource
+type Source interface {
+ logger.Writer
+ APISourceDescribe() APIPathSourceOrReader
+}
+
+// FormatsToCodecs returns the name of codecs of given formats.
+func FormatsToCodecs(formats []format.Format) []string {
+ ret := make([]string, len(formats))
+ for i, forma := range formats {
+ ret[i] = forma.Codec()
+ }
+ return ret
+}
+
+// FormatsInfo returns a description of formats.
+func FormatsInfo(formats []format.Format) string {
+ return fmt.Sprintf("%d %s (%s)",
+ len(formats),
+ func() string {
+ if len(formats) == 1 {
+ return "track"
+ }
+ return "tracks"
+ }(),
+ strings.Join(FormatsToCodecs(formats), ", "))
+}
+
+// MediasToCodecs returns the name of codecs of given formats.
+func MediasToCodecs(medias []*description.Media) []string {
+ var formats []format.Format
+ for _, media := range medias {
+ formats = append(formats, media.Formats...)
+ }
+
+ return FormatsToCodecs(formats)
+}
+
+// MediasInfo returns a description of medias.
+func MediasInfo(medias []*description.Media) string {
+ var formats []format.Format
+ for _, media := range medias {
+ formats = append(formats, media.Formats...)
+ }
+
+ return FormatsInfo(formats)
+}
diff --git a/internal/defs/static_source.go b/internal/defs/static_source.go
new file mode 100644
index 00000000000..eaf1cf4245b
--- /dev/null
+++ b/internal/defs/static_source.go
@@ -0,0 +1,29 @@
+package defs
+
+import (
+ "context"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// StaticSource is a static source.
+type StaticSource interface {
+ logger.Writer
+ Run(StaticSourceRunParams) error
+ APISourceDescribe() APIPathSourceOrReader
+}
+
+// StaticSourceParent is the parent of a static source.
+type StaticSourceParent interface {
+ logger.Writer
+ SetReady(req PathSourceStaticSetReadyReq) PathSourceStaticSetReadyRes
+ SetNotReady(req PathSourceStaticSetNotReadyReq)
+}
+
+// StaticSourceRunParams is the set of params passed to Run().
+type StaticSourceRunParams struct {
+ Context context.Context
+ Conf *conf.Path
+ ReloadConf chan *conf.Path
+}
diff --git a/internal/externalcmd/cmd.go b/internal/externalcmd/cmd.go
index aed7f7d6190..132465fade5 100644
--- a/internal/externalcmd/cmd.go
+++ b/internal/externalcmd/cmd.go
@@ -76,7 +76,7 @@ func (e *Cmd) run() {
for {
err := e.runOSSpecific()
- if err == errTerminated {
+ if errors.Is(err, errTerminated) {
return
}
@@ -87,7 +87,11 @@ func (e *Cmd) run() {
return
}
- e.onExit(fmt.Errorf("command exited with code 0"))
+ if err != nil {
+ e.onExit(err)
+ } else {
+ e.onExit(fmt.Errorf("command exited with code 0"))
+ }
select {
case <-time.After(restartPause):
diff --git a/internal/externalcmd/cmd_unix.go b/internal/externalcmd/cmd_unix.go
index 06cb30c899c..fa9b58317e7 100644
--- a/internal/externalcmd/cmd_unix.go
+++ b/internal/externalcmd/cmd_unix.go
@@ -4,6 +4,7 @@
package externalcmd
import (
+ "errors"
"fmt"
"os"
"os/exec"
@@ -40,11 +41,11 @@ func (e *Cmd) runOSSpecific() error {
if err == nil {
return 0
}
- ee, ok := err.(*exec.ExitError)
- if !ok {
- return 0
+ var ee *exec.ExitError
+ if errors.As(err, &ee) {
+ ee.ExitCode()
}
- return ee.ExitCode()
+ return 0
}()
}()
diff --git a/internal/formatprocessor/ac3.go b/internal/formatprocessor/ac3.go
index 90ef5aa3a2f..20badf04e7a 100644
--- a/internal/formatprocessor/ac3.go
+++ b/internal/formatprocessor/ac3.go
@@ -1,6 +1,7 @@
package formatprocessor
import (
+ "errors"
"fmt"
"time"
@@ -52,14 +53,13 @@ func (t *formatProcessorAC3) ProcessUnit(uu unit.Unit) error { //nolint:dupl
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -98,7 +98,8 @@ func (t *formatProcessorAC3) ProcessRTPPacket( //nolint:dupl
frames, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpac3.ErrNonStartingPacketAndNoPrevious || err == rtpac3.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpac3.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtpac3.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/av1.go b/internal/formatprocessor/av1.go
index 5d8b6418cd8..2bc8bae66f7 100644
--- a/internal/formatprocessor/av1.go
+++ b/internal/formatprocessor/av1.go
@@ -1,6 +1,7 @@
package formatprocessor //nolint:dupl
import (
+ "errors"
"fmt"
"time"
@@ -54,14 +55,13 @@ func (t *formatProcessorAV1) ProcessUnit(uu unit.Unit) error { //nolint:dupl
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -100,7 +100,8 @@ func (t *formatProcessorAV1) ProcessRTPPacket( //nolint:dupl
tu, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpav1.ErrNonStartingPacketAndNoPrevious || err == rtpav1.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpav1.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtpav1.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/av1_test.go b/internal/formatprocessor/av1_test.go
deleted file mode 100644
index 136e2bf8332..00000000000
--- a/internal/formatprocessor/av1_test.go
+++ /dev/null
@@ -1 +0,0 @@
-package formatprocessor
diff --git a/internal/formatprocessor/g711.go b/internal/formatprocessor/g711.go
new file mode 100644
index 00000000000..38b3bb0e66e
--- /dev/null
+++ b/internal/formatprocessor/g711.go
@@ -0,0 +1,111 @@
+package formatprocessor //nolint:dupl
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
+ "github.com/pion/rtp"
+
+ "github.com/bluenviron/mediamtx/internal/unit"
+)
+
+type formatProcessorG711 struct {
+ udpMaxPayloadSize int
+ format *format.G711
+ encoder *rtplpcm.Encoder
+ decoder *rtplpcm.Decoder
+}
+
+func newG711(
+ udpMaxPayloadSize int,
+ forma *format.G711,
+ generateRTPPackets bool,
+) (*formatProcessorG711, error) {
+ t := &formatProcessorG711{
+ udpMaxPayloadSize: udpMaxPayloadSize,
+ format: forma,
+ }
+
+ if generateRTPPackets {
+ err := t.createEncoder()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return t, nil
+}
+
+func (t *formatProcessorG711) createEncoder() error {
+ t.encoder = &rtplpcm.Encoder{
+ PayloadMaxSize: t.udpMaxPayloadSize - 12,
+ PayloadType: t.format.PayloadType(),
+ BitDepth: 8,
+ ChannelCount: t.format.ChannelCount,
+ }
+ return t.encoder.Init()
+}
+
+func (t *formatProcessorG711) ProcessUnit(uu unit.Unit) error { //nolint:dupl
+ u := uu.(*unit.G711)
+
+ pkts, err := t.encoder.Encode(u.Samples)
+ if err != nil {
+ return err
+ }
+ u.RTPPackets = pkts
+
+ ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
+ for _, pkt := range u.RTPPackets {
+ pkt.Timestamp += ts
+ }
+
+ return nil
+}
+
+func (t *formatProcessorG711) ProcessRTPPacket( //nolint:dupl
+ pkt *rtp.Packet,
+ ntp time.Time,
+ pts time.Duration,
+ hasNonRTSPReaders bool,
+) (Unit, error) {
+ u := &unit.G711{
+ Base: unit.Base{
+ RTPPackets: []*rtp.Packet{pkt},
+ NTP: ntp,
+ PTS: pts,
+ },
+ }
+
+ // remove padding
+ pkt.Header.Padding = false
+ pkt.PaddingSize = 0
+
+ if pkt.MarshalSize() > t.udpMaxPayloadSize {
+ return nil, fmt.Errorf("payload size (%d) is greater than maximum allowed (%d)",
+ pkt.MarshalSize(), t.udpMaxPayloadSize)
+ }
+
+ // decode from RTP
+ if hasNonRTSPReaders || t.decoder != nil {
+ if t.decoder == nil {
+ var err error
+ t.decoder, err = t.format.CreateDecoder()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ samples, err := t.decoder.Decode(pkt)
+ if err != nil {
+ return nil, err
+ }
+
+ u.Samples = samples
+ }
+
+ // route packet as is
+ return u, nil
+}
diff --git a/internal/formatprocessor/g711_test.go b/internal/formatprocessor/g711_test.go
new file mode 100644
index 00000000000..d907442f4db
--- /dev/null
+++ b/internal/formatprocessor/g711_test.go
@@ -0,0 +1,68 @@
+package formatprocessor
+
+import (
+ "testing"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/mediamtx/internal/unit"
+ "github.com/pion/rtp"
+ "github.com/stretchr/testify/require"
+)
+
+func TestG611Encode(t *testing.T) {
+ t.Run("alaw", func(t *testing.T) {
+ forma := &format.G711{
+ PayloadTyp: 8,
+ MULaw: false,
+ SampleRate: 8000,
+ ChannelCount: 1,
+ }
+
+ p, err := New(1472, forma, true)
+ require.NoError(t, err)
+
+ unit := &unit.G711{
+ Samples: []byte{1, 2, 3, 4},
+ }
+
+ err = p.ProcessUnit(unit)
+ require.NoError(t, err)
+ require.Equal(t, []*rtp.Packet{{
+ Header: rtp.Header{
+ Version: 2,
+ PayloadType: 8,
+ SequenceNumber: unit.RTPPackets[0].SequenceNumber,
+ SSRC: unit.RTPPackets[0].SSRC,
+ },
+ Payload: []byte{1, 2, 3, 4},
+ }}, unit.RTPPackets)
+ })
+
+ t.Run("mulaw", func(t *testing.T) {
+ forma := &format.G711{
+ PayloadTyp: 0,
+ MULaw: true,
+ SampleRate: 8000,
+ ChannelCount: 1,
+ }
+
+ p, err := New(1472, forma, true)
+ require.NoError(t, err)
+
+ unit := &unit.G711{
+ Samples: []byte{1, 2, 3, 4},
+ }
+
+ err = p.ProcessUnit(unit)
+ require.NoError(t, err)
+ require.Equal(t, []*rtp.Packet{{
+ Header: rtp.Header{
+ Version: 2,
+ PayloadType: 0,
+ SequenceNumber: unit.RTPPackets[0].SequenceNumber,
+ SSRC: unit.RTPPackets[0].SSRC,
+ },
+ Payload: []byte{1, 2, 3, 4},
+ }}, unit.RTPPackets)
+ })
+}
diff --git a/internal/formatprocessor/h264.go b/internal/formatprocessor/h264.go
index 80cfc70aa06..e34aef530d6 100644
--- a/internal/formatprocessor/h264.go
+++ b/internal/formatprocessor/h264.go
@@ -2,6 +2,7 @@ package formatprocessor
import (
"bytes"
+ "errors"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/format"
@@ -13,7 +14,7 @@ import (
)
// extract SPS and PPS without decoding RTP packets
-func rtpH264ExtractSPSPPS(payload []byte) ([]byte, []byte) {
+func rtpH264ExtractParams(payload []byte) ([]byte, []byte) {
if len(payload) < 1 {
return nil, nil
}
@@ -112,7 +113,7 @@ func (t *formatProcessorH264) createEncoder(
}
func (t *formatProcessorH264) updateTrackParametersFromRTPPacket(payload []byte) {
- sps, pps := rtpH264ExtractSPSPPS(payload)
+ sps, pps := rtpH264ExtractParams(payload)
if (sps != nil && !bytes.Equal(sps, t.format.SPS)) ||
(pps != nil && !bytes.Equal(pps, t.format.PPS)) {
@@ -223,13 +224,12 @@ func (t *formatProcessorH264) ProcessUnit(uu unit.Unit) error {
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
-
- u.RTPPackets = pkts
}
return nil
@@ -284,7 +284,8 @@ func (t *formatProcessorH264) ProcessRTPPacket( //nolint:dupl
}
if err != nil {
- if err == rtph264.ErrNonStartingPacketAndNoPrevious || err == rtph264.ErrMorePacketsNeeded {
+ if errors.Is(err, rtph264.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtph264.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
@@ -304,12 +305,11 @@ func (t *formatProcessorH264) ProcessRTPPacket( //nolint:dupl
if err != nil {
return nil, err
}
+ u.RTPPackets = pkts
- for _, newPKT := range pkts {
+ for _, newPKT := range u.RTPPackets {
newPKT.Timestamp = pkt.Timestamp
}
-
- u.RTPPackets = pkts
}
return u, nil
diff --git a/internal/formatprocessor/h264_test.go b/internal/formatprocessor/h264_test.go
index b0567cb936f..bf1fdeb1716 100644
--- a/internal/formatprocessor/h264_test.go
+++ b/internal/formatprocessor/h264_test.go
@@ -200,3 +200,9 @@ func TestH264EmptyPacket(t *testing.T) {
// if all NALUs have been removed, no RTP packets must be generated.
require.Equal(t, []*rtp.Packet(nil), unit.RTPPackets)
}
+
+func FuzzRTPH264ExtractParams(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ rtpH264ExtractParams(b)
+ })
+}
diff --git a/internal/formatprocessor/h265.go b/internal/formatprocessor/h265.go
index 629945a77a5..74b8b80b5f5 100644
--- a/internal/formatprocessor/h265.go
+++ b/internal/formatprocessor/h265.go
@@ -2,6 +2,7 @@ package formatprocessor
import (
"bytes"
+ "errors"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/format"
@@ -13,7 +14,7 @@ import (
)
// extract VPS, SPS and PPS without decoding RTP packets
-func rtpH265ExtractVPSSPSPPS(payload []byte) ([]byte, []byte, []byte) {
+func rtpH265ExtractParams(payload []byte) ([]byte, []byte, []byte) {
if len(payload) < 2 {
return nil, nil, nil
}
@@ -119,7 +120,7 @@ func (t *formatProcessorH265) createEncoder(
}
func (t *formatProcessorH265) updateTrackParametersFromRTPPacket(payload []byte) {
- vps, sps, pps := rtpH265ExtractVPSSPSPPS(payload)
+ vps, sps, pps := rtpH265ExtractParams(payload)
if (vps != nil && !bytes.Equal(vps, t.format.VPS)) ||
(sps != nil && !bytes.Equal(sps, t.format.SPS)) ||
@@ -242,13 +243,12 @@ func (t *formatProcessorH265) ProcessUnit(uu unit.Unit) error { //nolint:dupl
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
-
- u.RTPPackets = pkts
}
return nil
@@ -303,7 +303,8 @@ func (t *formatProcessorH265) ProcessRTPPacket( //nolint:dupl
}
if err != nil {
- if err == rtph265.ErrNonStartingPacketAndNoPrevious || err == rtph265.ErrMorePacketsNeeded {
+ if errors.Is(err, rtph265.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtph265.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
@@ -323,12 +324,11 @@ func (t *formatProcessorH265) ProcessRTPPacket( //nolint:dupl
if err != nil {
return nil, err
}
+ u.RTPPackets = pkts
- for _, newPKT := range pkts {
+ for _, newPKT := range u.RTPPackets {
newPKT.Timestamp = pkt.Timestamp
}
-
- u.RTPPackets = pkts
}
return u, nil
diff --git a/internal/formatprocessor/h265_test.go b/internal/formatprocessor/h265_test.go
index 6e183bdca7d..7ad5d86c3df 100644
--- a/internal/formatprocessor/h265_test.go
+++ b/internal/formatprocessor/h265_test.go
@@ -196,3 +196,9 @@ func TestH265EmptyPacket(t *testing.T) {
// if all NALUs have been removed, no RTP packets must be generated.
require.Equal(t, []*rtp.Packet(nil), unit.RTPPackets)
}
+
+func FuzzRTPH265ExtractParams(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ rtpH265ExtractParams(b)
+ })
+}
diff --git a/internal/formatprocessor/lpcm.go b/internal/formatprocessor/lpcm.go
new file mode 100644
index 00000000000..c26d03bd716
--- /dev/null
+++ b/internal/formatprocessor/lpcm.go
@@ -0,0 +1,111 @@
+package formatprocessor //nolint:dupl
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm"
+ "github.com/pion/rtp"
+
+ "github.com/bluenviron/mediamtx/internal/unit"
+)
+
+type formatProcessorLPCM struct {
+ udpMaxPayloadSize int
+ format *format.LPCM
+ encoder *rtplpcm.Encoder
+ decoder *rtplpcm.Decoder
+}
+
+func newLPCM(
+ udpMaxPayloadSize int,
+ forma *format.LPCM,
+ generateRTPPackets bool,
+) (*formatProcessorLPCM, error) {
+ t := &formatProcessorLPCM{
+ udpMaxPayloadSize: udpMaxPayloadSize,
+ format: forma,
+ }
+
+ if generateRTPPackets {
+ err := t.createEncoder()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return t, nil
+}
+
+func (t *formatProcessorLPCM) createEncoder() error {
+ t.encoder = &rtplpcm.Encoder{
+ PayloadMaxSize: t.udpMaxPayloadSize - 12,
+ PayloadType: t.format.PayloadTyp,
+ BitDepth: t.format.BitDepth,
+ ChannelCount: t.format.ChannelCount,
+ }
+ return t.encoder.Init()
+}
+
+func (t *formatProcessorLPCM) ProcessUnit(uu unit.Unit) error { //nolint:dupl
+ u := uu.(*unit.LPCM)
+
+ pkts, err := t.encoder.Encode(u.Samples)
+ if err != nil {
+ return err
+ }
+ u.RTPPackets = pkts
+
+ ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
+ for _, pkt := range u.RTPPackets {
+ pkt.Timestamp += ts
+ }
+
+ return nil
+}
+
+func (t *formatProcessorLPCM) ProcessRTPPacket( //nolint:dupl
+ pkt *rtp.Packet,
+ ntp time.Time,
+ pts time.Duration,
+ hasNonRTSPReaders bool,
+) (Unit, error) {
+ u := &unit.LPCM{
+ Base: unit.Base{
+ RTPPackets: []*rtp.Packet{pkt},
+ NTP: ntp,
+ PTS: pts,
+ },
+ }
+
+ // remove padding
+ pkt.Header.Padding = false
+ pkt.PaddingSize = 0
+
+ if pkt.MarshalSize() > t.udpMaxPayloadSize {
+ return nil, fmt.Errorf("payload size (%d) is greater than maximum allowed (%d)",
+ pkt.MarshalSize(), t.udpMaxPayloadSize)
+ }
+
+ // decode from RTP
+ if hasNonRTSPReaders || t.decoder != nil {
+ if t.decoder == nil {
+ var err error
+ t.decoder, err = t.format.CreateDecoder()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ samples, err := t.decoder.Decode(pkt)
+ if err != nil {
+ return nil, err
+ }
+
+ u.Samples = samples
+ }
+
+ // route packet as is
+ return u, nil
+}
diff --git a/internal/formatprocessor/lpcm_test.go b/internal/formatprocessor/lpcm_test.go
new file mode 100644
index 00000000000..a217a38f35d
--- /dev/null
+++ b/internal/formatprocessor/lpcm_test.go
@@ -0,0 +1,37 @@
+package formatprocessor
+
+import (
+ "testing"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/mediamtx/internal/unit"
+ "github.com/pion/rtp"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLPCMEncode(t *testing.T) {
+ forma := &format.LPCM{
+ PayloadTyp: 96,
+ BitDepth: 16,
+ ChannelCount: 2,
+ }
+
+ p, err := New(1472, forma, true)
+ require.NoError(t, err)
+
+ unit := &unit.LPCM{
+ Samples: []byte{1, 2, 3, 4},
+ }
+
+ err = p.ProcessUnit(unit)
+ require.NoError(t, err)
+ require.Equal(t, []*rtp.Packet{{
+ Header: rtp.Header{
+ Version: 2,
+ PayloadType: 96,
+ SequenceNumber: unit.RTPPackets[0].SequenceNumber,
+ SSRC: unit.RTPPackets[0].SSRC,
+ },
+ Payload: []byte{1, 2, 3, 4},
+ }}, unit.RTPPackets)
+}
diff --git a/internal/formatprocessor/mjpeg.go b/internal/formatprocessor/mjpeg.go
index 4dd5474a5e0..90c5cce25d5 100644
--- a/internal/formatprocessor/mjpeg.go
+++ b/internal/formatprocessor/mjpeg.go
@@ -1,6 +1,7 @@
package formatprocessor //nolint:dupl
import (
+ "errors"
"fmt"
"time"
@@ -53,14 +54,13 @@ func (t *formatProcessorMJPEG) ProcessUnit(uu unit.Unit) error { //nolint:dupl
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -99,7 +99,8 @@ func (t *formatProcessorMJPEG) ProcessRTPPacket( //nolint:dupl
frame, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpmjpeg.ErrNonStartingPacketAndNoPrevious || err == rtpmjpeg.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpmjpeg.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtpmjpeg.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/mpeg1_audio.go b/internal/formatprocessor/mpeg1_audio.go
index 68b39b4cfe5..6a02f62bd8d 100644
--- a/internal/formatprocessor/mpeg1_audio.go
+++ b/internal/formatprocessor/mpeg1_audio.go
@@ -1,6 +1,7 @@
package formatprocessor //nolint:dupl
import (
+ "errors"
"fmt"
"time"
@@ -52,14 +53,13 @@ func (t *formatProcessorMPEG1Audio) ProcessUnit(uu unit.Unit) error { //nolint:d
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -98,7 +98,8 @@ func (t *formatProcessorMPEG1Audio) ProcessRTPPacket( //nolint:dupl
frames, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpmpeg1audio.ErrNonStartingPacketAndNoPrevious || err == rtpmpeg1audio.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpmpeg1audio.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtpmpeg1audio.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/mpeg1_video.go b/internal/formatprocessor/mpeg1_video.go
index 998b149ff87..2ce207f8f9c 100644
--- a/internal/formatprocessor/mpeg1_video.go
+++ b/internal/formatprocessor/mpeg1_video.go
@@ -1,6 +1,7 @@
package formatprocessor //nolint:dupl
import (
+ "errors"
"fmt"
"time"
@@ -53,14 +54,13 @@ func (t *formatProcessorMPEG1Video) ProcessUnit(uu unit.Unit) error { //nolint:d
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -99,7 +99,8 @@ func (t *formatProcessorMPEG1Video) ProcessRTPPacket( //nolint:dupl
frame, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpmpeg1video.ErrNonStartingPacketAndNoPrevious || err == rtpmpeg1video.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpmpeg1video.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtpmpeg1video.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/mpeg4_audio.go b/internal/formatprocessor/mpeg4_audio.go
index 651760c63a5..6ca41deaf36 100644
--- a/internal/formatprocessor/mpeg4_audio.go
+++ b/internal/formatprocessor/mpeg4_audio.go
@@ -1,6 +1,7 @@
package formatprocessor
import (
+ "errors"
"fmt"
"time"
@@ -56,14 +57,13 @@ func (t *formatProcessorMPEG4Audio) ProcessUnit(uu unit.Unit) error { //nolint:d
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -102,7 +102,7 @@ func (t *formatProcessorMPEG4Audio) ProcessRTPPacket( //nolint:dupl
aus, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpmpeg4audio.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpmpeg4audio.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/mpeg4_video.go b/internal/formatprocessor/mpeg4_video.go
index acdfca107c0..56786761acc 100644
--- a/internal/formatprocessor/mpeg4_video.go
+++ b/internal/formatprocessor/mpeg4_video.go
@@ -2,6 +2,7 @@ package formatprocessor //nolint:dupl
import (
"bytes"
+ "errors"
"fmt"
"time"
@@ -93,7 +94,7 @@ func (t *formatProcessorMPEG4Video) ProcessUnit(uu unit.Unit) error { //nolint:d
}
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
@@ -140,7 +141,7 @@ func (t *formatProcessorMPEG4Video) ProcessRTPPacket( //nolint:dupl
frame, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpmpeg4video.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpmpeg4video.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/processor.go b/internal/formatprocessor/processor.go
index a2fb68d1c13..76713a3ffa1 100644
--- a/internal/formatprocessor/processor.go
+++ b/internal/formatprocessor/processor.go
@@ -75,6 +75,12 @@ func New(
case *format.AC3:
return newAC3(udpMaxPayloadSize, forma, generateRTPPackets)
+ case *format.G711:
+ return newG711(udpMaxPayloadSize, forma, generateRTPPackets)
+
+ case *format.LPCM:
+ return newLPCM(udpMaxPayloadSize, forma, generateRTPPackets)
+
default:
return newGeneric(udpMaxPayloadSize, forma, generateRTPPackets)
}
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/2470f01dca6d27ef b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/048b606517c23baf
similarity index 51%
rename from internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/2470f01dca6d27ef
rename to internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/048b606517c23baf
index 8a0b67e7812..d4bdeb5347f 100644
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/2470f01dca6d27ef
+++ b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/048b606517c23baf
@@ -1,2 +1,2 @@
go test fuzz v1
-[]byte("\xe6")
+[]byte("800")
diff --git a/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/32e7782636603e29 b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/32e7782636603e29
new file mode 100644
index 00000000000..c487badc21b
--- /dev/null
+++ b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/32e7782636603e29
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("8\x00\x00")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/f5aad145f6286289 b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/caf81e9797b19c76
similarity index 51%
rename from internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/f5aad145f6286289
rename to internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/caf81e9797b19c76
index feb39524911..67322c70489 100644
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/f5aad145f6286289
+++ b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/caf81e9797b19c76
@@ -1,2 +1,2 @@
go test fuzz v1
-[]byte("\x80")
+[]byte("")
diff --git a/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/f428976a5b2917c0 b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/f428976a5b2917c0
new file mode 100644
index 00000000000..9756ef63150
--- /dev/null
+++ b/internal/formatprocessor/testdata/fuzz/FuzzRTPH264ExtractSPSPPS/f428976a5b2917c0
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("80")
diff --git a/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/353ba911ad2dc191 b/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/353ba911ad2dc191
new file mode 100644
index 00000000000..955f0420e79
--- /dev/null
+++ b/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/353ba911ad2dc191
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("a00")
diff --git a/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/3c3a72c00adac0b3 b/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/3c3a72c00adac0b3
new file mode 100644
index 00000000000..79752f98335
--- /dev/null
+++ b/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/3c3a72c00adac0b3
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("a0\x00\x00")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/582528ddfad69eb5 b/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/582528ddfad69eb5
similarity index 100%
rename from internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/582528ddfad69eb5
rename to internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/582528ddfad69eb5
diff --git a/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/c4389a565e828050 b/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/c4389a565e828050
new file mode 100644
index 00000000000..81d331a20c5
--- /dev/null
+++ b/internal/formatprocessor/testdata/fuzz/FuzzRTPH265ExtractParams/c4389a565e828050
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("a000")
diff --git a/internal/formatprocessor/vp8.go b/internal/formatprocessor/vp8.go
index f82c16606ce..3b18397fb0f 100644
--- a/internal/formatprocessor/vp8.go
+++ b/internal/formatprocessor/vp8.go
@@ -1,6 +1,7 @@
package formatprocessor //nolint:dupl
import (
+ "errors"
"fmt"
"time"
@@ -53,14 +54,13 @@ func (t *formatProcessorVP8) ProcessUnit(uu unit.Unit) error { //nolint:dupl
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -99,7 +99,8 @@ func (t *formatProcessorVP8) ProcessRTPPacket( //nolint:dupl
frame, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpvp8.ErrNonStartingPacketAndNoPrevious || err == rtpvp8.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpvp8.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtpvp8.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/formatprocessor/vp9.go b/internal/formatprocessor/vp9.go
index 5b69d98df0b..a6af1cd6ff4 100644
--- a/internal/formatprocessor/vp9.go
+++ b/internal/formatprocessor/vp9.go
@@ -1,6 +1,7 @@
package formatprocessor //nolint:dupl
import (
+ "errors"
"fmt"
"time"
@@ -53,14 +54,13 @@ func (t *formatProcessorVP9) ProcessUnit(uu unit.Unit) error { //nolint:dupl
if err != nil {
return err
}
+ u.RTPPackets = pkts
ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
- for _, pkt := range pkts {
+ for _, pkt := range u.RTPPackets {
pkt.Timestamp += ts
}
- u.RTPPackets = pkts
-
return nil
}
@@ -99,7 +99,8 @@ func (t *formatProcessorVP9) ProcessRTPPacket( //nolint:dupl
frame, err := t.decoder.Decode(pkt)
if err != nil {
- if err == rtpvp9.ErrNonStartingPacketAndNoPrevious || err == rtpvp9.ErrMorePacketsNeeded {
+ if errors.Is(err, rtpvp9.ErrNonStartingPacketAndNoPrevious) ||
+ errors.Is(err, rtpvp9.ErrMorePacketsNeeded) {
return u, nil
}
return nil, err
diff --git a/internal/highleveltests/hls_manager_test.go b/internal/highleveltests/hls_manager_test.go
index 3f83aeabc03..257a8f98f46 100644
--- a/internal/highleveltests/hls_manager_test.go
+++ b/internal/highleveltests/hls_manager_test.go
@@ -13,7 +13,7 @@ import (
func TestHLSServerRead(t *testing.T) {
p, ok := newInstance("paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
@@ -48,7 +48,7 @@ func TestHLSServerAuth(t *testing.T) {
} {
t.Run(result, func(t *testing.T) {
conf := "paths:\n" +
- " all:\n" +
+ " all_others:\n" +
" readUser: testreader\n" +
" readPass: testpass\n" +
" readIPs: [127.0.0.0/16]\n"
diff --git a/internal/highleveltests/rtsp_server_test.go b/internal/highleveltests/rtsp_server_test.go
index 69f105e589c..776b69cd685 100644
--- a/internal/highleveltests/rtsp_server_test.go
+++ b/internal/highleveltests/rtsp_server_test.go
@@ -47,7 +47,7 @@ func TestRTSPServerPublishRead(t *testing.T) {
"webrtc: no\n" +
"readTimeout: 20s\n" +
"paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
} else {
@@ -71,7 +71,7 @@ func TestRTSPServerPublishRead(t *testing.T) {
"serverCert: " + serverCertFpath + "\n" +
"serverKey: " + serverKeyFpath + "\n" +
"paths:\n" +
- " all:\n")
+ " all_others:\n")
require.Equal(t, true, ok)
defer p.Close()
}
diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go
new file mode 100644
index 00000000000..cc592289080
--- /dev/null
+++ b/internal/hooks/hooks.go
@@ -0,0 +1,2 @@
+// Package hooks contains hook implementations.
+package hooks
diff --git a/internal/hooks/on_connect.go b/internal/hooks/on_connect.go
new file mode 100644
index 00000000000..2e6454ee4ee
--- /dev/null
+++ b/internal/hooks/on_connect.go
@@ -0,0 +1,65 @@
+package hooks
+
+import (
+ "net"
+
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/externalcmd"
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// OnConnectParams are the parameters of OnConnect.
+type OnConnectParams struct {
+ Logger logger.Writer
+ ExternalCmdPool *externalcmd.Pool
+ RunOnConnect string
+ RunOnConnectRestart bool
+ RunOnDisconnect string
+ RTSPAddress string
+ Desc defs.APIPathSourceOrReader
+}
+
+// OnConnect is the OnConnect hook.
+func OnConnect(params OnConnectParams) func() {
+ var env externalcmd.Environment
+ var onConnectCmd *externalcmd.Cmd
+
+ if params.RunOnConnect != "" || params.RunOnDisconnect != "" {
+ _, port, _ := net.SplitHostPort(params.RTSPAddress)
+ env = externalcmd.Environment{
+ "RTSP_PORT": port,
+ "MTX_CONN_TYPE": params.Desc.Type,
+ "MTX_CONN_ID": params.Desc.ID,
+ }
+ }
+
+ if params.RunOnConnect != "" {
+ params.Logger.Log(logger.Info, "runOnConnect command started")
+
+ onConnectCmd = externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.RunOnConnect,
+ params.RunOnConnectRestart,
+ env,
+ func(err error) {
+ params.Logger.Log(logger.Info, "runOnConnect command exited: %v", err)
+ })
+ }
+
+ return func() {
+ if onConnectCmd != nil {
+ onConnectCmd.Close()
+ params.Logger.Log(logger.Info, "runOnConnect command stopped")
+ }
+
+ if params.RunOnDisconnect != "" {
+ params.Logger.Log(logger.Info, "runOnDisconnect command launched")
+ externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.RunOnDisconnect,
+ false,
+ env,
+ nil)
+ }
+ }
+}
diff --git a/internal/hooks/on_demand.go b/internal/hooks/on_demand.go
new file mode 100644
index 00000000000..f5a789f0d45
--- /dev/null
+++ b/internal/hooks/on_demand.go
@@ -0,0 +1,57 @@
+package hooks
+
+import (
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/externalcmd"
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// OnDemandParams are the parameters of OnDemand.
+type OnDemandParams struct {
+ Logger logger.Writer
+ ExternalCmdPool *externalcmd.Pool
+ Conf *conf.Path
+ ExternalCmdEnv externalcmd.Environment
+ Query string
+}
+
+// OnDemand is the OnDemand hook.
+func OnDemand(params OnDemandParams) func(string) {
+ var env externalcmd.Environment
+ var onDemandCmd *externalcmd.Cmd
+
+ if params.Conf.RunOnDemand != "" || params.Conf.RunOnUnDemand != "" {
+ env = params.ExternalCmdEnv
+ env["MTX_QUERY"] = params.Query
+ }
+
+ if params.Conf.RunOnDemand != "" {
+ params.Logger.Log(logger.Info, "runOnDemand command started")
+
+ onDemandCmd = externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.Conf.RunOnDemand,
+ params.Conf.RunOnDemandRestart,
+ env,
+ func(err error) {
+ params.Logger.Log(logger.Info, "runOnDemand command exited: %v", err)
+ })
+ }
+
+ return func(reason string) {
+ if onDemandCmd != nil {
+ onDemandCmd.Close()
+ params.Logger.Log(logger.Info, "runOnDemand command stopped: %v", reason)
+ }
+
+ if params.Conf.RunOnUnDemand != "" {
+ params.Logger.Log(logger.Info, "runOnUnDemand command launched")
+ externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.Conf.RunOnUnDemand,
+ false,
+ env,
+ nil)
+ }
+ }
+}
diff --git a/internal/hooks/on_init.go b/internal/hooks/on_init.go
new file mode 100644
index 00000000000..bfda4211fd4
--- /dev/null
+++ b/internal/hooks/on_init.go
@@ -0,0 +1,39 @@
+package hooks
+
+import (
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/externalcmd"
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// OnInitParams are the parameters of OnInit.
+type OnInitParams struct {
+ Logger logger.Writer
+ ExternalCmdPool *externalcmd.Pool
+ Conf *conf.Path
+ ExternalCmdEnv externalcmd.Environment
+}
+
+// OnInit is the OnInit hook.
+func OnInit(params OnInitParams) func() {
+ var onInitCmd *externalcmd.Cmd
+
+ if params.Conf.RunOnInit != "" {
+ params.Logger.Log(logger.Info, "runOnInit command started")
+ onInitCmd = externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.Conf.RunOnInit,
+ params.Conf.RunOnInitRestart,
+ params.ExternalCmdEnv,
+ func(err error) {
+ params.Logger.Log(logger.Info, "runOnInit command exited: %v", err)
+ })
+ }
+
+ return func() {
+ if onInitCmd != nil {
+ onInitCmd.Close()
+ params.Logger.Log(logger.Info, "runOnInit command stopped")
+ }
+ }
+}
diff --git a/internal/hooks/on_read.go b/internal/hooks/on_read.go
new file mode 100644
index 00000000000..ee27206636a
--- /dev/null
+++ b/internal/hooks/on_read.go
@@ -0,0 +1,61 @@
+package hooks
+
+import (
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/externalcmd"
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// OnReadParams are the parameters of OnRead.
+type OnReadParams struct {
+ Logger logger.Writer
+ ExternalCmdPool *externalcmd.Pool
+ Conf *conf.Path
+ ExternalCmdEnv externalcmd.Environment
+ Reader defs.APIPathSourceOrReader
+ Query string
+}
+
+// OnRead is the OnRead hook.
+func OnRead(params OnReadParams) func() {
+ var env externalcmd.Environment
+ var onReadCmd *externalcmd.Cmd
+
+ if params.Conf.RunOnRead != "" || params.Conf.RunOnUnread != "" {
+ env = params.ExternalCmdEnv
+ desc := params.Reader
+ env["MTX_QUERY"] = params.Query
+ env["MTX_READER_TYPE"] = desc.Type
+ env["MTX_READER_ID"] = desc.ID
+ }
+
+ if params.Conf.RunOnRead != "" {
+ params.Logger.Log(logger.Info, "runOnRead command started")
+ onReadCmd = externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.Conf.RunOnRead,
+ params.Conf.RunOnReadRestart,
+ env,
+ func(err error) {
+ params.Logger.Log(logger.Info, "runOnRead command exited: %v", err)
+ })
+ }
+
+ return func() {
+ if onReadCmd != nil {
+ onReadCmd.Close()
+ params.Logger.Log(logger.Info, "runOnRead command stopped")
+ }
+
+ if params.Conf.RunOnUnread != "" {
+ params.Logger.Log(logger.Info, "runOnUnread command launched")
+ externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.Conf.RunOnUnread,
+ false,
+ env,
+ nil)
+ }
+ }
+}
diff --git a/internal/hooks/on_ready.go b/internal/hooks/on_ready.go
new file mode 100644
index 00000000000..f6e7e9fda49
--- /dev/null
+++ b/internal/hooks/on_ready.go
@@ -0,0 +1,60 @@
+package hooks
+
+import (
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/externalcmd"
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// OnReadyParams are the parameters of OnReady.
+type OnReadyParams struct {
+ Logger logger.Writer
+ ExternalCmdPool *externalcmd.Pool
+ Conf *conf.Path
+ ExternalCmdEnv externalcmd.Environment
+ Desc defs.APIPathSourceOrReader
+ Query string
+}
+
+// OnReady is the OnReady hook.
+func OnReady(params OnReadyParams) func() {
+ var env externalcmd.Environment
+ var onReadyCmd *externalcmd.Cmd
+
+ if params.Conf.RunOnReady != "" || params.Conf.RunOnNotReady != "" {
+ env = params.ExternalCmdEnv
+ env["MTX_QUERY"] = params.Query
+ env["MTX_SOURCE_TYPE"] = params.Desc.Type
+ env["MTX_SOURCE_ID"] = params.Desc.ID
+ }
+
+ if params.Conf.RunOnReady != "" {
+ params.Logger.Log(logger.Info, "runOnReady command started")
+ onReadyCmd = externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.Conf.RunOnReady,
+ params.Conf.RunOnReadyRestart,
+ env,
+ func(err error) {
+ params.Logger.Log(logger.Info, "runOnReady command exited: %v", err)
+ })
+ }
+
+ return func() {
+ if onReadyCmd != nil {
+ onReadyCmd.Close()
+ params.Logger.Log(logger.Info, "runOnReady command stopped")
+ }
+
+ if params.Conf.RunOnNotReady != "" {
+ params.Logger.Log(logger.Info, "runOnNotReady command launched")
+ externalcmd.NewCmd(
+ params.ExternalCmdPool,
+ params.Conf.RunOnNotReady,
+ false,
+ env,
+ nil)
+ }
+ }
+}
diff --git a/internal/core/metrics.go b/internal/metrics/metrics.go
similarity index 57%
rename from internal/core/metrics.go
rename to internal/metrics/metrics.go
index a05fe2184dd..b495d612262 100644
--- a/internal/core/metrics.go
+++ b/internal/metrics/metrics.go
@@ -1,19 +1,27 @@
-package core
+// Package metrics contains the metrics provider.
+package metrics
import (
"io"
"net/http"
+ "reflect"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
+ "github.com/bluenviron/mediamtx/internal/api"
"github.com/bluenviron/mediamtx/internal/conf"
- "github.com/bluenviron/mediamtx/internal/httpserv"
"github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
+ "github.com/bluenviron/mediamtx/internal/restrictnetwork"
)
+func interfaceIsEmpty(i interface{}) bool {
+ return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil()
+}
+
func metric(key string, tags string, value int64) string {
return key + tags + " " + strconv.FormatInt(value, 10) + "\n"
}
@@ -22,71 +30,71 @@ type metricsParent interface {
logger.Writer
}
-type metrics struct {
- parent metricsParent
-
- httpServer *httpserv.WrappedServer
- mutex sync.Mutex
- pathManager apiPathManager
- rtspServer apiRTSPServer
- rtspsServer apiRTSPServer
- rtmpServer apiRTMPServer
- hlsManager apiHLSManager
- webRTCManager apiWebRTCManager
+// Metrics is a metrics provider.
+type Metrics struct {
+ Address string
+ ReadTimeout conf.StringDuration
+ Parent metricsParent
+
+ httpServer *httpserv.WrappedServer
+ mutex sync.Mutex
+ pathManager api.PathManager
+ rtspServer api.RTSPServer
+ rtspsServer api.RTSPServer
+ rtmpServer api.RTMPServer
+ rtmpsServer api.RTMPServer
+ srtServer api.SRTServer
+ hlsManager api.HLSServer
+ webRTCServer api.WebRTCServer
}
-func newMetrics(
- address string,
- readTimeout conf.StringDuration,
- parent metricsParent,
-) (*metrics, error) {
- m := &metrics{
- parent: parent,
- }
-
+// Initialize initializes metrics.
+func (m *Metrics) Initialize() error {
router := gin.New()
router.SetTrustedProxies(nil) //nolint:errcheck
router.GET("/metrics", m.onMetrics)
- network, address := restrictNetwork("tcp", address)
+ network, address := restrictnetwork.Restrict("tcp", m.Address)
var err error
m.httpServer, err = httpserv.NewWrappedServer(
network,
address,
- time.Duration(readTimeout),
+ time.Duration(m.ReadTimeout),
"",
"",
router,
m,
)
if err != nil {
- return nil, err
+ return err
}
m.Log(logger.Info, "listener opened on "+address)
- return m, nil
+ return nil
}
-func (m *metrics) close() {
+// Close closes Metrics.
+func (m *Metrics) Close() {
m.Log(logger.Info, "listener is closing")
m.httpServer.Close()
}
-func (m *metrics) Log(level logger.Level, format string, args ...interface{}) {
- m.parent.Log(level, "[metrics] "+format, args...)
+// Log implements logger.Writer.
+func (m *Metrics) Log(level logger.Level, format string, args ...interface{}) {
+ m.Parent.Log(level, "[metrics] "+format, args...)
}
-func (m *metrics) onMetrics(ctx *gin.Context) {
+func (m *Metrics) onMetrics(ctx *gin.Context) {
out := ""
- data, err := m.pathManager.apiPathsList()
+ data, err := m.pathManager.APIPathsList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
var state string
- if i.SourceReady {
+ if i.Ready {
state = "ready"
} else {
state = "notReady"
@@ -95,13 +103,14 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
tags := "{name=\"" + i.Name + "\",state=\"" + state + "\"}"
out += metric("paths", tags, 1)
out += metric("paths_bytes_received", tags, int64(i.BytesReceived))
+ out += metric("paths_bytes_sent", tags, int64(i.BytesSent))
}
} else {
out += metric("paths", "", 0)
}
if !interfaceIsEmpty(m.hlsManager) {
- data, err := m.hlsManager.apiMuxersList()
+ data, err := m.hlsManager.APIMuxersList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
tags := "{name=\"" + i.Path + "\"}"
@@ -116,7 +125,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
if !interfaceIsEmpty(m.rtspServer) { //nolint:dupl
func() {
- data, err := m.rtspServer.apiConnsList()
+ data, err := m.rtspServer.APIConnsList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\"}"
@@ -132,7 +141,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
}()
func() {
- data, err := m.rtspServer.apiSessionsList()
+ data, err := m.rtspServer.APISessionsList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@@ -150,7 +159,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
if !interfaceIsEmpty(m.rtspsServer) { //nolint:dupl
func() {
- data, err := m.rtspsServer.apiConnsList()
+ data, err := m.rtspsServer.APIConnsList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\"}"
@@ -166,7 +175,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
}()
func() {
- data, err := m.rtspsServer.apiSessionsList()
+ data, err := m.rtspsServer.APISessionsList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@@ -183,7 +192,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
}
if !interfaceIsEmpty(m.rtmpServer) {
- data, err := m.rtmpServer.apiConnsList()
+ data, err := m.rtmpServer.APIConnsList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
@@ -198,11 +207,43 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
}
}
- if !interfaceIsEmpty(m.webRTCManager) {
- data, err := m.webRTCManager.apiSessionsList()
+ if !interfaceIsEmpty(m.rtmpsServer) {
+ data, err := m.rtmpsServer.APIConnsList()
if err == nil && len(data.Items) != 0 {
for _, i := range data.Items {
- tags := "{id=\"" + i.ID.String() + "\"}"
+ tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
+ out += metric("rtmps_conns", tags, 1)
+ out += metric("rtmps_conns_bytes_received", tags, int64(i.BytesReceived))
+ out += metric("rtmps_conns_bytes_sent", tags, int64(i.BytesSent))
+ }
+ } else {
+ out += metric("rtmps_conns", "", 0)
+ out += metric("rtmps_conns_bytes_received", "", 0)
+ out += metric("rtmps_conns_bytes_sent", "", 0)
+ }
+ }
+
+ if !interfaceIsEmpty(m.srtServer) {
+ data, err := m.srtServer.APIConnsList()
+ if err == nil && len(data.Items) != 0 {
+ for _, i := range data.Items {
+ tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
+ out += metric("srt_conns", tags, 1)
+ out += metric("srt_conns_bytes_received", tags, int64(i.BytesReceived))
+ out += metric("srt_conns_bytes_sent", tags, int64(i.BytesSent))
+ }
+ } else {
+ out += metric("srt_conns", "", 0)
+ out += metric("srt_conns_bytes_received", "", 0)
+ out += metric("srt_conns_bytes_sent", "", 0)
+ }
+ }
+
+ if !interfaceIsEmpty(m.webRTCServer) {
+ data, err := m.webRTCServer.APISessionsList()
+ if err == nil && len(data.Items) != 0 {
+ for _, i := range data.Items {
+ tags := "{id=\"" + i.ID.String() + "\",state=\"" + string(i.State) + "\"}"
out += metric("webrtc_sessions", tags, 1)
out += metric("webrtc_sessions_bytes_received", tags, int64(i.BytesReceived))
out += metric("webrtc_sessions_bytes_sent", tags, int64(i.BytesSent))
@@ -218,44 +259,58 @@ func (m *metrics) onMetrics(ctx *gin.Context) {
io.WriteString(ctx.Writer, out) //nolint:errcheck
}
-// pathManagerSet is called by pathManager.
-func (m *metrics) pathManagerSet(s apiPathManager) {
+// SetPathManager is called by core.
+func (m *Metrics) SetPathManager(s api.PathManager) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.pathManager = s
}
-// setHLSManager is called by hlsManager.
-func (m *metrics) setHLSManager(s apiHLSManager) {
+// SetHLSServer is called by core.
+func (m *Metrics) SetHLSServer(s api.HLSServer) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.hlsManager = s
}
-// setRTSPServer is called by rtspServer (plain).
-func (m *metrics) setRTSPServer(s apiRTSPServer) {
+// SetRTSPServer is called by core.
+func (m *Metrics) SetRTSPServer(s api.RTSPServer) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.rtspServer = s
}
-// setRTSPSServer is called by rtspServer (tls).
-func (m *metrics) setRTSPSServer(s apiRTSPServer) {
+// SetRTSPSServer is called by core.
+func (m *Metrics) SetRTSPSServer(s api.RTSPServer) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.rtspsServer = s
}
-// rtmpServerSet is called by rtmpServer.
-func (m *metrics) rtmpServerSet(s apiRTMPServer) {
+// SetRTMPServer is called by core.
+func (m *Metrics) SetRTMPServer(s api.RTMPServer) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.rtmpServer = s
}
-// webRTCManagerSet is called by webRTCManager.
-func (m *metrics) webRTCManagerSet(s apiWebRTCManager) {
+// SetRTMPSServer is called by core.
+func (m *Metrics) SetRTMPSServer(s api.RTMPServer) {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+ m.rtmpsServer = s
+}
+
+// SetSRTServer is called by core.
+func (m *Metrics) SetSRTServer(s api.SRTServer) {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+ m.srtServer = s
+}
+
+// SetWebRTCServer is called by core.
+func (m *Metrics) SetWebRTCServer(s api.WebRTCServer) {
m.mutex.Lock()
defer m.mutex.Unlock()
- m.webRTCManager = s
+ m.webRTCServer = s
}
diff --git a/internal/playback/fmp4.go b/internal/playback/fmp4.go
new file mode 100644
index 00000000000..a19e41de4e2
--- /dev/null
+++ b/internal/playback/fmp4.go
@@ -0,0 +1,410 @@
+package playback
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "github.com/abema/go-mp4"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
+)
+
+const (
+ sampleFlagIsNonSyncSample = 1 << 16
+)
+
+func durationGoToMp4(v time.Duration, timeScale uint32) uint64 {
+ timeScale64 := uint64(timeScale)
+ secs := v / time.Second
+ dec := v % time.Second
+ return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second)
+}
+
+func durationMp4ToGo(v uint64, timeScale uint32) time.Duration {
+ timeScale64 := uint64(timeScale)
+ secs := v / timeScale64
+ dec := v % timeScale64
+ return time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64)
+}
+
+var errTerminated = errors.New("terminated")
+
+func fmp4ReadInit(r io.ReadSeeker) ([]byte, error) {
+ buf := make([]byte, 8)
+ _, err := io.ReadFull(r, buf)
+ if err != nil {
+ return nil, err
+ }
+
+ if !bytes.Equal(buf[4:], []byte{'f', 't', 'y', 'p'}) {
+ return nil, fmt.Errorf("ftyp box not found")
+ }
+
+ ftypSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])
+
+ _, err = r.Seek(int64(ftypSize), io.SeekStart)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = io.ReadFull(r, buf)
+ if err != nil {
+ return nil, err
+ }
+
+ if !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'v'}) {
+ return nil, fmt.Errorf("moov box not found")
+ }
+
+ moovSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])
+
+ _, err = r.Seek(0, io.SeekStart)
+ if err != nil {
+ return nil, err
+ }
+
+ buf = make([]byte, ftypSize+moovSize)
+
+ _, err = io.ReadFull(r, buf)
+ if err != nil {
+ return nil, err
+ }
+
+ return buf, nil
+}
+
+func seekAndMuxParts(
+ r io.ReadSeeker,
+ init []byte,
+ minTime time.Duration,
+ maxTime time.Duration,
+ w io.Writer,
+) (time.Duration, error) {
+ minTimeMP4 := durationGoToMp4(minTime, 90000)
+ maxTimeMP4 := durationGoToMp4(maxTime, 90000)
+ moofOffset := uint64(0)
+ var tfhd *mp4.Tfhd
+ var tfdt *mp4.Tfdt
+ var outPart *fmp4.Part
+ var outTrack *fmp4.PartTrack
+ var outBuf seekablebuffer.Buffer
+ elapsed := uint64(0)
+ initWritten := false
+ firstSampleWritten := make(map[uint32]struct{})
+ gop := make(map[uint32][]*fmp4.PartSample)
+
+ _, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
+ switch h.BoxInfo.Type.String() {
+ case "moof":
+ moofOffset = h.BoxInfo.Offset
+ outPart = &fmp4.Part{}
+ return h.Expand()
+
+ case "traf":
+ return h.Expand()
+
+ case "tfhd":
+ box, _, err := h.ReadPayload()
+ if err != nil {
+ return nil, err
+ }
+ tfhd = box.(*mp4.Tfhd)
+
+ case "tfdt":
+ box, _, err := h.ReadPayload()
+ if err != nil {
+ return nil, err
+ }
+ tfdt = box.(*mp4.Tfdt)
+
+ if tfdt.BaseMediaDecodeTimeV1 >= maxTimeMP4 {
+ return nil, errTerminated
+ }
+
+ outTrack = &fmp4.PartTrack{ID: int(tfhd.TrackID)}
+
+ case "trun":
+ box, _, err := h.ReadPayload()
+ if err != nil {
+ return nil, err
+ }
+ trun := box.(*mp4.Trun)
+
+ dataOffset := moofOffset + uint64(trun.DataOffset)
+
+ _, err = r.Seek(int64(dataOffset), io.SeekStart)
+ if err != nil {
+ return nil, err
+ }
+
+ elapsed = tfdt.BaseMediaDecodeTimeV1
+ baseTimeSet := false
+
+ for _, e := range trun.Entries {
+ payload := make([]byte, e.SampleSize)
+ _, err := io.ReadFull(r, payload)
+ if err != nil {
+ return nil, err
+ }
+
+ if elapsed >= maxTimeMP4 {
+ break
+ }
+
+ isRandom := (e.SampleFlags & sampleFlagIsNonSyncSample) == 0
+ _, fsw := firstSampleWritten[tfhd.TrackID]
+
+ sa := &fmp4.PartSample{
+ Duration: e.SampleDuration,
+ PTSOffset: e.SampleCompositionTimeOffsetV1,
+ IsNonSyncSample: !isRandom,
+ Payload: payload,
+ }
+
+ if !fsw {
+ if isRandom {
+ gop[tfhd.TrackID] = []*fmp4.PartSample{sa}
+ } else {
+ gop[tfhd.TrackID] = append(gop[tfhd.TrackID], sa)
+ }
+ }
+
+ if elapsed >= minTimeMP4 {
+ if !baseTimeSet {
+ outTrack.BaseTime = elapsed - minTimeMP4
+
+ if !fsw {
+ if !isRandom {
+ for _, sa2 := range gop[tfhd.TrackID][:len(gop[tfhd.TrackID])-1] {
+ sa2.Duration = 0
+ sa2.PTSOffset = 0
+ outTrack.Samples = append(outTrack.Samples, sa2)
+ }
+ }
+
+ delete(gop, tfhd.TrackID)
+ firstSampleWritten[tfhd.TrackID] = struct{}{}
+ }
+ }
+
+ outTrack.Samples = append(outTrack.Samples, sa)
+ }
+
+ elapsed += uint64(e.SampleDuration)
+ }
+
+ if outTrack.Samples != nil {
+ outPart.Tracks = append(outPart.Tracks, outTrack)
+ }
+
+ outTrack = nil
+
+ case "mdat":
+ if outPart.Tracks != nil {
+ if !initWritten {
+ initWritten = true
+ _, err := w.Write(init)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ err := outPart.Marshal(&outBuf)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = w.Write(outBuf.Bytes())
+ if err != nil {
+ return nil, err
+ }
+
+ outBuf.Reset()
+ }
+
+ outPart = nil
+ }
+ return nil, nil
+ })
+ if err != nil && !errors.Is(err, errTerminated) {
+ return 0, err
+ }
+
+ if !initWritten {
+ return 0, errNoSegmentsFound
+ }
+
+ elapsed -= minTimeMP4
+
+ return durationMp4ToGo(elapsed, 90000), nil
+}
+
+func muxParts(
+ r io.ReadSeeker,
+ startTime time.Duration,
+ maxTime time.Duration,
+ w io.Writer,
+) (time.Duration, error) {
+ maxTimeMP4 := durationGoToMp4(maxTime, 90000)
+ moofOffset := uint64(0)
+ var tfhd *mp4.Tfhd
+ var tfdt *mp4.Tfdt
+ var outPart *fmp4.Part
+ var outTrack *fmp4.PartTrack
+ var outBuf seekablebuffer.Buffer
+ elapsed := uint64(0)
+
+ _, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
+ switch h.BoxInfo.Type.String() {
+ case "moof":
+ moofOffset = h.BoxInfo.Offset
+ outPart = &fmp4.Part{}
+ return h.Expand()
+
+ case "traf":
+ return h.Expand()
+
+ case "tfhd":
+ box, _, err := h.ReadPayload()
+ if err != nil {
+ return nil, err
+ }
+ tfhd = box.(*mp4.Tfhd)
+
+ case "tfdt":
+ box, _, err := h.ReadPayload()
+ if err != nil {
+ return nil, err
+ }
+ tfdt = box.(*mp4.Tfdt)
+
+ if tfdt.BaseMediaDecodeTimeV1 >= maxTimeMP4 {
+ return nil, errTerminated
+ }
+
+ outTrack = &fmp4.PartTrack{
+ ID: int(tfhd.TrackID),
+ BaseTime: tfdt.BaseMediaDecodeTimeV1 + durationGoToMp4(startTime, 90000),
+ }
+
+ case "trun":
+ box, _, err := h.ReadPayload()
+ if err != nil {
+ return nil, err
+ }
+ trun := box.(*mp4.Trun)
+
+ dataOffset := moofOffset + uint64(trun.DataOffset)
+
+ _, err = r.Seek(int64(dataOffset), io.SeekStart)
+ if err != nil {
+ return nil, err
+ }
+
+ elapsed = tfdt.BaseMediaDecodeTimeV1
+
+ for _, e := range trun.Entries {
+ payload := make([]byte, e.SampleSize)
+ _, err := io.ReadFull(r, payload)
+ if err != nil {
+ return nil, err
+ }
+
+ if elapsed >= maxTimeMP4 {
+ break
+ }
+
+ isRandom := (e.SampleFlags & sampleFlagIsNonSyncSample) == 0
+
+ sa := &fmp4.PartSample{
+ Duration: e.SampleDuration,
+ PTSOffset: e.SampleCompositionTimeOffsetV1,
+ IsNonSyncSample: !isRandom,
+ Payload: payload,
+ }
+
+ outTrack.Samples = append(outTrack.Samples, sa)
+
+ elapsed += uint64(e.SampleDuration)
+ }
+
+ if outTrack.Samples != nil {
+ outPart.Tracks = append(outPart.Tracks, outTrack)
+ }
+
+ outTrack = nil
+
+ case "mdat":
+ if outPart.Tracks != nil {
+ err := outPart.Marshal(&outBuf)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = w.Write(outBuf.Bytes())
+ if err != nil {
+ return nil, err
+ }
+
+ outBuf.Reset()
+ }
+
+ outPart = nil
+ }
+ return nil, nil
+ })
+ if err != nil && !errors.Is(err, errTerminated) {
+ return 0, err
+ }
+
+ return durationMp4ToGo(elapsed, 90000), nil
+}
+
+func fmp4SeekAndMux(
+ fpath string,
+ minTime time.Duration,
+ maxTime time.Duration,
+ w io.Writer,
+) (time.Duration, error) {
+ f, err := os.Open(fpath)
+ if err != nil {
+ return 0, err
+ }
+ defer f.Close()
+
+ init, err := fmp4ReadInit(f)
+ if err != nil {
+ return 0, err
+ }
+
+ elapsed, err := seekAndMuxParts(f, init, minTime, maxTime, w)
+ if err != nil {
+ return 0, err
+ }
+
+ return elapsed, nil
+}
+
+func fmp4Mux(
+ fpath string,
+ startTime time.Duration,
+ maxTime time.Duration,
+ w io.Writer,
+) (time.Duration, error) {
+ f, err := os.Open(fpath)
+ if err != nil {
+ return 0, err
+ }
+ defer f.Close()
+
+ elapsed, err := muxParts(f, startTime, maxTime, w)
+ if err != nil {
+ return 0, err
+ }
+
+ return elapsed, nil
+}
diff --git a/internal/playback/fmp4_test.go b/internal/playback/fmp4_test.go
new file mode 100644
index 00000000000..1df5ef195a9
--- /dev/null
+++ b/internal/playback/fmp4_test.go
@@ -0,0 +1,79 @@
+package playback
+
+import (
+ "io"
+ "os"
+ "testing"
+
+ "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+)
+
+func writeBenchInit(f io.WriteSeeker) {
+ init := fmp4.Init{
+ Tracks: []*fmp4.InitTrack{
+ {
+ ID: 1,
+ TimeScale: 90000,
+ Codec: &fmp4.CodecH264{
+ SPS: []byte{
+ 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,
+ },
+ PPS: []byte{0x08},
+ },
+ },
+ {
+ ID: 2,
+ TimeScale: 90000,
+ Codec: &fmp4.CodecMPEG4Audio{
+ Config: mpeg4audio.Config{
+ Type: mpeg4audio.ObjectTypeAACLC,
+ SampleRate: 48000,
+ ChannelCount: 2,
+ },
+ },
+ },
+ },
+ }
+
+ err := init.Marshal(f)
+ if err != nil {
+ panic(err)
+ }
+
+ _, err = f.Write([]byte{
+ 'm', 'o', 'o', 'f', 0x00, 0x00, 0x00, 0x10,
+ })
+ if err != nil {
+ panic(err)
+ }
+}
+
+func BenchmarkFMP4ReadInit(b *testing.B) {
+ f, err := os.CreateTemp(os.TempDir(), "mediamtx-playback-fmp4-")
+ if err != nil {
+ panic(err)
+ }
+ defer os.Remove(f.Name())
+
+ writeBenchInit(f)
+ f.Close()
+
+ for n := 0; n < b.N; n++ {
+ func() {
+ f, err := os.Open(f.Name())
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+
+ _, err = fmp4ReadInit(f)
+ if err != nil {
+ panic(err)
+ }
+ }()
+ }
+}
diff --git a/internal/playback/server.go b/internal/playback/server.go
new file mode 100644
index 00000000000..7aee0a468b9
--- /dev/null
+++ b/internal/playback/server.go
@@ -0,0 +1,299 @@
+// Package playback contains the playback server.
+package playback
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "net"
+ "net/http"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
+ "github.com/bluenviron/mediamtx/internal/record"
+ "github.com/bluenviron/mediamtx/internal/restrictnetwork"
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ concatenationTolerance = 1 * time.Second
+)
+
+var errNoSegmentsFound = errors.New("no recording segments found for the given timestamp")
+
+type writerWrapper struct {
+ ctx *gin.Context
+ written bool
+}
+
+func (w *writerWrapper) Write(p []byte) (int, error) {
+ if !w.written {
+ w.written = true
+ w.ctx.Header("Accept-Ranges", "none")
+ w.ctx.Header("Content-Type", "video/mp4")
+ }
+ return w.ctx.Writer.Write(p)
+}
+
+type segment struct {
+ fpath string
+ start time.Time
+}
+
+func findSegments(
+ pathConf *conf.Path,
+ pathName string,
+ start time.Time,
+ duration time.Duration,
+) ([]segment, error) {
+ if !pathConf.Playback {
+ return nil, fmt.Errorf("playback is disabled on path '%s'", pathName)
+ }
+
+ recordPath := record.PathAddExtension(
+ strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
+ pathConf.RecordFormat,
+ )
+
+ // we have to convert to absolute paths
+ // otherwise, recordPath and fpath inside Walk() won't have common elements
+ recordPath, _ = filepath.Abs(recordPath)
+
+ commonPath := record.CommonPath(recordPath)
+ end := start.Add(duration)
+ var segments []segment
+
+ // gather all segments that starts before the end of the playback
+ err := filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if !info.IsDir() {
+ var pa record.Path
+ ok := pa.Decode(recordPath, fpath)
+ if ok && !end.Before(time.Time(pa)) {
+ segments = append(segments, segment{
+ fpath: fpath,
+ start: time.Time(pa),
+ })
+ }
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if segments == nil {
+ return nil, errNoSegmentsFound
+ }
+
+ sort.Slice(segments, func(i, j int) bool {
+ return segments[i].start.Before(segments[j].start)
+ })
+
+ // find the segment that may contain the start of the playback and remove all previous ones
+ found := false
+ for i := 0; i < len(segments)-1; i++ {
+ if !start.Before(segments[i].start) && start.Before(segments[i+1].start) {
+ segments = segments[i:]
+ found = true
+ break
+ }
+ }
+
+ // otherwise, keep the last segment only and check whether it may contain the start of the playback
+ if !found {
+ segments = segments[len(segments)-1:]
+ if segments[len(segments)-1].start.After(start) {
+ return nil, errNoSegmentsFound
+ }
+ }
+
+ return segments, nil
+}
+
+// Server is the playback server.
+type Server struct {
+ Address string
+ ReadTimeout conf.StringDuration
+ PathConfs map[string]*conf.Path
+ Parent logger.Writer
+
+ httpServer *httpserv.WrappedServer
+ mutex sync.RWMutex
+}
+
+// Initialize initializes API.
+func (p *Server) Initialize() error {
+ router := gin.New()
+ router.SetTrustedProxies(nil) //nolint:errcheck
+
+ group := router.Group("/")
+
+ group.GET("/get", p.onGet)
+
+ network, address := restrictnetwork.Restrict("tcp", p.Address)
+
+ var err error
+ p.httpServer, err = httpserv.NewWrappedServer(
+ network,
+ address,
+ time.Duration(p.ReadTimeout),
+ "",
+ "",
+ router,
+ p,
+ )
+ if err != nil {
+ return err
+ }
+
+ p.Log(logger.Info, "listener opened on "+address)
+
+ return nil
+}
+
+// Close closes Server.
+func (p *Server) Close() {
+ p.Log(logger.Info, "listener is closing")
+ p.httpServer.Close()
+}
+
+// Log implements logger.Writer.
+func (p *Server) Log(level logger.Level, format string, args ...interface{}) {
+ p.Parent.Log(level, "[playback] "+format, args...)
+}
+
+// ReloadPathConfs is called by core.Core.
+func (p *Server) ReloadPathConfs(pathConfs map[string]*conf.Path) {
+ p.mutex.Lock()
+ defer p.mutex.Unlock()
+ p.PathConfs = pathConfs
+}
+
+func (p *Server) writeError(ctx *gin.Context, status int, err error) {
+ // show error in logs
+ p.Log(logger.Error, err.Error())
+
+ // add error to response
+ ctx.String(status, err.Error())
+}
+
+func (p *Server) safeFindPathConf(name string) (*conf.Path, error) {
+ p.mutex.RLock()
+ defer p.mutex.RUnlock()
+
+ _, pathConf, _, err := conf.FindPathConf(p.PathConfs, name)
+ return pathConf, err
+}
+
+func (p *Server) onGet(ctx *gin.Context) {
+ pathName := ctx.Query("path")
+
+ start, err := time.Parse(time.RFC3339, ctx.Query("start"))
+ if err != nil {
+ p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid start: %w", err))
+ return
+ }
+
+ duration, err := time.ParseDuration(ctx.Query("duration"))
+ if err != nil {
+ p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid duration: %w", err))
+ return
+ }
+
+ format := ctx.Query("format")
+ if format != "fmp4" {
+ p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid format: %s", format))
+ return
+ }
+
+ pathConf, err := p.safeFindPathConf(pathName)
+ if err != nil {
+ p.writeError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ segments, err := findSegments(pathConf, pathName, start, duration)
+ if err != nil {
+ if errors.Is(err, errNoSegmentsFound) {
+ p.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ p.writeError(ctx, http.StatusBadRequest, err)
+ }
+ return
+ }
+
+ if pathConf.RecordFormat != conf.RecordFormatFMP4 {
+ p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("format of recording segments is not fmp4"))
+ return
+ }
+
+ ww := &writerWrapper{ctx: ctx}
+ minTime := start.Sub(segments[0].start)
+ maxTime := minTime + duration
+
+ elapsed, err := fmp4SeekAndMux(
+ segments[0].fpath,
+ minTime,
+ maxTime,
+ ww)
+ if err != nil {
+ // user aborted the download
+ var neterr *net.OpError
+ if errors.As(err, &neterr) {
+ return
+ }
+
+ // nothing has been written yet; send back JSON
+ if !ww.written {
+ if errors.Is(err, errNoSegmentsFound) {
+ p.writeError(ctx, http.StatusNotFound, err)
+ } else {
+ p.writeError(ctx, http.StatusBadRequest, err)
+ }
+ return
+ }
+
+ // something has been already written: abort and write to logs only
+ p.Log(logger.Error, err.Error())
+ return
+ }
+
+ start = start.Add(elapsed)
+ duration -= elapsed
+ overallElapsed := elapsed
+
+ for _, seg := range segments[1:] {
+ // there's a gap between segments; stop serving the recording.
+ if seg.start.Before(start.Add(-concatenationTolerance)) || seg.start.After(start.Add(concatenationTolerance)) {
+ return
+ }
+
+ elapsed, err := fmp4Mux(seg.fpath, overallElapsed, duration, ctx.Writer)
+ if err != nil {
+ // user aborted the download
+ var neterr *net.OpError
+ if errors.As(err, &neterr) {
+ return
+ }
+
+ // something has been already written: abort and write to logs only
+ p.Log(logger.Error, err.Error())
+ return
+ }
+
+ start = seg.start.Add(elapsed)
+ duration -= elapsed
+ overallElapsed += elapsed
+ }
+}
diff --git a/internal/playback/server_test.go b/internal/playback/server_test.go
new file mode 100644
index 00000000000..3a26401f2d8
--- /dev/null
+++ b/internal/playback/server_test.go
@@ -0,0 +1,228 @@
+package playback
+
+import (
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/stretchr/testify/require"
+)
+
+type nilLogger struct{}
+
+func (nilLogger) Log(_ logger.Level, _ string, _ ...interface{}) {
+}
+
+func writeSegment1(t *testing.T, fpath string) {
+ init := fmp4.Init{
+ Tracks: []*fmp4.InitTrack{{
+ ID: 1,
+ TimeScale: 90000,
+ Codec: &fmp4.CodecH264{
+ SPS: []byte{
+ 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,
+ },
+ PPS: []byte{0x08},
+ },
+ }},
+ }
+
+ var buf1 seekablebuffer.Buffer
+ err := init.Marshal(&buf1)
+ require.NoError(t, err)
+
+ var buf2 seekablebuffer.Buffer
+ parts := fmp4.Parts{
+ {
+ SequenceNumber: 1,
+ Tracks: []*fmp4.PartTrack{{
+ ID: 1,
+ BaseTime: 0,
+ Samples: []*fmp4.PartSample{},
+ }},
+ },
+ {
+ SequenceNumber: 1,
+ Tracks: []*fmp4.PartTrack{{
+ ID: 1,
+ BaseTime: 30 * 90000,
+ Samples: []*fmp4.PartSample{
+ {
+ Duration: 30 * 90000,
+ IsNonSyncSample: false,
+ Payload: []byte{1, 2},
+ },
+ {
+ Duration: 1 * 90000,
+ IsNonSyncSample: false,
+ Payload: []byte{3, 4},
+ },
+ {
+ Duration: 1 * 90000,
+ IsNonSyncSample: true,
+ Payload: []byte{5, 6},
+ },
+ },
+ }},
+ },
+ }
+ err = parts.Marshal(&buf2)
+ require.NoError(t, err)
+
+ err = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)
+ require.NoError(t, err)
+}
+
+func writeSegment2(t *testing.T, fpath string) {
+ init := fmp4.Init{
+ Tracks: []*fmp4.InitTrack{{
+ ID: 1,
+ TimeScale: 90000,
+ Codec: &fmp4.CodecH264{
+ SPS: []byte{
+ 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,
+ },
+ PPS: []byte{0x08},
+ },
+ }},
+ }
+
+ var buf1 seekablebuffer.Buffer
+ err := init.Marshal(&buf1)
+ require.NoError(t, err)
+
+ var buf2 seekablebuffer.Buffer
+ parts := fmp4.Parts{
+ {
+ SequenceNumber: 1,
+ Tracks: []*fmp4.PartTrack{{
+ ID: 1,
+ BaseTime: 0,
+ Samples: []*fmp4.PartSample{
+ {
+ Duration: 1 * 90000,
+ IsNonSyncSample: false,
+ Payload: []byte{7, 8},
+ },
+ {
+ Duration: 1 * 90000,
+ IsNonSyncSample: false,
+ Payload: []byte{9, 10},
+ },
+ },
+ }},
+ },
+ }
+ err = parts.Marshal(&buf2)
+ require.NoError(t, err)
+
+ err = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)
+ require.NoError(t, err)
+}
+
+func TestServer(t *testing.T) {
+ dir, err := os.MkdirTemp("", "mediamtx-playback")
+ require.NoError(t, err)
+ defer os.RemoveAll(dir)
+
+ err = os.Mkdir(filepath.Join(dir, "mypath"), 0o755)
+ require.NoError(t, err)
+
+ writeSegment1(t, filepath.Join(dir, "mypath", "2008-11-07_11-22-00-000000.mp4"))
+ writeSegment2(t, filepath.Join(dir, "mypath", "2008-11-07_11-23-02-000000.mp4"))
+
+ s := &Server{
+ Address: "127.0.0.1:9996",
+ ReadTimeout: conf.StringDuration(10 * time.Second),
+ PathConfs: map[string]*conf.Path{
+ "mypath": {
+ Playback: true,
+ RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
+ },
+ },
+ Parent: &nilLogger{},
+ }
+ err = s.Initialize()
+ require.NoError(t, err)
+ defer s.Close()
+
+ v := url.Values{}
+ v.Set("path", "mypath")
+ v.Set("start", time.Date(2008, 11, 0o7, 11, 23, 1, 0, time.Local).Format(time.RFC3339))
+ v.Set("duration", "2s")
+ v.Set("format", "fmp4")
+
+ u := &url.URL{
+ Scheme: "http",
+ Host: "localhost:9996",
+ Path: "/get",
+ RawQuery: v.Encode(),
+ }
+
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+ require.NoError(t, err)
+
+ res, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+
+ require.Equal(t, http.StatusOK, res.StatusCode)
+
+ buf, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+
+ var parts fmp4.Parts
+ err = parts.Unmarshal(buf)
+ require.NoError(t, err)
+
+ require.Equal(t, fmp4.Parts{
+ {
+ SequenceNumber: 0,
+ Tracks: []*fmp4.PartTrack{
+ {
+ ID: 1,
+ Samples: []*fmp4.PartSample{
+ {
+ Duration: 0,
+ Payload: []byte{3, 4},
+ },
+ {
+ Duration: 90000,
+ IsNonSyncSample: true,
+ Payload: []byte{5, 6},
+ },
+ },
+ },
+ },
+ },
+ {
+ SequenceNumber: 0,
+ Tracks: []*fmp4.PartTrack{
+ {
+ ID: 1,
+ BaseTime: 90000,
+ Samples: []*fmp4.PartSample{
+ {
+ Duration: 90000,
+ Payload: []byte{7, 8},
+ },
+ },
+ },
+ },
+ },
+ }, parts)
+}
diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go
new file mode 100644
index 00000000000..63803fc9b9d
--- /dev/null
+++ b/internal/pprof/pprof.go
@@ -0,0 +1,62 @@
+// Package pprof contains a pprof exporter.
+package pprof
+
+import (
+ "net/http"
+ "time"
+
+ // start pprof
+ _ "net/http/pprof"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/bluenviron/mediamtx/internal/protocols/httpserv"
+ "github.com/bluenviron/mediamtx/internal/restrictnetwork"
+)
+
+type pprofParent interface {
+ logger.Writer
+}
+
+// PPROF is a pprof exporter.
+type PPROF struct {
+ Address string
+ ReadTimeout conf.StringDuration
+ Parent pprofParent
+
+ httpServer *httpserv.WrappedServer
+}
+
+// Initialize initializes PPROF.
+func (pp *PPROF) Initialize() error {
+ network, address := restrictnetwork.Restrict("tcp", pp.Address)
+
+ var err error
+ pp.httpServer, err = httpserv.NewWrappedServer(
+ network,
+ address,
+ time.Duration(pp.ReadTimeout),
+ "",
+ "",
+ http.DefaultServeMux,
+ pp,
+ )
+ if err != nil {
+ return err
+ }
+
+ pp.Log(logger.Info, "listener opened on "+address)
+
+ return nil
+}
+
+// Close closes PPROF.
+func (pp *PPROF) Close() {
+ pp.Log(logger.Info, "listener is closing")
+ pp.httpServer.Close()
+}
+
+// Log implements logger.Writer.
+func (pp *PPROF) Log(level logger.Level, format string, args ...interface{}) {
+ pp.Parent.Log(level, "[pprof] "+format, args...)
+}
diff --git a/internal/httpserv/handler_exit_on_panic.go b/internal/protocols/httpserv/handler_exit_on_panic.go
similarity index 100%
rename from internal/httpserv/handler_exit_on_panic.go
rename to internal/protocols/httpserv/handler_exit_on_panic.go
diff --git a/internal/httpserv/handler_filter_requests.go b/internal/protocols/httpserv/handler_filter_requests.go
similarity index 100%
rename from internal/httpserv/handler_filter_requests.go
rename to internal/protocols/httpserv/handler_filter_requests.go
diff --git a/internal/httpserv/handler_logger.go b/internal/protocols/httpserv/handler_logger.go
similarity index 100%
rename from internal/httpserv/handler_logger.go
rename to internal/protocols/httpserv/handler_logger.go
diff --git a/internal/httpserv/handler_server_header.go b/internal/protocols/httpserv/handler_server_header.go
similarity index 100%
rename from internal/httpserv/handler_server_header.go
rename to internal/protocols/httpserv/handler_server_header.go
diff --git a/internal/protocols/httpserv/location_with_trailing_slash.go b/internal/protocols/httpserv/location_with_trailing_slash.go
new file mode 100644
index 00000000000..339a4d5caca
--- /dev/null
+++ b/internal/protocols/httpserv/location_with_trailing_slash.go
@@ -0,0 +1,22 @@
+package httpserv
+
+import "net/url"
+
+// LocationWithTrailingSlash returns the URL in a relative format, with a trailing slash.
+func LocationWithTrailingSlash(u *url.URL) string {
+ l := "./"
+
+ for i := 1; i < len(u.Path); i++ {
+ if u.Path[i] == '/' {
+ l += "../"
+ }
+ }
+
+ l += u.Path[1:] + "/"
+
+ if u.RawQuery != "" {
+ l += "?" + u.RawQuery
+ }
+
+ return l
+}
diff --git a/internal/protocols/httpserv/location_with_trailing_slash_test.go b/internal/protocols/httpserv/location_with_trailing_slash_test.go
new file mode 100644
index 00000000000..e5d20c3ab33
--- /dev/null
+++ b/internal/protocols/httpserv/location_with_trailing_slash_test.go
@@ -0,0 +1,43 @@
+package httpserv
+
+import (
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestLocationWithTrailingSlash(t *testing.T) {
+ for _, ca := range []struct {
+ name string
+ url *url.URL
+ loc string
+ }{
+ {
+ "with query",
+ &url.URL{
+ Path: "/test",
+ RawQuery: "key=value",
+ },
+ "./test/?key=value",
+ },
+ {
+ "xss",
+ &url.URL{
+ Path: "/www.example.com",
+ },
+ "./www.example.com/",
+ },
+ {
+ "slashes in path",
+ &url.URL{
+ Path: "/my/path",
+ },
+ "./../my/path/",
+ },
+ } {
+ t.Run(ca.name, func(t *testing.T) {
+ require.Equal(t, ca.loc, LocationWithTrailingSlash(ca.url))
+ })
+ }
+}
diff --git a/internal/httpserv/wrapped_server.go b/internal/protocols/httpserv/wrapped_server.go
similarity index 94%
rename from internal/httpserv/wrapped_server.go
rename to internal/protocols/httpserv/wrapped_server.go
index 0974c1783f0..abc5e84c5cf 100644
--- a/internal/httpserv/wrapped_server.go
+++ b/internal/protocols/httpserv/wrapped_server.go
@@ -86,6 +86,8 @@ func NewWrappedServer(
// Close closes all resources and waits for all routines to return.
func (s *WrappedServer) Close() {
- s.inner.Shutdown(context.Background())
+ ctx, ctxCancel := context.WithCancel(context.Background())
+ ctxCancel()
+ s.inner.Shutdown(ctx)
s.ln.Close() // in case Shutdown() is called before Serve()
}
diff --git a/internal/httpserv/wrapped_server_test.go b/internal/protocols/httpserv/wrapped_server_test.go
similarity index 96%
rename from internal/httpserv/wrapped_server_test.go
rename to internal/protocols/httpserv/wrapped_server_test.go
index 02b04ff7339..1db5d4e99f8 100644
--- a/internal/httpserv/wrapped_server_test.go
+++ b/internal/protocols/httpserv/wrapped_server_test.go
@@ -14,7 +14,6 @@ import (
type testLogger struct{}
func (testLogger) Log(_ logger.Level, _ string, _ ...interface{}) {
- // fmt.Printf(format, args...)
}
func TestFilterEmptyPath(t *testing.T) {
diff --git a/internal/protocols/mpegts/from_stream.go b/internal/protocols/mpegts/from_stream.go
new file mode 100644
index 00000000000..b1a91344de5
--- /dev/null
+++ b/internal/protocols/mpegts/from_stream.go
@@ -0,0 +1,260 @@
+package mpegts
+
+import (
+ "bufio"
+ "fmt"
+ "time"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/mediacommon/pkg/codecs/ac3"
+ "github.com/bluenviron/mediacommon/pkg/codecs/h264"
+ "github.com/bluenviron/mediacommon/pkg/codecs/h265"
+ mcmpegts "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
+ srt "github.com/datarhei/gosrt"
+
+ "github.com/bluenviron/mediamtx/internal/asyncwriter"
+ "github.com/bluenviron/mediamtx/internal/stream"
+ "github.com/bluenviron/mediamtx/internal/unit"
+)
+
+func durationGoToMPEGTS(v time.Duration) int64 {
+ return int64(v.Seconds() * 90000)
+}
+
+// FromStream links a server stream to a MPEG-TS writer.
+func FromStream(
+ stream *stream.Stream,
+ writer *asyncwriter.Writer,
+ bw *bufio.Writer,
+ sconn srt.Conn,
+ writeTimeout time.Duration,
+) error {
+ var w *mcmpegts.Writer
+ var tracks []*mcmpegts.Track
+
+ addTrack := func(codec mcmpegts.Codec) *mcmpegts.Track {
+ track := &mcmpegts.Track{
+ Codec: codec,
+ }
+ tracks = append(tracks, track)
+ return track
+ }
+
+ for _, medi := range stream.Desc().Medias {
+ for _, forma := range medi.Formats {
+ switch forma := forma.(type) {
+ case *format.H265: //nolint:dupl
+ track := addTrack(&mcmpegts.CodecH265{})
+
+ var dtsExtractor *h265.DTSExtractor
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.H265)
+ if tunit.AU == nil {
+ return nil
+ }
+
+ randomAccess := h265.IsRandomAccess(tunit.AU)
+
+ if dtsExtractor == nil {
+ if !randomAccess {
+ return nil
+ }
+ dtsExtractor = h265.NewDTSExtractor()
+ }
+
+ dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
+ if err != nil {
+ return err
+ }
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU)
+ if err != nil {
+ return err
+ }
+ return bw.Flush()
+ })
+
+ case *format.H264: //nolint:dupl
+ track := addTrack(&mcmpegts.CodecH264{})
+
+ var dtsExtractor *h264.DTSExtractor
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.H264)
+ if tunit.AU == nil {
+ return nil
+ }
+
+ idrPresent := h264.IDRPresent(tunit.AU)
+
+ if dtsExtractor == nil {
+ if !idrPresent {
+ return nil
+ }
+ dtsExtractor = h264.NewDTSExtractor()
+ }
+
+ dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
+ if err != nil {
+ return err
+ }
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU)
+ if err != nil {
+ return err
+ }
+ return bw.Flush()
+ })
+
+ case *format.MPEG4Video:
+ track := addTrack(&mcmpegts.CodecMPEG4Video{})
+
+ firstReceived := false
+ var lastPTS time.Duration
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG4Video)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ if !firstReceived {
+ firstReceived = true
+ } else if tunit.PTS < lastPTS {
+ return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)")
+ }
+ lastPTS = tunit.PTS
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err := (*w).WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
+ if err != nil {
+ return err
+ }
+ return bw.Flush()
+ })
+
+ case *format.MPEG1Video:
+ track := addTrack(&mcmpegts.CodecMPEG1Video{})
+
+ firstReceived := false
+ var lastPTS time.Duration
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG1Video)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ if !firstReceived {
+ firstReceived = true
+ } else if tunit.PTS < lastPTS {
+ return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
+ }
+ lastPTS = tunit.PTS
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err := (*w).WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame)
+ if err != nil {
+ return err
+ }
+ return bw.Flush()
+ })
+
+ case *format.Opus:
+ track := addTrack(&mcmpegts.CodecOpus{
+ ChannelCount: func() int {
+ if forma.IsStereo {
+ return 2
+ }
+ return 1
+ }(),
+ })
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.Opus)
+ if tunit.Packets == nil {
+ return nil
+ }
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err := (*w).WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets)
+ if err != nil {
+ return err
+ }
+ return bw.Flush()
+ })
+
+ case *format.MPEG4Audio:
+ track := addTrack(&mcmpegts.CodecMPEG4Audio{
+ Config: *forma.GetConfig(),
+ })
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG4Audio)
+ if tunit.AUs == nil {
+ return nil
+ }
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err := (*w).WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs)
+ if err != nil {
+ return err
+ }
+ return bw.Flush()
+ })
+
+ case *format.MPEG1Audio:
+ track := addTrack(&mcmpegts.CodecMPEG1Audio{})
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG1Audio)
+ if tunit.Frames == nil {
+ return nil
+ }
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err := (*w).WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames)
+ if err != nil {
+ return err
+ }
+ return bw.Flush()
+ })
+
+ case *format.AC3:
+ track := addTrack(&mcmpegts.CodecAC3{})
+
+ sampleRate := time.Duration(forma.SampleRate)
+
+ stream.AddReader(writer, medi, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.AC3)
+ if tunit.Frames == nil {
+ return nil
+ }
+
+ for i, frame := range tunit.Frames {
+ framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame*
+ time.Second/sampleRate
+
+ sconn.SetWriteDeadline(time.Now().Add(writeTimeout))
+ err := (*w).WriteAC3(track, durationGoToMPEGTS(framePTS), frame)
+ if err != nil {
+ return err
+ }
+ }
+ return bw.Flush()
+ })
+ }
+ }
+ }
+
+ if len(tracks) == 0 {
+ return ErrNoTracks
+ }
+
+ w = mcmpegts.NewWriter(bw, tracks)
+
+ return nil
+}
diff --git a/internal/core/mpegts.go b/internal/protocols/mpegts/to_stream.go
similarity index 89%
rename from internal/core/mpegts.go
rename to internal/protocols/mpegts/to_stream.go
index 32182fa791a..4c4ff57fa7a 100644
--- a/internal/core/mpegts.go
+++ b/internal/protocols/mpegts/to_stream.go
@@ -1,7 +1,8 @@
-package core
+// Package mpegts contains MPEG-ts utilities.
+package mpegts
import (
- "fmt"
+ "errors"
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
@@ -12,7 +13,12 @@ import (
"github.com/bluenviron/mediamtx/internal/unit"
)
-func mpegtsSetupTracks(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, error) {
+// ErrNoTracks is returned when there are no supported tracks.
+var ErrNoTracks = errors.New("no supported tracks found (supported are H265, H264," +
+ " MPEG-4 Video, MPEG-1/2 Video, Opus, MPEG-4 Audio, MPEG-1 Audio, AC-3")
+
+// ToStream converts a MPEG-TS stream to a server stream.
+func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, error) {
var medias []*description.Media //nolint:prealloc
var td *mpegts.TimeDecoder
@@ -107,7 +113,7 @@ func mpegtsSetupTracks(r *mpegts.Reader, stream **stream.Stream) ([]*description
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
- IsStereo: (codec.ChannelCount == 2),
+ IsStereo: (codec.ChannelCount >= 2),
}},
}
@@ -191,7 +197,7 @@ func mpegtsSetupTracks(r *mpegts.Reader, stream **stream.Stream) ([]*description
}
if len(medias) == 0 {
- return nil, fmt.Errorf("no supported tracks found")
+ return nil, ErrNoTracks
}
return medias, nil
diff --git a/internal/rpicamera/exe/Makefile b/internal/protocols/rpicamera/exe/Makefile
similarity index 100%
rename from internal/rpicamera/exe/Makefile
rename to internal/protocols/rpicamera/exe/Makefile
diff --git a/internal/rpicamera/exe/base64.c b/internal/protocols/rpicamera/exe/base64.c
similarity index 100%
rename from internal/rpicamera/exe/base64.c
rename to internal/protocols/rpicamera/exe/base64.c
diff --git a/internal/rpicamera/exe/base64.h b/internal/protocols/rpicamera/exe/base64.h
similarity index 100%
rename from internal/rpicamera/exe/base64.h
rename to internal/protocols/rpicamera/exe/base64.h
diff --git a/internal/rpicamera/exe/camera.cpp b/internal/protocols/rpicamera/exe/camera.cpp
similarity index 90%
rename from internal/rpicamera/exe/camera.cpp
rename to internal/protocols/rpicamera/exe/camera.cpp
index e9586b46335..3ef8796069b 100644
--- a/internal/rpicamera/exe/camera.cpp
+++ b/internal/protocols/rpicamera/exe/camera.cpp
@@ -126,13 +126,23 @@ static void set_hdr(bool hdr) {
}
bool camera_create(const parameters_t *params, camera_frame_cb frame_cb, camera_t **cam) {
+ std::unique_ptr camp = std::make_unique();
+
set_hdr(params->hdr);
+ if (strcmp(params->log_level, "debug") == 0) {
+ setenv("LIBCAMERA_LOG_LEVELS", "*:DEBUG", 1);
+ } else if (strcmp(params->log_level, "info") == 0) {
+ setenv("LIBCAMERA_LOG_LEVELS", "*:INFO", 1);
+ } else if (strcmp(params->log_level, "warn") == 0) {
+ setenv("LIBCAMERA_LOG_LEVELS", "*:WARN", 1);
+ } else { // error
+ setenv("LIBCAMERA_LOG_LEVELS", "*:ERROR", 1);
+ }
+
// We make sure to set the environment variable before libcamera init
setenv("LIBCAMERA_RPI_TUNING_FILE", params->tuning_file, 1);
- std::unique_ptr camp = std::make_unique();
-
camp->camera_manager = std::make_unique();
int ret = camp->camera_manager->start();
if (ret != 0) {
@@ -378,6 +388,29 @@ bool camera_start(camera_t *cam) {
fill_dynamic_controls(camp->ctrls.get(), camp->params);
if (camp->camera->controls().count(&controls::AfMode) > 0) {
+ if (camp->params->af_window != NULL) {
+ std::optional opt = camp->camera->properties().get(properties::ScalerCropMaximum);
+ Rectangle sensor_area;
+ try {
+ sensor_area = opt.value();
+ } catch(const std::bad_optional_access& exc) {
+ set_error("get(ScalerCropMaximum) failed");
+ return false;
+ }
+
+ Rectangle afwindows_rectangle[1];
+
+ afwindows_rectangle[0] = Rectangle(
+ camp->params->af_window->x * sensor_area.width,
+ camp->params->af_window->y * sensor_area.height,
+ camp->params->af_window->width * sensor_area.width,
+ camp->params->af_window->height * sensor_area.height);
+
+ afwindows_rectangle[0].translateBy(sensor_area.topLeft());
+ camp->ctrls->set(controls::AfMetering, controls::AfMeteringWindows);
+ camp->ctrls->set(controls::AfWindows, afwindows_rectangle);
+ }
+
int af_mode;
if (strcmp(camp->params->af_mode, "manual") == 0) {
af_mode = controls::AfModeManual;
@@ -388,12 +421,6 @@ bool camera_start(camera_t *cam) {
}
camp->ctrls->set(controls::AfMode, af_mode);
- if (af_mode == controls::AfModeManual) {
- camp->ctrls->set(controls::LensPosition, camp->params->lens_position);
- }
- }
-
- if (camp->camera->controls().count(&controls::AfRange) > 0) {
int af_range;
if (strcmp(camp->params->af_range, "macro") == 0) {
af_range = controls::AfRangeMacro;
@@ -403,9 +430,7 @@ bool camera_start(camera_t *cam) {
af_range = controls::AfRangeNormal;
}
camp->ctrls->set(controls::AfRange, af_range);
- }
- if (camp->camera->controls().count(&controls::AfSpeed) > 0) {
int af_speed;
if (strcmp(camp->params->af_range, "fast") == 0) {
af_speed = controls::AfSpeedFast;
@@ -413,6 +438,12 @@ bool camera_start(camera_t *cam) {
af_speed = controls::AfSpeedNormal;
}
camp->ctrls->set(controls::AfSpeed, af_speed);
+
+ if (strcmp(camp->params->af_mode, "auto") == 0) {
+ camp->ctrls->set(controls::AfTrigger, controls::AfTriggerStart);
+ } else if (strcmp(camp->params->af_mode, "manual") == 0) {
+ camp->ctrls->set(controls::LensPosition, camp->params->lens_position);
+ }
}
if (camp->params->roi != NULL) {
@@ -434,29 +465,6 @@ bool camera_start(camera_t *cam) {
camp->ctrls->set(controls::ScalerCrop, crop);
}
- if (camp->params->af_window != NULL) {
- std::optional opt = camp->camera->properties().get(properties::ScalerCropMaximum);
- Rectangle sensor_area;
- try {
- sensor_area = opt.value();
- } catch(const std::bad_optional_access& exc) {
- set_error("get(ScalerCropMaximum) failed");
- return false;
- }
-
- Rectangle afwindows_rectangle[1];
-
- afwindows_rectangle[0] = Rectangle(
- camp->params->af_window->x * sensor_area.width,
- camp->params->af_window->y * sensor_area.height,
- camp->params->af_window->width * sensor_area.width,
- camp->params->af_window->height * sensor_area.height);
-
- afwindows_rectangle[0].translateBy(sensor_area.topLeft());
- camp->ctrls->set(controls::AfMetering, controls::AfMeteringWindows);
- camp->ctrls->set(controls::AfWindows, afwindows_rectangle);
- }
-
int res = camp->camera->start(camp->ctrls.get());
if (res != 0) {
set_error("Camera.start() failed");
diff --git a/internal/rpicamera/exe/camera.h b/internal/protocols/rpicamera/exe/camera.h
similarity index 100%
rename from internal/rpicamera/exe/camera.h
rename to internal/protocols/rpicamera/exe/camera.h
diff --git a/internal/rpicamera/exe/encoder.c b/internal/protocols/rpicamera/exe/encoder.c
similarity index 95%
rename from internal/rpicamera/exe/encoder.c
rename to internal/protocols/rpicamera/exe/encoder.c
index 2f3d638bfe5..0bfeb7b5b6f 100644
--- a/internal/rpicamera/exe/encoder.c
+++ b/internal/protocols/rpicamera/exe/encoder.c
@@ -111,6 +111,27 @@ static void *output_thread(void *userdata) {
return NULL;
}
+static bool fill_dynamic_params(int fd, const parameters_t *params) {
+ struct v4l2_control ctrl = {0};
+ ctrl.id = V4L2_CID_MPEG_VIDEO_H264_I_PERIOD;
+ ctrl.value = params->idr_period;
+ int res = ioctl(fd, VIDIOC_S_CTRL, &ctrl);
+ if (res != 0) {
+ set_error("unable to set IDR period");
+ return false;
+ }
+
+ ctrl.id = V4L2_CID_MPEG_VIDEO_BITRATE;
+ ctrl.value = params->bitrate;
+ res = ioctl(fd, VIDIOC_S_CTRL, &ctrl);
+ if (res != 0) {
+ set_error("unable to set bitrate");
+ return false;
+ }
+
+ return true;
+}
+
bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc) {
*enc = malloc(sizeof(encoder_priv_t));
encoder_priv_t *encp = (encoder_priv_t *)(*enc);
@@ -122,18 +143,15 @@ bool encoder_create(const parameters_t *params, int stride, int colorspace, enco
goto failed;
}
- struct v4l2_control ctrl = {0};
- ctrl.id = V4L2_CID_MPEG_VIDEO_BITRATE;
- ctrl.value = params->bitrate;
- int res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl);
- if (res != 0) {
- set_error("unable to set bitrate");
+ bool res2 = fill_dynamic_params(encp->fd, params);
+ if (!res2) {
goto failed;
}
+ struct v4l2_control ctrl = {0};
ctrl.id = V4L2_CID_MPEG_VIDEO_H264_PROFILE;
ctrl.value = params->profile;
- res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl);
+ int res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl);
if (res != 0) {
set_error("unable to set profile");
goto failed;
@@ -147,14 +165,6 @@ bool encoder_create(const parameters_t *params, int stride, int colorspace, enco
goto failed;
}
- ctrl.id = V4L2_CID_MPEG_VIDEO_H264_I_PERIOD;
- ctrl.value = params->idr_period;
- res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl);
- if (res != 0) {
- set_error("unable to set IDR period");
- goto failed;
- }
-
ctrl.id = V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER;
ctrl.value = 0;
res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl);
@@ -319,3 +329,9 @@ void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestam
// it happens when the raspberry is under pressure. do not exit.
}
}
+
+void encoder_reload_params(encoder_t *enc, const parameters_t *params) {
+ encoder_priv_t *encp = (encoder_priv_t *)enc;
+
+ fill_dynamic_params(encp->fd, params);
+}
diff --git a/internal/rpicamera/exe/encoder.h b/internal/protocols/rpicamera/exe/encoder.h
similarity index 85%
rename from internal/rpicamera/exe/encoder.h
rename to internal/protocols/rpicamera/exe/encoder.h
index d5a416aeb99..eb1e7928e9e 100644
--- a/internal/rpicamera/exe/encoder.h
+++ b/internal/protocols/rpicamera/exe/encoder.h
@@ -10,5 +10,6 @@ typedef void (*encoder_output_cb)(uint64_t ts, const uint8_t *buf, uint64_t size
const char *encoder_get_error();
bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc);
void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us);
+void encoder_reload_params(encoder_t *enc, const parameters_t *params);
#endif
diff --git a/internal/rpicamera/exe/main.c b/internal/protocols/rpicamera/exe/main.c
similarity index 98%
rename from internal/rpicamera/exe/main.c
rename to internal/protocols/rpicamera/exe/main.c
index 48dab5c3ca7..bb379215f2a 100644
--- a/internal/rpicamera/exe/main.c
+++ b/internal/protocols/rpicamera/exe/main.c
@@ -106,6 +106,7 @@ int main() {
continue;
}
camera_reload_params(cam, ¶ms);
+ encoder_reload_params(enc, ¶ms);
parameters_destroy(¶ms);
}
}
diff --git a/internal/rpicamera/exe/parameters.c b/internal/protocols/rpicamera/exe/parameters.c
similarity index 97%
rename from internal/rpicamera/exe/parameters.c
rename to internal/protocols/rpicamera/exe/parameters.c
index 8b9b071574c..5a8905ef045 100644
--- a/internal/rpicamera/exe/parameters.c
+++ b/internal/protocols/rpicamera/exe/parameters.c
@@ -37,7 +37,9 @@ bool parameters_unserialize(parameters_t *params, const uint8_t *buf, size_t buf
char *key = strsep(&entry, ":");
char *val = strsep(&entry, ":");
- if (strcmp(key, "CameraID") == 0) {
+ if (strcmp(key, "LogLevel") == 0) {
+ params->log_level = base64_decode(val);
+ } else if (strcmp(key, "CameraID") == 0) {
params->camera_id = atoi(val);
} else if (strcmp(key, "Width") == 0) {
params->width = atoi(val);
diff --git a/internal/rpicamera/exe/parameters.h b/internal/protocols/rpicamera/exe/parameters.h
similarity index 98%
rename from internal/rpicamera/exe/parameters.h
rename to internal/protocols/rpicamera/exe/parameters.h
index 2b19a942432..abfcfa206bc 100644
--- a/internal/rpicamera/exe/parameters.h
+++ b/internal/protocols/rpicamera/exe/parameters.h
@@ -8,6 +8,7 @@
#include "sensor_mode.h"
typedef struct {
+ char *log_level;
unsigned int camera_id;
unsigned int width;
unsigned int height;
diff --git a/internal/rpicamera/exe/pipe.c b/internal/protocols/rpicamera/exe/pipe.c
similarity index 100%
rename from internal/rpicamera/exe/pipe.c
rename to internal/protocols/rpicamera/exe/pipe.c
diff --git a/internal/rpicamera/exe/pipe.h b/internal/protocols/rpicamera/exe/pipe.h
similarity index 100%
rename from internal/rpicamera/exe/pipe.h
rename to internal/protocols/rpicamera/exe/pipe.h
diff --git a/internal/rpicamera/exe/sensor_mode.c b/internal/protocols/rpicamera/exe/sensor_mode.c
similarity index 100%
rename from internal/rpicamera/exe/sensor_mode.c
rename to internal/protocols/rpicamera/exe/sensor_mode.c
diff --git a/internal/rpicamera/exe/sensor_mode.h b/internal/protocols/rpicamera/exe/sensor_mode.h
similarity index 100%
rename from internal/rpicamera/exe/sensor_mode.h
rename to internal/protocols/rpicamera/exe/sensor_mode.h
diff --git a/internal/rpicamera/exe/text.c b/internal/protocols/rpicamera/exe/text.c
similarity index 100%
rename from internal/rpicamera/exe/text.c
rename to internal/protocols/rpicamera/exe/text.c
diff --git a/internal/rpicamera/exe/text.h b/internal/protocols/rpicamera/exe/text.h
similarity index 100%
rename from internal/rpicamera/exe/text.h
rename to internal/protocols/rpicamera/exe/text.h
diff --git a/internal/rpicamera/exe/text_font.ttf b/internal/protocols/rpicamera/exe/text_font.ttf
similarity index 100%
rename from internal/rpicamera/exe/text_font.ttf
rename to internal/protocols/rpicamera/exe/text_font.ttf
diff --git a/internal/rpicamera/exe/window.c b/internal/protocols/rpicamera/exe/window.c
similarity index 100%
rename from internal/rpicamera/exe/window.c
rename to internal/protocols/rpicamera/exe/window.c
diff --git a/internal/rpicamera/exe/window.h b/internal/protocols/rpicamera/exe/window.h
similarity index 100%
rename from internal/rpicamera/exe/window.h
rename to internal/protocols/rpicamera/exe/window.h
diff --git a/internal/rpicamera/params.go b/internal/protocols/rpicamera/params.go
similarity index 98%
rename from internal/rpicamera/params.go
rename to internal/protocols/rpicamera/params.go
index c8ff1e73c76..bf30d0a56cb 100644
--- a/internal/rpicamera/params.go
+++ b/internal/protocols/rpicamera/params.go
@@ -9,6 +9,7 @@ import (
// Params is a set of camera parameters.
type Params struct {
+ LogLevel string
CameraID int
Width int
Height int
diff --git a/internal/rpicamera/pipe.go b/internal/protocols/rpicamera/pipe.go
similarity index 100%
rename from internal/rpicamera/pipe.go
rename to internal/protocols/rpicamera/pipe.go
diff --git a/internal/rpicamera/rpicamera.go b/internal/protocols/rpicamera/rpicamera.go
similarity index 90%
rename from internal/rpicamera/rpicamera.go
rename to internal/protocols/rpicamera/rpicamera.go
index 969d9785f5a..d99e305b451 100644
--- a/internal/rpicamera/rpicamera.go
+++ b/internal/protocols/rpicamera/rpicamera.go
@@ -111,8 +111,10 @@ func checkLibraries64Bit() error {
return nil
}
+// RPICamera is a RPI Camera reader.
type RPICamera struct {
- onData func(time.Duration, [][]byte)
+ Params Params
+ OnData func(time.Duration, [][]byte)
cmd *exec.Cmd
pipeConf *pipe
@@ -122,31 +124,25 @@ type RPICamera struct {
readerDone chan error
}
-func New(
- params Params,
- onData func(time.Duration, [][]byte),
-) (*RPICamera, error) {
+// Initialize initializes a RPICamera.
+func (c *RPICamera) Initialize() error {
if runtime.GOARCH == "arm" {
err := checkLibraries64Bit()
if err != nil {
- return nil, err
+ return err
}
}
- c := &RPICamera{
- onData: onData,
- }
-
var err error
c.pipeConf, err = newPipe()
if err != nil {
- return nil, err
+ return err
}
c.pipeVideo, err = newPipe()
if err != nil {
c.pipeConf.close()
- return nil, err
+ return err
}
env := []string{
@@ -158,10 +154,10 @@ func New(
if err != nil {
c.pipeConf.close()
c.pipeVideo.close()
- return nil, err
+ return err
}
- c.pipeConf.write(append([]byte{'c'}, params.serialize()...))
+ c.pipeConf.write(append([]byte{'c'}, c.Params.serialize()...))
c.waitDone = make(chan error)
go func() {
@@ -178,7 +174,7 @@ func New(
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
- return nil, fmt.Errorf("process exited unexpectedly")
+ return fmt.Errorf("process exited unexpectedly")
case err := <-c.readerDone:
if err != nil {
@@ -186,7 +182,7 @@ func New(
<-c.waitDone
c.pipeConf.close()
c.pipeVideo.close()
- return nil, err
+ return err
}
}
@@ -195,7 +191,7 @@ func New(
c.readerDone <- c.readData()
}()
- return c, nil
+ return nil
}
func (c *RPICamera) Close() {
@@ -248,6 +244,6 @@ func (c *RPICamera) readData() error {
return err
}
- c.onData(dts, nalus)
+ c.OnData(dts, nalus)
}
}
diff --git a/internal/rpicamera/rpicamera_disabled.go b/internal/protocols/rpicamera/rpicamera_disabled.go
similarity index 64%
rename from internal/rpicamera/rpicamera_disabled.go
rename to internal/protocols/rpicamera/rpicamera_disabled.go
index a8663320a01..3bf10ce551f 100644
--- a/internal/rpicamera/rpicamera_disabled.go
+++ b/internal/protocols/rpicamera/rpicamera_disabled.go
@@ -14,14 +14,14 @@ func Cleanup() {
}
// RPICamera is a RPI Camera reader.
-type RPICamera struct{}
+type RPICamera struct {
+ Params Params
+ OnData func(time.Duration, [][]byte)
+}
-// New allocates a RPICamera.
-func New(
- _ Params,
- _ func(time.Duration, [][]byte),
-) (*RPICamera, error) {
- return nil, fmt.Errorf("server was compiled without support for the Raspberry Pi Camera")
+// Initialize initializes a RPICamera.
+func (c *RPICamera) Initialize() error {
+ return fmt.Errorf("server was compiled without support for the Raspberry Pi Camera")
}
// Close closes a RPICamera.
diff --git a/internal/rtmp/bytecounter/reader.go b/internal/protocols/rtmp/bytecounter/reader.go
similarity index 100%
rename from internal/rtmp/bytecounter/reader.go
rename to internal/protocols/rtmp/bytecounter/reader.go
diff --git a/internal/rtmp/bytecounter/reader_test.go b/internal/protocols/rtmp/bytecounter/reader_test.go
similarity index 100%
rename from internal/rtmp/bytecounter/reader_test.go
rename to internal/protocols/rtmp/bytecounter/reader_test.go
diff --git a/internal/rtmp/bytecounter/readwriter.go b/internal/protocols/rtmp/bytecounter/readwriter.go
similarity index 100%
rename from internal/rtmp/bytecounter/readwriter.go
rename to internal/protocols/rtmp/bytecounter/readwriter.go
diff --git a/internal/rtmp/bytecounter/writer.go b/internal/protocols/rtmp/bytecounter/writer.go
similarity index 100%
rename from internal/rtmp/bytecounter/writer.go
rename to internal/protocols/rtmp/bytecounter/writer.go
diff --git a/internal/rtmp/bytecounter/writer_test.go b/internal/protocols/rtmp/bytecounter/writer_test.go
similarity index 100%
rename from internal/rtmp/bytecounter/writer_test.go
rename to internal/protocols/rtmp/bytecounter/writer_test.go
diff --git a/internal/rtmp/chunk/chunk.go b/internal/protocols/rtmp/chunk/chunk.go
similarity index 50%
rename from internal/rtmp/chunk/chunk.go
rename to internal/protocols/rtmp/chunk/chunk.go
index 25e992a5ca8..51833ca061a 100644
--- a/internal/rtmp/chunk/chunk.go
+++ b/internal/protocols/rtmp/chunk/chunk.go
@@ -7,6 +7,6 @@ import (
// Chunk is a chunk.
type Chunk interface {
- Read(io.Reader, uint32) error
- Marshal() ([]byte, error)
+ Read(r io.Reader, bodyLen uint32, hasExtendedTimestamp bool) error
+ Marshal(hasExtendedTimestamp bool) ([]byte, error)
}
diff --git a/internal/protocols/rtmp/chunk/chunk0.go b/internal/protocols/rtmp/chunk/chunk0.go
new file mode 100644
index 00000000000..f0e0323dfb9
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/chunk0.go
@@ -0,0 +1,99 @@
+package chunk
+
+import (
+ "io"
+)
+
+// Chunk0 is a type 0 chunk.
+// This type MUST be used at
+// the start of a chunk stream, and whenever the stream timestamp goes
+// backward (e.g., because of a backward seek).
+type Chunk0 struct {
+ ChunkStreamID byte
+ Timestamp uint32
+ BodyLen uint32
+ Type uint8
+ MessageStreamID uint32
+ Body []byte
+}
+
+// Read reads the chunk.
+func (c *Chunk0) Read(r io.Reader, maxBodyLen uint32, _ bool) error {
+ header := make([]byte, 12)
+ _, err := io.ReadFull(r, header)
+ if err != nil {
+ return err
+ }
+
+ c.ChunkStreamID = header[0] & 0x3F
+ c.Timestamp = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
+ c.BodyLen = uint32(header[4])<<16 | uint32(header[5])<<8 | uint32(header[6])
+ c.Type = header[7]
+ c.MessageStreamID = uint32(header[8])<<24 | uint32(header[9])<<16 | uint32(header[10])<<8 | uint32(header[11])
+
+ if c.Timestamp >= 0xFFFFFF {
+ _, err := io.ReadFull(r, header[:4])
+ if err != nil {
+ return err
+ }
+
+ c.Timestamp = uint32(header[0])<<24 | uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
+ }
+
+ chunkBodyLen := c.BodyLen
+ if chunkBodyLen > maxBodyLen {
+ chunkBodyLen = maxBodyLen
+ }
+
+ c.Body = make([]byte, chunkBodyLen)
+ _, err = io.ReadFull(r, c.Body)
+ return err
+}
+
+func (c Chunk0) marshalSize() int {
+ n := 12 + len(c.Body)
+ if c.Timestamp >= 0xFFFFFF {
+ n += 4
+ }
+ return n
+}
+
+// Marshal writes the chunk.
+func (c Chunk0) Marshal(_ bool) ([]byte, error) {
+ buf := make([]byte, c.marshalSize())
+ buf[0] = c.ChunkStreamID
+
+ if c.Timestamp >= 0xFFFFFF {
+ buf[1] = 0xFF
+ buf[2] = 0xFF
+ buf[3] = 0xFF
+ buf[4] = byte(c.BodyLen >> 16)
+ buf[5] = byte(c.BodyLen >> 8)
+ buf[6] = byte(c.BodyLen)
+ buf[7] = c.Type
+ buf[8] = byte(c.MessageStreamID >> 24)
+ buf[9] = byte(c.MessageStreamID >> 16)
+ buf[10] = byte(c.MessageStreamID >> 8)
+ buf[11] = byte(c.MessageStreamID)
+ buf[12] = byte(c.Timestamp >> 24)
+ buf[13] = byte(c.Timestamp >> 16)
+ buf[14] = byte(c.Timestamp >> 8)
+ buf[15] = byte(c.Timestamp)
+ copy(buf[16:], c.Body)
+ } else {
+ buf[1] = byte(c.Timestamp >> 16)
+ buf[2] = byte(c.Timestamp >> 8)
+ buf[3] = byte(c.Timestamp)
+ buf[4] = byte(c.BodyLen >> 16)
+ buf[5] = byte(c.BodyLen >> 8)
+ buf[6] = byte(c.BodyLen)
+ buf[7] = c.Type
+ buf[8] = byte(c.MessageStreamID >> 24)
+ buf[9] = byte(c.MessageStreamID >> 16)
+ buf[10] = byte(c.MessageStreamID >> 8)
+ buf[11] = byte(c.MessageStreamID)
+ copy(buf[12:], c.Body)
+ }
+
+ return buf, nil
+}
diff --git a/internal/protocols/rtmp/chunk/chunk1.go b/internal/protocols/rtmp/chunk/chunk1.go
new file mode 100644
index 00000000000..28f3e5b438c
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/chunk1.go
@@ -0,0 +1,91 @@
+package chunk
+
+import (
+ "io"
+)
+
+// Chunk1 is a type 1 chunk.
+// The message stream ID is not
+// included; this chunk takes the same stream ID as the preceding chunk.
+// Streams with variable-sized messages (for example, many video
+// formats) SHOULD use this format for the first chunk of each new
+// message after the first.
+type Chunk1 struct {
+ ChunkStreamID byte
+ TimestampDelta uint32
+ BodyLen uint32
+ Type uint8
+ Body []byte
+}
+
+// Read reads the chunk.
+func (c *Chunk1) Read(r io.Reader, maxBodyLen uint32, _ bool) error {
+ header := make([]byte, 8)
+ _, err := io.ReadFull(r, header)
+ if err != nil {
+ return err
+ }
+
+ c.ChunkStreamID = header[0] & 0x3F
+ c.TimestampDelta = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
+ c.BodyLen = uint32(header[4])<<16 | uint32(header[5])<<8 | uint32(header[6])
+ c.Type = header[7]
+
+ if c.TimestampDelta >= 0xFFFFFF {
+ _, err = io.ReadFull(r, header[:4])
+ if err != nil {
+ return err
+ }
+
+ c.TimestampDelta = uint32(header[0])<<24 | uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
+ }
+
+ chunkBodyLen := (c.BodyLen)
+ if chunkBodyLen > maxBodyLen {
+ chunkBodyLen = maxBodyLen
+ }
+
+ c.Body = make([]byte, chunkBodyLen)
+ _, err = io.ReadFull(r, c.Body)
+ return err
+}
+
+func (c Chunk1) marshalSize() int {
+ n := 8 + len(c.Body)
+ if c.TimestampDelta >= 0xFFFFFF {
+ n += 4
+ }
+ return n
+}
+
+// Marshal writes the chunk.
+func (c Chunk1) Marshal(_ bool) ([]byte, error) {
+ buf := make([]byte, c.marshalSize())
+ buf[0] = 1<<6 | c.ChunkStreamID
+
+ if c.TimestampDelta >= 0xFFFFFF {
+ buf[1] = 0xFF
+ buf[2] = 0xFF
+ buf[3] = 0xFF
+ buf[4] = byte(c.BodyLen >> 16)
+ buf[5] = byte(c.BodyLen >> 8)
+ buf[6] = byte(c.BodyLen)
+ buf[7] = c.Type
+ buf[8] = byte(c.TimestampDelta >> 24)
+ buf[9] = byte(c.TimestampDelta >> 16)
+ buf[10] = byte(c.TimestampDelta >> 8)
+ buf[11] = byte(c.TimestampDelta)
+ copy(buf[12:], c.Body)
+ } else {
+ buf[1] = byte(c.TimestampDelta >> 16)
+ buf[2] = byte(c.TimestampDelta >> 8)
+ buf[3] = byte(c.TimestampDelta)
+ buf[4] = byte(c.BodyLen >> 16)
+ buf[5] = byte(c.BodyLen >> 8)
+ buf[6] = byte(c.BodyLen)
+ buf[7] = c.Type
+ copy(buf[8:], c.Body)
+ }
+
+ return buf, nil
+}
diff --git a/internal/protocols/rtmp/chunk/chunk2.go b/internal/protocols/rtmp/chunk/chunk2.go
new file mode 100644
index 00000000000..10d48833266
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/chunk2.go
@@ -0,0 +1,72 @@
+package chunk
+
+import (
+ "io"
+)
+
+// Chunk2 is a type 2 chunk.
+// Neither the stream ID nor the
+// message length is included; this chunk has the same stream ID and
+// message length as the preceding chunk.
+type Chunk2 struct {
+ ChunkStreamID byte
+ TimestampDelta uint32
+ Body []byte
+}
+
+// Read reads the chunk.
+func (c *Chunk2) Read(r io.Reader, bodyLen uint32, _ bool) error {
+ header := make([]byte, 4)
+ _, err := io.ReadFull(r, header)
+ if err != nil {
+ return err
+ }
+
+ c.ChunkStreamID = header[0] & 0x3F
+ c.TimestampDelta = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
+
+ if c.TimestampDelta >= 0xFFFFFF {
+ _, err = io.ReadFull(r, header[:4])
+ if err != nil {
+ return err
+ }
+
+ c.TimestampDelta = uint32(header[0])<<24 | uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
+ }
+
+ c.Body = make([]byte, bodyLen)
+ _, err = io.ReadFull(r, c.Body)
+ return err
+}
+
+func (c Chunk2) marshalSize() int {
+ n := 4 + len(c.Body)
+ if c.TimestampDelta >= 0xFFFFFF {
+ n += 4
+ }
+ return n
+}
+
+// Marshal writes the chunk.
+func (c Chunk2) Marshal(_ bool) ([]byte, error) {
+ buf := make([]byte, c.marshalSize())
+ buf[0] = 2<<6 | c.ChunkStreamID
+
+ if c.TimestampDelta >= 0xFFFFFF {
+ buf[1] = 0xFF
+ buf[2] = 0xFF
+ buf[3] = 0xFF
+ buf[4] = byte(c.TimestampDelta >> 24)
+ buf[5] = byte(c.TimestampDelta >> 16)
+ buf[6] = byte(c.TimestampDelta >> 8)
+ buf[7] = byte(c.TimestampDelta)
+ copy(buf[8:], c.Body)
+ } else {
+ buf[1] = byte(c.TimestampDelta >> 16)
+ buf[2] = byte(c.TimestampDelta >> 8)
+ buf[3] = byte(c.TimestampDelta)
+ copy(buf[4:], c.Body)
+ }
+
+ return buf, nil
+}
diff --git a/internal/rtmp/chunk/chunk3.go b/internal/protocols/rtmp/chunk/chunk3.go
similarity index 51%
rename from internal/rtmp/chunk/chunk3.go
rename to internal/protocols/rtmp/chunk/chunk3.go
index e5871bd0c2d..ea953b53bf2 100644
--- a/internal/rtmp/chunk/chunk3.go
+++ b/internal/protocols/rtmp/chunk/chunk3.go
@@ -16,24 +16,45 @@ type Chunk3 struct {
}
// Read reads the chunk.
-func (c *Chunk3) Read(r io.Reader, chunkBodyLen uint32) error {
- header := make([]byte, 1)
- _, err := io.ReadFull(r, header)
+func (c *Chunk3) Read(r io.Reader, bodyLen uint32, hasExtendedTimestamp bool) error {
+ header := make([]byte, 4)
+ _, err := io.ReadFull(r, header[:1])
if err != nil {
return err
}
c.ChunkStreamID = header[0] & 0x3F
- c.Body = make([]byte, chunkBodyLen)
+ if hasExtendedTimestamp {
+ _, err := io.ReadFull(r, header[:4])
+ if err != nil {
+ return err
+ }
+ }
+
+ c.Body = make([]byte, bodyLen)
_, err = io.ReadFull(r, c.Body)
return err
}
+func (c Chunk3) marshalSize(hasExtendedTimestamp bool) int {
+ n := 1 + len(c.Body)
+ if hasExtendedTimestamp {
+ n += 4
+ }
+ return n
+}
+
// Marshal writes the chunk.
-func (c Chunk3) Marshal() ([]byte, error) {
- buf := make([]byte, 1+len(c.Body))
+func (c Chunk3) Marshal(hasExtendedTimestamp bool) ([]byte, error) {
+ buf := make([]byte, c.marshalSize(hasExtendedTimestamp))
buf[0] = 3<<6 | c.ChunkStreamID
- copy(buf[1:], c.Body)
+
+ if hasExtendedTimestamp {
+ copy(buf[5:], c.Body)
+ } else {
+ copy(buf[1:], c.Body)
+ }
+
return buf, nil
}
diff --git a/internal/protocols/rtmp/chunk/chunk_test.go b/internal/protocols/rtmp/chunk/chunk_test.go
new file mode 100644
index 00000000000..9748aaae1d2
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/chunk_test.go
@@ -0,0 +1,186 @@
+package chunk
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+var cases = []struct {
+ name string
+ enc []byte
+ bodyLen uint32
+ hasExtendedTimestamp bool
+ dec Chunk
+}{
+ {
+ "chunk0 standard",
+ []byte{
+ 0x19, 0xb1, 0xa1, 0x91, 0x0, 0x0, 0x14, 0x14,
+ 0x3, 0x5d, 0x17, 0x3d, 0x1, 0x2, 0x3, 0x4,
+ },
+ 4,
+ false,
+ &Chunk0{
+ ChunkStreamID: 25,
+ Timestamp: 11641233,
+ Type: 20,
+ MessageStreamID: 56432445,
+ BodyLen: 20,
+ Body: []byte{1, 2, 3, 4},
+ },
+ },
+ {
+ "chunk0 extended timestamp",
+ []byte{
+ 0x19, 0xff, 0xff, 0xff, 0x00, 0x00, 0x14, 0x0f,
+ 0x00, 0x31, 0x84, 0xb2, 0xff, 0x34, 0x86, 0xa2,
+ 0x05, 0x06, 0x07, 0x08,
+ },
+ 4,
+ false,
+ &Chunk0{
+ ChunkStreamID: 25,
+ Timestamp: 0xFF3486a2,
+ Type: 15,
+ MessageStreamID: 3245234,
+ BodyLen: 20,
+ Body: []byte{5, 6, 7, 8},
+ },
+ },
+ {
+ "chunk1 standard",
+ []byte{
+ 0x59, 0xb1, 0xa1, 0x91, 0x0, 0x0, 0x14, 0x14,
+ 0x1, 0x2, 0x3, 0x4,
+ },
+ 4,
+ false,
+ &Chunk1{
+ ChunkStreamID: 25,
+ TimestampDelta: 11641233,
+ Type: 20,
+ BodyLen: 20,
+ Body: []byte{1, 2, 3, 4},
+ },
+ },
+ {
+ "chunk1 extended timestamp",
+ []byte{
+ 0x59, 0xff, 0xff, 0xff, 0x00, 0x00, 0x14, 0x14,
+ 0xff, 0x88, 0x4b, 0x6c, 0x05, 0x06, 0x07, 0x08,
+ },
+ 4,
+ false,
+ &Chunk1{
+ ChunkStreamID: 25,
+ TimestampDelta: 0xFF884B6C,
+ Type: 20,
+ BodyLen: 20,
+ Body: []byte{5, 6, 7, 8},
+ },
+ },
+ {
+ "chunk2 standard",
+ []byte{
+ 0x99, 0xb1, 0xa1, 0x91, 0x1, 0x2, 0x3, 0x4,
+ },
+ 4,
+ false,
+ &Chunk2{
+ ChunkStreamID: 25,
+ TimestampDelta: 11641233,
+ Body: []byte{1, 2, 3, 4},
+ },
+ },
+ {
+ "chunk2 extended timestamp",
+ []byte{
+ 0x99, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xbb, 0xcc,
+ 0x05, 0x06, 0x07, 0x08,
+ },
+ 4,
+ false,
+ &Chunk2{
+ ChunkStreamID: 25,
+ TimestampDelta: 0xFFAABBCC,
+ Body: []byte{5, 6, 7, 8},
+ },
+ },
+ {
+ "chunk3 standard",
+ []byte{
+ 0xd9, 0x1, 0x2, 0x3, 0x4,
+ },
+ 4,
+ false,
+ &Chunk3{
+ ChunkStreamID: 25,
+ Body: []byte{1, 2, 3, 4},
+ },
+ },
+ {
+ "chunk3 extended timestamp",
+ []byte{
+ 0xd9, 0x00, 0x00, 0x00, 0x00, 0x05, 0x06, 0x07,
+ 0x08,
+ },
+ 4,
+ true,
+ &Chunk3{
+ ChunkStreamID: 25,
+ Body: []byte{5, 6, 7, 8},
+ },
+ },
+}
+
+func TestChunkRead(t *testing.T) {
+ for _, ca := range cases {
+ t.Run(ca.name, func(t *testing.T) {
+ chunk := reflect.New(reflect.TypeOf(ca.dec).Elem()).Interface().(Chunk)
+ err := chunk.Read(bytes.NewReader(ca.enc), ca.bodyLen, ca.hasExtendedTimestamp)
+ require.NoError(t, err)
+ require.Equal(t, ca.dec, chunk)
+ })
+ }
+}
+
+func TestChunkMarshal(t *testing.T) {
+ for _, ca := range cases {
+ t.Run(ca.name, func(t *testing.T) {
+ buf, err := ca.dec.Marshal(ca.hasExtendedTimestamp)
+ require.NoError(t, err)
+ require.Equal(t, ca.enc, buf)
+ })
+ }
+}
+
+func FuzzChunk0Read(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ var chunk Chunk0
+ chunk.Read(bytes.NewReader(b), 65536, false) //nolint:errcheck
+ })
+}
+
+func FuzzChunk1Read(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ var chunk Chunk1
+ chunk.Read(bytes.NewReader(b), 65536, false) //nolint:errcheck
+ })
+}
+
+func FuzzChunk2Read(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ var chunk Chunk2
+ chunk.Read(bytes.NewReader(b), 65536, false) //nolint:errcheck
+ })
+}
+
+func FuzzChunk3Read(f *testing.F) {
+ f.Fuzz(func(t *testing.T, b []byte) {
+ var chunk Chunk3
+ chunk.Read(bytes.NewReader(b), 65536, true) //nolint:errcheck
+ })
+}
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/582528ddfad69eb5 b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/582528ddfad69eb5
new file mode 100644
index 00000000000..a96f5599e6b
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/582528ddfad69eb5
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0")
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/5f73a77c7f93e5f8 b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/5f73a77c7f93e5f8
new file mode 100644
index 00000000000..c9756ec7e5a
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk0Read/5f73a77c7f93e5f8
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0\xff\xff\xff00000000")
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/553384c8664fe971 b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/553384c8664fe971
new file mode 100644
index 00000000000..26f98f8852d
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/553384c8664fe971
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0\xff\xff\xff0000")
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/582528ddfad69eb5 b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/582528ddfad69eb5
new file mode 100644
index 00000000000..a96f5599e6b
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk1Read/582528ddfad69eb5
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0")
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/582528ddfad69eb5 b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/582528ddfad69eb5
new file mode 100644
index 00000000000..a96f5599e6b
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/582528ddfad69eb5
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0")
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/feb2b2a8b4ba63ba b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/feb2b2a8b4ba63ba
new file mode 100644
index 00000000000..01533d52714
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk2Read/feb2b2a8b4ba63ba
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0\xff\xff\xff")
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/582528ddfad69eb5 b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/582528ddfad69eb5
new file mode 100644
index 00000000000..a96f5599e6b
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/582528ddfad69eb5
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("0")
diff --git a/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/caf81e9797b19c76 b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/caf81e9797b19c76
new file mode 100644
index 00000000000..67322c70489
--- /dev/null
+++ b/internal/protocols/rtmp/chunk/testdata/fuzz/FuzzChunk3Read/caf81e9797b19c76
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("")
diff --git a/internal/rtmp/conn.go b/internal/protocols/rtmp/conn.go
similarity index 97%
rename from internal/rtmp/conn.go
rename to internal/protocols/rtmp/conn.go
index 431ecc0d418..f81fd0a801a 100644
--- a/internal/rtmp/conn.go
+++ b/internal/protocols/rtmp/conn.go
@@ -9,9 +9,9 @@ import (
"github.com/notedit/rtmp/format/flv/flvio"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/handshake"
- "github.com/bluenviron/mediamtx/internal/rtmp/message"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/handshake"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
)
func resultIsOK1(res *message.CommandAMF0) bool {
@@ -122,7 +122,7 @@ func readCommandResult(
}
if cmd, ok := msg.(*message.CommandAMF0); ok {
- if cmd.CommandID == commandID && cmd.Name == commandName {
+ if (cmd.CommandID == commandID || cmd.CommandID == 0) && cmd.Name == commandName {
if !isValid(cmd) {
return fmt.Errorf("server refused connect request")
}
diff --git a/internal/rtmp/conn_test.go b/internal/protocols/rtmp/conn_test.go
similarity index 95%
rename from internal/rtmp/conn_test.go
rename to internal/protocols/rtmp/conn_test.go
index d09fd40522b..4eb067b9e2d 100644
--- a/internal/rtmp/conn_test.go
+++ b/internal/protocols/rtmp/conn_test.go
@@ -9,13 +9,17 @@ import (
"github.com/notedit/rtmp/format/flv/flvio"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/handshake"
- "github.com/bluenviron/mediamtx/internal/rtmp/message"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/handshake"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
)
func TestNewClientConn(t *testing.T) {
- for _, ca := range []string{"read", "publish"} {
+ for _, ca := range []string{
+ "read",
+ "read nginx rtmp",
+ "publish",
+ } {
t.Run(ca, func(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:9121")
require.NoError(t, err)
@@ -92,7 +96,8 @@ func TestNewClientConn(t *testing.T) {
})
require.NoError(t, err)
- if ca == "read" {
+ switch ca {
+ case "read", "read nginx rtmp":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
@@ -138,7 +143,12 @@ func TestNewClientConn(t *testing.T) {
ChunkStreamID: 5,
MessageStreamID: 0x1000000,
Name: "onStatus",
- CommandID: 3,
+ CommandID: func() int {
+ if ca == "read nginx rtmp" {
+ return 0
+ }
+ return 3
+ }(),
Arguments: []interface{}{
nil,
flvio.AMFMap{
@@ -149,7 +159,8 @@ func TestNewClientConn(t *testing.T) {
},
})
require.NoError(t, err)
- } else {
+
+ case "publish":
msg, err = mrw.Read()
require.NoError(t, err)
require.Equal(t, &message.CommandAMF0{
@@ -240,10 +251,12 @@ func TestNewClientConn(t *testing.T) {
conn, err := NewClientConn(nconn, u, ca == "publish")
require.NoError(t, err)
- if ca == "read" {
+ switch ca {
+ case "read", "read nginx rtmp":
require.Equal(t, uint64(3421), conn.BytesReceived())
require.Equal(t, uint64(3409), conn.BytesSent())
- } else {
+
+ case "publish":
require.Equal(t, uint64(3427), conn.BytesReceived())
require.Equal(t, uint64(3466), conn.BytesSent())
}
diff --git a/internal/rtmp/h264conf/h264conf.go b/internal/protocols/rtmp/h264conf/h264conf.go
similarity index 100%
rename from internal/rtmp/h264conf/h264conf.go
rename to internal/protocols/rtmp/h264conf/h264conf.go
diff --git a/internal/rtmp/h264conf/h264conf_test.go b/internal/protocols/rtmp/h264conf/h264conf_test.go
similarity index 100%
rename from internal/rtmp/h264conf/h264conf_test.go
rename to internal/protocols/rtmp/h264conf/h264conf_test.go
diff --git a/internal/rtmp/handshake/c0s0.go b/internal/protocols/rtmp/handshake/c0s0.go
similarity index 100%
rename from internal/rtmp/handshake/c0s0.go
rename to internal/protocols/rtmp/handshake/c0s0.go
diff --git a/internal/rtmp/handshake/c0s0_test.go b/internal/protocols/rtmp/handshake/c0s0_test.go
similarity index 100%
rename from internal/rtmp/handshake/c0s0_test.go
rename to internal/protocols/rtmp/handshake/c0s0_test.go
diff --git a/internal/rtmp/handshake/c1s1.go b/internal/protocols/rtmp/handshake/c1s1.go
similarity index 100%
rename from internal/rtmp/handshake/c1s1.go
rename to internal/protocols/rtmp/handshake/c1s1.go
diff --git a/internal/rtmp/handshake/c1s1_test.go b/internal/protocols/rtmp/handshake/c1s1_test.go
similarity index 100%
rename from internal/rtmp/handshake/c1s1_test.go
rename to internal/protocols/rtmp/handshake/c1s1_test.go
diff --git a/internal/rtmp/handshake/c2s2.go b/internal/protocols/rtmp/handshake/c2s2.go
similarity index 100%
rename from internal/rtmp/handshake/c2s2.go
rename to internal/protocols/rtmp/handshake/c2s2.go
diff --git a/internal/rtmp/handshake/c2s2_test.go b/internal/protocols/rtmp/handshake/c2s2_test.go
similarity index 100%
rename from internal/rtmp/handshake/c2s2_test.go
rename to internal/protocols/rtmp/handshake/c2s2_test.go
diff --git a/internal/rtmp/handshake/dh.go b/internal/protocols/rtmp/handshake/dh.go
similarity index 100%
rename from internal/rtmp/handshake/dh.go
rename to internal/protocols/rtmp/handshake/dh.go
diff --git a/internal/rtmp/handshake/handshake.go b/internal/protocols/rtmp/handshake/handshake.go
similarity index 100%
rename from internal/rtmp/handshake/handshake.go
rename to internal/protocols/rtmp/handshake/handshake.go
diff --git a/internal/rtmp/handshake/handshake_test.go b/internal/protocols/rtmp/handshake/handshake_test.go
similarity index 100%
rename from internal/rtmp/handshake/handshake_test.go
rename to internal/protocols/rtmp/handshake/handshake_test.go
diff --git a/internal/rtmp/message/acknowledge.go b/internal/protocols/rtmp/message/acknowledge.go
similarity index 92%
rename from internal/rtmp/message/acknowledge.go
rename to internal/protocols/rtmp/message/acknowledge.go
index 3df1755ecfc..a40f95394e9 100644
--- a/internal/rtmp/message/acknowledge.go
+++ b/internal/protocols/rtmp/message/acknowledge.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// Acknowledge is an acknowledgement message.
diff --git a/internal/rtmp/message/audio.go b/internal/protocols/rtmp/message/audio.go
similarity index 77%
rename from internal/rtmp/message/audio.go
rename to internal/protocols/rtmp/message/audio.go
index 59725e02955..2ae4a6d4872 100644
--- a/internal/rtmp/message/audio.go
+++ b/internal/protocols/rtmp/message/audio.go
@@ -4,7 +4,7 @@ import (
"fmt"
"time"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
const (
@@ -12,12 +12,29 @@ const (
AudioChunkStreamID = 4
)
-// supported audio codecs
+// audio codecs
const (
CodecMPEG1Audio = 2
+ CodecLPCM = 3
+ CodecPCMA = 7
+ CodecPCMU = 8
CodecMPEG4Audio = 10
)
+// audio rates
+const (
+ Rate5512 = 0
+ Rate11025 = 1
+ Rate22050 = 2
+ Rate44100 = 3
+)
+
+// audio depths
+const (
+ Depth8 = 0
+ Depth16 = 1
+)
+
// AudioAACType is the AAC type of a Audio.
type AudioAACType uint8
@@ -35,7 +52,7 @@ type Audio struct {
Codec uint8
Rate uint8
Depth uint8
- Channels uint8
+ IsStereo bool
AACType AudioAACType // only for CodecMPEG4Audio
Payload []byte
}
@@ -52,18 +69,19 @@ func (m *Audio) Unmarshal(raw *rawmessage.Message) error {
m.Codec = raw.Body[0] >> 4
switch m.Codec {
- case CodecMPEG1Audio, CodecMPEG4Audio:
+ case CodecMPEG4Audio, CodecMPEG1Audio, CodecPCMA, CodecPCMU, CodecLPCM:
default:
return fmt.Errorf("unsupported audio codec: %d", m.Codec)
}
m.Rate = (raw.Body[0] >> 2) & 0x03
m.Depth = (raw.Body[0] >> 1) & 0x01
- m.Channels = raw.Body[0] & 0x01
- if m.Codec == CodecMPEG1Audio {
- m.Payload = raw.Body[1:]
- } else {
+ if (raw.Body[0] & 0x01) != 0 {
+ m.IsStereo = true
+ }
+
+ if m.Codec == CodecMPEG4Audio {
m.AACType = AudioAACType(raw.Body[1])
switch m.AACType {
case AudioAACTypeConfig, AudioAACTypeAU:
@@ -72,6 +90,8 @@ func (m *Audio) Unmarshal(raw *rawmessage.Message) error {
}
m.Payload = raw.Body[2:]
+ } else {
+ m.Payload = raw.Body[1:]
}
return nil
@@ -91,13 +111,17 @@ func (m Audio) marshalBodySize() int {
func (m Audio) Marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
- body[0] = m.Codec<<4 | m.Rate<<2 | m.Depth<<1 | m.Channels
+ body[0] = m.Codec<<4 | m.Rate<<2 | m.Depth<<1
- if m.Codec == CodecMPEG1Audio {
- copy(body[1:], m.Payload)
- } else {
+ if m.IsStereo {
+ body[0] |= 1
+ }
+
+ if m.Codec == CodecMPEG4Audio {
body[1] = uint8(m.AACType)
copy(body[2:], m.Payload)
+ } else {
+ copy(body[1:], m.Payload)
}
return &rawmessage.Message{
diff --git a/internal/rtmp/message/command_amf0.go b/internal/protocols/rtmp/message/command_amf0.go
similarity index 94%
rename from internal/rtmp/message/command_amf0.go
rename to internal/protocols/rtmp/message/command_amf0.go
index 73fb809cff2..c49dbd3d4fb 100644
--- a/internal/rtmp/message/command_amf0.go
+++ b/internal/protocols/rtmp/message/command_amf0.go
@@ -5,7 +5,7 @@ import (
"github.com/notedit/rtmp/format/flv/flvio"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// CommandAMF0 is a AMF0 command message.
diff --git a/internal/rtmp/message/data_amf0.go b/internal/protocols/rtmp/message/data_amf0.go
similarity index 92%
rename from internal/rtmp/message/data_amf0.go
rename to internal/protocols/rtmp/message/data_amf0.go
index 37708370a6c..b5d3041913d 100644
--- a/internal/rtmp/message/data_amf0.go
+++ b/internal/protocols/rtmp/message/data_amf0.go
@@ -3,7 +3,7 @@ package message
import (
"github.com/notedit/rtmp/format/flv/flvio"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// DataAMF0 is a AMF0 data message.
diff --git a/internal/rtmp/message/extended_coded_frames.go b/internal/protocols/rtmp/message/extended_coded_frames.go
similarity index 96%
rename from internal/rtmp/message/extended_coded_frames.go
rename to internal/protocols/rtmp/message/extended_coded_frames.go
index c25118c37c8..79cc6f4db13 100644
--- a/internal/rtmp/message/extended_coded_frames.go
+++ b/internal/protocols/rtmp/message/extended_coded_frames.go
@@ -4,7 +4,7 @@ import (
"fmt"
"time"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedCodedFrames is a CodedFrames extended message.
diff --git a/internal/rtmp/message/extended_frames_x.go b/internal/protocols/rtmp/message/extended_frames_x.go
similarity index 94%
rename from internal/rtmp/message/extended_frames_x.go
rename to internal/protocols/rtmp/message/extended_frames_x.go
index 53c823bf94d..b9b709cdf70 100644
--- a/internal/rtmp/message/extended_frames_x.go
+++ b/internal/protocols/rtmp/message/extended_frames_x.go
@@ -4,7 +4,7 @@ import (
"fmt"
"time"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedFramesX is a FramesX extended message.
diff --git a/internal/rtmp/message/extended_metadata.go b/internal/protocols/rtmp/message/extended_metadata.go
similarity index 89%
rename from internal/rtmp/message/extended_metadata.go
rename to internal/protocols/rtmp/message/extended_metadata.go
index 2a35cf79cd7..e2d0bb5a5b2 100644
--- a/internal/rtmp/message/extended_metadata.go
+++ b/internal/protocols/rtmp/message/extended_metadata.go
@@ -3,7 +3,7 @@ package message
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedMetadata is a metadata extended message.
diff --git a/internal/rtmp/message/extended_mpeg2ts_sequence_start.go b/internal/protocols/rtmp/message/extended_mpeg2ts_sequence_start.go
similarity index 90%
rename from internal/rtmp/message/extended_mpeg2ts_sequence_start.go
rename to internal/protocols/rtmp/message/extended_mpeg2ts_sequence_start.go
index d0546f237fe..04e7d8488e3 100644
--- a/internal/rtmp/message/extended_mpeg2ts_sequence_start.go
+++ b/internal/protocols/rtmp/message/extended_mpeg2ts_sequence_start.go
@@ -3,7 +3,7 @@ package message
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedMPEG2TSSequenceStart is a MPEG2-TS sequence start extended message.
diff --git a/internal/rtmp/message/extended_sequence_end.go b/internal/protocols/rtmp/message/extended_sequence_end.go
similarity index 89%
rename from internal/rtmp/message/extended_sequence_end.go
rename to internal/protocols/rtmp/message/extended_sequence_end.go
index c36997ba91e..8a2d63216a5 100644
--- a/internal/rtmp/message/extended_sequence_end.go
+++ b/internal/protocols/rtmp/message/extended_sequence_end.go
@@ -3,7 +3,7 @@ package message
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedSequenceEnd is a sequence end extended message.
diff --git a/internal/rtmp/message/extended_sequence_start.go b/internal/protocols/rtmp/message/extended_sequence_start.go
similarity index 94%
rename from internal/rtmp/message/extended_sequence_start.go
rename to internal/protocols/rtmp/message/extended_sequence_start.go
index 87c62b830ef..35faa9cc620 100644
--- a/internal/rtmp/message/extended_sequence_start.go
+++ b/internal/protocols/rtmp/message/extended_sequence_start.go
@@ -3,7 +3,7 @@ package message
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// ExtendedSequenceStart is a sequence start extended message.
diff --git a/internal/rtmp/message/message.go b/internal/protocols/rtmp/message/message.go
similarity index 96%
rename from internal/rtmp/message/message.go
rename to internal/protocols/rtmp/message/message.go
index 92df1f09563..3f82fe754d5 100644
--- a/internal/rtmp/message/message.go
+++ b/internal/protocols/rtmp/message/message.go
@@ -2,7 +2,7 @@
package message
import (
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
const (
diff --git a/internal/rtmp/message/reader.go b/internal/protocols/rtmp/message/reader.go
similarity index 95%
rename from internal/rtmp/message/reader.go
rename to internal/protocols/rtmp/message/reader.go
index 731655d5fa8..7b5654f8060 100644
--- a/internal/rtmp/message/reader.go
+++ b/internal/protocols/rtmp/message/reader.go
@@ -4,8 +4,8 @@ import (
"fmt"
"io"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
func allocateMessage(raw *rawmessage.Message) (Message, error) {
diff --git a/internal/rtmp/message/reader_test.go b/internal/protocols/rtmp/message/reader_test.go
similarity index 95%
rename from internal/rtmp/message/reader_test.go
rename to internal/protocols/rtmp/message/reader_test.go
index cb71ecd793c..dcda6bd3664 100644
--- a/internal/rtmp/message/reader_test.go
+++ b/internal/protocols/rtmp/message/reader_test.go
@@ -8,7 +8,7 @@ import (
"github.com/notedit/rtmp/format/flv/flvio"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
)
var readWriterCases = []struct {
@@ -33,9 +33,9 @@ var readWriterCases = []struct {
DTS: 6013806 * time.Millisecond,
MessageStreamID: 4534543,
Codec: CodecMPEG1Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: Rate44100,
+ Depth: Depth16,
+ IsStereo: true,
Payload: []byte{0x01, 0x02, 0x03, 0x04},
},
[]byte{
@@ -50,9 +50,9 @@ var readWriterCases = []struct {
DTS: 6013806 * time.Millisecond,
MessageStreamID: 4534543,
Codec: CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: Rate44100,
+ Depth: Depth16,
+ IsStereo: true,
AACType: AudioAACTypeAU,
Payload: []byte{0x5A, 0xC0, 0x77, 0x40},
},
diff --git a/internal/rtmp/message/readwriter.go b/internal/protocols/rtmp/message/readwriter.go
similarity index 93%
rename from internal/rtmp/message/readwriter.go
rename to internal/protocols/rtmp/message/readwriter.go
index e0755c63531..fe568807878 100644
--- a/internal/rtmp/message/readwriter.go
+++ b/internal/protocols/rtmp/message/readwriter.go
@@ -3,7 +3,7 @@ package message
import (
"io"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
)
// ReadWriter is a message reader/writer.
diff --git a/internal/rtmp/message/readwriter_test.go b/internal/protocols/rtmp/message/readwriter_test.go
similarity index 95%
rename from internal/rtmp/message/readwriter_test.go
rename to internal/protocols/rtmp/message/readwriter_test.go
index 54327c8c81c..696ed3e2856 100644
--- a/internal/rtmp/message/readwriter_test.go
+++ b/internal/protocols/rtmp/message/readwriter_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
)
type duplexRW struct {
diff --git a/internal/rtmp/message/set_chunk_size.go b/internal/protocols/rtmp/message/set_chunk_size.go
similarity index 92%
rename from internal/rtmp/message/set_chunk_size.go
rename to internal/protocols/rtmp/message/set_chunk_size.go
index 2b7f88fda1a..6eb9251a26f 100644
--- a/internal/rtmp/message/set_chunk_size.go
+++ b/internal/protocols/rtmp/message/set_chunk_size.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// SetChunkSize is a set chunk size message.
diff --git a/internal/rtmp/message/set_peer_bandwidth.go b/internal/protocols/rtmp/message/set_peer_bandwidth.go
similarity index 93%
rename from internal/rtmp/message/set_peer_bandwidth.go
rename to internal/protocols/rtmp/message/set_peer_bandwidth.go
index c1731540c62..3f739412e75 100644
--- a/internal/rtmp/message/set_peer_bandwidth.go
+++ b/internal/protocols/rtmp/message/set_peer_bandwidth.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// SetPeerBandwidth is a set peer bandwidth message.
diff --git a/internal/rtmp/message/set_window_ack_size.go b/internal/protocols/rtmp/message/set_window_ack_size.go
similarity index 93%
rename from internal/rtmp/message/set_window_ack_size.go
rename to internal/protocols/rtmp/message/set_window_ack_size.go
index 566bbb7069e..6cbaa314b41 100644
--- a/internal/rtmp/message/set_window_ack_size.go
+++ b/internal/protocols/rtmp/message/set_window_ack_size.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// SetWindowAckSize is a set window acknowledgement message.
diff --git a/internal/rtmp/message/testdata/fuzz/FuzzReader/05d2521061b772dd b/internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/05d2521061b772dd
similarity index 100%
rename from internal/rtmp/message/testdata/fuzz/FuzzReader/05d2521061b772dd
rename to internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/05d2521061b772dd
diff --git a/internal/rtmp/message/testdata/fuzz/FuzzReader/06f5bdb4e0ba6885 b/internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/06f5bdb4e0ba6885
similarity index 100%
rename from internal/rtmp/message/testdata/fuzz/FuzzReader/06f5bdb4e0ba6885
rename to internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/06f5bdb4e0ba6885
diff --git a/internal/rtmp/message/testdata/fuzz/FuzzReader/2fb5da434799f2aa b/internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/2fb5da434799f2aa
similarity index 100%
rename from internal/rtmp/message/testdata/fuzz/FuzzReader/2fb5da434799f2aa
rename to internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/2fb5da434799f2aa
diff --git a/internal/rtmp/message/testdata/fuzz/FuzzReader/420fac969d79c3d0 b/internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/420fac969d79c3d0
similarity index 100%
rename from internal/rtmp/message/testdata/fuzz/FuzzReader/420fac969d79c3d0
rename to internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/420fac969d79c3d0
diff --git a/internal/rtmp/message/testdata/fuzz/FuzzReader/6b1d357b508b38a4 b/internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/6b1d357b508b38a4
similarity index 100%
rename from internal/rtmp/message/testdata/fuzz/FuzzReader/6b1d357b508b38a4
rename to internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/6b1d357b508b38a4
diff --git a/internal/rtmp/message/testdata/fuzz/FuzzReader/b18a01392c7c91e9 b/internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/b18a01392c7c91e9
similarity index 100%
rename from internal/rtmp/message/testdata/fuzz/FuzzReader/b18a01392c7c91e9
rename to internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/b18a01392c7c91e9
diff --git a/internal/rtmp/message/testdata/fuzz/FuzzReader/f244ff2f55d1103f b/internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/f244ff2f55d1103f
similarity index 100%
rename from internal/rtmp/message/testdata/fuzz/FuzzReader/f244ff2f55d1103f
rename to internal/protocols/rtmp/message/testdata/fuzz/FuzzReader/f244ff2f55d1103f
diff --git a/internal/rtmp/message/user_control_ping_request.go b/internal/protocols/rtmp/message/user_control_ping_request.go
similarity index 93%
rename from internal/rtmp/message/user_control_ping_request.go
rename to internal/protocols/rtmp/message/user_control_ping_request.go
index db69bb779b5..c5056d29539 100644
--- a/internal/rtmp/message/user_control_ping_request.go
+++ b/internal/protocols/rtmp/message/user_control_ping_request.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// UserControlPingRequest is a user control message.
diff --git a/internal/rtmp/message/user_control_ping_response.go b/internal/protocols/rtmp/message/user_control_ping_response.go
similarity index 93%
rename from internal/rtmp/message/user_control_ping_response.go
rename to internal/protocols/rtmp/message/user_control_ping_response.go
index 288b555e7d9..dbcaebd5bee 100644
--- a/internal/rtmp/message/user_control_ping_response.go
+++ b/internal/protocols/rtmp/message/user_control_ping_response.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// UserControlPingResponse is a user control message.
diff --git a/internal/rtmp/message/user_control_set_buffer_length.go b/internal/protocols/rtmp/message/user_control_set_buffer_length.go
similarity index 95%
rename from internal/rtmp/message/user_control_set_buffer_length.go
rename to internal/protocols/rtmp/message/user_control_set_buffer_length.go
index ce7d4793ad4..c9fabece7a0 100644
--- a/internal/rtmp/message/user_control_set_buffer_length.go
+++ b/internal/protocols/rtmp/message/user_control_set_buffer_length.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// UserControlSetBufferLength is a user control message.
diff --git a/internal/rtmp/message/user_control_stream_begin.go b/internal/protocols/rtmp/message/user_control_stream_begin.go
similarity index 93%
rename from internal/rtmp/message/user_control_stream_begin.go
rename to internal/protocols/rtmp/message/user_control_stream_begin.go
index 14672b72032..ef08d7befc1 100644
--- a/internal/rtmp/message/user_control_stream_begin.go
+++ b/internal/protocols/rtmp/message/user_control_stream_begin.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// UserControlStreamBegin is a user control message.
diff --git a/internal/rtmp/message/user_control_stream_dry.go b/internal/protocols/rtmp/message/user_control_stream_dry.go
similarity index 93%
rename from internal/rtmp/message/user_control_stream_dry.go
rename to internal/protocols/rtmp/message/user_control_stream_dry.go
index a54604239dd..f9feaaaf28a 100644
--- a/internal/rtmp/message/user_control_stream_dry.go
+++ b/internal/protocols/rtmp/message/user_control_stream_dry.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// UserControlStreamDry is a user control message.
diff --git a/internal/rtmp/message/user_control_stream_eof.go b/internal/protocols/rtmp/message/user_control_stream_eof.go
similarity index 93%
rename from internal/rtmp/message/user_control_stream_eof.go
rename to internal/protocols/rtmp/message/user_control_stream_eof.go
index a2cd9857afc..83dd2c94090 100644
--- a/internal/rtmp/message/user_control_stream_eof.go
+++ b/internal/protocols/rtmp/message/user_control_stream_eof.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// UserControlStreamEOF is a user control message.
diff --git a/internal/rtmp/message/user_control_stream_is_recorded.go b/internal/protocols/rtmp/message/user_control_stream_is_recorded.go
similarity index 94%
rename from internal/rtmp/message/user_control_stream_is_recorded.go
rename to internal/protocols/rtmp/message/user_control_stream_is_recorded.go
index 9a605b37019..b63d4e5368d 100644
--- a/internal/rtmp/message/user_control_stream_is_recorded.go
+++ b/internal/protocols/rtmp/message/user_control_stream_is_recorded.go
@@ -3,7 +3,7 @@ package message //nolint:dupl
import (
"fmt"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// UserControlStreamIsRecorded is a user control message.
diff --git a/internal/rtmp/message/video.go b/internal/protocols/rtmp/message/video.go
similarity index 90%
rename from internal/rtmp/message/video.go
rename to internal/protocols/rtmp/message/video.go
index 8401a5a4604..287b1099142 100644
--- a/internal/rtmp/message/video.go
+++ b/internal/protocols/rtmp/message/video.go
@@ -4,9 +4,7 @@ import (
"fmt"
"time"
- "github.com/notedit/rtmp/format/flv/flvio"
-
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
const (
@@ -51,7 +49,7 @@ func (m *Video) Unmarshal(raw *rawmessage.Message) error {
return fmt.Errorf("invalid body size")
}
- m.IsKeyFrame = (raw.Body[0] >> 4) == flvio.FRAME_KEY
+ m.IsKeyFrame = (raw.Body[0] >> 4) == 1
m.Codec = raw.Body[0] & 0x0F
switch m.Codec {
@@ -83,9 +81,9 @@ func (m Video) Marshal() (*rawmessage.Message, error) {
body := make([]byte, m.marshalBodySize())
if m.IsKeyFrame {
- body[0] = flvio.FRAME_KEY << 4
+ body[0] = 1 << 4
} else {
- body[0] = flvio.FRAME_INTER << 4
+ body[0] = 2 << 4
}
body[0] |= m.Codec
body[1] = uint8(m.Type)
diff --git a/internal/rtmp/message/writer.go b/internal/protocols/rtmp/message/writer.go
similarity index 85%
rename from internal/rtmp/message/writer.go
rename to internal/protocols/rtmp/message/writer.go
index a0ad2fdfa51..fc8e0c8ec46 100644
--- a/internal/rtmp/message/writer.go
+++ b/internal/protocols/rtmp/message/writer.go
@@ -3,8 +3,8 @@ package message
import (
"io"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/rawmessage"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/rawmessage"
)
// Writer is a message writer.
diff --git a/internal/rtmp/message/writer_test.go b/internal/protocols/rtmp/message/writer_test.go
similarity index 84%
rename from internal/rtmp/message/writer_test.go
rename to internal/protocols/rtmp/message/writer_test.go
index 178a5b64efe..d4eb23951f8 100644
--- a/internal/rtmp/message/writer_test.go
+++ b/internal/protocols/rtmp/message/writer_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
)
func TestWriter(t *testing.T) {
diff --git a/internal/rtmp/rawmessage/message.go b/internal/protocols/rtmp/rawmessage/message.go
similarity index 100%
rename from internal/rtmp/rawmessage/message.go
rename to internal/protocols/rtmp/rawmessage/message.go
diff --git a/internal/rtmp/rawmessage/reader.go b/internal/protocols/rtmp/rawmessage/reader.go
similarity index 87%
rename from internal/rtmp/rawmessage/reader.go
rename to internal/protocols/rtmp/rawmessage/reader.go
index d4e691ad322..37480934644 100644
--- a/internal/rtmp/rawmessage/reader.go
+++ b/internal/protocols/rtmp/rawmessage/reader.go
@@ -7,8 +7,8 @@ import (
"io"
"time"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/chunk"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/chunk"
)
var errMoreChunksNeeded = errors.New("more chunks are needed")
@@ -37,10 +37,11 @@ type readerChunkStream struct {
curBodyRecv uint32
curTimestampDelta uint32
curTimestampDeltaAvailable bool
+ hasExtendedTimestamp bool
}
-func (rc *readerChunkStream) readChunk(c chunk.Chunk, chunkBodySize uint32) error {
- err := c.Read(rc.mr.br, chunkBodySize)
+func (rc *readerChunkStream) readChunk(c chunk.Chunk, bodySize uint32, hasExtendedTimestamp bool) error {
+ err := c.Read(rc.mr.br, bodySize, hasExtendedTimestamp)
if err != nil {
return err
}
@@ -70,7 +71,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
return nil, fmt.Errorf("received type 0 chunk but expected type 3 chunk")
}
- err := rc.readChunk(&rc.mr.c0, rc.mr.chunkSize)
+ err := rc.readChunk(&rc.mr.c0, rc.mr.chunkSize, false)
if err != nil {
return nil, err
}
@@ -81,6 +82,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
rc.curTimestampAvailable = true
rc.curTimestampDeltaAvailable = false
rc.curBodyLen = rc.mr.c0.BodyLen
+ rc.hasExtendedTimestamp = rc.mr.c0.Timestamp >= 0xFFFFFF
if rc.curBodyLen > maxBodySize {
return nil, fmt.Errorf("body size (%d) exceeds maximum (%d)", rc.curBodyLen, maxBodySize)
@@ -109,7 +111,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
return nil, fmt.Errorf("received type 1 chunk but expected type 3 chunk")
}
- err := rc.readChunk(&rc.mr.c1, rc.mr.chunkSize)
+ err := rc.readChunk(&rc.mr.c1, rc.mr.chunkSize, false)
if err != nil {
return nil, err
}
@@ -119,6 +121,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
rc.curTimestampDelta = rc.mr.c1.TimestampDelta
rc.curTimestampDeltaAvailable = true
rc.curBodyLen = rc.mr.c1.BodyLen
+ rc.hasExtendedTimestamp = rc.mr.c1.TimestampDelta >= 0xFFFFFF
if rc.curBodyLen > maxBodySize {
return nil, fmt.Errorf("body size (%d) exceeds maximum (%d)", rc.curBodyLen, maxBodySize)
@@ -152,7 +155,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
chunkBodyLen = rc.mr.chunkSize
}
- err := rc.readChunk(&rc.mr.c2, chunkBodyLen)
+ err := rc.readChunk(&rc.mr.c2, chunkBodyLen, false)
if err != nil {
return nil, err
}
@@ -160,6 +163,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
rc.curTimestamp += rc.mr.c2.TimestampDelta
rc.curTimestampDelta = rc.mr.c2.TimestampDelta
rc.curTimestampDeltaAvailable = true
+ rc.hasExtendedTimestamp = rc.mr.c2.TimestampDelta >= 0xFFFFFF
le := uint32(len(rc.mr.c2.Body))
@@ -182,7 +186,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
chunkBodyLen = rc.mr.chunkSize
}
- err := rc.readChunk(&rc.mr.c3, chunkBodyLen)
+ err := rc.readChunk(&rc.mr.c3, chunkBodyLen, rc.hasExtendedTimestamp)
if err != nil {
return nil, err
}
@@ -212,7 +216,7 @@ func (rc *readerChunkStream) readMessage(typ byte) (*Message, error) {
chunkBodyLen = rc.mr.chunkSize
}
- err := rc.readChunk(&rc.mr.c3, chunkBodyLen)
+ err := rc.readChunk(&rc.mr.c3, chunkBodyLen, rc.hasExtendedTimestamp)
if err != nil {
return nil, err
}
@@ -293,6 +297,10 @@ func (r *Reader) Read() (*Message, error) {
typ := byt >> 6
chunkStreamID := byt & 0x3F
+ if chunkStreamID < 2 {
+ return nil, fmt.Errorf("extended chunk stream IDs are not supported (yet)")
+ }
+
rc, ok := r.chunkStreams[chunkStreamID]
if !ok {
rc = &readerChunkStream{mr: r}
@@ -303,7 +311,7 @@ func (r *Reader) Read() (*Message, error) {
msg, err := rc.readMessage(typ)
if err != nil {
- if err == errMoreChunksNeeded {
+ if errors.Is(err, errMoreChunksNeeded) {
continue
}
return nil, err
diff --git a/internal/rtmp/rawmessage/reader_test.go b/internal/protocols/rtmp/rawmessage/reader_test.go
similarity index 85%
rename from internal/rtmp/rawmessage/reader_test.go
rename to internal/protocols/rtmp/rawmessage/reader_test.go
index 35f6dd1393e..6b65b28e46a 100644
--- a/internal/rtmp/rawmessage/reader_test.go
+++ b/internal/protocols/rtmp/rawmessage/reader_test.go
@@ -7,15 +7,14 @@ import (
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/chunk"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/chunk"
)
var cases = []struct {
- name string
- messages []*Message
- chunks []chunk.Chunk
- chunkSizes []uint32
+ name string
+ messages []*Message
+ chunks []chunk.Chunk
}{
{
"(chunk0) + (chunk1)",
@@ -52,10 +51,6 @@ var cases = []struct {
Body: bytes.Repeat([]byte{0x04}, 64),
},
},
- []uint32{
- 128,
- 128,
- },
},
{
"(chunk0) + (chunk2) + (chunk3)",
@@ -101,11 +96,6 @@ var cases = []struct {
Body: bytes.Repeat([]byte{0x05}, 64),
},
},
- []uint32{
- 128,
- 64,
- 64,
- },
},
{
"(chunk0 + chunk3) + (chunk1 + chunk3) + (chunk2 + chunk3) + (chunk3 + chunk3)",
@@ -181,15 +171,31 @@ var cases = []struct {
Body: bytes.Repeat([]byte{0x06}, 64),
},
},
- []uint32{
- 128,
- 62,
- 128,
- 64,
- 128,
- 64,
- 128,
- 64,
+ },
+ {
+ "(chunk0 + chunk3 with extended timestamp)",
+ []*Message{
+ {
+ ChunkStreamID: 27,
+ Timestamp: 0xFF123456 * time.Millisecond,
+ Type: 6,
+ MessageStreamID: 3123,
+ Body: bytes.Repeat([]byte{5}, 160),
+ },
+ },
+ []chunk.Chunk{
+ &chunk.Chunk0{
+ ChunkStreamID: 27,
+ Timestamp: 4279383126,
+ Type: 6,
+ MessageStreamID: 3123,
+ BodyLen: 160,
+ Body: bytes.Repeat([]byte{5}, 128),
+ },
+ &chunk.Chunk3{
+ ChunkStreamID: 27,
+ Body: bytes.Repeat([]byte{5}, 32),
+ },
},
},
}
@@ -203,10 +209,13 @@ func TestReader(t *testing.T) {
return nil
})
+ hasExtendedTimestamp := false
+
for _, cach := range ca.chunks {
- buf2, err := cach.Marshal()
+ buf2, err := cach.Marshal(hasExtendedTimestamp)
require.NoError(t, err)
buf.Write(buf2)
+ hasExtendedTimestamp = chunkHasExtendedTimestamp(cach)
}
for _, camsg := range ca.messages {
@@ -247,7 +256,7 @@ func TestReaderAcknowledge(t *testing.T) {
MessageStreamID: 3123,
BodyLen: 200,
Body: bytes.Repeat([]byte{0x03}, 200),
- }.Marshal()
+ }.Marshal(false)
require.NoError(t, err)
buf.Write(buf2)
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/19981bffc2abbaf1 b/internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/19981bffc2abbaf1
similarity index 100%
rename from internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/19981bffc2abbaf1
rename to internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/19981bffc2abbaf1
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzReader/2a3abe67115a80dc b/internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/2a3abe67115a80dc
similarity index 100%
rename from internal/rtmp/rawmessage/testdata/fuzz/FuzzReader/2a3abe67115a80dc
rename to internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/2a3abe67115a80dc
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzReader/321edca93ba341df b/internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/321edca93ba341df
similarity index 100%
rename from internal/rtmp/rawmessage/testdata/fuzz/FuzzReader/321edca93ba341df
rename to internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/321edca93ba341df
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzReader/7f07c167964a9467 b/internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/7f07c167964a9467
similarity index 100%
rename from internal/rtmp/rawmessage/testdata/fuzz/FuzzReader/7f07c167964a9467
rename to internal/protocols/rtmp/rawmessage/testdata/fuzz/FuzzReader/7f07c167964a9467
diff --git a/internal/rtmp/rawmessage/writer.go b/internal/protocols/rtmp/rawmessage/writer.go
similarity index 82%
rename from internal/rtmp/rawmessage/writer.go
rename to internal/protocols/rtmp/rawmessage/writer.go
index 790b3c723f0..868f0d5f5b9 100644
--- a/internal/rtmp/rawmessage/writer.go
+++ b/internal/protocols/rtmp/rawmessage/writer.go
@@ -6,20 +6,21 @@ import (
"io"
"time"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/chunk"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/chunk"
)
type writerChunkStream struct {
- mw *Writer
- lastMessageStreamID *uint32
- lastType *uint8
- lastBodyLen *uint32
- lastTimestamp *int64
- lastTimestampDelta *int64
+ mw *Writer
+ lastMessageStreamID *uint32
+ lastType *uint8
+ lastBodyLen *uint32
+ lastTimestamp *int64
+ lastTimestampDelta *int64
+ hasExtendedTimestamp bool
}
-func (wc *writerChunkStream) writeChunk(c chunk.Chunk) error {
+func (wc *writerChunkStream) writeChunk(c chunk.Chunk, hasExtendedTimestamp bool) error {
// check if we received an acknowledge
if wc.mw.checkAcknowledge && wc.mw.ackWindowSize != 0 {
diff := uint32(wc.mw.bcw.Count()) - wc.mw.ackValue
@@ -29,7 +30,7 @@ func (wc *writerChunkStream) writeChunk(c chunk.Chunk) error {
}
}
- buf, err := c.Marshal()
+ buf, err := c.Marshal(hasExtendedTimestamp)
if err != nil {
return err
}
@@ -72,45 +73,51 @@ func (wc *writerChunkStream) writeMessage(msg *Message) error {
switch {
case wc.lastMessageStreamID == nil || timestampDelta == nil || *wc.lastMessageStreamID != msg.MessageStreamID:
+ ts := uint32(timestamp)
err := wc.writeChunk(&chunk.Chunk0{
ChunkStreamID: msg.ChunkStreamID,
- Timestamp: uint32(timestamp),
+ Timestamp: ts,
Type: msg.Type,
MessageStreamID: msg.MessageStreamID,
BodyLen: (bodyLen),
Body: msg.Body[pos : pos+chunkBodyLen],
- })
+ }, false)
if err != nil {
return err
}
+ wc.hasExtendedTimestamp = ts >= 0xFFFFFF
case *wc.lastType != msg.Type || *wc.lastBodyLen != bodyLen:
+ ts := uint32(*timestampDelta)
err := wc.writeChunk(&chunk.Chunk1{
ChunkStreamID: msg.ChunkStreamID,
- TimestampDelta: uint32(*timestampDelta),
+ TimestampDelta: ts,
Type: msg.Type,
BodyLen: (bodyLen),
Body: msg.Body[pos : pos+chunkBodyLen],
- })
+ }, false)
if err != nil {
return err
}
+ wc.hasExtendedTimestamp = ts >= 0xFFFFFF
case wc.lastTimestampDelta == nil || *wc.lastTimestampDelta != *timestampDelta:
+ ts := uint32(*timestampDelta)
err := wc.writeChunk(&chunk.Chunk2{
ChunkStreamID: msg.ChunkStreamID,
- TimestampDelta: uint32(*timestampDelta),
+ TimestampDelta: ts,
Body: msg.Body[pos : pos+chunkBodyLen],
- })
+ }, false)
if err != nil {
return err
}
+ wc.hasExtendedTimestamp = ts >= 0xFFFFFF
default:
err := wc.writeChunk(&chunk.Chunk3{
ChunkStreamID: msg.ChunkStreamID,
Body: msg.Body[pos : pos+chunkBodyLen],
- })
+ }, wc.hasExtendedTimestamp)
if err != nil {
return err
}
@@ -133,7 +140,7 @@ func (wc *writerChunkStream) writeMessage(msg *Message) error {
err := wc.writeChunk(&chunk.Chunk3{
ChunkStreamID: msg.ChunkStreamID,
Body: msg.Body[pos : pos+chunkBodyLen],
- })
+ }, wc.hasExtendedTimestamp)
if err != nil {
return err
}
diff --git a/internal/rtmp/rawmessage/writer_test.go b/internal/protocols/rtmp/rawmessage/writer_test.go
similarity index 60%
rename from internal/rtmp/rawmessage/writer_test.go
rename to internal/protocols/rtmp/rawmessage/writer_test.go
index 7987ff990c8..ff5b7ea1a20 100644
--- a/internal/rtmp/rawmessage/writer_test.go
+++ b/internal/protocols/rtmp/rawmessage/writer_test.go
@@ -6,11 +6,39 @@ import (
"testing"
"time"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/chunk"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/chunk"
"github.com/stretchr/testify/require"
)
+func chunkBodySize(ch chunk.Chunk) uint32 {
+ switch ch := ch.(type) {
+ case *chunk.Chunk0:
+ return uint32(len(ch.Body))
+ case *chunk.Chunk1:
+ return uint32(len(ch.Body))
+ case *chunk.Chunk2:
+ return uint32(len(ch.Body))
+ case *chunk.Chunk3:
+ return uint32(len(ch.Body))
+ }
+ return 0
+}
+
+func chunkHasExtendedTimestamp(ch chunk.Chunk) bool {
+ switch ch := ch.(type) {
+ case *chunk.Chunk0:
+ return ch.Timestamp >= 0xFFFFFF
+ case *chunk.Chunk1:
+ return ch.TimestampDelta >= 0xFFFFFF
+ case *chunk.Chunk2:
+ return ch.TimestampDelta >= 0xFFFFFF
+ case *chunk.Chunk3:
+ return false
+ }
+ return false
+}
+
func TestWriter(t *testing.T) {
for _, ca := range cases {
t.Run(ca.name, func(t *testing.T) {
@@ -23,11 +51,14 @@ func TestWriter(t *testing.T) {
require.NoError(t, err)
}
- for i, cach := range ca.chunks {
+ hasExtendedTimestamp := false
+
+ for _, cach := range ca.chunks {
ch := reflect.New(reflect.TypeOf(cach).Elem()).Interface().(chunk.Chunk)
- err := ch.Read(&buf, ca.chunkSizes[i])
+ err := ch.Read(&buf, chunkBodySize(cach), hasExtendedTimestamp)
require.NoError(t, err)
require.Equal(t, cach, ch)
+ hasExtendedTimestamp = chunkHasExtendedTimestamp(cach)
}
})
}
diff --git a/internal/rtmp/rc4_readwriter.go b/internal/protocols/rtmp/rc4_readwriter.go
similarity index 100%
rename from internal/rtmp/rc4_readwriter.go
rename to internal/protocols/rtmp/rc4_readwriter.go
diff --git a/internal/rtmp/reader.go b/internal/protocols/rtmp/reader.go
similarity index 64%
rename from internal/rtmp/reader.go
rename to internal/protocols/rtmp/reader.go
index 3353811ae0e..ff3da74f121 100644
--- a/internal/rtmp/reader.go
+++ b/internal/protocols/rtmp/reader.go
@@ -2,6 +2,7 @@ package rtmp
import (
"bytes"
+ "errors"
"fmt"
"time"
@@ -13,8 +14,12 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/notedit/rtmp/format/flv/flvio"
- "github.com/bluenviron/mediamtx/internal/rtmp/h264conf"
- "github.com/bluenviron/mediamtx/internal/rtmp/message"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/h264conf"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
+)
+
+const (
+ analyzePeriod = 1 * time.Second
)
// OnDataAV1Func is the prototype of the callback passed to OnDataAV1().
@@ -32,6 +37,12 @@ type OnDataMPEG4AudioFunc func(pts time.Duration, au []byte)
// OnDataMPEG1AudioFunc is the prototype of the callback passed to OnDataMPEG1Audio().
type OnDataMPEG1AudioFunc func(pts time.Duration, frame []byte)
+// OnDataG711Func is the prototype of the callback passed to OnDataG711().
+type OnDataG711Func func(pts time.Duration, samples []byte)
+
+// OnDataLPCMFunc is the prototype of the callback passed to OnDataLPCM().
+type OnDataLPCMFunc func(pts time.Duration, samples []byte)
+
func hasVideo(md flvio.AMFMap) (bool, error) {
v, ok := md.GetV("videocodecid")
if !ok {
@@ -76,11 +87,17 @@ func hasAudio(md flvio.AMFMap, audioTrack *format.Format) (bool, error) {
case 0:
return false, nil
+ case message.CodecMPEG4Audio, message.CodecLPCM:
+ return true, nil
+
case message.CodecMPEG1Audio:
*audioTrack = &format.MPEG1Audio{}
return true, nil
- case message.CodecMPEG4Audio:
+ case message.CodecPCMA:
+ return true, nil
+
+ case message.CodecPCMU:
return true, nil
}
@@ -90,7 +107,7 @@ func hasAudio(md flvio.AMFMap, audioTrack *format.Format) (bool, error) {
}
}
- return false, fmt.Errorf("unsupported audio codec %v", v)
+ return false, fmt.Errorf("unsupported audio codec: %v", v)
}
func h265FindNALU(array []mp4.HEVCNaluArray, typ h265.NALUType) []byte {
@@ -107,7 +124,7 @@ func trackFromH264DecoderConfig(data []byte) (format.Format, error) {
var conf h264conf.Conf
err := conf.Unmarshal(data)
if err != nil {
- return nil, fmt.Errorf("unable to parse H264 config: %v", err)
+ return nil, fmt.Errorf("unable to parse H264 config: %w", err)
}
return &format.H264{
@@ -161,6 +178,9 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
return nil, nil, fmt.Errorf("metadata doesn't contain any track")
}
+ firstReceived := false
+ var startTime time.Duration
+
for {
if (!hasVideo || videoTrack != nil) &&
(!hasAudio || audioTrack != nil) {
@@ -172,24 +192,29 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
return nil, nil, err
}
- switch tmsg := msg.(type) {
+ switch msg := msg.(type) {
case *message.Video:
if !hasVideo {
return nil, nil, fmt.Errorf("unexpected video packet")
}
+ if !firstReceived {
+ firstReceived = true
+ startTime = msg.DTS
+ }
+
if videoTrack == nil {
- if tmsg.Type == message.VideoTypeConfig {
- videoTrack, err = trackFromH264DecoderConfig(tmsg.Payload)
+ if msg.Type == message.VideoTypeConfig {
+ videoTrack, err = trackFromH264DecoderConfig(msg.Payload)
if err != nil {
return nil, nil, err
}
// format used by OBS < 29.1 to publish H265
- } else if tmsg.Type == message.VideoTypeAU && tmsg.IsKeyFrame {
- nalus, err := h264.AVCCUnmarshal(tmsg.Payload)
+ } else if msg.Type == message.VideoTypeAU && msg.IsKeyFrame {
+ nalus, err := h264.AVCCUnmarshal(msg.Payload)
if err != nil {
- if err == h264.ErrAVCCNoNALUs {
+ if errors.Is(err, h264.ErrAVCCNoNALUs) {
continue
}
return nil, nil, err
@@ -225,14 +250,23 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
}
}
+ // video was found, but audio was not
+ if videoTrack != nil && (msg.DTS-startTime) >= analyzePeriod {
+ return videoTrack, nil, nil
+ }
+
case *message.ExtendedSequenceStart:
+ if !hasVideo {
+ return nil, nil, fmt.Errorf("unexpected video packet")
+ }
+
if videoTrack == nil {
- switch tmsg.FourCC {
+ switch msg.FourCC {
case message.FourCCHEVC:
var hvcc mp4.HvcC
- _, err := mp4.Unmarshal(bytes.NewReader(tmsg.Config), uint64(len(tmsg.Config)), &hvcc, mp4.Context{})
+ _, err := mp4.Unmarshal(bytes.NewReader(msg.Config), uint64(len(msg.Config)), &hvcc, mp4.Context{})
if err != nil {
- return nil, nil, fmt.Errorf("invalid H265 configuration: %v", err)
+ return nil, nil, fmt.Errorf("invalid H265 configuration: %w", err)
}
vps := h265FindNALU(hvcc.NaluArrays, h265.NALUType_VPS_NUT)
@@ -251,15 +285,15 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
case message.FourCCAV1:
var av1c mp4.Av1C
- _, err := mp4.Unmarshal(bytes.NewReader(tmsg.Config), uint64(len(tmsg.Config)), &av1c, mp4.Context{})
+ _, err := mp4.Unmarshal(bytes.NewReader(msg.Config), uint64(len(msg.Config)), &av1c, mp4.Context{})
if err != nil {
- return nil, nil, fmt.Errorf("invalid AV1 configuration: %v", err)
+ return nil, nil, fmt.Errorf("invalid AV1 configuration: %w", err)
}
// parse sequence header and metadata contained in ConfigOBUs, but do not use them
_, err = av1.BitstreamUnmarshal(av1c.ConfigOBUs, false)
if err != nil {
- return nil, nil, fmt.Errorf("invalid AV1 configuration: %v", err)
+ return nil, nil, fmt.Errorf("invalid AV1 configuration: %w", err)
}
videoTrack = &format.AV1{
@@ -268,9 +302,9 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
default: // VP9
var vpcc mp4.VpcC
- _, err := mp4.Unmarshal(bytes.NewReader(tmsg.Config), uint64(len(tmsg.Config)), &vpcc, mp4.Context{})
+ _, err := mp4.Unmarshal(bytes.NewReader(msg.Config), uint64(len(msg.Config)), &vpcc, mp4.Context{})
if err != nil {
- return nil, nil, fmt.Errorf("invalid VP9 configuration: %v", err)
+ return nil, nil, fmt.Errorf("invalid VP9 configuration: %w", err)
}
videoTrack = &format.VP9{
@@ -284,12 +318,58 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
return nil, nil, fmt.Errorf("unexpected audio packet")
}
- if audioTrack == nil &&
- tmsg.Codec == message.CodecMPEG4Audio &&
- tmsg.AACType == message.AudioAACTypeConfig {
- audioTrack, err = trackFromAACDecoderConfig(tmsg.Payload)
- if err != nil {
- return nil, nil, err
+ if audioTrack == nil {
+ switch {
+ case msg.Codec == message.CodecMPEG4Audio &&
+ msg.AACType == message.AudioAACTypeConfig:
+ audioTrack, err = trackFromAACDecoderConfig(msg.Payload)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ case msg.Codec == message.CodecPCMA:
+ audioTrack = &format.G711{
+ PayloadTyp: 8,
+ MULaw: false,
+ SampleRate: 8000,
+ ChannelCount: func() int {
+ if msg.IsStereo {
+ return 2
+ }
+ return 1
+ }(),
+ }
+
+ case msg.Codec == message.CodecPCMU:
+ audioTrack = &format.G711{
+ PayloadTyp: 0,
+ MULaw: true,
+ SampleRate: 8000,
+ ChannelCount: func() int {
+ if msg.IsStereo {
+ return 2
+ }
+ return 1
+ }(),
+ }
+
+ case msg.Codec == message.CodecLPCM:
+ audioTrack = &format.LPCM{
+ PayloadTyp: 96,
+ BitDepth: func() int {
+ if msg.Depth == message.Depth16 {
+ return 16
+ }
+ return 8
+ }(),
+ SampleRate: audioRateRTMPToInt(msg.Rate),
+ ChannelCount: func() int {
+ if msg.IsStereo {
+ return 2
+ }
+ return 1
+ }(),
+ }
}
}
}
@@ -297,24 +377,24 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma
}
func tracksFromMessages(conn *Conn, msg message.Message) (format.Format, format.Format, error) {
- var startTime *time.Duration
+ firstReceived := false
+ var startTime time.Duration
var videoTrack format.Format
var audioTrack format.Format
- // analyze 1 second of packets
outer:
for {
- switch tmsg := msg.(type) {
+ switch msg := msg.(type) {
case *message.Video:
- if startTime == nil {
- v := tmsg.DTS
- startTime = &v
+ if !firstReceived {
+ firstReceived = true
+ startTime = msg.DTS
}
- if tmsg.Type == message.VideoTypeConfig {
+ if msg.Type == message.VideoTypeConfig {
if videoTrack == nil {
var err error
- videoTrack, err = trackFromH264DecoderConfig(tmsg.Payload)
+ videoTrack, err = trackFromH264DecoderConfig(msg.Payload)
if err != nil {
return nil, nil, err
}
@@ -326,20 +406,20 @@ outer:
}
}
- if (tmsg.DTS - *startTime) >= 1*time.Second {
+ if (msg.DTS - startTime) >= analyzePeriod {
break outer
}
case *message.Audio:
- if startTime == nil {
- v := tmsg.DTS
- startTime = &v
+ if !firstReceived {
+ firstReceived = true
+ startTime = msg.DTS
}
- if tmsg.AACType == message.AudioAACTypeConfig {
+ if msg.AACType == message.AudioAACTypeConfig {
if audioTrack == nil {
var err error
- audioTrack, err = trackFromAACDecoderConfig(tmsg.Payload)
+ audioTrack, err = trackFromAACDecoderConfig(msg.Payload)
if err != nil {
return nil, nil, err
}
@@ -351,7 +431,7 @@ outer:
}
}
- if (tmsg.DTS - *startTime) >= 1*time.Second {
+ if (msg.DTS - startTime) >= analyzePeriod {
break outer
}
}
@@ -395,52 +475,45 @@ func NewReader(conn *Conn) (*Reader, error) {
}
func (r *Reader) readTracks() (format.Format, format.Format, error) {
- msg, err := func() (message.Message, error) {
- for {
- msg, err := r.conn.Read()
- if err != nil {
- return nil, err
- }
+ for {
+ msg, err := r.conn.Read()
+ if err != nil {
+ return nil, nil, err
+ }
- // skip play start and data start
- if cmd, ok := msg.(*message.CommandAMF0); ok && cmd.Name == "onStatus" {
- continue
- }
+ // skip play start and data start
+ if cmd, ok := msg.(*message.CommandAMF0); ok && cmd.Name == "onStatus" {
+ continue
+ }
- // skip RtmpSampleAccess
- if data, ok := msg.(*message.DataAMF0); ok && len(data.Payload) >= 1 {
- if s, ok := data.Payload[0].(string); ok && s == "|RtmpSampleAccess" {
- continue
- }
+ // skip RtmpSampleAccess
+ if data, ok := msg.(*message.DataAMF0); ok && len(data.Payload) >= 1 {
+ if s, ok := data.Payload[0].(string); ok && s == "|RtmpSampleAccess" {
+ continue
}
-
- return msg, nil
}
- }()
- if err != nil {
- return nil, nil, err
- }
- if data, ok := msg.(*message.DataAMF0); ok && len(data.Payload) >= 1 {
- payload := data.Payload
+ if data, ok := msg.(*message.DataAMF0); ok && len(data.Payload) >= 1 {
+ payload := data.Payload
- if s, ok := payload[0].(string); ok && s == "@setDataFrame" {
- payload = payload[1:]
- }
+ if s, ok := payload[0].(string); ok && s == "@setDataFrame" {
+ payload = payload[1:]
+ }
- if len(payload) >= 1 {
- if s, ok := payload[0].(string); ok && s == "onMetaData" {
- videoTrack, audioTrack, err := tracksFromMetadata(r.conn, payload[1:])
- if err != nil {
- return nil, nil, err
- }
+ if len(payload) >= 1 {
+ if s, ok := payload[0].(string); ok && s == "onMetaData" {
+ videoTrack, audioTrack, err := tracksFromMetadata(r.conn, payload[1:])
+ if err != nil {
+ return nil, nil, err
+ }
- return videoTrack, audioTrack, nil
+ return videoTrack, audioTrack, nil
+ }
}
}
- }
- return tracksFromMessages(r.conn, msg)
+ return tracksFromMessages(r.conn, msg)
+ }
}
// Tracks returns detected tracks
@@ -454,7 +527,7 @@ func (r *Reader) OnDataAV1(cb OnDataAV1Func) {
if msg, ok := msg.(*message.ExtendedCodedFrames); ok {
tu, err := av1.BitstreamUnmarshal(msg.Payload, true)
if err != nil {
- return fmt.Errorf("unable to decode bitstream: %v", err)
+ return fmt.Errorf("unable to decode bitstream: %w", err)
}
cb(msg.DTS, tu)
@@ -480,10 +553,10 @@ func (r *Reader) OnDataH265(cb OnDataH26xFunc) {
case *message.Video:
au, err := h264.AVCCUnmarshal(msg.Payload)
if err != nil {
- if err == h264.ErrAVCCNoNALUs {
+ if errors.Is(err, h264.ErrAVCCNoNALUs) {
return nil
}
- return fmt.Errorf("unable to decode AVCC: %v", err)
+ return fmt.Errorf("unable to decode AVCC: %w", err)
}
cb(msg.DTS+msg.PTSDelta, au)
@@ -491,10 +564,10 @@ func (r *Reader) OnDataH265(cb OnDataH26xFunc) {
case *message.ExtendedFramesX:
au, err := h264.AVCCUnmarshal(msg.Payload)
if err != nil {
- if err == h264.ErrAVCCNoNALUs {
+ if errors.Is(err, h264.ErrAVCCNoNALUs) {
return nil
}
- return fmt.Errorf("unable to decode AVCC: %v", err)
+ return fmt.Errorf("unable to decode AVCC: %w", err)
}
cb(msg.DTS, au)
@@ -502,10 +575,10 @@ func (r *Reader) OnDataH265(cb OnDataH26xFunc) {
case *message.ExtendedCodedFrames:
au, err := h264.AVCCUnmarshal(msg.Payload)
if err != nil {
- if err == h264.ErrAVCCNoNALUs {
+ if errors.Is(err, h264.ErrAVCCNoNALUs) {
return nil
}
- return fmt.Errorf("unable to decode AVCC: %v", err)
+ return fmt.Errorf("unable to decode AVCC: %w", err)
}
cb(msg.DTS+msg.PTSDelta, au)
@@ -524,7 +597,7 @@ func (r *Reader) OnDataH264(cb OnDataH26xFunc) {
var conf h264conf.Conf
err := conf.Unmarshal(msg.Payload)
if err != nil {
- return fmt.Errorf("unable to parse H264 config: %v", err)
+ return fmt.Errorf("unable to parse H264 config: %w", err)
}
au := [][]byte{
@@ -537,10 +610,10 @@ func (r *Reader) OnDataH264(cb OnDataH26xFunc) {
case message.VideoTypeAU:
au, err := h264.AVCCUnmarshal(msg.Payload)
if err != nil {
- if err == h264.ErrAVCCNoNALUs {
+ if errors.Is(err, h264.ErrAVCCNoNALUs) {
return nil
}
- return fmt.Errorf("unable to decode AVCC: %v", err)
+ return fmt.Errorf("unable to decode AVCC: %w", err)
}
cb(msg.DTS+msg.PTSDelta, au)
@@ -569,6 +642,41 @@ func (r *Reader) OnDataMPEG1Audio(cb OnDataMPEG1AudioFunc) {
}
}
+// OnDataG711 sets a callback that is called when G711 data is received.
+func (r *Reader) OnDataG711(cb OnDataG711Func) {
+ r.onDataAudio = func(msg *message.Audio) error {
+ cb(msg.DTS, msg.Payload)
+ return nil
+ }
+}
+
+// OnDataLPCM sets a callback that is called when LPCM data is received.
+func (r *Reader) OnDataLPCM(cb OnDataLPCMFunc) {
+ bitDepth := r.audioTrack.(*format.LPCM).BitDepth
+
+ if bitDepth == 16 {
+ r.onDataAudio = func(msg *message.Audio) error {
+ le := len(msg.Payload)
+ if le%2 != 0 {
+ return fmt.Errorf("invalid payload length: %d", le)
+ }
+
+ // convert from little endian to big endian
+ for i := 0; i < le; i += 2 {
+ msg.Payload[i], msg.Payload[i+1] = msg.Payload[i+1], msg.Payload[i]
+ }
+
+ cb(msg.DTS, msg.Payload)
+ return nil
+ }
+ } else {
+ r.onDataAudio = func(msg *message.Audio) error {
+ cb(msg.DTS, msg.Payload)
+ return nil
+ }
+ }
+}
+
// Read reads data.
func (r *Reader) Read() error {
msg, err := r.conn.Read()
diff --git a/internal/rtmp/reader_test.go b/internal/protocols/rtmp/reader_test.go
similarity index 73%
rename from internal/rtmp/reader_test.go
rename to internal/protocols/rtmp/reader_test.go
index cae9109279f..e6ff927c7f0 100644
--- a/internal/rtmp/reader_test.go
+++ b/internal/protocols/rtmp/reader_test.go
@@ -13,9 +13,9 @@ import (
"github.com/notedit/rtmp/format/flv/flvio"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/h264conf"
- "github.com/bluenviron/mediamtx/internal/rtmp/message"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/h264conf"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
)
func TestReadTracks(t *testing.T) {
@@ -109,7 +109,7 @@ func TestReadTracks(t *testing.T) {
messages []message.Message
}{
{
- "video+audio",
+ "h264 + aac",
&format.H264{
PayloadTyp: 96,
SPS: h264SPS,
@@ -172,9 +172,9 @@ func TestReadTracks(t *testing.T) {
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: func() []byte {
enc, err := mpeg4audio.Config{
@@ -189,7 +189,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "video",
+ "h264",
&format.H264{
PayloadTyp: 96,
SPS: h264SPS,
@@ -241,7 +241,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "issue mediamtx/386 (missing metadata), video+audio",
+ "h264 + aac, issue mediamtx/386 (missing metadata)",
&format.H264{
PayloadTyp: 96,
SPS: h264SPS,
@@ -292,9 +292,9 @@ func TestReadTracks(t *testing.T) {
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: func() []byte {
enc, err := mpeg4audio.Config{
@@ -309,7 +309,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "issue mediamtx/386 (missing metadata), audio",
+ "aac, issue mediamtx/386 (missing metadata)",
nil,
&format.MPEG4Audio{
PayloadTyp: 96,
@@ -327,9 +327,9 @@ func TestReadTracks(t *testing.T) {
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: func() []byte {
enc, err := mpeg4audio.Config{
@@ -345,9 +345,9 @@ func TestReadTracks(t *testing.T) {
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: func() []byte {
enc, err := mpeg4audio.Config{
@@ -363,7 +363,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "obs studio pre 29.1 h265",
+ "h265 + aac, obs studio pre 29.1 h265",
&format.H265{
PayloadTyp: 96,
VPS: h265VPS,
@@ -428,9 +428,9 @@ func TestReadTracks(t *testing.T) {
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: func() []byte {
enc, err := mpeg4audio.Config{
@@ -445,7 +445,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "issue mediamtx/2232 (xsplit broadcaster)",
+ "h265, issue mediamtx/2232 (xsplit broadcaster)",
&format.H265{
PayloadTyp: 96,
VPS: h265VPS,
@@ -494,7 +494,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "obs 30",
+ "h265, obs 30.0",
&format.H265{
PayloadTyp: 96,
VPS: h265VPS,
@@ -543,7 +543,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "ffmpeg av1",
+ "av1, ffmpeg",
&format.AV1{
PayloadTyp: 96,
},
@@ -604,7 +604,7 @@ func TestReadTracks(t *testing.T) {
},
},
{
- "issue mediamtx/2289 (missing videocodecid)",
+ "h264 + aac, issue mediamtx/2289 (missing videocodecid)",
&format.H264{
PayloadTyp: 96,
SPS: []byte{
@@ -674,11 +674,219 @@ func TestReadTracks(t *testing.T) {
Codec: 0xa,
Rate: 0x3,
Depth: 0x1,
- Channels: 0x1,
+ IsStereo: true,
Payload: []uint8{0x11, 0x88},
},
},
},
+ {
+ "h264, issue mediamtx/2352",
+ &format.H264{
+ PayloadTyp: 96,
+ SPS: h264SPS,
+ PPS: h264PPS,
+ PacketizationMode: 1,
+ },
+ nil,
+ []message.Message{
+ &message.DataAMF0{
+ ChunkStreamID: 8,
+ MessageStreamID: 0x1000000,
+ Payload: []interface{}{
+ "@setDataFrame",
+ "onMetaData",
+ flvio.AMFMap{
+ {
+ K: "audiodatarate",
+ V: float64(128),
+ },
+ {
+ K: "framerate",
+ V: float64(30),
+ },
+ {
+ K: "videocodecid",
+ V: float64(7),
+ },
+ {
+ K: "videodatarate",
+ V: float64(2500),
+ },
+ {
+ K: "audiocodecid",
+ V: float64(10),
+ },
+ {
+ K: "height",
+ V: float64(720),
+ },
+ {
+ K: "width",
+ V: float64(1280),
+ },
+ },
+ },
+ },
+ &message.Video{
+ ChunkStreamID: message.VideoChunkStreamID,
+ MessageStreamID: 0x1000000,
+ Codec: message.CodecH264,
+ IsKeyFrame: true,
+ Type: message.VideoTypeConfig,
+ Payload: func() []byte {
+ buf, _ := h264conf.Conf{
+ SPS: h264SPS,
+ PPS: h264PPS,
+ }.Marshal()
+ return buf
+ }(),
+ },
+ &message.Video{
+ ChunkStreamID: message.VideoChunkStreamID,
+ MessageStreamID: 0x1000000,
+ Codec: 0x7,
+ IsKeyFrame: true,
+ Payload: []uint8{
+ 5,
+ },
+ },
+ &message.Video{
+ ChunkStreamID: message.VideoChunkStreamID,
+ MessageStreamID: 0x1000000,
+ Codec: 0x7,
+ IsKeyFrame: true,
+ DTS: 2 * time.Second,
+ Payload: []uint8{
+ 5,
+ },
+ },
+ },
+ },
+ {
+ "mpeg-1 audio",
+ nil,
+ &format.MPEG1Audio{},
+ []message.Message{
+ &message.DataAMF0{
+ ChunkStreamID: 4,
+ MessageStreamID: 1,
+ Payload: []interface{}{
+ "@setDataFrame",
+ "onMetaData",
+ flvio.AMFMap{
+ {K: "duration", V: 0},
+ {K: "audiocodecid", V: 2},
+ {K: "encoder", V: "Lavf58.45.100"},
+ {K: "filesize", V: 0},
+ },
+ },
+ },
+ },
+ },
+ {
+ "pcma",
+ nil,
+ &format.G711{
+ PayloadTyp: 8,
+ MULaw: false,
+ SampleRate: 8000,
+ ChannelCount: 1,
+ },
+ []message.Message{
+ &message.DataAMF0{
+ ChunkStreamID: 4,
+ MessageStreamID: 1,
+ Payload: []interface{}{
+ "@setDataFrame",
+ "onMetaData",
+ flvio.AMFMap{
+ {K: "duration", V: 0},
+ {K: "audiocodecid", V: 7},
+ {K: "encoder", V: "Lavf58.45.100"},
+ {K: "filesize", V: 0},
+ },
+ },
+ },
+ &message.Audio{
+ ChunkStreamID: message.AudioChunkStreamID,
+ MessageStreamID: 0x1000000,
+ Codec: message.CodecPCMA,
+ Rate: message.Rate5512,
+ Depth: message.Depth16,
+ IsStereo: false,
+ Payload: []byte{1, 2, 3, 4},
+ },
+ },
+ },
+ {
+ "pcmu",
+ nil,
+ &format.G711{
+ PayloadTyp: 0,
+ MULaw: true,
+ SampleRate: 8000,
+ ChannelCount: 1,
+ },
+ []message.Message{
+ &message.DataAMF0{
+ ChunkStreamID: 4,
+ MessageStreamID: 1,
+ Payload: []interface{}{
+ "@setDataFrame",
+ "onMetaData",
+ flvio.AMFMap{
+ {K: "duration", V: 0},
+ {K: "audiocodecid", V: 8},
+ {K: "encoder", V: "Lavf58.45.100"},
+ {K: "filesize", V: 0},
+ },
+ },
+ },
+ &message.Audio{
+ ChunkStreamID: message.AudioChunkStreamID,
+ MessageStreamID: 0x1000000,
+ Codec: message.CodecPCMU,
+ Rate: message.Rate5512,
+ Depth: message.Depth16,
+ IsStereo: false,
+ Payload: []byte{1, 2, 3, 4},
+ },
+ },
+ },
+ {
+ "lpcm gstreamer",
+ nil,
+ &format.LPCM{
+ PayloadTyp: 96,
+ BitDepth: 16,
+ SampleRate: 44100,
+ ChannelCount: 2,
+ },
+ []message.Message{
+ &message.DataAMF0{
+ ChunkStreamID: 4,
+ MessageStreamID: 1,
+ Payload: []interface{}{
+ "@setDataFrame",
+ "onMetaData",
+ flvio.AMFMap{
+ {K: "duration", V: 0},
+ {K: "audiocodecid", V: 3},
+ {K: "filesize", V: 0},
+ },
+ },
+ },
+ &message.Audio{
+ ChunkStreamID: message.AudioChunkStreamID,
+ MessageStreamID: 0x1000000,
+ Codec: message.CodecLPCM,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
+ Payload: []byte{1, 2, 3, 4},
+ },
+ },
+ },
} {
t.Run(ca.name, func(t *testing.T) {
var buf bytes.Buffer
diff --git a/internal/rtmp/writer.go b/internal/protocols/rtmp/writer.go
similarity index 81%
rename from internal/rtmp/writer.go
rename to internal/protocols/rtmp/writer.go
index ded9941530c..8ff2af1b5f4 100644
--- a/internal/rtmp/writer.go
+++ b/internal/protocols/rtmp/writer.go
@@ -9,28 +9,38 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/notedit/rtmp/format/flv/flvio"
- "github.com/bluenviron/mediamtx/internal/rtmp/h264conf"
- "github.com/bluenviron/mediamtx/internal/rtmp/message"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/h264conf"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
)
-func mpeg1AudioRate(sr int) uint8 {
- switch sr {
- case 5500:
- return flvio.SOUND_5_5Khz
+func audioRateRTMPToInt(v uint8) int {
+ switch v {
+ case message.Rate5512:
+ return 5512
+ case message.Rate11025:
+ return 11025
+ case message.Rate22050:
+ return 22050
+ default:
+ return 44100
+ }
+}
+
+func audioRateIntToRTMP(v int) uint8 {
+ switch v {
+ case 5512:
+ return message.Rate5512
case 11025:
- return flvio.SOUND_11Khz
+ return message.Rate11025
case 22050:
- return flvio.SOUND_22Khz
+ return message.Rate22050
default:
- return flvio.SOUND_44Khz
+ return message.Rate44100
}
}
-func mpeg1AudioChannels(m mpeg1audio.ChannelMode) uint8 {
- if m == mpeg1audio.ChannelModeMono {
- return flvio.SOUND_MONO
- }
- return flvio.SOUND_STEREO
+func mpeg1AudioChannels(m mpeg1audio.ChannelMode) bool {
+ return m != mpeg1audio.ChannelModeMono
}
// Writer is a wrapper around Conn that provides utilities to mux outgoing data.
@@ -141,9 +151,9 @@ func (w *Writer) writeTracks(videoTrack format.Format, audioTrack format.Format)
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: enc,
})
@@ -180,9 +190,9 @@ func (w *Writer) WriteMPEG4Audio(pts time.Duration, au []byte) error {
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeAU,
Payload: au,
DTS: pts,
@@ -195,9 +205,9 @@ func (w *Writer) WriteMPEG1Audio(pts time.Duration, h *mpeg1audio.FrameHeader, f
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG1Audio,
- Rate: mpeg1AudioRate(h.SampleRate),
- Depth: flvio.SOUND_16BIT,
- Channels: mpeg1AudioChannels(h.ChannelMode),
+ Rate: audioRateIntToRTMP(h.SampleRate),
+ Depth: message.Depth16,
+ IsStereo: mpeg1AudioChannels(h.ChannelMode),
Payload: frame,
DTS: pts,
})
diff --git a/internal/rtmp/writer_test.go b/internal/protocols/rtmp/writer_test.go
similarity index 90%
rename from internal/rtmp/writer_test.go
rename to internal/protocols/rtmp/writer_test.go
index 26c6eed2a44..00fbdd381ba 100644
--- a/internal/rtmp/writer_test.go
+++ b/internal/protocols/rtmp/writer_test.go
@@ -9,8 +9,8 @@ import (
"github.com/notedit/rtmp/format/flv/flvio"
"github.com/stretchr/testify/require"
- "github.com/bluenviron/mediamtx/internal/rtmp/bytecounter"
- "github.com/bluenviron/mediamtx/internal/rtmp/message"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/bytecounter"
+ "github.com/bluenviron/mediamtx/internal/protocols/rtmp/message"
)
func TestWriteTracks(t *testing.T) {
@@ -89,9 +89,9 @@ func TestWriteTracks(t *testing.T) {
ChunkStreamID: message.AudioChunkStreamID,
MessageStreamID: 0x1000000,
Codec: message.CodecMPEG4Audio,
- Rate: flvio.SOUND_44Khz,
- Depth: flvio.SOUND_16BIT,
- Channels: flvio.SOUND_STEREO,
+ Rate: message.Rate44100,
+ Depth: message.Depth16,
+ IsStereo: true,
AACType: message.AudioAACTypeConfig,
Payload: []byte{0x12, 0x10},
}, msg)
diff --git a/internal/protocols/tls/tls_config.go b/internal/protocols/tls/tls_config.go
new file mode 100644
index 00000000000..6d8870b7787
--- /dev/null
+++ b/internal/protocols/tls/tls_config.go
@@ -0,0 +1,35 @@
+// Package tls contains TLS utilities.
+package tls
+
+import (
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "fmt"
+ "strings"
+)
+
+// ConfigForFingerprint returns a tls.Config that supports given fingerprint.
+func ConfigForFingerprint(fingerprint string) *tls.Config {
+ if fingerprint == "" {
+ return nil
+ }
+
+ fingerprintLower := strings.ToLower(fingerprint)
+
+ return &tls.Config{
+ InsecureSkipVerify: true,
+ VerifyConnection: func(cs tls.ConnectionState) error {
+ h := sha256.New()
+ h.Write(cs.PeerCertificates[0].Raw)
+ hstr := hex.EncodeToString(h.Sum(nil))
+
+ if hstr != fingerprintLower {
+ return fmt.Errorf("source fingerprint does not match: expected %s, got %s",
+ fingerprintLower, hstr)
+ }
+
+ return nil
+ },
+ }
+}
diff --git a/internal/protocols/webrtc/api.go b/internal/protocols/webrtc/api.go
new file mode 100644
index 00000000000..d1b9b167da5
--- /dev/null
+++ b/internal/protocols/webrtc/api.go
@@ -0,0 +1,168 @@
+package webrtc
+
+import (
+ "github.com/pion/ice/v2"
+ "github.com/pion/interceptor"
+ "github.com/pion/webrtc/v3"
+)
+
+func stringInSlice(a string, list []string) bool {
+ for _, b := range list {
+ if b == a {
+ return true
+ }
+ }
+ return false
+}
+
+var videoCodecs = []webrtc.RTPCodecParameters{
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeAV1,
+ ClockRate: 90000,
+ },
+ PayloadType: 96,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeVP9,
+ ClockRate: 90000,
+ SDPFmtpLine: "profile-id=0",
+ },
+ PayloadType: 97,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeVP9,
+ ClockRate: 90000,
+ SDPFmtpLine: "profile-id=1",
+ },
+ PayloadType: 98,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeVP8,
+ ClockRate: 90000,
+ },
+ PayloadType: 99,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeH264,
+ ClockRate: 90000,
+ SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
+ },
+ PayloadType: 100,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeH264,
+ ClockRate: 90000,
+ SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
+ },
+ PayloadType: 101,
+ },
+}
+
+var audioCodecs = []webrtc.RTPCodecParameters{
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeOpus,
+ ClockRate: 48000,
+ Channels: 2,
+ SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1",
+ },
+ PayloadType: 111,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeG722,
+ ClockRate: 8000,
+ },
+ PayloadType: 9,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypePCMU,
+ ClockRate: 8000,
+ },
+ PayloadType: 0,
+ },
+ {
+ RTPCodecCapability: webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypePCMA,
+ ClockRate: 8000,
+ },
+ PayloadType: 8,
+ },
+}
+
+// APIConf is the configuration passed to NewAPI().
+type APIConf struct {
+ ICEUDPMux ice.UDPMux
+ ICETCPMux ice.TCPMux
+ LocalRandomUDP bool
+ IPsFromInterfaces bool
+ IPsFromInterfacesList []string
+ AdditionalHosts []string
+}
+
+// NewAPI allocates a webrtc API.
+func NewAPI(cnf APIConf) (*webrtc.API, error) {
+ settingsEngine := webrtc.SettingEngine{}
+
+ settingsEngine.SetInterfaceFilter(func(iface string) bool {
+ return cnf.IPsFromInterfaces && (len(cnf.IPsFromInterfacesList) == 0 ||
+ stringInSlice(iface, cnf.IPsFromInterfacesList))
+ })
+
+ settingsEngine.SetAdditionalHosts(cnf.AdditionalHosts)
+
+ var networkTypes []webrtc.NetworkType
+
+ // always enable UDP in order to support STUN/TURN
+ networkTypes = append(networkTypes, webrtc.NetworkTypeUDP4)
+
+ if cnf.ICEUDPMux != nil {
+ settingsEngine.SetICEUDPMux(cnf.ICEUDPMux)
+ }
+
+ if cnf.ICETCPMux != nil {
+ settingsEngine.SetICETCPMux(cnf.ICETCPMux)
+ networkTypes = append(networkTypes, webrtc.NetworkTypeTCP4)
+ }
+
+ if cnf.LocalRandomUDP {
+ settingsEngine.SetICEUDPRandom(true)
+ }
+
+ settingsEngine.SetNetworkTypes(networkTypes)
+
+ mediaEngine := &webrtc.MediaEngine{}
+
+ for _, codec := range videoCodecs {
+ err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ for _, codec := range audioCodecs {
+ err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeAudio)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ interceptorRegistry := &interceptor.Registry{}
+
+ err := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry)
+ if err != nil {
+ return nil, err
+ }
+
+ return webrtc.NewAPI(
+ webrtc.WithSettingEngine(settingsEngine),
+ webrtc.WithMediaEngine(mediaEngine),
+ webrtc.WithInterceptorRegistry(interceptorRegistry)), nil
+}
diff --git a/internal/whip/ice_fragment.go b/internal/protocols/webrtc/ice_fragment.go
similarity index 99%
rename from internal/whip/ice_fragment.go
rename to internal/protocols/webrtc/ice_fragment.go
index 2c615ff7f29..178ff0fbd76 100644
--- a/internal/whip/ice_fragment.go
+++ b/internal/protocols/webrtc/ice_fragment.go
@@ -1,4 +1,4 @@
-package whip
+package webrtc
import (
"fmt"
diff --git a/internal/whip/ice_fragment_test.go b/internal/protocols/webrtc/ice_fragment_test.go
similarity index 99%
rename from internal/whip/ice_fragment_test.go
rename to internal/protocols/webrtc/ice_fragment_test.go
index 7042d536c72..42554723692 100644
--- a/internal/whip/ice_fragment_test.go
+++ b/internal/protocols/webrtc/ice_fragment_test.go
@@ -1,4 +1,4 @@
-package whip
+package webrtc
import (
"testing"
diff --git a/internal/core/webrtc_incoming_track.go b/internal/protocols/webrtc/incoming_track.go
similarity index 52%
rename from internal/core/webrtc_incoming_track.go
rename to internal/protocols/webrtc/incoming_track.go
index 4f4187f2c41..7f11868f2d7 100644
--- a/internal/core/webrtc_incoming_track.go
+++ b/internal/protocols/webrtc/incoming_track.go
@@ -1,159 +1,122 @@
-package core
+package webrtc
import (
"fmt"
"strings"
"time"
- "github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/gortsplib/v4/pkg/rtptime"
+ "github.com/bluenviron/gortsplib/v4/pkg/liberrors"
+ "github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3"
- "github.com/bluenviron/mediamtx/internal/stream"
+ "github.com/bluenviron/mediamtx/internal/logger"
)
const (
keyFrameInterval = 2 * time.Second
)
-type webRTCIncomingTrack struct {
- track *webrtc.TrackRemote
- receiver *webrtc.RTPReceiver
- writeRTCP func([]rtcp.Packet) error
+// IncomingTrack is an incoming track.
+type IncomingTrack struct {
+ track *webrtc.TrackRemote
+ log logger.Writer
- mediaType description.MediaType
format format.Format
- media *description.Media
+ reorderer *rtpreorderer.Reorderer
+ pkts []*rtp.Packet
}
-func newWebRTCIncomingTrack(
+func newIncomingTrack(
track *webrtc.TrackRemote,
receiver *webrtc.RTPReceiver,
writeRTCP func([]rtcp.Packet) error,
-) (*webRTCIncomingTrack, error) {
- t := &webRTCIncomingTrack{
+ log logger.Writer,
+) (*IncomingTrack, error) {
+ t := &IncomingTrack{
track: track,
- receiver: receiver,
- writeRTCP: writeRTCP,
+ log: log,
+ reorderer: rtpreorderer.New(),
}
+ isVideo := false
+
switch strings.ToLower(track.Codec().MimeType) {
case strings.ToLower(webrtc.MimeTypeAV1):
- t.mediaType = description.MediaTypeVideo
+ isVideo = true
t.format = &format.AV1{
PayloadTyp: uint8(track.PayloadType()),
}
case strings.ToLower(webrtc.MimeTypeVP9):
- t.mediaType = description.MediaTypeVideo
+ isVideo = true
t.format = &format.VP9{
PayloadTyp: uint8(track.PayloadType()),
}
case strings.ToLower(webrtc.MimeTypeVP8):
- t.mediaType = description.MediaTypeVideo
+ isVideo = true
t.format = &format.VP8{
PayloadTyp: uint8(track.PayloadType()),
}
case strings.ToLower(webrtc.MimeTypeH264):
- t.mediaType = description.MediaTypeVideo
+ isVideo = true
t.format = &format.H264{
PayloadTyp: uint8(track.PayloadType()),
PacketizationMode: 1,
}
case strings.ToLower(webrtc.MimeTypeOpus):
- t.mediaType = description.MediaTypeAudio
t.format = &format.Opus{
PayloadTyp: uint8(track.PayloadType()),
+ IsStereo: strings.Contains(track.Codec().SDPFmtpLine, "stereo=1"),
}
case strings.ToLower(webrtc.MimeTypeG722):
- t.mediaType = description.MediaTypeAudio
t.format = &format.G722{}
case strings.ToLower(webrtc.MimeTypePCMU):
- t.mediaType = description.MediaTypeAudio
t.format = &format.G711{
- MULaw: true,
+ PayloadTyp: 0,
+ MULaw: true,
+ SampleRate: 8000,
+ ChannelCount: 1,
}
case strings.ToLower(webrtc.MimeTypePCMA):
- t.mediaType = description.MediaTypeAudio
t.format = &format.G711{
- MULaw: false,
+ PayloadTyp: 8,
+ MULaw: false,
+ SampleRate: 8000,
+ ChannelCount: 1,
}
default:
return nil, fmt.Errorf("unsupported codec: %v", track.Codec())
}
- t.media = &description.Media{
- Type: t.mediaType,
- Formats: []format.Format{t.format},
- }
-
- return t, nil
-}
-
-type webrtcTrackWrapper struct {
- clockRate int
-}
-
-func (w webrtcTrackWrapper) ClockRate() int {
- return w.clockRate
-}
-
-func (webrtcTrackWrapper) PTSEqualsDTS(*rtp.Packet) bool {
- return true
-}
-
-func (t *webRTCIncomingTrack) start(stream *stream.Stream, timeDecoder *rtptime.GlobalDecoder) {
- trackWrapper := &webrtcTrackWrapper{clockRate: int(t.track.Codec().ClockRate)}
-
- go func() {
- for {
- pkt, _, err := t.track.ReadRTP()
- if err != nil {
- return
- }
-
- // sometimes Chrome sends empty RTP packets. ignore them.
- if len(pkt.Payload) == 0 {
- continue
- }
-
- pts, ok := timeDecoder.Decode(trackWrapper, pkt)
- if !ok {
- continue
- }
-
- stream.WriteRTPPacket(t.media, t.format, pkt, time.Now(), pts)
- }
- }()
-
// read incoming RTCP packets to make interceptors work
go func() {
buf := make([]byte, 1500)
for {
- _, _, err := t.receiver.Read(buf)
+ _, _, err := receiver.Read(buf)
if err != nil {
return
}
}
}()
- if t.mediaType == description.MediaTypeVideo {
+ // send period key frame requests
+ if isVideo {
go func() {
keyframeTicker := time.NewTicker(keyFrameInterval)
defer keyframeTicker.Stop()
for range keyframeTicker.C {
- err := t.writeRTCP([]rtcp.Packet{
+ err := writeRTCP([]rtcp.Packet{
&rtcp.PictureLossIndication{
MediaSSRC: uint32(t.track.SSRC()),
},
@@ -164,4 +127,53 @@ func (t *webRTCIncomingTrack) start(stream *stream.Stream, timeDecoder *rtptime.
}
}()
}
+
+ return t, nil
+}
+
+// Format returns the track format.
+func (t *IncomingTrack) Format() format.Format {
+ return t.format
+}
+
+// ReadRTP reads a RTP packet.
+func (t *IncomingTrack) ReadRTP() (*rtp.Packet, error) {
+ for {
+ if len(t.pkts) != 0 {
+ var pkt *rtp.Packet
+ pkt, t.pkts = t.pkts[0], t.pkts[1:]
+
+ // sometimes Chrome sends empty RTP packets. ignore them.
+ if len(pkt.Payload) == 0 {
+ continue
+ }
+
+ return pkt, nil
+ }
+
+ pkt, _, err := t.track.ReadRTP()
+ if err != nil {
+ return nil, err
+ }
+
+ var lost int
+ t.pkts, lost = t.reorderer.Process(pkt)
+ if lost != 0 {
+ t.log.Log(logger.Warn, (liberrors.ErrClientRTPPacketsLost{Lost: lost}).Error())
+ // do not return
+ }
+
+ if len(t.pkts) == 0 {
+ continue
+ }
+
+ pkt, t.pkts = t.pkts[0], t.pkts[1:]
+
+ // sometimes Chrome sends empty RTP packets. ignore them.
+ if len(pkt.Payload) == 0 {
+ continue
+ }
+
+ return pkt, nil
+ }
}
diff --git a/internal/whip/link_header.go b/internal/protocols/webrtc/link_header.go
similarity index 99%
rename from internal/whip/link_header.go
rename to internal/protocols/webrtc/link_header.go
index 4fb338b8475..a7fbfafb250 100644
--- a/internal/whip/link_header.go
+++ b/internal/protocols/webrtc/link_header.go
@@ -1,4 +1,4 @@
-package whip
+package webrtc
import (
"encoding/json"
diff --git a/internal/whip/link_header_test.go b/internal/protocols/webrtc/link_header_test.go
similarity index 98%
rename from internal/whip/link_header_test.go
rename to internal/protocols/webrtc/link_header_test.go
index 80bcdc35e47..4f59ef9c42e 100644
--- a/internal/whip/link_header_test.go
+++ b/internal/protocols/webrtc/link_header_test.go
@@ -1,4 +1,4 @@
-package whip
+package webrtc
import (
"testing"
diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go
new file mode 100644
index 00000000000..ff53c1a5d4e
--- /dev/null
+++ b/internal/protocols/webrtc/outgoing_track.go
@@ -0,0 +1,154 @@
+package webrtc
+
+import (
+ "fmt"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/pion/rtp"
+ "github.com/pion/webrtc/v3"
+)
+
+type addTrackFunc func(webrtc.TrackLocal) (*webrtc.RTPSender, error)
+
+// OutgoingTrack is a WebRTC outgoing track
+type OutgoingTrack struct {
+ track *webrtc.TrackLocalStaticRTP
+}
+
+func newOutgoingTrack(forma format.Format, addTrack addTrackFunc) (*OutgoingTrack, error) {
+ t := &OutgoingTrack{}
+
+ switch forma := forma.(type) {
+ case *format.AV1:
+ var err error
+ t.track, err = webrtc.NewTrackLocalStaticRTP(
+ webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeAV1,
+ ClockRate: 90000,
+ },
+ "av1",
+ webrtcStreamID,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ case *format.VP9:
+ var err error
+ t.track, err = webrtc.NewTrackLocalStaticRTP(
+ webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeVP9,
+ ClockRate: uint32(forma.ClockRate()),
+ },
+ "vp9",
+ webrtcStreamID,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ case *format.VP8:
+ var err error
+ t.track, err = webrtc.NewTrackLocalStaticRTP(
+ webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeVP8,
+ ClockRate: uint32(forma.ClockRate()),
+ },
+ "vp8",
+ webrtcStreamID,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ case *format.H264:
+ var err error
+ t.track, err = webrtc.NewTrackLocalStaticRTP(
+ webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeH264,
+ ClockRate: uint32(forma.ClockRate()),
+ },
+ "h264",
+ webrtcStreamID,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ case *format.Opus:
+ var err error
+ t.track, err = webrtc.NewTrackLocalStaticRTP(
+ webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeOpus,
+ ClockRate: uint32(forma.ClockRate()),
+ Channels: 2,
+ },
+ "opus",
+ webrtcStreamID,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ case *format.G722:
+ var err error
+ t.track, err = webrtc.NewTrackLocalStaticRTP(
+ webrtc.RTPCodecCapability{
+ MimeType: webrtc.MimeTypeG722,
+ ClockRate: uint32(forma.ClockRate()),
+ },
+ "g722",
+ webrtcStreamID,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ case *format.G711:
+ var mtyp string
+ if forma.MULaw {
+ mtyp = webrtc.MimeTypePCMU
+ } else {
+ mtyp = webrtc.MimeTypePCMA
+ }
+
+ var err error
+ t.track, err = webrtc.NewTrackLocalStaticRTP(
+ webrtc.RTPCodecCapability{
+ MimeType: mtyp,
+ ClockRate: uint32(forma.ClockRate()),
+ },
+ "g711",
+ webrtcStreamID,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ default:
+ return nil, fmt.Errorf("unsupported track type: %T", forma)
+ }
+
+ sender, err := addTrack(t.track)
+ if err != nil {
+ return nil, err
+ }
+
+ // read incoming RTCP packets to make interceptors work
+ go func() {
+ buf := make([]byte, 1500)
+ for {
+ _, _, err := sender.Read(buf)
+ if err != nil {
+ return
+ }
+ }
+ }()
+
+ return t, nil
+}
+
+// WriteRTP writes a RTP packet.
+func (t *OutgoingTrack) WriteRTP(pkt *rtp.Packet) error {
+ return t.track.WriteRTP(pkt)
+}
diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go
new file mode 100644
index 00000000000..46903d2fb1c
--- /dev/null
+++ b/internal/protocols/webrtc/peer_connection.go
@@ -0,0 +1,381 @@
+package webrtc
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/pion/webrtc/v3"
+
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+const (
+ webrtcHandshakeTimeout = 10 * time.Second
+ webrtcTrackGatherTimeout = 2 * time.Second
+ webrtcStreamID = "mediamtx"
+)
+
+type nilLogger struct{}
+
+func (nilLogger) Log(_ logger.Level, _ string, _ ...interface{}) {
+}
+
+type trackRecvPair struct {
+ track *webrtc.TrackRemote
+ receiver *webrtc.RTPReceiver
+}
+
+// PeerConnection is a wrapper around webrtc.PeerConnection.
+type PeerConnection struct {
+ ICEServers []webrtc.ICEServer
+ API *webrtc.API
+ Publish bool
+ Log logger.Writer
+
+ wr *webrtc.PeerConnection
+ stateChangeMutex sync.Mutex
+ newLocalCandidate chan *webrtc.ICECandidateInit
+ connected chan struct{}
+ disconnected chan struct{}
+ closed chan struct{}
+ gatheringDone chan struct{}
+ incomingTrack chan trackRecvPair
+}
+
+// Start starts the peer connection.
+func (co *PeerConnection) Start() error {
+ if co.Log == nil {
+ co.Log = &nilLogger{}
+ }
+
+ configuration := webrtc.Configuration{
+ ICEServers: co.ICEServers,
+ }
+
+ var err error
+ co.wr, err = co.API.NewPeerConnection(configuration)
+ if err != nil {
+ return err
+ }
+
+ co.newLocalCandidate = make(chan *webrtc.ICECandidateInit)
+ co.connected = make(chan struct{})
+ co.disconnected = make(chan struct{})
+ co.closed = make(chan struct{})
+ co.gatheringDone = make(chan struct{})
+ co.incomingTrack = make(chan trackRecvPair)
+
+ if !co.Publish {
+ _, err = co.wr.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RtpTransceiverInit{
+ Direction: webrtc.RTPTransceiverDirectionRecvonly,
+ })
+ if err != nil {
+ co.wr.Close() //nolint:errcheck
+ return err
+ }
+
+ _, err = co.wr.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RtpTransceiverInit{
+ Direction: webrtc.RTPTransceiverDirectionRecvonly,
+ })
+ if err != nil {
+ co.wr.Close() //nolint:errcheck
+ return err
+ }
+
+ co.wr.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
+ select {
+ case co.incomingTrack <- trackRecvPair{track, receiver}:
+ case <-co.closed:
+ }
+ })
+ }
+
+ co.wr.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
+ co.stateChangeMutex.Lock()
+ defer co.stateChangeMutex.Unlock()
+
+ select {
+ case <-co.closed:
+ return
+ default:
+ }
+
+ co.Log.Log(logger.Debug, "peer connection state: "+state.String())
+
+ switch state {
+ case webrtc.PeerConnectionStateConnected:
+ co.Log.Log(logger.Info, "peer connection established, local candidate: %v, remote candidate: %v",
+ co.LocalCandidate(), co.RemoteCandidate())
+
+ close(co.connected)
+
+ case webrtc.PeerConnectionStateDisconnected:
+ close(co.disconnected)
+
+ case webrtc.PeerConnectionStateClosed:
+ close(co.closed)
+ }
+ })
+
+ co.wr.OnICECandidate(func(i *webrtc.ICECandidate) {
+ if i != nil {
+ v := i.ToJSON()
+ select {
+ case co.newLocalCandidate <- &v:
+ case <-co.connected:
+ case <-co.closed:
+ }
+ } else {
+ close(co.gatheringDone)
+ }
+ })
+
+ return nil
+}
+
+// Close closes the connection.
+func (co *PeerConnection) Close() {
+ co.wr.Close() //nolint:errcheck
+ <-co.closed
+}
+
+// CreatePartialOffer creates a partial offer.
+func (co *PeerConnection) CreatePartialOffer() (*webrtc.SessionDescription, error) {
+ offer, err := co.wr.CreateOffer(nil)
+ if err != nil {
+ return nil, err
+ }
+
+ err = co.wr.SetLocalDescription(offer)
+ if err != nil {
+ return nil, err
+ }
+
+ return &offer, nil
+}
+
+// SetAnswer sets the answer.
+func (co *PeerConnection) SetAnswer(answer *webrtc.SessionDescription) error {
+ return co.wr.SetRemoteDescription(*answer)
+}
+
+// AddRemoteCandidate adds a remote candidate.
+func (co *PeerConnection) AddRemoteCandidate(candidate webrtc.ICECandidateInit) error {
+ return co.wr.AddICECandidate(candidate)
+}
+
+// CreateFullAnswer creates a full answer.
+func (co *PeerConnection) CreateFullAnswer(
+ ctx context.Context,
+ offer *webrtc.SessionDescription,
+) (*webrtc.SessionDescription, error) {
+ err := co.wr.SetRemoteDescription(*offer)
+ if err != nil {
+ return nil, err
+ }
+
+ answer, err := co.wr.CreateAnswer(nil)
+ if err != nil {
+ return nil, err
+ }
+
+ err = co.wr.SetLocalDescription(answer)
+ if err != nil {
+ return nil, err
+ }
+
+ err = co.WaitGatheringDone(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return co.wr.LocalDescription(), nil
+}
+
+// WaitGatheringDone waits until candidate gathering is complete.
+func (co *PeerConnection) WaitGatheringDone(ctx context.Context) error {
+ for {
+ select {
+ case <-co.NewLocalCandidate():
+ case <-co.GatheringDone():
+ return nil
+ case <-ctx.Done():
+ return fmt.Errorf("terminated")
+ }
+ }
+}
+
+// WaitUntilConnected waits until connection is established.
+func (co *PeerConnection) WaitUntilConnected(
+ ctx context.Context,
+) error {
+ t := time.NewTimer(webrtcHandshakeTimeout)
+ defer t.Stop()
+
+outer:
+ for {
+ select {
+ case <-t.C:
+ return fmt.Errorf("deadline exceeded while waiting connection")
+
+ case <-co.connected:
+ break outer
+
+ case <-ctx.Done():
+ return fmt.Errorf("terminated")
+ }
+ }
+
+ return nil
+}
+
+// GatherIncomingTracks gathers incoming tracks.
+func (co *PeerConnection) GatherIncomingTracks(
+ ctx context.Context,
+ count int,
+) ([]*IncomingTrack, error) {
+ var tracks []*IncomingTrack
+
+ t := time.NewTimer(webrtcTrackGatherTimeout)
+ defer t.Stop()
+
+ for {
+ select {
+ case <-t.C:
+ if count == 0 {
+ return tracks, nil
+ }
+ return nil, fmt.Errorf("deadline exceeded while waiting tracks")
+
+ case pair := <-co.incomingTrack:
+ track, err := newIncomingTrack(pair.track, pair.receiver, co.wr.WriteRTCP, co.Log)
+ if err != nil {
+ return nil, err
+ }
+ tracks = append(tracks, track)
+
+ if len(tracks) == count || len(tracks) >= 2 {
+ return tracks, nil
+ }
+
+ case <-co.Disconnected():
+ return nil, fmt.Errorf("peer connection closed")
+
+ case <-ctx.Done():
+ return nil, fmt.Errorf("terminated")
+ }
+ }
+}
+
+// SetupOutgoingTracks setups outgoing tracks.
+func (co *PeerConnection) SetupOutgoingTracks(
+ videoTrack format.Format,
+ audioTrack format.Format,
+) ([]*OutgoingTrack, error) {
+ var tracks []*OutgoingTrack
+
+ for _, forma := range []format.Format{videoTrack, audioTrack} {
+ if forma != nil {
+ track, err := newOutgoingTrack(forma, co.wr.AddTrack)
+ if err != nil {
+ return nil, err
+ }
+
+ tracks = append(tracks, track)
+ }
+ }
+
+ return tracks, nil
+}
+
+// Connected returns when connected.
+func (co *PeerConnection) Connected() <-chan struct{} {
+ return co.connected
+}
+
+// Disconnected returns when disconnected.
+func (co *PeerConnection) Disconnected() <-chan struct{} {
+ return co.disconnected
+}
+
+// NewLocalCandidate returns when there's a new local candidate.
+func (co *PeerConnection) NewLocalCandidate() <-chan *webrtc.ICECandidateInit {
+ return co.newLocalCandidate
+}
+
+// GatheringDone returns when candidate gathering is complete.
+func (co *PeerConnection) GatheringDone() <-chan struct{} {
+ return co.gatheringDone
+}
+
+// LocalCandidate returns the local candidate.
+func (co *PeerConnection) LocalCandidate() string {
+ var cid string
+ for _, stats := range co.wr.GetStats() {
+ if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated {
+ cid = tstats.LocalCandidateID
+ break
+ }
+ }
+
+ if cid != "" {
+ for _, stats := range co.wr.GetStats() {
+ if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid {
+ return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" +
+ tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10)
+ }
+ }
+ }
+
+ return ""
+}
+
+// RemoteCandidate returns the remote candidate.
+func (co *PeerConnection) RemoteCandidate() string {
+ var cid string
+ for _, stats := range co.wr.GetStats() {
+ if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated {
+ cid = tstats.RemoteCandidateID
+ break
+ }
+ }
+
+ if cid != "" {
+ for _, stats := range co.wr.GetStats() {
+ if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid {
+ return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" +
+ tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10)
+ }
+ }
+ }
+
+ return ""
+}
+
+// BytesReceived returns received bytes.
+func (co *PeerConnection) BytesReceived() uint64 {
+ for _, stats := range co.wr.GetStats() {
+ if tstats, ok := stats.(webrtc.TransportStats); ok {
+ if tstats.ID == "iceTransport" {
+ return tstats.BytesReceived
+ }
+ }
+ }
+ return 0
+}
+
+// BytesSent returns sent bytes.
+func (co *PeerConnection) BytesSent() uint64 {
+ for _, stats := range co.wr.GetStats() {
+ if tstats, ok := stats.(webrtc.TransportStats); ok {
+ if tstats.ID == "iceTransport" {
+ return tstats.BytesSent
+ }
+ }
+ }
+ return 0
+}
diff --git a/internal/protocols/webrtc/track_count.go b/internal/protocols/webrtc/track_count.go
new file mode 100644
index 00000000000..99e9abea126
--- /dev/null
+++ b/internal/protocols/webrtc/track_count.go
@@ -0,0 +1,37 @@
+package webrtc
+
+import (
+ "fmt"
+
+ "github.com/pion/sdp/v3"
+)
+
+// TrackCount returns the track count.
+func TrackCount(medias []*sdp.MediaDescription) (int, error) {
+ videoTrack := false
+ audioTrack := false
+ trackCount := 0
+
+ for _, media := range medias {
+ switch media.MediaName.Media {
+ case "video":
+ if videoTrack {
+ return 0, fmt.Errorf("only a single video and a single audio track are supported")
+ }
+ videoTrack = true
+
+ case "audio":
+ if audioTrack {
+ return 0, fmt.Errorf("only a single video and a single audio track are supported")
+ }
+ audioTrack = true
+
+ default:
+ return 0, fmt.Errorf("unsupported media '%s'", media.MediaName.Media)
+ }
+
+ trackCount++
+ }
+
+ return trackCount, nil
+}
diff --git a/internal/protocols/webrtc/track_wrapper.go b/internal/protocols/webrtc/track_wrapper.go
new file mode 100644
index 00000000000..f336f30dfd3
--- /dev/null
+++ b/internal/protocols/webrtc/track_wrapper.go
@@ -0,0 +1,20 @@
+package webrtc
+
+import (
+ "github.com/pion/rtp"
+)
+
+// TrackWrapper provides ClockRate() and PTSEqualsDTS() to WebRTC tracks.
+type TrackWrapper struct {
+ ClockRat int
+}
+
+// ClockRate returns the clock rate.
+func (w TrackWrapper) ClockRate() int {
+ return w.ClockRat
+}
+
+// PTSEqualsDTS returns whether PTS equals DTS.
+func (TrackWrapper) PTSEqualsDTS(*rtp.Packet) bool {
+ return true
+}
diff --git a/internal/protocols/webrtc/tracks_to_medias.go b/internal/protocols/webrtc/tracks_to_medias.go
new file mode 100644
index 00000000000..809eeec4d53
--- /dev/null
+++ b/internal/protocols/webrtc/tracks_to_medias.go
@@ -0,0 +1,32 @@
+package webrtc
+
+import (
+ "github.com/bluenviron/gortsplib/v4/pkg/description"
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+)
+
+// TracksToMedias converts WebRTC tracks into a media description.
+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},
+ }
+ }
+
+ return ret
+}
diff --git a/internal/protocols/webrtc/webrtc.go b/internal/protocols/webrtc/webrtc.go
new file mode 100644
index 00000000000..53d485f9d75
--- /dev/null
+++ b/internal/protocols/webrtc/webrtc.go
@@ -0,0 +1,2 @@
+// Package webrtc contains WebRTC utilities.
+package webrtc
diff --git a/internal/protocols/webrtc/whip_client.go b/internal/protocols/webrtc/whip_client.go
new file mode 100644
index 00000000000..eb129403157
--- /dev/null
+++ b/internal/protocols/webrtc/whip_client.go
@@ -0,0 +1,219 @@
+package webrtc
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/pion/sdp/v3"
+
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+// WHIPClient is a WHIP client.
+type WHIPClient struct {
+ HTTPClient *http.Client
+ URL *url.URL
+ Log logger.Writer
+
+ pc *PeerConnection
+}
+
+// Publish publishes tracks.
+func (c *WHIPClient) Publish(
+ ctx context.Context,
+ videoTrack format.Format,
+ audioTrack format.Format,
+) ([]*OutgoingTrack, error) {
+ iceServers, err := WHIPOptionsICEServers(ctx, c.HTTPClient, c.URL.String())
+ if err != nil {
+ return nil, err
+ }
+
+ api, err := NewAPI(APIConf{
+ LocalRandomUDP: true,
+ IPsFromInterfaces: true,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ c.pc = &PeerConnection{
+ ICEServers: iceServers,
+ API: api,
+ Publish: true,
+ Log: c.Log,
+ }
+ err = c.pc.Start()
+ if err != nil {
+ return nil, err
+ }
+
+ tracks, err := c.pc.SetupOutgoingTracks(videoTrack, audioTrack)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ offer, err := c.pc.CreatePartialOffer()
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ res, err := PostOffer(ctx, c.HTTPClient, c.URL.String(), offer)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ c.URL, err = c.URL.Parse(res.Location)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ err = c.pc.SetAnswer(res.Answer)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ t := time.NewTimer(webrtcHandshakeTimeout)
+ defer t.Stop()
+
+outer:
+ for {
+ select {
+ case ca := <-c.pc.NewLocalCandidate():
+ err := WHIPPatchCandidate(context.Background(), c.HTTPClient, c.URL.String(), offer, res.ETag, ca)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ case <-c.pc.GatheringDone():
+
+ case <-c.pc.Connected():
+ break outer
+
+ case <-t.C:
+ c.pc.Close()
+ return nil, fmt.Errorf("deadline exceeded while waiting connection")
+ }
+ }
+
+ return tracks, nil
+}
+
+// Read reads tracks.
+func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) {
+ iceServers, err := WHIPOptionsICEServers(ctx, c.HTTPClient, c.URL.String())
+ if err != nil {
+ return nil, err
+ }
+
+ api, err := NewAPI(APIConf{
+ LocalRandomUDP: true,
+ IPsFromInterfaces: true,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ c.pc = &PeerConnection{
+ ICEServers: iceServers,
+ API: api,
+ Publish: false,
+ Log: c.Log,
+ }
+ err = c.pc.Start()
+ if err != nil {
+ return nil, err
+ }
+
+ offer, err := c.pc.CreatePartialOffer()
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ res, err := PostOffer(ctx, c.HTTPClient, c.URL.String(), offer)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ c.URL, err = c.URL.Parse(res.Location)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ var sdp sdp.SessionDescription
+ err = sdp.Unmarshal([]byte(res.Answer.SDP))
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ // check that there are at most two tracks
+ _, err = TrackCount(sdp.MediaDescriptions)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ err = c.pc.SetAnswer(res.Answer)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ t := time.NewTimer(webrtcHandshakeTimeout)
+ defer t.Stop()
+
+outer:
+ for {
+ select {
+ case ca := <-c.pc.NewLocalCandidate():
+ err := WHIPPatchCandidate(context.Background(), c.HTTPClient, c.URL.String(), offer, res.ETag, ca)
+ if err != nil {
+ c.pc.Close()
+ return nil, err
+ }
+
+ case <-c.pc.GatheringDone():
+
+ case <-c.pc.Connected():
+ break outer
+
+ case <-t.C:
+ c.pc.Close()
+ return nil, fmt.Errorf("deadline exceeded while waiting connection")
+ }
+ }
+
+ return c.pc.GatherIncomingTracks(ctx, 0)
+}
+
+// Close closes the client.
+func (c *WHIPClient) Close() error {
+ err := WHIPDeleteSession(context.Background(), c.HTTPClient, c.URL.String())
+ c.pc.Close()
+ return err
+}
+
+// Wait waits for client errors.
+func (c *WHIPClient) Wait(ctx context.Context) error {
+ select {
+ case <-c.pc.Disconnected():
+ return fmt.Errorf("peer connection closed")
+
+ case <-ctx.Done():
+ return fmt.Errorf("terminated")
+ }
+}
diff --git a/internal/protocols/webrtc/whip_delete_session.go b/internal/protocols/webrtc/whip_delete_session.go
new file mode 100644
index 00000000000..19277f597eb
--- /dev/null
+++ b/internal/protocols/webrtc/whip_delete_session.go
@@ -0,0 +1,31 @@
+package webrtc
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+)
+
+// WHIPDeleteSession deletes a WHIP/WHEP session.
+func WHIPDeleteSession(
+ ctx context.Context,
+ hc *http.Client,
+ ur string,
+) error {
+ req, err := http.NewRequestWithContext(ctx, http.MethodDelete, ur, nil)
+ if err != nil {
+ return err
+ }
+
+ res, err := hc.Do(req)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("bad status code: %v", res.StatusCode)
+ }
+
+ return nil
+}
diff --git a/internal/whip/get_ice_servers.go b/internal/protocols/webrtc/whip_options_ice_servers.go
similarity index 72%
rename from internal/whip/get_ice_servers.go
rename to internal/protocols/webrtc/whip_options_ice_servers.go
index 907026aec31..15bdce942ba 100644
--- a/internal/whip/get_ice_servers.go
+++ b/internal/protocols/webrtc/whip_options_ice_servers.go
@@ -1,4 +1,4 @@
-package whip
+package webrtc
import (
"context"
@@ -8,13 +8,13 @@ import (
"github.com/pion/webrtc/v3"
)
-// GetICEServers posts a WHIP/WHEP request for ICE servers.
-func GetICEServers(
+// WHIPOptionsICEServers sends a WHIP/WHEP request for ICE servers.
+func WHIPOptionsICEServers(
ctx context.Context,
hc *http.Client,
ur string,
) ([]webrtc.ICEServer, error) {
- req, err := http.NewRequestWithContext(ctx, "OPTIONS", ur, nil)
+ req, err := http.NewRequestWithContext(ctx, http.MethodOptions, ur, nil)
if err != nil {
return nil, err
}
diff --git a/internal/whip/post_candidate.go b/internal/protocols/webrtc/whip_patch_candidate.go
similarity index 76%
rename from internal/whip/post_candidate.go
rename to internal/protocols/webrtc/whip_patch_candidate.go
index 22b54226f02..975e8840076 100644
--- a/internal/whip/post_candidate.go
+++ b/internal/protocols/webrtc/whip_patch_candidate.go
@@ -1,5 +1,4 @@
-// Package whip contains WebRTC / WHIP utilities.
-package whip
+package webrtc
import (
"bytes"
@@ -10,8 +9,8 @@ import (
"github.com/pion/webrtc/v3"
)
-// PostCandidate posts a WHIP/WHEP candidate.
-func PostCandidate(
+// WHIPPatchCandidate sends a WHIP/WHEP candidate.
+func WHIPPatchCandidate(
ctx context.Context,
hc *http.Client,
ur string,
@@ -24,7 +23,7 @@ func PostCandidate(
return err
}
- req, err := http.NewRequestWithContext(ctx, "PATCH", ur, bytes.NewReader(frag))
+ req, err := http.NewRequestWithContext(ctx, http.MethodPatch, ur, bytes.NewReader(frag))
if err != nil {
return err
}
diff --git a/internal/whip/post_offer.go b/internal/protocols/webrtc/whip_post_offer.go
similarity index 83%
rename from internal/whip/post_offer.go
rename to internal/protocols/webrtc/whip_post_offer.go
index 3e549ecfb18..c32abd055d0 100644
--- a/internal/whip/post_offer.go
+++ b/internal/protocols/webrtc/whip_post_offer.go
@@ -1,4 +1,4 @@
-package whip
+package webrtc
import (
"bytes"
@@ -10,8 +10,8 @@ import (
"github.com/pion/webrtc/v3"
)
-// PostOfferResponse is the response to a post offer.
-type PostOfferResponse struct {
+// WHIPPostOfferResponse is the response to a post offer.
+type WHIPPostOfferResponse struct {
Answer *webrtc.SessionDescription
Location string
ETag string
@@ -23,8 +23,8 @@ func PostOffer(
hc *http.Client,
ur string,
offer *webrtc.SessionDescription,
-) (*PostOfferResponse, error) {
- req, err := http.NewRequestWithContext(ctx, "POST", ur, bytes.NewReader([]byte(offer.SDP)))
+) (*WHIPPostOfferResponse, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, ur, bytes.NewReader([]byte(offer.SDP)))
if err != nil {
return nil, err
}
@@ -68,7 +68,7 @@ func PostOffer(
SDP: string(sdp),
}
- return &PostOfferResponse{
+ return &WHIPPostOfferResponse{
Answer: answer,
Location: Location,
ETag: etag,
diff --git a/internal/websocket/serverconn.go b/internal/protocols/websocket/serverconn.go
similarity index 100%
rename from internal/websocket/serverconn.go
rename to internal/protocols/websocket/serverconn.go
diff --git a/internal/websocket/serverconn_test.go b/internal/protocols/websocket/serverconn_test.go
similarity index 100%
rename from internal/websocket/serverconn_test.go
rename to internal/protocols/websocket/serverconn_test.go
diff --git a/internal/record/agent.go b/internal/record/agent.go
index 1f95749e761..7ecde7db3bd 100644
--- a/internal/record/agent.go
+++ b/internal/record/agent.go
@@ -1,879 +1,92 @@
package record
import (
- "bytes"
- "context"
- "fmt"
- "strings"
"time"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
- "github.com/bluenviron/mediacommon/pkg/codecs/ac3"
- "github.com/bluenviron/mediacommon/pkg/codecs/av1"
- "github.com/bluenviron/mediacommon/pkg/codecs/h264"
- "github.com/bluenviron/mediacommon/pkg/codecs/h265"
- "github.com/bluenviron/mediacommon/pkg/codecs/jpeg"
- "github.com/bluenviron/mediacommon/pkg/codecs/mpeg1audio"
- "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
- "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4video"
- "github.com/bluenviron/mediacommon/pkg/codecs/opus"
- "github.com/bluenviron/mediacommon/pkg/codecs/vp9"
- "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
-
- "github.com/bluenviron/mediamtx/internal/asyncwriter"
+ "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
- "github.com/bluenviron/mediamtx/internal/unit"
)
-func durationGoToMp4(v time.Duration, timeScale uint32) uint64 {
- timeScale64 := uint64(timeScale)
- secs := v / time.Second
- dec := v % time.Second
- return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second)
-}
-
-func mpeg1audioChannelCount(cm mpeg1audio.ChannelMode) int {
- switch cm {
- case mpeg1audio.ChannelModeStereo,
- mpeg1audio.ChannelModeJointStereo,
- mpeg1audio.ChannelModeDualChannel:
- return 2
-
- default:
- return 1
- }
-}
-
-func jpegExtractSize(image []byte) (int, int, error) {
- l := len(image)
- if l < 2 || image[0] != 0xFF || image[1] != jpeg.MarkerStartOfImage {
- return 0, 0, fmt.Errorf("invalid header")
- }
-
- image = image[2:]
-
- for {
- if len(image) < 2 {
- return 0, 0, fmt.Errorf("not enough bits")
- }
-
- h0, h1 := image[0], image[1]
- image = image[2:]
-
- if h0 != 0xFF {
- return 0, 0, fmt.Errorf("invalid image")
- }
-
- switch h1 {
- case 0xE0, 0xE1, 0xE2, // JFIF
- jpeg.MarkerDefineHuffmanTable,
- jpeg.MarkerComment,
- jpeg.MarkerDefineQuantizationTable,
- jpeg.MarkerDefineRestartInterval:
- mlen := int(image[0])<<8 | int(image[1])
- if len(image) < mlen {
- return 0, 0, fmt.Errorf("not enough bits")
- }
- image = image[mlen:]
-
- case jpeg.MarkerStartOfFrame1:
- mlen := int(image[0])<<8 | int(image[1])
- if len(image) < mlen {
- return 0, 0, fmt.Errorf("not enough bits")
- }
-
- var sof jpeg.StartOfFrame1
- err := sof.Unmarshal(image[2:mlen])
- if err != nil {
- return 0, 0, err
- }
-
- return sof.Width, sof.Height, nil
-
- case jpeg.MarkerStartOfScan:
- return 0, 0, fmt.Errorf("SOF not found")
-
- default:
- return 0, 0, fmt.Errorf("unknown marker: 0x%.2x", h1)
- }
- }
-}
-
-type sample struct {
- *fmp4.PartSample
- dts time.Duration
-}
-
-// Agent saves streams on disk.
+// Agent writes recordings to disk.
type Agent struct {
- path string
- partDuration time.Duration
- segmentDuration time.Duration
- stream *stream.Stream
- onSegmentComplete func(string)
- parent logger.Writer
-
- ctx context.Context
- ctxCancel func()
- writer *asyncwriter.Writer
- tracks []*track
- hasVideo bool
- currentSegment *segment
-
- done chan struct{}
+ WriteQueueSize int
+ PathFormat string
+ Format conf.RecordFormat
+ PartDuration time.Duration
+ SegmentDuration time.Duration
+ PathName string
+ Stream *stream.Stream
+ OnSegmentCreate OnSegmentFunc
+ OnSegmentComplete OnSegmentFunc
+ Parent logger.Writer
+
+ restartPause time.Duration
+
+ currentInstance *agentInstance
+
+ terminate chan struct{}
+ done chan struct{}
}
-// NewAgent allocates a nAgent.
-func NewAgent(
- writeQueueSize int,
- recordPath string,
- partDuration time.Duration,
- segmentDuration time.Duration,
- pathName string,
- stream *stream.Stream,
- onSegmentComplete func(string),
- parent logger.Writer,
-) *Agent {
- recordPath = strings.ReplaceAll(recordPath, "%path", pathName)
- recordPath += ".mp4"
-
- if onSegmentComplete == nil {
- onSegmentComplete = func(_ string) {}
- }
-
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- r := &Agent{
- path: recordPath,
- partDuration: partDuration,
- segmentDuration: segmentDuration,
- stream: stream,
- onSegmentComplete: onSegmentComplete,
- parent: parent,
- ctx: ctx,
- ctxCancel: ctxCancel,
- done: make(chan struct{}),
- }
-
- r.writer = asyncwriter.New(writeQueueSize, r)
-
- nextID := 1
-
- addTrack := func(codec fmp4.Codec) *track {
- initTrack := &fmp4.InitTrack{
- TimeScale: 90000,
- Codec: codec,
+// Initialize initializes Agent.
+func (w *Agent) Initialize() {
+ if w.OnSegmentCreate == nil {
+ w.OnSegmentCreate = func(string) {
}
- initTrack.ID = nextID
- nextID++
-
- track := newTrack(r, initTrack)
- r.tracks = append(r.tracks, track)
-
- return track
}
-
- for _, media := range stream.Desc().Medias {
- for _, forma := range media.Formats {
- switch forma := forma.(type) {
- case *format.AV1:
- codec := &fmp4.CodecAV1{
- SequenceHeader: []byte{
- 8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64,
- },
- }
- track := addTrack(codec)
-
- firstReceived := false
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.AV1)
- if tunit.TU == nil {
- return nil
- }
-
- randomAccess := false
-
- for _, obu := range tunit.TU {
- var h av1.OBUHeader
- err := h.Unmarshal(obu)
- if err != nil {
- return err
- }
-
- if h.Type == av1.OBUTypeSequenceHeader {
- if !bytes.Equal(codec.SequenceHeader, obu) {
- codec.SequenceHeader = obu
- r.updateCodecs()
- }
- randomAccess = true
- }
- }
-
- if !firstReceived {
- if !randomAccess {
- return nil
- }
- firstReceived = true
- }
-
- sampl, err := fmp4.NewPartSampleAV1(
- randomAccess,
- tunit.TU)
- if err != nil {
- return err
- }
-
- return track.record(&sample{
- PartSample: sampl,
- dts: tunit.PTS,
- })
- })
-
- case *format.VP9:
- codec := &fmp4.CodecVP9{
- Width: 1280,
- Height: 720,
- Profile: 1,
- BitDepth: 8,
- ChromaSubsampling: 1,
- ColorRange: false,
- }
- track := addTrack(codec)
-
- firstReceived := false
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.VP9)
- if tunit.Frame == nil {
- return nil
- }
-
- var h vp9.Header
- err := h.Unmarshal(tunit.Frame)
- if err != nil {
- return err
- }
-
- randomAccess := false
-
- if h.FrameType == vp9.FrameTypeKeyFrame {
- randomAccess = true
-
- if w := h.Width(); codec.Width != w {
- codec.Width = w
- r.updateCodecs()
- }
- if h := h.Width(); codec.Height != h {
- codec.Height = h
- r.updateCodecs()
- }
- if codec.Profile != h.Profile {
- codec.Profile = h.Profile
- r.updateCodecs()
- }
- if codec.BitDepth != h.ColorConfig.BitDepth {
- codec.BitDepth = h.ColorConfig.BitDepth
- r.updateCodecs()
- }
- if c := h.ChromaSubsampling(); codec.ChromaSubsampling != c {
- codec.ChromaSubsampling = c
- r.updateCodecs()
- }
- if codec.ColorRange != h.ColorConfig.ColorRange {
- codec.ColorRange = h.ColorConfig.ColorRange
- r.updateCodecs()
- }
- }
-
- if !firstReceived {
- if !randomAccess {
- return nil
- }
- firstReceived = true
- }
-
- return track.record(&sample{
- PartSample: &fmp4.PartSample{
- IsNonSyncSample: !randomAccess,
- Payload: tunit.Frame,
- },
- dts: tunit.PTS,
- })
- })
-
- case *format.VP8:
- // TODO
-
- case *format.H265:
- vps, sps, pps := forma.SafeParams()
-
- if vps == nil || sps == nil || pps == nil {
- vps = []byte{
- 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,
- 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,
- 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,
- }
-
- sps = []byte{
- 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,
- 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,
- 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,
- 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,
- 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,
- 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,
- 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,
- 0x02, 0x02, 0x02, 0x01,
- }
-
- pps = []byte{
- 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
- }
- }
-
- codec := &fmp4.CodecH265{
- VPS: vps,
- SPS: sps,
- PPS: pps,
- }
- track := addTrack(codec)
-
- var dtsExtractor *h265.DTSExtractor
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.H265)
- if tunit.AU == nil {
- return nil
- }
-
- randomAccess := false
-
- for _, nalu := range tunit.AU {
- typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
-
- switch typ {
- case h265.NALUType_VPS_NUT:
- if !bytes.Equal(codec.VPS, nalu) {
- codec.VPS = nalu
- r.updateCodecs()
- }
-
- case h265.NALUType_SPS_NUT:
- if !bytes.Equal(codec.SPS, nalu) {
- codec.SPS = nalu
- r.updateCodecs()
- }
-
- case h265.NALUType_PPS_NUT:
- if !bytes.Equal(codec.PPS, nalu) {
- codec.PPS = nalu
- r.updateCodecs()
- }
-
- case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
- randomAccess = true
- }
- }
-
- if dtsExtractor == nil {
- if !randomAccess {
- return nil
- }
- dtsExtractor = h265.NewDTSExtractor()
- }
-
- dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
- if err != nil {
- return err
- }
-
- sampl, err := fmp4.NewPartSampleH26x(
- int32(durationGoToMp4(tunit.PTS-dts, 90000)),
- randomAccess,
- tunit.AU)
- if err != nil {
- return err
- }
-
- return track.record(&sample{
- PartSample: sampl,
- dts: dts,
- })
- })
-
- case *format.H264:
- sps, pps := forma.SafeParams()
-
- if sps == nil || pps == nil {
- sps = []byte{
- 0x67, 0x42, 0xc0, 0x1f, 0xd9, 0x00, 0xf0, 0x11,
- 0x7e, 0xf0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01,
- 0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, 0x18, 0x32,
- 0x48,
- }
-
- pps = []byte{
- 0x68, 0xcb, 0x8c, 0xb2,
- }
- }
-
- codec := &fmp4.CodecH264{
- SPS: sps,
- PPS: pps,
- }
- track := addTrack(codec)
-
- var dtsExtractor *h264.DTSExtractor
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.H264)
- if tunit.AU == nil {
- return nil
- }
-
- randomAccess := false
-
- for _, nalu := range tunit.AU {
- typ := h264.NALUType(nalu[0] & 0x1F)
- switch typ {
- case h264.NALUTypeSPS:
- if !bytes.Equal(codec.SPS, nalu) {
- codec.SPS = nalu
- r.updateCodecs()
- }
-
- case h264.NALUTypePPS:
- if !bytes.Equal(codec.PPS, nalu) {
- codec.PPS = nalu
- r.updateCodecs()
- }
-
- case h264.NALUTypeIDR:
- randomAccess = true
- }
- }
-
- if dtsExtractor == nil {
- if !randomAccess {
- return nil
- }
- dtsExtractor = h264.NewDTSExtractor()
- }
-
- dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
- if err != nil {
- return err
- }
-
- sampl, err := fmp4.NewPartSampleH26x(
- int32(durationGoToMp4(tunit.PTS-dts, 90000)),
- randomAccess,
- tunit.AU)
- if err != nil {
- return err
- }
-
- return track.record(&sample{
- PartSample: sampl,
- dts: dts,
- })
- })
-
- case *format.MPEG4Video:
- config := forma.SafeParams()
-
- if config == nil {
- config = []byte{
- 0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01,
- 0xb5, 0x89, 0x13, 0x00, 0x00, 0x01, 0x00, 0x00,
- 0x00, 0x01, 0x20, 0x00, 0xc4, 0x8d, 0x88, 0x00,
- 0xf5, 0x3c, 0x04, 0x87, 0x14, 0x63, 0x00, 0x00,
- 0x01, 0xb2, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x38,
- 0x2e, 0x31, 0x33, 0x34, 0x2e, 0x31, 0x30, 0x30,
- }
- }
-
- codec := &fmp4.CodecMPEG4Video{
- Config: config,
- }
- track := addTrack(codec)
-
- firstReceived := false
- var lastPTS time.Duration
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG4Video)
- if tunit.Frame == nil {
- return nil
- }
-
- randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
-
- if bytes.HasPrefix(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.VisualObjectSequenceStartCode)}) {
- end := bytes.Index(tunit.Frame[4:], []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
- if end >= 0 {
- config := tunit.Frame[:end+4]
-
- if !bytes.Equal(codec.Config, config) {
- codec.Config = config
- r.updateCodecs()
- }
- }
- }
-
- if !firstReceived {
- if !randomAccess {
- return nil
- }
- firstReceived = true
- } else if tunit.PTS < lastPTS {
- return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)")
- }
- lastPTS = tunit.PTS
-
- return track.record(&sample{
- PartSample: &fmp4.PartSample{
- Payload: tunit.Frame,
- IsNonSyncSample: !randomAccess,
- },
- dts: tunit.PTS,
- })
- })
-
- case *format.MPEG1Video:
- codec := &fmp4.CodecMPEG1Video{
- Config: []byte{
- 0x00, 0x00, 0x01, 0xb3, 0x78, 0x04, 0x38, 0x35,
- 0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5,
- 0x14, 0x4a, 0x00, 0x01, 0x00, 0x00,
- },
- }
- track := addTrack(codec)
-
- firstReceived := false
- var lastPTS time.Duration
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG1Video)
- if tunit.Frame == nil {
- return nil
- }
-
- randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8})
-
- if bytes.HasPrefix(tunit.Frame, []byte{0, 0, 1, 0xB3}) {
- end := bytes.Index(tunit.Frame[4:], []byte{0, 0, 1, 0xB8})
- if end >= 0 {
- config := tunit.Frame[:end+4]
-
- if !bytes.Equal(codec.Config, config) {
- codec.Config = config
- r.updateCodecs()
- }
- }
- }
-
- if !firstReceived {
- if !randomAccess {
- return nil
- }
- firstReceived = true
- } else if tunit.PTS < lastPTS {
- return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
- }
- lastPTS = tunit.PTS
-
- return track.record(&sample{
- PartSample: &fmp4.PartSample{
- Payload: tunit.Frame,
- IsNonSyncSample: !randomAccess,
- },
- dts: tunit.PTS,
- })
- })
-
- case *format.MJPEG:
- codec := &fmp4.CodecMJPEG{
- Width: 800,
- Height: 600,
- }
- track := addTrack(codec)
-
- parsed := false
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MJPEG)
- if tunit.Frame == nil {
- return nil
- }
-
- if !parsed {
- parsed = true
- width, height, err := jpegExtractSize(tunit.Frame)
- if err != nil {
- return err
- }
- codec.Width = width
- codec.Height = height
- r.updateCodecs()
- }
-
- return track.record(&sample{
- PartSample: &fmp4.PartSample{
- Payload: tunit.Frame,
- },
- dts: tunit.PTS,
- })
- })
-
- case *format.Opus:
- codec := &fmp4.CodecOpus{
- ChannelCount: func() int {
- if forma.IsStereo {
- return 2
- }
- return 1
- }(),
- }
- track := addTrack(codec)
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.Opus)
- if tunit.Packets == nil {
- return nil
- }
-
- pts := tunit.PTS
-
- for _, packet := range tunit.Packets {
- err := track.record(&sample{
- PartSample: &fmp4.PartSample{
- Payload: packet,
- },
- dts: pts,
- })
- if err != nil {
- return err
- }
-
- pts += opus.PacketDuration(packet)
- }
-
- return nil
- })
-
- case *format.MPEG4Audio:
- codec := &fmp4.CodecMPEG4Audio{
- Config: *forma.GetConfig(),
- }
- track := addTrack(codec)
-
- sampleRate := time.Duration(forma.ClockRate())
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG4Audio)
- if tunit.AUs == nil {
- return nil
- }
-
- for i, au := range tunit.AUs {
- auPTS := tunit.PTS + time.Duration(i)*mpeg4audio.SamplesPerAccessUnit*
- time.Second/sampleRate
-
- err := track.record(&sample{
- PartSample: &fmp4.PartSample{
- Payload: au,
- },
- dts: auPTS,
- })
- if err != nil {
- return err
- }
- }
-
- return nil
- })
-
- case *format.MPEG1Audio:
- codec := &fmp4.CodecMPEG1Audio{
- SampleRate: 32000,
- ChannelCount: 2,
- }
- track := addTrack(codec)
-
- parsed := false
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.MPEG1Audio)
- if tunit.Frames == nil {
- return nil
- }
-
- pts := tunit.PTS
-
- for _, frame := range tunit.Frames {
- var h mpeg1audio.FrameHeader
- err := h.Unmarshal(frame)
- if err != nil {
- return err
- }
-
- if !parsed {
- parsed = true
- codec.SampleRate = h.SampleRate
- codec.ChannelCount = mpeg1audioChannelCount(h.ChannelMode)
- r.updateCodecs()
- }
-
- err = track.record(&sample{
- PartSample: &fmp4.PartSample{
- Payload: frame,
- },
- dts: pts,
- })
- if err != nil {
- return err
- }
-
- pts += time.Duration(h.SampleCount()) *
- time.Second / time.Duration(h.SampleRate)
- }
-
- return nil
- })
-
- case *format.AC3:
- codec := &fmp4.CodecAC3{
- SampleRate: forma.SampleRate,
- ChannelCount: forma.ChannelCount,
- Fscod: 0,
- Bsid: 8,
- Bsmod: 0,
- Acmod: 7,
- LfeOn: true,
- BitRateCode: 7,
- }
- track := addTrack(codec)
-
- parsed := false
-
- stream.AddReader(r.writer, media, forma, func(u unit.Unit) error {
- tunit := u.(*unit.AC3)
- if tunit.Frames == nil {
- return nil
- }
-
- pts := tunit.PTS
-
- for _, frame := range tunit.Frames {
- var syncInfo ac3.SyncInfo
- err := syncInfo.Unmarshal(frame)
- if err != nil {
- return fmt.Errorf("invalid AC-3 frame: %s", err)
- }
-
- var bsi ac3.BSI
- err = bsi.Unmarshal(frame[5:])
- if err != nil {
- return fmt.Errorf("invalid AC-3 frame: %s", err)
- }
-
- if !parsed {
- parsed = true
- codec.SampleRate = syncInfo.SampleRate()
- codec.ChannelCount = bsi.ChannelCount()
- codec.Fscod = syncInfo.Fscod
- codec.Bsid = bsi.Bsid
- codec.Bsmod = bsi.Bsmod
- codec.Acmod = bsi.Acmod
- codec.LfeOn = bsi.LfeOn
- codec.BitRateCode = syncInfo.Frmsizecod >> 1
- r.updateCodecs()
- }
-
- err = track.record(&sample{
- PartSample: &fmp4.PartSample{
- Payload: frame,
- },
- dts: pts,
- })
- if err != nil {
- return err
- }
-
- pts += time.Duration(ac3.SamplesPerFrame) *
- time.Second / time.Duration(codec.SampleRate)
- }
-
- return nil
- })
-
- case *format.G722:
- // TODO
-
- case *format.G711:
- // TODO
-
- case *format.LPCM:
- // TODO
- }
+ if w.OnSegmentComplete == nil {
+ w.OnSegmentComplete = func(string) {
}
}
+ if w.restartPause == 0 {
+ w.restartPause = 2 * time.Second
+ }
- r.Log(logger.Info, "recording %d %s",
- len(r.tracks),
- func() string {
- if len(r.tracks) == 1 {
- return "track"
- }
- return "tracks"
- }())
+ w.terminate = make(chan struct{})
+ w.done = make(chan struct{})
- go r.run()
+ w.currentInstance = &agentInstance{
+ agent: w,
+ }
+ w.currentInstance.initialize()
- return r
+ go w.run()
}
-// Close closes the Agent.
-func (r *Agent) Close() {
- r.Log(logger.Info, "recording stopped")
-
- r.ctxCancel()
- <-r.done
+// Log implements logger.Writer.
+func (w *Agent) Log(level logger.Level, format string, args ...interface{}) {
+ w.Parent.Log(level, "[record] "+format, args...)
}
-// Log is the main logging function.
-func (r *Agent) Log(level logger.Level, format string, args ...interface{}) {
- r.parent.Log(level, "[record] "+format, args...)
+// Close closes the agent.
+func (w *Agent) Close() {
+ w.Log(logger.Info, "recording stopped")
+ close(w.terminate)
+ <-w.done
}
-func (r *Agent) run() {
- defer close(r.done)
+func (w *Agent) run() {
+ defer close(w.done)
- r.writer.Start()
-
- select {
- case err := <-r.writer.Error():
- r.Log(logger.Error, err.Error())
- r.stream.RemoveReader(r.writer)
-
- case <-r.ctx.Done():
- r.stream.RemoveReader(r.writer)
- r.writer.Stop()
- }
+ for {
+ select {
+ case <-w.currentInstance.done:
+ w.currentInstance.close()
+ case <-w.terminate:
+ w.currentInstance.close()
+ return
+ }
- if r.currentSegment != nil {
- r.currentSegment.close() //nolint:errcheck
- }
-}
+ select {
+ case <-time.After(w.restartPause):
+ case <-w.terminate:
+ return
+ }
-func (r *Agent) updateCodecs() {
- // if codec parameters have been updated,
- // and current segment has already written codec parameters on disk,
- // close current segment.
- if r.currentSegment != nil && r.currentSegment.f != nil {
- r.currentSegment.close() //nolint:errcheck
- r.currentSegment = nil
+ w.currentInstance = &agentInstance{
+ agent: w,
+ }
+ w.currentInstance.initialize()
}
}
diff --git a/internal/record/agent_instance.go b/internal/record/agent_instance.go
new file mode 100644
index 00000000000..8e722a8fa4a
--- /dev/null
+++ b/internal/record/agent_instance.go
@@ -0,0 +1,85 @@
+package record
+
+import (
+ "strings"
+ "time"
+
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+
+ "github.com/bluenviron/mediamtx/internal/asyncwriter"
+ "github.com/bluenviron/mediamtx/internal/conf"
+ "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
+ ntp time.Time
+}
+
+type agentInstance struct {
+ agent *Agent
+
+ pathFormat string
+ writer *asyncwriter.Writer
+ format format
+
+ terminate chan struct{}
+ done chan struct{}
+}
+
+func (a *agentInstance) initialize() {
+ a.pathFormat = a.agent.PathFormat
+
+ a.pathFormat = PathAddExtension(
+ strings.ReplaceAll(a.pathFormat, "%path", a.agent.PathName),
+ a.agent.Format,
+ )
+
+ a.terminate = make(chan struct{})
+ a.done = make(chan struct{})
+
+ a.writer = asyncwriter.New(a.agent.WriteQueueSize, a.agent)
+
+ switch a.agent.Format {
+ case conf.RecordFormatMPEGTS:
+ a.format = &formatMPEGTS{
+ a: a,
+ }
+ a.format.initialize()
+
+ default:
+ a.format = &formatFMP4{
+ a: a,
+ }
+ a.format.initialize()
+ }
+
+ go a.run()
+}
+
+func (a *agentInstance) close() {
+ close(a.terminate)
+ <-a.done
+}
+
+func (a *agentInstance) run() {
+ defer close(a.done)
+
+ a.writer.Start()
+
+ select {
+ case err := <-a.writer.Error():
+ a.agent.Log(logger.Error, err.Error())
+ a.agent.Stream.RemoveReader(a.writer)
+
+ case <-a.terminate:
+ a.agent.Stream.RemoveReader(a.writer)
+ a.writer.Stop()
+ }
+
+ a.format.close()
+}
diff --git a/internal/record/agent_test.go b/internal/record/agent_test.go
index 8ae9880064b..0332370b79e 100644
--- a/internal/record/agent_test.go
+++ b/internal/record/agent_test.go
@@ -7,12 +7,14 @@ import (
"time"
"github.com/bluenviron/gortsplib/v4/pkg/description"
- "github.com/bluenviron/gortsplib/v4/pkg/format"
+ rtspformat "github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediacommon/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/stretchr/testify/require"
+ "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
)
@@ -23,32 +25,229 @@ func (nilLogger) Log(_ logger.Level, _ string, _ ...interface{}) {
}
func TestAgent(t *testing.T) {
- n := 0
- timeNow = func() time.Time {
- n++
- if n >= 2 {
- return time.Date(2008, 0o5, 20, 22, 15, 25, 125000, time.UTC)
- }
- return time.Date(2009, 0o5, 20, 22, 15, 25, 427000, time.UTC)
- }
-
desc := &description.Session{Medias: []*description.Media{
{
Type: description.MediaTypeVideo,
- Formats: []format.Format{&format.H265{
+ Formats: []rtspformat.Format{&rtspformat.H265{
PayloadTyp: 96,
}},
},
{
Type: description.MediaTypeVideo,
- Formats: []format.Format{&format.H264{
+ Formats: []rtspformat.Format{&rtspformat.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}},
},
{
Type: description.MediaTypeAudio,
- Formats: []format.Format{&format.MPEG4Audio{
+ Formats: []rtspformat.Format{&rtspformat.MPEG4Audio{
+ PayloadTyp: 96,
+ Config: &mpeg4audio.Config{
+ Type: 2,
+ SampleRate: 44100,
+ ChannelCount: 2,
+ },
+ SizeLength: 13,
+ IndexLength: 3,
+ IndexDeltaLength: 3,
+ }},
+ },
+ {
+ Type: description.MediaTypeAudio,
+ Formats: []rtspformat.Format{&rtspformat.G711{
+ PayloadTyp: 8,
+ MULaw: false,
+ SampleRate: 8000,
+ ChannelCount: 1,
+ }},
+ },
+ {
+ Type: description.MediaTypeAudio,
+ Formats: []rtspformat.Format{&rtspformat.G711{
+ PayloadTyp: 0,
+ MULaw: true,
+ SampleRate: 8000,
+ ChannelCount: 1,
+ }},
+ },
+ }}
+
+ writeToStream := func(stream *stream.Stream, ntp time.Time) {
+ for i := 0; i < 3; i++ {
+ stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H265{
+ Base: unit.Base{
+ PTS: (50 + time.Duration(i)) * time.Second,
+ NTP: ntp.Add(time.Duration(i) * 60 * time.Second),
+ },
+ AU: [][]byte{
+ { // VPS
+ 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,
+ 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,
+ 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,
+ },
+ { // SPS
+ 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,
+ 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,
+ 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,
+ 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,
+ 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,
+ 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,
+ 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,
+ 0x02, 0x02, 0x02, 0x01,
+ },
+ { // PPS
+ 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
+ },
+ {byte(h265.NALUType_CRA_NUT) << 1, 0}, // IDR
+ },
+ })
+
+ stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H264{
+ Base: unit.Base{
+ PTS: (50 + time.Duration(i)) * time.Second,
+ },
+ AU: [][]byte{
+ { // SPS
+ 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,
+ },
+ { // PPS
+ 0x08, 0x06, 0x07, 0x08,
+ },
+ {5}, // IDR
+ },
+ })
+
+ stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{
+ Base: unit.Base{
+ PTS: (50 + time.Duration(i)) * time.Second,
+ },
+ AUs: [][]byte{{1, 2, 3, 4}},
+ })
+
+ stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{
+ Base: unit.Base{
+ PTS: (50 + time.Duration(i)) * time.Second,
+ },
+ Samples: []byte{1, 2, 3, 4},
+ })
+
+ stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.G711{
+ Base: unit.Base{
+ PTS: (50 + time.Duration(i)) * time.Second,
+ },
+ Samples: []byte{1, 2, 3, 4},
+ })
+ }
+ }
+
+ for _, ca := range []string{"fmp4", "mpegts"} {
+ t.Run(ca, func(t *testing.T) {
+ stream, err := stream.New(
+ 1460,
+ desc,
+ true,
+ &nilLogger{},
+ )
+ require.NoError(t, err)
+ defer stream.Close()
+
+ dir, err := os.MkdirTemp("", "mediamtx-agent")
+ require.NoError(t, err)
+ defer os.RemoveAll(dir)
+
+ recordPath := filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")
+
+ segCreated := make(chan struct{}, 4)
+ segDone := make(chan struct{}, 4)
+
+ var f conf.RecordFormat
+ if ca == "fmp4" {
+ f = conf.RecordFormatFMP4
+ } else {
+ f = conf.RecordFormatMPEGTS
+ }
+
+ w := &Agent{
+ WriteQueueSize: 1024,
+ PathFormat: recordPath,
+ Format: f,
+ PartDuration: 100 * time.Millisecond,
+ SegmentDuration: 1 * time.Second,
+ PathName: "mypath",
+ Stream: stream,
+ OnSegmentCreate: func(fpath string) {
+ segCreated <- struct{}{}
+ },
+ OnSegmentComplete: func(fpath string) {
+ segDone <- struct{}{}
+ },
+ Parent: &nilLogger{},
+ restartPause: 1 * time.Millisecond,
+ }
+ w.Initialize()
+
+ writeToStream(stream, time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC))
+
+ // simulate a write error
+ stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H264{
+ Base: unit.Base{
+ PTS: 0,
+ },
+ AU: [][]byte{
+ {5}, // IDR
+ },
+ })
+
+ for i := 0; i < 2; i++ {
+ <-segCreated
+ <-segDone
+ }
+
+ var ext string
+ if ca == "fmp4" {
+ ext = "mp4"
+ } else {
+ ext = "ts"
+ }
+
+ _, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext))
+ require.NoError(t, err)
+
+ _, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext))
+ require.NoError(t, err)
+
+ time.Sleep(50 * time.Millisecond)
+
+ writeToStream(stream, 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)
+
+ _, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-16-25-000000."+ext))
+ require.NoError(t, err)
+ })
+ }
+}
+
+func TestAgentFMP4NegativeDTS(t *testing.T) {
+ desc := &description.Session{Medias: []*description.Media{
+ {
+ Type: description.MediaTypeVideo,
+ Formats: []rtspformat.Format{&rtspformat.H264{
+ PayloadTyp: 96,
+ PacketizationMode: 1,
+ }},
+ },
+ {
+ Type: description.MediaTypeAudio,
+ Formats: []rtspformat.Format{&rtspformat.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.Config{
Type: 2,
@@ -77,52 +276,23 @@ func TestAgent(t *testing.T) {
recordPath := filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")
- segDone := make(chan struct{}, 2)
-
- a := NewAgent(
- 1024,
- recordPath,
- 100*time.Millisecond,
- 1*time.Second,
- "mypath",
- stream,
- func(fpath string) {
- segDone <- struct{}{}
- },
- &nilLogger{},
- )
+ w := &Agent{
+ WriteQueueSize: 1024,
+ PathFormat: recordPath,
+ Format: conf.RecordFormatFMP4,
+ PartDuration: 100 * time.Millisecond,
+ SegmentDuration: 1 * time.Second,
+ PathName: "mypath",
+ Stream: stream,
+ Parent: &nilLogger{},
+ }
+ w.Initialize()
for i := 0; i < 3; i++ {
- stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H265{
+ stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
Base: unit.Base{
- PTS: (50 + time.Duration(i)) * time.Second,
- },
- AU: [][]byte{
- { // VPS
- 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,
- 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,
- 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,
- },
- { // SPS
- 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,
- 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,
- 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,
- 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,
- 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,
- 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,
- 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,
- 0x02, 0x02, 0x02, 0x01,
- },
- { // PPS
- 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
- },
- {byte(h265.NALUType_CRA_NUT) << 1, 0}, // IDR
- },
- })
-
- stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H264{
- Base: unit.Base{
- PTS: (50 + time.Duration(i)) * time.Second,
+ PTS: -50*time.Millisecond + (time.Duration(i) * 200 * time.Millisecond),
+ NTP: time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC),
},
AU: [][]byte{
{ // SPS
@@ -137,21 +307,35 @@ func TestAgent(t *testing.T) {
},
})
- stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{
+ stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.MPEG4Audio{
Base: unit.Base{
- PTS: (50 + time.Duration(i)) * time.Second,
+ PTS: -100*time.Millisecond + (time.Duration(i) * 200 * time.Millisecond),
},
AUs: [][]byte{{1, 2, 3, 4}},
})
}
- <-segDone
- <-segDone
- a.Close()
+ time.Sleep(50 * time.Millisecond)
+
+ w.Close()
- _, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000125.mp4"))
+ byts, err := os.ReadFile(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000.mp4"))
require.NoError(t, err)
- _, err = os.Stat(filepath.Join(dir, "mypath", "2009-05-20_22-15-25-000427.mp4"))
+ var parts fmp4.Parts
+ err = parts.Unmarshal(byts)
require.NoError(t, err)
+
+ found := false
+
+ for _, part := range parts {
+ for _, track := range part.Tracks {
+ if track.ID == 2 {
+ require.Less(t, track.BaseTime, uint64(1*90000))
+ found = true
+ }
+ }
+ }
+
+ require.Equal(t, true, found)
}
diff --git a/internal/record/cleaner.go b/internal/record/cleaner.go
index b5cac5734f9..695b0d1d301 100644
--- a/internal/record/cleaner.go
+++ b/internal/record/cleaner.go
@@ -5,72 +5,38 @@ import (
"io/fs"
"os"
"path/filepath"
- "strings"
"time"
+ "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
)
-func commonPath(v string) string {
- common := ""
- remaining := v
+var timeNow = time.Now
- for {
- i := strings.IndexAny(remaining, "\\/")
- if i < 0 {
- break
- }
-
- var part string
- part, remaining = remaining[:i+1], remaining[i+1:]
-
- if strings.Contains(part, "%") {
- break
- }
-
- common += part
- }
-
- if len(common) > 0 {
- common = common[:len(common)-1]
- }
-
- return common
+// CleanerEntry is a cleaner entry.
+type CleanerEntry struct {
+ Path string
+ Format conf.RecordFormat
+ DeleteAfter time.Duration
}
-// Cleaner removes expired recordings from disk.
+// Cleaner removes expired recording segments from disk.
type Cleaner struct {
- ctx context.Context
- ctxCancel func()
- path string
- deleteAfter time.Duration
- parent logger.Writer
+ Entries []CleanerEntry
+ Parent logger.Writer
+
+ ctx context.Context
+ ctxCancel func()
done chan struct{}
}
-// NewCleaner allocates a Cleaner.
-func NewCleaner(
- recordPath string,
- deleteAfter time.Duration,
- parent logger.Writer,
-) *Cleaner {
- recordPath += ".mp4"
-
- ctx, ctxCancel := context.WithCancel(context.Background())
-
- c := &Cleaner{
- ctx: ctx,
- ctxCancel: ctxCancel,
- path: recordPath,
- deleteAfter: deleteAfter,
- parent: parent,
- done: make(chan struct{}),
- }
+// Initialize initializes a Cleaner.
+func (c *Cleaner) Initialize() {
+ c.ctx, c.ctxCancel = context.WithCancel(context.Background())
+ c.done = make(chan struct{})
go c.run()
-
- return c
}
// Close closes the Cleaner.
@@ -79,17 +45,19 @@ func (c *Cleaner) Close() {
<-c.done
}
-// Log is the main logging function.
+// Log implements logger.Writer.
func (c *Cleaner) Log(level logger.Level, format string, args ...interface{}) {
- c.parent.Log(level, "[record cleaner]"+format, args...)
+ c.Parent.Log(level, "[record cleaner]"+format, args...)
}
func (c *Cleaner) run() {
defer close(c.done)
interval := 30 * 60 * time.Second
- if interval > (c.deleteAfter / 2) {
- interval = c.deleteAfter / 2
+ for _, e := range c.Entries {
+ if interval > (e.DeleteAfter / 2) {
+ interval = e.DeleteAfter / 2
+ }
}
c.doRun() //nolint:errcheck
@@ -97,7 +65,7 @@ func (c *Cleaner) run() {
for {
select {
case <-time.After(interval):
- c.doRun() //nolint:errcheck
+ c.doRun()
case <-c.ctx.Done():
return
@@ -105,21 +73,34 @@ func (c *Cleaner) run() {
}
}
-func (c *Cleaner) doRun() error {
- commonPath := commonPath(c.path)
+func (c *Cleaner) doRun() {
+ for _, e := range c.Entries {
+ c.doRunEntry(&e) //nolint:errcheck
+ }
+}
+
+func (c *Cleaner) doRunEntry(e *CleanerEntry) error {
+ entryPath := PathAddExtension(e.Path, e.Format)
+
+ // we have to convert to absolute paths
+ // otherwise, entryPath and fpath inside Walk() won't have common elements
+ entryPath, _ = filepath.Abs(entryPath)
+
+ commonPath := CommonPath(entryPath)
now := timeNow()
- filepath.Walk(commonPath, func(path string, info fs.FileInfo, err error) error { //nolint:errcheck
+ filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error { //nolint:errcheck
if err != nil {
return err
}
if !info.IsDir() {
- params := decodeRecordPath(c.path, path)
- if params != nil {
- if now.Sub(params.time) > c.deleteAfter {
- c.Log(logger.Debug, "removing %s", path)
- os.Remove(path)
+ var pa Path
+ ok := pa.Decode(entryPath, fpath)
+ if ok {
+ if now.Sub(time.Time(pa)) > e.DeleteAfter {
+ c.Log(logger.Debug, "removing %s", fpath)
+ os.Remove(fpath)
}
}
}
@@ -127,13 +108,13 @@ func (c *Cleaner) doRun() error {
return nil
})
- filepath.Walk(commonPath, func(path string, info fs.FileInfo, err error) error { //nolint:errcheck
+ filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error { //nolint:errcheck
if err != nil {
return err
}
if info.IsDir() {
- os.Remove(path)
+ os.Remove(fpath)
}
return nil
diff --git a/internal/record/cleaner_test.go b/internal/record/cleaner_test.go
index 133769b6163..788a41f637a 100644
--- a/internal/record/cleaner_test.go
+++ b/internal/record/cleaner_test.go
@@ -6,41 +6,46 @@ import (
"testing"
"time"
+ "github.com/bluenviron/mediamtx/internal/conf"
"github.com/stretchr/testify/require"
)
func TestCleaner(t *testing.T) {
timeNow = func() time.Time {
- return time.Date(2009, 0o5, 20, 22, 15, 25, 427000, time.UTC)
+ return time.Date(2009, 0o5, 20, 22, 15, 25, 427000, time.Local)
}
dir, err := os.MkdirTemp("", "mediamtx-cleaner")
require.NoError(t, err)
defer os.RemoveAll(dir)
- recordPath := filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")
+ const specialChars = "_-+*?^$()[]{}|"
- err = os.Mkdir(filepath.Join(dir, "mypath"), 0o755)
+ err = os.Mkdir(filepath.Join(dir, specialChars+"_mypath"), 0o755)
require.NoError(t, err)
- err = os.WriteFile(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000125.mp4"), []byte{1}, 0o644)
+ err = os.WriteFile(filepath.Join(dir, specialChars+"_mypath", "2008-05-20_22-15-25-000125.mp4"), []byte{1}, 0o644)
require.NoError(t, err)
- err = os.WriteFile(filepath.Join(dir, "mypath", "2009-05-20_22-15-25-000427.mp4"), []byte{1}, 0o644)
+ err = os.WriteFile(filepath.Join(dir, specialChars+"_mypath", "2009-05-20_22-15-25-000427.mp4"), []byte{1}, 0o644)
require.NoError(t, err)
- c := NewCleaner(
- recordPath,
- 10*time.Second,
- nilLogger{},
- )
+ c := &Cleaner{
+ Entries: []CleanerEntry{{
+ Path: filepath.Join(dir, specialChars+"_%path/%Y-%m-%d_%H-%M-%S-%f"),
+ Format: conf.RecordFormatFMP4,
+ DeleteAfter: 10 * time.Second,
+ }},
+ Parent: nilLogger{},
+ }
+ c.Initialize()
defer c.Close()
time.Sleep(500 * time.Millisecond)
- _, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000125.mp4"))
+ _, err = os.Stat(filepath.Join(dir, specialChars+"_mypath", "2008-05-20_22-15-25-000125.mp4"))
require.Error(t, err)
- _, err = os.Stat(filepath.Join(dir, "mypath", "2009-05-20_22-15-25-000427.mp4"))
+ _, err = os.Stat(filepath.Join(dir, specialChars+"_mypath", "2009-05-20_22-15-25-000427.mp4"))
require.NoError(t, err)
}
diff --git a/internal/record/format.go b/internal/record/format.go
new file mode 100644
index 00000000000..0ccc55cd4a3
--- /dev/null
+++ b/internal/record/format.go
@@ -0,0 +1,6 @@
+package record
+
+type format interface {
+ initialize()
+ close()
+}
diff --git a/internal/record/format_fmp4.go b/internal/record/format_fmp4.go
new file mode 100644
index 00000000000..309b751381e
--- /dev/null
+++ b/internal/record/format_fmp4.go
@@ -0,0 +1,855 @@
+package record
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+
+ rtspformat "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/mediacommon/pkg/codecs/ac3"
+ "github.com/bluenviron/mediacommon/pkg/codecs/av1"
+ "github.com/bluenviron/mediacommon/pkg/codecs/g711"
+ "github.com/bluenviron/mediacommon/pkg/codecs/h264"
+ "github.com/bluenviron/mediacommon/pkg/codecs/h265"
+ "github.com/bluenviron/mediacommon/pkg/codecs/jpeg"
+ "github.com/bluenviron/mediacommon/pkg/codecs/mpeg1audio"
+ "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
+ "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4video"
+ "github.com/bluenviron/mediacommon/pkg/codecs/opus"
+ "github.com/bluenviron/mediacommon/pkg/codecs/vp9"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/bluenviron/mediamtx/internal/unit"
+)
+
+func durationGoToMp4(v time.Duration, timeScale uint32) uint64 {
+ timeScale64 := uint64(timeScale)
+ secs := v / time.Second
+ dec := v % time.Second
+ return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second)
+}
+
+func mpeg1audioChannelCount(cm mpeg1audio.ChannelMode) int {
+ switch cm {
+ case mpeg1audio.ChannelModeStereo,
+ mpeg1audio.ChannelModeJointStereo,
+ mpeg1audio.ChannelModeDualChannel:
+ return 2
+
+ default:
+ return 1
+ }
+}
+
+func jpegExtractSize(image []byte) (int, int, error) {
+ l := len(image)
+ if l < 2 || image[0] != 0xFF || image[1] != jpeg.MarkerStartOfImage {
+ return 0, 0, fmt.Errorf("invalid header")
+ }
+
+ image = image[2:]
+
+ for {
+ if len(image) < 2 {
+ return 0, 0, fmt.Errorf("not enough bits")
+ }
+
+ h0, h1 := image[0], image[1]
+ image = image[2:]
+
+ if h0 != 0xFF {
+ return 0, 0, fmt.Errorf("invalid image")
+ }
+
+ switch h1 {
+ case 0xE0, 0xE1, 0xE2, // JFIF
+ jpeg.MarkerDefineHuffmanTable,
+ jpeg.MarkerComment,
+ jpeg.MarkerDefineQuantizationTable,
+ jpeg.MarkerDefineRestartInterval:
+ mlen := int(image[0])<<8 | int(image[1])
+ if len(image) < mlen {
+ return 0, 0, fmt.Errorf("not enough bits")
+ }
+ image = image[mlen:]
+
+ case jpeg.MarkerStartOfFrame1:
+ mlen := int(image[0])<<8 | int(image[1])
+ if len(image) < mlen {
+ return 0, 0, fmt.Errorf("not enough bits")
+ }
+
+ var sof jpeg.StartOfFrame1
+ err := sof.Unmarshal(image[2:mlen])
+ if err != nil {
+ return 0, 0, err
+ }
+
+ return sof.Width, sof.Height, nil
+
+ case jpeg.MarkerStartOfScan:
+ return 0, 0, fmt.Errorf("SOF not found")
+
+ default:
+ return 0, 0, fmt.Errorf("unknown marker: 0x%.2x", h1)
+ }
+ }
+}
+
+type formatFMP4 struct {
+ a *agentInstance
+
+ tracks []*formatFMP4Track
+ hasVideo bool
+ currentSegment *formatFMP4Segment
+ nextSequenceNumber uint32
+}
+
+func (f *formatFMP4) initialize() {
+ nextID := 1
+ var formats []rtspformat.Format
+
+ addTrack := func(format rtspformat.Format, codec fmp4.Codec) *formatFMP4Track {
+ initTrack := &fmp4.InitTrack{
+ TimeScale: 90000,
+ Codec: codec,
+ }
+ initTrack.ID = nextID
+ nextID++
+
+ track := &formatFMP4Track{
+ f: f,
+ initTrack: initTrack,
+ }
+
+ f.tracks = append(f.tracks, track)
+ formats = append(formats, format)
+ return track
+ }
+
+ updateCodecs := func() {
+ // if codec parameters have been updated,
+ // and current segment has already written codec parameters on disk,
+ // close current segment.
+ if f.currentSegment != nil && f.currentSegment.fi != nil {
+ f.currentSegment.close() //nolint:errcheck
+ f.currentSegment = nil
+ }
+ }
+
+ for _, media := range f.a.agent.Stream.Desc().Medias {
+ for _, forma := range media.Formats {
+ switch forma := forma.(type) {
+ case *rtspformat.AV1:
+ codec := &fmp4.CodecAV1{
+ SequenceHeader: []byte{
+ 8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64,
+ },
+ }
+ track := addTrack(forma, codec)
+
+ firstReceived := false
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.AV1)
+ if tunit.TU == nil {
+ return nil
+ }
+
+ randomAccess := false
+
+ for _, obu := range tunit.TU {
+ var h av1.OBUHeader
+ err := h.Unmarshal(obu)
+ if err != nil {
+ return err
+ }
+
+ if h.Type == av1.OBUTypeSequenceHeader {
+ if !bytes.Equal(codec.SequenceHeader, obu) {
+ codec.SequenceHeader = obu
+ updateCodecs()
+ }
+ randomAccess = true
+ }
+ }
+
+ if !firstReceived {
+ if !randomAccess {
+ return nil
+ }
+ firstReceived = true
+ }
+
+ sampl, err := fmp4.NewPartSampleAV1(
+ randomAccess,
+ tunit.TU)
+ if err != nil {
+ return err
+ }
+
+ return track.record(&sample{
+ PartSample: sampl,
+ dts: tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.VP9:
+ codec := &fmp4.CodecVP9{
+ Width: 1280,
+ Height: 720,
+ Profile: 1,
+ BitDepth: 8,
+ ChromaSubsampling: 1,
+ ColorRange: false,
+ }
+ track := addTrack(forma, codec)
+
+ firstReceived := false
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.VP9)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ var h vp9.Header
+ err := h.Unmarshal(tunit.Frame)
+ if err != nil {
+ return err
+ }
+
+ randomAccess := false
+
+ if h.FrameType == vp9.FrameTypeKeyFrame {
+ randomAccess = true
+
+ if w := h.Width(); codec.Width != w {
+ codec.Width = w
+ updateCodecs()
+ }
+ if h := h.Width(); codec.Height != h {
+ codec.Height = h
+ updateCodecs()
+ }
+ if codec.Profile != h.Profile {
+ codec.Profile = h.Profile
+ updateCodecs()
+ }
+ if codec.BitDepth != h.ColorConfig.BitDepth {
+ codec.BitDepth = h.ColorConfig.BitDepth
+ updateCodecs()
+ }
+ if c := h.ChromaSubsampling(); codec.ChromaSubsampling != c {
+ codec.ChromaSubsampling = c
+ updateCodecs()
+ }
+ if codec.ColorRange != h.ColorConfig.ColorRange {
+ codec.ColorRange = h.ColorConfig.ColorRange
+ updateCodecs()
+ }
+ }
+
+ if !firstReceived {
+ if !randomAccess {
+ return nil
+ }
+ firstReceived = true
+ }
+
+ return track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ IsNonSyncSample: !randomAccess,
+ Payload: tunit.Frame,
+ },
+ dts: tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.VP8:
+ // TODO
+
+ case *rtspformat.H265:
+ vps, sps, pps := forma.SafeParams()
+
+ if vps == nil || sps == nil || pps == nil {
+ vps = []byte{
+ 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,
+ 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,
+ 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,
+ }
+
+ sps = []byte{
+ 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,
+ 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,
+ 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,
+ 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,
+ 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,
+ 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,
+ 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,
+ 0x02, 0x02, 0x02, 0x01,
+ }
+
+ pps = []byte{
+ 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
+ }
+ }
+
+ codec := &fmp4.CodecH265{
+ VPS: vps,
+ SPS: sps,
+ PPS: pps,
+ }
+ track := addTrack(forma, codec)
+
+ var dtsExtractor *h265.DTSExtractor
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.H265)
+ if tunit.AU == nil {
+ return nil
+ }
+
+ randomAccess := false
+
+ for _, nalu := range tunit.AU {
+ typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
+
+ switch typ {
+ case h265.NALUType_VPS_NUT:
+ if !bytes.Equal(codec.VPS, nalu) {
+ codec.VPS = nalu
+ updateCodecs()
+ }
+
+ case h265.NALUType_SPS_NUT:
+ if !bytes.Equal(codec.SPS, nalu) {
+ codec.SPS = nalu
+ updateCodecs()
+ }
+
+ case h265.NALUType_PPS_NUT:
+ if !bytes.Equal(codec.PPS, nalu) {
+ codec.PPS = nalu
+ updateCodecs()
+ }
+
+ case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
+ randomAccess = true
+ }
+ }
+
+ if dtsExtractor == nil {
+ if !randomAccess {
+ return nil
+ }
+ dtsExtractor = h265.NewDTSExtractor()
+ }
+
+ dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
+ if err != nil {
+ return err
+ }
+
+ sampl, err := fmp4.NewPartSampleH26x(
+ int32(durationGoToMp4(tunit.PTS-dts, 90000)),
+ randomAccess,
+ tunit.AU)
+ if err != nil {
+ return err
+ }
+
+ return track.record(&sample{
+ PartSample: sampl,
+ dts: dts,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.H264:
+ sps, pps := forma.SafeParams()
+
+ if sps == nil || pps == nil {
+ sps = []byte{
+ 0x67, 0x42, 0xc0, 0x1f, 0xd9, 0x00, 0xf0, 0x11,
+ 0x7e, 0xf0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01,
+ 0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, 0x18, 0x32,
+ 0x48,
+ }
+
+ pps = []byte{
+ 0x68, 0xcb, 0x8c, 0xb2,
+ }
+ }
+
+ codec := &fmp4.CodecH264{
+ SPS: sps,
+ PPS: pps,
+ }
+ track := addTrack(forma, codec)
+
+ var dtsExtractor *h264.DTSExtractor
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.H264)
+ if tunit.AU == nil {
+ return nil
+ }
+
+ randomAccess := false
+
+ for _, nalu := range tunit.AU {
+ typ := h264.NALUType(nalu[0] & 0x1F)
+ switch typ {
+ case h264.NALUTypeSPS:
+ if !bytes.Equal(codec.SPS, nalu) {
+ codec.SPS = nalu
+ updateCodecs()
+ }
+
+ case h264.NALUTypePPS:
+ if !bytes.Equal(codec.PPS, nalu) {
+ codec.PPS = nalu
+ updateCodecs()
+ }
+
+ case h264.NALUTypeIDR:
+ randomAccess = true
+ }
+ }
+
+ if dtsExtractor == nil {
+ if !randomAccess {
+ return nil
+ }
+ dtsExtractor = h264.NewDTSExtractor()
+ }
+
+ dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
+ if err != nil {
+ return err
+ }
+
+ sampl, err := fmp4.NewPartSampleH26x(
+ int32(durationGoToMp4(tunit.PTS-dts, 90000)),
+ randomAccess,
+ tunit.AU)
+ if err != nil {
+ return err
+ }
+
+ return track.record(&sample{
+ PartSample: sampl,
+ dts: dts,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.MPEG4Video:
+ config := forma.SafeParams()
+
+ if config == nil {
+ config = []byte{
+ 0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01,
+ 0xb5, 0x89, 0x13, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x01, 0x20, 0x00, 0xc4, 0x8d, 0x88, 0x00,
+ 0xf5, 0x3c, 0x04, 0x87, 0x14, 0x63, 0x00, 0x00,
+ 0x01, 0xb2, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x38,
+ 0x2e, 0x31, 0x33, 0x34, 0x2e, 0x31, 0x30, 0x30,
+ }
+ }
+
+ codec := &fmp4.CodecMPEG4Video{
+ Config: config,
+ }
+ track := addTrack(forma, codec)
+
+ firstReceived := false
+ var lastPTS time.Duration
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG4Video)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
+
+ if bytes.HasPrefix(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.VisualObjectSequenceStartCode)}) {
+ end := bytes.Index(tunit.Frame[4:], []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
+ if end >= 0 {
+ config := tunit.Frame[:end+4]
+
+ if !bytes.Equal(codec.Config, config) {
+ codec.Config = config
+ updateCodecs()
+ }
+ }
+ }
+
+ if !firstReceived {
+ if !randomAccess {
+ return nil
+ }
+ firstReceived = true
+ } else if tunit.PTS < lastPTS {
+ return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)")
+ }
+ lastPTS = tunit.PTS
+
+ return track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: tunit.Frame,
+ IsNonSyncSample: !randomAccess,
+ },
+ dts: tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.MPEG1Video:
+ codec := &fmp4.CodecMPEG1Video{
+ Config: []byte{
+ 0x00, 0x00, 0x01, 0xb3, 0x78, 0x04, 0x38, 0x35,
+ 0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5,
+ 0x14, 0x4a, 0x00, 0x01, 0x00, 0x00,
+ },
+ }
+ track := addTrack(forma, codec)
+
+ firstReceived := false
+ var lastPTS time.Duration
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG1Video)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8})
+
+ if bytes.HasPrefix(tunit.Frame, []byte{0, 0, 1, 0xB3}) {
+ end := bytes.Index(tunit.Frame[4:], []byte{0, 0, 1, 0xB8})
+ if end >= 0 {
+ config := tunit.Frame[:end+4]
+
+ if !bytes.Equal(codec.Config, config) {
+ codec.Config = config
+ updateCodecs()
+ }
+ }
+ }
+
+ if !firstReceived {
+ if !randomAccess {
+ return nil
+ }
+ firstReceived = true
+ } else if tunit.PTS < lastPTS {
+ return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
+ }
+ lastPTS = tunit.PTS
+
+ return track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: tunit.Frame,
+ IsNonSyncSample: !randomAccess,
+ },
+ dts: tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.MJPEG:
+ codec := &fmp4.CodecMJPEG{
+ Width: 800,
+ Height: 600,
+ }
+ track := addTrack(forma, codec)
+
+ parsed := false
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MJPEG)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ if !parsed {
+ parsed = true
+ width, height, err := jpegExtractSize(tunit.Frame)
+ if err != nil {
+ return err
+ }
+ codec.Width = width
+ codec.Height = height
+ updateCodecs()
+ }
+
+ return track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: tunit.Frame,
+ },
+ dts: tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.Opus:
+ codec := &fmp4.CodecOpus{
+ ChannelCount: func() int {
+ if forma.IsStereo {
+ return 2
+ }
+ return 1
+ }(),
+ }
+ track := addTrack(forma, codec)
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.Opus)
+ if tunit.Packets == nil {
+ return nil
+ }
+
+ var dt time.Duration
+
+ for _, packet := range tunit.Packets {
+ err := track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: packet,
+ },
+ dts: tunit.PTS + dt,
+ ntp: tunit.NTP.Add(dt),
+ })
+ if err != nil {
+ return err
+ }
+
+ dt += opus.PacketDuration(packet)
+ }
+
+ return nil
+ })
+
+ case *rtspformat.MPEG4Audio:
+ codec := &fmp4.CodecMPEG4Audio{
+ Config: *forma.GetConfig(),
+ }
+ track := addTrack(forma, codec)
+
+ sampleRate := time.Duration(forma.ClockRate())
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG4Audio)
+ if tunit.AUs == nil {
+ return nil
+ }
+
+ for i, au := range tunit.AUs {
+ dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit *
+ time.Second / sampleRate
+
+ err := track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: au,
+ },
+ dts: tunit.PTS + dt,
+ ntp: tunit.NTP.Add(dt),
+ })
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+
+ case *rtspformat.MPEG1Audio:
+ codec := &fmp4.CodecMPEG1Audio{
+ SampleRate: 32000,
+ ChannelCount: 2,
+ }
+ track := addTrack(forma, codec)
+
+ parsed := false
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG1Audio)
+ if tunit.Frames == nil {
+ return nil
+ }
+
+ var dt time.Duration
+
+ for _, frame := range tunit.Frames {
+ var h mpeg1audio.FrameHeader
+ err := h.Unmarshal(frame)
+ if err != nil {
+ return err
+ }
+
+ if !parsed {
+ parsed = true
+ codec.SampleRate = h.SampleRate
+ codec.ChannelCount = mpeg1audioChannelCount(h.ChannelMode)
+ updateCodecs()
+ }
+
+ err = track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: frame,
+ },
+ dts: tunit.PTS + tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ if err != nil {
+ return err
+ }
+
+ dt += time.Duration(h.SampleCount()) *
+ time.Second / time.Duration(h.SampleRate)
+ }
+
+ return nil
+ })
+
+ case *rtspformat.AC3:
+ codec := &fmp4.CodecAC3{
+ SampleRate: forma.SampleRate,
+ ChannelCount: forma.ChannelCount,
+ Fscod: 0,
+ Bsid: 8,
+ Bsmod: 0,
+ Acmod: 7,
+ LfeOn: true,
+ BitRateCode: 7,
+ }
+ track := addTrack(forma, codec)
+
+ parsed := false
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.AC3)
+ if tunit.Frames == nil {
+ return nil
+ }
+
+ for i, frame := range tunit.Frames {
+ var syncInfo ac3.SyncInfo
+ err := syncInfo.Unmarshal(frame)
+ if err != nil {
+ return fmt.Errorf("invalid AC-3 frame: %w", err)
+ }
+
+ var bsi ac3.BSI
+ err = bsi.Unmarshal(frame[5:])
+ if err != nil {
+ return fmt.Errorf("invalid AC-3 frame: %w", err)
+ }
+
+ if !parsed {
+ parsed = true
+ codec.SampleRate = syncInfo.SampleRate()
+ codec.ChannelCount = bsi.ChannelCount()
+ codec.Fscod = syncInfo.Fscod
+ codec.Bsid = bsi.Bsid
+ codec.Bsmod = bsi.Bsmod
+ codec.Acmod = bsi.Acmod
+ codec.LfeOn = bsi.LfeOn
+ codec.BitRateCode = syncInfo.Frmsizecod >> 1
+ updateCodecs()
+ }
+
+ dt := time.Duration(i) * time.Duration(ac3.SamplesPerFrame) *
+ time.Second / time.Duration(codec.SampleRate)
+
+ err = track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: frame,
+ },
+ dts: tunit.PTS + dt,
+ ntp: tunit.NTP.Add(dt),
+ })
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+
+ case *rtspformat.G722:
+ // TODO
+
+ case *rtspformat.G711:
+ codec := &fmp4.CodecLPCM{
+ LittleEndian: false,
+ BitDepth: 16,
+ SampleRate: forma.SampleRate,
+ ChannelCount: forma.ChannelCount,
+ }
+ track := addTrack(forma, codec)
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.G711)
+ if tunit.Samples == nil {
+ return nil
+ }
+
+ var out []byte
+ if forma.MULaw {
+ out = g711.DecodeMulaw(tunit.Samples)
+ } else {
+ out = g711.DecodeAlaw(tunit.Samples)
+ }
+
+ return track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: out,
+ },
+ dts: tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ })
+
+ case *rtspformat.LPCM:
+ codec := &fmp4.CodecLPCM{
+ LittleEndian: false,
+ BitDepth: forma.BitDepth,
+ SampleRate: forma.SampleRate,
+ ChannelCount: forma.ChannelCount,
+ }
+ track := addTrack(forma, codec)
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.LPCM)
+ if tunit.Samples == nil {
+ return nil
+ }
+
+ return track.record(&sample{
+ PartSample: &fmp4.PartSample{
+ Payload: tunit.Samples,
+ },
+ dts: tunit.PTS,
+ ntp: tunit.NTP,
+ })
+ })
+ }
+ }
+ }
+
+ f.a.agent.Log(logger.Info, "recording %s",
+ defs.FormatsInfo(formats))
+}
+
+func (f *formatFMP4) close() {
+ if f.currentSegment != nil {
+ f.currentSegment.close() //nolint:errcheck
+ }
+}
diff --git a/internal/record/format_fmp4_part.go b/internal/record/format_fmp4_part.go
new file mode 100644
index 00000000000..dbe8a4a5d41
--- /dev/null
+++ b/internal/record/format_fmp4_part.go
@@ -0,0 +1,102 @@
+package record
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
+
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+func writePart(
+ f io.Writer,
+ sequenceNumber uint32,
+ partTracks map[*formatFMP4Track]*fmp4.PartTrack,
+) error {
+ fmp4PartTracks := make([]*fmp4.PartTrack, len(partTracks))
+ i := 0
+ for _, partTrack := range partTracks {
+ fmp4PartTracks[i] = partTrack
+ i++
+ }
+
+ part := &fmp4.Part{
+ SequenceNumber: sequenceNumber,
+ Tracks: fmp4PartTracks,
+ }
+
+ var buf seekablebuffer.Buffer
+ err := part.Marshal(&buf)
+ if err != nil {
+ return err
+ }
+
+ _, err = f.Write(buf.Bytes())
+ return err
+}
+
+type formatFMP4Part struct {
+ s *formatFMP4Segment
+ sequenceNumber uint32
+ startDTS time.Duration
+
+ partTracks map[*formatFMP4Track]*fmp4.PartTrack
+ endDTS time.Duration
+}
+
+func (p *formatFMP4Part) initialize() {
+ p.partTracks = make(map[*formatFMP4Track]*fmp4.PartTrack)
+}
+
+func (p *formatFMP4Part) close() error {
+ if p.s.fi == nil {
+ p.s.path = Path(p.s.startNTP).Encode(p.s.f.a.pathFormat)
+ p.s.f.a.agent.Log(logger.Debug, "creating segment %s", p.s.path)
+
+ err := os.MkdirAll(filepath.Dir(p.s.path), 0o755)
+ if err != nil {
+ return err
+ }
+
+ fi, err := os.Create(p.s.path)
+ if err != nil {
+ return err
+ }
+
+ p.s.f.a.agent.OnSegmentCreate(p.s.path)
+
+ err = writeInit(fi, p.s.f.tracks)
+ if err != nil {
+ fi.Close()
+ return err
+ }
+
+ p.s.fi = fi
+ }
+
+ return writePart(p.s.fi, p.sequenceNumber, p.partTracks)
+}
+
+func (p *formatFMP4Part) record(track *formatFMP4Track, sample *sample) error {
+ partTrack, ok := p.partTracks[track]
+ if !ok {
+ partTrack = &fmp4.PartTrack{
+ ID: track.initTrack.ID,
+ BaseTime: durationGoToMp4(sample.dts-p.s.startDTS, track.initTrack.TimeScale),
+ }
+ p.partTracks[track] = partTrack
+ }
+
+ partTrack.Samples = append(partTrack.Samples, sample.PartSample)
+ p.endDTS = sample.dts
+
+ return nil
+}
+
+func (p *formatFMP4Part) duration() time.Duration {
+ return p.endDTS - p.startDTS
+}
diff --git a/internal/record/format_fmp4_segment.go b/internal/record/format_fmp4_segment.go
new file mode 100644
index 00000000000..f0695c006c1
--- /dev/null
+++ b/internal/record/format_fmp4_segment.go
@@ -0,0 +1,96 @@
+package record
+
+import (
+ "io"
+ "os"
+ "time"
+
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
+
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+func writeInit(f io.Writer, tracks []*formatFMP4Track) error {
+ fmp4Tracks := make([]*fmp4.InitTrack, len(tracks))
+ for i, track := range tracks {
+ fmp4Tracks[i] = track.initTrack
+ }
+
+ init := fmp4.Init{
+ Tracks: fmp4Tracks,
+ }
+
+ var buf seekablebuffer.Buffer
+ err := init.Marshal(&buf)
+ if err != nil {
+ return err
+ }
+
+ _, err = f.Write(buf.Bytes())
+ return err
+}
+
+type formatFMP4Segment struct {
+ f *formatFMP4
+ startDTS time.Duration
+ startNTP time.Time
+
+ path string
+ fi *os.File
+ curPart *formatFMP4Part
+}
+
+func (s *formatFMP4Segment) initialize() {
+}
+
+func (s *formatFMP4Segment) close() error {
+ var err error
+
+ if s.curPart != nil {
+ err = s.curPart.close()
+ }
+
+ if s.fi != nil {
+ s.f.a.agent.Log(logger.Debug, "closing segment %s", s.path)
+ err2 := s.fi.Close()
+ if err == nil {
+ err = err2
+ }
+
+ if err2 == nil {
+ s.f.a.agent.OnSegmentComplete(s.path)
+ }
+ }
+
+ return err
+}
+
+func (s *formatFMP4Segment) record(track *formatFMP4Track, sample *sample) error {
+ if s.curPart == nil {
+ s.curPart = &formatFMP4Part{
+ s: s,
+ sequenceNumber: s.f.nextSequenceNumber,
+ startDTS: sample.dts,
+ }
+ s.curPart.initialize()
+ s.f.nextSequenceNumber++
+ } else if s.curPart.duration() >= s.f.a.agent.PartDuration {
+ err := s.curPart.close()
+ s.curPart = nil
+
+ if err != nil {
+ return err
+ }
+
+ s.curPart = &formatFMP4Part{
+ s: s,
+ sequenceNumber: s.f.nextSequenceNumber,
+ startDTS: sample.dts,
+ }
+ s.curPart.initialize()
+ s.f.nextSequenceNumber++
+ }
+
+ return s.curPart.record(track, sample)
+}
diff --git a/internal/record/format_fmp4_track.go b/internal/record/format_fmp4_track.go
new file mode 100644
index 00000000000..1ff214f2275
--- /dev/null
+++ b/internal/record/format_fmp4_track.go
@@ -0,0 +1,60 @@
+package record
+
+import (
+ "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
+)
+
+type formatFMP4Track struct {
+ f *formatFMP4
+ initTrack *fmp4.InitTrack
+
+ nextSample *sample
+}
+
+func (t *formatFMP4Track) record(sample *sample) error {
+ // wait the first video sample before setting hasVideo
+ if t.initTrack.Codec.IsVideo() {
+ t.f.hasVideo = true
+ }
+
+ sample, t.nextSample = t.nextSample, sample
+ if sample == nil {
+ return nil
+ }
+ sample.Duration = uint32(durationGoToMp4(t.nextSample.dts-sample.dts, t.initTrack.TimeScale))
+
+ if t.f.currentSegment == nil {
+ t.f.currentSegment = &formatFMP4Segment{
+ f: t.f,
+ startDTS: sample.dts,
+ startNTP: sample.ntp,
+ }
+ t.f.currentSegment.initialize()
+ // BaseTime is negative, this is not supported by fMP4. Reject the sample silently.
+ } else if (sample.dts - t.f.currentSegment.startDTS) < 0 {
+ return nil
+ }
+
+ err := t.f.currentSegment.record(t, sample)
+ if err != nil {
+ return err
+ }
+
+ if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) &&
+ !t.nextSample.IsNonSyncSample &&
+ (t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration {
+ err := t.f.currentSegment.close()
+ if err != nil {
+ return err
+ }
+
+ t.f.currentSegment = &formatFMP4Segment{
+ f: t.f,
+ startDTS: t.nextSample.dts,
+ startNTP: t.nextSample.ntp,
+ }
+ t.f.currentSegment.initialize()
+ }
+
+ return nil
+}
diff --git a/internal/record/format_mpegts.go b/internal/record/format_mpegts.go
new file mode 100644
index 00000000000..a541a06ed0a
--- /dev/null
+++ b/internal/record/format_mpegts.go
@@ -0,0 +1,343 @@
+package record
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+ "time"
+
+ rtspformat "github.com/bluenviron/gortsplib/v4/pkg/format"
+ "github.com/bluenviron/mediacommon/pkg/codecs/ac3"
+ "github.com/bluenviron/mediacommon/pkg/codecs/h264"
+ "github.com/bluenviron/mediacommon/pkg/codecs/h265"
+ "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4video"
+ "github.com/bluenviron/mediacommon/pkg/formats/mpegts"
+
+ "github.com/bluenviron/mediamtx/internal/defs"
+ "github.com/bluenviron/mediamtx/internal/logger"
+ "github.com/bluenviron/mediamtx/internal/unit"
+)
+
+const (
+ mpegtsMaxBufferSize = 64 * 1024
+)
+
+func durationGoToMPEGTS(v time.Duration) int64 {
+ return int64(v.Seconds() * 90000)
+}
+
+type dynamicWriter struct {
+ w io.Writer
+}
+
+func (d *dynamicWriter) Write(p []byte) (int, error) {
+ return d.w.Write(p)
+}
+
+func (d *dynamicWriter) setTarget(w io.Writer) {
+ d.w = w
+}
+
+type formatMPEGTS struct {
+ a *agentInstance
+
+ dw *dynamicWriter
+ bw *bufio.Writer
+ mw *mpegts.Writer
+ hasVideo bool
+ currentSegment *formatMPEGTSSegment
+}
+
+func (f *formatMPEGTS) initialize() {
+ var tracks []*mpegts.Track
+ var formats []rtspformat.Format
+
+ addTrack := func(format rtspformat.Format, codec mpegts.Codec) *mpegts.Track {
+ track := &mpegts.Track{
+ Codec: codec,
+ }
+
+ tracks = append(tracks, track)
+ formats = append(formats, format)
+ return track
+ }
+
+ for _, media := range f.a.agent.Stream.Desc().Medias {
+ for _, forma := range media.Formats {
+ switch forma := forma.(type) {
+ case *rtspformat.H265:
+ track := addTrack(forma, &mpegts.CodecH265{})
+
+ var dtsExtractor *h265.DTSExtractor
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.H265)
+ if tunit.AU == nil {
+ return nil
+ }
+
+ randomAccess := h265.IsRandomAccess(tunit.AU)
+
+ if dtsExtractor == nil {
+ if !randomAccess {
+ return nil
+ }
+ dtsExtractor = h265.NewDTSExtractor()
+ }
+
+ dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
+ if err != nil {
+ return err
+ }
+
+ return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, randomAccess, tunit.AU)
+ })
+
+ case *rtspformat.H264:
+ track := addTrack(forma, &mpegts.CodecH264{})
+
+ var dtsExtractor *h264.DTSExtractor
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.H264)
+ if tunit.AU == nil {
+ return nil
+ }
+
+ idrPresent := h264.IDRPresent(tunit.AU)
+
+ if dtsExtractor == nil {
+ if !idrPresent {
+ return nil
+ }
+ dtsExtractor = h264.NewDTSExtractor()
+ }
+
+ dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
+ if err != nil {
+ return err
+ }
+
+ return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, idrPresent, tunit.AU)
+ })
+
+ case *rtspformat.MPEG4Video:
+ track := addTrack(forma, &mpegts.CodecMPEG4Video{})
+
+ firstReceived := false
+ var lastPTS time.Duration
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG4Video)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ if !firstReceived {
+ firstReceived = true
+ } else if tunit.PTS < lastPTS {
+ return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)")
+ }
+ 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)
+ })
+
+ case *rtspformat.MPEG1Video:
+ track := addTrack(forma, &mpegts.CodecMPEG1Video{})
+
+ firstReceived := false
+ var lastPTS time.Duration
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG1Video)
+ if tunit.Frame == nil {
+ return nil
+ }
+
+ if !firstReceived {
+ firstReceived = true
+ } else if tunit.PTS < lastPTS {
+ return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
+ }
+ 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)
+ })
+
+ case *rtspformat.Opus:
+ track := addTrack(forma, &mpegts.CodecOpus{
+ ChannelCount: func() int {
+ if forma.IsStereo {
+ return 2
+ }
+ return 1
+ }(),
+ })
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.Opus)
+ if tunit.Packets == nil {
+ 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)
+ })
+
+ case *rtspformat.MPEG4Audio:
+ track := addTrack(forma, &mpegts.CodecMPEG4Audio{
+ Config: *forma.GetConfig(),
+ })
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG4Audio)
+ if tunit.AUs == nil {
+ 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)
+ })
+
+ case *rtspformat.MPEG1Audio:
+ track := addTrack(forma, &mpegts.CodecMPEG1Audio{})
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.MPEG1Audio)
+ if tunit.Frames == nil {
+ 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)
+ })
+
+ case *rtspformat.AC3:
+ track := addTrack(forma, &mpegts.CodecAC3{})
+
+ sampleRate := time.Duration(forma.SampleRate)
+
+ f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+ tunit := u.(*unit.AC3)
+ if tunit.Frames == nil {
+ return nil
+ }
+
+ 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
+ }
+ }
+
+ return nil
+ })
+ }
+ }
+ }
+
+ f.dw = &dynamicWriter{}
+ f.bw = bufio.NewWriterSize(f.dw, mpegtsMaxBufferSize)
+ f.mw = mpegts.NewWriter(f.bw, tracks)
+
+ f.a.agent.Log(logger.Info, "recording %s",
+ defs.FormatsInfo(formats))
+}
+
+func (f *formatMPEGTS) close() {
+ if f.currentSegment != nil {
+ f.currentSegment.close() //nolint:errcheck
+ }
+}
+
+func (f *formatMPEGTS) setupSegment(
+ dts time.Duration,
+ ntp time.Time,
+ isVideo bool,
+ randomAccess bool,
+) error {
+ switch {
+ case f.currentSegment == nil:
+ f.currentSegment = &formatMPEGTSSegment{
+ f: f,
+ startDTS: dts,
+ startNTP: ntp,
+ }
+ f.currentSegment.initialize()
+ case (!f.hasVideo || isVideo) &&
+ randomAccess &&
+ (dts-f.currentSegment.startDTS) >= f.a.agent.SegmentDuration:
+ err := f.currentSegment.close()
+ if err != nil {
+ return err
+ }
+
+ f.currentSegment = &formatMPEGTSSegment{
+ f: f,
+ startDTS: dts,
+ startNTP: ntp,
+ }
+ f.currentSegment.initialize()
+
+ case (dts - f.currentSegment.lastFlush) >= f.a.agent.PartDuration:
+ err := f.bw.Flush()
+ if err != nil {
+ return err
+ }
+
+ 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)
+}
diff --git a/internal/record/format_mpegts_segment.go b/internal/record/format_mpegts_segment.go
new file mode 100644
index 00000000000..a3870241057
--- /dev/null
+++ b/internal/record/format_mpegts_segment.go
@@ -0,0 +1,65 @@
+package record
+
+import (
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/bluenviron/mediamtx/internal/logger"
+)
+
+type formatMPEGTSSegment struct {
+ f *formatMPEGTS
+ startDTS time.Duration
+ startNTP time.Time
+
+ lastFlush time.Duration
+ path string
+ fi *os.File
+}
+
+func (s *formatMPEGTSSegment) initialize() {
+ s.lastFlush = s.startDTS
+ s.f.dw.setTarget(s)
+}
+
+func (s *formatMPEGTSSegment) close() error {
+ err := s.f.bw.Flush()
+
+ if s.fi != nil {
+ s.f.a.agent.Log(logger.Debug, "closing segment %s", s.path)
+ err2 := s.fi.Close()
+ if err == nil {
+ err = err2
+ }
+
+ if err2 == nil {
+ s.f.a.agent.OnSegmentComplete(s.path)
+ }
+ }
+
+ return err
+}
+
+func (s *formatMPEGTSSegment) Write(p []byte) (int, error) {
+ if s.fi == nil {
+ s.path = Path(s.startNTP).Encode(s.f.a.pathFormat)
+ s.f.a.agent.Log(logger.Debug, "creating segment %s", s.path)
+
+ err := os.MkdirAll(filepath.Dir(s.path), 0o755)
+ if err != nil {
+ return 0, err
+ }
+
+ fi, err := os.Create(s.path)
+ if err != nil {
+ return 0, err
+ }
+
+ s.f.a.agent.OnSegmentCreate(s.path)
+
+ s.fi = fi
+ }
+
+ return s.fi.Write(p)
+}
diff --git a/internal/record/part.go b/internal/record/part.go
deleted file mode 100644
index a9dd6ec5d26..00000000000
--- a/internal/record/part.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package record
-
-import (
- "io"
- "os"
- "path/filepath"
- "time"
-
- "github.com/aler9/writerseeker"
- "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
-
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-func writePart(f io.Writer, partTracks map[*track]*fmp4.PartTrack) error {
- fmp4PartTracks := make([]*fmp4.PartTrack, len(partTracks))
- i := 0
- for _, partTrack := range partTracks {
- fmp4PartTracks[i] = partTrack
- i++
- }
-
- part := &fmp4.Part{
- Tracks: fmp4PartTracks,
- }
-
- var ws writerseeker.WriterSeeker
- err := part.Marshal(&ws)
- if err != nil {
- return err
- }
-
- _, err = f.Write(ws.Bytes())
- return err
-}
-
-type part struct {
- s *segment
- startDTS time.Duration
-
- partTracks map[*track]*fmp4.PartTrack
- endDTS time.Duration
-}
-
-func newPart(
- s *segment,
- startDTS time.Duration,
-) *part {
- return &part{
- s: s,
- startDTS: startDTS,
- partTracks: make(map[*track]*fmp4.PartTrack),
- }
-}
-
-func (p *part) close() error {
- if p.s.f == nil {
- p.s.fpath = encodeRecordPath(&recordPathParams{time: timeNow()}, p.s.r.path)
- p.s.r.Log(logger.Debug, "opening segment %s", p.s.fpath)
-
- err := os.MkdirAll(filepath.Dir(p.s.fpath), 0o755)
- if err != nil {
- return err
- }
-
- f, err := os.Create(p.s.fpath)
- if err != nil {
- return err
- }
-
- err = writeInit(f, p.s.r.tracks)
- if err != nil {
- f.Close()
- return err
- }
-
- p.s.f = f
- }
-
- return writePart(p.s.f, p.partTracks)
-}
-
-func (p *part) record(track *track, sample *sample) error {
- partTrack, ok := p.partTracks[track]
- if !ok {
- partTrack = &fmp4.PartTrack{
- ID: track.initTrack.ID,
- BaseTime: durationGoToMp4(sample.dts-p.s.startDTS, track.initTrack.TimeScale),
- }
- p.partTracks[track] = partTrack
- }
-
- partTrack.Samples = append(partTrack.Samples, sample.PartSample)
- p.endDTS = sample.dts
-
- return nil
-}
-
-func (p *part) duration() time.Duration {
- return p.endDTS - p.startDTS
-}
diff --git a/internal/record/path.go b/internal/record/path.go
new file mode 100644
index 00000000000..1c31a0495a9
--- /dev/null
+++ b/internal/record/path.go
@@ -0,0 +1,206 @@
+package record
+
+import (
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/bluenviron/mediamtx/internal/conf"
+)
+
+func leadingZeros(v int, size int) string {
+ out := strconv.FormatInt(int64(v), 10)
+ if len(out) >= size {
+ return out
+ }
+
+ out2 := ""
+ for i := 0; i < (size - len(out)); i++ {
+ out2 += "0"
+ }
+
+ return out2 + out
+}
+
+// PathAddExtension adds the file extension to path.
+func PathAddExtension(path string, format conf.RecordFormat) string {
+ switch format {
+ case conf.RecordFormatMPEGTS:
+ return path + ".ts"
+
+ default:
+ return path + ".mp4"
+ }
+}
+
+// CommonPath returns the common path between all segments with given recording path.
+func CommonPath(v string) string {
+ common := ""
+ remaining := v
+
+ for {
+ i := strings.IndexAny(remaining, "\\/")
+ if i < 0 {
+ break
+ }
+
+ var part string
+ part, remaining = remaining[:i+1], remaining[i+1:]
+
+ if strings.Contains(part, "%") {
+ break
+ }
+
+ common += part
+ }
+
+ if len(common) > 0 {
+ common = common[:len(common)-1]
+ }
+
+ return common
+}
+
+// Path is a record path.
+type Path time.Time
+
+// Decode decodes a Path.
+func (p *Path) Decode(format string, v string) bool {
+ re := format
+
+ for _, ch := range []uint8{
+ '\\',
+ '.',
+ '+',
+ '*',
+ '?',
+ '^',
+ '$',
+ '(',
+ ')',
+ '[',
+ ']',
+ '{',
+ '}',
+ '|',
+ } {
+ re = strings.ReplaceAll(re, string(ch), "\\"+string(ch))
+ }
+
+ re = strings.ReplaceAll(re, "%path", "(.*?)")
+ re = strings.ReplaceAll(re, "%Y", "([0-9]{4})")
+ re = strings.ReplaceAll(re, "%m", "([0-9]{2})")
+ re = strings.ReplaceAll(re, "%d", "([0-9]{2})")
+ re = strings.ReplaceAll(re, "%H", "([0-9]{2})")
+ re = strings.ReplaceAll(re, "%M", "([0-9]{2})")
+ re = strings.ReplaceAll(re, "%S", "([0-9]{2})")
+ re = strings.ReplaceAll(re, "%f", "([0-9]{6})")
+ re = strings.ReplaceAll(re, "%s", "([0-9]{10})")
+ r := regexp.MustCompile(re)
+
+ var groupMapping []string
+ cur := format
+ for {
+ i := strings.Index(cur, "%")
+ if i < 0 {
+ break
+ }
+
+ cur = cur[i:]
+
+ for _, va := range []string{
+ "%path",
+ "%Y",
+ "%m",
+ "%d",
+ "%H",
+ "%M",
+ "%S",
+ "%f",
+ "%s",
+ } {
+ if strings.HasPrefix(cur, va) {
+ groupMapping = append(groupMapping, va)
+ }
+ }
+
+ cur = cur[1:]
+ }
+
+ matches := r.FindStringSubmatch(v)
+ if matches == nil {
+ return false
+ }
+
+ values := make(map[string]string)
+
+ for i, match := range matches[1:] {
+ values[groupMapping[i]] = match
+ }
+
+ var year int
+ var month time.Month = 1
+ day := 1
+ var hour int
+ var minute int
+ var second int
+ var micros int
+ var unixSec int64 = -1
+
+ for k, v := range values {
+ switch k {
+ case "%Y":
+ tmp, _ := strconv.ParseInt(v, 10, 64)
+ year = int(tmp)
+
+ case "%m":
+ tmp, _ := strconv.ParseInt(v, 10, 64)
+ month = time.Month(int(tmp))
+
+ case "%d":
+ tmp, _ := strconv.ParseInt(v, 10, 64)
+ day = int(tmp)
+
+ case "%H":
+ tmp, _ := strconv.ParseInt(v, 10, 64)
+ hour = int(tmp)
+
+ case "%M":
+ tmp, _ := strconv.ParseInt(v, 10, 64)
+ minute = int(tmp)
+
+ case "%S":
+ tmp, _ := strconv.ParseInt(v, 10, 64)
+ second = int(tmp)
+
+ case "%f":
+ tmp, _ := strconv.ParseInt(v, 10, 64)
+ micros = int(tmp)
+
+ case "%s":
+ unixSec, _ = strconv.ParseInt(v, 10, 64)
+ }
+ }
+
+ if unixSec > 0 {
+ *p = Path(time.Unix(unixSec, 0))
+ } else {
+ *p = Path(time.Date(year, month, day, hour, minute, second, micros*1000, time.Local))
+ }
+
+ return true
+}
+
+// Encode encodes a path.
+func (p Path) Encode(format string) string {
+ format = strings.ReplaceAll(format, "%Y", strconv.FormatInt(int64(time.Time(p).Year()), 10))
+ format = strings.ReplaceAll(format, "%m", leadingZeros(int(time.Time(p).Month()), 2))
+ format = strings.ReplaceAll(format, "%d", leadingZeros(time.Time(p).Day(), 2))
+ format = strings.ReplaceAll(format, "%H", leadingZeros(time.Time(p).Hour(), 2))
+ format = strings.ReplaceAll(format, "%M", leadingZeros(time.Time(p).Minute(), 2))
+ format = strings.ReplaceAll(format, "%S", leadingZeros(time.Time(p).Second(), 2))
+ format = strings.ReplaceAll(format, "%f", leadingZeros(time.Time(p).Nanosecond()/1000, 6))
+ format = strings.ReplaceAll(format, "%s", strconv.FormatInt(time.Time(p).Unix(), 10))
+ return format
+}
diff --git a/internal/record/path_test.go b/internal/record/path_test.go
new file mode 100644
index 00000000000..3831561e0f4
--- /dev/null
+++ b/internal/record/path_test.go
@@ -0,0 +1,47 @@
+package record
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+var pathCases = []struct {
+ name string
+ format string
+ dec Path
+ enc string
+}{
+ {
+ "standard",
+ "%path/%Y-%m-%d_%H-%M-%S-%f.mp4",
+ Path(time.Date(2008, 11, 0o7, 11, 22, 4, 123456000, time.Local)),
+ "%path/2008-11-07_11-22-04-123456.mp4",
+ },
+ {
+ "unix seconds",
+ "%path/%s.mp4",
+ Path(time.Date(2021, 12, 2, 12, 15, 23, 0, time.UTC).Local()),
+ "%path/1638447323.mp4",
+ },
+}
+
+func TestPathDecode(t *testing.T) {
+ for _, ca := range pathCases {
+ t.Run(ca.name, func(t *testing.T) {
+ var dec Path
+ ok := dec.Decode(ca.format, ca.enc)
+ require.Equal(t, true, ok)
+ require.Equal(t, ca.dec, dec)
+ })
+ }
+}
+
+func TestPathEncode(t *testing.T) {
+ for _, ca := range pathCases {
+ t.Run(ca.name, func(t *testing.T) {
+ require.Equal(t, ca.enc, ca.dec.Encode(ca.format))
+ })
+ }
+}
diff --git a/internal/record/record_path.go b/internal/record/record_path.go
deleted file mode 100644
index 4f164391466..00000000000
--- a/internal/record/record_path.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package record
-
-import (
- "regexp"
- "strconv"
- "strings"
- "time"
-)
-
-func leadingZeros(v int, size int) string {
- out := strconv.FormatInt(int64(v), 10)
- if len(out) >= size {
- return out
- }
-
- out2 := ""
- for i := 0; i < (size - len(out)); i++ {
- out2 += "0"
- }
-
- return out2 + out
-}
-
-type recordPathParams struct {
- path string
- time time.Time
-}
-
-func decodeRecordPath(format string, v string) *recordPathParams {
- re := format
- re = strings.ReplaceAll(re, "\\", "\\\\")
- re = strings.ReplaceAll(re, "%path", "(.*?)")
- re = strings.ReplaceAll(re, "%Y", "([0-9]{4})")
- re = strings.ReplaceAll(re, "%m", "([0-9]{2})")
- re = strings.ReplaceAll(re, "%d", "([0-9]{2})")
- re = strings.ReplaceAll(re, "%H", "([0-9]{2})")
- re = strings.ReplaceAll(re, "%M", "([0-9]{2})")
- re = strings.ReplaceAll(re, "%S", "([0-9]{2})")
- re = strings.ReplaceAll(re, "%f", "([0-9]{6})")
- r := regexp.MustCompile(re)
-
- var groupMapping []string
- cur := format
- for {
- i := strings.Index(cur, "%")
- if i < 0 {
- break
- }
-
- cur = cur[i:]
-
- for _, va := range []string{
- "%path",
- "%Y",
- "%m",
- "%d",
- "%H",
- "%M",
- "%S",
- "%f",
- } {
- if strings.HasPrefix(cur, va) {
- groupMapping = append(groupMapping, va)
- }
- }
-
- cur = cur[1:]
- }
-
- matches := r.FindStringSubmatch(v)
- if matches == nil {
- return nil
- }
-
- values := make(map[string]string)
-
- for i, match := range matches[1:] {
- values[groupMapping[i]] = match
- }
-
- var year int
- var month time.Month = 1
- day := 1
- var hour int
- var minute int
- var second int
- var micros int
-
- for k, v := range values {
- switch k {
- case "%Y":
- tmp, _ := strconv.ParseInt(v, 10, 64)
- year = int(tmp)
-
- case "%m":
- tmp, _ := strconv.ParseInt(v, 10, 64)
- month = time.Month(int(tmp))
-
- case "%d":
- tmp, _ := strconv.ParseInt(v, 10, 64)
- day = int(tmp)
-
- case "%H":
- tmp, _ := strconv.ParseInt(v, 10, 64)
- hour = int(tmp)
-
- case "%M":
- tmp, _ := strconv.ParseInt(v, 10, 64)
- minute = int(tmp)
-
- case "%S":
- tmp, _ := strconv.ParseInt(v, 10, 64)
- second = int(tmp)
-
- case "%f":
- tmp, _ := strconv.ParseInt(v, 10, 64)
- micros = int(tmp)
- }
- }
-
- t := time.Date(year, month, day, hour, minute, second, micros*1000, time.Local)
-
- return &recordPathParams{
- path: values["%path"],
- time: t,
- }
-}
-
-func encodeRecordPath(params *recordPathParams, v string) string {
- v = strings.ReplaceAll(v, "%Y", strconv.FormatInt(int64(params.time.Year()), 10))
- v = strings.ReplaceAll(v, "%m", leadingZeros(int(params.time.Month()), 2))
- v = strings.ReplaceAll(v, "%d", leadingZeros(params.time.Day(), 2))
- v = strings.ReplaceAll(v, "%H", leadingZeros(params.time.Hour(), 2))
- v = strings.ReplaceAll(v, "%M", leadingZeros(params.time.Minute(), 2))
- v = strings.ReplaceAll(v, "%S", leadingZeros(params.time.Second(), 2))
- v = strings.ReplaceAll(v, "%f", leadingZeros(params.time.Nanosecond()/1000, 6))
- return v
-}
diff --git a/internal/record/segment.go b/internal/record/segment.go
deleted file mode 100644
index 06c527ed7c9..00000000000
--- a/internal/record/segment.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package record
-
-import (
- "io"
- "os"
- "time"
-
- "github.com/aler9/writerseeker"
- "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
-
- "github.com/bluenviron/mediamtx/internal/logger"
-)
-
-var timeNow = time.Now
-
-func writeInit(f io.Writer, tracks []*track) error {
- fmp4Tracks := make([]*fmp4.InitTrack, len(tracks))
- for i, track := range tracks {
- fmp4Tracks[i] = track.initTrack
- }
-
- init := fmp4.Init{
- Tracks: fmp4Tracks,
- }
-
- var ws writerseeker.WriterSeeker
- err := init.Marshal(&ws)
- if err != nil {
- return err
- }
-
- _, err = f.Write(ws.Bytes())
- return err
-}
-
-type segment struct {
- r *Agent
- startDTS time.Duration
-
- fpath string
- f *os.File
- curPart *part
-}
-
-func newSegment(
- r *Agent,
- startDTS time.Duration,
-) *segment {
- return &segment{
- r: r,
- startDTS: startDTS,
- }
-}
-
-func (s *segment) close() error {
- var err error
-
- if s.curPart != nil {
- err = s.curPart.close()
- }
-
- if s.f != nil {
- s.r.Log(logger.Debug, "closing segment %s", s.fpath)
- err2 := s.f.Close()
- if err == nil {
- err = err2
- }
-
- if err2 == nil {
- s.r.onSegmentComplete(s.fpath)
- }
- }
-
- return err
-}
-
-func (s *segment) record(track *track, sample *sample) error {
- if s.curPart == nil {
- s.curPart = newPart(s, sample.dts)
- } else if s.curPart.duration() >= s.r.partDuration {
- err := s.curPart.close()
- s.curPart = nil
-
- if err != nil {
- return err
- }
-
- s.curPart = newPart(s, sample.dts)
- }
-
- return s.curPart.record(track, sample)
-}
diff --git a/internal/record/track.go b/internal/record/track.go
deleted file mode 100644
index 22fce7510d6..00000000000
--- a/internal/record/track.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package record
-
-import (
- "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
-)
-
-type track struct {
- r *Agent
- initTrack *fmp4.InitTrack
-
- nextSample *sample
-}
-
-func newTrack(
- r *Agent,
- initTrack *fmp4.InitTrack,
-) *track {
- return &track{
- r: r,
- initTrack: initTrack,
- }
-}
-
-func (t *track) record(sample *sample) error {
- // wait the first video sample before setting hasVideo
- if t.initTrack.Codec.IsVideo() {
- t.r.hasVideo = true
- }
-
- if t.r.currentSegment == nil {
- t.r.currentSegment = newSegment(t.r, sample.dts)
- }
-
- sample, t.nextSample = t.nextSample, sample
- if sample == nil {
- return nil
- }
- sample.Duration = uint32(durationGoToMp4(t.nextSample.dts-sample.dts, t.initTrack.TimeScale))
-
- err := t.r.currentSegment.record(t, sample)
- if err != nil {
- return err
- }
-
- if (!t.r.hasVideo || t.initTrack.Codec.IsVideo()) &&
- !t.nextSample.IsNonSyncSample &&
- (t.nextSample.dts-t.r.currentSegment.startDTS) >= t.r.segmentDuration {
- err := t.r.currentSegment.close()
- if err != nil {
- return err
- }
-
- t.r.currentSegment = newSegment(t.r, t.nextSample.dts)
- }
-
- return nil
-}
diff --git a/internal/restrictnetwork/restrict_network.go b/internal/restrictnetwork/restrict_network.go
new file mode 100644
index 00000000000..9c5cdef7def
--- /dev/null
+++ b/internal/restrictnetwork/restrict_network.go
@@ -0,0 +1,18 @@
+// Package restrictnetwork contains Restrict().
+package restrictnetwork
+
+import (
+ "net"
+)
+
+// Restrict avoids listening on IPv6 when address is 0.0.0.0.
+func Restrict(network string, address string) (string, string) {
+ host, _, err := net.SplitHostPort(address)
+ if err == nil {
+ if host == "0.0.0.0" {
+ return network + "4", address
+ }
+ }
+
+ return network, address
+}
diff --git a/internal/rtmp/chunk/chunk0.go b/internal/rtmp/chunk/chunk0.go
deleted file mode 100644
index 7bb76d0908e..00000000000
--- a/internal/rtmp/chunk/chunk0.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package chunk
-
-import (
- "io"
-)
-
-// Chunk0 is a type 0 chunk.
-// This type MUST be used at
-// the start of a chunk stream, and whenever the stream timestamp goes
-// backward (e.g., because of a backward seek).
-type Chunk0 struct {
- ChunkStreamID byte
- Timestamp uint32
- Type uint8
- MessageStreamID uint32
- BodyLen uint32
- Body []byte
-}
-
-// Read reads the chunk.
-func (c *Chunk0) Read(r io.Reader, chunkMaxBodyLen uint32) error {
- header := make([]byte, 12)
- _, err := io.ReadFull(r, header)
- if err != nil {
- return err
- }
-
- c.ChunkStreamID = header[0] & 0x3F
- c.Timestamp = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
- c.BodyLen = uint32(header[4])<<16 | uint32(header[5])<<8 | uint32(header[6])
- c.Type = header[7]
- c.MessageStreamID = uint32(header[8])<<24 | uint32(header[9])<<16 | uint32(header[10])<<8 | uint32(header[11])
-
- chunkBodyLen := c.BodyLen
- if chunkBodyLen > chunkMaxBodyLen {
- chunkBodyLen = chunkMaxBodyLen
- }
-
- c.Body = make([]byte, chunkBodyLen)
- _, err = io.ReadFull(r, c.Body)
- return err
-}
-
-// Marshal writes the chunk.
-func (c Chunk0) Marshal() ([]byte, error) {
- buf := make([]byte, 12+len(c.Body))
- buf[0] = c.ChunkStreamID
- buf[1] = byte(c.Timestamp >> 16)
- buf[2] = byte(c.Timestamp >> 8)
- buf[3] = byte(c.Timestamp)
- buf[4] = byte(c.BodyLen >> 16)
- buf[5] = byte(c.BodyLen >> 8)
- buf[6] = byte(c.BodyLen)
- buf[7] = c.Type
- buf[8] = byte(c.MessageStreamID >> 24)
- buf[9] = byte(c.MessageStreamID >> 16)
- buf[10] = byte(c.MessageStreamID >> 8)
- buf[11] = byte(c.MessageStreamID)
- copy(buf[12:], c.Body)
- return buf, nil
-}
diff --git a/internal/rtmp/chunk/chunk0_test.go b/internal/rtmp/chunk/chunk0_test.go
deleted file mode 100644
index 48105584013..00000000000
--- a/internal/rtmp/chunk/chunk0_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package chunk
-
-import (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-var chunk0enc = []byte{
- 0x19, 0xb1, 0xa1, 0x91, 0x0, 0x0, 0x14, 0x14,
- 0x3, 0x5d, 0x17, 0x3d, 0x1, 0x2, 0x3, 0x4,
-}
-
-var chunk0dec = Chunk0{
- ChunkStreamID: 25,
- Timestamp: 11641233,
- Type: 20,
- MessageStreamID: 56432445,
- BodyLen: 20,
- Body: []byte{0x01, 0x02, 0x03, 0x04},
-}
-
-func TestChunk0Read(t *testing.T) {
- var chunk0 Chunk0
- err := chunk0.Read(bytes.NewReader(chunk0enc), 4)
- require.NoError(t, err)
- require.Equal(t, chunk0dec, chunk0)
-}
-
-func TestChunk0Marshal(t *testing.T) {
- buf, err := chunk0dec.Marshal()
- require.NoError(t, err)
- require.Equal(t, chunk0enc, buf)
-}
diff --git a/internal/rtmp/chunk/chunk1.go b/internal/rtmp/chunk/chunk1.go
deleted file mode 100644
index 3c2ed0bdc7b..00000000000
--- a/internal/rtmp/chunk/chunk1.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package chunk
-
-import (
- "io"
-)
-
-// Chunk1 is a type 1 chunk.
-// The message stream ID is not
-// included; this chunk takes the same stream ID as the preceding chunk.
-// Streams with variable-sized messages (for example, many video
-// formats) SHOULD use this format for the first chunk of each new
-// message after the first.
-type Chunk1 struct {
- ChunkStreamID byte
- TimestampDelta uint32
- Type uint8
- BodyLen uint32
- Body []byte
-}
-
-// Read reads the chunk.
-func (c *Chunk1) Read(r io.Reader, chunkMaxBodyLen uint32) error {
- header := make([]byte, 8)
- _, err := io.ReadFull(r, header)
- if err != nil {
- return err
- }
-
- c.ChunkStreamID = header[0] & 0x3F
- c.TimestampDelta = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
- c.BodyLen = uint32(header[4])<<16 | uint32(header[5])<<8 | uint32(header[6])
- c.Type = header[7]
-
- chunkBodyLen := (c.BodyLen)
- if chunkBodyLen > chunkMaxBodyLen {
- chunkBodyLen = chunkMaxBodyLen
- }
-
- c.Body = make([]byte, chunkBodyLen)
- _, err = io.ReadFull(r, c.Body)
- return err
-}
-
-// Marshal writes the chunk.
-func (c Chunk1) Marshal() ([]byte, error) {
- buf := make([]byte, 8+len(c.Body))
- buf[0] = 1<<6 | c.ChunkStreamID
- buf[1] = byte(c.TimestampDelta >> 16)
- buf[2] = byte(c.TimestampDelta >> 8)
- buf[3] = byte(c.TimestampDelta)
- buf[4] = byte(c.BodyLen >> 16)
- buf[5] = byte(c.BodyLen >> 8)
- buf[6] = byte(c.BodyLen)
- buf[7] = c.Type
- copy(buf[8:], c.Body)
- return buf, nil
-}
diff --git a/internal/rtmp/chunk/chunk1_test.go b/internal/rtmp/chunk/chunk1_test.go
deleted file mode 100644
index 3d6cf33c8a5..00000000000
--- a/internal/rtmp/chunk/chunk1_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package chunk
-
-import (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-var chunk1enc = []byte{
- 0x59, 0xb1, 0xa1, 0x91, 0x0, 0x0, 0x14, 0x14,
- 0x1, 0x2, 0x3, 0x4,
-}
-
-var chunk1dec = Chunk1{
- ChunkStreamID: 25,
- TimestampDelta: 11641233,
- Type: 20,
- BodyLen: 20,
- Body: []byte{0x01, 0x02, 0x03, 0x04},
-}
-
-func TestChunk1Read(t *testing.T) {
- var chunk1 Chunk1
- err := chunk1.Read(bytes.NewReader(chunk1enc), 4)
- require.NoError(t, err)
- require.Equal(t, chunk1dec, chunk1)
-}
-
-func TestChunk1Marshal(t *testing.T) {
- buf, err := chunk1dec.Marshal()
- require.NoError(t, err)
- require.Equal(t, chunk1enc, buf)
-}
diff --git a/internal/rtmp/chunk/chunk2.go b/internal/rtmp/chunk/chunk2.go
deleted file mode 100644
index e43a69d4c2d..00000000000
--- a/internal/rtmp/chunk/chunk2.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package chunk
-
-import (
- "io"
-)
-
-// Chunk2 is a type 2 chunk.
-// Neither the stream ID nor the
-// message length is included; this chunk has the same stream ID and
-// message length as the preceding chunk.
-type Chunk2 struct {
- ChunkStreamID byte
- TimestampDelta uint32
- Body []byte
-}
-
-// Read reads the chunk.
-func (c *Chunk2) Read(r io.Reader, chunkBodyLen uint32) error {
- header := make([]byte, 4)
- _, err := io.ReadFull(r, header)
- if err != nil {
- return err
- }
-
- c.ChunkStreamID = header[0] & 0x3F
- c.TimestampDelta = uint32(header[1])<<16 | uint32(header[2])<<8 | uint32(header[3])
-
- c.Body = make([]byte, chunkBodyLen)
- _, err = io.ReadFull(r, c.Body)
- return err
-}
-
-// Marshal writes the chunk.
-func (c Chunk2) Marshal() ([]byte, error) {
- buf := make([]byte, 4+len(c.Body))
- buf[0] = 2<<6 | c.ChunkStreamID
- buf[1] = byte(c.TimestampDelta >> 16)
- buf[2] = byte(c.TimestampDelta >> 8)
- buf[3] = byte(c.TimestampDelta)
- copy(buf[4:], c.Body)
- return buf, nil
-}
diff --git a/internal/rtmp/chunk/chunk2_test.go b/internal/rtmp/chunk/chunk2_test.go
deleted file mode 100644
index e5d8defcdfd..00000000000
--- a/internal/rtmp/chunk/chunk2_test.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package chunk
-
-import (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-var chunk2enc = []byte{
- 0x99, 0xb1, 0xa1, 0x91, 0x1, 0x2, 0x3, 0x4,
-}
-
-var chunk2dec = Chunk2{
- ChunkStreamID: 25,
- TimestampDelta: 11641233,
- Body: []byte{0x01, 0x02, 0x03, 0x04},
-}
-
-func TestChunk2Read(t *testing.T) {
- var chunk2 Chunk2
- err := chunk2.Read(bytes.NewReader(chunk2enc), 4)
- require.NoError(t, err)
- require.Equal(t, chunk2dec, chunk2)
-}
-
-func TestChunk2Marshal(t *testing.T) {
- buf, err := chunk2dec.Marshal()
- require.NoError(t, err)
- require.Equal(t, chunk2enc, buf)
-}
diff --git a/internal/rtmp/chunk/chunk3_test.go b/internal/rtmp/chunk/chunk3_test.go
deleted file mode 100644
index ba5b8fa0fa1..00000000000
--- a/internal/rtmp/chunk/chunk3_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package chunk
-
-import (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-var chunk3enc = []byte{
- 0xd9, 0x1, 0x2, 0x3, 0x4,
-}
-
-var chunk3dec = Chunk3{
- ChunkStreamID: 25,
- Body: []byte{0x01, 0x02, 0x03, 0x04},
-}
-
-func TestChunk3Read(t *testing.T) {
- var chunk3 Chunk3
- err := chunk3.Read(bytes.NewReader(chunk3enc), 4)
- require.NoError(t, err)
- require.Equal(t, chunk3dec, chunk3)
-}
-
-func TestChunk3Marshal(t *testing.T) {
- buf, err := chunk3dec.Marshal()
- require.NoError(t, err)
- require.Equal(t, chunk3enc, buf)
-}
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/2c0aa0d43cf2378b b/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/2c0aa0d43cf2378b
deleted file mode 100644
index fffa343cf17..00000000000
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/2c0aa0d43cf2378b
+++ /dev/null
@@ -1,2 +0,0 @@
-go test fuzz v1
-[]byte("0000\xf4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/3225fb43d226570f b/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/3225fb43d226570f
deleted file mode 100644
index 6bd85a31fa2..00000000000
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/3225fb43d226570f
+++ /dev/null
@@ -1,2 +0,0 @@
-go test fuzz v1
-[]byte("0000\x00\x00000000000000000000000000000000000000000000000000000000\xb0")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/649388b35b8d7d24 b/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/649388b35b8d7d24
deleted file mode 100644
index 8e2e990bea9..00000000000
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/649388b35b8d7d24
+++ /dev/null
@@ -1,2 +0,0 @@
-go test fuzz v1
-[]byte("\x00000\x00\x00\x04000000000\x800000000\xc0")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/899c4ec5c6184841 b/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/899c4ec5c6184841
deleted file mode 100644
index 1edf25d0e62..00000000000
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/899c4ec5c6184841
+++ /dev/null
@@ -1,2 +0,0 @@
-go test fuzz v1
-[]byte("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\xf0")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/a2d2a54b9b1b0098 b/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/a2d2a54b9b1b0098
deleted file mode 100644
index 250cd245730..00000000000
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/a2d2a54b9b1b0098
+++ /dev/null
@@ -1,2 +0,0 @@
-go test fuzz v1
-[]byte("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/ab461fd3f1e0b76d b/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/ab461fd3f1e0b76d
deleted file mode 100644
index 78cd7065080..00000000000
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/ab461fd3f1e0b76d
+++ /dev/null
@@ -1,2 +0,0 @@
-go test fuzz v1
-[]byte("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000p")
diff --git a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/cf0f70a31328c9ba b/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/cf0f70a31328c9ba
deleted file mode 100644
index 25808deee89..00000000000
--- a/internal/rtmp/rawmessage/testdata/fuzz/FuzzDecoder/cf0f70a31328c9ba
+++ /dev/null
@@ -1,2 +0,0 @@
-go test fuzz v1
-[]byte("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\xb0")
diff --git a/internal/servers/hls/hls.min.js b/internal/servers/hls/hls.min.js
new file mode 100644
index 00000000000..631227e9c97
--- /dev/null
+++ b/internal/servers/hls/hls.min.js
@@ -0,0 +1,2 @@
+!function t(e){var r,i;r=this,i=function(){"use strict";function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,i)}return r}function i(t){for(var e=1;et.length)&&(e=t.length);for(var r=0,i=new Array(e);r=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function v(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var m={exports:{}};!function(t,e){var r,i,n,a,s;r=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,i=/^(?=([^\/?#]*))\1([^]*)$/,n=/(?:\/|^)\.(?=\/)/g,a=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,s={buildAbsoluteURL:function(t,e,r){if(r=r||{},t=t.trim(),!(e=e.trim())){if(!r.alwaysNormalize)return t;var n=s.parseURL(t);if(!n)throw new Error("Error trying to parse base URL.");return n.path=s.normalizePath(n.path),s.buildURLFromParts(n)}var a=s.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return r.alwaysNormalize?(a.path=s.normalizePath(a.path),s.buildURLFromParts(a)):e;var o=s.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=i.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var u={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(u.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var h=o.path,d=h.substring(0,h.lastIndexOf("/")+1)+a.path;u.path=s.normalizePath(d)}else u.path=o.path,a.params||(u.params=o.params,a.query||(u.query=o.query));return null===u.path&&(u.path=r.alwaysNormalize?s.normalizePath(a.path):a.path),s.buildURLFromParts(u)},parseURL:function(t){var e=r.exec(t);return e?{scheme:e[1]||"",netLoc:e[2]||"",path:e[3]||"",params:e[4]||"",query:e[5]||"",fragment:e[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(n,"");t.length!==(t=t.replace(a,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}},t.exports=s}(m);var p=m.exports,y=Number.isFinite||function(t){return"number"==typeof t&&isFinite(t)},E=Number.isSafeInteger||function(t){return"number"==typeof t&&Math.abs(t)<=T},T=Number.MAX_SAFE_INTEGER||9007199254740991,S=function(t){return t.MEDIA_ATTACHING="hlsMediaAttaching",t.MEDIA_ATTACHED="hlsMediaAttached",t.MEDIA_DETACHING="hlsMediaDetaching",t.MEDIA_DETACHED="hlsMediaDetached",t.BUFFER_RESET="hlsBufferReset",t.BUFFER_CODECS="hlsBufferCodecs",t.BUFFER_CREATED="hlsBufferCreated",t.BUFFER_APPENDING="hlsBufferAppending",t.BUFFER_APPENDED="hlsBufferAppended",t.BUFFER_EOS="hlsBufferEos",t.BUFFER_FLUSHING="hlsBufferFlushing",t.BUFFER_FLUSHED="hlsBufferFlushed",t.MANIFEST_LOADING="hlsManifestLoading",t.MANIFEST_LOADED="hlsManifestLoaded",t.MANIFEST_PARSED="hlsManifestParsed",t.LEVEL_SWITCHING="hlsLevelSwitching",t.LEVEL_SWITCHED="hlsLevelSwitched",t.LEVEL_LOADING="hlsLevelLoading",t.LEVEL_LOADED="hlsLevelLoaded",t.LEVEL_UPDATED="hlsLevelUpdated",t.LEVEL_PTS_UPDATED="hlsLevelPtsUpdated",t.LEVELS_UPDATED="hlsLevelsUpdated",t.AUDIO_TRACKS_UPDATED="hlsAudioTracksUpdated",t.AUDIO_TRACK_SWITCHING="hlsAudioTrackSwitching",t.AUDIO_TRACK_SWITCHED="hlsAudioTrackSwitched",t.AUDIO_TRACK_LOADING="hlsAudioTrackLoading",t.AUDIO_TRACK_LOADED="hlsAudioTrackLoaded",t.SUBTITLE_TRACKS_UPDATED="hlsSubtitleTracksUpdated",t.SUBTITLE_TRACKS_CLEARED="hlsSubtitleTracksCleared",t.SUBTITLE_TRACK_SWITCH="hlsSubtitleTrackSwitch",t.SUBTITLE_TRACK_LOADING="hlsSubtitleTrackLoading",t.SUBTITLE_TRACK_LOADED="hlsSubtitleTrackLoaded",t.SUBTITLE_FRAG_PROCESSED="hlsSubtitleFragProcessed",t.CUES_PARSED="hlsCuesParsed",t.NON_NATIVE_TEXT_TRACKS_FOUND="hlsNonNativeTextTracksFound",t.INIT_PTS_FOUND="hlsInitPtsFound",t.FRAG_LOADING="hlsFragLoading",t.FRAG_LOAD_EMERGENCY_ABORTED="hlsFragLoadEmergencyAborted",t.FRAG_LOADED="hlsFragLoaded",t.FRAG_DECRYPTED="hlsFragDecrypted",t.FRAG_PARSING_INIT_SEGMENT="hlsFragParsingInitSegment",t.FRAG_PARSING_USERDATA="hlsFragParsingUserdata",t.FRAG_PARSING_METADATA="hlsFragParsingMetadata",t.FRAG_PARSED="hlsFragParsed",t.FRAG_BUFFERED="hlsFragBuffered",t.FRAG_CHANGED="hlsFragChanged",t.FPS_DROP="hlsFpsDrop",t.FPS_DROP_LEVEL_CAPPING="hlsFpsDropLevelCapping",t.MAX_AUTO_LEVEL_UPDATED="hlsMaxAutoLevelUpdated",t.ERROR="hlsError",t.DESTROYING="hlsDestroying",t.KEY_LOADING="hlsKeyLoading",t.KEY_LOADED="hlsKeyLoaded",t.LIVE_BACK_BUFFER_REACHED="hlsLiveBackBufferReached",t.BACK_BUFFER_REACHED="hlsBackBufferReached",t.STEERING_MANIFEST_LOADED="hlsSteeringManifestLoaded",t}({}),L=function(t){return t.NETWORK_ERROR="networkError",t.MEDIA_ERROR="mediaError",t.KEY_SYSTEM_ERROR="keySystemError",t.MUX_ERROR="muxError",t.OTHER_ERROR="otherError",t}({}),A=function(t){return t.KEY_SYSTEM_NO_KEYS="keySystemNoKeys",t.KEY_SYSTEM_NO_ACCESS="keySystemNoAccess",t.KEY_SYSTEM_NO_SESSION="keySystemNoSession",t.KEY_SYSTEM_NO_CONFIGURED_LICENSE="keySystemNoConfiguredLicense",t.KEY_SYSTEM_LICENSE_REQUEST_FAILED="keySystemLicenseRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED="keySystemServerCertificateRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED="keySystemServerCertificateUpdateFailed",t.KEY_SYSTEM_SESSION_UPDATE_FAILED="keySystemSessionUpdateFailed",t.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED="keySystemStatusOutputRestricted",t.KEY_SYSTEM_STATUS_INTERNAL_ERROR="keySystemStatusInternalError",t.MANIFEST_LOAD_ERROR="manifestLoadError",t.MANIFEST_LOAD_TIMEOUT="manifestLoadTimeOut",t.MANIFEST_PARSING_ERROR="manifestParsingError",t.MANIFEST_INCOMPATIBLE_CODECS_ERROR="manifestIncompatibleCodecsError",t.LEVEL_EMPTY_ERROR="levelEmptyError",t.LEVEL_LOAD_ERROR="levelLoadError",t.LEVEL_LOAD_TIMEOUT="levelLoadTimeOut",t.LEVEL_PARSING_ERROR="levelParsingError",t.LEVEL_SWITCH_ERROR="levelSwitchError",t.AUDIO_TRACK_LOAD_ERROR="audioTrackLoadError",t.AUDIO_TRACK_LOAD_TIMEOUT="audioTrackLoadTimeOut",t.SUBTITLE_LOAD_ERROR="subtitleTrackLoadError",t.SUBTITLE_TRACK_LOAD_TIMEOUT="subtitleTrackLoadTimeOut",t.FRAG_LOAD_ERROR="fragLoadError",t.FRAG_LOAD_TIMEOUT="fragLoadTimeOut",t.FRAG_DECRYPT_ERROR="fragDecryptError",t.FRAG_PARSING_ERROR="fragParsingError",t.FRAG_GAP="fragGap",t.REMUX_ALLOC_ERROR="remuxAllocError",t.KEY_LOAD_ERROR="keyLoadError",t.KEY_LOAD_TIMEOUT="keyLoadTimeOut",t.BUFFER_ADD_CODEC_ERROR="bufferAddCodecError",t.BUFFER_INCOMPATIBLE_CODECS_ERROR="bufferIncompatibleCodecsError",t.BUFFER_APPEND_ERROR="bufferAppendError",t.BUFFER_APPENDING_ERROR="bufferAppendingError",t.BUFFER_STALLED_ERROR="bufferStalledError",t.BUFFER_FULL_ERROR="bufferFullError",t.BUFFER_SEEK_OVER_HOLE="bufferSeekOverHole",t.BUFFER_NUDGE_ON_STALL="bufferNudgeOnStall",t.INTERNAL_EXCEPTION="internalException",t.INTERNAL_ABORTED="aborted",t.UNKNOWN="unknown",t}({}),R=function(){},k={trace:R,debug:R,log:R,warn:R,info:R,error:R},b=k;function D(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i"):R}(e)}))}function I(t,e){if("object"==typeof console&&!0===t||"object"==typeof t){D(t,"debug","log","info","warn","error");try{b.log('Debug logs enabled for "'+e+'" in hls.js version 1.5.1')}catch(t){b=k}}else b=k}var w=b,C=/^(\d+)x(\d+)$/,_=/(.+?)=(".*?"|.*?)(?:,|$)/g,x=function(){function t(e){"string"==typeof e&&(e=t.parseAttrList(e)),o(this,e)}var e=t.prototype;return e.decimalInteger=function(t){var e=parseInt(this[t],10);return e>Number.MAX_SAFE_INTEGER?1/0:e},e.hexadecimalInteger=function(t){if(this[t]){var e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;for(var r=new Uint8Array(e.length/2),i=0;iNumber.MAX_SAFE_INTEGER?1/0:e},e.decimalFloatingPoint=function(t){return parseFloat(this[t])},e.optionalFloat=function(t,e){var r=this[t];return r?parseFloat(r):e},e.enumeratedString=function(t){return this[t]},e.bool=function(t){return"YES"===this[t]},e.decimalResolution=function(t){var e=C.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}},t.parseAttrList=function(t){var e,r={};for(_.lastIndex=0;null!==(e=_.exec(t));){var i=e[2];0===i.indexOf('"')&&i.lastIndexOf('"')===i.length-1&&(i=i.slice(1,-1)),r[e[1].trim()]=i}return r},s(t,[{key:"clientAttrs",get:function(){return Object.keys(this).filter((function(t){return"X-"===t.substring(0,2)}))}}]),t}();function P(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}var F=function(){function t(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){var r=e.attr;for(var i in r)if(Object.prototype.hasOwnProperty.call(t,i)&&t[i]!==r[i]){w.warn('DATERANGE tag attribute: "'+i+'" does not match for tags with ID: "'+t.ID+'"'),this._badValueForSameId=i;break}t=o(new x({}),r,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){var n=new Date(this.attr["END-DATE"]);y(n.getTime())&&(this._endDate=n)}}return s(t,[{key:"id",get:function(){return this.attr.ID}},{key:"class",get:function(){return this.attr.CLASS}},{key:"startDate",get:function(){return this._startDate}},{key:"endDate",get:function(){if(this._endDate)return this._endDate;var t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}},{key:"duration",get:function(){if("DURATION"in this.attr){var t=this.attr.decimalFloatingPoint("DURATION");if(y(t))return t}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}},{key:"plannedDuration",get:function(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}},{key:"endOnNext",get:function(){return this.attr.bool("END-ON-NEXT")}},{key:"isValid",get:function(){return!!this.id&&!this._badValueForSameId&&y(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}]),t}(),M=function(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}},O="audio",N="video",U="audiovideo",B=function(){function t(t){var e;this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams=((e={})[O]=null,e[N]=null,e[U]=null,e),this.baseurl=t}return t.prototype.setByteRange=function(t,e){var r,i=t.split("@",2);r=1===i.length?(null==e?void 0:e.byteRangeEndOffset)||0:parseInt(i[1]),this._byteRange=[r,parseInt(i[0])+r]},s(t,[{key:"byteRange",get:function(){return this._byteRange?this._byteRange:[]}},{key:"byteRangeStartOffset",get:function(){return this.byteRange[0]}},{key:"byteRangeEndOffset",get:function(){return this.byteRange[1]}},{key:"url",get:function(){return!this._url&&this.baseurl&&this.relurl&&(this._url=p.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""},set:function(t){this._url=t}}]),t}(),G=function(t){function e(e,r){var i;return(i=t.call(this,r)||this)._decryptdata=null,i.rawProgramDateTime=null,i.programDateTime=null,i.tagList=[],i.duration=0,i.sn=0,i.levelkeys=void 0,i.type=void 0,i.loader=null,i.keyLoader=null,i.level=-1,i.cc=0,i.startPTS=void 0,i.endPTS=void 0,i.startDTS=void 0,i.endDTS=void 0,i.start=0,i.deltaPTS=void 0,i.maxStartPTS=void 0,i.minEndPTS=void 0,i.stats=new M,i.data=void 0,i.bitrateTest=!1,i.title=null,i.initSegment=null,i.endList=void 0,i.gap=void 0,i.urlId=0,i.type=e,i}l(e,t);var r=e.prototype;return r.setKeyFormat=function(t){if(this.levelkeys){var e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}},r.abortRequests=function(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()},r.setElementaryStreamInfo=function(t,e,r,i,n,a){void 0===a&&(a=!1);var s=this.elementaryStreams,o=s[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,r),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,n)):s[t]={startPTS:e,endPTS:r,startDTS:i,endDTS:n,partial:a}},r.clearElementaryStreamInfo=function(){var t=this.elementaryStreams;t[O]=null,t[N]=null,t[U]=null},s(e,[{key:"decryptdata",get:function(){if(!this.levelkeys&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){var t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{var e=Object.keys(this.levelkeys);if(1===e.length)return this._decryptdata=this.levelkeys[e[0]].getDecryptData(this.sn)}}return this._decryptdata}},{key:"end",get:function(){return this.start+this.duration}},{key:"endProgramDateTime",get:function(){if(null===this.programDateTime)return null;if(!y(this.programDateTime))return null;var t=y(this.duration)?this.duration:0;return this.programDateTime+1e3*t}},{key:"encrypted",get:function(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){var e=Object.keys(this.levelkeys),r=e.length;if(r>1||1===r&&this.levelkeys[e[0]].encrypted)return!0}return!1}}]),e}(B),K=function(t){function e(e,r,i,n,a){var s;(s=t.call(this,i)||this).fragOffset=0,s.duration=0,s.gap=!1,s.independent=!1,s.relurl=void 0,s.fragment=void 0,s.index=void 0,s.stats=new M,s.duration=e.decimalFloatingPoint("DURATION"),s.gap=e.bool("GAP"),s.independent=e.bool("INDEPENDENT"),s.relurl=e.enumeratedString("URI"),s.fragment=r,s.index=n;var o=e.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,a),a&&(s.fragOffset=a.fragOffset+a.duration),s}return l(e,t),s(e,[{key:"start",get:function(){return this.fragment.start+this.fragOffset}},{key:"end",get:function(){return this.start+this.duration}},{key:"loaded",get:function(){var t=this.elementaryStreams;return!!(t.audio||t.video||t.audiovideo)}}]),e}(B),H=function(){function t(t){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}return t.prototype.reloaded=function(t){if(!t)return this.advanced=!0,void(this.updated=!0);var e=this.lastPartSn-t.lastPartSn,r=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!r||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&r>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay},s(t,[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&y(this.fragments[this.fragments.length-1].programDateTime)}},{key:"levelTargetDuration",get:function(){return this.averagetargetduration||this.targetduration||10}},{key:"drift",get:function(){var t=this.driftEndTime-this.driftStartTime;return t>0?1e3*(this.driftEnd-this.driftStart)/t:1}},{key:"edge",get:function(){return this.partEnd||this.fragmentEnd}},{key:"partEnd",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}},{key:"age",get:function(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}},{key:"lastPartIndex",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}},{key:"lastPartSn",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}]),t}();function V(t){return Uint8Array.from(atob(t),(function(t){return t.charCodeAt(0)}))}function Y(t){var e,r,i=t.split(":"),n=null;if("data"===i[0]&&2===i.length){var a=i[1].split(";"),s=a[a.length-1].split(",");if(2===s.length){var o="base64"===s[0],l=s[1];o?(a.splice(-1,1),n=V(l)):(e=W(l).subarray(0,16),(r=new Uint8Array(16)).set(e,16-e.length),n=r)}}return n}function W(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(function(t){return t.charCodeAt(0)}))}var j="undefined"!=typeof self?self:void 0,q={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},X="org.w3.clearkey",z="com.apple.streamingkeydelivery",Q="com.microsoft.playready",J="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function $(t){switch(t){case z:return q.FAIRPLAY;case Q:return q.PLAYREADY;case J:return q.WIDEVINE;case X:return q.CLEARKEY}}var Z="edef8ba979d64acea3c827dcd51d21ed";function tt(t){switch(t){case q.FAIRPLAY:return z;case q.PLAYREADY:return Q;case q.WIDEVINE:return J;case q.CLEARKEY:return X}}function et(t){var e=t.drmSystems,r=t.widevineLicenseUrl,i=e?[q.FAIRPLAY,q.WIDEVINE,q.PLAYREADY,q.CLEARKEY].filter((function(t){return!!e[t]})):[];return!i[q.WIDEVINE]&&r&&i.push(q.WIDEVINE),i}var rt,it=null!=j&&null!=(rt=j.navigator)&&rt.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function nt(t,e,r){return Uint8Array.prototype.slice?t.slice(e,r):new Uint8Array(Array.prototype.slice.call(t,e,r))}var at,st=function(t,e){return e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},ot=function(t,e){return e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},lt=function(t,e){for(var r=e,i=0;st(t,e);)i+=10,i+=ut(t,e+6),ot(t,e+10)&&(i+=10),e+=i;if(i>0)return t.subarray(r,r+i)},ut=function(t,e){var r=0;return r=(127&t[e])<<21,r|=(127&t[e+1])<<14,r|=(127&t[e+2])<<7,r|=127&t[e+3]},ht=function(t,e){return st(t,e)&&ut(t,e+6)+10<=t.length-e},dt=function(t){for(var e=gt(t),r=0;r>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:u+=String.fromCharCode(a);break;case 12:case 13:s=t[h++],u+=String.fromCharCode((31&a)<<6|63&s);break;case 14:s=t[h++],o=t[h++],u+=String.fromCharCode((15&a)<<12|(63&s)<<6|(63&o)<<0)}}return u};function St(){if(!navigator.userAgent.includes("PlayStation 4"))return at||void 0===self.TextDecoder||(at=new self.TextDecoder("utf-8")),at}var Lt=function(t){for(var e="",r=0;r>24,t[e+1]=r>>16&255,t[e+2]=r>>8&255,t[e+3]=255&r}function _t(t,e){var r=[];if(!e.length)return r;for(var i=t.byteLength,n=0;n1?n+a:i;if(bt(t.subarray(n+4,n+8))===e[0])if(1===e.length)r.push(t.subarray(n+8,s));else{var o=_t(t.subarray(n+8,s),e.slice(1));o.length&&Rt.apply(r,o)}n=s}return r}function xt(t){var e=[],r=t[0],i=8,n=It(t,i);i+=4,i+=0===r?8:16,i+=2;var a=t.length+0,s=Dt(t,i);i+=2;for(var o=0;o>>31)return w.warn("SIDX has hierarchical references (not supported)"),null;var d=It(t,l);l+=4,e.push({referenceSize:h,subsegmentDuration:d,info:{duration:d/n,start:a,end:a+h-1}}),a+=h,i=l+=4}return{earliestPresentationTime:0,timescale:n,version:r,referencesCount:s,references:e}}function Pt(t){for(var e=[],r=_t(t,["moov","trak"]),n=0;n12){var h=4;if(3!==u[h++])break;h=Mt(u,h),h+=2;var d=u[h++];if(128&d&&(h+=2),64&d&&(h+=u[h++]),4!==u[h++])break;h=Mt(u,h);var c=u[h++];if(64!==c)break;if(n+="."+Ot(c),h+=12,5!==u[h++])break;h=Mt(u,h);var f=u[h++],g=(248&f)>>3;31===g&&(g+=1+((7&f)<<3)+((224&u[h])>>5)),n+="."+g}break;case"hvc1":case"hev1":var v=_t(r,["hvcC"])[0],m=v[1],p=["","A","B","C"][m>>6],y=31&m,E=It(v,2),T=(32&m)>>5?"H":"L",S=v[12],L=v.subarray(6,12);n+="."+p+y,n+="."+E.toString(16).toUpperCase(),n+="."+T+S;for(var A="",R=L.length;R--;){var k=L[R];(k||A)&&(A="."+k.toString(16).toUpperCase()+A)}n+=A;break;case"dvh1":case"dvhe":var b=_t(r,["dvcC"])[0],D=b[2]>>1&127,I=b[2]<<5&32|b[3]>>3&31;n+="."+Nt(D)+"."+Nt(I);break;case"vp09":var w=_t(r,["vpcC"])[0],C=w[4],_=w[5],x=w[6]>>4&15;n+="."+Nt(C)+"."+Nt(_)+"."+Nt(x);break;case"av01":var P=_t(r,["av1C"])[0],F=P[1]>>>5,M=31&P[1],O=P[2]>>>7?"H":"M",N=(64&P[2])>>6,U=(32&P[2])>>5,B=2===F&&N?U?12:10:N?10:8,G=(16&P[2])>>4,K=(8&P[2])>>3,H=(4&P[2])>>2,V=3&P[2];n+="."+F+"."+Nt(M)+O+"."+Nt(B)+"."+G+"."+K+H+V+"."+Nt(1)+"."+Nt(1)+"."+Nt(1)+".0"}return{codec:n,encrypted:a}}function Mt(t,e){for(var r=e+5;128&t[e++]&&e>1&63;return 39===r||40===r}return 6==(31&e)}function Vt(t,e,r,i){var n=Yt(t),a=0;a+=e;for(var s=0,o=0,l=0;a=n.length)break;s+=l=n[a++]}while(255===l);o=0;do{if(a>=n.length)break;o+=l=n[a++]}while(255===l);var u=n.length-a,h=a;if(ou){w.error("Malformed SEI payload. "+o+" is too small, only "+u+" bytes left to parse.");break}if(4===s){if(181===n[h++]){var d=Dt(n,h);if(h+=2,49===d){var c=It(n,h);if(h+=4,1195456820===c){var f=n[h++];if(3===f){var g=n[h++],v=64&g,m=v?2+3*(31&g):0,p=new Uint8Array(m);if(v){p[0]=g;for(var y=1;y16){for(var E=[],T=0;T<16;T++){var S=n[h++].toString(16);E.push(1==S.length?"0"+S:S),3!==T&&5!==T&&7!==T&&9!==T||E.push("-")}for(var L=o-16,A=new Uint8Array(L),R=0;R0?(a=new Uint8Array(4),e.length>0&&new DataView(a.buffer).setUint32(0,e.length,!1)):a=new Uint8Array;var l=new Uint8Array(4);return r&&r.byteLength>0&&new DataView(l.buffer).setUint32(0,r.byteLength,!1),function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i>24&255,o[1]=a>>16&255,o[2]=a>>8&255,o[3]=255&a,o.set(t,4),s=0,a=8;s>8*(15-r)&255;return e}(e);return new t(this.method,this.uri,"identity",this.keyFormatVersions,r)}var i=Y(this.uri);if(i)switch(this.keyFormat){case J:this.pssh=i,i.length>=22&&(this.keyId=i.subarray(i.length-22,i.length-6));break;case Q:var n=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=Wt(n,null,i);var a=new Uint16Array(i.buffer,i.byteOffset,i.byteLength/2),s=String.fromCharCode.apply(null,Array.from(a)),o=s.substring(s.indexOf("<"),s.length),l=(new DOMParser).parseFromString(o,"text/xml").getElementsByTagName("KID")[0];if(l){var u=l.childNodes[0]?l.childNodes[0].nodeValue:l.getAttribute("VALUE");if(u){var h=V(u).subarray(0,16);!function(t){var e=function(t,e,r){var i=t[e];t[e]=t[r],t[r]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(h),this.keyId=h}}break;default:var d=i.subarray(0,16);if(16!==d.length){var c=new Uint8Array(16);c.set(d,16-d.length),d=c}this.keyId=d}if(!this.keyId||16!==this.keyId.byteLength){var f=jt[this.uri];if(!f){var g=Object.keys(jt).length%Number.MAX_SAFE_INTEGER;f=new Uint8Array(16),new DataView(f.buffer,12,4).setUint32(0,g),jt[this.uri]=f}this.keyId=f}return this},t}(),Xt=/\{\$([a-zA-Z0-9-_]+)\}/g;function zt(t){return Xt.test(t)}function Qt(t,e,r){if(null!==t.variableList||t.hasVariableRefs)for(var i=r.length;i--;){var n=r[i],a=e[n];a&&(e[n]=Jt(t,a))}}function Jt(t,e){if(null!==t.variableList||t.hasVariableRefs){var r=t.variableList;return e.replace(Xt,(function(e){var i=e.substring(2,e.length-1),n=null==r?void 0:r[i];return void 0===n?(t.playlistParsingError||(t.playlistParsingError=new Error('Missing preceding EXT-X-DEFINE tag for Variable Reference: "'+i+'"')),e):n}))}return e}function $t(t,e,r){var i,n,a=t.variableList;if(a||(t.variableList=a={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{var s=new self.URL(r).searchParams;if(!s.has(i))throw new Error('"'+i+'" does not match any query parameter in URI: "'+r+'"');n=s.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error("EXT-X-DEFINE QUERYPARAM: "+e.message))}}else i=e.NAME,n=e.VALUE;i in a?t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE duplicate Variable Name declarations: "'+i+'"')):a[i]=n||""}function Zt(t,e,r){var i=e.IMPORT;if(r&&i in r){var n=t.variableList;n||(t.variableList=n={}),n[i]=r[i]}else t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "'+i+'"'))}function te(t){if(void 0===t&&(t=!0),"undefined"!=typeof self)return(t||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}var ee={audio:{a3ds:1,"ac-3":.95,"ac-4":1,alac:.9,alaw:1,dra1:1,"dts+":1,"dts-":1,dtsc:1,dtse:1,dtsh:1,"ec-3":.9,enca:1,fLaC:.9,flac:.9,FLAC:.9,g719:1,g726:1,m4ae:1,mha1:1,mha2:1,mhm1:1,mhm2:1,mlpa:1,mp4a:1,"raw ":1,Opus:1,opus:1,samr:1,sawb:1,sawp:1,sevc:1,sqcp:1,ssmv:1,twos:1,ulaw:1},video:{avc1:1,avc2:1,avc3:1,avc4:1,avcp:1,av01:.8,drac:1,dva1:1,dvav:1,dvh1:.7,dvhe:.7,encv:1,hev1:.75,hvc1:.75,mjp2:1,mp4v:1,mvc1:1,mvc2:1,mvc3:1,mvc4:1,resv:1,rv60:1,s263:1,svc1:1,svc2:1,"vc-1":1,vp08:1,vp09:.9},text:{stpp:1,wvtt:1}};function re(t,e,r){return void 0===r&&(r=!0),!t.split(",").some((function(t){return!ie(t,e,r)}))}function ie(t,e,r){var i;void 0===r&&(r=!0);var n=te(r);return null!=(i=null==n?void 0:n.isTypeSupported(ne(t,e)))&&i}function ne(t,e){return e+'/mp4;codecs="'+t+'"'}function ae(t){if(t){var e=t.substring(0,4);return ee.video[e]}return 2}function se(t){return t.split(",").reduce((function(t,e){var r=ee.video[e];return r?(2*r+t)/(t?3:2):(ee.audio[e]+t)/(t?2:1)}),0)}var oe={},le=/flac|opus/i;function ue(t,e){return void 0===e&&(e=!0),t.replace(le,(function(t){return function(t,e){if(void 0===e&&(e=!0),oe[t])return oe[t];for(var r={flac:["flac","fLaC","FLAC"],opus:["opus","Opus"]}[t],i=0;i0&&a.length0&&X.bool("CAN-SKIP-DATERANGES"),h.partHoldBack=X.optionalFloat("PART-HOLD-BACK",0),h.holdBack=X.optionalFloat("HOLD-BACK",0);break;case"PART-INF":var z=new x(I);h.partTarget=z.decimalFloatingPoint("PART-TARGET");break;case"PART":var Q=h.partList;Q||(Q=h.partList=[]);var J=g>0?Q[Q.length-1]:void 0,$=g++,Z=new x(I);Qt(h,Z,["BYTERANGE","URI"]);var tt=new K(Z,E,e,$,J);Q.push(tt),E.duration+=tt.duration;break;case"PRELOAD-HINT":var et=new x(I);Qt(h,et,["URI"]),h.preloadHint=et;break;case"RENDITION-REPORT":var rt=new x(I);Qt(h,rt,["URI"]),h.renditionReports=h.renditionReports||[],h.renditionReports.push(rt);break;default:w.warn("line parsed but not handled: "+s)}}}p&&!p.relurl?(d.pop(),v-=p.duration,h.partList&&(h.fragmentHint=p)):h.partList&&(Se(E,p),E.cc=m,h.fragmentHint=E,u&&Ae(E,u,h));var it=d.length,nt=d[0],at=d[it-1];if((v+=h.skippedSegments*h.targetduration)>0&&it&&at){h.averagetargetduration=v/it;var st=at.sn;h.endSN="initSegment"!==st?st:0,h.live||(at.endList=!0),nt&&(h.startCC=nt.cc)}else h.endSN=0,h.startCC=0;return h.fragmentHint&&(v+=h.fragmentHint.duration),h.totalduration=v,h.endCC=m,T>0&&function(t,e){for(var r=t[e],i=e;i--;){var n=t[i];if(!n)return;n.programDateTime=r.programDateTime-1e3*n.duration,r=n}}(d,T),h},t}();function pe(t,e,r){var i,n,a=new x(t);Qt(r,a,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);var s=null!=(i=a.METHOD)?i:"",o=a.URI,l=a.hexadecimalInteger("IV"),u=a.KEYFORMATVERSIONS,h=null!=(n=a.KEYFORMAT)?n:"identity";o&&a.IV&&!l&&w.error("Invalid IV: "+a.IV);var d=o?me.resolve(o,e):"",c=(u||"1").split("/").map(Number).filter(Number.isFinite);return new qt(s,d,h,c,l)}function ye(t){var e=new x(t).decimalFloatingPoint("TIME-OFFSET");return y(e)?e:null}function Ee(t,e){var r=(t||"").split(/[ ,]+/).filter((function(t){return t}));["video","audio","text"].forEach((function(t){var i=r.filter((function(e){return function(t,e){var r=ee[e];return!!r&&!!r[t.slice(0,4)]}(e,t)}));i.length&&(e[t+"Codec"]=i.join(","),r=r.filter((function(t){return-1===i.indexOf(t)})))})),e.unknownCodecs=r}function Te(t,e,r){var i=e[r];i&&(t[r]=i)}function Se(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),y(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function Le(t,e,r,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=r,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function Ae(t,e,r){t.levelkeys=e;var i=r.encryptedFragments;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((function(t){return e[t].isCommonEncryption}))||i.push(t)}var Re="manifest",ke="level",be="audioTrack",De="subtitleTrack",Ie="main",we="audio",Ce="subtitle";function _e(t){switch(t.type){case be:return we;case De:return Ce;default:return Ie}}function xe(t,e){var r=t.url;return void 0!==r&&0!==r.indexOf("data:")||(r=e.url),r}var Pe=function(){function t(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}var e=t.prototype;return e.startLoad=function(t){},e.stopLoad=function(){this.destroyInternalLoaders()},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_LOADING,this.onLevelLoading,this),t.on(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_LOADING,this.onLevelLoading,this),t.off(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.createInternalLoader=function(t){var e=this.hls.config,r=e.pLoader,i=e.loader,n=new(r||i)(e);return this.loaders[t.type]=n,n},e.getInternalLoader=function(t){return this.loaders[t.type]},e.resetInternalLoader=function(t){this.loaders[t]&&delete this.loaders[t]},e.destroyInternalLoaders=function(){for(var t in this.loaders){var e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}},e.destroy=function(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()},e.onManifestLoading=function(t,e){var r=e.url;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:Re,url:r,deliveryDirectives:null})},e.onLevelLoading=function(t,e){var r=e.id,i=e.level,n=e.pathwayId,a=e.url,s=e.deliveryDirectives;this.load({id:r,level:i,pathwayId:n,responseType:"text",type:ke,url:a,deliveryDirectives:s})},e.onAudioTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:be,url:n,deliveryDirectives:a})},e.onSubtitleTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:De,url:n,deliveryDirectives:a})},e.load=function(t){var e,r,i,n=this,a=this.hls.config,s=this.getInternalLoader(t);if(s){var l=s.context;if(l&&l.url===t.url&&l.level===t.level)return void w.trace("[playlist-loader]: playlist request ongoing");w.log("[playlist-loader]: aborting previous loader for type: "+t.type),s.abort()}if(r=t.type===Re?a.manifestLoadPolicy.default:o({},a.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),s=this.createInternalLoader(t),y(null==(e=t.deliveryDirectives)?void 0:e.part)&&(t.type===ke&&null!==t.level?i=this.hls.levels[t.level].details:t.type===be&&null!==t.id?i=this.hls.audioTracks[t.id].details:t.type===De&&null!==t.id&&(i=this.hls.subtitleTracks[t.id].details),i)){var u=i.partTarget,h=i.targetduration;if(u&&h){var d=1e3*Math.max(3*u,.8*h);r=o({},r,{maxTimeToFirstByteMs:Math.min(d,r.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(d,r.maxTimeToFirstByteMs)})}}var c=r.errorRetry||r.timeoutRetry||{},f={loadPolicy:r,timeout:r.maxLoadTimeMs,maxRetry:c.maxNumRetry||0,retryDelay:c.retryDelayMs||0,maxRetryDelay:c.maxRetryDelayMs||0},g={onSuccess:function(t,e,r,i){var a=n.getInternalLoader(r);n.resetInternalLoader(r.type);var s=t.data;0===s.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),me.isMediaPlaylist(s)?n.handleTrackOrLevelPlaylist(t,e,r,i||null,a):n.handleMasterPlaylist(t,e,r,i)):n.handleManifestParsingError(t,r,new Error("no EXTM3U delimiter"),i||null,e)},onError:function(t,e,r,i){n.handleNetworkError(e,r,!1,t,i)},onTimeout:function(t,e,r){n.handleNetworkError(e,r,!0,void 0,t)}};s.load(t,f,g)},e.handleMasterPlaylist=function(t,e,r,i){var n=this.hls,a=t.data,s=xe(t,r),o=me.parseMasterPlaylist(a,s);if(o.playlistParsingError)this.handleManifestParsingError(t,r,o.playlistParsingError,i,e);else{var l=o.contentSteering,u=o.levels,h=o.sessionData,d=o.sessionKeys,c=o.startTimeOffset,f=o.variableList;this.variableList=f;var g=me.parseMasterPlaylistMedia(a,s,o),v=g.AUDIO,m=void 0===v?[]:v,p=g.SUBTITLES,y=g["CLOSED-CAPTIONS"];m.length&&(m.some((function(t){return!t.url}))||!u[0].audioCodec||u[0].attrs.AUDIO||(w.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),m.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new x({}),bitrate:0,url:""}))),n.trigger(S.MANIFEST_LOADED,{levels:u,audioTracks:m,subtitles:p,captions:y,contentSteering:l,url:s,stats:e,networkDetails:i,sessionData:h,sessionKeys:d,startTimeOffset:c,variableList:f})}},e.handleTrackOrLevelPlaylist=function(t,e,r,i,n){var a=this.hls,s=r.id,o=r.level,l=r.type,u=xe(t,r),h=y(o)?o:y(s)?s:0,d=_e(r),c=me.parseLevelPlaylist(t.data,u,h,d,0,this.variableList);if(l===Re){var f={attrs:new x({}),bitrate:0,details:c,name:"",url:u};a.trigger(S.MANIFEST_LOADED,{levels:[f],audioTracks:[],url:u,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),r.levelDetails=c,this.handlePlaylistLoaded(c,t,e,r,i,n)},e.handleManifestParsingError=function(t,e,r,i,n){this.hls.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.MANIFEST_PARSING_ERROR,fatal:e.type===Re,url:t.url,err:r,error:r,reason:r.message,response:t,context:e,networkDetails:i,stats:n})},e.handleNetworkError=function(t,e,r,n,a){void 0===r&&(r=!1);var s="A network "+(r?"timeout":"error"+(n?" (status "+n.code+")":""))+" occurred while loading "+t.type;t.type===ke?s+=": "+t.level+" id: "+t.id:t.type!==be&&t.type!==De||(s+=" id: "+t.id+' group-id: "'+t.groupId+'"');var o=new Error(s);w.warn("[playlist-loader]: "+s);var l=A.UNKNOWN,u=!1,h=this.getInternalLoader(t);switch(t.type){case Re:l=r?A.MANIFEST_LOAD_TIMEOUT:A.MANIFEST_LOAD_ERROR,u=!0;break;case ke:l=r?A.LEVEL_LOAD_TIMEOUT:A.LEVEL_LOAD_ERROR,u=!1;break;case be:l=r?A.AUDIO_TRACK_LOAD_TIMEOUT:A.AUDIO_TRACK_LOAD_ERROR,u=!1;break;case De:l=r?A.SUBTITLE_TRACK_LOAD_TIMEOUT:A.SUBTITLE_LOAD_ERROR,u=!1}h&&this.resetInternalLoader(t.type);var d={type:L.NETWORK_ERROR,details:l,fatal:u,url:t.url,loader:h,context:t,error:o,networkDetails:e,stats:a};if(n){var c=(null==e?void 0:e.url)||t.url;d.response=i({url:c,data:void 0},n)}this.hls.trigger(S.ERROR,d)},e.handlePlaylistLoaded=function(t,e,r,i,n,a){var s=this.hls,o=i.type,l=i.level,u=i.id,h=i.groupId,d=i.deliveryDirectives,c=xe(e,i),f=_e(i),g="number"==typeof i.level&&f===Ie?l:void 0;if(t.fragments.length){t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));var v=t.playlistParsingError;if(v)s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_PARSING_ERROR,fatal:!1,url:c,error:v,reason:v.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r});else switch(t.live&&a&&(a.getCacheAge&&(t.ageHeader=a.getCacheAge()||0),a.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case Re:case ke:s.trigger(S.LEVEL_LOADED,{details:t,level:g||0,id:u||0,stats:r,networkDetails:n,deliveryDirectives:d});break;case be:s.trigger(S.AUDIO_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d});break;case De:s.trigger(S.SUBTITLE_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d})}}else{var m=new Error("No Segments found in Playlist");s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_EMPTY_ERROR,fatal:!1,url:c,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r})}},t}();function Fe(t,e){var r;try{r=new Event("addtrack")}catch(t){(r=document.createEvent("Event")).initEvent("addtrack",!1,!1)}r.track=t,e.dispatchEvent(r)}function Me(t,e){var r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error("addCue is failed for: "+e)}catch(r){w.debug("[texttrack-utils]: "+r);try{var i=new self.TextTrackCue(e.startTime,e.endTime,e.text);i.id=e.id,t.addCue(i)}catch(t){w.debug("[texttrack-utils]: Legacy TextTrackCue fallback failed: "+t)}}"disabled"===r&&(t.mode=r)}function Oe(t){var e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(var r=t.cues.length;r--;)t.removeCue(t.cues[r]);"disabled"===e&&(t.mode=e)}function Ne(t,e,r,i){var n=t.mode;if("disabled"===n&&(t.mode="hidden"),t.cues&&t.cues.length>0)for(var a=function(t,e,r){var i=[],n=function(t,e){if(et[r].endTime)return-1;for(var i=0,n=r;i<=n;){var a=Math.floor((n+i)/2);if(et[a].startTime&&i-1)for(var a=n,s=t.length;a=e&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(t.cues,e,r),s=0;sYe&&(d=Ye),d-h<=0&&(d=h+.25);for(var c=0;ce.startDate&&(!t||e.startDate.05&&this.forwardBufferLength>1){var l=Math.min(2,Math.max(1,a)),u=Math.round(2/(1+Math.exp(-.75*o-this.edgeStalled))*20)/20;t.playbackRate=Math.min(l,Math.max(1,u))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}}}}},e.estimateLiveEdge=function(){var t=this.levelDetails;return null===t?null:t.edge+t.age},e.computeLatency=function(){var t=this.estimateLiveEdge();return null===t?null:t-this.currentTime},s(t,[{key:"latency",get:function(){return this._latency||0}},{key:"maxLatency",get:function(){var t=this.config,e=this.levelDetails;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}},{key:"targetLatency",get:function(){var t=this.levelDetails;if(null===t)return null;var e=t.holdBack,r=t.partHoldBack,i=t.targetduration,n=this.config,a=n.liveSyncDuration,s=n.liveSyncDurationCount,o=n.lowLatencyMode,l=this.hls.userConfig,u=o&&r||e;(l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==a?a:s*i);var h=i;return u+Math.min(1*this.stallCount,h)}},{key:"liveSyncPosition",get:function(){var t=this.estimateLiveEdge(),e=this.targetLatency,r=this.levelDetails;if(null===t||null===e||null===r)return null;var i=r.edge,n=t-e-this.edgeStalled,a=i-r.totalduration,s=i-(this.config.lowLatencyMode&&r.partTarget||r.targetduration);return Math.min(Math.max(a,n),s)}},{key:"drift",get:function(){var t=this.levelDetails;return null===t?1:t.drift}},{key:"edgeStalled",get:function(){var t=this.levelDetails;if(null===t)return 0;var e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}},{key:"forwardBufferLength",get:function(){var t=this.media,e=this.levelDetails;if(!t||!e)return 0;var r=t.buffered.length;return(r?t.buffered.end(r-1):e.edge)-this.currentTime}}]),t}(),Xe=["NONE","TYPE-0","TYPE-1",null],ze=["SDR","PQ","HLG"],Qe="",Je="YES",$e="v2",Ze=function(){function t(t,e,r){this.msn=void 0,this.part=void 0,this.skip=void 0,this.msn=t,this.part=e,this.skip=r}return t.prototype.addDirectives=function(t){var e=new self.URL(t);return void 0!==this.msn&&e.searchParams.set("_HLS_msn",this.msn.toString()),void 0!==this.part&&e.searchParams.set("_HLS_part",this.part.toString()),this.skip&&e.searchParams.set("_HLS_skip",this.skip),e.href},t}(),tr=function(){function t(t){this._attrs=void 0,this.audioCodec=void 0,this.bitrate=void 0,this.codecSet=void 0,this.url=void 0,this.frameRate=void 0,this.height=void 0,this.id=void 0,this.name=void 0,this.videoCodec=void 0,this.width=void 0,this.details=void 0,this.fragmentError=0,this.loadError=0,this.loaded=void 0,this.realBitrate=0,this.supportedPromise=void 0,this.supportedResult=void 0,this._avgBitrate=0,this._audioGroups=void 0,this._subtitleGroups=void 0,this._urlId=0,this.url=[t.url],this._attrs=[t.attrs],this.bitrate=t.bitrate,t.details&&(this.details=t.details),this.id=t.id||0,this.name=t.name,this.width=t.width||0,this.height=t.height||0,this.frameRate=t.attrs.optionalFloat("FRAME-RATE",0),this._avgBitrate=t.attrs.decimalInteger("AVERAGE-BANDWIDTH"),this.audioCodec=t.audioCodec,this.videoCodec=t.videoCodec,this.codecSet=[t.videoCodec,t.audioCodec].filter((function(t){return!!t})).map((function(t){return t.substring(0,4)})).join(","),this.addGroupId("audio",t.attrs.AUDIO),this.addGroupId("text",t.attrs.SUBTITLES)}var e=t.prototype;return e.hasAudioGroup=function(t){return er(this._audioGroups,t)},e.hasSubtitleGroup=function(t){return er(this._subtitleGroups,t)},e.addGroupId=function(t,e){if(e)if("audio"===t){var r=this._audioGroups;r||(r=this._audioGroups=[]),-1===r.indexOf(e)&&r.push(e)}else if("text"===t){var i=this._subtitleGroups;i||(i=this._subtitleGroups=[]),-1===i.indexOf(e)&&i.push(e)}},e.addFallback=function(){},s(t,[{key:"maxBitrate",get:function(){return Math.max(this.realBitrate,this.bitrate)}},{key:"averageBitrate",get:function(){return this._avgBitrate||this.realBitrate||this.bitrate}},{key:"attrs",get:function(){return this._attrs[0]}},{key:"codecs",get:function(){return this.attrs.CODECS||""}},{key:"pathwayId",get:function(){return this.attrs["PATHWAY-ID"]||"."}},{key:"videoRange",get:function(){return this.attrs["VIDEO-RANGE"]||"SDR"}},{key:"score",get:function(){return this.attrs.optionalFloat("SCORE",0)}},{key:"uri",get:function(){return this.url[0]||""}},{key:"audioGroups",get:function(){return this._audioGroups}},{key:"subtitleGroups",get:function(){return this._subtitleGroups}},{key:"urlId",get:function(){return 0},set:function(t){}},{key:"audioGroupIds",get:function(){return this.audioGroups?[this.audioGroupId]:void 0}},{key:"textGroupIds",get:function(){return this.subtitleGroups?[this.textGroupId]:void 0}},{key:"audioGroupId",get:function(){var t;return null==(t=this.audioGroups)?void 0:t[0]}},{key:"textGroupId",get:function(){var t;return null==(t=this.subtitleGroups)?void 0:t[0]}}]),t}();function er(t,e){return!(!e||!t)&&-1!==t.indexOf(e)}function rr(t,e){var r=e.startPTS;if(y(r)){var i,n=0;e.sn>t.sn?(n=r-t.start,i=t):(n=t.start-r,i=e),i.duration!==n&&(i.duration=n)}else e.sn>t.sn?t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration:e.start=Math.max(t.start-e.duration,0)}function ir(t,e,r,i,n,a){i-r<=0&&(w.warn("Fragment should have a positive duration",e),i=r+e.duration,a=n+e.duration);var s=r,o=i,l=e.startPTS,u=e.endPTS;if(y(l)){var h=Math.abs(l-r);y(e.deltaPTS)?e.deltaPTS=Math.max(h,e.deltaPTS):e.deltaPTS=h,s=Math.max(r,l),r=Math.min(r,l),n=Math.min(n,e.startDTS),o=Math.min(i,u),i=Math.max(i,u),a=Math.max(a,e.endDTS)}var d=r-e.start;0!==e.start&&(e.start=r),e.duration=i-e.start,e.startPTS=r,e.maxStartPTS=s,e.startDTS=n,e.endPTS=i,e.minEndPTS=o,e.endDTS=a;var c,f=e.sn;if(!t||ft.endSN)return 0;var g=f-t.startSN,v=t.fragments;for(v[g]=e,c=g;c>0;c--)rr(v[c],v[c-1]);for(c=g;c=0;n--){var a=i[n].initSegment;if(a){r=a;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;var s,l,u,h,d,c=0;if(function(t,e,r){for(var i=e.skippedSegments,n=Math.max(t.startSN,e.startSN)-e.startSN,a=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,s=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments,u=n;u<=a;u++){var h=l[s+u],d=o[u];i&&!d&&u=i.length||sr(e,i[r].start)}function sr(t,e){if(e){for(var r=t.fragments,i=t.skippedSegments;i499)}(n)||!!r);return t.shouldRetry?t.shouldRetry(t,e,r,i,a):a}var vr=function(t,e){for(var r=0,i=t.length-1,n=null,a=null;r<=i;){var s=e(a=t[n=(r+i)/2|0]);if(s>0)r=n+1;else{if(!(s<0))return a;i=n-1}}return null};function mr(t,e,r,i){void 0===r&&(r=0),void 0===i&&(i=0);var n=null;if(t){n=e[t.sn-e[0].sn+1]||null;var a=t.endDTS-r;a>0&&a<15e-7&&(r+=15e-7)}else 0===r&&0===e[0].start&&(n=e[0]);if(n&&(!t||t.level===n.level)&&0===pr(r,i,n))return n;var s=vr(e,pr.bind(null,r,i));return!s||s===t&&n?n:s}function pr(t,e,r){if(void 0===t&&(t=0),void 0===e&&(e=0),r.start<=t&&r.start+r.duration>t)return 0;var i=Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return r.start+r.duration-i<=t?1:r.start-i>t&&r.start?-1:0}function yr(t,e,r){var i=1e3*Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>t}var Er=0,Tr=2,Sr=3,Lr=5,Ar=0,Rr=1,kr=2,br=function(){function t(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=w.log.bind(w,"[info]:"),this.warn=w.warn.bind(w,"[warning]:"),this.error=w.error.bind(w,"[error]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.ERROR,this.onError,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.ERROR,this.onError,this),t.off(S.ERROR,this.onErrorOut,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this))},e.destroy=function(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}},e.startLoad=function(t){},e.stopLoad=function(){this.playlistError=0},e.getVariantLevelIndex=function(t){return(null==t?void 0:t.type)===Ie?t.level:this.hls.loadLevel},e.onManifestLoading=function(){this.playlistError=0,this.penalizedRenditions={}},e.onLevelUpdated=function(){this.playlistError=0},e.onError=function(t,e){var r,i;if(!e.fatal){var n=this.hls,a=e.context;switch(e.details){case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case A.FRAG_PARSING_ERROR:if(null!=(r=e.frag)&&r.gap)return void(e.errorAction={action:Er,flags:Ar});case A.FRAG_GAP:case A.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=Tr);case A.LEVEL_EMPTY_ERROR:case A.LEVEL_PARSING_ERROR:var s,o,l=e.parent===Ie?e.level:n.loadLevel;return void(e.details===A.LEVEL_EMPTY_ERROR&&null!=(s=e.context)&&null!=(o=s.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,l):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,l)));case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==a?void 0:a.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,a.level)));case A.AUDIO_TRACK_LOAD_ERROR:case A.AUDIO_TRACK_LOAD_TIMEOUT:case A.SUBTITLE_LOAD_ERROR:case A.SUBTITLE_TRACK_LOAD_TIMEOUT:if(a){var u=n.levels[n.loadLevel];if(u&&(a.type===be&&u.hasAudioGroup(a.groupId)||a.type===De&&u.hasSubtitleGroup(a.groupId)))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.loadLevel),e.errorAction.action=Tr,void(e.errorAction.flags=Rr)}return;case A.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:var h=n.levels[n.loadLevel],d=null==h?void 0:h.attrs["HDCP-LEVEL"];return void(d?e.errorAction={action:Tr,flags:kr,hdcpLevel:d}:this.keySystemError(e));case A.BUFFER_ADD_CODEC_ERROR:case A.REMUX_ALLOC_ERROR:case A.BUFFER_APPEND_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:n.loadLevel));case A.INTERNAL_EXCEPTION:case A.BUFFER_APPENDING_ERROR:case A.BUFFER_FULL_ERROR:case A.LEVEL_SWITCH_ERROR:case A.BUFFER_STALLED_ERROR:case A.BUFFER_SEEK_OVER_HOLE:case A.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:Er,flags:Ar})}e.type===L.KEY_SYSTEM_ERROR&&this.keySystemError(e)}},e.keySystemError=function(t){var e=this.getVariantLevelIndex(t.frag);t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,e)},e.getPlaylistRetryOrSwitchAction=function(t,e){var r=dr(this.hls.config.playlistLoadPolicy,t),i=this.playlistError++;if(gr(r,i,hr(t),t.response))return{action:Lr,flags:Ar,retryConfig:r,retryCount:i};var n=this.getLevelSwitchAction(t,e);return r&&(n.retryConfig=r,n.retryCount=i),n},e.getFragRetryOrSwitchAction=function(t){var e=this.hls,r=this.getVariantLevelIndex(t.frag),i=e.levels[r],n=e.config,a=n.fragLoadPolicy,s=n.keyLoadPolicy,o=dr(t.details.startsWith("key")?s:a,t),l=e.levels.reduce((function(t,e){return t+e.fragmentError}),0);if(i&&(t.details!==A.FRAG_GAP&&i.fragmentError++,gr(o,l,hr(t),t.response)))return{action:Lr,flags:Ar,retryConfig:o,retryCount:l};var u=this.getLevelSwitchAction(t,r);return o&&(u.retryConfig=o,u.retryCount=l),u},e.getLevelSwitchAction=function(t,e){var r=this.hls;null==e&&(e=r.loadLevel);var i=this.hls.levels[e];if(i){var n,a,s=t.details;i.loadError++,s===A.BUFFER_APPEND_ERROR&&i.fragmentError++;var o=-1,l=r.levels,u=r.loadLevel,h=r.minAutoLevel,d=r.maxAutoLevel;r.autoLevelEnabled||(r.loadLevel=-1);for(var c,f=null==(n=t.frag)?void 0:n.type,g=(f===we&&s===A.FRAG_PARSING_ERROR||"audio"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR))&&l.some((function(t){var e=t.audioCodec;return i.audioCodec!==e})),v="video"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR)&&l.some((function(t){var e=t.codecSet,r=t.audioCodec;return i.codecSet!==e&&i.audioCodec===r})),m=null!=(a=t.context)?a:{},p=m.type,y=m.groupId,E=function(){var e=(T+u)%l.length;if(e!==u&&e>=h&&e<=d&&0===l[e].loadError){var r,n,a=l[e];if(s===A.FRAG_GAP&&t.frag){var c=l[e].details;if(c){var m=mr(t.frag,c.fragments,t.frag.start);if(null!=m&&m.gap)return 0}}else{if(p===be&&a.hasAudioGroup(y)||p===De&&a.hasSubtitleGroup(y))return 0;if(f===we&&null!=(r=i.audioGroups)&&r.some((function(t){return a.hasAudioGroup(t)}))||f===Ce&&null!=(n=i.subtitleGroups)&&n.some((function(t){return a.hasSubtitleGroup(t)}))||g&&i.audioCodec===a.audioCodec||!g&&i.audioCodec!==a.audioCodec||v&&i.codecSet===a.codecSet)return 0}return o=e,1}},T=l.length;T--&&(0===(c=E())||1!==c););if(o>-1&&r.loadLevel!==o)return t.levelRetry=!0,this.playlistError=0,{action:Tr,flags:Ar,nextAutoLevel:o}}return{action:Tr,flags:Rr}},e.onErrorOut=function(t,e){var r;switch(null==(r=e.errorAction)?void 0:r.action){case Er:break;case Tr:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===A.FRAG_GAP?/MediaSource readyState: ended/.test(e.error.message)&&(this.warn('MediaSource ended after "'+e.sourceBufferName+'" sourceBuffer append error. Attempting to recover from media error.'),this.hls.recoverMediaError()):e.fatal=!0}e.fatal&&this.hls.stopLoad()},e.sendAlternateToPenaltyBox=function(t){var e=this.hls,r=t.errorAction;if(r){var i=r.flags,n=r.hdcpLevel,a=r.nextAutoLevel;switch(i){case Ar:this.switchLevel(t,a);break;case kr:n&&(e.maxHdcpLevel=Xe[Xe.indexOf(n)-1],r.resolved=!0),this.warn('Restricting playback to HDCP-LEVEL of "'+e.maxHdcpLevel+'" or lower')}r.resolved||this.switchLevel(t,a)}},e.switchLevel=function(t,e){void 0!==e&&t.errorAction&&(this.warn("switching to level "+e+" after "+t.details),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)},t}(),Dr=function(){function t(t,e){this.hls=void 0,this.timer=-1,this.requestScheduled=-1,this.canLoad=!1,this.log=void 0,this.warn=void 0,this.log=w.log.bind(w,e+":"),this.warn=w.warn.bind(w,e+":"),this.hls=t}var e=t.prototype;return e.destroy=function(){this.clearTimer(),this.hls=this.log=this.warn=null},e.clearTimer=function(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)},e.startLoad=function(){this.canLoad=!0,this.requestScheduled=-1,this.loadPlaylist()},e.stopLoad=function(){this.canLoad=!1,this.clearTimer()},e.switchParams=function(t,e){var r=null==e?void 0:e.renditionReports;if(r){for(var i=-1,n=0;n=0&&h>e.partTarget&&(u+=1)}return new Ze(l,u>=0?u:void 0,Qe)}}},e.loadPlaylist=function(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())},e.shouldLoadPlaylist=function(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)},e.shouldReloadPlaylist=function(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)},e.playlistLoaded=function(t,e,r){var i=this,n=e.details,a=e.stats,s=self.performance.now(),o=a.loading.first?Math.max(0,s-a.loading.first):0;if(n.advancedDateTime=Date.now()-o,n.live||null!=r&&r.live){if(n.reloaded(r),r&&this.log("live playlist "+t+" "+(n.advanced?"REFRESHED "+n.lastPartSn+"-"+n.lastPartIndex:n.updated?"UPDATED":"MISSED")),r&&n.fragments.length>0&&nr(r,n),!this.canLoad||!n.live)return;var l,u=void 0,h=void 0;if(n.canBlockReload&&n.endSN&&n.advanced){var d=this.hls.config.lowLatencyMode,c=n.lastPartSn,f=n.endSN,g=n.lastPartIndex,v=c===f;-1!==g?(u=v?f+1:c,h=v?d?0:g:g+1):u=f+1;var m=n.age,p=m+n.ageHeader,y=Math.min(p-n.partTarget,1.5*n.targetduration);if(y>0){if(r&&y>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+y+" with playlist age: "+n.age),y=0;else{var E=Math.floor(y/n.targetduration);u+=E,void 0!==h&&(h+=Math.round(y%n.targetduration/n.partTarget)),this.log("CDN Tune-in age: "+n.ageHeader+"s last advanced "+m.toFixed(2)+"s goal: "+y+" skip sn "+E+" to part "+h)}n.tuneInGoal=y}if(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h),d||!v)return void this.loadPlaylist(l)}else(n.canBlockReload||n.canSkipUntil)&&(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h));var T=this.hls.mainForwardBufferInfo,S=T?T.end-T.len:0,L=function(t,e){void 0===e&&(e=1/0);var r=1e3*t.targetduration;if(t.updated){var i=t.fragments;if(i.length&&4*r>e){var n=1e3*i[i.length-1].duration;nthis.requestScheduled+L&&(this.requestScheduled=a.loading.start),void 0!==u&&n.canBlockReload?this.requestScheduled=a.loading.first+L-(1e3*n.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+L=u.maxNumRetry)return!1;if(i&&null!=(d=t.context)&&d.deliveryDirectives)this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" without delivery-directives'),this.loadPlaylist();else{var c=cr(u,l);this.timer=self.setTimeout((function(){return e.loadPlaylist()}),c),this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" in '+c+"ms")}t.levelRetry=!0,n.resolved=!0}return h},t}(),Ir=function(){function t(t,e,r){void 0===e&&(e=0),void 0===r&&(r=0),this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=r}var e=t.prototype;return e.sample=function(t,e){var r=Math.pow(this.alpha_,t);this.estimate_=e*(1-r)+r*this.estimate_,this.totalWeight_+=t},e.getTotalWeight=function(){return this.totalWeight_},e.getEstimate=function(){if(this.alpha_){var t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_},t}(),wr=function(){function t(t,e,r,i){void 0===i&&(i=100),this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=r,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new Ir(t),this.fast_=new Ir(e),this.defaultTTFB_=i,this.ttfb_=new Ir(t)}var e=t.prototype;return e.update=function(t,e){var r=this.slow_,i=this.fast_,n=this.ttfb_;r.halfLife!==t&&(this.slow_=new Ir(t,r.getEstimate(),r.getTotalWeight())),i.halfLife!==e&&(this.fast_=new Ir(e,i.getEstimate(),i.getTotalWeight())),n.halfLife!==t&&(this.ttfb_=new Ir(t,n.getEstimate(),n.getTotalWeight()))},e.sample=function(t,e){var r=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/r;this.fast_.sample(r,i),this.slow_.sample(r,i)},e.sampleTTFB=function(t){var e=t/1e3,r=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(r,Math.max(t,5))},e.canEstimate=function(){return this.fast_.getTotalWeight()>=this.minWeight_},e.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},e.getEstimateTTFB=function(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_},e.destroy=function(){},t}(),Cr={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]},_r={};function xr(t,e,r){var n=t.videoCodec,a=t.audioCodec;if(!n||!a||!r)return Promise.resolve(Cr);var s={width:t.width,height:t.height,bitrate:Math.ceil(Math.max(.9*t.bitrate,t.averageBitrate)),framerate:t.frameRate||30},o=t.videoRange;"SDR"!==o&&(s.transferFunction=o.toLowerCase());var l=n.split(",").map((function(t){return{type:"media-source",video:i(i({},s),{},{contentType:ne(t,"video")})}}));return a&&t.audioGroups&&t.audioGroups.forEach((function(t){var r;t&&(null==(r=e.groups[t])||r.tracks.forEach((function(e){if(e.groupId===t){var r=e.channels||"",i=parseFloat(r);y(i)&&i>2&&l.push.apply(l,a.split(",").map((function(t){return{type:"media-source",audio:{contentType:ne(t,"audio"),channels:""+i}}})))}})))})),Promise.all(l.map((function(t){var e=function(t){var e=t.audio,r=t.video,i=r||e;if(i){var n=i.contentType.split('"')[1];if(r)return"r"+r.height+"x"+r.width+"f"+Math.ceil(r.framerate)+(r.transferFunction||"sd")+"_"+n+"_"+Math.ceil(r.bitrate/1e5);if(e)return"c"+e.channels+(e.spatialRendering?"s":"n")+"_"+n}return""}(t);return _r[e]||(_r[e]=r.decodingInfo(t))}))).then((function(t){return{supported:!t.some((function(t){return!t.supported})),configurations:l,decodingInfoResults:t}})).catch((function(t){return{supported:!1,configurations:l,decodingInfoResults:[],error:t}}))}function Pr(t,e){var r=!1,i=[];return t&&(r="SDR"!==t,i=[t]),e&&(i=e.allowedVideoRanges||ze.slice(0),i=(r=void 0!==e.preferHDR?e.preferHDR:function(){if("function"==typeof matchMedia){var t=matchMedia("(dynamic-range: high)"),e=matchMedia("bad query");if(t.media!==e.media)return!0===t.matches}return!1}())?i.filter((function(t){return"SDR"!==t})):["SDR"]),{preferHDR:r,allowedVideoRanges:i}}function Fr(t,e){w.log('[abr] start candidates with "'+t+'" ignored because '+e)}function Mr(t,e,r){if("attrs"in t){var i=e.indexOf(t);if(-1!==i)return i}for(var n=0;n-1,p=e.getBwEstimate(),E=i.levels,T=E[t.level],L=o.total||Math.max(o.loaded,Math.round(l*T.maxBitrate/8)),A=m?u-v:u;A<1&&m&&(A=Math.min(u,8*o.loaded/p));var R=m?1e3*o.loaded/A:0,k=R?(L-o.loaded)/R:8*L/p+c/1e3;if(!(k<=g)){var b,D=R?8*R:p,I=Number.POSITIVE_INFINITY;for(b=t.level-1;b>h;b--){var C=E[b].maxBitrate;if((I=e.getTimeToLoadFrag(c/1e3,D,l*C,!E[b].details))=k||I>10*l)){i.nextLoadLevel=i.nextAutoLevel=b,m?e.bwEstimator.sample(u-Math.min(c,v),o.loaded):e.bwEstimator.sampleTTFB(u);var _=E[b].bitrate;e.getBwEstimate()*e.hls.config.abrBandWidthUpFactor>_&&e.resetEstimator(_),e.clearTimer(),w.warn("[abr] Fragment "+t.sn+(r?" part "+r.index:"")+" of level "+t.level+" is loading too slowly;\n Time to underbuffer: "+g.toFixed(3)+" s\n Estimated load time for current fragment: "+k.toFixed(3)+" s\n Estimated load time for down switch fragment: "+I.toFixed(3)+" s\n TTFB estimate: "+(0|v)+" ms\n Current BW estimate: "+(y(p)?0|p:"Unknown")+" bps\n New BW estimate: "+(0|e.getBwEstimate())+" bps\n Switching to level "+b+" @ "+(0|_)+" bps"),i.trigger(S.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:r,stats:o})}}}}}}},this.hls=t,this.bwEstimator=this.initEstimator(),this.registerListeners()}var e=t.prototype;return e.resetEstimator=function(t){t&&(w.log("setting initial bwe to "+t),this.hls.config.abrEwmaDefaultEstimate=t),this.firstSelection=-1,this.bwEstimator=this.initEstimator()},e.initEstimator=function(){var t=this.hls.config;return new wr(t.abrEwmaSlowVoD,t.abrEwmaFastVoD,t.abrEwmaDefaultEstimate)},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.off(S.ERROR,this.onError,this))},e.destroy=function(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=null,this.fragCurrent=this.partCurrent=null},e.onManifestLoading=function(t,e){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()},e.onLevelsUpdated=function(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null},e.onMaxAutoLevelUpdated=function(){this.firstSelection=-1,this.nextAutoLevelKey=""},e.onFragLoading=function(t,e){var r,i=e.frag;this.ignoreFragment(i)||(i.bitrateTest||(this.fragCurrent=i,this.partCurrent=null!=(r=e.part)?r:null),this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100))},e.onLevelSwitching=function(t,e){this.clearTimer()},e.onError=function(t,e){if(!e.fatal)switch(e.details){case A.BUFFER_ADD_CODEC_ERROR:case A.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case A.FRAG_LOAD_TIMEOUT:var r=e.frag,i=this.fragCurrent,n=this.partCurrent;if(r&&i&&r.sn===i.sn&&r.level===i.level){var a=performance.now(),s=n?n.stats:r.stats,o=a-s.loading.start,l=s.loading.first?s.loading.first-s.loading.start:-1;if(s.loaded&&l>-1){var u=this.bwEstimator.getEstimateTTFB();this.bwEstimator.sample(o-Math.min(u,l),s.loaded)}else this.bwEstimator.sampleTTFB(o)}}},e.getTimeToLoadFrag=function(t,e,r,i){return t+r/e+(i?this.lastLevelLoadSec:0)},e.onLevelLoaded=function(t,e){var r=this.hls.config,i=e.stats.loading,n=i.end-i.start;y(n)&&(this.lastLevelLoadSec=n/1e3),e.details.live?this.bwEstimator.update(r.abrEwmaSlowLive,r.abrEwmaFastLive):this.bwEstimator.update(r.abrEwmaSlowVoD,r.abrEwmaFastVoD)},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part,n=i?i.stats:r.stats;if(r.type===Ie&&this.bwEstimator.sampleTTFB(n.loading.first-n.loading.start),!this.ignoreFragment(r)){if(this.clearTimer(),r.level===this._nextAutoLevel&&(this._nextAutoLevel=-1),this.firstSelection=-1,this.hls.config.abrMaxWithRealBitrate){var a=i?i.duration:r.duration,s=this.hls.levels[r.level],o=(s.loaded?s.loaded.bytes:0)+n.loaded,l=(s.loaded?s.loaded.duration:0)+a;s.loaded={bytes:o,duration:l},s.realBitrate=Math.round(8*o/l)}if(r.bitrateTest){var u={stats:n,frag:r,part:i,id:r.type};this.onFragBuffered(S.FRAG_BUFFERED,u),r.bitrateTest=!1}else this.lastLoadedFragLevel=r.level}},e.onFragBuffered=function(t,e){var r=e.frag,i=e.part,n=null!=i&&i.stats.loaded?i.stats:r.stats;if(!n.aborted&&!this.ignoreFragment(r)){var a=n.parsing.end-n.loading.start-Math.min(n.loading.first-n.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(a,n.loaded),n.bwEstimate=this.getBwEstimate(),r.bitrateTest?this.bitrateTestDelay=a/1e3:this.bitrateTestDelay=0}},e.ignoreFragment=function(t){return t.type!==Ie||"initSegment"===t.sn},e.clearTimer=function(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)},e.getAutoLevelKey=function(){var t;return this.getBwEstimate()+"_"+(null==(t=this.hls.mainForwardBufferInfo)?void 0:t.len)},e.getNextABRAutoLevel=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.maxAutoLevel,n=r.config,a=r.minAutoLevel,s=r.media,o=e?e.duration:t?t.duration:0,l=s&&0!==s.playbackRate?Math.abs(s.playbackRate):1,u=this.getBwEstimate(),h=r.mainForwardBufferInfo,d=(h?h.len:0)/l,c=n.abrBandWidthFactor,f=n.abrBandWidthUpFactor;if(d){var g=this.findBestLevel(u,a,i,d,0,c,f);if(g>=0)return g}var v=o?Math.min(o,n.maxStarvationDelay):n.maxStarvationDelay;if(!d){var m=this.bitrateTestDelay;m&&(v=(o?Math.min(o,n.maxLoadingDelay):n.maxLoadingDelay)-m,w.info("[abr] bitrate test took "+Math.round(1e3*m)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*v)+" ms"),c=f=1)}var p=this.findBestLevel(u,a,i,d,v,c,f);if(w.info("[abr] "+(d?"rebuffering expected":"buffer is empty")+", optimal quality level "+p),p>-1)return p;var y=r.levels[a],E=r.levels[r.loadLevel];return(null==y?void 0:y.bitrate)<(null==E?void 0:E.bitrate)?a:r.loadLevel},e.getBwEstimate=function(){return this.bwEstimator.canEstimate()?this.bwEstimator.getEstimate():this.hls.config.abrEwmaDefaultEstimate},e.findBestLevel=function(t,e,r,i,n,a,s){var o,l=this,u=i+n,h=this.lastLoadedFragLevel,d=-1===h?this.hls.firstLevel:h,c=this.fragCurrent,f=this.partCurrent,g=this.hls,v=g.levels,m=g.allAudioTracks,p=g.loadLevel,E=g.config;if(1===v.length)return 0;var T,S=v[d],L=!(null==S||null==(o=S.details)||!o.live),A=-1===p||-1===h,R="SDR",k=(null==S?void 0:S.frameRate)||0,b=E.audioPreference,D=E.videoPreference,I=this.audioTracksByGroup||(this.audioTracksByGroup=function(t){return t.reduce((function(t,e){var r=t.groups[e.groupId];r||(r=t.groups[e.groupId]={tracks:[],channels:{2:0},hasDefault:!1,hasAutoSelect:!1}),r.tracks.push(e);var i=e.channels||"2";return r.channels[i]=(r.channels[i]||0)+1,r.hasDefault=r.hasDefault||e.default,r.hasAutoSelect=r.hasAutoSelect||e.autoselect,r.hasDefault&&(t.hasDefaultAudio=!0),r.hasAutoSelect&&(t.hasAutoSelectAudio=!0),t}),{hasDefaultAudio:!1,hasAutoSelectAudio:!1,groups:{}})}(m));if(A){if(-1!==this.firstSelection)return this.firstSelection;var C=this.codecTiers||(this.codecTiers=function(t,e,r,i){return t.slice(r,i+1).reduce((function(t,r){if(!r.codecSet)return t;var i=r.audioGroups,n=t[r.codecSet];n||(t[r.codecSet]=n={minBitrate:1/0,minHeight:1/0,minFramerate:1/0,maxScore:0,videoRanges:{SDR:0},channels:{2:0},hasDefaultAudio:!i,fragmentError:0}),n.minBitrate=Math.min(n.minBitrate,r.bitrate);var a=Math.min(r.height,r.width);return n.minHeight=Math.min(n.minHeight,a),n.minFramerate=Math.min(n.minFramerate,r.frameRate),n.maxScore=Math.max(n.maxScore,r.score),n.fragmentError+=r.fragmentError,n.videoRanges[r.videoRange]=(n.videoRanges[r.videoRange]||0)+1,i&&i.forEach((function(t){if(t){var r=e.groups[t];n.hasDefaultAudio=n.hasDefaultAudio||e.hasDefaultAudio?r.hasDefault:r.hasAutoSelect||!e.hasDefaultAudio&&!e.hasAutoSelectAudio,Object.keys(r.channels).forEach((function(t){n.channels[t]=(n.channels[t]||0)+r.channels[t]}))}})),t}),{})}(v,I,e,r)),_=function(t,e,r,i,n){for(var a=Object.keys(t),s=null==i?void 0:i.channels,o=null==i?void 0:i.audioCodec,l=s&&2===parseInt(s),u=!0,h=!1,d=1/0,c=1/0,f=1/0,g=0,v=[],m=Pr(e,n),p=m.preferHDR,E=m.allowedVideoRanges,T=function(){var e=t[a[S]];u=e.channels[2]>0,d=Math.min(d,e.minHeight),c=Math.min(c,e.minFramerate),f=Math.min(f,e.minBitrate);var r=E.filter((function(t){return e.videoRanges[t]>0}));r.length>0&&(h=!0,v=r)},S=a.length;S--;)T();d=y(d)?d:0,c=y(c)?c:0;var L=Math.max(1080,d),A=Math.max(30,c);return f=y(f)?f:r,r=Math.max(f,r),h||(e=void 0,v=[]),{codecSet:a.reduce((function(e,i){var n=t[i];if(i===e)return e;if(n.minBitrate>r)return Fr(i,"min bitrate of "+n.minBitrate+" > current estimate of "+r),e;if(!n.hasDefaultAudio)return Fr(i,"no renditions with default or auto-select sound found"),e;if(o&&i.indexOf(o.substring(0,4))%5!=0)return Fr(i,'audio codec preference "'+o+'" not found'),e;if(s&&!l){if(!n.channels[s])return Fr(i,"no renditions with "+s+" channel sound found (channels options: "+Object.keys(n.channels)+")"),e}else if((!o||l)&&u&&0===n.channels[2])return Fr(i,"no renditions with stereo sound found"),e;return n.minHeight>L?(Fr(i,"min resolution of "+n.minHeight+" > maximum of "+L),e):n.minFramerate>A?(Fr(i,"min framerate of "+n.minFramerate+" > maximum of "+A),e):v.some((function(t){return n.videoRanges[t]>0}))?n.maxScore=se(e)||n.fragmentError>t[e].fragmentError)?e:(g=n.maxScore,i):(Fr(i,"no variants with VIDEO-RANGE of "+JSON.stringify(v)+" found"),e)}),void 0),videoRanges:v,preferHDR:p,minFramerate:c,minBitrate:f}}(C,R,t,b,D),x=_.codecSet,P=_.videoRanges,F=_.minFramerate,M=_.minBitrate,O=_.preferHDR;T=x,R=O?P[P.length-1]:P[0],k=F,t=Math.max(t,M),w.log("[abr] picked start tier "+JSON.stringify(_))}else T=null==S?void 0:S.codecSet,R=null==S?void 0:S.videoRange;for(var N,U=f?f.duration:c?c.duration:0,B=this.bwEstimator.getEstimateTTFB()/1e3,G=[],K=function(){var e,o,c=v[H],g=H>d;if(!c)return 0;if(E.useMediaCapabilities&&!c.supportedResult&&!c.supportedPromise){var m=navigator.mediaCapabilities;"function"==typeof(null==m?void 0:m.decodingInfo)&&function(t,e,r,i,n,a){var s=t.audioCodec?t.audioGroups:null,o=null==a?void 0:a.audioCodec,l=null==a?void 0:a.channels,u=l?parseInt(l):o?1/0:2,h=null;if(null!=s&&s.length)try{h=1===s.length&&s[0]?e.groups[s[0]].channels:s.reduce((function(t,r){if(r){var i=e.groups[r];if(!i)throw new Error("Audio track group "+r+" not found");Object.keys(i.channels).forEach((function(e){t[e]=(t[e]||0)+i.channels[e]}))}return t}),{2:0})}catch(t){return!0}return void 0!==t.videoCodec&&(t.width>1920&&t.height>1088||t.height>1920&&t.width>1088||t.frameRate>Math.max(i,30)||"SDR"!==t.videoRange&&t.videoRange!==r||t.bitrate>Math.max(n,8e6))||!!h&&y(u)&&Object.keys(h).some((function(t){return parseInt(t)>u}))}(c,I,R,k,t,b)?(c.supportedPromise=xr(c,I,m),c.supportedPromise.then((function(t){c.supportedResult=t;var e=l.hls.levels,r=e.indexOf(c);t.error?w.warn('[abr] MediaCapabilities decodingInfo error: "'+t.error+'" for level '+r+" "+JSON.stringify(t)):t.supported||(w.warn("[abr] Unsupported MediaCapabilities decodingInfo result for level "+r+" "+JSON.stringify(t)),r>-1&&e.length>1&&(w.log("[abr] Removing unsupported level "+r),l.hls.removeLevel(r)))}))):c.supportedResult=Cr}if(T&&c.codecSet!==T||R&&c.videoRange!==R||g&&k>c.frameRate||!g&&k>0&&k=2*U&&0===n?v[H].averageBitrate:v[H].maxBitrate,P=l.getTimeToLoadFrag(B,D,x*_,void 0===C);if(D>=x&&(H===h||0===c.loadError&&0===c.fragmentError)&&(P<=B||!y(P)||L&&!l.bitrateTestDelay||P"+H+" adjustedbw("+Math.round(D)+")-bitrate="+Math.round(D-x)+" ttfb:"+B.toFixed(1)+" avgDuration:"+_.toFixed(1)+" maxFetchDuration:"+u.toFixed(1)+" fetchDuration:"+P.toFixed(1)+" firstSelection:"+A+" codecSet:"+T+" videoRange:"+R+" hls.loadLevel:"+p)),A&&(l.firstSelection=H),{v:H}}},H=r;H>=e;H--)if(0!==(N=K())&&N)return N.v;return-1},s(t,[{key:"firstAutoLevel",get:function(){var t=this.hls,e=t.maxAutoLevel,r=t.minAutoLevel,i=this.getBwEstimate(),n=this.hls.config.maxStarvationDelay,a=this.findBestLevel(i,r,e,0,n,1,1);if(a>-1)return a;var s=this.hls.firstLevel,o=Math.min(Math.max(s,r),e);return w.warn("[abr] Could not find best starting auto level. Defaulting to first in playlist "+s+" clamped to "+o),o}},{key:"forcedAutoLevel",get:function(){return this.nextAutoLevelKey?-1:this._nextAutoLevel}},{key:"nextAutoLevel",get:function(){var t=this.forcedAutoLevel,e=this.bwEstimator.canEstimate(),r=this.lastLoadedFragLevel>-1;if(!(-1===t||e&&r&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return t;var i=e&&r?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==t){var n=this.hls.levels;if(n.length>Math.max(t,i)&&n[t].loadError<=n[i].loadError)return t}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i},set:function(t){var e=Math.max(this.hls.minAutoLevel,t);this._nextAutoLevel!=e&&(this.nextAutoLevelKey="",this._nextAutoLevel=e)}}]),t}(),Gr=function(){function t(){this._boundTick=void 0,this._tickTimer=null,this._tickInterval=null,this._tickCallCount=0,this._boundTick=this.tick.bind(this)}var e=t.prototype;return e.destroy=function(){this.onHandlerDestroying(),this.onHandlerDestroyed()},e.onHandlerDestroying=function(){this.clearNextTick(),this.clearInterval()},e.onHandlerDestroyed=function(){},e.hasInterval=function(){return!!this._tickInterval},e.hasNextTick=function(){return!!this._tickTimer},e.setInterval=function(t){return!this._tickInterval&&(this._tickCallCount=0,this._tickInterval=self.setInterval(this._boundTick,t),!0)},e.clearInterval=function(){return!!this._tickInterval&&(self.clearInterval(this._tickInterval),this._tickInterval=null,!0)},e.clearNextTick=function(){return!!this._tickTimer&&(self.clearTimeout(this._tickTimer),this._tickTimer=null,!0)},e.tick=function(){this._tickCallCount++,1===this._tickCallCount&&(this.doTick(),this._tickCallCount>1&&this.tickImmediate(),this._tickCallCount=0)},e.tickImmediate=function(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)},e.doTick=function(){},t}(),Kr="NOT_LOADED",Hr="APPENDING",Vr="PARTIAL",Yr="OK",Wr=function(){function t(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){var t=this.hls;t.on(S.BUFFER_APPENDED,this.onBufferAppended,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this)},e._unregisterListeners=function(){var t=this.hls;t.off(S.BUFFER_APPENDED,this.onBufferAppended,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this)},e.destroy=function(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null},e.getAppendedFrag=function(t,e){var r=this.activePartLists[e];if(r)for(var i=r.length;i--;){var n=r[i];if(!n)break;var a=n.end;if(n.start<=t&&null!==a&&t<=a)return n}return this.getBufferedFrag(t,e)},e.getBufferedFrag=function(t,e){for(var r=this.fragments,i=Object.keys(r),n=i.length;n--;){var a=r[i[n]];if((null==a?void 0:a.body.type)===e&&a.buffered){var s=a.body;if(s.start<=t&&t<=s.end)return s}}return null},e.detectEvictedFragments=function(t,e,r,i){var n=this;this.timeRanges&&(this.timeRanges[t]=e);var a=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((function(i){var s=n.fragments[i];if(s&&!(a>=s.body.sn))if(s.buffered||s.loaded){var o=s.range[t];o&&o.time.some((function(t){var r=!n.isTimeBuffered(t.startPTS,t.endPTS,e);return r&&n.removeFragment(s.body),r}))}else s.body.type===r&&n.removeFragment(s.body)}))},e.detectPartialFragments=function(t){var e=this,r=this.timeRanges,i=t.frag,n=t.part;if(r&&"initSegment"!==i.sn){var a=qr(i),s=this.fragments[a];if(!(!s||s.buffered&&i.gap)){var o=!i.relurl;Object.keys(r).forEach((function(t){var a=i.elementaryStreams[t];if(a){var l=r[t],u=o||!0===a.partial;s.range[t]=e.getBufferedTimes(i,n,u,l)}})),s.loaded=null,Object.keys(s.range).length?(s.buffered=!0,(s.body.endList=i.endList||s.body.endList)&&(this.endListFragments[s.body.type]=s),jr(s)||this.removeParts(i.sn-1,i.type)):this.removeFragment(s.body)}}},e.removeParts=function(t,e){var r=this.activePartLists[e];r&&(this.activePartLists[e]=r.filter((function(e){return e.fragment.sn>=t})))},e.fragBuffered=function(t,e){var r=qr(t),i=this.fragments[r];!i&&e&&(i=this.fragments[r]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)},e.getBufferedTimes=function(t,e,r,i){for(var n={time:[],partial:r},a=t.start,s=t.end,o=t.minEndPTS||s,l=t.maxStartPTS||a,u=0;u=h&&o<=d){n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(ah){var c=Math.max(a,i.start(u)),f=Math.min(s,i.end(u));f>c&&(n.partial=!0,n.time.push({startPTS:c,endPTS:f}))}else if(s<=h)break}return n},e.getPartialFragment=function(t){var e,r,i,n=null,a=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&jr(u)&&(r=u.body.start-s,i=u.body.end+s,t>=r&&t<=i&&(e=Math.min(t-r,i-t),a<=e&&(n=u.body,a=e)))})),n},e.isEndListAppended=function(t){var e=this.endListFragments[t];return void 0!==e&&(e.buffered||jr(e))},e.getState=function(t){var e=qr(t),r=this.fragments[e];return r?r.buffered?jr(r)?Vr:Yr:Hr:Kr},e.isTimeBuffered=function(t,e,r){for(var i,n,a=0;a=i&&e<=n)return!0;if(e<=i)return!1}return!1},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part;if("initSegment"!==r.sn&&!r.bitrateTest){var n=i?null:e,a=qr(r);this.fragments[a]={body:r,appendedPTS:null,loaded:n,buffered:!1,range:Object.create(null)}}},e.onBufferAppended=function(t,e){var r=this,i=e.frag,n=e.part,a=e.timeRanges;if("initSegment"!==i.sn){var s=i.type;if(n){var o=this.activePartLists[s];o||(this.activePartLists[s]=o=[]),o.push(n)}this.timeRanges=a,Object.keys(a).forEach((function(t){var e=a[t];r.detectEvictedFragments(t,e,s,n)}))}},e.onFragBuffered=function(t,e){this.detectPartialFragments(e)},e.hasFragment=function(t){var e=qr(t);return!!this.fragments[e]},e.hasParts=function(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)},e.removeFragmentsInRange=function(t,e,r,i,n){var a=this;i&&!this.hasGaps||Object.keys(this.fragments).forEach((function(s){var o=a.fragments[s];if(o){var l=o.body;l.type!==r||i&&!l.gap||l.startt&&(o.buffered||n)&&a.removeFragment(l)}}))},e.removeFragment=function(t){var e=qr(t);t.stats.loaded=0,t.clearElementaryStreamInfo();var r=this.activePartLists[t.type];if(r){var i=t.sn;this.activePartLists[t.type]=r.filter((function(t){return t.fragment.sn!==i}))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]},e.removeAllFragments=function(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1},t}();function jr(t){var e,r,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(r=t.range.audio)?void 0:r.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function qr(t){return t.type+"_"+t.level+"_"+t.sn}var Xr={length:0,start:function(){return 0},end:function(){return 0}},zr=function(){function t(){}return t.isBuffered=function(e,r){try{if(e)for(var i=t.getBuffered(e),n=0;n=i.start(n)&&r<=i.end(n))return!0}catch(t){}return!1},t.bufferInfo=function(e,r,i){try{if(e){var n,a=t.getBuffered(e),s=[];for(n=0;ns&&(i[a-1].end=t[n].end):i.push(t[n])}else i.push(t[n])}else i=t;for(var o,l=0,u=e,h=e,d=0;d=c&&er.startCC||t&&t.cc>>8^255&m^99,t[f]=m,e[m]=f;var p=c[f],y=c[p],E=c[y],T=257*c[m]^16843008*m;i[f]=T<<24|T>>>8,n[f]=T<<16|T>>>16,a[f]=T<<8|T>>>24,s[f]=T,T=16843009*E^65537*y^257*p^16843008*f,l[m]=T<<24|T>>>8,u[m]=T<<16|T>>>16,h[m]=T<<8|T>>>24,d[m]=T,f?(f=p^c[c[c[E^p]]],g^=c[c[g]]):f=g=1}},e.expandKey=function(t){for(var e=this.uint8ArrayToUint32Array_(t),r=!0,i=0;is.end){var h=a>u;(a0&&null!=a&&a.key&&a.iv&&"AES-128"===a.method){var s=self.performance.now();return r.decrypter.decrypt(new Uint8Array(n),a.key.buffer,a.iv.buffer).catch((function(e){throw i.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((function(n){var a=self.performance.now();return i.trigger(S.FRAG_DECRYPTED,{frag:t,payload:n,stats:{tstart:s,tdecrypt:a}}),e.payload=n,r.completeInitSegmentLoad(e)}))}return r.completeInitSegmentLoad(e)})).catch((function(e){r.state!==ci&&r.state!==Si&&(r.warn(e),r.resetFragmentLoading(t))}))},r.completeInitSegmentLoad=function(t){if(!this.levels)throw new Error("init load aborted, missing levels");var e=t.frag.stats;this.state=fi,t.frag.data=new Uint8Array(t.payload),e.parsing.start=e.buffering.start=self.performance.now(),e.parsing.end=e.buffering.end=self.performance.now(),this.tick()},r.fragContextChanged=function(t){var e=this.fragCurrent;return!t||!e||t.sn!==e.sn||t.level!==e.level},r.fragBufferedComplete=function(t,e){var r,i,n,a,s=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log("Buffered "+t.type+" sn: "+t.sn+(e?" part: "+e.index:"")+" of "+(this.playlistType===Ie?"level":"track")+" "+t.level+" (frag:["+(null!=(r=t.startPTS)?r:NaN).toFixed(3)+"-"+(null!=(i=t.endPTS)?i:NaN).toFixed(3)+"] > buffer:"+(s?di(zr.getBuffered(s)):"(detached)")+")"),"initSegment"!==t.sn){var o;if(t.type!==Ce){var l=t.elementaryStreams;if(!Object.keys(l).some((function(t){return!!l[t]})))return void(this.state=fi)}var u=null==(o=this.levels)?void 0:o[t.level];null!=u&&u.fragmentError&&(this.log("Resetting level fragment error count of "+u.fragmentError+" on frag buffered"),u.fragmentError=0)}this.state=fi,s&&(!this.loadedmetadata&&t.type==Ie&&s.buffered.length&&(null==(n=this.fragCurrent)?void 0:n.sn)===(null==(a=this.fragPrevious)?void 0:a.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())},r.seekToStartPos=function(){},r._handleFragmentLoadComplete=function(t){var e=this.transmuxer;if(e){var r=t.frag,i=t.part,n=t.partsLoaded,a=!n||0===n.length||n.some((function(t){return!t})),s=new Qr(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!a);e.flush(s)}},r._handleFragmentLoadProgress=function(t){},r._doFragLoad=function(t,e,r,i){var n,a=this;void 0===r&&(r=null);var s=null==e?void 0:e.details;if(!this.levels||!s)throw new Error("frag load aborted, missing level"+(s?"":" detail")+"s");var o=null;if(!t.encrypted||null!=(n=t.decryptdata)&&n.key?!t.encrypted&&s.encryptedFragments.length&&this.keyLoader.loadClear(t,s.encryptedFragments):(this.log("Loading key for "+t.sn+" of ["+s.startSN+"-"+s.endSN+"], "+("[stream-controller]"===this.logPrefix?"level":"track")+" "+t.level),this.state=gi,this.fragCurrent=t,o=this.keyLoader.load(t).then((function(t){if(!a.fragContextChanged(t.frag))return a.hls.trigger(S.KEY_LOADED,t),a.state===gi&&(a.state=fi),t})),this.hls.trigger(S.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(o=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),r=Math.max(t.start,r||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){var l=s.partList;if(l&&i){r>t.end&&s.fragmentHint&&(t=s.fragmentHint);var u=this.getNextPart(l,t,r);if(u>-1){var h,d=l[u];return this.log("Loading part sn: "+t.sn+" p: "+d.index+" cc: "+t.cc+" of playlist ["+s.startSN+"-"+s.endSN+"] parts [0-"+u+"-"+(l.length-1)+"] "+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=d.start+d.duration,this.state=vi,h=o?o.then((function(r){return!r||a.fragContextChanged(r.frag)?null:a.doFragPartsLoad(t,d,e,i)})).catch((function(t){return a.handleFragLoadError(t)})):this.doFragPartsLoad(t,d,e,i).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,part:d,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(l,r))return Promise.resolve(null)}}this.log("Loading fragment "+t.sn+" cc: "+t.cc+" "+(s?"of ["+s.startSN+"-"+s.endSN+"] ":"")+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),y(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=vi;var c,f=this.config.progressive;return c=f&&o?o.then((function(e){return!e||a.fragContextChanged(null==e?void 0:e.frag)?null:a.fragmentLoader.load(t,i)})).catch((function(t){return a.handleFragLoadError(t)})):Promise.all([this.fragmentLoader.load(t,f?i:void 0),o]).then((function(t){var e=t[0];return!f&&e&&i&&i(e),e})).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):c},r.doFragPartsLoad=function(t,e,r,i){var n=this;return new Promise((function(a,s){var o,l=[],u=null==(o=r.details)?void 0:o.partList;!function e(o){n.fragmentLoader.loadPart(t,o,i).then((function(i){l[o.index]=i;var s=i.part;n.hls.trigger(S.FRAG_LOADED,i);var h=or(r,t.sn,o.index+1)||lr(u,t.sn,o.index+1);if(!h)return a({frag:t,part:s,partsLoaded:l});e(h)})).catch(s)}(e)}))},r.handleFragLoadError=function(t){if("data"in t){var e=t.data;t.data&&e.details===A.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(S.ERROR,e)}else this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null},r._handleTransmuxerFlush=function(t){var e=this.getCurrentContext(t);if(e&&this.state===yi){var r=e.frag,i=e.part,n=e.level,a=self.performance.now();r.stats.parsing.end=a,i&&(i.stats.parsing.end=a),this.updateLevelTiming(r,i,n,t.partial)}else this.fragCurrent||this.state===ci||this.state===Si||(this.state=fi)},r.getCurrentContext=function(t){var e=this.levels,r=this.fragCurrent,i=t.level,n=t.sn,a=t.part;if(null==e||!e[i])return this.warn("Levels object was unset while buffering fragment "+n+" of level "+i+". The current chunk will not be buffered."),null;var s=e[i],o=a>-1?or(s,n,a):null,l=o?o.fragment:function(t,e,r){if(null==t||!t.details)return null;var i=t.details,n=i.fragments[e-i.startSN];return n||((n=i.fragmentHint)&&n.sn===e?n:ea&&this.flushMainBuffer(s,t.start)}else this.flushMainBuffer(0,t.start)},r.getFwdBufferInfo=function(t,e){var r=this.getLoadPosition();return y(r)?this.getFwdBufferInfoAtPos(t,r,e):null},r.getFwdBufferInfoAtPos=function(t,e,r){var i=this.config.maxBufferHole,n=zr.bufferInfo(t,e,i);if(0===n.len&&void 0!==n.nextStart){var a=this.fragmentTracker.getBufferedFrag(e,r);if(a&&n.nextStart=r&&(e.maxMaxBufferLength/=2,this.warn("Reduce max buffer length to "+e.maxMaxBufferLength+"s"),!0)},r.getAppendedFrag=function(t,e){var r=this.fragmentTracker.getAppendedFrag(t,Ie);return r&&"fragment"in r?r.fragment:r},r.getNextFragment=function(t,e){var r=e.fragments,i=r.length;if(!i)return null;var n,a=this.config,s=r[0].start;if(e.live){var o=a.initialLiveManifestSize;if(ie},r.getNextFragmentLoopLoading=function(t,e,r,i,n){var a=t.gap,s=this.getNextFragment(this.nextLoadPosition,e);if(null===s)return s;if(t=s,a&&t&&!t.gap&&r.nextStart){var o=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,r.nextStart,i);if(null!==o&&r.len+o.len>=n)return this.log('buffer full after gaps in "'+i+'" playlist starting at sn: '+t.sn),null}return t},r.mapToInitFragWhenRequired=function(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment},r.getNextPart=function(t,e,r){for(var i=-1,n=!1,a=!0,s=0,o=t.length;s-1&&rr.start&&r.loaded},r.getInitialLiveFragment=function(t,e){var r=this.fragPrevious,i=null;if(r){if(t.hasProgramDateTime&&(this.log("Live playlist, switching playlist, load frag with same PDT: "+r.programDateTime),i=function(t,e,r){if(null===e||!Array.isArray(t)||!t.length||!y(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;r=r||0;for(var i=0;i=t.startSN&&n<=t.endSN){var a=e[n-t.startSN];r.cc===a.cc&&(i=a,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=function(t,e){return vr(t,(function(t){return t.cce?-1:0}))}(e,r.cc),i&&this.log("Live playlist, switching playlist, load frag with same CC: "+i.sn))}}else{var s=this.hls.liveSyncPosition;null!==s&&(i=this.getFragmentAtPosition(s,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i},r.getFragmentAtPosition=function(t,e,r){var i,n=this.config,a=this.fragPrevious,s=r.fragments,o=r.endSN,l=r.fragmentHint,u=n.maxFragLookUpTolerance,h=r.partList,d=!!(n.lowLatencyMode&&null!=h&&h.length&&l);if(d&&l&&!this.bitrateTest&&(s=s.concat(l),o=l.sn),i=te-u?0:u):s[s.length-1]){var c=i.sn-r.startSN,f=this.fragmentTracker.getState(i);if((f===Yr||f===Vr&&i.gap)&&(a=i),a&&i.sn===a.sn&&(!d||h[0].fragment.sn>i.sn)&&a&&i.level===a.level){var g=s[c+1];i=i.sn=a-e.maxFragLookUpTolerance&&n<=s;if(null!==i&&r.duration>i&&(n"+t.startSN+" prev-sn: "+(o?o.sn:"na")+" fragments: "+i),l}return n},r.waitForCdnTuneIn=function(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)},r.setStartPosition=function(t,e){var r=this.startPosition;if(r "+(null==(n=this.fragCurrent)?void 0:n.url))}else{var a=e.details===A.FRAG_GAP;a&&this.fragmentTracker.fragBuffered(i,!0);var s=e.errorAction,o=s||{},l=o.action,u=o.retryCount,h=void 0===u?0:u,d=o.retryConfig;if(s&&l===Lr&&d){this.resetStartWhenNotLoaded(this.levelLastLoaded);var c=cr(d,h);this.warn("Fragment "+i.sn+" of "+t+" "+i.level+" errored with "+e.details+", retrying loading "+(h+1)+"/"+d.maxNumRetry+" in "+c+"ms"),s.resolved=!0,this.retryDate=self.performance.now()+c,this.state=mi}else if(d&&s){if(this.resetFragmentErrors(t),!(h.5;i&&this.reduceMaxBufferLength(r.len);var n=!i;return n&&this.warn("Buffer full error while media.currentTime is not buffered, flush "+e+" buffer"),t.frag&&(this.fragmentTracker.removeFragment(t.frag),this.nextLoadPosition=t.frag.start),this.resetLoadingState(),n}return!1},r.resetFragmentErrors=function(t){t===we&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==ci&&(this.state=fi)},r.afterBufferFlushed=function(t,e,r){if(t){var i=zr.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,r),this.state===Ti&&this.resetLoadingState()}},r.resetLoadingState=function(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=fi},r.resetStartWhenNotLoaded=function(t){if(!this.loadedmetadata){this.startFragRequested=!1;var e=t?t.details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}},r.resetWhenMissingContext=function(t){this.warn("The loading context changed while buffering fragment "+t.sn+" of level "+t.level+". This chunk will not be buffered."),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState()},r.removeUnbufferedFrags=function(t){void 0===t&&(t=0),this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)},r.updateLevelTiming=function(t,e,r,i){var n,a=this,s=r.details;if(s){if(!Object.keys(t.elementaryStreams).reduce((function(e,n){var o=t.elementaryStreams[n];if(o){var l=o.endPTS-o.startPTS;if(l<=0)return a.warn("Could not parse fragment "+t.sn+" "+n+" duration reliably ("+l+")"),e||!1;var u=i?0:ir(s,t,o.startPTS,o.endPTS,o.startDTS,o.endDTS);return a.hls.trigger(S.LEVEL_PTS_UPDATED,{details:s,level:r,drift:u,type:n,frag:t,start:o.startPTS,end:o.endPTS}),!0}return e}),!1)&&null===(null==(n=this.transmuxer)?void 0:n.error)){var o=new Error("Found no media in fragment "+t.sn+" of level "+t.level+" resetting transmuxer to fallback to playlist timing");if(0===r.fragmentError&&(r.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(o.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:o,frag:t,reason:"Found no media in msn "+t.sn+' of level "'+r.url+'"'}),!this.hls)return;this.resetTransmuxer()}this.state=Ei,this.hls.trigger(S.FRAG_PARSED,{frag:t,part:e})}else this.warn("level.details undefined")},r.resetTransmuxer=function(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)},r.recoverWorkerError=function(t){"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState())},s(e,[{key:"state",get:function(){return this._state},set:function(t){var e=this._state;e!==t&&(this._state=t,this.log(e+"->"+t))}}]),e}(Gr),ki=function(){function t(){this.chunks=[],this.dataLength=0}var e=t.prototype;return e.push=function(t){this.chunks.push(t),this.dataLength+=t.length},e.flush=function(){var t,e=this.chunks,r=this.dataLength;return e.length?(t=1===e.length?e[0]:function(t,e){for(var r=new Uint8Array(e),i=0,n=0;n0&&s.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:Be,duration:Number.POSITIVE_INFINITY});n>>5}function xi(t,e){return e+1=t.length)return!1;var i=_i(t,e);if(i<=r)return!1;var n=e+i;return n===t.length||xi(t,n)}return!1}function Fi(t,e,r,i,n){if(!t.samplerate){var a=function(t,e,r,i){var n,a,s,o,l=navigator.userAgent.toLowerCase(),u=i,h=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];n=1+((192&e[r+2])>>>6);var d=(60&e[r+2])>>>2;if(!(d>h.length-1))return s=(1&e[r+2])<<2,s|=(192&e[r+3])>>>6,w.log("manifest codec:"+i+", ADTS type:"+n+", samplingIndex:"+d),/firefox/i.test(l)?d>=6?(n=5,o=new Array(4),a=d-3):(n=2,o=new Array(2),a=d):-1!==l.indexOf("android")?(n=2,o=new Array(2),a=d):(n=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&d>=6?a=d-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(d>=6&&1===s||/vivaldi/i.test(l))||!i&&1===s)&&(n=2,o=new Array(2)),a=d)),o[0]=n<<3,o[0]|=(14&d)>>1,o[1]|=(1&d)<<7,o[1]|=s<<3,5===n&&(o[1]|=(14&a)>>1,o[2]=(1&a)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:h[d],channelCount:s,codec:"mp4a.40."+n,manifestCodec:u};var c=new Error("invalid ADTS sampling index:"+d);t.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!0,error:c,reason:c.message})}(e,r,i,n);if(!a)return;t.config=a.config,t.samplerate=a.samplerate,t.channelCount=a.channelCount,t.codec=a.codec,t.manifestCodec=a.manifestCodec,w.log("parsed codec:"+t.codec+", rate:"+a.samplerate+", channels:"+a.channelCount)}}function Mi(t){return 9216e4/t}function Oi(t,e,r,i,n){var a,s=i+n*Mi(t.samplerate),o=function(t,e){var r=Ci(t,e);if(e+r<=t.length){var i=_i(t,e)-r;if(i>0)return{headerLength:r,frameLength:i}}}(e,r);if(o){var l=o.frameLength,u=o.headerLength,h=u+l,d=Math.max(0,r+h-e.length);d?(a=new Uint8Array(h-u)).set(e.subarray(r+u,e.length),0):a=e.subarray(r+u,r+h);var c={unit:a,pts:s};return d||t.samples.push(c),{sample:c,length:h,missing:d}}var f=e.length-r;return(a=new Uint8Array(f)).set(e.subarray(r,e.length),0),{sample:{unit:a,pts:s},length:f,missing:-1}}var Ni=null,Ui=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],Bi=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],Gi=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],Ki=[0,1,1,4];function Hi(t,e,r,i,n){if(!(r+24>e.length)){var a=Vi(e,r);if(a&&r+a.frameLength<=e.length){var s=i+n*(9e4*a.samplesPerFrame/a.sampleRate),o={unit:e.subarray(r,r+a.frameLength),pts:s,dts:s};return t.config=[],t.channelCount=a.channelCount,t.samplerate=a.sampleRate,t.samples.push(o),{sample:o,length:a.frameLength,missing:0}}}}function Vi(t,e){var r=t[e+1]>>3&3,i=t[e+1]>>1&3,n=t[e+2]>>4&15,a=t[e+2]>>2&3;if(1!==r&&0!==n&&15!==n&&3!==a){var s=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*Ui[14*(3===r?3-i:3===i?3:4)+n-1],u=Bi[3*(3===r?0:2===r?1:2)+a],h=3===o?1:2,d=Gi[r][i],c=Ki[i],f=8*d*c,g=Math.floor(d*l/u+s)*c;if(null===Ni){var v=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Ni=v?parseInt(v[1]):0}return!!Ni&&Ni<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:u,channelCount:h,frameLength:g,samplesPerFrame:f}}}function Yi(t,e){return 255===t[e]&&224==(224&t[e+1])&&0!=(6&t[e+1])}function Wi(t,e){return e+18&&109===t[r+4]&&111===t[r+5]&&111===t[r+6]&&102===t[r+7])return!0;r=i>1?r+i:e}return!1}(t)},e.demux=function(t,e){this.timeOffset=e;var r=t,i=this.videoTrack,n=this.txtTrack;if(this.config.progressive){this.remainderData&&(r=Gt(this.remainderData,t));var a=function(t){var e={valid:null,remainder:null},r=_t(t,["moof"]);if(r.length<2)return e.remainder=t,e;var i=r[r.length-1];return e.valid=nt(t,0,i.byteOffset-8),e.remainder=nt(t,i.byteOffset-8),e}(r);this.remainderData=a.remainder,i.samples=a.valid||new Uint8Array}else i.samples=r;var s=this.extractID3Track(i,e);return n.samples=Kt(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:s,textTrack:this.txtTrack}},e.flush=function(){var t=this.timeOffset,e=this.videoTrack,r=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;var i=this.extractID3Track(e,this.timeOffset);return r.samples=Kt(t,e),{videoTrack:e,audioTrack:bi(),id3Track:i,textTrack:bi()}},e.extractID3Track=function(t,e){var r=this.id3Track;if(t.samples.length){var i=_t(t.samples,["emsg"]);i&&i.forEach((function(t){var i=function(t){var e=t[0],r="",i="",n=0,a=0,s=0,o=0,l=0,u=0;if(0===e){for(;"\0"!==bt(t.subarray(u,u+1));)r+=bt(t.subarray(u,u+1)),u+=1;for(r+=bt(t.subarray(u,u+1)),u+=1;"\0"!==bt(t.subarray(u,u+1));)i+=bt(t.subarray(u,u+1)),u+=1;i+=bt(t.subarray(u,u+1)),u+=1,n=It(t,12),a=It(t,16),o=It(t,20),l=It(t,24),u=28}else if(1===e){n=It(t,u+=4);var h=It(t,u+=4),d=It(t,u+=4);for(u+=4,s=Math.pow(2,32)*h+d,E(s)||(s=Number.MAX_SAFE_INTEGER,w.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=It(t,u),l=It(t,u+=4),u+=4;"\0"!==bt(t.subarray(u,u+1));)r+=bt(t.subarray(u,u+1)),u+=1;for(r+=bt(t.subarray(u,u+1)),u+=1;"\0"!==bt(t.subarray(u,u+1));)i+=bt(t.subarray(u,u+1)),u+=1;i+=bt(t.subarray(u,u+1)),u+=1}return{schemeIdUri:r,value:i,timeScale:n,presentationTime:s,presentationTimeDelta:a,eventDuration:o,id:l,payload:t.subarray(u,t.byteLength)}}(t);if(Xi.test(i.schemeIdUri)){var n=y(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale,a=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;a<=.001&&(a=Number.POSITIVE_INFINITY);var s=i.payload;r.samples.push({data:s,len:s.byteLength,dts:n,pts:n,type:Ke,duration:a})}}))}return r},e.demuxSampleAes=function(t,e,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},e.destroy=function(){},t}(),Qi=function(t,e){var r=0,i=5;e+=i;for(var n=new Uint32Array(1),a=new Uint32Array(1),s=new Uint8Array(1);i>0;){s[0]=t[e];var o=Math.min(i,8),l=8-o;a[0]=4278190080>>>24+l<>l,r=r?r<e.length)return-1;if(11!==e[r]||119!==e[r+1])return-1;var a=e[r+4]>>6;if(a>=3)return-1;var s=[48e3,44100,32e3][a],o=63&e[r+4],l=2*[64,69,96,64,70,96,80,87,120,80,88,120,96,104,144,96,105,144,112,121,168,112,122,168,128,139,192,128,140,192,160,174,240,160,175,240,192,208,288,192,209,288,224,243,336,224,244,336,256,278,384,256,279,384,320,348,480,320,349,480,384,417,576,384,418,576,448,487,672,448,488,672,512,557,768,512,558,768,640,696,960,640,697,960,768,835,1152,768,836,1152,896,975,1344,896,976,1344,1024,1114,1536,1024,1115,1536,1152,1253,1728,1152,1254,1728,1280,1393,1920,1280,1394,1920][3*o+a];if(r+l>e.length)return-1;var u=e[r+6]>>5,h=0;2===u?h+=2:(1&u&&1!==u&&(h+=2),4&u&&(h+=2));var d=(e[r+6]<<8|e[r+7])>>12-h&1,c=[2,1,2,3,3,4,4,5][u]+d,f=e[r+5]>>3,g=7&e[r+5],v=new Uint8Array([a<<6|f<<1|g>>2,(3&g)<<6|u<<3|d<<2|o>>4,o<<4&224]),m=i+n*(1536/s*9e4),p=e.subarray(r,r+l);return t.config=v,t.channelCount=c,t.samplerate=s,t.samples.push({unit:p,pts:m}),l}var Zi=function(){function t(){this.VideoSample=null}var e=t.prototype;return e.createVideoSample=function(t,e,r,i){return{key:t,frame:!1,pts:e,dts:r,units:[],debug:i,length:0}},e.getLastNalUnit=function(t){var e,r,i=this.VideoSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){var n=i.units;r=n[n.length-1]}return r},e.pushAccessUnit=function(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){var r=e.samples,i=r.length;if(!i)return void e.dropped++;var n=r[i-1];t.pts=n.pts,t.dts=n.dts}e.samples.push(t)}t.debug.length&&w.log(t.pts+"/"+t.dts+":"+t.debug)},t}(),tn=function(){function t(t){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=t,this.bytesAvailable=t.byteLength,this.word=0,this.bitsAvailable=0}var e=t.prototype;return e.loadWord=function(){var t=this.data,e=this.bytesAvailable,r=t.byteLength-e,i=new Uint8Array(4),n=Math.min(4,e);if(0===n)throw new Error("no bytes available");i.set(t.subarray(r,r+n)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*n,this.bytesAvailable-=n},e.skipBits=function(t){var e;t=Math.min(t,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>t?(this.word<<=t,this.bitsAvailable-=t):(t-=this.bitsAvailable,t-=(e=t>>3)<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)},e.readBits=function(t){var e=Math.min(this.bitsAvailable,t),r=this.word>>>32-e;if(t>32&&w.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return(e=t-e)>0&&this.bitsAvailable?r<>>t))return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()},e.skipUEG=function(){this.skipBits(1+this.skipLZ())},e.skipEG=function(){this.skipBits(1+this.skipLZ())},e.readUEG=function(){var t=this.skipLZ();return this.readBits(t+1)-1},e.readEG=function(){var t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)},e.readBoolean=function(){return 1===this.readBits(1)},e.readUByte=function(){return this.readBits(8)},e.readUShort=function(){return this.readBits(16)},e.readUInt=function(){return this.readBits(32)},e.skipScalingList=function(t){for(var e=8,r=8,i=0;i4){var f=new tn(c).readSliceType();2!==f&&4!==f&&7!==f&&9!==f||(h=!0)}h&&null!=(d=l)&&d.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.frame=!0,l.key=h;break;case 5:a=!0,null!=(o=l)&&o.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.key=!0,l.frame=!0;break;case 6:a=!0,Vt(i.data,1,r.pts,e.samples);break;case 7:var g,v;a=!0,u=!0;var m=i.data,p=new tn(m).readSPS();if(!t.sps||t.width!==p.width||t.height!==p.height||(null==(g=t.pixelRatio)?void 0:g[0])!==p.pixelRatio[0]||(null==(v=t.pixelRatio)?void 0:v[1])!==p.pixelRatio[1]){t.width=p.width,t.height=p.height,t.pixelRatio=p.pixelRatio,t.sps=[m],t.duration=n;for(var y=m.subarray(1,4),E="avc1.",T=0;T<3;T++){var S=y[T].toString(16);S.length<2&&(S="0"+S),E+=S}t.codec=E}break;case 8:a=!0,t.pps=[i.data];break;case 9:a=!0,t.audFound=!0,l&&s.pushAccessUnit(l,t),l=s.VideoSample=s.createVideoSample(!1,r.pts,r.dts,"");break;case 12:a=!0;break;default:a=!1,l&&(l.debug+="unknown NAL "+i.type+" ")}l&&a&&l.units.push(i)})),i&&l&&(this.pushAccessUnit(l,t),this.VideoSample=null)},r.parseAVCNALu=function(t,e){var r,i,n=e.byteLength,a=t.naluState||0,s=a,o=[],l=0,u=-1,h=0;for(-1===a&&(u=0,h=31&e[0],a=0,l=1);l=0){var d={data:e.subarray(u,i),type:h};o.push(d)}else{var c=this.getLastNalUnit(t.samples);c&&(s&&l<=4-s&&c.state&&(c.data=c.data.subarray(0,c.data.byteLength-s)),i>0&&(c.data=Gt(c.data,e.subarray(0,i)),c.state=0))}l=0&&a>=0){var f={data:e.subarray(u,n),type:h,state:a};o.push(f)}if(0===o.length){var g=this.getLastNalUnit(t.samples);g&&(g.data=Gt(g.data,e))}return t.naluState=a,o},e}(Zi),rn=function(){function t(t,e,r){this.keyData=void 0,this.decrypter=void 0,this.keyData=r,this.decrypter=new hi(e,{removePKCS7Padding:!1})}var e=t.prototype;return e.decryptBuffer=function(t){return this.decrypter.decrypt(t,this.keyData.key.buffer,this.keyData.iv.buffer)},e.decryptAacSample=function(t,e,r){var i=this,n=t[e].unit;if(!(n.length<=16)){var a=n.subarray(16,n.length-n.length%16),s=a.buffer.slice(a.byteOffset,a.byteOffset+a.length);this.decryptBuffer(s).then((function(a){var s=new Uint8Array(a);n.set(s,16),i.decrypter.isSync()||i.decryptAacSamples(t,e+1,r)}))}},e.decryptAacSamples=function(t,e,r){for(;;e++){if(e>=t.length)return void r();if(!(t[e].unit.length<32||(this.decryptAacSample(t,e,r),this.decrypter.isSync())))return}},e.getAvcEncryptedData=function(t){for(var e=16*Math.floor((t.length-48)/160)+16,r=new Int8Array(e),i=0,n=32;n=t.length)return void i();for(var n=t[e].units;!(r>=n.length);r++){var a=n[r];if(!(a.data.length<=48||1!==a.type&&5!==a.type||(this.decryptAvcSample(t,e,r,i,a),this.decrypter.isSync())))return}}},t}(),nn=188,an=function(){function t(t,e,r){this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._duration=0,this._pmtId=-1,this._videoTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.remainderData=null,this.videoParser=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.videoParser=new en}t.probe=function(e){var r=t.syncOffset(e);return r>0&&w.warn("MPEG2-TS detected but first sync word found @ offset "+r),-1!==r},t.syncOffset=function(t){for(var e=t.length,r=Math.min(940,e-nn)+1,i=0;i1&&(0===a&&s>2||o+nn>r))return a}i++}return-1},t.createTrack=function(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:kt[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}};var e=t.prototype;return e.resetInitSegment=function(e,r,i,n){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=t.createTrack("video"),this._audioTrack=t.createTrack("audio",n),this._id3Track=t.createTrack("id3"),this._txtTrack=t.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.remainderData=null,this.audioCodec=r,this.videoCodec=i,this._duration=n},e.resetTimeStamp=function(){},e.resetContiguity=function(){var t=this._audioTrack,e=this._videoTrack,r=this._id3Track;t&&(t.pesData=null),e&&(e.pesData=null),r&&(r.pesData=null),this.aacOverFlow=null,this.remainderData=null},e.demux=function(e,r,i,n){var a;void 0===i&&(i=!1),void 0===n&&(n=!1),i||(this.sampleAes=null);var s=this._videoTrack,o=this._audioTrack,l=this._id3Track,u=this._txtTrack,h=s.pid,d=s.pesData,c=o.pid,f=l.pid,g=o.pesData,v=l.pesData,m=null,p=this.pmtParsed,y=this._pmtId,E=e.length;if(this.remainderData&&(E=(e=Gt(this.remainderData,e)).length,this.remainderData=null),E>4>1){if((I=k+5+e[k+4])===k+nn)continue}else I=k+4;switch(D){case h:b&&(d&&(a=hn(d))&&this.videoParser.parseAVCPES(s,u,a,!1,this._duration),d={data:[],size:0}),d&&(d.data.push(e.subarray(I,k+nn)),d.size+=k+nn-I);break;case c:if(b){if(g&&(a=hn(g)))switch(o.segmentCodec){case"aac":this.parseAACPES(o,a);break;case"mp3":this.parseMPEGPES(o,a);break;case"ac3":this.parseAC3PES(o,a)}g={data:[],size:0}}g&&(g.data.push(e.subarray(I,k+nn)),g.size+=k+nn-I);break;case f:b&&(v&&(a=hn(v))&&this.parseID3PES(l,a),v={data:[],size:0}),v&&(v.data.push(e.subarray(I,k+nn)),v.size+=k+nn-I);break;case 0:b&&(I+=e[I]+1),y=this._pmtId=on(e,I);break;case y:b&&(I+=e[I]+1);var C=ln(e,I,this.typeSupported,i);(h=C.videoPid)>0&&(s.pid=h,s.segmentCodec=C.segmentVideoCodec),(c=C.audioPid)>0&&(o.pid=c,o.segmentCodec=C.segmentAudioCodec),(f=C.id3Pid)>0&&(l.pid=f),null===m||p||(w.warn("MPEG-TS PMT found at "+k+" after unknown PID '"+m+"'. Backtracking to sync byte @"+T+" to parse all TS packets."),m=null,k=T-188),p=this.pmtParsed=!0;break;case 17:case 8191:break;default:m=D}}else R++;if(R>0){var _=new Error("Found "+R+" TS packet/s that do not start with 0x47");this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:_,reason:_.message})}s.pesData=d,o.pesData=g,l.pesData=v;var x={audioTrack:o,videoTrack:s,id3Track:l,textTrack:u};return n&&this.extractRemainingSamples(x),x},e.flush=function(){var t,e=this.remainderData;return this.remainderData=null,t=e?this.demux(e,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(t),this.sampleAes?this.decrypt(t,this.sampleAes):t},e.extractRemainingSamples=function(t){var e,r=t.audioTrack,i=t.videoTrack,n=t.id3Track,a=t.textTrack,s=i.pesData,o=r.pesData,l=n.pesData;if(s&&(e=hn(s))?(this.videoParser.parseAVCPES(i,a,e,!0,this._duration),i.pesData=null):i.pesData=s,o&&(e=hn(o))){switch(r.segmentCodec){case"aac":this.parseAACPES(r,e);break;case"mp3":this.parseMPEGPES(r,e);break;case"ac3":this.parseAC3PES(r,e)}r.pesData=null}else null!=o&&o.size&&w.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=o;l&&(e=hn(l))?(this.parseID3PES(n,e),n.pesData=null):n.pesData=l},e.demuxSampleAes=function(t,e,r){var i=this.demux(t,r,!0,!this.config.progressive),n=this.sampleAes=new rn(this.observer,this.config,e);return this.decrypt(i,n)},e.decrypt=function(t,e){return new Promise((function(r){var i=t.audioTrack,n=t.videoTrack;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(function(){n.samples?e.decryptAvcSamples(n.samples,0,0,(function(){r(t)})):r(t)})):n.samples&&e.decryptAvcSamples(n.samples,0,0,(function(){r(t)}))}))},e.destroy=function(){this._duration=0},e.parseAACPES=function(t,e){var r,i,n,a=0,s=this.aacOverFlow,o=e.data;if(s){this.aacOverFlow=null;var l=s.missing,u=s.sample.unit.byteLength;if(-1===l)o=Gt(s.sample.unit,o);else{var h=u-l;s.sample.unit.set(o.subarray(0,l),h),t.samples.push(s.sample),a=s.missing}}for(r=a,i=o.length;r0;)o+=n;else w.warn("[tsdemuxer]: AC3 PES unknown PTS")},e.parseID3PES=function(t,e){if(void 0!==e.pts){var r=o({},e,{type:this._videoTrack?Ke:Be,duration:Number.POSITIVE_INFINITY});t.samples.push(r)}else w.warn("[tsdemuxer]: ID3 PES unknown PTS")},t}();function sn(t,e){return((31&t[e+1])<<8)+t[e+2]}function on(t,e){return(31&t[e+10])<<8|t[e+11]}function ln(t,e,r,i){var n={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},a=e+3+((15&t[e+1])<<8|t[e+2])-4;for(e+=12+((15&t[e+10])<<8|t[e+11]);e0)for(var l=e+5,u=o;u>2;){106===t[l]&&(!0!==r.ac3?w.log("AC-3 audio found, not supported in this browser for now"):(n.audioPid=s,n.segmentAudioCodec="ac3"));var h=t[l+1]+2;l+=h,u-=h}break;case 194:case 135:w.warn("Unsupported EC-3 in M2TS found");break;case 36:w.warn("Unsupported HEVC in M2TS found")}e+=o+5}return n}function un(t){w.log(t+" with AES-128-CBC encryption found in unencrypted stream")}function hn(t){var e,r,i,n,a,s=0,o=t.data;if(!t||0===t.size)return null;for(;o[0].length<19&&o.length>1;)o[0]=Gt(o[0],o[1]),o.splice(1,1);if(1===((e=o[0])[0]<<16)+(e[1]<<8)+e[2]){if((r=(e[4]<<8)+e[5])&&r>t.size-6)return null;var l=e[7];192&l&&(n=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&l?n-(a=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2)>54e5&&(w.warn(Math.round((n-a)/9e4)+"s delta between PTS and DTS, align them"),n=a):a=n);var u=(i=e[8])+9;if(t.size<=u)return null;t.size-=u;for(var h=new Uint8Array(t.size),d=0,c=o.length;df){u-=f;continue}e=e.subarray(u),f-=u,u=0}h.set(e,s),s+=f}return r&&(r-=i+3),{data:h,pts:n,dts:a,len:r}}return null}var dn=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var r=e.prototype;return r.resetInitSegment=function(e,r,i,n){t.prototype.resetInitSegment.call(this,e,r,i,n),this._audioTrack={container:"audio/mpeg",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"mp3",samples:[],manifestCodec:r,duration:n,inputTimeScale:9e4,dropped:0}},e.probe=function(t){if(!t)return!1;var e=lt(t,0),r=(null==e?void 0:e.length)||0;if(e&&11===t[r]&&119===t[r+1]&&void 0!==dt(e)&&Qi(t,r)<=16)return!1;for(var i=t.length;r1?r-1:0),n=1;n>24&255,o[1]=e>>16&255,o[2]=e>>8&255,o[3]=255&e,o.set(t,4),a=0,e=8;a>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))},t.mdia=function(e){return t.box(t.types.mdia,t.mdhd(e.timescale,e.duration),t.hdlr(e.type),t.minf(e))},t.mfhd=function(e){return t.box(t.types.mfhd,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e]))},t.minf=function(e){return"audio"===e.type?t.box(t.types.minf,t.box(t.types.smhd,t.SMHD),t.DINF,t.stbl(e)):t.box(t.types.minf,t.box(t.types.vmhd,t.VMHD),t.DINF,t.stbl(e))},t.moof=function(e,r,i){return t.box(t.types.moof,t.mfhd(e),t.traf(i,r))},t.moov=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trak(e[r]);return t.box.apply(null,[t.types.moov,t.mvhd(e[0].timescale,e[0].duration)].concat(i).concat(t.mvex(e)))},t.mvex=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trex(e[r]);return t.box.apply(null,[t.types.mvex].concat(i))},t.mvhd=function(e,r){r*=e;var i=Math.floor(r/(fn+1)),n=Math.floor(r%(fn+1)),a=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return t.box(t.types.mvhd,a)},t.sdtp=function(e){var r,i,n=e.samples||[],a=new Uint8Array(4+n.length);for(r=0;r>>8&255),a.push(255&n),a=a.concat(Array.prototype.slice.call(i));for(r=0;r>>8&255),s.push(255&n),s=s.concat(Array.prototype.slice.call(i));var o=t.box(t.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|e.sps.length].concat(a).concat([e.pps.length]).concat(s))),l=e.width,u=e.height,h=e.pixelRatio[0],d=e.pixelRatio[1];return t.box(t.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,l>>8&255,255&l,u>>8&255,255&u,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),o,t.box(t.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),t.box(t.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))},t.esds=function(t){var e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))},t.audioStsd=function(t){var e=t.samplerate;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,e>>8&255,255&e,0,0])},t.mp4a=function(e){return t.box(t.types.mp4a,t.audioStsd(e),t.box(t.types.esds,t.esds(e)))},t.mp3=function(e){return t.box(t.types[".mp3"],t.audioStsd(e))},t.ac3=function(e){return t.box(t.types["ac-3"],t.audioStsd(e),t.box(t.types.dac3,e.config))},t.stsd=function(e){return"audio"===e.type?"mp3"===e.segmentCodec&&"mp3"===e.codec?t.box(t.types.stsd,t.STSD,t.mp3(e)):"ac3"===e.segmentCodec?t.box(t.types.stsd,t.STSD,t.ac3(e)):t.box(t.types.stsd,t.STSD,t.mp4a(e)):t.box(t.types.stsd,t.STSD,t.avc1(e))},t.tkhd=function(e){var r=e.id,i=e.duration*e.timescale,n=e.width,a=e.height,s=Math.floor(i/(fn+1)),o=Math.floor(i%(fn+1));return t.box(t.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,r>>24&255,r>>16&255,r>>8&255,255&r,0,0,0,0,s>>24,s>>16&255,s>>8&255,255&s,o>>24,o>>16&255,o>>8&255,255&o,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,n>>8&255,255&n,0,0,a>>8&255,255&a,0,0]))},t.traf=function(e,r){var i=t.sdtp(e),n=e.id,a=Math.floor(r/(fn+1)),s=Math.floor(r%(fn+1));return t.box(t.types.traf,t.box(t.types.tfhd,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),t.box(t.types.tfdt,new Uint8Array([1,0,0,0,a>>24,a>>16&255,a>>8&255,255&a,s>>24,s>>16&255,s>>8&255,255&s])),t.trun(e,i.length+16+20+8+16+8+8),i)},t.trak=function(e){return e.duration=e.duration||4294967295,t.box(t.types.trak,t.tkhd(e),t.mdia(e))},t.trex=function(e){var r=e.id;return t.box(t.types.trex,new Uint8Array([0,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))},t.trun=function(e,r){var i,n,a,s,o,l,u=e.samples||[],h=u.length,d=12+16*h,c=new Uint8Array(d);for(r+=8+d,c.set(["video"===e.type?1:0,0,15,1,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r],0),i=0;i>>24&255,a>>>16&255,a>>>8&255,255&a,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o.isLeading<<2|o.dependsOn,o.isDependedOn<<6|o.hasRedundancy<<4|o.paddingValue<<1|o.isNonSync,61440&o.degradPrio,15&o.degradPrio,l>>>24&255,l>>>16&255,l>>>8&255,255&l],12+16*i);return t.box(t.types.trun,c)},t.initSegment=function(e){t.types||t.init();var r=t.moov(e);return Gt(t.FTYP,r)},t}();gn.types=void 0,gn.HDLR_TYPES=void 0,gn.STTS=void 0,gn.STSC=void 0,gn.STCO=void 0,gn.STSZ=void 0,gn.VMHD=void 0,gn.SMHD=void 0,gn.STSD=void 0,gn.FTYP=void 0,gn.DINF=void 0;var vn=9e4;function mn(t,e,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var n=t*e*r;return i?Math.round(n):n}function pn(t,e){return void 0===e&&(e=!1),mn(t,1e3,1/vn,e)}var yn=null,En=null,Tn=function(){function t(t,e,r,i){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.videoTrackConfig=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.ISGenerated=!1,null===yn){var n=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);yn=n?parseInt(n[1]):0}if(null===En){var a=navigator.userAgent.match(/Safari\/(\d+)/i);En=a?parseInt(a[1]):0}}var e=t.prototype;return e.destroy=function(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null},e.resetTimeStamp=function(t){w.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t},e.resetNextTimestamp=function(){w.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1},e.resetInitSegment=function(){w.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0},e.getVideoStartPts=function(t){var e=!1,r=t.reduce((function(t,r){var i=r.pts-t;return i<-4294967296?(e=!0,Sn(t,r.pts)):i>0?t:r.pts}),t[0].pts);return e&&w.debug("PTS rollover detected"),r},e.remux=function(t,e,r,i,n,a,s,o){var l,u,h,d,c,f,g=n,v=n,m=t.pid>-1,p=e.pid>-1,y=e.samples.length,E=t.samples.length>0,T=s&&y>0||y>1;if((!m||E)&&(!p||T)||this.ISGenerated||s){if(this.ISGenerated){var S,L,A,R,k=this.videoTrackConfig;!k||e.width===k.width&&e.height===k.height&&(null==(S=e.pixelRatio)?void 0:S[0])===(null==(L=k.pixelRatio)?void 0:L[0])&&(null==(A=e.pixelRatio)?void 0:A[1])===(null==(R=k.pixelRatio)?void 0:R[1])||this.resetInitSegment()}else h=this.generateIS(t,e,n,a);var b,D=this.isVideoContiguous,I=-1;if(T&&(I=function(t){for(var e=0;e0){w.warn("[mp4-remuxer]: Dropped "+I+" out of "+y+" video samples due to a missing keyframe");var C=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(I),e.dropped+=I,b=v+=(e.samples[0].pts-C)/e.inputTimeScale}else-1===I&&(w.warn("[mp4-remuxer]: No keyframe found out of "+y+" video samples"),f=!1);if(this.ISGenerated){if(E&&T){var _=this.getVideoStartPts(e.samples),x=(Sn(t.samples[0].pts,_)-_)/e.inputTimeScale;g+=Math.max(0,x),v+=Math.max(0,-x)}if(E){if(t.samplerate||(w.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),h=this.generateIS(t,e,n,a)),u=this.remuxAudio(t,g,this.isAudioContiguous,a,p||T||o===we?v:void 0),T){var P=u?u.endPTS-u.startPTS:0;e.inputTimeScale||(w.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),h=this.generateIS(t,e,n,a)),l=this.remuxVideo(e,v,D,P)}}else T&&(l=this.remuxVideo(e,v,D,0));l&&(l.firstKeyFrame=I,l.independent=-1!==I,l.firstKeyFramePTS=b)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(r.samples.length&&(c=Ln(r,n,this._initPTS,this._initDTS)),i.samples.length&&(d=An(i,n,this._initPTS))),{audio:u,video:l,initSegment:h,independent:f,text:d,id3:c}},e.generateIS=function(t,e,r,i){var n,a,s,o=t.samples,l=e.samples,u=this.typeSupported,h={},d=this._initPTS,c=!d||i,f="audio/mp4";if(c&&(n=a=1/0),t.config&&o.length){switch(t.timescale=t.samplerate,t.segmentCodec){case"mp3":u.mpeg?(f="audio/mpeg",t.codec=""):u.mp3&&(t.codec="mp3");break;case"ac3":t.codec="ac-3"}h.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&u.mpeg?new Uint8Array(0):gn.initSegment([t]),metadata:{channelCount:t.channelCount}},c&&(s=t.inputTimeScale,d&&s===d.timescale?c=!1:n=a=o[0].pts-Math.round(s*r))}if(e.sps&&e.pps&&l.length){if(e.timescale=e.inputTimeScale,h.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:gn.initSegment([e]),metadata:{width:e.width,height:e.height}},c)if(s=e.inputTimeScale,d&&s===d.timescale)c=!1;else{var g=this.getVideoStartPts(l),v=Math.round(s*r);a=Math.min(a,Sn(l[0].dts,g)-v),n=Math.min(n,g-v)}this.videoTrackConfig={width:e.width,height:e.height,pixelRatio:e.pixelRatio}}if(Object.keys(h).length)return this.ISGenerated=!0,c?(this._initPTS={baseTime:n,timescale:s},this._initDTS={baseTime:a,timescale:s}):n=s=void 0,{tracks:h,initPTS:n,timescale:s}},e.remuxVideo=function(t,e,r,i){var n,a,s=t.inputTimeScale,l=t.samples,u=[],h=l.length,d=this._initPTS,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,v=Number.POSITIVE_INFINITY,m=Number.NEGATIVE_INFINITY,p=!1;if(!r||null===c){var y=e*s,E=l[0].pts-Sn(l[0].dts,l[0].pts);yn&&null!==c&&Math.abs(y-E-c)<15e3?r=!0:c=y-E}for(var T=d.baseTime*s/d.timescale,R=0;R0?R-1:R].dts&&(p=!0)}p&&l.sort((function(t,e){var r=t.dts-e.dts,i=t.pts-e.pts;return r||i})),n=l[0].dts;var b=(a=l[l.length-1].dts)-n,D=b?Math.round(b/(h-1)):g||t.inputTimeScale/30;if(r){var I=n-c,C=I>D,_=I<-1;if((C||_)&&(C?w.warn("AVC: "+pn(I,!0)+" ms ("+I+"dts) hole between fragments detected at "+e.toFixed(3)):w.warn("AVC: "+pn(-I,!0)+" ms ("+I+"dts) overlapping between fragments detected at "+e.toFixed(3)),!_||c>=l[0].pts||yn)){n=c;var x=l[0].pts-I;if(C)l[0].dts=n,l[0].pts=x;else for(var P=0;Px);P++)l[P].dts-=I,l[P].pts-=I;w.log("Video: Initial PTS/DTS adjusted: "+pn(x,!0)+"/"+pn(n,!0)+", delta: "+pn(I,!0)+" ms")}}for(var F=0,M=0,O=n=Math.max(0,n),N=0;N0?$.dts-l[J-1].dts:D;if(st=J>0?$.pts-l[J-1].pts:D,ot.stretchShortVideoTrack&&null!==this.nextAudioPts){var ut=Math.floor(ot.maxBufferHole*s),ht=(i?v+i*s:this.nextAudioPts)-$.pts;ht>ut?((g=ht-lt)<0?g=lt:j=!0,w.log("[mp4-remuxer]: It is approximately "+ht/90+" ms to the next segment; using duration "+g/90+" ms for the last video frame.")):g=lt}else g=lt}var dt=Math.round($.pts-$.dts);q=Math.min(q,g),z=Math.max(z,g),X=Math.min(X,st),Q=Math.max(Q,st),u.push(new kn($.key,g,tt,dt))}if(u.length)if(yn){if(yn<70){var ct=u[0].flags;ct.dependsOn=2,ct.isNonSync=0}}else if(En&&Q-X0&&(i&&Math.abs(p-m)<9e3||Math.abs(Sn(g[0].pts-y,p)-m)<20*u),g.forEach((function(t){t.pts=Sn(t.pts-y,p)})),!r||m<0){if(g=g.filter((function(t){return t.pts>=0})),!g.length)return;m=0===n?0:i&&!f?Math.max(0,p):g[0].pts}if("aac"===t.segmentCodec)for(var E=this.config.maxAudioFramesDrift,T=0,R=m;T=E*u&&I<1e4&&f){var C=Math.round(D/u);(R=b-C*u)<0&&(C--,R+=u),0===T&&(this.nextAudioPts=m=R),w.warn("[mp4-remuxer]: Injecting "+C+" audio frame @ "+(R/a).toFixed(3)+"s due to "+Math.round(1e3*D/a)+" ms gap.");for(var _=0;_0))return;N+=v;try{F=new Uint8Array(N)}catch(t){return void this.observer.emit(S.ERROR,S.ERROR,{type:L.MUX_ERROR,details:A.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:N,reason:"fail allocating audio mdat "+N})}d||(new DataView(F.buffer).setUint32(0,N),F.set(gn.types.mdat,4))}F.set(H,v);var Y=H.byteLength;v+=Y,c.push(new kn(!0,l,Y,0)),O=V}var W=c.length;if(W){var j=c[c.length-1];this.nextAudioPts=m=O+s*j.duration;var q=d?new Uint8Array(0):gn.moof(t.sequenceNumber++,M/s,o({},t,{samples:c}));t.samples=[];var X=M/a,z=m/a,Q={data1:q,data2:F,startPTS:X,endPTS:z,startDTS:X,endDTS:z,type:"audio",hasAudio:!0,hasVideo:!1,nb:W};return this.isAudioContiguous=!0,Q}},e.remuxEmptyAudio=function(t,e,r,i){var n=t.inputTimeScale,a=n/(t.samplerate?t.samplerate:n),s=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,u=(null!==s?s:i.startDTS*n)+l,h=i.endDTS*n+l,d=1024*a,c=Math.ceil((h-u)/d),f=cn.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(w.warn("[mp4-remuxer]: remux empty Audio"),f){for(var g=[],v=0;v4294967296;)t+=r;return t}function Ln(t,e,r,i){var n=t.samples.length;if(n){for(var a=t.inputTimeScale,s=0;s0;n||(i=_t(e,["encv"])),i.forEach((function(t){_t(n?t.subarray(28):t.subarray(78),["sinf"]).forEach((function(t){var e=Ut(t);if(e){var i=e.subarray(8,24);i.some((function(t){return 0!==t}))||(w.log("[eme] Patching keyId in 'enc"+(n?"a":"v")+">sinf>>tenc' box: "+Lt(i)+" -> "+Lt(r)),e.set(r,8))}}))}))})),t}(t,i)),this.emitInitSegment=!0},e.generateInitSegment=function(t){var e=this.audioCodec,r=this.videoCodec;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);var i=this.initData=Pt(t);i.audio&&(e=Dn(i.audio,O)),i.video&&(r=Dn(i.video,N));var n={};i.audio&&i.video?n.audiovideo={container:"video/mp4",codec:e+","+r,initSegment:t,id:"main"}:i.audio?n.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?n.video={container:"video/mp4",codec:r,initSegment:t,id:"main"}:w.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=n},e.remux=function(t,e,r,i,n,a){var s,o,l=this.initPTS,u=this.lastEndTime,h={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};y(u)||(u=this.lastEndTime=n||0);var d=e.samples;if(null==d||!d.length)return h;var c={initPTS:void 0,timescale:1},f=this.initData;if(null!=(s=f)&&s.length||(this.generateInitSegment(d),f=this.initData),null==(o=f)||!o.length)return w.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),h;this.emitInitSegment&&(c.tracks=this.initTracks,this.emitInitSegment=!1);var g=function(t,e){for(var r=0,i=0,n=0,a=_t(t,["moof","traf"]),s=0;sn}(l,m,n,g)||c.timescale!==l.timescale&&a)&&(c.initPTS=m-n,l&&1===l.timescale&&w.warn("Adjusting initPTS by "+(c.initPTS-l.baseTime)),this.initPTS=l={baseTime:c.initPTS,timescale:1});var p=t?m-l.baseTime/l.timescale:u,E=p+g;!function(t,e,r){_t(e,["moof","traf"]).forEach((function(e){_t(e,["tfhd"]).forEach((function(i){var n=It(i,4),a=t[n];if(a){var s=a.timescale||9e4;_t(e,["tfdt"]).forEach((function(t){var e=t[0],i=r*s;if(i){var n=It(t,4);if(0===e)n-=i,Ct(t,4,n=Math.max(n,0));else{n*=Math.pow(2,32),n+=It(t,8),n-=i,n=Math.max(n,0);var a=Math.floor(n/(At+1)),o=Math.floor(n%(At+1));Ct(t,4,a),Ct(t,8,o)}}}))}}))}))}(f,d,l.baseTime/l.timescale),g>0?this.lastEndTime=E:(w.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var T=!!f.audio,S=!!f.video,L="";T&&(L+="audio"),S&&(L+="video");var A={data1:d,startPTS:p,startDTS:p,endPTS:E,endDTS:E,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return h.audio="audio"===A.type?A:void 0,h.video="audio"!==A.type?A:void 0,h.initSegment=c,h.id3=Ln(r,n,l,l),i.samples.length&&(h.text=An(i,n,l)),h},t}();function Dn(t,e){var r=null==t?void 0:t.codec;if(r&&r.length>4)return r;if(e===O){if("ec-3"===r||"ac-3"===r||"alac"===r)return r;if("fLaC"===r||"Opus"===r)return ue(r,!1);var i="mp4a.40.5";return w.info('Parsed audio codec "'+r+'" or audio object type not handled. Using "'+i+'"'),i}return w.warn('Unhandled video codec "'+r+'"'),"hvc1"===r||"hev1"===r?"hvc1.1.6.L120.90":"av01"===r?"av01.0.04M.08":"avc1.42e01e"}try{Rn=self.performance.now.bind(self.performance)}catch(t){w.debug("Unable to use Performance API on this environment"),Rn=null==j?void 0:j.Date.now}var In=[{demux:zi,remux:bn},{demux:an,remux:Tn},{demux:qi,remux:Tn},{demux:dn,remux:Tn}];In.splice(2,0,{demux:Ji,remux:Tn});var wn=function(){function t(t,e,r,i,n){this.async=!1,this.observer=void 0,this.typeSupported=void 0,this.config=void 0,this.vendor=void 0,this.id=void 0,this.demuxer=void 0,this.remuxer=void 0,this.decrypter=void 0,this.probe=void 0,this.decryptionPromise=null,this.transmuxConfig=void 0,this.currentTransmuxState=void 0,this.observer=t,this.typeSupported=e,this.config=r,this.vendor=i,this.id=n}var e=t.prototype;return e.configure=function(t){this.transmuxConfig=t,this.decrypter&&this.decrypter.reset()},e.push=function(t,e,r,i){var n=this,a=r.transmuxing;a.executeStart=Rn();var s=new Uint8Array(t),o=this.currentTransmuxState,l=this.transmuxConfig;i&&(this.currentTransmuxState=i);var u=i||o,h=u.contiguous,d=u.discontinuity,c=u.trackSwitch,f=u.accurateTimeOffset,g=u.timeOffset,v=u.initSegmentChange,m=l.audioCodec,p=l.videoCodec,y=l.defaultInitPts,E=l.duration,T=l.initSegmentData,R=function(t,e){var r=null;return t.byteLength>0&&null!=(null==e?void 0:e.key)&&null!==e.iv&&null!=e.method&&(r=e),r}(s,e);if(R&&"AES-128"===R.method){var k=this.getDecrypter();if(!k.isSync())return this.decryptionPromise=k.webCryptoDecrypt(s,R.key.buffer,R.iv.buffer).then((function(t){var e=n.push(t,null,r);return n.decryptionPromise=null,e})),this.decryptionPromise;var b=k.softwareDecrypt(s,R.key.buffer,R.iv.buffer);if(r.part>-1&&(b=k.flush()),!b)return a.executeEnd=Rn(),Cn(r);s=new Uint8Array(b)}var D=this.needsProbing(d,c);if(D){var I=this.configureTransmuxer(s);if(I)return w.warn("[transmuxer] "+I.message),this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:I,reason:I.message}),a.executeEnd=Rn(),Cn(r)}(d||c||v||D)&&this.resetInitSegment(T,m,p,E,e),(d||v||D)&&this.resetInitialTimestamp(y),h||this.resetContiguity();var C=this.transmux(s,R,g,f,r),_=this.currentTransmuxState;return _.contiguous=!0,_.discontinuity=!1,_.trackSwitch=!1,a.executeEnd=Rn(),C},e.flush=function(t){var e=this,r=t.transmuxing;r.executeStart=Rn();var i=this.decrypter,n=this.currentTransmuxState,a=this.decryptionPromise;if(a)return a.then((function(){return e.flush(t)}));var s=[],o=n.timeOffset;if(i){var l=i.flush();l&&s.push(this.push(l,null,t))}var u=this.demuxer,h=this.remuxer;if(!u||!h)return r.executeEnd=Rn(),[Cn(t)];var d=u.flush(o);return _n(d)?d.then((function(r){return e.flushRemux(s,r,t),s})):(this.flushRemux(s,d,t),s)},e.flushRemux=function(t,e,r){var i=e.audioTrack,n=e.videoTrack,a=e.id3Track,s=e.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;w.log("[transmuxer.ts]: Flushed fragment "+r.sn+(r.part>-1?" p: "+r.part:"")+" of level "+r.level);var h=this.remuxer.remux(i,n,a,s,u,l,!0,this.id);t.push({remuxResult:h,chunkMeta:r}),r.transmuxing.executeEnd=Rn()},e.resetInitialTimestamp=function(t){var e=this.demuxer,r=this.remuxer;e&&r&&(e.resetTimeStamp(t),r.resetTimeStamp(t))},e.resetContiguity=function(){var t=this.demuxer,e=this.remuxer;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())},e.resetInitSegment=function(t,e,r,i,n){var a=this.demuxer,s=this.remuxer;a&&s&&(a.resetInitSegment(t,e,r,i),s.resetInitSegment(t,e,r,n))},e.destroy=function(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)},e.transmux=function(t,e,r,i,n){return e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,r,i,n):this.transmuxUnencrypted(t,r,i,n)},e.transmuxUnencrypted=function(t,e,r,i){var n=this.demuxer.demux(t,e,!1,!this.config.progressive),a=n.audioTrack,s=n.videoTrack,o=n.id3Track,l=n.textTrack;return{remuxResult:this.remuxer.remux(a,s,o,l,e,r,!1,this.id),chunkMeta:i}},e.transmuxSampleAes=function(t,e,r,i,n){var a=this;return this.demuxer.demuxSampleAes(t,e,r).then((function(t){return{remuxResult:a.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,r,i,!1,a.id),chunkMeta:n}}))},e.configureTransmuxer=function(t){for(var e,r=this.config,i=this.observer,n=this.typeSupported,a=this.vendor,s=0,o=In.length;s1&&l.id===(null==m?void 0:m.stats.chunkCount),L=!y&&(1===E||0===E&&(1===T||S&&T<=0)),A=self.performance.now();(y||E||0===n.stats.parsing.start)&&(n.stats.parsing.start=A),!a||!T&&L||(a.stats.parsing.start=A);var R=!(m&&(null==(h=n.initSegment)?void 0:h.url)===(null==(d=m.initSegment)?void 0:d.url)),k=new Pn(p,L,o,y,g,R);if(!L||p||R){w.log("[transmuxer-interface, "+n.type+"]: Starting new transmux session for sn: "+l.sn+" p: "+l.part+" level: "+l.level+" id: "+l.id+"\n discontinuity: "+p+"\n trackSwitch: "+y+"\n contiguous: "+L+"\n accurateTimeOffset: "+o+"\n timeOffset: "+g+"\n initSegmentChange: "+R);var b=new xn(r,i,e,s,u);this.configureTransmuxer(b)}if(this.frag=n,this.part=a,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:v,chunkMeta:l,state:k},t instanceof ArrayBuffer?[t]:[]);else if(f){var D=f.push(t,v,l,k);_n(D)?(f.async=!0,D.then((function(t){c.handleTransmuxComplete(t)})).catch((function(t){c.transmuxerError(t,l,"transmuxer-interface push error")}))):(f.async=!1,this.handleTransmuxComplete(D))}},r.flush=function(t){var e=this;t.transmuxing.start=self.performance.now();var r=this.transmuxer;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(r){var i=r.flush(t);_n(i)||r.async?(_n(i)||(i=Promise.resolve(i)),i.then((function(r){e.handleFlushResult(r,t)})).catch((function(r){e.transmuxerError(r,t,"transmuxer-interface flush error")}))):this.handleFlushResult(i,t)}},r.transmuxerError=function(t,e,r){this.hls&&(this.error=t,this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,chunkMeta:e,fatal:!1,error:t,err:t,reason:r}))},r.handleFlushResult=function(t,e){var r=this;t.forEach((function(t){r.handleTransmuxComplete(t)})),this.onFlush(e)},r.onWorkerMessage=function(t){var e=t.data,r=this.hls;switch(e.event){case"init":var i,n=null==(i=this.workerContext)?void 0:i.objectURL;n&&self.URL.revokeObjectURL(n);break;case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":w[e.data.logType]&&w[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,r.trigger(e.event,e.data)}},r.configureTransmuxer=function(t){var e=this.transmuxer;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)},r.handleTransmuxComplete=function(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)},e}();function Gn(t,e){if(t.length!==e.length)return!1;for(var r=0;r0&&-1===t?(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e,this.state=fi):(this.loadedmetadata=!1,this.state=pi),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()},r.doTick=function(){switch(this.state){case fi:this.doTickIdle();break;case pi:var e,r=this.levels,i=this.trackId,n=null==r||null==(e=r[i])?void 0:e.details;if(n){if(this.waitForCdnTuneIn(n))break;this.state=Li}break;case mi:var a,s=performance.now(),o=this.retryDate;if(!o||s>=o||null!=(a=this.media)&&a.seeking){var l=this.levels,u=this.trackId;this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded((null==l?void 0:l[u])||null),this.state=fi}break;case Li:var h=this.waitingData;if(h){var d=h.frag,c=h.part,f=h.cache,g=h.complete;if(void 0!==this.initPTS[d.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=vi;var v={frag:d,part:c,payload:f.flush(),networkDetails:null};this._handleFragmentLoadProgress(v),g&&t.prototype._handleFragmentLoadComplete.call(this,v)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log("Waiting fragment cc ("+d.cc+") cancelled because video is at cc "+this.videoTrackCC),this.clearWaitingFragment();else{var m=this.getLoadPosition(),p=zr.bufferInfo(this.mediaBuffer,m,this.config.maxBufferHole);pr(p.end,this.config.maxFragLookUpTolerance,d)<0&&(this.log("Waiting fragment cc ("+d.cc+") @ "+d.start+" cancelled because another fragment at "+p.end+" is needed"),this.clearWaitingFragment())}}else this.state=fi}this.onTickEnd()},r.clearWaitingFragment=function(){var t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=fi)},r.resetLoadingState=function(){this.clearWaitingFragment(),t.prototype.resetLoadingState.call(this)},r.onTickEnd=function(){var t=this.media;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)},r.doTickIdle=function(){var t=this.hls,e=this.levels,r=this.media,i=this.trackId,n=t.config;if((r||!this.startFragRequested&&n.startFragPrefetch)&&null!=e&&e[i]){var a=e[i],s=a.details;if(!s||s.live&&this.levelLastLoaded!==a||this.waitForCdnTuneIn(s))this.state=pi;else{var o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,O,we));var l=this.getFwdBufferInfo(o,we);if(null!==l){var u=this.bufferedTrack,h=this.switchingTrack;if(!h&&this._streamEnded(l,s))return t.trigger(S.BUFFER_EOS,{type:"audio"}),void(this.state=Ti);var d=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,Ie),c=l.len,f=this.getMaxBufferLength(null==d?void 0:d.len),g=s.fragments,v=g[0].start,m=this.flushing?this.getLoadPosition():l.end;if(h&&r){var p=this.getLoadPosition();u&&!Kn(h.attrs,u.attrs)&&(m=p),s.PTSKnown&&pv||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),r.currentTime=v+.05)}if(!(c>=f&&!h&&md.end+s.targetduration;if(T||(null==d||!d.len)&&l.len){var L=this.getAppendedFrag(y.start,Ie);if(null===L)return;if(E||(E=!!L.gap||!!T&&0===d.len),T&&!E||E&&l.nextStart&&l.nextStart-1)n=a[o];else{var l=Mr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={audioTracks:a};this.log("Updating audio tracks, "+a.length+" track(s) found in group(s): "+(null==r?void 0:r.join(","))),this.hls.trigger(S.AUDIO_TRACKS_UPDATED,h);var d=this.trackId;if(-1!==u&&-1===d)this.setAudioTrack(u);else if(a.length&&-1===d){var c,f=new Error("No audio track selected for current audio group-ID(s): "+(null==(c=this.groupIds)?void 0:c.join(","))+" track count: "+a.length);this.warn(f.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:f})}}else this.shouldReloadPlaylist(n)&&this.setAudioTrack(this.trackId)}},r.onError=function(t,e){!e.fatal&&e.context&&(e.context.type!==be||e.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(e.context.groupId)||(this.requestScheduled=-1,this.checkRetry(e)))},r.setAudioOption=function(t){var e=this.hls;if(e.config.audioPreference=t,t){var r=this.allAudioTracks;if(this.selectDefaultTrack=!1,r.length){var i=this.currentTrack;if(i&&Or(t,i,Nr))return i;var n=Mr(t,this.tracksInGroup,Nr);if(n>-1){var a=this.tracksInGroup[n];return this.setAudioTrack(n),a}if(i){var s=e.loadLevel;-1===s&&(s=e.firstAutoLevel);var o=function(t,e,r,i,n){var a=e[i],s=e.reduce((function(t,e,r){var i=e.uri;return(t[i]||(t[i]=[])).push(r),t}),{})[a.uri];s.length>1&&(i=Math.max.apply(Math,s));var o=a.videoRange,l=a.frameRate,u=a.codecSet.substring(0,4),h=Ur(e,i,(function(e){if(e.videoRange!==o||e.frameRate!==l||e.codecSet.substring(0,4)!==u)return!1;var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Mr(t,a,n)>-1}));return h>-1?h:Ur(e,i,(function(e){var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Mr(t,a,n)>-1}))}(t,e.levels,r,s,Nr);if(-1===o)return null;e.nextLoadLevel=o}if(t.channels||t.audioCodec){var l=Mr(t,r);if(l>-1)return r[l]}}}return null},r.setAudioTrack=function(t){var e=this.tracksInGroup;if(t<0||t>=e.length)this.warn("Invalid audio track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,n=e[t],a=n.details&&!n.details.live;if(!(t===this.trackId&&n===r&&a||(this.log("Switching to audio-track "+t+' "'+n.name+'" lang:'+n.lang+" group:"+n.groupId+" channels:"+n.channels),this.trackId=t,this.currentTrack=n,this.hls.trigger(S.AUDIO_TRACK_SWITCHING,i({},n)),a))){var s=this.switchParams(n.url,null==r?void 0:r.details);this.loadPlaylist(s)}}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=0;r=n[o].start&&s<=n[o].end){a=n[o];break}var l=r.start+r.duration;a?a.end=l:(a={start:s,end:l},n.push(a)),this.fragmentTracker.fragBuffered(r),this.fragBufferedComplete(r,null)}}},r.onBufferFlushing=function(t,e){var r=e.startOffset,i=e.endOffset;if(0===r&&i!==Number.POSITIVE_INFINITY){var n=i-1;if(n<=0)return;e.endOffsetSubtitles=Math.max(0,n),this.tracksBuffered.forEach((function(t){for(var e=0;e=n.length||s!==i)&&o){this.log("Subtitle track "+s+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+",duration:"+a.totalduration),this.mediaBuffer=this.mediaBufferTimeRanges;var l=0;if(a.live||null!=(r=o.details)&&r.live){var u=this.mainDetails;if(a.deltaUpdateFailed||!u)return;var h,d=u.fragments[0];o.details?0===(l=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details))&&d&&sr(a,l=d.start):a.hasProgramDateTime&&u.hasProgramDateTime?(ei(a,u),l=a.fragments[0].start):d&&sr(a,l=d.start)}o.details=a,this.levelLastLoaded=o,this.startFragRequested||!this.mainDetails&&a.live||this.setStartPosition(o.details,l),this.tick(),a.live&&!this.fragCurrent&&this.media&&this.state===fi&&(mr(null,a.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0))}}else this.warn("Subtitle tracks were reset while loading level "+s)},r._handleFragmentLoadComplete=function(t){var e=this,r=t.frag,i=t.payload,n=r.decryptdata,a=this.hls;if(!this.fragContextChanged(r)&&i&&i.byteLength>0&&null!=n&&n.key&&n.iv&&"AES-128"===n.method){var s=performance.now();this.decrypter.decrypt(new Uint8Array(i),n.key.buffer,n.iv.buffer).catch((function(t){throw a.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:r}),t})).then((function(t){var e=performance.now();a.trigger(S.FRAG_DECRYPTED,{frag:r,payload:t,stats:{tstart:s,tdecrypt:e}})})).catch((function(t){e.warn(t.name+": "+t.message),e.state=fi}))}},r.doTick=function(){if(this.media){if(this.state===fi){var t=this.currentTrackId,e=this.levels,r=null==e?void 0:e[t];if(!r||!e.length||!r.details)return;var i=this.config,n=this.getLoadPosition(),a=zr.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],n,i.maxBufferHole),s=a.end,o=a.len,l=this.getFwdBufferInfo(this.media,Ie),u=r.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+u.levelTargetDuration)return;var h=u.fragments,d=h.length,c=u.edge,f=null,g=this.fragPrevious;if(sc-v?0:v;!(f=mr(g,h,Math.max(h[0].start,s),m))&&g&&g.start>>=0)>i-1)throw new DOMException("Failed to execute '"+e+"' on 'TimeRanges': The index provided ("+r+") is greater than the maximum bound ("+i+")");return t[r][e]};this.buffered={get length(){return t.length},end:function(r){return e("end",r,t.length)},start:function(r){return e("start",r,t.length)}}},qn=function(t){function e(e){var r;return(r=t.call(this,e,"[subtitle-track-controller]")||this).media=null,r.tracks=[],r.groupIds=null,r.tracksInGroup=[],r.trackId=-1,r.currentTrack=null,r.selectDefaultTrack=!0,r.queuedDefaultTrack=-1,r.asyncPollTrackChange=function(){return r.pollTrackChange(0)},r.useTextTrackPolling=!1,r.subtitlePollingInterval=-1,r._subtitleDisplay=!0,r.onTextTracksChanged=function(){if(r.useTextTrackPolling||self.clearInterval(r.subtitlePollingInterval),r.media&&r.hls.config.renderTextTracksNatively){for(var t=null,e=Ue(r.media.textTracks),i=0;i-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))},r.pollTrackChange=function(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,t)},r.onMediaDetaching=function(){this.media&&(self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId),Ue(this.media.textTracks).forEach((function(t){Oe(t)})),this.subtitleTrack=-1,this.media=null)},r.onManifestLoading=function(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0},r.onManifestParsed=function(t,e){this.tracks=e.subtitleTracks},r.onSubtitleTrackLoaded=function(t,e){var r=e.id,i=e.groupId,n=e.details,a=this.tracksInGroup[r];if(a&&a.groupId===i){var s=a.details;a.details=e.details,this.log("Subtitle track "+r+' "'+a.name+'" lang:'+a.lang+" group:"+i+" loaded ["+n.startSN+"-"+n.endSN+"]"),r===this.trackId&&this.playlistLoaded(r,e,s)}else this.warn("Subtitle track with id:"+r+" and group:"+i+" not found in active group "+(null==a?void 0:a.groupId))},r.onLevelLoading=function(t,e){this.switchLevel(e.level)},r.onLevelSwitching=function(t,e){this.switchLevel(e.level)},r.switchLevel=function(t){var e=this.hls.levels[t];if(e){var r=e.subtitleGroups||null,i=this.groupIds,n=this.currentTrack;if(!r||(null==i?void 0:i.length)!==(null==r?void 0:r.length)||null!=r&&r.some((function(t){return-1===(null==i?void 0:i.indexOf(t))}))){this.groupIds=r,this.trackId=-1,this.currentTrack=null;var a=this.tracks.filter((function(t){return!r||-1!==r.indexOf(t.groupId)}));if(a.length)this.selectDefaultTrack&&!a.some((function(t){return t.default}))&&(this.selectDefaultTrack=!1),a.forEach((function(t,e){t.id=e}));else if(!n&&!this.tracksInGroup.length)return;this.tracksInGroup=a;var s=this.hls.config.subtitlePreference;if(!n&&s){this.selectDefaultTrack=!1;var o=Mr(s,a);if(o>-1)n=a[o];else{var l=Mr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={subtitleTracks:a};this.log("Updating subtitle tracks, "+a.length+' track(s) found in "'+(null==r?void 0:r.join(","))+'" group-id'),this.hls.trigger(S.SUBTITLE_TRACKS_UPDATED,h),-1!==u&&-1===this.trackId&&this.setSubtitleTrack(u)}else this.shouldReloadPlaylist(n)&&this.setSubtitleTrack(this.trackId)}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=this.selectDefaultTrack,i=0;i-1){var n=this.tracksInGroup[i];return this.setSubtitleTrack(i),n}if(r)return null;var a=Mr(t,e);if(a>-1)return e[a]}}return null},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentTrack;if(this.shouldLoadPlaylist(r)&&r){var i=r.id,n=r.groupId,a=r.url;if(e)try{a=e.addDirectives(a)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}this.log("Loading subtitle playlist for id "+i),this.hls.trigger(S.SUBTITLE_TRACK_LOADING,{url:a,id:i,groupId:n,deliveryDirectives:e||null})}},r.toggleTrackModes=function(){var t=this.media;if(t){var e,r=Ue(t.textTracks),i=this.currentTrack;if(i&&((e=r.filter((function(t){return Hn(i,t)}))[0])||this.warn('Unable to find subtitle TextTrack with name "'+i.name+'" and language "'+i.lang+'"')),[].slice.call(r).forEach((function(t){"disabled"!==t.mode&&t!==e&&(t.mode="disabled")})),e){var n=this.subtitleDisplay?"showing":"hidden";e.mode!==n&&(e.mode=n)}}},r.setSubtitleTrack=function(t){var e=this.tracksInGroup;if(this.media)if(t<-1||t>=e.length||!y(t))this.warn("Invalid subtitle track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,i=e[t]||null;if(this.trackId=t,this.currentTrack=i,this.toggleTrackModes(),i){var n=!!i.details&&!i.details.live;if(t!==this.trackId||i!==r||!n){this.log("Switching to subtitle-track "+t+(i?' "'+i.name+'" lang:'+i.lang+" group:"+i.groupId:""));var a=i.id,s=i.groupId,o=void 0===s?"":s,l=i.name,u=i.type,h=i.url;this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:a,groupId:o,name:l,type:u,url:h});var d=this.switchParams(i.url,null==r?void 0:r.details);this.loadPlaylist(d)}}else this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:t})}else this.queuedDefaultTrack=t},s(e,[{key:"subtitleDisplay",get:function(){return this._subtitleDisplay},set:function(t){this._subtitleDisplay=t,this.trackId>-1&&this.toggleTrackModes()}},{key:"allSubtitleTracks",get:function(){return this.tracks}},{key:"subtitleTracks",get:function(){return this.tracksInGroup}},{key:"subtitleTrack",get:function(){return this.trackId},set:function(t){this.selectDefaultTrack=!1,this.setSubtitleTrack(t)}}]),e}(Dr),Xn=function(){function t(t){this.buffers=void 0,this.queues={video:[],audio:[],audiovideo:[]},this.buffers=t}var e=t.prototype;return e.append=function(t,e,r){var i=this.queues[e];i.push(t),1!==i.length||r||this.executeNext(e)},e.insertAbort=function(t,e){this.queues[e].unshift(t),this.executeNext(e)},e.appendBlocker=function(t){var e,r=new Promise((function(t){e=t})),i={execute:e,onStart:function(){},onComplete:function(){},onError:function(){}};return this.append(i,t),r},e.executeNext=function(t){var e=this.queues[t];if(e.length){var r=e[0];try{r.execute()}catch(e){w.warn('[buffer-operation-queue]: Exception executing "'+t+'" SourceBuffer operation: '+e),r.onError(e);var i=this.buffers[t];null!=i&&i.updating||this.shiftAndExecuteNext(t)}}},e.shiftAndExecuteNext=function(t){this.queues[t].shift(),this.executeNext(t)},e.current=function(t){return this.queues[t][0]},t}(),zn=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/,Qn=function(){function t(t){var e=this;this.details=null,this._objectUrl=null,this.operationQueue=void 0,this.listeners=void 0,this.hls=void 0,this.bufferCodecEventsExpected=0,this._bufferCodecEventsTotal=0,this.media=null,this.mediaSource=null,this.lastMpegAudioChunk=null,this.appendSource=void 0,this.appendErrors={audio:0,video:0,audiovideo:0},this.tracks={},this.pendingTracks={},this.sourceBuffer=void 0,this.log=void 0,this.warn=void 0,this.error=void 0,this._onEndStreaming=function(t){e.hls&&e.hls.pauseBuffering()},this._onStartStreaming=function(t){e.hls&&e.hls.resumeBuffering()},this._onMediaSourceOpen=function(){var t=e.media,r=e.mediaSource;e.log("Media source opened"),t&&(t.removeEventListener("emptied",e._onMediaEmptied),e.updateMediaElementDuration(),e.hls.trigger(S.MEDIA_ATTACHED,{media:t,mediaSource:r})),r&&r.removeEventListener("sourceopen",e._onMediaSourceOpen),e.checkPendingTracks()},this._onMediaSourceClose=function(){e.log("Media source closed")},this._onMediaSourceEnded=function(){e.log("Media source ended")},this._onMediaEmptied=function(){var t=e.mediaSrc,r=e._objectUrl;t!==r&&w.error("Media element src was set while attaching MediaSource ("+r+" > "+t+")")},this.hls=t;var r="[buffer-controller]";this.appendSource=t.config.preferManagedMediaSource,this.log=w.log.bind(w,r),this.warn=w.warn.bind(w,r),this.error=w.error.bind(w,r),this._initSourceBuffer(),this.registerListeners()}var e=t.prototype;return e.hasSourceTypes=function(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0},e.destroy=function(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null,this.hls=null},e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.BUFFER_RESET,this.onBufferReset,this),t.on(S.BUFFER_APPENDING,this.onBufferAppending,this),t.on(S.BUFFER_CODECS,this.onBufferCodecs,this),t.on(S.BUFFER_EOS,this.onBufferEos,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(S.FRAG_PARSED,this.onFragParsed,this),t.on(S.FRAG_CHANGED,this.onFragChanged,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.BUFFER_RESET,this.onBufferReset,this),t.off(S.BUFFER_APPENDING,this.onBufferAppending,this),t.off(S.BUFFER_CODECS,this.onBufferCodecs,this),t.off(S.BUFFER_EOS,this.onBufferEos,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(S.FRAG_PARSED,this.onFragParsed,this),t.off(S.FRAG_CHANGED,this.onFragChanged,this)},e._initSourceBuffer=function(){this.sourceBuffer={},this.operationQueue=new Xn(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.appendErrors={audio:0,video:0,audiovideo:0},this.lastMpegAudioChunk=null},e.onManifestLoading=function(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null},e.onManifestParsed=function(t,e){var r=2;(e.audio&&!e.video||!e.altAudio)&&(r=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=r,this.log(this.bufferCodecEventsExpected+" bufferCodec event(s) expected")},e.onMediaAttaching=function(t,e){var r=this.media=e.media,i=te(this.appendSource);if(r&&i){var n,a=this.mediaSource=new i;this.log("created media source: "+(null==(n=a.constructor)?void 0:n.name)),a.addEventListener("sourceopen",this._onMediaSourceOpen),a.addEventListener("sourceended",this._onMediaSourceEnded),a.addEventListener("sourceclose",this._onMediaSourceClose),a.addEventListener("startstreaming",this._onStartStreaming),a.addEventListener("endstreaming",this._onEndStreaming);var s=this._objectUrl=self.URL.createObjectURL(a);if(this.appendSource)try{r.removeAttribute("src");var o=self.ManagedMediaSource;r.disableRemotePlayback=r.disableRemotePlayback||o&&a instanceof o,Jn(r),function(t,e){var r=self.document.createElement("source");r.type="video/mp4",r.src=e,t.appendChild(r)}(r,s),r.load()}catch(t){r.src=s}else r.src=s;r.addEventListener("emptied",this._onMediaEmptied)}},e.onMediaDetaching=function(){var t=this.media,e=this.mediaSource,r=this._objectUrl;if(e){if(this.log("media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){this.warn("onMediaDetaching: "+t.message+" while calling endOfStream")}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),e.removeEventListener("startstreaming",this._onStartStreaming),e.removeEventListener("endstreaming",this._onEndStreaming),t&&(t.removeEventListener("emptied",this._onMediaEmptied),r&&self.URL.revokeObjectURL(r),this.mediaSrc===r?(t.removeAttribute("src"),this.appendSource&&Jn(t),t.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(S.MEDIA_DETACHED,void 0)},e.onBufferReset=function(){var t=this;this.getSourceBufferTypes().forEach((function(e){t.resetBuffer(e)})),this._initSourceBuffer()},e.resetBuffer=function(t){var e=this.sourceBuffer[t];try{var r;e&&(this.removeBufferListeners(t),this.sourceBuffer[t]=void 0,null!=(r=this.mediaSource)&&r.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(e))}catch(e){this.warn("onBufferReset "+t,e)}},e.onBufferCodecs=function(t,e){var r=this,i=this.getSourceBufferTypes().length,n=Object.keys(e);if(n.forEach((function(t){if(i){var n=r.tracks[t];if(n&&"function"==typeof n.buffer.changeType){var a,s=e[t],o=s.id,l=s.codec,u=s.levelCodec,h=s.container,d=s.metadata,c=he(n.codec,n.levelCodec),f=null==c?void 0:c.replace(zn,"$1"),g=he(l,u),v=null==(a=g)?void 0:a.replace(zn,"$1");if(g&&f!==v){"audio"===t.slice(0,5)&&(g=ue(g,r.hls.config.preferManagedMediaSource));var m=h+";codecs="+g;r.appendChangeType(t,m),r.log("switching codec "+c+" to "+g),r.tracks[t]={buffer:n.buffer,codec:l,container:h,levelCodec:u,metadata:d,id:o}}}}else r.pendingTracks[t]=e[t]})),!i){var a=Math.max(this.bufferCodecEventsExpected-1,0);this.bufferCodecEventsExpected!==a&&(this.log(a+" bufferCodec event(s) expected "+n.join(",")),this.bufferCodecEventsExpected=a),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks()}},e.appendChangeType=function(t,e){var r=this,i=this.operationQueue,n={execute:function(){var n=r.sourceBuffer[t];n&&(r.log("changing "+t+" sourceBuffer type to "+e),n.changeType(e)),i.shiftAndExecuteNext(t)},onStart:function(){},onComplete:function(){},onError:function(e){r.warn("Failed to change "+t+" SourceBuffer type",e)}};i.append(n,t,!!this.pendingTracks[t])},e.onBufferAppending=function(t,e){var r=this,i=this.hls,n=this.operationQueue,a=this.tracks,s=e.data,o=e.type,l=e.frag,u=e.part,h=e.chunkMeta,d=h.buffering[o],c=self.performance.now();d.start=c;var f=l.stats.buffering,g=u?u.stats.buffering:null;0===f.start&&(f.start=c),g&&0===g.start&&(g.start=c);var v=a.audio,m=!1;"audio"===o&&"audio/mpeg"===(null==v?void 0:v.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);var p=l.start,y={execute:function(){if(d.executeStart=self.performance.now(),m){var t=r.sourceBuffer[o];if(t){var e=p-t.timestampOffset;Math.abs(e)>=.1&&(r.log("Updating audio SourceBuffer timestampOffset to "+p+" (delta: "+e+") sn: "+l.sn+")"),t.timestampOffset=p)}}r.appendExecutor(s,o)},onStart:function(){},onComplete:function(){var t=self.performance.now();d.executeEnd=d.end=t,0===f.first&&(f.first=t),g&&0===g.first&&(g.first=t);var e=r.sourceBuffer,i={};for(var n in e)i[n]=zr.getBuffered(e[n]);r.appendErrors[o]=0,"audio"===o||"video"===o?r.appendErrors.audiovideo=0:(r.appendErrors.audio=0,r.appendErrors.video=0),r.hls.trigger(S.BUFFER_APPENDED,{type:o,frag:l,part:u,chunkMeta:h,parent:l.type,timeRanges:i})},onError:function(t){var e={type:L.MEDIA_ERROR,parent:l.type,details:A.BUFFER_APPEND_ERROR,sourceBufferName:o,frag:l,part:u,chunkMeta:h,error:t,err:t,fatal:!1};if(t.code===DOMException.QUOTA_EXCEEDED_ERR)e.details=A.BUFFER_FULL_ERROR;else{var n=++r.appendErrors[o];e.details=A.BUFFER_APPEND_ERROR,r.warn("Failed "+n+"/"+i.config.appendErrorMaxRetry+' times to append segment in "'+o+'" sourceBuffer'),n>=i.config.appendErrorMaxRetry&&(e.fatal=!0)}i.trigger(S.ERROR,e)}};n.append(y,o,!!this.pendingTracks[o])},e.onBufferFlushing=function(t,e){var r=this,i=this.operationQueue,n=function(t){return{execute:r.removeExecutor.bind(r,t,e.startOffset,e.endOffset),onStart:function(){},onComplete:function(){r.hls.trigger(S.BUFFER_FLUSHED,{type:t})},onError:function(e){r.warn("Failed to remove from "+t+" SourceBuffer",e)}}};e.type?i.append(n(e.type),e.type):this.getSourceBufferTypes().forEach((function(t){i.append(n(t),t)}))},e.onFragParsed=function(t,e){var r=this,i=e.frag,n=e.part,a=[],s=n?n.elementaryStreams:i.elementaryStreams;s[U]?a.push("audiovideo"):(s[O]&&a.push("audio"),s[N]&&a.push("video")),0===a.length&&this.warn("Fragments must have at least one ElementaryStreamType set. type: "+i.type+" level: "+i.level+" sn: "+i.sn),this.blockBuffers((function(){var t=self.performance.now();i.stats.buffering.end=t,n&&(n.stats.buffering.end=t);var e=n?n.stats:i.stats;r.hls.trigger(S.FRAG_BUFFERED,{frag:i,part:n,stats:e,id:i.type})}),a)},e.onFragChanged=function(t,e){this.trimBuffers()},e.onBufferEos=function(t,e){var r=this;this.getSourceBufferTypes().reduce((function(t,i){var n=r.sourceBuffer[i];return!n||e.type&&e.type!==i||(n.ending=!0,n.ended||(n.ended=!0,r.log(i+" sourceBuffer now EOS"))),t&&!(n&&!n.ended)}),!0)&&(this.log("Queueing mediaSource.endOfStream()"),this.blockBuffers((function(){r.getSourceBufferTypes().forEach((function(t){var e=r.sourceBuffer[t];e&&(e.ending=!1)}));var t=r.mediaSource;t&&"open"===t.readyState?(r.log("Calling mediaSource.endOfStream()"),t.endOfStream()):t&&r.log("Could not call mediaSource.endOfStream(). mediaSource.readyState: "+t.readyState)})))},e.onLevelUpdated=function(t,e){var r=e.details;r.fragments.length&&(this.details=r,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())},e.trimBuffers=function(){var t=this.hls,e=this.details,r=this.media;if(r&&null!==e&&this.getSourceBufferTypes().length){var i=t.config,n=r.currentTime,a=e.levelTargetDuration,s=e.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(y(s)&&s>0){var o=Math.max(s,a),l=Math.floor(n/a)*a-o;this.flushBackBuffer(n,a,l)}if(y(i.frontBufferFlushThreshold)&&i.frontBufferFlushThreshold>0){var u=Math.max(i.maxBufferLength,i.frontBufferFlushThreshold),h=Math.max(u,a),d=Math.floor(n/a)*a+h;this.flushFrontBuffer(n,a,d)}}},e.flushBackBuffer=function(t,e,r){var i=this,n=this.details,a=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(s){var o=a[s];if(o){var l=zr.getBuffered(o);if(l.length>0&&r>l.start(0)){if(i.hls.trigger(S.BACK_BUFFER_REACHED,{bufferEnd:r}),null!=n&&n.live)i.hls.trigger(S.LIVE_BACK_BUFFER_REACHED,{bufferEnd:r});else if(o.ended&&l.end(l.length-1)-t<2*e)return void i.log("Cannot flush "+s+" back buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:r,type:s})}}}))},e.flushFrontBuffer=function(t,e,r){var i=this,n=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(a){var s=n[a];if(s){var o=zr.getBuffered(s),l=o.length;if(l<2)return;var u=o.start(l-1),h=o.end(l-1);if(r>u||t>=u&&t<=h)return;if(s.ended&&t-h<2*e)return void i.log("Cannot flush "+a+" front buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:u,endOffset:1/0,type:a})}}))},e.updateMediaElementDuration=function(){if(this.details&&this.media&&this.mediaSource&&"open"===this.mediaSource.readyState){var t=this.details,e=this.hls,r=this.media,i=this.mediaSource,n=t.fragments[0].start+t.totalduration,a=r.duration,s=y(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(i.duration=1/0,this.updateSeekableRange(t)):(n>s&&n>a||!y(a))&&(this.log("Updating Media Source duration to "+n.toFixed(3)),i.duration=n)}},e.updateSeekableRange=function(t){var e=this.mediaSource,r=t.fragments;if(r.length&&t.live&&null!=e&&e.setLiveSeekableRange){var i=Math.max(0,r[0].start),n=Math.max(i,i+t.totalduration);this.log("Media Source duration is set to "+e.duration+". Setting seekable range to "+i+"-"+n+"."),e.setLiveSeekableRange(i,n)}},e.checkPendingTracks=function(){var t=this.bufferCodecEventsExpected,e=this.operationQueue,r=this.pendingTracks,i=Object.keys(r).length;if(i&&(!t||2===i||"audiovideo"in r)){this.createSourceBuffers(r),this.pendingTracks={};var n=this.getSourceBufferTypes();if(n.length)this.hls.trigger(S.BUFFER_CREATED,{tracks:this.tracks}),n.forEach((function(t){e.executeNext(t)}));else{var a=new Error("could not create source buffer for media codec(s)");this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:a,reason:a.message})}}},e.createSourceBuffers=function(t){var e=this,r=this.sourceBuffer,i=this.mediaSource;if(!i)throw Error("createSourceBuffers called when mediaSource was null");var n=function(n){if(!r[n]){var a=t[n];if(!a)throw Error("source buffer exists for track "+n+", however track does not");var s=a.levelCodec||a.codec;s&&"audio"===n.slice(0,5)&&(s=ue(s,e.hls.config.preferManagedMediaSource));var o=a.container+";codecs="+s;e.log("creating sourceBuffer("+o+")");try{var l=r[n]=i.addSourceBuffer(o),u=n;e.addBufferListener(u,"updatestart",e._onSBUpdateStart),e.addBufferListener(u,"updateend",e._onSBUpdateEnd),e.addBufferListener(u,"error",e._onSBUpdateError),e.addBufferListener(u,"bufferedchange",(function(t,r){var i=r.removedRanges;null!=i&&i.length&&e.hls.trigger(S.BUFFER_FLUSHED,{type:n})})),e.tracks[n]={buffer:l,codec:s,container:a.container,levelCodec:a.levelCodec,metadata:a.metadata,id:a.id}}catch(t){e.error("error while trying to add sourceBuffer: "+t.message),e.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,sourceBufferName:n,mimeType:o})}}};for(var a in t)n(a)},e._onSBUpdateStart=function(t){this.operationQueue.current(t).onStart()},e._onSBUpdateEnd=function(t){var e;if("closed"!==(null==(e=this.mediaSource)?void 0:e.readyState)){var r=this.operationQueue;r.current(t).onComplete(),r.shiftAndExecuteNext(t)}else this.resetBuffer(t)},e._onSBUpdateError=function(t,e){var r,i=new Error(t+" SourceBuffer error. MediaSource readyState: "+(null==(r=this.mediaSource)?void 0:r.readyState));this.error(""+i,e),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_APPENDING_ERROR,sourceBufferName:t,error:i,fatal:!1});var n=this.operationQueue.current(t);n&&n.onError(i)},e.removeExecutor=function(t,e,r){var i=this.media,n=this.mediaSource,a=this.operationQueue,s=this.sourceBuffer[t];if(!i||!n||!s)return this.warn("Attempting to remove from the "+t+" SourceBuffer, but it does not exist"),void a.shiftAndExecuteNext(t);var o=y(i.duration)?i.duration:1/0,l=y(n.duration)?n.duration:1/0,u=Math.max(0,e),h=Math.min(r,o,l);h>u&&(!s.ending||s.ended)?(s.ended=!1,this.log("Removing ["+u+","+h+"] from the "+t+" SourceBuffer"),s.remove(u,h)):a.shiftAndExecuteNext(t)},e.appendExecutor=function(t,e){var r=this.sourceBuffer[e];if(r)r.ended=!1,r.appendBuffer(t);else if(!this.pendingTracks[e])throw new Error("Attempting to append to the "+e+" SourceBuffer, but it does not exist")},e.blockBuffers=function(t,e){var r=this;if(void 0===e&&(e=this.getSourceBufferTypes()),!e.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);var i=this.operationQueue,n=e.map((function(t){return i.appendBlocker(t)}));Promise.all(n).then((function(){t(),e.forEach((function(t){var e=r.sourceBuffer[t];null!=e&&e.updating||i.shiftAndExecuteNext(t)}))}))},e.getSourceBufferTypes=function(){return Object.keys(this.sourceBuffer)},e.addBufferListener=function(t,e,r){var i=this.sourceBuffer[t];if(i){var n=r.bind(this,t);this.listeners[t].push({event:e,listener:n}),i.addEventListener(e,n)}},e.removeBufferListeners=function(t){var e=this.sourceBuffer[t];e&&this.listeners[t].forEach((function(t){e.removeEventListener(t.event,t.listener)}))},s(t,[{key:"mediaSrc",get:function(){var t,e=(null==(t=this.media)?void 0:t.firstChild)||this.media;return null==e?void 0:e.src}}]),t}();function Jn(t){var e=t.querySelectorAll("source");[].slice.call(e).forEach((function(e){t.removeChild(e)}))}var $n={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},Zn=function(t){var e=t;return $n.hasOwnProperty(t)&&(e=$n[t]),String.fromCharCode(e)},ta=15,ea=100,ra={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},ia={17:2,18:4,21:6,22:8,23:10,19:13,20:15},na={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},aa={25:2,26:4,29:6,30:8,31:10,27:13,28:15},sa=["white","green","blue","cyan","red","yellow","magenta","black","transparent"],oa=function(){function t(){this.time=null,this.verboseLevel=0}return t.prototype.log=function(t,e){if(this.verboseLevel>=t){var r="function"==typeof e?e():e;w.log(this.time+" ["+t+"] "+r)}},t}(),la=function(t){for(var e=[],r=0;rea&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=ea)},e.moveCursor=function(t){var e=this.pos+t;if(t>1)for(var r=this.pos+1;r=144&&this.backSpace();var r=Zn(t);this.pos>=ea?this.logger.log(0,(function(){return"Cannot insert "+t.toString(16)+" ("+r+") at position "+e.pos+". Skipping it!"})):(this.chars[this.pos].setChar(r,this.currPenState),this.moveCursor(1))},e.clearFromPos=function(t){var e;for(e=t;e0&&(r=t?"["+e.join(" | ")+"]":e.join("\n")),r},e.getTextAndFormat=function(){return this.rows},t}(),fa=function(){function t(t,e,r){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new ca(r),this.nonDisplayedMemory=new ca(r),this.lastOutputScreen=new ca(r),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=r}var e=t.prototype;return e.reset=function(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null},e.getHandler=function(){return this.outputFilter},e.setHandler=function(t){this.outputFilter=t},e.setPAC=function(t){this.writeScreen.setPAC(t)},e.setBkgData=function(t){this.writeScreen.setBkgData(t)},e.setMode=function(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(function(){return"MODE="+t})),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=t)},e.insertChars=function(t){for(var e=this,r=0;r=46,e.italics)e.foreground="white";else{var r=Math.floor(t/2)-16;e.foreground=["white","green","blue","cyan","red","yellow","magenta"][r]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)},e.outputDataUpdate=function(t){void 0===t&&(t=!1);var e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))},e.cueSplitAtTime=function(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))},t}(),ga=function(){function t(t,e,r){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;var i=this.logger=new oa;this.channels=[null,new fa(t,e,i),new fa(t+1,r,i)]}var e=t.prototype;return e.getHandler=function(t){return this.channels[t].getHandler()},e.setHandler=function(t,e){this.channels[t].setHandler(e)},e.addData=function(t,e){var r,i,n,a=!1;this.logger.time=t;for(var s=0;s ("+la([i,n])+")"),(r=this.parseCmd(i,n))||(r=this.parseMidrow(i,n)),r||(r=this.parsePAC(i,n)),r||(r=this.parseBackgroundAttributes(i,n)),!r&&(a=this.parseChars(i,n))){var o=this.currentChannel;o&&o>0?this.channels[o].insertChars(a):this.logger.log(2,"No channel found yet. TEXT-MODE?")}r||a||this.logger.log(2,"Couldn't parse cleaned data "+la([i,n])+" orig: "+la([e[s],e[s+1]]))}},e.parseCmd=function(t,e){var r=this.cmdHistory;if(!((20===t||28===t||21===t||29===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=33&&e<=35))return!1;if(ma(t,e,r))return va(null,null,r),this.logger.log(3,"Repeated command ("+la([t,e])+") is dropped"),!0;var i=20===t||21===t||23===t?1:2,n=this.channels[i];return 20===t||21===t||28===t||29===t?32===e?n.ccRCL():33===e?n.ccBS():34===e?n.ccAOF():35===e?n.ccAON():36===e?n.ccDER():37===e?n.ccRU(2):38===e?n.ccRU(3):39===e?n.ccRU(4):40===e?n.ccFON():41===e?n.ccRDC():42===e?n.ccTR():43===e?n.ccRTD():44===e?n.ccEDM():45===e?n.ccCR():46===e?n.ccENM():47===e&&n.ccEOC():n.ccTO(e-32),va(t,e,r),this.currentChannel=i,!0},e.parseMidrow=function(t,e){var r=0;if((17===t||25===t)&&e>=32&&e<=47){if((r=17===t?1:2)!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;var i=this.channels[r];return!!i&&(i.ccMIDROW(e),this.logger.log(3,"MIDROW ("+la([t,e])+")"),!0)}return!1},e.parsePAC=function(t,e){var r,i=this.cmdHistory;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127||(16===t||24===t)&&e>=64&&e<=95))return!1;if(ma(t,e,i))return va(null,null,i),!0;var n=t<=23?1:2;r=e>=64&&e<=95?1===n?ra[t]:na[t]:1===n?ia[t]:aa[t];var a=this.channels[n];return!!a&&(a.setPAC(this.interpretPAC(r,e)),va(t,e,i),this.currentChannel=n,!0)},e.interpretPAC=function(t,e){var r,i={color:null,italics:!1,indent:null,underline:!1,row:t};return r=e>95?e-96:e-64,i.underline=1==(1&r),r<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(r/2)]:r<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((r-16)/2),i},e.parseChars=function(t,e){var r,i,n=null,a=null;if(t>=25?(r=2,a=t-8):(r=1,a=t),a>=17&&a<=19?(i=17===a?e+80:18===a?e+112:e+144,this.logger.log(2,"Special char '"+Zn(i)+"' in channel "+r),n=[i]):t>=32&&t<=127&&(n=0===e?[t]:[t,e]),n){var s=la(n);this.logger.log(3,"Char codes = "+s.join(",")),va(t,e,this.cmdHistory)}return n},e.parseBackgroundAttributes=function(t,e){var r;if(!((16===t||24===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=45&&e<=47))return!1;var i={};16===t||24===t?(r=Math.floor((e-32)/2),i.background=sa[r],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));var n=t<=23?1:2;return this.channels[n].setBkgData(i),va(t,e,this.cmdHistory),!0},e.reset=function(){for(var t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=r,this.timelineController.createCaptionsTrack(this.trackName)},e.reset=function(){this.cueRanges=[],this.startTime=null},t}(),ya=function(){if(null!=j&&j.VTTCue)return self.VTTCue;var t=["","lr","rl"],e=["start","middle","end","left","right"];function r(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;var r=e.toLowerCase();return!!~t.indexOf(r)&&r}function i(t){return r(e,t)}function n(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i100)throw new Error("Position must be between 0 and 100.");E=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",n({},l,{get:function(){return T},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");T=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",n({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",n({},l,{get:function(){return L},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return a.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},a}(),Ea=function(){function t(){}return t.prototype.decode=function(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))},t}();function Ta(t){function e(t,e,r,i){return 3600*(0|t)+60*(0|e)+(0|r)+parseFloat(i||0)}var r=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return r?parseFloat(r[2])>59?e(r[2],r[3],0,r[4]):e(r[1],r[2],r[3],r[4]):null}var Sa=function(){function t(){this.values=Object.create(null)}var e=t.prototype;return e.set=function(t,e){this.get(t)||""===e||(this.values[t]=e)},e.get=function(t,e,r){return r?this.has(t)?this.values[t]:e[r]:this.has(t)?this.values[t]:e},e.has=function(t){return t in this.values},e.alt=function(t,e,r){for(var i=0;i=0&&r<=100)return this.set(t,r),!0}return!1},t}();function La(t,e,r,i){var n=i?t.split(i):[t];for(var a in n)if("string"==typeof n[a]){var s=n[a].split(r);2===s.length&&e(s[0],s[1])}}var Aa=new ya(0,0,""),Ra="middle"===Aa.align?"middle":"center";function ka(t,e,r){var i=t;function n(){var e=Ta(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function a(){t=t.replace(/^\s+/,"")}if(a(),e.startTime=n(),a(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),a(),e.endTime=n(),a(),function(t,e){var i=new Sa;La(t,(function(t,e){var n;switch(t){case"region":for(var a=r.length-1;a>=0;a--)if(r[a].id===e){i.set(t,r[a].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":n=e.split(","),i.integer(t,n[0]),i.percent(t,n[0])&&i.set("snapToLines",!1),i.alt(t,n[0],["auto"]),2===n.length&&i.alt("lineAlign",n[1],["start",Ra,"end"]);break;case"position":n=e.split(","),i.percent(t,n[0]),2===n.length&&i.alt("positionAlign",n[1],["start",Ra,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",Ra,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");var n=i.get("line","auto");"auto"===n&&-1===Aa.line&&(n=-1),e.line=n,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",Ra);var a=i.get("position","auto");"auto"===a&&50===Aa.position&&(a="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=a}(t,e)}function ba(t){return t.replace(/ /gi,"\n")}var Da=function(){function t(){this.state="INITIAL",this.buffer="",this.decoder=new Ea,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}var e=t.prototype;return e.parse=function(t){var e=this;function r(){var t=e.buffer,r=0;for(t=ba(t);r>>0).toString()};function _a(t,e,r){return Ca(t.toString())+Ca(e.toString())+Ca(r)}function xa(t,e,r,i,n,a,s){var o,l,u,h=new Da,d=Tt(new Uint8Array(t)).trim().replace(Ia,"\n").split("\n"),c=[],f=e?(o=e.baseTime,void 0===(l=e.timescale)&&(l=1),mn(o,vn,1/l)):0,g="00:00.000",v=0,m=0,p=!0;h.oncue=function(t){var a=r[i],s=r.ccOffset,o=(v-f)/9e4;if(null!=a&&a.new&&(void 0!==m?s=r.ccOffset=a.start:function(t,e,r){var i=t[e],n=t[i.prevCC];if(!n||!n.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(a=n)&&a.new;){var a;t.ccOffset+=i.start-n.start,i.new=!1,n=t[(i=n).prevCC]}t.presentationOffset=r}(r,i,o)),o){if(!e)return void(u=new Error("Missing initPTS for VTT MPEGTS"));s=o-r.presentationOffset}var l=t.endTime-t.startTime,h=Sn(9e4*(t.startTime+s-m),9e4*n)/9e4;t.startTime=Math.max(h,0),t.endTime=Math.max(h+l,0);var d=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(d)),t.id||(t.id=_a(t.startTime,t.endTime,d)),t.endTime>0&&c.push(t)},h.onparsingerror=function(t){u=t},h.onflush=function(){u?s(u):a(c)},d.forEach((function(t){if(p){if(wa(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((function(t){wa(t,"LOCAL:")?g=t.slice(6):wa(t,"MPEGTS:")&&(v=parseInt(t.slice(7)))}));try{m=function(t){var e=parseInt(t.slice(-3)),r=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),n=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(y(e)&&y(r)&&y(i)&&y(n)))throw Error("Malformed X-TIMESTAMP-MAP: Local:"+t);return e+=1e3*r,(e+=6e4*i)+36e5*n}(g)/1e3}catch(t){u=t}return}""===t&&(p=!1)}h.parse(t+"\n")})),h.flush()}var Pa="stpp.ttml.im1t",Fa=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,Ma=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,Oa={left:"start",center:"center",right:"end",start:"start",end:"end"};function Na(t,e,r,i){var n=_t(new Uint8Array(t),["mdat"]);if(0!==n.length){var a,s,l,u,h=n.map((function(t){return Tt(t)})),d=(a=e.baseTime,s=1,void 0===(l=e.timescale)&&(l=1),void 0===u&&(u=!1),mn(a,s,1/l,u));try{h.forEach((function(t){return r(function(t,e){var r=(new DOMParser).parseFromString(t,"text/xml"),i=r.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");var n={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},a=Object.keys(n).reduce((function(t,e){return t[e]=i.getAttribute("ttp:"+e)||n[e],t}),{}),s="preserve"!==i.getAttribute("xml:space"),l=Ba(Ua(i,"styling","style")),u=Ba(Ua(i,"layout","region")),h=Ua(i,"body","[begin]");return[].map.call(h,(function(t){var r=Ga(t,s);if(!r||!t.hasAttribute("begin"))return null;var i=Va(t.getAttribute("begin"),a),n=Va(t.getAttribute("dur"),a),h=Va(t.getAttribute("end"),a);if(null===i)throw Ha(t);if(null===h){if(null===n)throw Ha(t);h=i+n}var d=new ya(i-e,h-e,r);d.id=_a(d.startTime,d.endTime,d.text);var c=function(t,e,r){var i="http://www.w3.org/ns/ttml#styling",n=null,a=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],s=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;return s&&r.hasOwnProperty(s)&&(n=r[s]),a.reduce((function(r,a){var s=Ka(e,i,a)||Ka(t,i,a)||Ka(n,i,a);return s&&(r[a]=s),r}),{})}(u[t.getAttribute("region")],l[t.getAttribute("style")],l),f=c.textAlign;if(f){var g=Oa[f];g&&(d.lineAlign=g),d.align=f}return o(d,c),d})).filter((function(t){return null!==t}))}(t,d))}))}catch(t){i(t)}}else i(new Error("Could not parse IMSC1 mdat"))}function Ua(t,e,r){var i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(r)):[]}function Ba(t){return t.reduce((function(t,e){var r=e.getAttribute("xml:id");return r&&(t[r]=e),t}),{})}function Ga(t,e){return[].slice.call(t.childNodes).reduce((function(t,r,i){var n;return"br"===r.nodeName&&i?t+"\n":null!=(n=r.childNodes)&&n.length?Ga(r,e):e?t+r.textContent.trim().replace(/\s+/g," "):t+r.textContent}),"")}function Ka(t,e,r){return t&&t.hasAttributeNS(e,r)?t.getAttributeNS(e,r):null}function Ha(t){return new Error("Could not parse ttml timestamp "+t)}function Va(t,e){if(!t)return null;var r=Ta(t);return null===r&&(Fa.test(t)?r=function(t,e){var r=Fa.exec(t),i=(0|r[4])+(0|r[5])/e.subFrameRate;return 3600*(0|r[1])+60*(0|r[2])+(0|r[3])+i/e.frameRate}(t,e):Ma.test(t)&&(r=function(t,e){var r=Ma.exec(t),i=Number(r[1]);switch(r[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),r}var Ya=function(){function t(t){this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=t,this.config=t.config,this.Cues=t.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this)}var e=t.prototype;return e.destroy=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=null,this.cea608Parser1=this.cea608Parser2=void 0},e.initCea608Parsers=function(){if(this.config.enableCEA708Captions&&(!this.cea608Parser1||!this.cea608Parser2)){var t=new pa(this,"textTrack1"),e=new pa(this,"textTrack2"),r=new pa(this,"textTrack3"),i=new pa(this,"textTrack4");this.cea608Parser1=new ga(1,t,e),this.cea608Parser2=new ga(3,r,i)}},e.addCues=function(t,e,r,i,n){for(var a,s,o,l,u=!1,h=n.length;h--;){var d=n[h],c=(a=d[0],s=d[1],o=e,l=r,Math.min(s,l)-Math.max(a,o));if(c>=0&&(d[0]=Math.min(d[0],e),d[1]=Math.max(d[1],r),u=!0,c/(r-e)>.5))return}if(u||n.push([e,r]),this.config.renderTextTracksNatively){var f=this.captionsTracks[t];this.Cues.newCue(f,e,r,i)}else{var g=this.Cues.newCue(null,e,r,i);this.hls.trigger(S.CUES_PARSED,{type:"captions",cues:g,track:t})}},e.onInitPtsFound=function(t,e){var r=this,i=e.frag,n=e.id,a=e.initPTS,s=e.timescale,o=this.unparsedVttFrags;"main"===n&&(this.initPTS[i.cc]={baseTime:a,timescale:s}),o.length&&(this.unparsedVttFrags=[],o.forEach((function(t){r.onFragLoaded(S.FRAG_LOADED,t)})))},e.getExistingTrack=function(t,e){var r=this.media;if(r)for(var i=0;ii.cc||l.trigger(S.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:i,error:e})}))}else s.push(t)},e._fallbackToIMSC1=function(t,e){var r=this,i=this.tracks[t.level];i.textCodec||Na(e,this.initPTS[t.cc],(function(){i.textCodec=Pa,r._parseIMSC1(t,e)}),(function(){i.textCodec="wvtt"}))},e._appendCues=function(t,e){var r=this.hls;if(this.config.renderTextTracksNatively){var i=this.textTracks[e];if(!i||"disabled"===i.mode)return;t.forEach((function(t){return Me(i,t)}))}else{var n=this.tracks[e];if(!n)return;var a=n.default?"default":"subtitles"+e;r.trigger(S.CUES_PARSED,{type:"subtitles",cues:t,track:a})}},e.onFragDecrypted=function(t,e){e.frag.type===Ce&&this.onFragLoaded(S.FRAG_LOADED,e)},e.onSubtitleTracksCleared=function(){this.tracks=[],this.captionsTracks={}},e.onFragParsingUserdata=function(t,e){this.initCea608Parsers();var r=this.cea608Parser1,i=this.cea608Parser2;if(this.enabled&&r&&i){var n=e.frag,a=e.samples;if(n.type!==Ie||"NONE"!==this.closedCaptionsForLevel(n))for(var s=0;sthis.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}},e.getMaxLevel=function(e){var r=this,i=this.hls.levels;if(!i.length)return-1;var n=i.filter((function(t,i){return r.isLevelAllowed(t)&&i<=e}));return this.clientRect=null,t.getMaxLevelByMediaSize(n,this.mediaWidth,this.mediaHeight)},e.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},e.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},e.getDimensions=function(){if(this.clientRect)return this.clientRect;var t=this.media,e={width:0,height:0};if(t){var r=t.getBoundingClientRect();e.width=r.width,e.height=r.height,e.width||e.height||(e.width=r.right-r.left||t.width||0,e.height=r.bottom-r.top||t.height||0)}return this.clientRect=e,e},e.isLevelAllowed=function(t){return!this.restrictedLevels.some((function(e){return t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height}))},t.getMaxLevelByMediaSize=function(t,e,r){if(null==t||!t.length)return-1;for(var i,n,a=t.length-1,s=Math.max(e,r),o=0;o=s||l.height>=s)&&(i=l,!(n=t[o+1])||i.width!==n.width||i.height!==n.height)){a=o;break}}return a},s(t,[{key:"mediaWidth",get:function(){return this.getDimensions().width*this.contentScaleFactor}},{key:"mediaHeight",get:function(){return this.getDimensions().height*this.contentScaleFactor}},{key:"contentScaleFactor",get:function(){var t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}}]),t}(),Xa=function(){function t(t){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=t,this.registerListeners()}var e=t.prototype;return e.setStreamController=function(t){this.streamController=t},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.destroy=function(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null},e.onMediaAttaching=function(t,e){var r=this.hls.config;if(r.capLevelOnFPSDrop){var i=e.media instanceof self.HTMLVideoElement?e.media:null;this.media=i,i&&"function"==typeof i.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),r.fpsDroppedMonitoringPeriod)}},e.checkFPS=function(t,e,r){var i=performance.now();if(e){if(this.lastTime){var n=i-this.lastTime,a=r-this.lastDroppedFrames,s=e-this.lastDecodedFrames,o=1e3*a/n,l=this.hls;if(l.trigger(S.FPS_DROP,{currentDropped:a,currentDecoded:s,totalDroppedFrames:r}),o>0&&a>l.config.fpsDroppedMonitoringThreshold*s){var u=l.currentLevel;w.warn("drop FPS ratio greater than max allowed value for currentLevel: "+u),u>0&&(-1===l.autoLevelCapping||l.autoLevelCapping>=u)&&(u-=1,l.trigger(S.FPS_DROP_LEVEL_CAPPING,{level:u,droppedLevel:l.currentLevel}),l.autoLevelCapping=u,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=r,this.lastDecodedFrames=e}},e.checkFPSInterval=function(){var t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){var e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)},t}(),za="[eme]",Qa=function(){function t(e){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=t.CDMCleanupPromise?[t.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=w.debug.bind(w,za),this.log=w.log.bind(w,za),this.warn=w.warn.bind(w,za),this.error=w.error.bind(w,za),this.hls=e,this.config=e.config,this.registerListeners()}var e=t.prototype;return e.destroy=function(){this.unregisterListeners(),this.onMediaDetached();var t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.getLicenseServerUrl=function(t){var e=this.config,r=e.drmSystems,i=e.widevineLicenseUrl,n=r[t];if(n)return n.licenseUrl;if(t===q.WIDEVINE&&i)return i;throw new Error('no license server URL configured for key-system "'+t+'"')},e.getServerCertificateUrl=function(t){var e=this.config.drmSystems[t];if(e)return e.serverCertificateUrl;this.log('No Server Certificate in config.drmSystems["'+t+'"]')},e.attemptKeySystemAccess=function(t){var e=this,r=this.hls.levels,i=function(t,e,r){return!!t&&r.indexOf(t)===e},n=r.map((function(t){return t.audioCodec})).filter(i),a=r.map((function(t){return t.videoCodec})).filter(i);return n.length+a.length===0&&a.push("avc1.42e01e"),new Promise((function(r,i){!function t(s){var o=s.shift();e.getMediaKeysPromise(o,n,a).then((function(t){return r({keySystem:o,mediaKeys:t})})).catch((function(e){s.length?t(s):i(e instanceof es?e:new es({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))}(t)}))},e.requestMediaKeySystemAccess=function(t,e){var r=this.config.requestMediaKeySystemAccessFunc;if("function"!=typeof r){var i="Configured requestMediaKeySystemAccess is not a function "+r;return null===it&&"http:"===self.location.protocol&&(i="navigator.requestMediaKeySystemAccess is not available over insecure protocol "+location.protocol),Promise.reject(new Error(i))}return r(t,e)},e.getMediaKeysPromise=function(t,e,r){var i=this,n=function(t,e,r,i){var n;switch(t){case q.FAIRPLAY:n=["cenc","sinf"];break;case q.WIDEVINE:case q.PLAYREADY:n=["cenc"];break;case q.CLEARKEY:n=["cenc","keyids"];break;default:throw new Error("Unknown key-system: "+t)}return function(t,e,r,i){return[{initDataTypes:t,persistentState:i.persistentState||"optional",distinctiveIdentifier:i.distinctiveIdentifier||"optional",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((function(t){return{contentType:'audio/mp4; codecs="'+t+'"',robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}})),videoCapabilities:r.map((function(t){return{contentType:'video/mp4; codecs="'+t+'"',robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null}}))}]}(n,e,r,i)}(t,e,r,this.config.drmSystemOptions),a=this.keySystemAccessPromises[t],s=null==a?void 0:a.keySystemAccess;if(!s){this.log('Requesting encrypted media "'+t+'" key-system access with config: '+JSON.stringify(n)),s=this.requestMediaKeySystemAccess(t,n);var o=this.keySystemAccessPromises[t]={keySystemAccess:s};return s.catch((function(e){i.log('Failed to obtain access to key-system "'+t+'": '+e)})),s.then((function(e){i.log('Access for key-system "'+e.keySystem+'" obtained');var r=i.fetchServerCertificate(t);return i.log('Create media-keys for "'+t+'"'),o.mediaKeys=e.createMediaKeys().then((function(e){return i.log('Media-keys created for "'+t+'"'),r.then((function(r){return r?i.setMediaKeysServerCertificate(e,t,r):e}))})),o.mediaKeys.catch((function(e){i.error('Failed to create media-keys for "'+t+'"}: '+e)})),o.mediaKeys}))}return s.then((function(){return a.mediaKeys}))},e.createMediaKeySessionContext=function(t){var e=t.decryptdata,r=t.keySystem,i=t.mediaKeys;this.log('Creating key-system session "'+r+'" keyId: '+Lt(e.keyId||[]));var n=i.createSession(),a={decryptdata:e,keySystem:r,mediaKeys:i,mediaKeysSession:n,keyStatus:"status-pending"};return this.mediaKeySessions.push(a),a},e.renewKeySession=function(t){var e=t.decryptdata;if(e.pssh){var r=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e);this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(r,"cenc",e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)},e.getKeyIdString=function(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return Lt(t.keyId)},e.updateKeySession=function(t,e){var r,i=t.mediaKeysSession;return this.log('Updating key-session "'+i.sessionId+'" for keyID '+Lt((null==(r=t.decryptdata)?void 0:r.keyId)||[])+"\n } (data length: "+(e?e.byteLength:e)+")"),i.update(e)},e.selectKeySystemFormat=function(t){var e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log("Selecting key-system from fragment (sn: "+t.sn+" "+t.type+": "+t.level+") key formats "+e.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise},e.getKeyFormatPromise=function(t){var e=this;return new Promise((function(r,i){var n=et(e.config),a=t.map($).filter((function(t){return!!t&&-1!==n.indexOf(t)}));return e.getKeySystemSelectionPromise(a).then((function(t){var e=t.keySystem,n=tt(e);n?r(n):i(new Error('Unable to find format for key-system "'+e+'"'))})).catch(i)}))},e.loadKey=function(t){var e=this,r=t.keyInfo.decryptdata,i=this.getKeyIdString(r),n="(keyId: "+i+' format: "'+r.keyFormat+'" method: '+r.method+" uri: "+r.uri+")";this.log("Starting session for key "+n);var a=this.keyIdToKeySessionPromise[i];return a||(a=this.keyIdToKeySessionPromise[i]=this.getKeySystemForKeyPromise(r).then((function(i){var a=i.keySystem,s=i.mediaKeys;return e.throwIfDestroyed(),e.log("Handle encrypted media sn: "+t.frag.sn+" "+t.frag.type+": "+t.frag.level+" using key "+n),e.attemptSetMediaKeys(a,s).then((function(){e.throwIfDestroyed();var t=e.createMediaKeySessionContext({keySystem:a,mediaKeys:s,decryptdata:r});return e.generateRequestWithPreferredKeySession(t,"cenc",r.pssh,"playlist-key")}))}))).catch((function(t){return e.handleError(t)})),a},e.throwIfDestroyed=function(t){if(!this.hls)throw new Error("invalid state")},e.handleError=function(t){this.hls&&(this.error(t.message),t instanceof es?this.hls.trigger(S.ERROR,t.data):this.hls.trigger(S.ERROR,{type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))},e.getKeySystemForKeyPromise=function(t){var e=this.getKeyIdString(t),r=this.keyIdToKeySessionPromise[e];if(!r){var i=$(t.keyFormat),n=i?[i]:et(this.config);return this.attemptKeySystemAccess(n)}return r},e.getKeySystemSelectionPromise=function(t){if(t.length||(t=et(this.config)),0===t.length)throw new es({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},"Missing key-system license configuration options "+JSON.stringify({drmSystems:this.config.drmSystems}));return this.attemptKeySystemAccess(t)},e._onMediaEncrypted=function(t){var e=this,r=t.initDataType,i=t.initData;if(this.debug('"'+t.type+'" event: init data type: "'+r+'"'),null!==i){var n,a;if("sinf"===r&&this.config.drmSystems[q.FAIRPLAY]){var s=bt(new Uint8Array(i));try{var o=V(JSON.parse(s).sinf),l=Ut(new Uint8Array(o));if(!l)return;n=l.subarray(8,24),a=q.FAIRPLAY}catch(t){return void this.warn('Failed to parse sinf "encrypted" event message initData')}}else{var u=function(t){if(!(t instanceof ArrayBuffer)||t.byteLength<32)return null;var e={version:0,systemId:"",kids:null,data:null},r=new DataView(t),i=r.getUint32(0);if(t.byteLength!==i&&i>44)return null;if(1886614376!==r.getUint32(4))return null;if(e.version=r.getUint32(8)>>>24,e.version>1)return null;e.systemId=Lt(new Uint8Array(t,12,16));var n=r.getUint32(28);if(0===e.version){if(i-320)for(var a,s=0,o=n.length;s in key message");return W(atob(f))},e.setupLicenseXHR=function(t,e,r,i){var n=this,a=this.config.licenseXhrSetup;return a?Promise.resolve().then((function(){if(!r.decryptdata)throw new Error("Key removed");return a.call(n.hls,t,e,r,i)})).catch((function(s){if(!r.decryptdata)throw s;return t.open("POST",e,!0),a.call(n.hls,t,e,r,i)})).then((function(r){return t.readyState||t.open("POST",e,!0),{xhr:t,licenseChallenge:r||i}})):(t.open("POST",e,!0),Promise.resolve({xhr:t,licenseChallenge:i}))},e.requestLicense=function(t,e){var r=this,i=this.config.keyLoadPolicy.default;return new Promise((function(n,a){var s=r.getLicenseServerUrl(t.keySystem);r.log("Sending license request to URL: "+s);var o=new XMLHttpRequest;o.responseType="arraybuffer",o.onreadystatechange=function(){if(!r.hls||!t.mediaKeysSession)return a(new Error("invalid state"));if(4===o.readyState)if(200===o.status){r._requestLicenseFailureCount=0;var l=o.response;r.log("License received "+(l instanceof ArrayBuffer?l.byteLength:l));var u=r.config.licenseResponseCallback;if(u)try{l=u.call(r.hls,o,s,t)}catch(t){r.error(t)}n(l)}else{var h=i.errorRetry,d=h?h.maxNumRetry:0;if(r._requestLicenseFailureCount++,r._requestLicenseFailureCount>d||o.status>=400&&o.status<500)a(new es({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_LICENSE_REQUEST_FAILED,fatal:!0,networkDetails:o,response:{url:s,data:void 0,code:o.status,text:o.statusText}},"License Request XHR failed ("+s+"). Status: "+o.status+" ("+o.statusText+")"));else{var c=d-r._requestLicenseFailureCount+1;r.warn("Retrying license request, "+c+" attempts left"),r.requestLicense(t,e).then(n,a)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=o,r.setupLicenseXHR(o,s,t,e).then((function(e){var i=e.xhr,n=e.licenseChallenge;t.keySystem==q.PLAYREADY&&(n=r.unpackPlayReadyKeyMessage(i,n)),i.send(n)}))}))},e.onMediaAttached=function(t,e){if(this.config.emeEnabled){var r=e.media;this.media=r,r.addEventListener("encrypted",this.onMediaEncrypted),r.addEventListener("waitingforkey",this.onWaitingForKey)}},e.onMediaDetached=function(){var e=this,r=this.media,i=this.mediaKeySessions;r&&(r.removeEventListener("encrypted",this.onMediaEncrypted),r.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},qt.clearKeyUriToKeyIdMap();var n=i.length;t.CDMCleanupPromise=Promise.all(i.map((function(t){return e.removeSession(t)})).concat(null==r?void 0:r.setMediaKeys(null).catch((function(t){e.log("Could not clear media keys: "+t)})))).then((function(){n&&(e.log("finished closing key sessions and clearing media keys"),i.length=0)})).catch((function(t){e.log("Could not close sessions and clear media keys: "+t)}))},e.onManifestLoading=function(){this.keyFormatPromise=null},e.onManifestLoaded=function(t,e){var r=e.sessionKeys;if(r&&this.config.emeEnabled&&!this.keyFormatPromise){var i=r.reduce((function(t,e){return-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t}),[]);this.log("Selecting key-system from session-keys "+i.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(i)}},e.removeSession=function(t){var e=this,r=t.mediaKeysSession,i=t.licenseXhr;if(r){this.log("Remove licenses and keys and close session "+r.sessionId),t._onmessage&&(r.removeEventListener("message",t._onmessage),t._onmessage=void 0),t._onkeystatuseschange&&(r.removeEventListener("keystatuseschange",t._onkeystatuseschange),t._onkeystatuseschange=void 0),i&&i.readyState!==XMLHttpRequest.DONE&&i.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;var n=this.mediaKeySessions.indexOf(t);return n>-1&&this.mediaKeySessions.splice(n,1),r.remove().catch((function(t){e.log("Could not remove session: "+t)})).then((function(){return r.close()})).catch((function(t){e.log("Could not close session: "+t)}))}},t}();Qa.CDMCleanupPromise=void 0;var Ja,$a,Za,ts,es=function(t){function e(e,r){var i;return(i=t.call(this,r)||this).data=void 0,e.error||(e.error=new Error(r)),i.data=e,e.err=e.error,i}return l(e,t),e}(c(Error));!function(t){t.MANIFEST="m",t.AUDIO="a",t.VIDEO="v",t.MUXED="av",t.INIT="i",t.CAPTION="c",t.TIMED_TEXT="tt",t.KEY="k",t.OTHER="o"}(Ja||(Ja={})),function(t){t.DASH="d",t.HLS="h",t.SMOOTH="s",t.OTHER="o"}($a||($a={})),function(t){t.OBJECT="CMCD-Object",t.REQUEST="CMCD-Request",t.SESSION="CMCD-Session",t.STATUS="CMCD-Status"}(Za||(Za={}));var rs=((ts={})[Za.OBJECT]=["br","d","ot","tb"],ts[Za.REQUEST]=["bl","dl","mtp","nor","nrr","su"],ts[Za.SESSION]=["cid","pr","sf","sid","st","v"],ts[Za.STATUS]=["bs","rtp"],ts),is=function t(e,r){this.value=void 0,this.params=void 0,Array.isArray(e)&&(e=e.map((function(e){return e instanceof t?e:new t(e)}))),this.value=e,this.params=r},ns=function(t){this.description=void 0,this.description=t},as="Dict";function ss(t,e,r,i){return new Error("failed to "+t+' "'+(n=e,(Array.isArray(n)?JSON.stringify(n):n instanceof Map?"Map{}":n instanceof Set?"Set{}":"object"==typeof n?JSON.stringify(n):String(n))+'" as ')+r,{cause:i});var n}var os="Bare Item",ls="Boolean",us="Byte Sequence",hs="Decimal",ds="Integer",cs=/[\x00-\x1f\x7f]+/,fs="Token",gs="Key";function vs(t,e,r){return ss("serialize",t,e,r)}function ms(t){if(!1===ArrayBuffer.isView(t))throw vs(t,us);return":"+(e=t,btoa(String.fromCharCode.apply(String,e))+":");var e}function ps(t){if(function(t){return t<-999999999999999||99999999999999912)throw vs(t,hs);var r=e.toString();return r.includes(".")?r:r+".0"}var Ts="String";function Ss(t){var e,r=(e=t).description||e.toString().slice(7,-1);if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(r))throw vs(r,fs);return r}function Ls(t){switch(typeof t){case"number":if(!y(t))throw vs(t,os);return Number.isInteger(t)?ps(t):Es(t);case"string":return function(t){if(cs.test(t))throw vs(t,Ts);return'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}(t);case"symbol":return Ss(t);case"boolean":return function(t){if("boolean"!=typeof t)throw vs(t,ls);return t?"?1":"?0"}(t);case"object":if(t instanceof Date)return function(t){return"@"+ps(t.getTime()/1e3)}(t);if(t instanceof Uint8Array)return ms(t);if(t instanceof ns)return Ss(t);default:throw vs(t,os)}}function As(t){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(t))throw vs(t,gs);return t}function Rs(t){return null==t?"":Object.entries(t).map((function(t){var e=t[0],r=t[1];return!0===r?";"+As(e):";"+As(e)+"="+Ls(r)})).join("")}function ks(t){return t instanceof is?""+Ls(t.value)+Rs(t.params):Ls(t)}function bs(t,e){var r;if(void 0===e&&(e={whitespace:!0}),"object"!=typeof t)throw vs(t,as);var i=t instanceof Map?t.entries():Object.entries(t),n=null!=(r=e)&&r.whitespace?" ":"";return Array.from(i).map((function(t){var e=t[0],r=t[1];r instanceof is==0&&(r=new is(r));var i,n=As(e);return!0===r.value?n+=Rs(r.params):(n+="=",Array.isArray(r.value)?n+="("+(i=r).value.map(ks).join(" ")+")"+Rs(i.params):n+=ks(r)),n})).join(","+n)}var Ds=function(t){return"ot"===t||"sf"===t||"st"===t},Is=function(t){return"number"==typeof t?y(t):null!=t&&""!==t&&!1!==t},ws=function(t){return Math.round(t)},Cs=function(t){return 100*ws(t/100)},_s={br:ws,d:ws,bl:Cs,dl:Cs,mtp:Cs,nor:function(t,e){return null!=e&&e.baseUrl&&(t=function(t,e){var r=new URL(t),i=new URL(e);if(r.origin!==i.origin)return t;for(var n=r.pathname.split("/").slice(1),a=i.pathname.split("/").slice(1,-1);n[0]===a[0];)n.shift(),a.shift();for(;a.length;)a.shift(),n.unshift("..");return n.join("/")}(t,e.baseUrl)),encodeURIComponent(t)},rtp:Cs,tb:ws};function xs(t,e){return void 0===e&&(e={}),t?function(t,e){return bs(t,e)}(function(t,e){var r={};if(null==t||"object"!=typeof t)return r;var i=Object.keys(t).sort(),n=o({},_s,null==e?void 0:e.formatters),a=null==e?void 0:e.filter;return i.forEach((function(i){if(null==a||!a(i)){var s=t[i],o=n[i];o&&(s=o(s,e)),"v"===i&&1===s||"pr"==i&&1===s||Is(s)&&(Ds(i)&&"string"==typeof s&&(s=new ns(s)),r[i]=s)}})),r}(t,e),o({whitespace:!1},e)):""}function Ps(t,e,r){return o(t,function(t,e){var r;if(void 0===e&&(e={}),!t)return{};var i=Object.entries(t),n=Object.entries(rs).concat(Object.entries((null==(r=e)?void 0:r.customHeaderMap)||{})),a=i.reduce((function(t,e){var r,i=e[0],a=e[1],s=(null==(r=n.find((function(t){return t[1].includes(i)})))?void 0:r[0])||Za.REQUEST;return null!=t[s]||(t[s]={}),t[s][i]=a,t}),{});return Object.entries(a).reduce((function(t,r){var i=r[0],n=r[1];return t[i]=xs(n,e),t}),{})}(e,r))}var Fs="CMCD",Ms=/CMCD=[^]+/;function Os(t,e,r){var i=function(t,e){if(void 0===e&&(e={}),!t)return"";var r=xs(t,e);return Fs+"="+encodeURIComponent(r)}(e,r);if(!i)return t;if(Ms.test(t))return t.replace(Ms,i);var n=t.includes("?")?"&":"?";return""+t+n+i}var Ns=function(){function t(t){var e=this;this.hls=void 0,this.config=void 0,this.media=void 0,this.sid=void 0,this.cid=void 0,this.useHeaders=!1,this.includeKeys=void 0,this.initialized=!1,this.starved=!1,this.buffering=!0,this.audioBuffer=void 0,this.videoBuffer=void 0,this.onWaiting=function(){e.initialized&&(e.starved=!0),e.buffering=!0},this.onPlaying=function(){e.initialized||(e.initialized=!0),e.buffering=!1},this.applyPlaylistData=function(t){try{e.apply(t,{ot:Ja.MANIFEST,su:!e.initialized})}catch(t){w.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=function(t){try{var r=t.frag,i=e.hls.levels[r.level],n=e.getObjectType(r),a={d:1e3*r.duration,ot:n};n!==Ja.VIDEO&&n!==Ja.AUDIO&&n!=Ja.MUXED||(a.br=i.bitrate/1e3,a.tb=e.getTopBandwidth(n)/1e3,a.bl=e.getBufferLength(n)),e.apply(t,a)}catch(t){w.warn("Could not generate segment CMCD data.",t)}},this.hls=t;var r=this.config=t.config,i=r.cmcd;null!=i&&(r.pLoader=this.createPlaylistLoader(),r.fLoader=this.createFragmentLoader(),this.sid=i.sessionId||function(){try{return crypto.randomUUID()}catch(i){try{var t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)}catch(t){var r=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(t){var e=(r+16*Math.random())%16|0;return r=Math.floor(r/16),("x"==t?e:3&e|8).toString(16)}))}}}(),this.cid=i.contentId,this.useHeaders=!0===i.useHeaders,this.includeKeys=i.includeKeys,this.registerListeners())}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(S.MEDIA_DETACHED,this.onMediaDetached,this),t.on(S.BUFFER_CREATED,this.onBufferCreated,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(S.MEDIA_DETACHED,this.onMediaDetached,this),t.off(S.BUFFER_CREATED,this.onBufferCreated,this)},e.destroy=function(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=null},e.onMediaAttached=function(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)},e.onMediaDetached=function(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)},e.onBufferCreated=function(t,e){var r,i;this.audioBuffer=null==(r=e.tracks.audio)?void 0:r.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer},e.createData=function(){var t;return{v:1,sf:$a.HLS,sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}},e.apply=function(t,e){void 0===e&&(e={}),o(e,this.createData());var r=e.ot===Ja.INIT||e.ot===Ja.VIDEO||e.ot===Ja.MUXED;this.starved&&r&&(e.bs=!0,e.su=!0,this.starved=!1),null==e.su&&(e.su=this.buffering);var i=this.includeKeys;i&&(e=Object.keys(e).reduce((function(t,r){return i.includes(r)&&(t[r]=e[r]),t}),{})),this.useHeaders?(t.headers||(t.headers={}),Ps(t.headers,e)):t.url=Os(t.url,e)},e.getObjectType=function(t){var e=t.type;return"subtitle"===e?Ja.TIMED_TEXT:"initSegment"===t.sn?Ja.INIT:"audio"===e?Ja.AUDIO:"main"===e?this.hls.audioTracks.length?Ja.VIDEO:Ja.MUXED:void 0},e.getTopBandwidth=function(t){var e,r=0,i=this.hls;if(t===Ja.AUDIO)e=i.audioTracks;else{var n=i.maxAutoLevel,a=n>-1?n+1:i.levels.length;e=i.levels.slice(0,a)}for(var s,o=g(e);!(s=o()).done;){var l=s.value;l.bitrate>r&&(r=l.bitrate)}return r>0?r:NaN},e.getBufferLength=function(t){var e=this.hls.media,r=t===Ja.AUDIO?this.audioBuffer:this.videoBuffer;return r&&e?1e3*zr.bufferInfo(r,e.currentTime,this.config.maxBufferHole).len:NaN},e.createPlaylistLoader=function(){var t=this.config.pLoader,e=this.applyPlaylistData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},e.createFragmentLoader=function(){var t=this.config.fLoader,e=this.applyFragmentData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},t}(),Us=function(){function t(t){this.hls=void 0,this.log=void 0,this.loader=null,this.uri=null,this.pathwayId=".",this.pathwayPriority=null,this.timeToLoad=300,this.reloadTimer=-1,this.updated=0,this.started=!1,this.enabled=!0,this.levels=null,this.audioTracks=null,this.subtitleTracks=null,this.penalizedPathways={},this.hls=t,this.log=w.log.bind(w,"[content-steering]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.ERROR,this.onError,this))},e.startLoad=function(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){var t=1e3*this.timeToLoad-(performance.now()-this.updated);if(t>0)return void this.scheduleRefresh(this.uri,t)}this.loadSteeringManifest(this.uri)}},e.stopLoad=function(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()},e.clearTimeout=function(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)},e.destroy=function(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null},e.removeLevel=function(t){var e=this.levels;e&&(this.levels=e.filter((function(e){return e!==t})))},e.onManifestLoading=function(){this.stopLoad(),this.enabled=!0,this.timeToLoad=300,this.updated=0,this.uri=null,this.pathwayId=".",this.levels=this.audioTracks=this.subtitleTracks=null},e.onManifestLoaded=function(t,e){var r=e.contentSteering;null!==r&&(this.pathwayId=r.pathwayId,this.uri=r.uri,this.started&&this.startLoad())},e.onManifestParsed=function(t,e){this.audioTracks=e.audioTracks,this.subtitleTracks=e.subtitleTracks},e.onError=function(t,e){var r=e.errorAction;if((null==r?void 0:r.action)===Tr&&r.flags===Rr){var i=this.levels,n=this.pathwayPriority,a=this.pathwayId;if(e.context){var s=e.context,o=s.groupId,l=s.pathwayId,u=s.type;o&&i?a=this.getPathwayForGroupId(o,u,a):l&&(a=l)}a in this.penalizedPathways||(this.penalizedPathways[a]=performance.now()),!n&&i&&(n=i.reduce((function(t,e){return-1===t.indexOf(e.pathwayId)&&t.push(e.pathwayId),t}),[])),n&&n.length>1&&(this.updatePathwayPriority(n),r.resolved=this.pathwayId!==a),r.resolved||w.warn("Could not resolve "+e.details+' ("'+e.error.message+'") with content-steering for Pathway: '+a+" levels: "+(i?i.length:i)+" priorities: "+JSON.stringify(n)+" penalized: "+JSON.stringify(this.penalizedPathways))}},e.filterParsedLevels=function(t){this.levels=t;var e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){var r=t[0].pathwayId;this.log("No levels found in Pathway "+this.pathwayId+'. Setting initial Pathway to "'+r+'"'),e=this.getLevelsForPathway(r),this.pathwayId=r}return e.length!==t.length?(this.log("Found "+e.length+"/"+t.length+' levels in Pathway "'+this.pathwayId+'"'),e):t},e.getLevelsForPathway=function(t){return null===this.levels?[]:this.levels.filter((function(e){return t===e.pathwayId}))},e.updatePathwayPriority=function(t){var e;this.pathwayPriority=t;var r=this.penalizedPathways,i=performance.now();Object.keys(r).forEach((function(t){i-r[t]>3e5&&delete r[t]}));for(var n=0;n0){this.log('Setting Pathway to "'+a+'"'),this.pathwayId=a,ur(e),this.hls.trigger(S.LEVELS_UPDATED,{levels:e});var l=this.hls.levels[s];o&&l&&this.levels&&(l.attrs["STABLE-VARIANT-ID"]!==o.attrs["STABLE-VARIANT-ID"]&&l.bitrate!==o.bitrate&&this.log("Unstable Pathways change from bitrate "+o.bitrate+" to "+l.bitrate),this.hls.nextLoadLevel=s);break}}}},e.getPathwayForGroupId=function(t,e,r){for(var i=this.getLevelsForPathway(r).concat(this.levels||[]),n=0;n=2&&(0===r.loading.first&&(r.loading.first=Math.max(self.performance.now(),r.loading.start),n.timeout!==n.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),n.timeout=n.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),n.loadPolicy.maxLoadTimeMs-(r.loading.first-r.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),e.onreadystatechange=null,e.onprogress=null;var a=e.status,s="text"!==e.responseType;if(a>=200&&a<300&&(s&&e.response||null!==e.responseText)){r.loading.end=Math.max(self.performance.now(),r.loading.first);var o=s?e.response:e.responseText,l="arraybuffer"===e.responseType?o.byteLength:o.length;if(r.loaded=r.total=l,r.bwEstimate=8e3*r.total/(r.loading.end-r.loading.first),!this.callbacks)return;var u=this.callbacks.onProgress;if(u&&u(r,t,o,e),!this.callbacks)return;var h={url:e.responseURL,data:o,code:a};this.callbacks.onSuccess(h,r,t,e)}else{var d=n.loadPolicy.errorRetry;gr(d,r.retry,!1,{url:t.url,data:void 0,code:a})?this.retry(d):(w.error(a+" while loading "+t.url),this.callbacks.onError({code:a,text:e.statusText},t,e,r))}}}},e.loadtimeout=function(){var t,e=null==(t=this.config)?void 0:t.loadPolicy.timeoutRetry;if(gr(e,this.stats.retry,!0))this.retry(e);else{var r;w.warn("timeout while loading "+(null==(r=this.context)?void 0:r.url));var i=this.callbacks;i&&(this.abortInternal(),i.onTimeout(this.stats,this.context,this.loader))}},e.retry=function(t){var e=this.context,r=this.stats;this.retryDelay=cr(t,r.retry),r.retry++,w.warn((status?"HTTP Status "+status:"Timeout")+" while loading "+(null==e?void 0:e.url)+", retrying "+r.retry+"/"+t.maxNumRetry+" in "+this.retryDelay+"ms"),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)},e.loadprogress=function(t){var e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)},e.getCacheAge=function(){var t=null;if(this.loader&&Ks.test(this.loader.getAllResponseHeaders())){var e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.loader&&new RegExp("^"+t+":\\s*[\\d.]+\\s*$","im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null},t}(),Vs=/(\d+)-(\d+)\/(\d+)/,Ys=function(){function t(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=null,this.response=null,this.controller=void 0,this.context=null,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||Ws,this.controller=new self.AbortController,this.stats=new M}var e=t.prototype;return e.destroy=function(){this.loader=this.callbacks=this.context=this.config=this.request=null,this.abortInternal(),this.response=null,this.fetchSetup=this.controller=this.stats=null},e.abortInternal=function(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())},e.abort=function(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)},e.load=function(t,e,r){var i=this,n=this.stats;if(n.loading.start)throw new Error("Loader can only be used once.");n.loading.start=self.performance.now();var a=function(t,e){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(o({},t.headers))};return t.rangeEnd&&r.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1)),r}(t,this.controller.signal),s=r.onProgress,l="arraybuffer"===t.responseType,u=l?"byteLength":"length",h=e.loadPolicy,d=h.maxTimeToFirstByteMs,c=h.maxLoadTimeMs;this.context=t,this.config=e,this.callbacks=r,this.request=this.fetchSetup(t,a),self.clearTimeout(this.requestTimeout),e.timeout=d&&y(d)?d:c,this.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),e.timeout),self.fetch(this.request).then((function(a){i.response=i.loader=a;var o=Math.max(self.performance.now(),n.loading.start);if(self.clearTimeout(i.requestTimeout),e.timeout=c,i.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),c-(o-n.loading.start)),!a.ok){var u=a.status,h=a.statusText;throw new qs(h||"fetch, bad network response",u,a)}return n.loading.first=o,n.total=function(t){var e=t.get("Content-Range");if(e){var r=function(t){var e=Vs.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(y(r))return r}var i=t.get("Content-Length");if(i)return parseInt(i)}(a.headers)||n.total,s&&y(e.highWaterMark)?i.loadProgressively(a,n,t,e.highWaterMark,s):l?a.arrayBuffer():"json"===t.responseType?a.json():a.text()})).then((function(a){var o=i.response;if(!o)throw new Error("loader destroyed");self.clearTimeout(i.requestTimeout),n.loading.end=Math.max(self.performance.now(),n.loading.first);var l=a[u];l&&(n.loaded=n.total=l);var h={url:o.url,data:a,code:o.status};s&&!y(e.highWaterMark)&&s(n,t,a,o),r.onSuccess(h,n,t,o)})).catch((function(e){if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=e&&e.code||0,s=e?e.message:null;r.onError({code:a,text:s},t,e?e.details:null,n)}}))},e.getCacheAge=function(){var t=null;if(this.response){var e=this.response.headers.get("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.response?this.response.headers.get(t):null},e.loadProgressively=function(t,e,r,i,n){void 0===i&&(i=0);var a=new ki,s=t.body.getReader();return function o(){return s.read().then((function(s){if(s.done)return a.dataLength&&n(e,r,a.flush(),t),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return e.loaded+=u,u=i&&n(e,r,a.flush(),t)):n(e,r,l,t),o()})).catch((function(){return Promise.reject()}))}()},t}();function Ws(t,e){return new self.Request(t.url,e)}var js,qs=function(t){function e(e,r,i){var n;return(n=t.call(this,e)||this).code=void 0,n.details=void 0,n.code=r,n.details=i,n}return l(e,t),e}(c(Error)),Xs=/\s/,zs=i(i({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,loader:Hs,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:Br,bufferController:Qn,capLevelController:qa,errorController:br,fpsController:Xa,stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrEwmaDefaultEstimateMax:5e6,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:it,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,useMediaCapabilities:!0,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:{newCue:function(t,e,r,i){for(var n,a,s,o,l,u=[],h=self.VTTCue||self.TextTrackCue,d=0;d=16?o--:o++;var g=ba(l.trim()),v=_a(e,r,g);null!=t&&null!=(c=t.cues)&&c.getCueById(v)||((a=new h(e,r,g)).id=v,a.line=d+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*o/32)),u.push(a))}return t&&u.length&&(u.sort((function(t,e){return"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line})),u.forEach((function(e){return Me(t,e)}))),u}},enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:Wn,subtitleTrackController:qn,timelineController:Ya,audioStreamController:Vn,audioTrackController:Yn,emeController:Qa,cmcdController:Ns,contentSteeringController:Us});function Qs(t){return t&&"object"==typeof t?Array.isArray(t)?t.map(Qs):Object.keys(t).reduce((function(e,r){return e[r]=Qs(t[r]),e}),{}):t}function Js(t){var e=t.loader;e!==Ys&&e!==Hs?(w.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1):function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1}()&&(t.loader=Ys,t.progressive=!0,t.enableSoftwareAES=!0,w.log("[config]: Progressive streaming enabled, using FetchLoader"))}var $s=function(t){function e(e,r){var i;return(i=t.call(this,e,"[level-controller]")||this)._levels=[],i._firstLevel=-1,i._maxAutoLevel=-1,i._startLevel=void 0,i.currentLevel=null,i.currentLevelIndex=-1,i.manualLevelIndex=-1,i.steering=void 0,i.onParsedComplete=void 0,i.steering=r,i._registerListeners(),i}l(e,t);var r=e.prototype;return r._registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.ERROR,this.onError,this)},r._unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.ERROR,this.onError,this)},r.destroy=function(){this._unregisterListeners(),this.steering=null,this.resetLevels(),t.prototype.destroy.call(this)},r.stopLoad=function(){this._levels.forEach((function(t){t.loadError=0,t.fragmentError=0})),t.prototype.stopLoad.call(this)},r.resetLevels=function(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[],this._maxAutoLevel=-1},r.onManifestLoading=function(t,e){this.resetLevels()},r.onManifestLoaded=function(t,e){var r=this.hls.config.preferManagedMediaSource,i=[],n={},a={},s=!1,o=!1,l=!1;e.levels.forEach((function(t){var e,u,h=t.attrs,d=t.audioCodec,c=t.videoCodec;-1!==(null==(e=d)?void 0:e.indexOf("mp4a.40.34"))&&(js||(js=/chrome|firefox/i.test(navigator.userAgent)),js&&(t.audioCodec=d=void 0)),d&&(t.audioCodec=d=ue(d,r)),0===(null==(u=c)?void 0:u.indexOf("avc1"))&&(c=t.videoCodec=function(t){var e=t.split(".");if(e.length>2){var r=e.shift()+".";return(r+=parseInt(e.shift()).toString(16))+("000"+parseInt(e.shift()).toString(16)).slice(-4)}return t}(c));var f=t.width,g=t.height,v=t.unknownCodecs;if(s||(s=!(!f||!g)),o||(o=!!c),l||(l=!!d),!(null!=v&&v.length||d&&!re(d,"audio",r)||c&&!re(c,"video",r))){var m=h.CODECS,p=h["FRAME-RATE"],y=h["HDCP-LEVEL"],E=h["PATHWAY-ID"],T=h.RESOLUTION,S=h["VIDEO-RANGE"],L=(E||".")+"-"+t.bitrate+"-"+T+"-"+p+"-"+m+"-"+S+"-"+y;if(n[L])if(n[L].uri===t.url||t.attrs["PATHWAY-ID"])n[L].addGroupId("audio",h.AUDIO),n[L].addGroupId("text",h.SUBTITLES);else{var A=a[L]+=1;t.attrs["PATHWAY-ID"]=new Array(A+1).join(".");var R=new tr(t);n[L]=R,i.push(R)}else{var k=new tr(t);n[L]=k,a[L]=1,i.push(k)}}})),this.filterAndSortMediaOptions(i,e,s,o,l)},r.filterAndSortMediaOptions=function(t,e,r,i,n){var a=this,s=[],o=[],l=t;if((r||i)&&n&&(l=l.filter((function(t){var e,r=t.videoCodec,i=t.videoRange,n=t.width,a=t.height;return(!!r||!(!n||!a))&&!!(e=i)&&ze.indexOf(e)>-1}))),0!==l.length){if(e.audioTracks){var u=this.hls.config.preferManagedMediaSource;Zs(s=e.audioTracks.filter((function(t){return!t.audioCodec||re(t.audioCodec,"audio",u)})))}e.subtitles&&Zs(o=e.subtitles);var h=l.slice(0);l.sort((function(t,e){if(t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"])return(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1;if(r&&t.height!==e.height)return t.height-e.height;if(t.frameRate!==e.frameRate)return t.frameRate-e.frameRate;if(t.videoRange!==e.videoRange)return ze.indexOf(t.videoRange)-ze.indexOf(e.videoRange);if(t.videoCodec!==e.videoCodec){var i=ae(t.videoCodec),n=ae(e.videoCodec);if(i!==n)return n-i}if(t.uri===e.uri&&t.codecSet!==e.codecSet){var a=se(t.codecSet),s=se(e.codecSet);if(a!==s)return s-a}return t.bitrate!==e.bitrate?t.bitrate-e.bitrate:0}));var d=h[0];if(this.steering&&(l=this.steering.filterParsedLevels(l)).length!==h.length)for(var c=0;cm&&m===zs.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=p)}break}var y=n&&!i,E={levels:l,audioTracks:s,subtitleTracks:o,sessionData:e.sessionData,sessionKeys:e.sessionKeys,firstLevel:this._firstLevel,stats:e.stats,audio:n,video:i,altAudio:!y&&s.some((function(t){return!!t.url}))};this.hls.trigger(S.MANIFEST_PARSED,E),(this.hls.config.autoStartLoad||this.hls.forceStartLoad)&&this.hls.startLoad(this.hls.config.startPosition)}else Promise.resolve().then((function(){if(a.hls){e.levels.length&&a.warn("One or more CODECS in variant not supported: "+JSON.stringify(e.levels[0].attrs));var t=new Error("no level with compatible codecs found in manifest");a.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:e.url,error:t,reason:t.message})}}))},r.onError=function(t,e){!e.fatal&&e.context&&e.context.type===ke&&e.context.level===this.level&&this.checkRetry(e)},r.onFragBuffered=function(t,e){var r=e.frag;if(void 0!==r&&r.type===Ie){var i=r.elementaryStreams;if(!Object.keys(i).some((function(t){return!!i[t]})))return;var n=this._levels[r.level];null!=n&&n.loadError&&(this.log("Resetting level error count of "+n.loadError+" on frag buffered"),n.loadError=0)}},r.onLevelLoaded=function(t,e){var r,i,n=e.level,a=e.details,s=this._levels[n];if(!s)return this.warn("Invalid level index "+n),void(null!=(i=e.deliveryDirectives)&&i.skip&&(a.deltaUpdateFailed=!0));n===this.currentLevelIndex?(0===s.fragmentError&&(s.loadError=0),this.playlistLoaded(n,e,s.details)):null!=(r=e.deliveryDirectives)&&r.skip&&(a.deltaUpdateFailed=!0)},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentLevelIndex,i=this.currentLevel;if(i&&this.shouldLoadPlaylist(i)){var n=i.uri;if(e)try{n=e.addDirectives(n)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}var a=i.attrs["PATHWAY-ID"];this.log("Loading level index "+r+(void 0!==(null==e?void 0:e.msn)?" at sn "+e.msn+" part "+e.part:"")+" with"+(a?" Pathway "+a:"")+" "+n),this.clearTimer(),this.hls.trigger(S.LEVEL_LOADING,{url:n,level:r,pathwayId:i.attrs["PATHWAY-ID"],id:0,deliveryDirectives:e||null})}},r.removeLevel=function(t){var e,r=this,i=this._levels.filter((function(e,i){return i!==t||(r.steering&&r.steering.removeLevel(e),e===r.currentLevel&&(r.currentLevel=null,r.currentLevelIndex=-1,e.details&&e.details.fragments.forEach((function(t){return t.level=-1}))),!1)}));ur(i),this._levels=i,this.currentLevelIndex>-1&&null!=(e=this.currentLevel)&&e.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.hls.trigger(S.LEVELS_UPDATED,{levels:i})},r.onLevelsUpdated=function(t,e){var r=e.levels;this._levels=r},r.checkMaxAutoUpdated=function(){var t=this.hls,e=t.autoLevelCapping,r=t.maxAutoLevel,i=t.maxHdcpLevel;this._maxAutoLevel!==r&&(this._maxAutoLevel=r,this.hls.trigger(S.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:e,levels:this.levels,maxAutoLevel:r,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:i}))},s(e,[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"level",get:function(){return this.currentLevelIndex},set:function(t){var e=this._levels;if(0!==e.length){if(t<0||t>=e.length){var r=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:r,reason:r.message}),i)return;t=Math.min(t,e.length-1)}var n=this.currentLevelIndex,a=this.currentLevel,s=a?a.attrs["PATHWAY-ID"]:void 0,o=e[t],l=o.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=o,n!==t||!o.details||!a||s!==l){this.log("Switching to level "+t+" ("+(o.height?o.height+"p ":"")+(o.videoRange?o.videoRange+" ":"")+(o.codecSet?o.codecSet+" ":"")+"@"+o.bitrate+")"+(l?" with Pathway "+l:"")+" from level "+n+(s?" with Pathway "+s:""));var u={level:t,attrs:o.attrs,details:o.details,bitrate:o.bitrate,averageBitrate:o.averageBitrate,maxBitrate:o.maxBitrate,realBitrate:o.realBitrate,width:o.width,height:o.height,codecSet:o.codecSet,audioCodec:o.audioCodec,videoCodec:o.videoCodec,audioGroups:o.audioGroups,subtitleGroups:o.subtitleGroups,loaded:o.loaded,loadError:o.loadError,fragmentError:o.fragmentError,name:o.name,id:o.id,uri:o.uri,url:o.url,urlId:0,audioGroupIds:o.audioGroupIds,textGroupIds:o.textGroupIds};this.hls.trigger(S.LEVEL_SWITCHING,u);var h=o.details;if(!h||h.live){var d=this.switchParams(o.uri,null==a?void 0:a.details);this.loadPlaylist(d)}}}}},{key:"manualLevel",get:function(){return this.manualLevelIndex},set:function(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}},{key:"firstLevel",get:function(){return this._firstLevel},set:function(t){this._firstLevel=t}},{key:"startLevel",get:function(){if(void 0===this._startLevel){var t=this.hls.config.startLevel;return void 0!==t?t:this.hls.firstAutoLevel}return this._startLevel},set:function(t){this._startLevel=t}},{key:"nextLoadLevel",get:function(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel},set:function(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}}]),e}(Dr);function Zs(t){var e={};t.forEach((function(t){var r=t.groupId||"";t.id=e[r]=e[r]||0,e[r]++}))}var to=function(){function t(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}var e=t.prototype;return e.abort=function(t){for(var e in this.keyUriToKeyInfo){var r=this.keyUriToKeyInfo[e].loader;if(r){var i;if(t&&t!==(null==(i=r.context)?void 0:i.frag.type))return;r.abort()}}},e.detach=function(){for(var t in this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}},e.destroy=function(){for(var t in this.detach(),this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}},e.createKeyLoadError=function(t,e,r,i,n){return void 0===e&&(e=A.KEY_LOAD_ERROR),new si({type:L.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:n,error:r,networkDetails:i})},e.loadClear=function(t,e){var r=this;if(this.emeController&&this.config.emeEnabled)for(var i=t.sn,n=t.cc,a=function(){var t=e[s];if(n<=t.cc&&("initSegment"===i||"initSegment"===t.sn||i