Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2399f3bb3 | |||
| 2a8a3f1cbf | |||
| b1ba5bab62 | |||
| 6878f05e57 | |||
| d428a8964a | |||
| f432e72dd0 | |||
| 2929db9cec | |||
| 6d967bc1f9 | |||
| 83c0053b2c | |||
| ecfd7404f5 | |||
| 41badbfb8e | |||
| 0cb013a7fd | |||
| 75020d4df7 | |||
| 69c288b154 | |||
| 0ea651db62 | |||
| 4823e60a92 | |||
| c4949eb81f | |||
| aa4c81c266 | |||
| 063fef5813 | |||
| d9fb734c85 | |||
| a51156cf18 | |||
| 32e0ee4a10 | |||
| e6bea97936 | |||
| 9776e09ca7 | |||
| ad273d3a98 | |||
| 69c301e79f | |||
| 8f2bb3f34b | |||
| e4ff6d224f | |||
| 00751459a2 | |||
| 874c07b887 | |||
| 152df3ef5d | |||
| c950bb0252 | |||
| dd7ea2657a | |||
| 5889791847 | |||
| 9160403b99 | |||
| 5ccbd7c1c2 | |||
| 778245dd1c | |||
| 205018c96a | |||
| eaba451a47 | |||
| b7c11db604 | |||
| f7b98044e6 | |||
| 1b1bdb37db | |||
| ab453d275e | |||
| ee387b79e1 | |||
| e71ed5e7eb | |||
| 122a550599 | |||
| f3f08afac8 | |||
| a0030194cb | |||
| f158ffb33e | |||
| a9f2b5158c | |||
| b9f984dad0 | |||
| 290e011061 | |||
| 09109e783e | |||
| 8ac834bdd4 | |||
| 06d8503fd0 | |||
| 4c3de3bbf4 | |||
| 4933c1415b | |||
| 322c332170 | |||
| 5d9c254282 | |||
| a03db503c3 | |||
| 2ea66deb08 | |||
| b3c5ef8c86 | |||
| fb1e7613cb | |||
| 8a7ab63b00 | |||
| 07f51e6929 | |||
| f64d279672 | |||
| 4185202496 | |||
| edbcd3e736 | |||
| abe617a346 | |||
| 9c98f5e769 | |||
| b4a524f46d | |||
| 297096a93b | |||
| e23e64ab00 | |||
| 0698f90273 | |||
| bec792797d | |||
| fd6014c11f | |||
| b8b90aba51 | |||
| 652dc93e9a | |||
| 6f1cc94ea5 | |||
| 52832223f8 | |||
| e080eac204 | |||
| 7a0646fd5f | |||
| 732fe47836 | |||
| 4e0185cfe6 | |||
| 5f2d523242 | |||
| 64ac27d93d | |||
| d6774bbdb9 | |||
| a1983c725d | |||
| 070ea3892f | |||
| cf4f6468f3 | |||
| c7af5028be | |||
| 9527a2be2e | |||
| ee5c663467 | |||
| e304035f76 | |||
| d96701453d | |||
| 1682d18ba6 | |||
| fb756b7473 | |||
| 3bc5274461 | |||
| 5f0366ac32 | |||
| abda47045d | |||
| 51c5d51786 | |||
| c309bb83e7 | |||
| 0eeb3c7585 | |||
| ae29b8271f | |||
| ab405b35f3 | |||
| 8d6aabce7a | |||
| 8516f825e1 | |||
| bcfc64bef1 | |||
| 1d59c02745 | |||
| 12a75034c7 | |||
| fffb22dd1f | |||
| 65b5ca2dec | |||
| ef74fb8497 | |||
| 675476a8f6 | |||
| 2d86ffd18c | |||
| a1be812052 | |||
| 9c534b1df5 | |||
| 261feb5858 | |||
| e4d970233e | |||
| 7bd346c402 | |||
| 439319141b | |||
| a404c2c86c | |||
| 6cf3cd142a | |||
| 418cabb852 | |||
| 2ce8cec12f | |||
| 905ef9b1ba | |||
| 7dc9eaa543 | |||
| 215d55771c | |||
| ac3d931576 | |||
| fcfef3080a | |||
| e610081634 | |||
| 484d401021 | |||
| 55d95691c8 | |||
| 2d8ef99df2 | |||
| 01e2ed2306 | |||
| 166287ce1b | |||
| 8495c7350e | |||
| 40dd3907a0 | |||
| 621d2e017e | |||
| d0a9c7a126 | |||
| 2301d8d7b2 | |||
| d28ae5caea | |||
| 5cf343cb69 | |||
| de7326375d | |||
| 936e84f6e0 | |||
| e1ebed4859 | |||
| 0bda4d8308 | |||
| adf49b8475 | |||
| 8d825346ab | |||
| ef38468fa7 | |||
| ef54b04ffc | |||
| 51e20497ac | |||
| 4ddadc08cb | |||
| 801bb2d534 | |||
| 20dd16badf | |||
| 31398a7e6b | |||
| de70b0a861 | |||
| a50c99b8e5 | |||
| 63de86a409 | |||
| 9fc3d91a17 | |||
| 2ff7a20eba | |||
| 3fa481bdfc | |||
| 9f7448d255 | |||
| 3afe8d7c1d | |||
| 15c27e16cc | |||
| 14a9763c73 | |||
| 6fbd141576 | |||
| c0455a20aa | |||
| 6f9b8b732d | |||
| 5fa31fe4d6 | |||
| f237119b9a | |||
| b08b88357e | |||
| f73ee41d93 | |||
| 93dad05bde | |||
| b844722af1 | |||
| a4b212d906 | |||
| 152719441e | |||
| 4b62a6e34f | |||
| 48fabec431 | |||
| f8d9fccf74 | |||
| 8793c36364 | |||
| 59d25c10b3 | |||
| 3b3d5b033a | |||
| 249ae49b43 | |||
| 33eafd5691 | |||
| 2b9247d630 | |||
| cc6b8277c9 | |||
| f65b18842a | |||
| db190e69ed | |||
| bc516bce7d |
+41
-27
@@ -15,99 +15,113 @@ jobs:
|
||||
env: { CGO_ENABLED: 0 }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with: { go-version: '1.21' }
|
||||
|
||||
- name: Build go2rtc_win64
|
||||
env: { GOOS: windows, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_win32
|
||||
env: { GOOS: windows, GOARCH: 386 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win32
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_win32, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_win_arm64
|
||||
env: { GOOS: windows, GOARCH: arm64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win_arm64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_linux_amd64
|
||||
env: { GOOS: linux, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_linux_amd64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_linux_amd64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_linux_i386
|
||||
env: { GOOS: linux, GOARCH: 386 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_linux_i386
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_linux_i386, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_linux_arm64
|
||||
env: { GOOS: linux, GOARCH: arm64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_linux_arm64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_linux_arm64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_linux_arm
|
||||
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_linux_arm
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_linux_arm, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_linux_armv6
|
||||
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_linux_armv6
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_linux_armv6, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_linux_mipsel
|
||||
env: { GOOS: linux, GOARCH: mipsle }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_linux_mipsel
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_mac_amd64
|
||||
env: { GOOS: darwin, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_mac_amd64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_mac_amd64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_mac_arm64
|
||||
env: { GOOS: darwin, GOARCH: arm64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_mac_arm64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_mac_arm64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_freebsd_amd64
|
||||
env: { GOOS: freebsd, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_amd64
|
||||
uses: actions/upload-artifact@v3
|
||||
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_freebsd_arm64
|
||||
env: { GOOS: freebsd, GOARCH: arm64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_arm64
|
||||
uses: actions/upload-artifact@v3
|
||||
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
|
||||
|
||||
docker-master:
|
||||
name: Build docker master
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ github.repository }}
|
||||
tags: |
|
||||
@@ -116,20 +130,20 @@ jobs:
|
||||
type=match,pattern=v(.*),group=1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: |
|
||||
@@ -148,36 +162,36 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta-hw
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-hardware
|
||||
latest=false
|
||||
suffix=-hardware,onlatest=true
|
||||
latest=auto
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}},enable=false
|
||||
type=match,pattern=v(.*),group=1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
|
||||
@@ -25,13 +25,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './website'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
@@ -70,13 +70,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/${{ matrix.platform }}
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
- name: Build and push Hardware
|
||||
if: matrix.platform == 'amd64'
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
|
||||
@@ -5,3 +5,5 @@ go2rtc.yaml
|
||||
go2rtc.json
|
||||
|
||||
0_test.go
|
||||
|
||||
.DS_Store
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# go2rtc
|
||||
<h1 align="center">
|
||||
|
||||
[](https://github.com/AlexxIT/go2rtc/stargazers)
|
||||
[](https://hub.docker.com/r/alexxit/go2rtc)
|
||||
[](https://github.com/AlexxIT/go2rtc/releases)
|
||||
[](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
|
||||

|
||||
<br>
|
||||
[](https://github.com/AlexxIT/go2rtc/stargazers)
|
||||
[](https://hub.docker.com/r/alexxit/go2rtc)
|
||||
[](https://github.com/AlexxIT/go2rtc/releases)
|
||||
[](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
|
||||
</h1>
|
||||
|
||||
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
|
||||
|
||||
@@ -34,6 +37,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
|
||||
- [MediaSoup](https://mediasoup.org/) framework routing idea
|
||||
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
|
||||
- creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev)
|
||||
|
||||
---
|
||||
|
||||
@@ -127,7 +131,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
### go2rtc: Docker
|
||||
|
||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok) and [Python](#source-echo).
|
||||
The Docker container [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) supports multiple architectures including `amd64`, `386`, `arm64`, and `arm`. This container offers the same functionality as the [Home Assistant Add-on](#go2rtc-home-assistant-add-on) but is designed to operate independently of Home Assistant. It comes preinstalled with [FFmpeg](#source-ffmpeg), [ngrok](#module-ngrok), and [Python](#source-echo).
|
||||
|
||||
### go2rtc: Home Assistant Add-on
|
||||
|
||||
@@ -146,13 +150,13 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
|
||||
|
||||
Latest, but maybe unstable version:
|
||||
|
||||
- Binary: GitHub > [Actions](https://github.com/AlexxIT/go2rtc/actions) > [Build and Push](https://github.com/AlexxIT/go2rtc/actions/workflows/build.yml) > latest run > Artifacts section (you should be logged in to GitHub)
|
||||
- Binary: [latest nightly release](https://nightly.link/AlexxIT/go2rtc/workflows/build/master)
|
||||
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
|
||||
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
|
||||
|
||||
## Configuration
|
||||
|
||||
- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory
|
||||
- by default go2rtc will search `go2rtc.yaml` in the current work directory
|
||||
- `api` server will start on default **1984 port** (TCP)
|
||||
- `rtsp` server will start on default **8554 port** (TCP)
|
||||
- `webrtc` will use port **8555** (TCP/UDP) for connections
|
||||
@@ -213,6 +217,7 @@ Supported for sources:
|
||||
- [TP-Link Tapo](#source-tapo) cameras
|
||||
- [Hikvision ISAPI](#source-isapi) cameras
|
||||
- [Roborock vacuums](#source-roborock) models with cameras
|
||||
- [Exec](#source-exec) audio on server
|
||||
- [Any Browser](#incoming-browser) as IP-camera
|
||||
|
||||
Two way audio can be used in browser with [WebRTC](#module-webrtc) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
|
||||
@@ -230,7 +235,7 @@ streams:
|
||||
amcrest_doorbell:
|
||||
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
|
||||
glichy_camera: ffmpeg:rstp://username:password@192.168.1.123/live/ch00_1
|
||||
glichy_camera: ffmpeg:rtsp://username:password@192.168.1.123/live/ch00_1
|
||||
```
|
||||
|
||||
**Recommendations**
|
||||
@@ -265,7 +270,7 @@ streams:
|
||||
|
||||
#### Source: RTMP
|
||||
|
||||
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp).
|
||||
You can get stream from RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -403,20 +408,31 @@ Exec source can run any external application and expect data from it. Two transp
|
||||
|
||||
If you want to use **RTSP** transport - the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
|
||||
|
||||
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**.
|
||||
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. Also pipe can write data to app stdin in two formats: **PCMA** and **PCM/48000**.
|
||||
|
||||
The source can be used with:
|
||||
|
||||
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source
|
||||
- [FFplay](https://ffmpeg.org/ffplay.html) - play audio on your server
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
|
||||
- any your own software
|
||||
|
||||
Pipe commands support parameters (format: `exec:{command}#{param1}#{param2}`):
|
||||
|
||||
- `killsignal` - signal which will be send to stop the process (numeric form)
|
||||
- `killtimeout` - time in seconds for forced termination with sigkill
|
||||
- `backchannel` - enable backchannel for two-way audio
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
||||
picam_h264: exec:libcamera-vid -t 0 --inline -o -
|
||||
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
|
||||
pi5cam_h264: exec:libcamera-vid -t 0 --libav-format h264 -o -
|
||||
canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5
|
||||
play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1
|
||||
play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1
|
||||
```
|
||||
|
||||
#### Source: Echo
|
||||
@@ -537,11 +553,16 @@ echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
|
||||
|
||||
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
|
||||
|
||||
- `username` - urlsafe email, `alex@gmail.com` -> `alex%40gmail.com`
|
||||
- `password` - base64password, `secret1` -> `c2VjcmV0MQ==`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
|
||||
kc401: kasa://username:password@192.168.1.123:19443/https/stream/mixed
|
||||
```
|
||||
|
||||
Tested: KD110, KC200, KC401, KC420WS, EC71.
|
||||
|
||||
#### Source: GoPro
|
||||
|
||||
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
|
||||
@@ -579,7 +600,7 @@ streams:
|
||||
|
||||
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat.
|
||||
|
||||
The Nest API only allows you to get a link to a stream for 5 minutes. So every 5 minutes the stream will be reconnected.
|
||||
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes. Do not use this with Frigate! If the stream expires, Frigate will consume all available ram on your machine within seconds. It's recommended to use [Nest source](#source-nest) - it supports extending the stream.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
@@ -610,7 +631,7 @@ streams:
|
||||
|
||||
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
|
||||
|
||||
Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes.
|
||||
Currently only WebRTC cameras are supported.
|
||||
|
||||
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass.
|
||||
|
||||
@@ -640,7 +661,7 @@ This source type support four connection formats.
|
||||
|
||||
**whep**
|
||||
|
||||
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-02.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) - is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.
|
||||
|
||||
**go2rtc**
|
||||
|
||||
@@ -790,6 +811,8 @@ streams:
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||
|
||||
**Important!** go2rtc passes requests from localhost and from unix socket without HTTP authorisation, even if you have it configured! It is your responsibility to set up secure external access to API. If not properly configured, an attacker can gain access to your cameras and even your server.
|
||||
|
||||
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
|
||||
|
||||
**Module config**
|
||||
@@ -817,6 +840,7 @@ api:
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...
|
||||
-----END PRIVATE KEY-----
|
||||
unix_listen: "/tmp/go2rtc.sock" # default "", unix socket listener for API
|
||||
```
|
||||
|
||||
**PS:**
|
||||
@@ -1169,6 +1193,10 @@ API examples:
|
||||
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
|
||||
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
|
||||
|
||||
**PS.** This module also supports streaming to the server console (terminal) in the **animated ASCII art** format ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/mjpeg/README.md)):
|
||||
|
||||
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
||||
|
||||
### Module: Log
|
||||
|
||||
You can set different log levels for different modules.
|
||||
@@ -1349,6 +1377,7 @@ streams:
|
||||
**Distributions**
|
||||
|
||||
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
|
||||
- [Arch User Repository](https://linux-packages.com/aur/package/go2rtc)
|
||||
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
|
||||
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
|
||||
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -4,44 +4,45 @@ go 1.21
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.15.7
|
||||
github.com/expr-lang/expr v1.16.5
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/miekg/dns v1.1.57
|
||||
github.com/pion/ice/v2 v2.3.11
|
||||
github.com/pion/interceptor v0.1.25
|
||||
github.com/pion/rtcp v1.2.13
|
||||
github.com/pion/rtp v1.8.3
|
||||
github.com/pion/sdp/v3 v3.0.6
|
||||
github.com/miekg/dns v1.1.59
|
||||
github.com/pion/ice/v2 v2.3.24
|
||||
github.com/pion/interceptor v0.1.29
|
||||
github.com/pion/rtcp v1.2.14
|
||||
github.com/pion/rtp v1.8.6
|
||||
github.com/pion/sdp/v3 v3.0.9
|
||||
github.com/pion/srtp/v2 v2.0.18
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.2.24
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
||||
github.com/pion/webrtc/v3 v3.2.40
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.17.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.8 // indirect
|
||||
github.com/pion/datachannel v1.5.6 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.11 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pion/sctp v1.8.16 // indirect
|
||||
github.com/pion/transport/v2 v2.2.5 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/tools v0.20.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,31 +6,14 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
||||
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/expr-lang/expr v1.15.7 h1:BK0JcWUkoW6nrbLBo6xCKhz4BvH5DSOOu1Gx5lucyZo=
|
||||
github.com/expr-lang/expr v1.15.7/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
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/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
||||
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.3.1/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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@@ -43,149 +26,125 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
||||
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA=
|
||||
github.com/pion/dtls/v2 v2.2.8/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.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
|
||||
github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
|
||||
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc=
|
||||
github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI=
|
||||
github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.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/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
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/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
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/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
|
||||
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
|
||||
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
||||
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
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.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/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=
|
||||
github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
|
||||
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
|
||||
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o=
|
||||
github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA=
|
||||
github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU=
|
||||
github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220811171246-fbc7d0a398ab/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.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=
|
||||
@@ -194,57 +153,42 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.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=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+8
-4
@@ -1,8 +1,9 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
# only debian 12 (bookworm) has latest ffmpeg
|
||||
ARG DEBIAN_VERSION="bookworm-slim"
|
||||
# only debian 13 (trixie) has latest ffmpeg
|
||||
# https://packages.debian.org/trixie/ffmpeg
|
||||
ARG DEBIAN_VERSION="trixie-slim"
|
||||
ARG GO_VERSION="1.21-bookworm"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
@@ -44,13 +45,16 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||
# mesa-va-drivers for AMD APU
|
||||
# libasound2-plugins for ALSA support
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||
echo 'deb http://deb.debian.org/debian trixie non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||
apt-get -y update && apt-get -y install tini ffmpeg \
|
||||
python3 curl jq \
|
||||
intel-media-va-driver-non-free \
|
||||
libasound2-plugins
|
||||
mesa-va-drivers \
|
||||
libasound2-plugins && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --link --from=rootfs / /
|
||||
|
||||
|
||||
+17
-5
@@ -11,9 +11,9 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -96,7 +96,10 @@ func listen(network, address string) {
|
||||
Port = ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
server := http.Server{Handler: Handler}
|
||||
server := http.Server{
|
||||
Handler: Handler,
|
||||
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
||||
}
|
||||
if err = server.Serve(ln); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
@@ -126,8 +129,9 @@ func tlsListen(network, address, certFile, keyFile string) {
|
||||
log.Info().Str("addr", address).Msg("[api] tls listen")
|
||||
|
||||
server := &http.Server{
|
||||
Handler: Handler,
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||
Handler: Handler,
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
if err = server.ServeTLS(ln, "", ""); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] tls serve")
|
||||
@@ -251,7 +255,15 @@ func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
go shell.Restart()
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[api] restart %s", path)
|
||||
|
||||
go syscall.Exec(path, os.Args, os.Environ())
|
||||
}
|
||||
|
||||
func logHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -23,11 +23,15 @@ func Init() {
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("api")
|
||||
|
||||
initWS(cfg.Mod.Origin)
|
||||
|
||||
api.HandleFunc("api/ws", apiWS)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
// Message - struct for data exchange in Web API
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
@@ -79,7 +83,7 @@ func initWS(origin string) {
|
||||
if o.Host == r.Host {
|
||||
return true
|
||||
}
|
||||
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
|
||||
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
|
||||
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||
return o.Host[:i] == r.Host
|
||||
@@ -123,7 +127,7 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
|
||||
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
|
||||
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
go func() {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
- By default go2rtc will search config file `go2rtc.yaml` in current work directory
|
||||
- go2rtc support multiple config files
|
||||
- go2rtc support inline config as `YAML`, `JSON` or `key=value` format from command line
|
||||
- Every next config will overwrite previous (but only defined params)
|
||||
|
||||
```
|
||||
go2rtc -config "{log: {format: text}}" -config /config/go2rtc.yaml -config "{rtsp: {listen: ''}}" -config /usr/local/go2rtc/go2rtc.yaml
|
||||
```
|
||||
|
||||
or simple version
|
||||
|
||||
```
|
||||
go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local/go2rtc/go2rtc.yaml
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
Also go2rtc support templates for using environment variables in any part of config:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
||||
|
||||
${LOGS:} # empty default value
|
||||
|
||||
rtsp:
|
||||
username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
```yaml
|
||||
api:
|
||||
listen: ":1984"
|
||||
|
||||
ffmpeg:
|
||||
bin: "ffmpeg"
|
||||
|
||||
log:
|
||||
level: "info"
|
||||
|
||||
rtsp:
|
||||
listen: ":8554"
|
||||
default_query: "video&audio"
|
||||
|
||||
srtp:
|
||||
listen: ":8443"
|
||||
|
||||
webrtc:
|
||||
listen: ":8555/tcp"
|
||||
ice_servers:
|
||||
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||
```
|
||||
+106
-11
@@ -5,8 +5,10 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
@@ -14,7 +16,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var Version = "1.8.5"
|
||||
var Version = "1.9.2"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
@@ -22,16 +24,53 @@ var Info = map[string]any{
|
||||
"version": Version,
|
||||
}
|
||||
|
||||
const usage = `Usage of go2rtc:
|
||||
|
||||
-c, --config Path to config file or config string as YAML or JSON, support multiple
|
||||
-d, --daemon Run in background
|
||||
-v, --version Print version and exit
|
||||
`
|
||||
|
||||
func Init() {
|
||||
var confs Config
|
||||
var daemon bool
|
||||
var version bool
|
||||
|
||||
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
||||
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
|
||||
flag.Var(&confs, "config", "")
|
||||
flag.Var(&confs, "c", "")
|
||||
flag.BoolVar(&daemon, "daemon", false, "")
|
||||
flag.BoolVar(&daemon, "d", false, "")
|
||||
flag.BoolVar(&version, "version", false, "")
|
||||
flag.BoolVar(&version, "v", false, "")
|
||||
|
||||
flag.Usage = func() { fmt.Print(usage) }
|
||||
flag.Parse()
|
||||
|
||||
revision, vcsTime := readRevisionTime()
|
||||
|
||||
if version {
|
||||
fmt.Println("Current version: ", Version)
|
||||
fmt.Printf("go2rtc version %s (%s) %s/%s\n", Version, revision, runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if daemon {
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Println("Daemon not supported on Windows")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
args := os.Args[1:]
|
||||
for i, arg := range args {
|
||||
if arg == "-daemon" {
|
||||
args[i] = ""
|
||||
}
|
||||
}
|
||||
// Re-run the program in background and exit
|
||||
cmd := exec.Command(os.Args[0], args...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal().Err(err).Send()
|
||||
}
|
||||
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -40,22 +79,26 @@ func Init() {
|
||||
}
|
||||
|
||||
for _, conf := range confs {
|
||||
if conf[0] != '{' {
|
||||
if len(conf) == 0 {
|
||||
continue
|
||||
}
|
||||
if conf[0] == '{' {
|
||||
// config as raw YAML or JSON
|
||||
configs = append(configs, []byte(conf))
|
||||
} else if data := parseConfString(conf); data != nil {
|
||||
configs = append(configs, data)
|
||||
} else {
|
||||
// config as file
|
||||
if ConfigPath == "" {
|
||||
ConfigPath = conf
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(conf)
|
||||
if data == nil {
|
||||
if data, _ = os.ReadFile(conf); data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
data = []byte(shell.ReplaceEnvVars(string(data)))
|
||||
configs = append(configs, data)
|
||||
} else {
|
||||
// config as raw YAML
|
||||
configs = append(configs, []byte(conf))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +111,8 @@ func Init() {
|
||||
Info["config_path"] = ConfigPath
|
||||
}
|
||||
|
||||
Info["revision"] = revision
|
||||
|
||||
var cfg struct {
|
||||
Mod map[string]string `yaml:"log"`
|
||||
}
|
||||
@@ -78,7 +123,13 @@ func Init() {
|
||||
|
||||
modules = cfg.Mod
|
||||
|
||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
||||
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
log.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
|
||||
log.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
|
||||
|
||||
if ConfigPath != "" {
|
||||
log.Info().Str("path", ConfigPath).Msg("config")
|
||||
}
|
||||
|
||||
migrateStore()
|
||||
}
|
||||
@@ -121,3 +172,47 @@ func (c *Config) Set(value string) error {
|
||||
}
|
||||
|
||||
var configs [][]byte
|
||||
|
||||
func readRevisionTime() (revision, vcsTime string) {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "vcs.revision":
|
||||
if len(setting.Value) > 7 {
|
||||
revision = setting.Value[:7]
|
||||
} else {
|
||||
revision = setting.Value
|
||||
}
|
||||
case "vcs.time":
|
||||
vcsTime = setting.Value
|
||||
case "vcs.modified":
|
||||
if setting.Value == "true" {
|
||||
revision = "mod." + revision
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseConfString(s string) []byte {
|
||||
i := strings.IndexByte(s, '=')
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
items := strings.Split(s[:i], ".")
|
||||
if len(items) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// `log.level=trace` => `{log: {level: trace}}`
|
||||
var pre string
|
||||
var suf = s[i+1:]
|
||||
for _, item := range items {
|
||||
pre += "{" + item + ": "
|
||||
suf += "}"
|
||||
}
|
||||
|
||||
return []byte(pre + suf)
|
||||
}
|
||||
|
||||
+69
-35
@@ -5,8 +5,11 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -17,6 +20,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/stdin"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -44,37 +48,44 @@ func Init() {
|
||||
log = app.GetLogger("exec")
|
||||
}
|
||||
|
||||
func execHandle(url string) (core.Producer, error) {
|
||||
func execHandle(rawURL string) (core.Producer, error) {
|
||||
var path string
|
||||
var query url.Values
|
||||
|
||||
args := shell.QuoteSplit(url[5:]) // remove `exec:`
|
||||
for i, arg := range args {
|
||||
if arg == "{output}" {
|
||||
if rtsp.Port == "" {
|
||||
return nil, errors.New("rtsp module disabled")
|
||||
}
|
||||
|
||||
sum := md5.Sum([]byte(url))
|
||||
path = "/" + hex.EncodeToString(sum[:])
|
||||
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
|
||||
break
|
||||
// RTSP flow should have `{output}` inside URL
|
||||
// pipe flow may have `#{params}` inside URL
|
||||
if i := strings.Index(rawURL, "{output}"); i > 0 {
|
||||
if rtsp.Port == "" {
|
||||
return nil, errors.New("exec: rtsp module disabled")
|
||||
}
|
||||
|
||||
sum := md5.Sum([]byte(rawURL))
|
||||
path = "/" + hex.EncodeToString(sum[:])
|
||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||
} else if i = strings.IndexByte(rawURL, '#'); i > 0 {
|
||||
query = streams.ParseQuery(rawURL[i+1:])
|
||||
rawURL = rawURL[:i]
|
||||
}
|
||||
|
||||
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
if log.Debug().Enabled() {
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return handlePipe(url, cmd)
|
||||
return handlePipe(rawURL, cmd, query)
|
||||
}
|
||||
|
||||
return handleRTSP(url, path, cmd)
|
||||
return handleRTSP(rawURL, cmd, path)
|
||||
}
|
||||
|
||||
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
r, err := PipeCloser(cmd)
|
||||
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
|
||||
if query.Get("backchannel") == "1" {
|
||||
return stdin.NewClient(cmd)
|
||||
}
|
||||
|
||||
r, err := PipeCloser(cmd, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,15 +102,23 @@ func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
return prod, err
|
||||
}
|
||||
|
||||
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
||||
stderr := limitBuffer{buf: make([]byte, 512)}
|
||||
|
||||
if cmd.Stderr != nil {
|
||||
cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr)
|
||||
} else {
|
||||
cmd.Stderr = &stderr
|
||||
}
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
|
||||
ch := make(chan core.Producer)
|
||||
waiter := make(chan core.Producer)
|
||||
|
||||
waitersMu.Lock()
|
||||
waiters[path] = ch
|
||||
waiters[path] = waiter
|
||||
waitersMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
@@ -108,7 +127,7 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
waitersMu.Unlock()
|
||||
}()
|
||||
|
||||
log.Debug().Str("url", url).Msg("[exec] run")
|
||||
log.Debug().Str("url", url).Str("cmd", fmt.Sprintf("%s", strings.Join(cmd.Args, " "))).Msg("[exec] run")
|
||||
|
||||
ts := time.Now()
|
||||
|
||||
@@ -117,16 +136,9 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chErr := make(chan error)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
// unblocking write to channel
|
||||
select {
|
||||
case chErr <- err:
|
||||
default:
|
||||
log.Trace().Str("url", url).Msg("[exec] close")
|
||||
}
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
@@ -134,9 +146,10 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
_ = cmd.Process.Kill()
|
||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
||||
return nil, errors.New("timeout")
|
||||
case err := <-chErr:
|
||||
return nil, fmt.Errorf("exec: %s", err)
|
||||
case prod := <-ch:
|
||||
case <-done:
|
||||
// limit message size
|
||||
return nil, errors.New("exec: " + stderr.String())
|
||||
case prod := <-waiter:
|
||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
||||
return prod, nil
|
||||
}
|
||||
@@ -144,6 +157,27 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||
|
||||
// internal
|
||||
|
||||
var log zerolog.Logger
|
||||
var waiters = map[string]chan core.Producer{}
|
||||
var waitersMu sync.Mutex
|
||||
var (
|
||||
log zerolog.Logger
|
||||
waiters = map[string]chan core.Producer{}
|
||||
waitersMu sync.Mutex
|
||||
)
|
||||
|
||||
type limitBuffer struct {
|
||||
buf []byte
|
||||
n int
|
||||
}
|
||||
|
||||
func (l *limitBuffer) String() string {
|
||||
if l.n == len(l.buf) {
|
||||
return string(l.buf) + "..."
|
||||
}
|
||||
return string(l.buf[:l.n])
|
||||
}
|
||||
|
||||
func (l *limitBuffer) Write(p []byte) (int, error) {
|
||||
if l.n < cap(l.buf) {
|
||||
l.n += copy(l.buf[l.n:], p)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
+31
-5
@@ -2,29 +2,55 @@ package exec
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
// PipeCloser - return StdoutPipe that Kill cmd on Close call
|
||||
func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) {
|
||||
func PipeCloser(cmd *exec.Cmd, query url.Values) (io.ReadCloser, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add buffer for pipe reader to reduce syscall
|
||||
return pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd}, nil
|
||||
return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil
|
||||
}
|
||||
|
||||
type pipeCloser struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
cmd *exec.Cmd
|
||||
cmd *exec.Cmd
|
||||
query url.Values
|
||||
}
|
||||
|
||||
func (p pipeCloser) Close() error {
|
||||
return core.Any(p.Closer.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
|
||||
func (p *pipeCloser) Close() error {
|
||||
return errors.Join(p.Closer.Close(), p.Kill(), p.Wait())
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Kill() error {
|
||||
if s := p.query.Get("killsignal"); s != "" {
|
||||
log.Trace().Msgf("[exec] kill with custom sig=%s", s)
|
||||
sig := syscall.Signal(core.Atoi(s))
|
||||
return p.cmd.Process.Signal(sig)
|
||||
}
|
||||
return p.cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func (p *pipeCloser) Wait() error {
|
||||
if s := p.query.Get("killtimeout"); s != "" {
|
||||
timeout := time.Duration(core.Atoi(s)) * time.Second
|
||||
timer := time.AfterFunc(timeout, func() {
|
||||
log.Trace().Msgf("[exec] kill after timeout=%s", s)
|
||||
_ = p.cmd.Process.Kill()
|
||||
})
|
||||
defer timer.Stop() // stop timer if Wait ends before timeout
|
||||
}
|
||||
return p.cmd.Wait()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//go:build freebsd || netbsd || openbsd || dragonfly
|
||||
|
||||
package device
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func queryToInput(query url.Values) string {
|
||||
if video := query.Get("video"); video != "" {
|
||||
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
|
||||
input := "-f v4l2"
|
||||
|
||||
for key, value := range query {
|
||||
switch key {
|
||||
case "resolution":
|
||||
input += " -video_size " + value[0]
|
||||
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
|
||||
input += " -" + key + " " + value[0]
|
||||
}
|
||||
}
|
||||
|
||||
return input + " -i " + indexToItem(videos, video)
|
||||
}
|
||||
|
||||
if audio := query.Get("audio"); audio != "" {
|
||||
input := "-f oss"
|
||||
|
||||
for key, value := range query {
|
||||
switch key {
|
||||
case "channels", "sample_rate":
|
||||
input += " -" + key + " " + value[0]
|
||||
}
|
||||
}
|
||||
|
||||
return input + " -i " + indexToItem(audios, audio)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func initDevices() {
|
||||
files, err := os.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := "/dev/" + file.Name()
|
||||
|
||||
cmd := exec.Command(
|
||||
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
|
||||
)
|
||||
b, _ := cmd.CombinedOutput()
|
||||
|
||||
// [video4linux2,v4l2 @ 0x860b92280] Raw : yuyv422 : YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||
// [video4linux2,v4l2 @ 0x860b92280] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
|
||||
m := re.FindAllStringSubmatch(string(b), -1)
|
||||
for _, i := range m {
|
||||
size, _, _ := strings.Cut(i[4], " ")
|
||||
stream := &api.Source{
|
||||
Name: i[3],
|
||||
Info: i[4],
|
||||
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
|
||||
}
|
||||
|
||||
if i[1] != "Compressed" {
|
||||
stream.URL += "#video=h264#hardware"
|
||||
}
|
||||
|
||||
videos = append(videos, name)
|
||||
streams = append(streams, stream)
|
||||
}
|
||||
}
|
||||
|
||||
err = exec.Command(Bin, "-f", "oss", "-i", "/dev/dsp", "-t", "1", "-f", "null", "-").Run()
|
||||
if err == nil {
|
||||
stream := &api.Source{
|
||||
Name: "OSS default",
|
||||
Info: " ",
|
||||
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
|
||||
}
|
||||
|
||||
audios = append(audios, "default")
|
||||
streams = append(streams, stream)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin || ios
|
||||
|
||||
package device
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
|
||||
|
||||
package device
|
||||
|
||||
import (
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package device
|
||||
|
||||
import (
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||
@@ -54,7 +55,7 @@ var defaults = map[string]string{
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"mjpeg": "-c:v mjpeg",
|
||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
@@ -145,7 +146,7 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
}
|
||||
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
||||
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||
query = streams.ParseQuery(s[i+1:])
|
||||
args.Video = len(query["video"])
|
||||
args.Audio = len(query["audio"])
|
||||
@@ -193,6 +194,11 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
} else if strings.HasPrefix(s, "virtual?") {
|
||||
var err error
|
||||
if args.Input, err = virtual.GetInput(s[8:]); err != nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
args.Input = inputTemplate("file", s, query)
|
||||
}
|
||||
@@ -274,6 +280,12 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
}
|
||||
}
|
||||
|
||||
if query["bitrate"] != nil {
|
||||
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||
b := query["bitrate"][0]
|
||||
args.AddCodec("-b:v " + b + " -maxrate " + b + " -bufsize " + b)
|
||||
}
|
||||
|
||||
// 4. Process audio codecs
|
||||
if args.Audio > 0 {
|
||||
for _, audio := range query["audio"] {
|
||||
|
||||
+109
-56
@@ -7,29 +7,48 @@ import (
|
||||
)
|
||||
|
||||
func TestParseArgsFile(t *testing.T) {
|
||||
// [FILE] all tracks will be copied without transcoding codecs
|
||||
args := parseArgs("/media/bbb.mp4")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be transcoded to H264, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=h264")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be copied, audio will be transcoded to pcmu
|
||||
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=h265#rotate=-90")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=mjpeg")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, args.String())
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/509
|
||||
args = parseArgs("ffmpeg:test.mp4#raw=-ss 00:00:20")
|
||||
require.Equal(t, `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[FILE] all tracks will be copied without transcoding codecs",
|
||||
source: "/media/bbb.mp4",
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] video will be transcoded to H264, audio will be skipped",
|
||||
source: "/media/bbb.mp4#video=h264",
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] video will be copied, audio will be transcoded to pcmu",
|
||||
source: "/media/bbb.mp4#video=copy#audio=pcmu",
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
|
||||
source: "/media/bbb.mp4#video=h265#rotate=-90",
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
|
||||
source: "/media/bbb.mp4#video=mjpeg",
|
||||
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`,
|
||||
},
|
||||
{
|
||||
name: "https://github.com/AlexxIT/go2rtc/issues/509",
|
||||
source: "ffmpeg:test.mp4#raw=-ss 00:00:20",
|
||||
expect: `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsDevice(t *testing.T) {
|
||||
@@ -87,7 +106,7 @@ func TestParseArgsAudio(t *testing.T) {
|
||||
|
||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=opus")
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
||||
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
||||
@@ -115,28 +134,46 @@ func TestParseArgsAudio(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseArgsHwVaapi(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with rotation, should be transcoded, so select H264
|
||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720:out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
|
||||
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -f mjpeg -`, args.String())
|
||||
|
||||
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
|
||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[HTTP-MJPEG] video will be transcoded to H264",
|
||||
source: "http:///example.com#video=h264#hardware=vaapi",
|
||||
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video with rotation, should be transcoded, so select H264",
|
||||
source: "rtsp://example.com#video=h264#rotate=180#hardware=vaapi",
|
||||
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
|
||||
source: "rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi",
|
||||
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
|
||||
source: "/media/bbb.mp4#video=mjpeg#hardware=vaapi",
|
||||
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`,
|
||||
},
|
||||
{
|
||||
name: "[DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265",
|
||||
source: "device?video=0&video_size=1920x1080#video=h265#hardware=vaapi",
|
||||
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i "video=0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgsHwV4l2m2m(t *testing.T) {
|
||||
func _TestParseArgsHwV4l2m2m(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
@@ -166,7 +203,7 @@ func TestParseArgsHwRKMPP(t *testing.T) {
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -height 320 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsHwCuda(t *testing.T) {
|
||||
func _TestParseArgsHwCuda(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
@@ -184,7 +221,7 @@ func TestParseArgsHwCuda(t *testing.T) {
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsHwDxva2(t *testing.T) {
|
||||
func _TestParseArgsHwDxva2(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
@@ -206,7 +243,7 @@ func TestParseArgsHwDxva2(t *testing.T) {
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestParseArgsHwVideotoolbox(t *testing.T) {
|
||||
func _TestParseArgsHwVideotoolbox(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
@@ -226,16 +263,32 @@ func TestParseArgsHwVideotoolbox(t *testing.T) {
|
||||
|
||||
func TestDeckLink(t *testing.T) {
|
||||
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_range=tv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
}
|
||||
|
||||
func TestDrawText(t *testing.T) {
|
||||
args := parseArgs("http:///example.com#video=h264#drawtext=fontsize=12")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi")
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1:out_color_matrix=bt709:out_range=tv,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
source: "http:///example.com#video=h264#drawtext=fontsize=12",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi",
|
||||
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//go:build freebsd || netbsd || openbsd || dragonfly
|
||||
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
)
|
||||
|
||||
const (
|
||||
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
||||
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
||||
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
|
||||
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
|
||||
)
|
||||
|
||||
func ProbeAll(bin string) []*api.Source {
|
||||
return []*api.Source{
|
||||
{
|
||||
Name: runToString(bin, ProbeV4L2M2MH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeV4L2M2MH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeRKMPPH264),
|
||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeRKMPPH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ProbeHardware(bin, name string) string {
|
||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(bin, ProbeV4L2M2MH264) {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
if run(bin, ProbeRKMPPH264) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
case "h265":
|
||||
if run(bin, ProbeV4L2M2MH265) {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
if run(bin, ProbeRKMPPH265) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin || ios
|
||||
|
||||
package hardware
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
|
||||
|
||||
package hardware
|
||||
|
||||
import (
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package hardware
|
||||
|
||||
import "github.com/AlexxIT/go2rtc/internal/api"
|
||||
|
||||
@@ -19,5 +19,5 @@ func TestParseQuery(t *testing.T) {
|
||||
query, err = url.ParseQuery("hw=vaapi")
|
||||
require.Nil(t, err)
|
||||
args = parseQuery(query)
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
|
||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package virtual
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func GetInput(src string) (string, error) {
|
||||
query, err := url.ParseQuery(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// set defaults (using Add instead of Set)
|
||||
query.Add("video", "testsrc")
|
||||
query.Add("size", "1920x1080")
|
||||
query.Add("decimals", "2")
|
||||
|
||||
// https://ffmpeg.org/ffmpeg-filters.html
|
||||
video := query.Get("video")
|
||||
input := "-re -f lavfi -i " + video
|
||||
|
||||
sep := "=" // first separator
|
||||
for key, values := range query {
|
||||
value := values[0]
|
||||
|
||||
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
|
||||
switch key {
|
||||
case "color", "rate", "duration", "sar":
|
||||
case "size":
|
||||
switch value {
|
||||
case "720":
|
||||
value = "1280x720" // crf=1 -> 12 Mbps
|
||||
case "1080":
|
||||
value = "1920x1080" // crf=1 -> 25 Mbps
|
||||
case "2K":
|
||||
value = "2560x1440" // crf=1 -> 43 Mbps
|
||||
case "4K":
|
||||
value = "3840x2160" // crf=1 -> 103 Mbps
|
||||
case "8K":
|
||||
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
|
||||
}
|
||||
case "decimals":
|
||||
if video != "testsrc" {
|
||||
continue
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
input += sep + key + "=" + value
|
||||
sep = ":" // next separator
|
||||
}
|
||||
|
||||
if s := query.Get("format"); s != "" {
|
||||
input += ",format=" + s
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
func Init() {
|
||||
var conf struct {
|
||||
API struct {
|
||||
Listen string `json:"listen"`
|
||||
Listen string `yaml:"listen"`
|
||||
} `yaml:"api"`
|
||||
Mod struct {
|
||||
Config string `yaml:"config"`
|
||||
|
||||
@@ -22,12 +22,11 @@ import (
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod map[string]struct {
|
||||
Pin string `json:"pin"`
|
||||
Name string `json:"name"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DevicePrivate string `json:"device_private"`
|
||||
Pairings []string `json:"pairings"`
|
||||
//Listen string `json:"listen"`
|
||||
Pin string `yaml:"pin"`
|
||||
Name string `yaml:"name"`
|
||||
DeviceID string `yaml:"device_id"`
|
||||
DevicePrivate string `yaml:"device_private"`
|
||||
Pairings []string `yaml:"pairings"`
|
||||
} `yaml:"homekit"`
|
||||
}
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
## Stream as ASCII to Terminal
|
||||
|
||||
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
||||
|
||||
**Tips**
|
||||
|
||||
- this feature works only with MJPEG codec (use transcoding)
|
||||
- choose a low frame rate (FPS)
|
||||
- choose the width and height to fit in your terminal
|
||||
- different terminals support different numbers of colours (8, 256, rgb)
|
||||
- escape text param with urlencode
|
||||
- you can stream any camera or file from a disc
|
||||
|
||||
**go2rtc.yaml** - transcoding to MJPEG, terminal size - 210x59 (16/9), fps - 10
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
gamazda: ffmpeg:gamazda.mp4#video=mjpeg#hardware#width=210#height=59#raw=-r 10
|
||||
```
|
||||
|
||||
**API params**
|
||||
|
||||
- `color` - foreground color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
- example: `30` (black), `37` (white), `38;5;226` (yellow)
|
||||
- `back` - background color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
- example: `40` (black), `47` (white), `48;5;226` (yellow)
|
||||
- `text` - character set, values: empty, one char, `block`, list of chars (in order of brightness)
|
||||
- example: `%20` (space), `block` (keyword for block elements), `ox` (two chars)
|
||||
|
||||
**Examples**
|
||||
|
||||
```bash
|
||||
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda"
|
||||
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&color=256"
|
||||
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=256&text=%20"
|
||||
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20"
|
||||
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld"
|
||||
```
|
||||
+15
-26
@@ -5,12 +5,14 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ascii"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
func Init() {
|
||||
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
||||
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
||||
api.HandleFunc("api/stream.ascii", handlerStream)
|
||||
|
||||
ws.HandleFunc("mjpeg", handlerWS)
|
||||
}
|
||||
@@ -57,6 +60,8 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
|
||||
case core.CodecJPEG:
|
||||
b = mjpeg.FixJPEG(b)
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
@@ -97,38 +102,22 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||
h.Set("Cache-Control", "no-cache")
|
||||
h.Set("Connection", "close")
|
||||
h.Set("Pragma", "no-cache")
|
||||
|
||||
wr := &writer{wr: w, buf: []byte(header)}
|
||||
_, _ = cons.WriteTo(wr)
|
||||
if strings.HasSuffix(r.URL.Path, "mjpeg") {
|
||||
wr := mjpeg.NewWriter(w)
|
||||
_, _ = cons.WriteTo(wr)
|
||||
} else {
|
||||
cons.Type = "ASCII passive consumer "
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
|
||||
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
||||
|
||||
type writer struct {
|
||||
wr io.Writer
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (n int, err error) {
|
||||
w.buf = w.buf[:len(header)]
|
||||
w.buf = append(w.buf, strconv.Itoa(len(p))...)
|
||||
w.buf = append(w.buf, "\r\n\r\n"...)
|
||||
w.buf = append(w.buf, p...)
|
||||
w.buf = append(w.buf, "\r\n"...)
|
||||
|
||||
// Chrome bug: mjpeg image always shows the second to last image
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
||||
if n, err = w.wr.Write(w.buf); err == nil {
|
||||
w.wr.(http.Flusher).Flush()
|
||||
query := r.URL.Query()
|
||||
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
|
||||
_, _ = cons.WriteTo(wr)
|
||||
}
|
||||
|
||||
return
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
|
||||
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -163,7 +152,7 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -91,8 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
src := query.Get("src")
|
||||
stream := streams.Get(src)
|
||||
stream := streams.GetOrPatch(query)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
|
||||
@@ -50,7 +50,7 @@ func Init() {
|
||||
|
||||
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
|
||||
|
||||
webrtc.AddCandidate(address, "tcp")
|
||||
webrtc.AddCandidate("tcp", address)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ func Init() {
|
||||
Username string `yaml:"username" json:"-"`
|
||||
Password string `yaml:"password" json:"-"`
|
||||
DefaultQuery string `yaml:"default_query" json:"default_query"`
|
||||
PacketSize uint16 `yaml:"pkt_size"`
|
||||
PacketSize uint16 `yaml:"pkt_size" json:"pkt_size,omitempty"`
|
||||
} `yaml:"rtsp"`
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
||||
|
||||
if closer != nil {
|
||||
if err := conn.Handle(); err != nil {
|
||||
log.Debug().Msgf("[rtsp] handle=%s", err)
|
||||
log.Debug().Err(err).Msg("[rtsp] handle")
|
||||
}
|
||||
|
||||
closer()
|
||||
|
||||
@@ -3,18 +3,17 @@ package streams
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
// support for multiple simultaneous requests from different consumers
|
||||
consN := atomic.AddInt32(&s.requests, 1) - 1
|
||||
// support for multiple simultaneous pending from different consumers
|
||||
consN := s.pending.Add(1) - 1
|
||||
|
||||
var prodErrors []error
|
||||
var prodErrors = make([]error, len(s.producers))
|
||||
var prodMedias []*core.Media
|
||||
var prods []*Producer // matched producers for consumer
|
||||
var prodStarts []*Producer
|
||||
|
||||
// Step 1. Get consumer medias
|
||||
consMedias := cons.GetMedias()
|
||||
@@ -23,15 +22,20 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
producers:
|
||||
for prodN, prod := range s.producers {
|
||||
if prodErrors[prodN] != nil {
|
||||
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = prod.Dial(); err != nil {
|
||||
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
|
||||
prodErrors = append(prodErrors, err)
|
||||
log.Trace().Err(err).Msgf("[streams] dial cons=%d prod=%d", consN, prodN)
|
||||
prodErrors[prodN] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2. Get producer medias (not tracks yet)
|
||||
for _, prodMedia := range prod.GetMedias() {
|
||||
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
|
||||
log.Trace().Msgf("[streams] check cons=%d prod=%d media=%s", consN, prodN, prodMedia)
|
||||
prodMedias = append(prodMedias, prodMedia)
|
||||
|
||||
// Step 3. Match consumer/producer codecs list
|
||||
@@ -44,11 +48,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
|
||||
switch prodMedia.Direction {
|
||||
case core.DirectionRecvonly:
|
||||
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
|
||||
log.Trace().Msgf("[streams] match cons=%d <= prod=%d", consN, prodN)
|
||||
|
||||
// Step 4. Get recvonly track from producer
|
||||
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't get track")
|
||||
prodErrors[prodN] = err
|
||||
continue
|
||||
}
|
||||
// Step 5. Add track to consumer
|
||||
@@ -68,11 +73,12 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
// Step 5. Add track to producer
|
||||
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
|
||||
log.Info().Err(err).Msg("[streams] can't add track")
|
||||
prodErrors[prodN] = err
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
prods = append(prods, prod)
|
||||
prodStarts = append(prodStarts, prod)
|
||||
|
||||
if !consMedia.MatchAll() {
|
||||
break producers
|
||||
@@ -82,11 +88,11 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
}
|
||||
|
||||
// stop producers if they don't have readers
|
||||
if atomic.AddInt32(&s.requests, -1) == 0 {
|
||||
if s.pending.Add(-1) == 0 {
|
||||
s.stopProducers()
|
||||
}
|
||||
|
||||
if len(prods) == 0 {
|
||||
if len(prodStarts) == 0 {
|
||||
return formatError(consMedias, prodMedias, prodErrors)
|
||||
}
|
||||
|
||||
@@ -95,7 +101,7 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
s.mu.Unlock()
|
||||
|
||||
// there may be duplicates, but that's not a problem
|
||||
for _, prod := range prods {
|
||||
for _, prod := range prodStarts {
|
||||
prod.start()
|
||||
}
|
||||
|
||||
@@ -103,6 +109,20 @@ func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||
}
|
||||
|
||||
func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error {
|
||||
// 1. Return errors if any not nil
|
||||
var text string
|
||||
|
||||
for _, err := range prodErrors {
|
||||
if err != nil {
|
||||
text = appendString(text, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(text) != 0 {
|
||||
return errors.New("streams: " + text)
|
||||
}
|
||||
|
||||
// 2. Return "codecs not matched"
|
||||
if prodMedias != nil {
|
||||
var prod, cons string
|
||||
|
||||
@@ -125,16 +145,7 @@ func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error
|
||||
return errors.New("streams: codecs not matched: " + prod + " => " + cons)
|
||||
}
|
||||
|
||||
if prodErrors != nil {
|
||||
var text string
|
||||
|
||||
for _, err := range prodErrors {
|
||||
text = appendString(text, err.Error())
|
||||
}
|
||||
|
||||
return errors.New("streams: " + text)
|
||||
}
|
||||
|
||||
// 3. Return unknown error
|
||||
return errors.New("streams: unknown error")
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
)
|
||||
|
||||
func ParseQuery(s string) url.Values {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
params := url.Values{}
|
||||
for _, key := range strings.Split(s, "#") {
|
||||
var value string
|
||||
|
||||
@@ -245,10 +245,10 @@ func (p *Producer) stop() {
|
||||
|
||||
switch p.state {
|
||||
case stateExternal:
|
||||
log.Debug().Msgf("[streams] can't stop external producer")
|
||||
log.Trace().Msgf("[streams] skip stop external producer")
|
||||
return
|
||||
case stateNone:
|
||||
log.Debug().Msgf("[streams] can't stop none producer")
|
||||
log.Trace().Msgf("[streams] skip stop none producer")
|
||||
return
|
||||
case stateStart:
|
||||
p.workerID++
|
||||
|
||||
@@ -3,6 +3,7 @@ package streams
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
@@ -11,7 +12,7 @@ type Stream struct {
|
||||
producers []*Producer
|
||||
consumers []core.Consumer
|
||||
mu sync.Mutex
|
||||
requests int32
|
||||
pending atomic.Int32
|
||||
}
|
||||
|
||||
func NewStream(source any) *Stream {
|
||||
@@ -22,8 +23,13 @@ func NewStream(source any) *Stream {
|
||||
}
|
||||
case []any:
|
||||
s := new(Stream)
|
||||
for _, source := range source {
|
||||
s.producers = append(s.producers, NewProducer(source.(string)))
|
||||
for _, src := range source {
|
||||
str, ok := src.(string)
|
||||
if !ok {
|
||||
log.Error().Msgf("[stream] NewStream: Expected string, got %v", src)
|
||||
continue
|
||||
}
|
||||
s.producers = append(s.producers, NewProducer(str))
|
||||
}
|
||||
return s
|
||||
case map[string]any:
|
||||
@@ -82,6 +88,11 @@ func (s *Stream) RemoveProducer(prod core.Producer) {
|
||||
}
|
||||
|
||||
func (s *Stream) stopProducers() {
|
||||
if s.pending.Load() > 0 {
|
||||
log.Trace().Msg("[streams] skip stop pending producer")
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
producers:
|
||||
for _, producer := range s.producers {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func TestRecursion(t *testing.T) {
|
||||
// create stream with some source
|
||||
stream1 := New("from_yaml", "does not matter")
|
||||
stream1 := New("from_yaml", "does_not_matter")
|
||||
require.Len(t, streams, 1)
|
||||
|
||||
// ask another unnamed stream that links go2rtc
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -148,7 +150,27 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Not sure about all this API. Should be rewrited...
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
api.ResponsePrettyJSON(w, streams[src])
|
||||
stream := Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := probe.NewProbe(query)
|
||||
if len(cons.Medias) != 0 {
|
||||
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||
cons.UserAgent = r.UserAgent()
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
api.ResponsePrettyJSON(w, stream)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
} else {
|
||||
api.ResponsePrettyJSON(w, streams[src])
|
||||
}
|
||||
|
||||
case "PUT":
|
||||
name := query.Get("name")
|
||||
|
||||
@@ -1,13 +1,105 @@
|
||||
What you should to know about WebRTC:
|
||||
|
||||
- It's almost always a **direct [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) connection** from your browser to go2rtc app
|
||||
- When you use Home Assistant, Frigate, Nginx, Nabu Casa, Cloudflare and other software - they are only **involved in establishing** the connection, but they are **not involved in transferring** media data
|
||||
- WebRTC media cannot be transferred inside an HTTP connection
|
||||
- Usually, WebRTC uses random UDP ports on client and server side to establish a connection
|
||||
- Usually, WebRTC uses public [STUN](https://en.wikipedia.org/wiki/STUN) servers to establish a connection outside LAN, such servers are only needed to establish a connection and are not involved in data transfer
|
||||
- Usually, WebRTC will automatically discover all of your local addresses and all of your public addresses and try to establish a connection
|
||||
|
||||
If an external connection via STUN is used:
|
||||
|
||||
- Uses [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology to bypass NAT even if you not open your server to the World
|
||||
- For about 20% of users, the techology will not work because of the [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/)
|
||||
- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate
|
||||
|
||||
## Default config
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
listen: ":8555/tcp"
|
||||
ice_servers:
|
||||
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
- supported TCP: fixed port (default), disabled
|
||||
- supported UDP: random port (default), fixed port
|
||||
**Important!** This example is not for copypasting!
|
||||
|
||||
| Config examples | TCP | UDP |
|
||||
|-----------------------|-------|--------|
|
||||
| `listen: ":8555/tcp"` | fixed | random |
|
||||
| `listen: ":8555"` | fixed | fixed |
|
||||
| `listen: ""` | no | random |
|
||||
```yaml
|
||||
webrtc:
|
||||
# fix local TCP or UDP or both ports for WebRTC media
|
||||
listen: ":8555/tcp" # address of your local server
|
||||
|
||||
# add additional host candidates manually
|
||||
# order is important, the first will have a higher priority
|
||||
candidates:
|
||||
- 216.58.210.174:8555 # if you have static public IP-address
|
||||
- stun:8555 # if you have dynamic public IP-address
|
||||
- home.duckdns.org:8555 # if you have domain
|
||||
|
||||
# add custom STUN and TURN servers
|
||||
# use `ice_servers: []` for remove defaults and leave empty
|
||||
ice_servers:
|
||||
- urls: [ stun:stun1.l.google.com:19302 ]
|
||||
- urls: [ turn:123.123.123.123:3478 ]
|
||||
username: your_user
|
||||
credential: your_pass
|
||||
|
||||
# optional filter list for auto discovery logic
|
||||
# some settings only make sense if you don't specify a fixed UDP port
|
||||
filters:
|
||||
# list of host candidates from auto discovery to be sent
|
||||
# including candidates from the `listen` option
|
||||
# use `candidates: []` to remove all auto discovery candidates
|
||||
candidates: [ 192.168.1.123 ]
|
||||
|
||||
# list of network types to be used for connection
|
||||
# including candidates from the `listen` option
|
||||
networks: [ udp4, udp6, tcp4, tcp6 ]
|
||||
|
||||
# list of interfaces to be used for connection
|
||||
# not related to the `listen` option
|
||||
interfaces: [ eno1 ]
|
||||
|
||||
# list of host IP-addresses to be used for connection
|
||||
# not related to the `listen` option
|
||||
ips: [ 192.168.1.123 ]
|
||||
|
||||
# range for random UDP ports [min, max] to be used for connection
|
||||
# not related to the `listen` option
|
||||
udp_ports: [ 50000, 50100 ]
|
||||
```
|
||||
|
||||
By default go2rtc uses **fixed TCP** port and multiple **random UDP** ports for each WebRTC connection - `listen: ":8555/tcp"`.
|
||||
|
||||
You can set **fixed TCP** and **fixed UDP** port for all connections - `listen: ":8555"`. This may has lower performance, but it's your choice.
|
||||
|
||||
Don't know why, but you can disable TCP port and leave only random UDP ports - `listen: ""`.
|
||||
|
||||
## Config filters
|
||||
|
||||
Filters allow you to exclude unnecessary candidates. Extra candidates don't make your connection worse or better. But the wrong filter settings can break everything. Skip this setting if you don't understand it.
|
||||
|
||||
For example, go2rtc is installed on the host system. And there are unnecessary interfaces. You can keep only the relevant via `interfaces` or `ips` options. You can also exclude IPv6 candidates if your server supports them but your home network does not.
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
listen: ":8555/tcp" # use fixed TCP port and random UDP ports
|
||||
filters:
|
||||
ips: [ 192.168.1.2 ] # IP-address of your server
|
||||
networks: [ udp4, tcp4 ] # skip IPv6, if it's not supported for you
|
||||
```
|
||||
|
||||
For example, go2rtc inside closed docker container (ex. [Frigate](https://frigate.video/)). You shouldn't filter docker interfaces, otherwise go2rtc will not be able to connect anywhere. But you can filter the docker candidates because no one can connect to them.
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
listen: ":8555" # use fixed TCP and UDP ports
|
||||
candidates: [ 192.168.1.2:8555 ] # add manual host candidate (use docker port forwarding)
|
||||
filters:
|
||||
candidates: [] # skip all internal docker candidates
|
||||
```
|
||||
|
||||
## Userful links
|
||||
|
||||
|
||||
@@ -2,57 +2,60 @@ package webrtc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
Host string
|
||||
Port string
|
||||
Network string
|
||||
Offset int
|
||||
host string
|
||||
Port string
|
||||
Network string
|
||||
Priority uint32
|
||||
}
|
||||
|
||||
func (a *Address) Marshal() string {
|
||||
host := a.Host
|
||||
if host == "stun" {
|
||||
func (a *Address) Host() string {
|
||||
if a.host == "stun" {
|
||||
ip, err := webrtc.GetCachedPublicIP()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
host = ip.String()
|
||||
return ip.String()
|
||||
}
|
||||
return a.host
|
||||
}
|
||||
|
||||
switch a.Network {
|
||||
case "udp":
|
||||
return webrtc.CandidateManualHostUDP(host, a.Port, a.Offset)
|
||||
case "tcp":
|
||||
return webrtc.CandidateManualHostTCPPassive(host, a.Port, a.Offset)
|
||||
func (a *Address) Marshal() string {
|
||||
if host := a.Host(); host != "" {
|
||||
return webrtc.CandidateICE(a.Network, host, a.Port, a.Priority)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
var addresses []*Address
|
||||
var filters webrtc.Filters
|
||||
|
||||
func AddCandidate(network, address string) {
|
||||
if network == "" {
|
||||
AddCandidate("tcp", address)
|
||||
AddCandidate("udp", address)
|
||||
return
|
||||
}
|
||||
|
||||
func AddCandidate(address, network string) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offset := -1 - len(addresses) // every next candidate will have a lower priority
|
||||
// start from 1, so manual candidates will be lower than built-in
|
||||
// and every next candidate will have a lower priority
|
||||
candidateIndex := 1 + len(addresses)
|
||||
|
||||
switch network {
|
||||
case "tcp", "udp":
|
||||
addresses = append(addresses, &Address{host, port, network, offset})
|
||||
default:
|
||||
addresses = append(
|
||||
addresses, &Address{host, port, "udp", offset}, &Address{host, port, "tcp", offset},
|
||||
)
|
||||
}
|
||||
priority := webrtc.CandidateHostPriority(network, candidateIndex)
|
||||
addresses = append(addresses, &Address{host, port, network, priority})
|
||||
}
|
||||
|
||||
func GetCandidates() (candidates []string) {
|
||||
@@ -64,6 +67,38 @@ func GetCandidates() (candidates []string) {
|
||||
return
|
||||
}
|
||||
|
||||
// FilterCandidate return true if candidate passed the check
|
||||
func FilterCandidate(candidate *pion.ICECandidate) bool {
|
||||
if candidate == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// host candidate should be in the hosts list
|
||||
if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {
|
||||
if !slices.Contains(filters.Candidates, candidate.Address) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if filters.Networks != nil {
|
||||
networkType := NetworkType(candidate.Protocol.String(), candidate.Address)
|
||||
if !slices.Contains(filters.Networks, networkType) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NetworkType convert tcp/udp network to tcp4/tcp6/udp4/udp6
|
||||
func NetworkType(network, host string) string {
|
||||
if strings.IndexByte(host, ':') >= 0 {
|
||||
return network + "6"
|
||||
} else {
|
||||
return network + "4"
|
||||
}
|
||||
}
|
||||
|
||||
func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
|
||||
tr.WithContext(func(ctx map[any]any) {
|
||||
if candidates, ok := ctx["candidate"].([]string); ok {
|
||||
@@ -86,30 +121,6 @@ func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func syncCanditates(answer string) (string, error) {
|
||||
if len(addresses) == 0 {
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err := sd.Unmarshal([]byte(answer)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
md := sd.MediaDescriptions[0]
|
||||
|
||||
for _, candidate := range GetCandidates() {
|
||||
md.WithPropertyAttribute(candidate)
|
||||
}
|
||||
|
||||
data, err := sd.Marshal()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func candidateHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
// process incoming candidate in sync function
|
||||
tr.WithContext(func(ctx map[any]any) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
@@ -48,7 +49,9 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
case "http", "https":
|
||||
if format == "wyze" {
|
||||
if format == "milestone" {
|
||||
return milestoneClient(rawURL, query)
|
||||
} else if format == "wyze" {
|
||||
// https://github.com/mrlt8/docker-wyze-bridge
|
||||
return wyzeClient(rawURL)
|
||||
} else {
|
||||
@@ -80,6 +83,7 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
|
||||
// waiter will wait PC error or WS error or nil (connection OK)
|
||||
var connState core.Waiter
|
||||
var connMu sync.Mutex
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/WebSocket async"
|
||||
@@ -89,7 +93,9 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
case *pion.ICECandidate:
|
||||
s := msg.ToJSON().Candidate
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local ")
|
||||
connMu.Lock()
|
||||
_ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s})
|
||||
connMu.Unlock()
|
||||
|
||||
case pion.PeerConnectionState:
|
||||
switch msg {
|
||||
@@ -116,9 +122,9 @@ func go2rtcClient(url string) (core.Producer, error) {
|
||||
|
||||
// 4. Send offer
|
||||
msg := &ws.Message{Type: "webrtc/offer", Value: offer}
|
||||
if err = conn.WriteJSON(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connMu.Lock()
|
||||
_ = conn.WriteJSON(msg)
|
||||
connMu.Unlock()
|
||||
|
||||
// 5. Get answer
|
||||
if err = conn.ReadJSON(msg); err != nil {
|
||||
@@ -189,10 +195,10 @@ func whepClient(url string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(offer))
|
||||
req.Header.Set("Content-Type", MimeSDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", MimeSDP)
|
||||
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
// This package handles the Milestone WebRTC session lifecycle, including authentication,
|
||||
// session creation, and session update with an SDP answer. It is designed to be used with
|
||||
// a specific URL format that encodes session parameters. For example:
|
||||
// webrtc:https://milestone-host/api#format=milestone#username=User#password=TestPassword#cameraId=a539f254-af05-4d67-a1bb-cd9b3c74d122
|
||||
//
|
||||
// https://github.com/milestonesys/mipsdk-samples-protocol/tree/main/WebRTC_JavaScript
|
||||
|
||||
type milestoneAPI struct {
|
||||
url string
|
||||
query url.Values
|
||||
token string
|
||||
sessionID string
|
||||
}
|
||||
|
||||
func (m *milestoneAPI) GetToken() error {
|
||||
data := url.Values{
|
||||
"client_id": {"GrantValidatorClient"},
|
||||
"grant_type": {"password"},
|
||||
"username": m.query["username"],
|
||||
"password": m.query["password"],
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", m.url+"/IDP/connect/token", strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// support httpx protocol
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New("milesone: authentication failed: " + res.Status)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err = json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, ok := payload["access_token"].(string)
|
||||
if !ok {
|
||||
return errors.New("milesone: token not found in the response")
|
||||
}
|
||||
|
||||
m.token = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFloat(s string) float64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
}
|
||||
|
||||
func (m *milestoneAPI) GetOffer() (string, error) {
|
||||
request := struct {
|
||||
CameraId string `json:"cameraId"`
|
||||
StreamId string `json:"streamId,omitempty"`
|
||||
PlaybackTimeNode struct {
|
||||
PlaybackTime string `json:"playbackTime,omitempty"`
|
||||
SkipGaps bool `json:"skipGaps,omitempty"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
} `json:"playbackTimeNode,omitempty"`
|
||||
//ICEServers []string `json:"iceServers,omitempty"`
|
||||
//Resolution string `json:"resolution,omitempty"`
|
||||
}{
|
||||
CameraId: m.query.Get("cameraId"),
|
||||
StreamId: m.query.Get("streamId"),
|
||||
}
|
||||
request.PlaybackTimeNode.PlaybackTime = m.query.Get("playbackTime")
|
||||
request.PlaybackTimeNode.SkipGaps = m.query.Has("skipGaps")
|
||||
request.PlaybackTimeNode.Speed = parseFloat(m.query.Get("speed"))
|
||||
|
||||
data, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", m.url+"/REST/v1/WebRTC/Session", bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+m.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return "", errors.New("milesone: create session: " + res.Status)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
SessionId string `json:"sessionId"`
|
||||
OfferSDP string `json:"offerSDP"`
|
||||
}
|
||||
if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var offer pion.SessionDescription
|
||||
if err = json.Unmarshal([]byte(response.OfferSDP), &offer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
m.sessionID = response.SessionId
|
||||
|
||||
return offer.SDP, nil
|
||||
}
|
||||
|
||||
func (m *milestoneAPI) SetAnswer(sdp string) error {
|
||||
answer := pion.SessionDescription{
|
||||
Type: pion.SDPTypeAnswer,
|
||||
SDP: sdp,
|
||||
}
|
||||
data, err := json.Marshal(answer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := struct {
|
||||
AnswerSDP string `json:"answerSDP"`
|
||||
}{
|
||||
AnswerSDP: string(data),
|
||||
}
|
||||
if data, err = json.Marshal(request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PATCH", m.url+"/REST/v1/WebRTC/Session/"+m.sessionID, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+m.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New("milesone: patch session: " + res.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
mc := &milestoneAPI{url: rawURL, query: query}
|
||||
if err := mc.GetToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api, err := webrtc.NewAPI()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := pion.Configuration{}
|
||||
pc, err := api.NewPeerConnection(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod := webrtc.NewConn(pc)
|
||||
prod.Desc = "WebRTC/Milestone"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
|
||||
offer, err := mc.GetOffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = prod.SetOffer(offer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
answer, err := prod.GetAnswer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = mc.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
@@ -178,10 +178,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
answer, err := prod.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = syncCanditates(answer)
|
||||
}
|
||||
answer, err := prod.GetCompleteAnswer(GetCandidates(), FilterCandidate)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
+10
-15
@@ -20,6 +20,7 @@ func Init() {
|
||||
Listen string `yaml:"listen"`
|
||||
Candidates []string `yaml:"candidates"`
|
||||
IceServers []pion.ICEServer `yaml:"ice_servers"`
|
||||
Filters webrtc.Filters `yaml:"filters"`
|
||||
} `yaml:"webrtc"`
|
||||
}
|
||||
|
||||
@@ -32,20 +33,15 @@ func Init() {
|
||||
|
||||
log = app.GetLogger("webrtc")
|
||||
|
||||
filters = cfg.Mod.Filters
|
||||
|
||||
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
|
||||
|
||||
var candidateHost []string
|
||||
for _, candidate := range cfg.Mod.Candidates {
|
||||
if strings.HasPrefix(candidate, "host:") {
|
||||
candidateHost = append(candidateHost, candidate[5:])
|
||||
continue
|
||||
}
|
||||
|
||||
AddCandidate(candidate, network)
|
||||
AddCandidate(network, candidate)
|
||||
}
|
||||
|
||||
// create pionAPI with custom codecs list and custom network settings
|
||||
serverAPI, err := webrtc.NewServerAPI(address, network, candidateHost)
|
||||
serverAPI, err := webrtc.NewServerAPI(network, address, &filters)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
@@ -55,8 +51,7 @@ func Init() {
|
||||
clientAPI := serverAPI
|
||||
|
||||
if address != "" {
|
||||
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
||||
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
|
||||
clientAPI, _ = webrtc.NewAPI()
|
||||
}
|
||||
|
||||
@@ -139,6 +134,9 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
|
||||
case *pion.ICECandidate:
|
||||
if !FilterCandidate(msg) {
|
||||
return
|
||||
}
|
||||
_ = sendAnswer.Wait()
|
||||
|
||||
s := msg.ToJSON().Candidate
|
||||
@@ -248,10 +246,7 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
|
||||
stream.AddProducer(conn)
|
||||
}
|
||||
|
||||
answer, err = conn.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = syncCanditates(answer)
|
||||
}
|
||||
answer, err = conn.GetCompleteAnswer(GetCandidates(), FilterCandidate)
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
## Useful links
|
||||
|
||||
- https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
- https://paulbourke.net/dataformats/asciiart/
|
||||
- https://github.com/kutuluk/xterm-color-chart
|
||||
- https://github.com/hugomd/parrot.live
|
||||
@@ -0,0 +1,166 @@
|
||||
package ascii
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func NewWriter(w io.Writer, foreground, background, text string) io.Writer {
|
||||
// once clear screen
|
||||
_, _ = w.Write([]byte(csiClear))
|
||||
|
||||
// every frame - move to home
|
||||
a := &writer{wr: w, buf: []byte(csiHome)}
|
||||
|
||||
// https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
switch foreground {
|
||||
case "":
|
||||
case "8":
|
||||
a.color = func(r, g, b uint8) {
|
||||
idx := xterm256color(r, g, b, 8)
|
||||
a.appendEsc(fmt.Sprintf("\033[%dm", 30+idx))
|
||||
|
||||
}
|
||||
case "256":
|
||||
a.color = func(r, g, b uint8) {
|
||||
idx := xterm256color(r, g, b, 255)
|
||||
a.appendEsc(fmt.Sprintf("\033[38;5;%dm", idx))
|
||||
}
|
||||
case "rgb":
|
||||
a.color = func(r, g, b uint8) {
|
||||
a.appendEsc(fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b))
|
||||
}
|
||||
default:
|
||||
a.buf = append(a.buf, "\033["+foreground+"m"...)
|
||||
}
|
||||
|
||||
switch background {
|
||||
case "":
|
||||
case "8":
|
||||
a.color = func(r, g, b uint8) {
|
||||
idx := xterm256color(r, g, b, 8)
|
||||
a.appendEsc(fmt.Sprintf("\033[%dm", 40+idx))
|
||||
}
|
||||
case "256":
|
||||
a.color = func(r, g, b uint8) {
|
||||
idx := xterm256color(r, g, b, 255)
|
||||
a.appendEsc(fmt.Sprintf("\033[48;5;%dm", idx))
|
||||
}
|
||||
case "rgb":
|
||||
a.color = func(r, g, b uint8) {
|
||||
a.appendEsc(fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b))
|
||||
}
|
||||
default:
|
||||
a.buf = append(a.buf, "\033["+background+"m"...)
|
||||
}
|
||||
|
||||
a.pre = len(a.buf) // save prefix size
|
||||
|
||||
if len(text) == 1 {
|
||||
// fast 1 symbol version
|
||||
a.text = func(_, _, _ uint32) {
|
||||
a.buf = append(a.buf, text[0])
|
||||
}
|
||||
} else {
|
||||
switch text {
|
||||
case "":
|
||||
text = ` .::--~~==++**##%%$@` // default for empty text
|
||||
case "block":
|
||||
text = " ░░▒▒▓▓█" // https://en.wikipedia.org/wiki/Block_Elements
|
||||
}
|
||||
|
||||
if runes := []rune(text); len(runes) != len(text) {
|
||||
k := float32(len(runes)-1) / 255
|
||||
a.text = func(r, g, b uint32) {
|
||||
i := gray(r, g, b, k)
|
||||
a.buf = utf8.AppendRune(a.buf, runes[i])
|
||||
}
|
||||
} else {
|
||||
k := float32(len(text)-1) / 255
|
||||
a.text = func(r, g, b uint32) {
|
||||
i := gray(r, g, b, k)
|
||||
a.buf = append(a.buf, text[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
type writer struct {
|
||||
wr io.Writer
|
||||
buf []byte
|
||||
pre int
|
||||
esc string
|
||||
color func(r, g, b uint8)
|
||||
text func(r, g, b uint32)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/37774983/clearing-the-screen-by-printing-a-character
|
||||
const csiClear = "\033[2J"
|
||||
const csiHome = "\033[H"
|
||||
|
||||
func (a *writer) Write(p []byte) (n int, err error) {
|
||||
img, err := jpeg.Decode(bytes.NewReader(p))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
a.buf = a.buf[:a.pre] // restore prefix
|
||||
|
||||
w := img.Bounds().Dx()
|
||||
h := img.Bounds().Dy()
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
r, g, b, _ := img.At(x, y).RGBA()
|
||||
if a.color != nil {
|
||||
a.color(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||
}
|
||||
a.text(r, g, b)
|
||||
}
|
||||
a.buf = append(a.buf, '\n')
|
||||
}
|
||||
|
||||
a.appendEsc("\033[0m")
|
||||
|
||||
if _, err = a.wr.Write(a.buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
a.wr.(http.Flusher).Flush()
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// appendEsc - append ESC code to buffer, and skip duplicates
|
||||
func (a *writer) appendEsc(s string) {
|
||||
if a.esc != s {
|
||||
a.esc = s
|
||||
a.buf = append(a.buf, s...)
|
||||
}
|
||||
}
|
||||
|
||||
func gray(r, g, b uint32, k float32) uint8 {
|
||||
gr := (19595*r + 38470*g + 7471*b + 1<<15) >> 24 // uint8
|
||||
return uint8(float32(gr) * k)
|
||||
}
|
||||
|
||||
const x256r = "\x00\x80\x00\x80\x00\x80\x00\xc0\x80\xff\x00\xff\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee"
|
||||
const x256g = "\x00\x00\x80\x80\x00\x00\x80\xc0\x80\x00\xff\xff\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x5f\x5f\x5f\x5f\x5f\x5f\x87\x87\x87\x87\x87\x87\xaf\xaf\xaf\xaf\xaf\xaf\xd7\xd7\xd7\xd7\xd7\xd7\xff\xff\xff\xff\xff\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee"
|
||||
const x256b = "\x00\x00\x00\x00\x80\x80\x80\xc0\x80\x00\x00\x00\xff\xff\xff\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x00\x5f\x87\xaf\xd7\xff\x08\x12\x1c\x26\x30\x3a\x44\x4e\x58\x60\x66\x76\x80\x8a\x94\x9e\xa8\xb2\xbc\xc6\xd0\xda\xe4\xee"
|
||||
|
||||
func xterm256color(r, g, b uint8, n int) (index uint8) {
|
||||
best := uint16(0xFFFF)
|
||||
for i := 0; i < n; i++ {
|
||||
diff := uint16(r-x256r[i]) + uint16(g-x256g[i]) + uint16(b-x256b[i])
|
||||
if diff < best {
|
||||
best = diff
|
||||
index = uint8(i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -38,15 +38,6 @@ func RandString(size, base byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Any(errs ...error) error {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
|
||||
+35
-16
@@ -73,9 +73,6 @@ func (t *Receiver) Replace(target *Receiver) {
|
||||
// move this receiver senders to new receiver
|
||||
t.mu.Lock()
|
||||
senders := t.senders
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/828
|
||||
// TODO: fix the reason, not the consequence
|
||||
t.senders = nil
|
||||
t.mu.Unlock()
|
||||
|
||||
target.mu.Lock()
|
||||
@@ -116,7 +113,12 @@ func NewSender(media *Media, codec *Codec) *Sender {
|
||||
type HandlerFunc func(packet *rtp.Packet)
|
||||
|
||||
func (s *Sender) HandleRTP(track *Receiver) {
|
||||
bufferSize := 100
|
||||
s.Bind(track)
|
||||
go s.worker(track)
|
||||
}
|
||||
|
||||
func (s *Sender) Bind(track *Receiver) {
|
||||
var bufferSize uint16
|
||||
|
||||
if GetKind(track.Codec.Name) == KindVideo {
|
||||
if track.Codec.IsRTP() {
|
||||
@@ -126,6 +128,8 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
||||
} else {
|
||||
bufferSize = 50
|
||||
}
|
||||
} else {
|
||||
bufferSize = 100
|
||||
}
|
||||
|
||||
buffer := make(chan *rtp.Packet, bufferSize)
|
||||
@@ -136,28 +140,43 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
||||
}
|
||||
track.senders[s] = buffer
|
||||
track.mu.Unlock()
|
||||
|
||||
s.mu.Lock()
|
||||
s.receivers = append(s.receivers, track)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
go func() {
|
||||
// read packets from buffer channel until it will be closed
|
||||
func (s *Sender) worker(track *Receiver) {
|
||||
track.mu.Lock()
|
||||
buffer := track.senders[s]
|
||||
track.mu.Unlock()
|
||||
|
||||
// read packets from buffer channel until it will be closed
|
||||
if buffer != nil {
|
||||
for packet := range buffer {
|
||||
s.bytes += len(packet.Payload)
|
||||
s.Handler(packet)
|
||||
}
|
||||
}
|
||||
|
||||
// remove current receiver from list
|
||||
// it can only happen when receiver close buffer channel
|
||||
s.mu.Lock()
|
||||
for i, receiver := range s.receivers {
|
||||
if receiver == track {
|
||||
s.receivers = append(s.receivers[:i], s.receivers[i+1:]...)
|
||||
break
|
||||
}
|
||||
// remove current receiver from list
|
||||
// it can only happen when receiver close buffer channel
|
||||
s.mu.Lock()
|
||||
for i, receiver := range s.receivers {
|
||||
if receiver == track {
|
||||
s.receivers = append(s.receivers[:i], s.receivers[i+1:]...)
|
||||
break
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Sender) Start() {
|
||||
s.mu.Lock()
|
||||
for _, track := range s.receivers {
|
||||
go s.worker(track)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Sender) Close() {
|
||||
|
||||
@@ -141,7 +141,7 @@ func TestNewReader(t *testing.T) {
|
||||
name: "obs-connect",
|
||||
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
|
||||
expect: []any{
|
||||
"connect", 1,
|
||||
"connect", float64(1),
|
||||
map[string]any{
|
||||
"app": "app1/stream1",
|
||||
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||
|
||||
@@ -67,11 +67,15 @@ func EmitNalus(nals []byte, isAVC bool, emit func([]byte)) {
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
end := 4 + binary.BigEndian.Uint32(nals)
|
||||
emit(nals[4:end])
|
||||
if int(end) >= len(nals) {
|
||||
n := uint32(len(nals))
|
||||
if n < 4 {
|
||||
break
|
||||
}
|
||||
end := 4 + binary.BigEndian.Uint32(nals)
|
||||
if n < end {
|
||||
break
|
||||
}
|
||||
emit(nals[4:end])
|
||||
nals = nals[end:]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ func ServiceCameraRTPStreamManagement() *hap.Service {
|
||||
VideoAttrs: []VideoAttrs{
|
||||
{Width: 1920, Height: 1080, Framerate: 30},
|
||||
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
|
||||
{Width: 320, Height: 240, Framerate: 15}, // apple watch
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ func (r *reader) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func (r *reader) getSegment() ([]byte, error) {
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := 0; i < 10; i++ {
|
||||
if r.playlist == nil {
|
||||
if wait := time.Second - time.Since(r.lastTime); wait > 0 {
|
||||
time.Sleep(wait)
|
||||
|
||||
+13
-5
@@ -2,12 +2,13 @@ package isapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -84,8 +85,15 @@ func (c *Client) Dial() (err error) {
|
||||
}
|
||||
|
||||
func (c *Client) Open() (err error) {
|
||||
link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel
|
||||
// Hikvision ISAPI may not accept a new open request if the previous one was not closed (e.g.
|
||||
// using the test button on-camera or via curl command) but a close request can be sent even if
|
||||
// the audio is already closed. So, we send a close request first and then open it again. Seems
|
||||
// janky but it works.
|
||||
if err = c.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel
|
||||
req, err := http.NewRequest("PUT", link+"/open", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -124,8 +132,8 @@ func (c *Client) Open() (err error) {
|
||||
}
|
||||
|
||||
func (c *Client) Close() (err error) {
|
||||
link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel + "/close"
|
||||
req, err := http.NewRequest("PUT", link+"/open", nil)
|
||||
link := c.url + "/ISAPI/System/TwoWayAudio/channels/" + c.channel
|
||||
req, err := http.NewRequest("PUT", link+"/close", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -132,6 +132,9 @@ func (c *Client) Handle() error {
|
||||
case "stream-init":
|
||||
continue
|
||||
|
||||
case "metadata":
|
||||
continue
|
||||
|
||||
case "fragment":
|
||||
_, data, err = c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
@@ -183,6 +186,9 @@ func (c *Client) getTracks() error {
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case "metadata":
|
||||
continue
|
||||
|
||||
case "stream-init":
|
||||
s := msg.CodecString
|
||||
i := strings.IndexByte(s, '.')
|
||||
|
||||
+21
-1
@@ -37,14 +37,34 @@ func Dial(url string) (*Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// KC200
|
||||
// HTTP/1.0 200 OK
|
||||
// Content-Type: multipart/x-mixed-replace;boundary=data-boundary--
|
||||
// KD110, KC401, KC420WS:
|
||||
// HTTP/1.0 200 OK
|
||||
// Content-Type: multipart/x-mixed-replace;boundary=data-boundary--
|
||||
// Transfer-Encoding: chunked
|
||||
// HTTP/1.0 + chunked = out of standard, so golang remove this header
|
||||
// and we need to check first two bytes
|
||||
buf := bufio.NewReader(res.Body)
|
||||
|
||||
b, err := buf.Peek(2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rd := struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
httputil.NewChunkedReader(res.Body),
|
||||
buf,
|
||||
res.Body,
|
||||
}
|
||||
|
||||
if string(b) != "--" {
|
||||
rd.Reader = httputil.NewChunkedReader(buf)
|
||||
}
|
||||
|
||||
prod := &Producer{rd: core.NewReadBuffer(rd)}
|
||||
if err = prod.probe(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -34,12 +34,12 @@ func Open(r io.Reader) (core.Producer, error) {
|
||||
case bytes.HasPrefix(b, []byte(flv.Signature)):
|
||||
return flv.Open(rd)
|
||||
|
||||
case bytes.HasPrefix(b, []byte{0xFF, 0xF1}):
|
||||
return aac.Open(rd)
|
||||
|
||||
case bytes.HasPrefix(b, []byte("--")):
|
||||
return multipart.Open(rd)
|
||||
|
||||
case b[0] == 0xFF && b[1]&0xF7 == 0xF1:
|
||||
return aac.Open(rd)
|
||||
|
||||
case b[0] == mpegts.SyncByte:
|
||||
return mpegts.Open(rd)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !(darwin || ios || freebsd || openbsd || netbsd || dragonfly || windows)
|
||||
|
||||
package mdns
|
||||
|
||||
import (
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin || ios || freebsd || openbsd || netbsd || dragonfly
|
||||
|
||||
package mdns
|
||||
|
||||
import (
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package mdns
|
||||
|
||||
import "syscall"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image/jpeg"
|
||||
)
|
||||
|
||||
// FixJPEG - reencode JPEG if it has wrong header
|
||||
//
|
||||
// for example, this app produce "bad" images:
|
||||
// https://github.com/jacksonliam/mjpg-streamer
|
||||
//
|
||||
// and they can't be uploaded to the Telegram servers:
|
||||
// {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"}
|
||||
func FixJPEG(b []byte) []byte {
|
||||
// skip non-JPEG
|
||||
if len(b) < 10 || b[0] != 0xFF || b[1] != 0xD8 {
|
||||
return b
|
||||
}
|
||||
// skip if header OK for imghdr library
|
||||
// https://docs.python.org/3/library/imghdr.html
|
||||
if string(b[2:4]) == "\xFF\xDB" || string(b[6:10]) == "JFIF" || string(b[6:10]) == "Exif" {
|
||||
return b
|
||||
}
|
||||
|
||||
img, err := jpeg.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return b
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err = jpeg.Encode(buf, img, nil); err != nil {
|
||||
return b
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func NewWriter(w io.Writer) io.Writer {
|
||||
h := w.(http.ResponseWriter).Header()
|
||||
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||
return &writer{wr: w, buf: []byte(header)}
|
||||
}
|
||||
|
||||
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
||||
|
||||
type writer struct {
|
||||
wr io.Writer
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (n int, err error) {
|
||||
w.buf = w.buf[:len(header)]
|
||||
w.buf = append(w.buf, strconv.Itoa(len(p))...)
|
||||
w.buf = append(w.buf, "\r\n\r\n"...)
|
||||
w.buf = append(w.buf, p...)
|
||||
w.buf = append(w.buf, "\r\n"...)
|
||||
|
||||
// Chrome bug: mjpeg image always shows the second to last image
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
||||
if _, err = w.wr.Write(w.buf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
w.wr.(http.Flusher).Flush()
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
+84
-1
@@ -14,6 +14,13 @@ import (
|
||||
type API struct {
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
|
||||
StreamProjectID string
|
||||
StreamDeviceID string
|
||||
StreamSessionID string
|
||||
StreamExpiresAt time.Time
|
||||
|
||||
extendTimer *time.Timer
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
@@ -159,7 +166,7 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) {
|
||||
Results struct {
|
||||
Answer string `json:"answerSdp"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
MediaSessionId string `json:"mediaSessionId"`
|
||||
MediaSessionID string `json:"mediaSessionId"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
@@ -167,9 +174,65 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
a.StreamProjectID = projectID
|
||||
a.StreamDeviceID = deviceID
|
||||
a.StreamSessionID = resv.Results.MediaSessionID
|
||||
a.StreamExpiresAt = resv.Results.ExpiresAt
|
||||
|
||||
return resv.Results.Answer, nil
|
||||
}
|
||||
|
||||
func (a *API) ExtendStream() error {
|
||||
var reqv struct {
|
||||
Command string `json:"command"`
|
||||
Params struct {
|
||||
MediaSessionID string `json:"mediaSessionId"`
|
||||
} `json:"params"`
|
||||
}
|
||||
reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream"
|
||||
reqv.Params.MediaSessionID = a.StreamSessionID
|
||||
|
||||
b, err := json.Marshal(reqv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" +
|
||||
a.StreamProjectID + "/devices/" + a.StreamDeviceID + ":executeCommand"
|
||||
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+a.Token)
|
||||
|
||||
client := &http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return errors.New("nest: wrong status: " + res.Status)
|
||||
}
|
||||
|
||||
var resv struct {
|
||||
Results struct {
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
MediaSessionID string `json:"mediaSessionId"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.StreamSessionID = resv.Results.MediaSessionID
|
||||
a.StreamExpiresAt = resv.Results.ExpiresAt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
@@ -203,3 +266,23 @@ type Device struct {
|
||||
// DisplayName string `json:"displayName"`
|
||||
//} `json:"parentRelations"`
|
||||
}
|
||||
|
||||
func (a *API) StartExtendStreamTimer() {
|
||||
// Calculate the duration until 30 seconds before the stream expires
|
||||
duration := time.Until(a.StreamExpiresAt.Add(-30 * time.Second))
|
||||
a.extendTimer = time.AfterFunc(duration, func() {
|
||||
if err := a.ExtendStream(); err != nil {
|
||||
return
|
||||
}
|
||||
duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second))
|
||||
a.extendTimer.Reset(duration)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a *API) StopExtendStreamTimer() {
|
||||
if a.extendTimer == nil {
|
||||
return
|
||||
}
|
||||
a.extendTimer.Stop()
|
||||
}
|
||||
|
||||
+4
-1
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
type Client struct {
|
||||
conn *webrtc.Conn
|
||||
api *API
|
||||
}
|
||||
|
||||
func NewClient(rawURL string) (*Client, error) {
|
||||
@@ -74,7 +75,7 @@ func NewClient(rawURL string) (*Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{conn: conn}, nil
|
||||
return &Client{conn: conn, api: nestAPI}, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
@@ -90,10 +91,12 @@ func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Rece
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
c.api.StartExtendStreamTimer()
|
||||
return c.conn.Start()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
c.api.StopExtendStreamTimer()
|
||||
return c.conn.Stop()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type Probe struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty"`
|
||||
Medias []*core.Media `json:"medias,omitempty"`
|
||||
Receivers []*core.Receiver `json:"receivers,omitempty"`
|
||||
Senders []*core.Sender `json:"senders,omitempty"`
|
||||
}
|
||||
|
||||
func NewProbe(query url.Values) *Probe {
|
||||
c := &Probe{Type: "probe"}
|
||||
c.Medias = core.ParseQuery(query)
|
||||
|
||||
for _, value := range query["microphone"] {
|
||||
media := &core.Media{Kind: core.KindAudio, Direction: core.DirectionRecvonly}
|
||||
|
||||
for _, name := range strings.Split(value, ",") {
|
||||
name = strings.ToUpper(name)
|
||||
switch name {
|
||||
case "", "COPY":
|
||||
name = core.CodecAny
|
||||
}
|
||||
media.Codecs = append(media.Codecs, &core.Codec{Name: name})
|
||||
}
|
||||
|
||||
c.Medias = append(c.Medias, media)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (p *Probe) GetMedias() []*core.Media {
|
||||
return p.Medias
|
||||
}
|
||||
|
||||
func (p *Probe) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
sender.Bind(track)
|
||||
p.Senders = append(p.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Probe) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
receiver := core.NewReceiver(media, codec)
|
||||
p.Receivers = append(p.Receivers, receiver)
|
||||
return receiver, nil
|
||||
}
|
||||
|
||||
func (p *Probe) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Probe) Stop() error {
|
||||
for _, receiver := range p.Receivers {
|
||||
receiver.Close()
|
||||
}
|
||||
for _, sender := range p.Senders {
|
||||
sender.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -219,6 +219,9 @@ func (c *Conn) SetupMedia(media *core.Media) (byte, error) {
|
||||
rawURL += "/"
|
||||
}
|
||||
rawURL += media.ID
|
||||
} else if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
|
||||
// fix https://github.com/AlexxIT/go2rtc/issues/830
|
||||
rawURL = rawURL[7:]
|
||||
}
|
||||
trackURL, err := urlParse(rawURL)
|
||||
if err != nil {
|
||||
|
||||
+37
-1
@@ -146,7 +146,19 @@ func (c *Conn) Accept() error {
|
||||
if strings.HasPrefix(tr, transport) {
|
||||
c.session = core.RandString(8, 10)
|
||||
c.state = StateSetup
|
||||
res.Header.Set("Transport", tr[:len(transport)+3])
|
||||
|
||||
if c.mode == core.ModePassiveConsumer {
|
||||
if i := reqTrackID(req); i >= 0 && i < len(c.senders) {
|
||||
// mark sender as SETUP
|
||||
c.senders[i].Media.ID = MethodSetup
|
||||
tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1)
|
||||
res.Header.Set("Transport", tr)
|
||||
} else {
|
||||
res.Status = "400 Bad Request"
|
||||
}
|
||||
} else {
|
||||
res.Header.Set("Transport", tr[:len(transport)+3])
|
||||
}
|
||||
} else {
|
||||
res.Status = "461 Unsupported transport"
|
||||
}
|
||||
@@ -156,6 +168,15 @@ func (c *Conn) Accept() error {
|
||||
}
|
||||
|
||||
case MethodRecord, MethodPlay:
|
||||
if c.mode == core.ModePassiveConsumer {
|
||||
// stop unconfigured senders
|
||||
for _, track := range c.senders {
|
||||
if track.Media.ID != MethodSetup {
|
||||
track.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := &tcp.Response{Request: req}
|
||||
err = c.WriteResponse(res)
|
||||
c.playOK = true
|
||||
@@ -172,3 +193,18 @@ func (c *Conn) Accept() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reqTrackID(req *tcp.Request) int {
|
||||
var s string
|
||||
if req.URL.RawQuery != "" {
|
||||
s = req.URL.RawQuery
|
||||
} else {
|
||||
s = req.URL.Path
|
||||
}
|
||||
if i := strings.LastIndexByte(s, '='); i > 0 {
|
||||
if i, err := strconv.Atoi(s[i+1:]); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ package shell
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -70,20 +68,3 @@ func RunUntilSignal() {
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
println("exit with signal:", (<-sigs).String())
|
||||
}
|
||||
|
||||
// Restart idea taken from https://github.com/tillberg/autorestart
|
||||
// Copyright (c) 2015, Dan Tillberg
|
||||
func Restart() {
|
||||
path, err := exec.LookPath(os.Args[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if err = syscall.Exec(path, os.Args, os.Environ()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package stdin
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cmd *exec.Cmd
|
||||
|
||||
medias []*core.Media
|
||||
sender *core.Sender
|
||||
send int
|
||||
}
|
||||
|
||||
func NewClient(cmd *exec.Cmd) (*Client, error) {
|
||||
c := &Client{
|
||||
cmd: cmd,
|
||||
medias: []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||
{Name: core.CodecPCM},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package stdin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*core.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
if c.sender == nil {
|
||||
stdin, err := c.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sender = core.NewSender(media, track.Codec)
|
||||
c.sender.Handler = func(packet *rtp.Packet) {
|
||||
_, _ = stdin.Write(packet.Payload)
|
||||
c.send += len(packet.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
c.sender.HandleRTP(track)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() (err error) {
|
||||
return c.cmd.Run()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() (err error) {
|
||||
if c.sender != nil {
|
||||
c.sender.Close()
|
||||
}
|
||||
if c.cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
return errors.Join(c.cmd.Process.Kill(), c.cmd.Wait())
|
||||
}
|
||||
|
||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
info := &core.Info{
|
||||
Type: "Exec active consumer",
|
||||
Medias: c.medias,
|
||||
Send: c.send,
|
||||
}
|
||||
if c.sender != nil {
|
||||
info.Senders = []*core.Sender{c.sender}
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
+14
-3
@@ -75,6 +75,12 @@ func (c *Client) newConn() (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
|
||||
if deviceId := query.Get("deviceId"); deviceId != "" {
|
||||
req.URL.RawQuery = "deviceId=" + deviceId
|
||||
}
|
||||
|
||||
req.URL.User = u.User
|
||||
req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")
|
||||
|
||||
@@ -91,7 +97,6 @@ func (c *Client) newConn() (net.Conn, error) {
|
||||
c.newDectypter(res)
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
channel := query.Get("channel")
|
||||
if channel == "" {
|
||||
channel = "0"
|
||||
@@ -277,11 +282,12 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_ = res.Body.Close() // ignore response body
|
||||
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
|
||||
if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode)
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
@@ -301,14 +307,19 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
|
||||
ha1 := tcp.HexMD5(username, realm, password)
|
||||
ha2 := tcp.HexMD5(req.Method, uri)
|
||||
nc := "00000001"
|
||||
cnonce := "00000001"
|
||||
cnonce := core.RandString(32, 64)
|
||||
response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc7616
|
||||
header := fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
|
||||
username, realm, nonce, uri, qop, nc, cnonce, response,
|
||||
)
|
||||
|
||||
if opaque := tcp.Between(auth, `opaque="`, `"`); opaque != "" {
|
||||
header += fmt.Sprintf(`, opaque="%s", algorithm=MD5`, opaque)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", header)
|
||||
|
||||
if err = req.Write(conn); err != nil {
|
||||
|
||||
+3
-1
@@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
// Do - http.Client with support Digest Authorization
|
||||
@@ -115,7 +117,7 @@ func Do(req *http.Request) (*http.Response, error) {
|
||||
)
|
||||
case "auth":
|
||||
nc := "00000001"
|
||||
cnonce := "00000001" // TODO: random...
|
||||
cnonce := core.RandString(32, 64)
|
||||
response := HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
|
||||
header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
|
||||
|
||||
+54
-13
@@ -2,6 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"slices"
|
||||
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v3"
|
||||
@@ -15,7 +16,15 @@ func NewAPI() (*webrtc.API, error) {
|
||||
return NewServerAPI("", "", nil)
|
||||
}
|
||||
|
||||
func NewServerAPI(address, network string, candidateHost []string) (*webrtc.API, error) {
|
||||
type Filters struct {
|
||||
Candidates []string `yaml:"candidates"`
|
||||
Interfaces []string `yaml:"interfaces"`
|
||||
IPs []string `yaml:"ips"`
|
||||
Networks []string `yaml:"networks"`
|
||||
UDPPorts []uint16 `yaml:"udp_ports"`
|
||||
}
|
||||
|
||||
func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error) {
|
||||
// for debug logs add to env: `PION_LOG_DEBUG=all`
|
||||
m := &webrtc.MediaEngine{}
|
||||
//if err := m.RegisterDefaultCodecs(); err != nil {
|
||||
@@ -32,23 +41,55 @@ func NewServerAPI(address, network string, candidateHost []string) (*webrtc.API,
|
||||
|
||||
s := webrtc.SettingEngine{}
|
||||
|
||||
// disable listen on Hassio docker interfaces
|
||||
s.SetInterfaceFilter(func(name string) bool {
|
||||
return name != "hassio" && name != "docker0"
|
||||
})
|
||||
|
||||
// fix https://github.com/pion/webrtc/pull/2407
|
||||
s.SetDTLSInsecureSkipHelloVerify(true)
|
||||
|
||||
s.SetReceiveMTU(ReceiveMTU)
|
||||
if filters != nil && filters.Interfaces != nil {
|
||||
s.SetIncludeLoopbackCandidate(true)
|
||||
s.SetInterfaceFilter(func(name string) bool {
|
||||
return slices.Contains(filters.Interfaces, name)
|
||||
})
|
||||
} else {
|
||||
// disable listen on Hassio docker interfaces
|
||||
s.SetInterfaceFilter(func(name string) bool {
|
||||
return name != "hassio" && name != "docker0"
|
||||
})
|
||||
}
|
||||
|
||||
s.SetNAT1To1IPs(candidateHost, webrtc.ICECandidateTypeHost)
|
||||
if filters != nil && filters.IPs != nil {
|
||||
s.SetIncludeLoopbackCandidate(true)
|
||||
s.SetIPFilter(func(ip net.IP) bool {
|
||||
return slices.Contains(filters.IPs, ip.String())
|
||||
})
|
||||
}
|
||||
|
||||
// by default enable IPv4 + IPv6 modes
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeTCP4,
|
||||
webrtc.NetworkTypeUDP6, webrtc.NetworkTypeTCP6,
|
||||
})
|
||||
if filters != nil && filters.Networks != nil {
|
||||
var networkTypes []webrtc.NetworkType
|
||||
for _, s := range filters.Networks {
|
||||
if networkType, err := webrtc.NewNetworkType(s); err == nil {
|
||||
networkTypes = append(networkTypes, networkType)
|
||||
}
|
||||
}
|
||||
s.SetNetworkTypes(networkTypes)
|
||||
} else {
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
||||
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
||||
})
|
||||
}
|
||||
|
||||
if filters != nil && len(filters.UDPPorts) == 2 {
|
||||
_ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1])
|
||||
}
|
||||
|
||||
//if len(hosts) != 0 {
|
||||
// // support only: host, srflx
|
||||
// if candidateType, err := webrtc.NewICECandidateType(hosts[0]); err == nil {
|
||||
// s.SetNAT1To1IPs(hosts[1:], candidateType)
|
||||
// } else {
|
||||
// s.SetNAT1To1IPs(hosts, 0) // 0 = host
|
||||
// }
|
||||
//}
|
||||
|
||||
if address != "" {
|
||||
if network == "" || network == "tcp" {
|
||||
|
||||
@@ -120,6 +120,10 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
|
||||
c.Fire(state)
|
||||
|
||||
switch state {
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
for _, sender := range c.senders {
|
||||
sender.Start()
|
||||
}
|
||||
case webrtc.PeerConnectionStateDisconnected, webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed:
|
||||
// disconnect event comes earlier, than failed
|
||||
// but it comes only for success connections
|
||||
|
||||
@@ -20,7 +20,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
|
||||
for _, sender := range c.senders {
|
||||
if sender.Codec == codec {
|
||||
sender.HandleRTP(track)
|
||||
sender.Bind(track)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
|
||||
sender.Handler = pcm.RepackG711(false, sender.Handler)
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
sender.Bind(track)
|
||||
|
||||
c.senders = append(c.senders, sender)
|
||||
return nil
|
||||
|
||||
+45
-24
@@ -47,6 +47,9 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media
|
||||
continue
|
||||
}
|
||||
|
||||
// skip non-media codecs to avoid confusing users in info and logs
|
||||
media.Codecs = SkipNonMediaCodecs(media.Codecs)
|
||||
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
@@ -54,6 +57,21 @@ func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media
|
||||
return
|
||||
}
|
||||
|
||||
func SkipNonMediaCodecs(input []*core.Codec) (output []*core.Codec) {
|
||||
for _, codec := range input {
|
||||
switch codec.Name {
|
||||
case "RTX", "RED", "ULPFEC", "FLEXFEC-03":
|
||||
continue
|
||||
case "CN", "TELEPHONE-EVENT":
|
||||
continue // https://datatracker.ietf.org/doc/html/rfc7874
|
||||
}
|
||||
// VP8, VP9, H264, H265, AV1
|
||||
// OPUS, G722, PCMU, PCMA
|
||||
output = append(output, codec)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WithResampling - will add for consumer: PCMA/0, PCMU/0, PCM/0, PCML/0
|
||||
// so it can add resampling for PCMA/PCMU and repack for PCM/PCML
|
||||
func WithResampling(medias []*core.Media) []*core.Media {
|
||||
@@ -255,38 +273,41 @@ func MimeType(codec *core.Codec) string {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// 4.1.2.2. Guidelines for Choosing Type and Local Preferences
|
||||
// The RECOMMENDED values are 126 for host candidates, 100
|
||||
// for server reflexive candidates, 110 for peer reflexive candidates,
|
||||
// and 0 for relayed candidates.
|
||||
|
||||
const PriorityTypeHostUDP = (1 << 24) * int(126)
|
||||
const PriorityTypeHostTCP = (1 << 24) * int(126-27)
|
||||
const PriorityLocalUDP = (1 << 8) * int(65535)
|
||||
const PriorityLocalTCPPassive = (1 << 8) * int((1<<13)*4+8191)
|
||||
const PriorityComponentRTP = 1 * int(256-ice.ComponentRTP)
|
||||
|
||||
func CandidateManualHostUDP(host, port string, offset int) string {
|
||||
foundation := crc32.ChecksumIEEE([]byte("host" + host + "udp4"))
|
||||
priority := PriorityTypeHostUDP + PriorityLocalUDP + PriorityComponentRTP + offset
|
||||
|
||||
func CandidateICE(network, host, port string, priority uint32) string {
|
||||
// 1. Foundation
|
||||
// 2. Component, always 1 because RTP
|
||||
// 3. udp or tcp
|
||||
// 3. "udp" or "tcp"
|
||||
// 4. Priority
|
||||
// 5. Host - IP4 or IP6 or domain name
|
||||
// 6. Port
|
||||
// 7. typ host
|
||||
return fmt.Sprintf("candidate:%d 1 udp %d %s %s typ host", foundation, priority, host, port)
|
||||
// 7. "typ host"
|
||||
foundation := crc32.ChecksumIEEE([]byte("host" + host + network + "4"))
|
||||
s := fmt.Sprintf("candidate:%d 1 %s %d %s %s typ host", foundation, network, priority, host, port)
|
||||
if network == "tcp" {
|
||||
return s + " tcptype passive"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func CandidateManualHostTCPPassive(host, port string, offset int) string {
|
||||
foundation := crc32.ChecksumIEEE([]byte("host" + host + "tcp4"))
|
||||
priority := PriorityTypeHostTCP + PriorityLocalTCPPassive + PriorityComponentRTP + offset
|
||||
// Priority = type << 24 + local << 8 + component
|
||||
// https://www.rfc-editor.org/rfc/rfc8445#section-5.1.2.1
|
||||
|
||||
return fmt.Sprintf(
|
||||
"candidate:%d 1 tcp %d %s %s typ host tcptype passive", foundation, priority, host, port,
|
||||
)
|
||||
const PriorityHostUDP uint32 = 0x001F_FFFF |
|
||||
126<<24 | // udp host
|
||||
7<<21 // udp
|
||||
const PriorityHostTCPPassive uint32 = 0x001F_FFFF |
|
||||
99<<24 | // tcp host
|
||||
4<<21 // tcp passive
|
||||
|
||||
// CandidateHostPriority (lower indexes has a higher priority)
|
||||
func CandidateHostPriority(network string, index int) uint32 {
|
||||
switch network {
|
||||
case "udp":
|
||||
return PriorityHostUDP - uint32(index)
|
||||
case "tcp":
|
||||
return PriorityHostTCPPassive - uint32(index)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func UnmarshalICEServers(b []byte) ([]webrtc.ICEServer, error) {
|
||||
|
||||
+36
-5
@@ -81,11 +81,42 @@ transeivers:
|
||||
return c.pc.LocalDescription().SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) GetCompleteAnswer() (answer string, err error) {
|
||||
if _, err = c.GetAnswer(); err != nil {
|
||||
return
|
||||
// GetCompleteAnswer - get SDP answer with candidates inside
|
||||
func (c *Conn) GetCompleteAnswer(candidates []string, filter func(*webrtc.ICECandidate) bool) (string, error) {
|
||||
var done = make(chan struct{})
|
||||
|
||||
c.pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate != nil {
|
||||
if filter == nil || filter(candidate) {
|
||||
candidates = append(candidates, candidate.ToJSON().Candidate)
|
||||
}
|
||||
} else {
|
||||
done <- struct{}{}
|
||||
}
|
||||
})
|
||||
|
||||
answer, err := c.GetAnswer()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
<-webrtc.GatheringCompletePromise(c.pc)
|
||||
return c.pc.LocalDescription().SDP, nil
|
||||
<-done
|
||||
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err = sd.Unmarshal([]byte(answer)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
md := sd.MediaDescriptions[0]
|
||||
|
||||
for _, candidate := range candidates {
|
||||
md.WithPropertyAttribute(candidate)
|
||||
}
|
||||
|
||||
b, err := sd.Marshal()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
+22
-12
@@ -1,6 +1,8 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
@@ -12,6 +14,7 @@ type Track struct {
|
||||
sequence uint16
|
||||
ssrc uint32
|
||||
writer webrtc.TrackLocalWriter
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewTrack(kind string) *Track {
|
||||
@@ -23,8 +26,10 @@ func NewTrack(kind string) *Track {
|
||||
}
|
||||
|
||||
func (t *Track) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) {
|
||||
t.mu.Lock()
|
||||
t.ssrc = uint32(context.SSRC())
|
||||
t.writer = context.WriteStream()
|
||||
t.mu.Unlock()
|
||||
|
||||
for _, parameters := range context.CodecParameters() {
|
||||
// return first parameters
|
||||
@@ -35,7 +40,9 @@ func (t *Track) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameter
|
||||
}
|
||||
|
||||
func (t *Track) Unbind(context webrtc.TrackLocalContext) error {
|
||||
t.mu.Lock()
|
||||
t.writer = nil
|
||||
t.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -55,19 +62,22 @@ func (t *Track) Kind() webrtc.RTPCodecType {
|
||||
return webrtc.NewRTPCodecType(t.kind)
|
||||
}
|
||||
|
||||
func (t *Track) WriteRTP(payloadType uint8, packet *rtp.Packet) error {
|
||||
func (t *Track) WriteRTP(payloadType uint8, packet *rtp.Packet) (err error) {
|
||||
// using mutex because Unbind https://github.com/AlexxIT/go2rtc/issues/994
|
||||
t.mu.Lock()
|
||||
|
||||
// in case when we start WriteRTP before Track.Bind
|
||||
if t.writer == nil {
|
||||
return nil
|
||||
if t.writer != nil {
|
||||
// important to have internal counter if input packets from different sources
|
||||
t.sequence++
|
||||
|
||||
header := packet.Header
|
||||
header.SSRC = t.ssrc
|
||||
header.PayloadType = payloadType
|
||||
header.SequenceNumber = t.sequence
|
||||
_, err = t.writer.WriteRTP(&header, packet.Payload)
|
||||
}
|
||||
|
||||
// important to have internal counter if input packets from different sources
|
||||
t.sequence++
|
||||
|
||||
header := packet.Header
|
||||
header.SSRC = t.ssrc
|
||||
header.PayloadType = payloadType
|
||||
header.SequenceNumber = t.sequence
|
||||
_, err := t.writer.WriteRTP(&header, packet.Payload)
|
||||
return err
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/bin/sh
|
||||
|
||||
check_command() {
|
||||
if ! command -v $1 &> /dev/null
|
||||
then
|
||||
echo "Error: $1 could not be found. Please install it."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for required commands
|
||||
check_command go
|
||||
check_command 7z
|
||||
check_command upx
|
||||
|
||||
# Windows amd64
|
||||
export GOOS=windows
|
||||
export GOARCH=amd64
|
||||
FILENAME="go2rtc_win64.zip"
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc.exe
|
||||
|
||||
# Windows 386
|
||||
export GOOS=windows
|
||||
export GOARCH=386
|
||||
FILENAME="go2rtc_win32.zip"
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc.exe
|
||||
|
||||
# Windows arm64
|
||||
export GOOS=windows
|
||||
export GOARCH=arm64
|
||||
FILENAME="go2rtc_win_arm64.zip"
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc.exe
|
||||
|
||||
# Linux amd64
|
||||
export GOOS=linux
|
||||
export GOARCH=amd64
|
||||
FILENAME="go2rtc_linux_amd64"
|
||||
go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME
|
||||
|
||||
# Linux 386
|
||||
export GOOS=linux
|
||||
export GOARCH=386
|
||||
FILENAME="go2rtc_linux_i386"
|
||||
go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME
|
||||
|
||||
# Linux arm64
|
||||
export GOOS=linux
|
||||
export GOARCH=arm64
|
||||
FILENAME="go2rtc_linux_arm64"
|
||||
go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME
|
||||
|
||||
# Linux arm v7
|
||||
export GOOS=linux
|
||||
export GOARCH=arm
|
||||
export GOARM=7
|
||||
FILENAME="go2rtc_linux_arm"
|
||||
go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME
|
||||
|
||||
# Linux arm v6
|
||||
export GOOS=linux
|
||||
export GOARCH=arm
|
||||
export GOARM=6
|
||||
FILENAME="go2rtc_linux_armv6"
|
||||
go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME
|
||||
|
||||
# Linux mipsle
|
||||
export GOOS=linux
|
||||
export GOARCH=mipsle
|
||||
FILENAME="go2rtc_linux_mipsel"
|
||||
go build -ldflags "-s -w" -trimpath -o $FILENAME && upx --lzma --force-overwrite -q --no-progress $FILENAME
|
||||
|
||||
# Darwin amd64
|
||||
export GOOS=darwin
|
||||
export GOARCH=amd64
|
||||
FILENAME="go2rtc_mac_amd64.zip"
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc
|
||||
|
||||
# Darwin arm64
|
||||
export GOOS=darwin
|
||||
export GOARCH=arm64
|
||||
FILENAME="go2rtc_mac_arm64.zip"
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "go2rtc",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://alexxit.github.io/go2rtc/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://alexxit.github.io/go2rtc/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000"
|
||||
}
|
||||
@@ -72,6 +72,22 @@ User-Agent: `Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gec
|
||||
|
||||
https://webrtc.org/getting-started/unified-plan-transition-guide?hl=en
|
||||
|
||||
## Web Icons
|
||||
|
||||
[Favicon checker](https://realfavicongenerator.net/), skip:
|
||||
|
||||
- Windows 8 and 10 (`browserconfig.xml`)
|
||||
- Mac OS X El Capitan Safari
|
||||
|
||||
```html
|
||||
<!-- iOS Safari -->
|
||||
<link rel="apple-touch-icon" href="https://alexxit.github.io/go2rtc/icons/apple-touch-icon-180x180.png" sizes="180x180">
|
||||
<!-- Classic, desktop browsers -->
|
||||
<link rel="icon" href="https://alexxit.github.io/go2rtc/icons/favicon.ico">
|
||||
<!-- Android Chrome -->
|
||||
<link rel="manifest" href="https://alexxit.github.io/go2rtc/manifest.json">
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://www.webrtc-experiment.com/DetectRTC/
|
||||
|
||||
+1
-28
@@ -1,15 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Add Stream</title>
|
||||
<title>go2rtc - Add Stream</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -27,33 +22,11 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
border: 1px solid black;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
|
||||
table tbody td {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background: #CFCFCF;
|
||||
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
|
||||
border-bottom: 3px solid black;
|
||||
}
|
||||
|
||||
table thead th {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>File Editor</title>
|
||||
<title>go2rtc - File Editor</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<script src="https://unpkg.com/ace-builds@1.28.0/src-min/ace.js"></script>
|
||||
<script src="https://unpkg.com/ace-builds@1.33.1/src-min/ace.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
@@ -31,7 +31,7 @@
|
||||
<script>
|
||||
let dump;
|
||||
|
||||
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.28.0/src-min/');
|
||||
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.33.1/src-min/');
|
||||
const editor = ace.edit('config', {
|
||||
mode: 'ace/mode/yaml',
|
||||
});
|
||||
@@ -46,7 +46,7 @@
|
||||
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
|
||||
if (r.ok) {
|
||||
alert('OK');
|
||||
fetch('api/restart', {method: 'POST'});
|
||||
await fetch('api/restart', {method: 'POST'});
|
||||
} else {
|
||||
alert(await r.text());
|
||||
}
|
||||
|
||||
+55
-41
@@ -4,43 +4,22 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
|
||||
<link rel="apple-touch-icon" href="https://alexxit.github.io/go2rtc/icons/apple-touch-icon-180x180.png" sizes="180x180">
|
||||
<link rel="icon" href="https://alexxit.github.io/go2rtc/icons/favicon.ico">
|
||||
<link rel="manifest" href="https://alexxit.github.io/go2rtc/manifest.json">
|
||||
<title>go2rtc</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
border: 1px solid black;
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
table tbody td {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background: #CFCFCF;
|
||||
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
|
||||
border-bottom: 3px solid black;
|
||||
}
|
||||
|
||||
table thead th {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -102,15 +81,28 @@
|
||||
});
|
||||
|
||||
const tbody = document.getElementById('streams');
|
||||
tbody.addEventListener('click', ev => {
|
||||
tbody.addEventListener('click', async ev => {
|
||||
if (ev.target.innerText !== 'delete') return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
const url = new URL('api/streams', location.href);
|
||||
const src = decodeURIComponent(ev.target.dataset.name);
|
||||
|
||||
const message = `Please type the name of the stream "${src}" to confirm its deletion from the configuration. This action is irreversible.`;
|
||||
if (prompt(message) !== src) {
|
||||
alert('Stream name does not match. Deletion cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL('api/streams', location.href);
|
||||
url.searchParams.set('src', src);
|
||||
fetch(url, {method: 'DELETE'}).then(reload);
|
||||
|
||||
try {
|
||||
await fetch(url, {method: 'DELETE'});
|
||||
reload();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete the stream:', error);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('selectall').addEventListener('change', ev => {
|
||||
@@ -121,27 +113,49 @@
|
||||
|
||||
function reload() {
|
||||
const url = new URL('api/streams', location.href);
|
||||
const checkboxStates = {};
|
||||
tbody.querySelectorAll('input[type="checkbox"][name]').forEach(checkbox => {
|
||||
checkboxStates[checkbox.name] = checkbox.checked;
|
||||
});
|
||||
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||
tbody.innerHTML = '';
|
||||
const existingIds = Array.from(tbody.querySelectorAll('tr')).map(tr => tr.dataset['id']);
|
||||
const fetchedIds = [];
|
||||
|
||||
for (const [name, value] of Object.entries(data)) {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const name = key.replace(/[<">]/g, ''); // sanitize
|
||||
fetchedIds.push(name);
|
||||
|
||||
let tr = tbody.querySelector(`tr[data-id="${name}"]`);
|
||||
const online = value && value.consumers ? value.consumers.length : 0;
|
||||
const src = encodeURIComponent(name);
|
||||
const links = templates.map(link => {
|
||||
return link.replace('{name}', src);
|
||||
}).join(' ');
|
||||
const links = templates.map(link => link.replace('{name}', src)).join(' ');
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset['id'] = name;
|
||||
if (!tr) {
|
||||
tr = document.createElement('tr');
|
||||
tr.dataset['id'] = name;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
const isChecked = checkboxStates[name] ? 'checked' : '';
|
||||
tr.innerHTML =
|
||||
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
|
||||
`<td><a href="api/streams?src=${src}">${online} / info</a></td>` +
|
||||
`<td><label><input type="checkbox" name="${name}" ${isChecked}>${name}</label></td>` +
|
||||
`<td><a href="api/streams?src=${src}">${online} / info</a> / <a href="api/streams?src=${src}&video=all&audio=allµphone">probe</a></td>` +
|
||||
`<td>${links}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Remove old rows
|
||||
existingIds.forEach(id => {
|
||||
if (!fetchedIds.includes(id)) {
|
||||
const trToRemove = tbody.querySelector(`tr[data-id="${id}"]`);
|
||||
tbody.removeChild(trToRemove);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-reload
|
||||
setInterval(reload, 1000);
|
||||
|
||||
const url = new URL('api', location.href);
|
||||
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||
const info = document.querySelector('.info');
|
||||
|
||||
+27
-6
@@ -5,10 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -43,7 +39,7 @@
|
||||
<script src="main.js"></script>
|
||||
<div id="links"></div>
|
||||
<script>
|
||||
const src = new URLSearchParams(location.search).get('src');
|
||||
const src = new URLSearchParams(location.search).get('src').replace(/[<">]/g, ''); // sanitize
|
||||
|
||||
document.getElementById('links').innerHTML = `
|
||||
<h2>Any codec in source</h2>
|
||||
@@ -170,6 +166,31 @@ Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx</pre>
|
||||
document.getElementById('sharedel').style.display = 'none';
|
||||
}
|
||||
|
||||
function copyTextToClipboard(text) {
|
||||
// https://web.dev/patterns/clipboard/copy-text
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).catch(err => {
|
||||
console.error(err.name, err.message);
|
||||
});
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('shareadd').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
share('POST').then(r => r.json()).then(r => onshareadd(r));
|
||||
@@ -177,7 +198,7 @@ Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx</pre>
|
||||
|
||||
document.getElementById('shareget').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
navigator.clipboard.writeText(ev.target.href);
|
||||
copyTextToClipboard(ev.target.href);
|
||||
});
|
||||
|
||||
document.getElementById('sharedel').addEventListener('click', ev => {
|
||||
|
||||
+42
-28
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Logs</title>
|
||||
<title>go2rtc - Logs</title>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<style>
|
||||
@@ -19,33 +19,29 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
border: 1px solid black;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
|
||||
table tbody td {
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table thead {
|
||||
background: #CFCFCF;
|
||||
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
|
||||
border-bottom: 3px solid black;
|
||||
.info {
|
||||
color: #0174DF;
|
||||
}
|
||||
|
||||
table thead th {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
text-align: center;
|
||||
.debug {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #DF0101;
|
||||
}
|
||||
|
||||
.trace {
|
||||
color: #585858;
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: #FF9966;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -54,6 +50,7 @@
|
||||
<div>
|
||||
<button id="clean">Clean</button>
|
||||
<button id="update">Auto Update: ON</button>
|
||||
<button id="reverse">Reverse Log Order: OFF</button>
|
||||
</div>
|
||||
<br>
|
||||
<table>
|
||||
@@ -85,15 +82,27 @@
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
const reverseBtn = document.getElementById('reverse');
|
||||
const update = document.getElementById('update');
|
||||
|
||||
let reverseOrder = false;
|
||||
let autoUpdateEnabled = true;
|
||||
|
||||
reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`;
|
||||
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
|
||||
|
||||
function applyLogStyling(jsonlines) {
|
||||
const KEYS = ['time', 'level', 'message'];
|
||||
const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']');
|
||||
let lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']');
|
||||
if (reverseOrder) {
|
||||
lines = lines.reverse();
|
||||
}
|
||||
return lines.map(line => {
|
||||
const ts = new Date(line['time']);
|
||||
const msg = Object.keys(line).reduce((msg, key) => {
|
||||
return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg;
|
||||
}, line['message']);
|
||||
return `<tr><td>${ts.toLocaleString()}</td><td>${line['level']}</td><td>${escapeHTML(msg)}</td></tr>`;
|
||||
return `<tr class="${line['level']}"><td>${ts.toLocaleString()}</td><td>${escapeHTML(line['level'])}</td><td>${escapeHTML(msg)}</td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -112,19 +121,24 @@
|
||||
|
||||
reload();
|
||||
|
||||
// Handle auto-update switch
|
||||
let autoUpdateEnabled = true;
|
||||
|
||||
const update = document.getElementById('update');
|
||||
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
|
||||
update.addEventListener('click', () => {
|
||||
autoUpdateEnabled = !autoUpdateEnabled;
|
||||
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
|
||||
});
|
||||
|
||||
// Toggle log order
|
||||
reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`;
|
||||
reverseBtn.addEventListener('click', () => {
|
||||
reverseOrder = !reverseOrder;
|
||||
reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`;
|
||||
reload(); // Reload logs to apply the new order
|
||||
});
|
||||
|
||||
// Reload the logs every 5 seconds
|
||||
setInterval(() => {
|
||||
if (autoUpdateEnabled) reload();
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+147
-1
@@ -18,7 +18,6 @@ i {
|
||||
|
||||
nav {
|
||||
display: block;
|
||||
/*width: 660px;*/
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
@@ -41,6 +40,97 @@ nav a:hover {
|
||||
nav li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
table {
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table thead {
|
||||
background: #CFCFCF;
|
||||
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
|
||||
border-bottom: 3px solid black;
|
||||
}
|
||||
table thead th {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
table td, table th {
|
||||
border: 1px solid black;
|
||||
padding: 5px 5px;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
body.dark-mode {
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.dark-mode nav ul {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
body.dark-mode a {
|
||||
background: rgba(45, 45, 45, .8);
|
||||
border-right: 1px solid #2c2c2c;
|
||||
color: #c7c7c7;
|
||||
}
|
||||
|
||||
body.dark-mode a:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
body.dark-mode a:visited {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
body.dark-mode table {
|
||||
background-color: #222;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body.dark-mode table thead {
|
||||
background: linear-gradient(to bottom, #444 0%, #3d3d3d 66%, #333 100%);
|
||||
border-bottom: 3px solid #888;
|
||||
}
|
||||
body.dark-mode table thead th {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
text-align: center;
|
||||
}
|
||||
body.dark-mode table td, body.dark-mode table th {
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
body.dark-mode button {
|
||||
background: rgba(255, 255, 255, .1);
|
||||
border: 1px solid #444;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
body.dark-mode input,
|
||||
body.dark-mode select,
|
||||
body.dark-mode textarea {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
body.dark-mode input::placeholder,
|
||||
body.dark-mode textarea::placeholder {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body.dark-mode hr {
|
||||
border-top: 1px solid #444;
|
||||
}
|
||||
</style>
|
||||
<nav>
|
||||
<ul>
|
||||
@@ -48,6 +138,62 @@ nav li {
|
||||
<li><a href="add.html">Add</a></li>
|
||||
<li><a href="editor.html">Config</a></li>
|
||||
<li><a href="log.html">Log</a></li>
|
||||
<li><a href="#" id="darkModeToggle">
|
||||
🌙
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
` + document.body.innerHTML;
|
||||
|
||||
const sunIcon = '☀️';
|
||||
const moonIcon = '🌕';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const isDarkModeEnabled = () => document.body.classList.contains('dark-mode');
|
||||
|
||||
// Update the toggle button based on the dark mode state
|
||||
const updateToggleButton = () => {
|
||||
if (isDarkModeEnabled()) {
|
||||
darkModeToggle.innerHTML = sunIcon;
|
||||
darkModeToggle.setAttribute('aria-label', 'Enable light mode');
|
||||
} else {
|
||||
darkModeToggle.innerHTML = moonIcon;
|
||||
darkModeToggle.setAttribute('aria-label', 'Enable dark mode');
|
||||
}
|
||||
};
|
||||
|
||||
const updateDarkMode = () => {
|
||||
if (localStorage.getItem('darkMode') === 'enabled' || prefersDarkScheme.matches && localStorage.getItem('darkMode') !== 'disabled') {
|
||||
document.body.classList.add('dark-mode');
|
||||
} else {
|
||||
document.body.classList.remove('dark-mode');
|
||||
}
|
||||
updateEditorTheme();
|
||||
updateToggleButton();
|
||||
};
|
||||
|
||||
// Update the editor theme based on the dark mode state
|
||||
const updateEditorTheme = () => {
|
||||
if (typeof editor !== 'undefined') {
|
||||
editor.setTheme(isDarkModeEnabled() ? 'ace/theme/tomorrow_night_eighties' : 'ace/theme/github');
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update for dark mode and toggle button
|
||||
updateDarkMode();
|
||||
|
||||
// Listen for changes in the system's color scheme preference
|
||||
prefersDarkScheme.addEventListener('change', updateDarkMode); // Modern approach
|
||||
|
||||
// Toggle dark mode and update local storage on button click
|
||||
darkModeToggle.addEventListener('click', () => {
|
||||
const enabled = document.body.classList.toggle('dark-mode');
|
||||
localStorage.setItem('darkMode', enabled ? 'enabled' : 'disabled');
|
||||
updateToggleButton(); // Update the button after toggling
|
||||
updateEditorTheme();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="apple-touch-icon" href="https://alexxit.github.io/go2rtc/icons/apple-touch-icon-180x180.png" sizes="180x180">
|
||||
<link rel="icon" href="https://alexxit.github.io/go2rtc/icons/favicon.ico">
|
||||
<link rel="manifest" href="https://alexxit.github.io/go2rtc/manifest.json">
|
||||
<title>go2rtc - Stream</title>
|
||||
<style>
|
||||
body {
|
||||
|
||||
+7
-1
@@ -19,7 +19,7 @@ export class VideoRTC extends HTMLElement {
|
||||
super();
|
||||
|
||||
this.DISCONNECT_TIMEOUT = 5000;
|
||||
this.RECONNECT_TIMEOUT = 30000;
|
||||
this.RECONNECT_TIMEOUT = 15000;
|
||||
|
||||
this.CODECS = [
|
||||
'avc1.640029', // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
|
||||
@@ -70,6 +70,7 @@ export class VideoRTC extends HTMLElement {
|
||||
* @type {RTCConfiguration}
|
||||
*/
|
||||
this.pcConfig = {
|
||||
bundlePolicy: 'max-bundle',
|
||||
iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
|
||||
sdpSemantics: 'unified-plan', // important for Chromecast 1
|
||||
};
|
||||
@@ -247,6 +248,11 @@ export class VideoRTC extends HTMLElement {
|
||||
|
||||
this.appendChild(this.video);
|
||||
|
||||
this.video.addEventListener('error', ev => {
|
||||
console.warn(ev);
|
||||
if (this.ws) this.ws.close(); // run reconnect for broken MSE stream
|
||||
});
|
||||
|
||||
// all Safari lies about supported audio codecs
|
||||
const m = window.navigator.userAgent.match(/Version\/(\d+).+Safari/);
|
||||
if (m) {
|
||||
|
||||
Reference in New Issue
Block a user