Compare commits
291 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c98f5e769 | |||
| b4a524f46d | |||
| 297096a93b | |||
| e23e64ab00 | |||
| 0698f90273 | |||
| bec792797d | |||
| fd6014c11f | |||
| b8b90aba51 | |||
| 652dc93e9a | |||
| 6f1cc94ea5 | |||
| 52832223f8 | |||
| 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 | |||
| ccec41a10f | |||
| 9feb98db3f | |||
| a724c5f3ce | |||
| c60767c8b0 | |||
| ae13a72fde | |||
| 458d5e7d0d | |||
| 89e15d9b57 | |||
| 0d2292c311 | |||
| 62343af009 | |||
| c8c3b22d19 | |||
| 853e98879b | |||
| bf5cb33385 | |||
| 7ad4d350f8 | |||
| c63fc6a2ad | |||
| 7036d196be | |||
| d3bc18c369 | |||
| 1f3a32023f | |||
| a46bad0522 | |||
| d0dfa1d3dd | |||
| fc5b36acd3 | |||
| 0a8ab9bbd1 | |||
| b60000ac34 | |||
| 39d87625d7 | |||
| 0da8b46148 | |||
| 8d9f87061c | |||
| 4bdfa62039 | |||
| 67ea2d9d02 | |||
| 39b614fb0f | |||
| 84469dcd25 | |||
| eceb4a476f | |||
| 051a4eabd7 | |||
| e68a304698 | |||
| 2e6c6b1d41 | |||
| 0def6f8de9 | |||
| 7ac5b4f114 | |||
| ab47d5718f | |||
| 94aced0fc0 | |||
| 66a4c3d06e | |||
| 8d382afa0f | |||
| 051c5ff913 | |||
| a87dafbbec | |||
| 742cb7699b | |||
| 43449e7b08 | |||
| 33512e73bd | |||
| b367ffee6d | |||
| 69447df6b3 | |||
| a6eac4ff02 | |||
| 1eaf879a76 | |||
| c9ae6dcc03 | |||
| befa6bd356 | |||
| 100ab62ab4 | |||
| a0f999d9c9 | |||
| 9bda2f7e60 | |||
| 54b19999c6 | |||
| aa3c081352 | |||
| 2d16ee8884 | |||
| ec96a14807 | |||
| af72548a43 | |||
| 6d85b36f47 | |||
| 28830a697d | |||
| 5d3953a948 | |||
| 4d6432d38d | |||
| bcbebd5a36 | |||
| 50e2a626a6 | |||
| f4fe8c3769 | |||
| e42085a237 | |||
| a060b3447c | |||
| d7784b24c6 | |||
| 39645cb3d8 | |||
| 36166caccc | |||
| 0f1dc73d55 | |||
| 6b29c37433 | |||
| 535bacf9d6 | |||
| e6fb4081f7 | |||
| eb04fafaa4 | |||
| b4ed738d17 | |||
| 6a9ae93fa1 | |||
| 2dd47654e6 | |||
| c27e735c17 | |||
| 8bc65e4c91 | |||
| 0a476a74b3 | |||
| b5be4ce03b | |||
| f291f1d827 | |||
| 041ce885c7 | |||
| df16f28825 | |||
| a8867bc3cb | |||
| b2b115ec9c | |||
| 95de3a1f3e | |||
| dd4376cd37 | |||
| 20d45bff92 | |||
| 4ad67e9f6f | |||
| e367940bd9 | |||
| 6f2af78392 | |||
| 548d8133eb | |||
| 36ee2b29fb | |||
| 05accb4555 | |||
| f949a278da | |||
| bfae16f3a0 | |||
| d09d21434b | |||
| 2b9926cedb | |||
| af24fd67aa | |||
| e2cd34ffe3 | |||
| ecdf5ba271 | |||
| 995ef5bb36 | |||
| 8165adcab1 | |||
| 91c4a3e7b5 | |||
| cb710ea2be | |||
| 843a3ae9c9 | |||
| de040fb160 | |||
| acec8a76aa | |||
| 6c07c59454 | |||
| 4d708b5385 | |||
| 2e9f3181d4 | |||
| 3ae15d8f80 | |||
| d016529030 | |||
| 09f1553e40 | |||
| 52e4bf1b35 | |||
| bbe6ae0059 | |||
| c02117e626 | |||
| b8fb3acbab | |||
| d4d0064220 | |||
| 855bbdeb60 | |||
| 05893c9203 | |||
| c9c8e73587 | |||
| c7b6eb5d5b | |||
| 96bc88d8ce | |||
| 9a2e9dd6d1 | |||
| b252fcaaa1 | |||
| c582b932c7 | |||
| c3f26c4db8 | |||
| f27f7d28bb | |||
| 0424b1a92a | |||
| 81fb8fc238 | |||
| 037970a4ea | |||
| 3f6e83e87c | |||
| aa5b23fa80 | |||
| 02bde2c8b7 | |||
| cb5e90cc3b | |||
| 209fe09806 | |||
| dca8279e0c | |||
| 8163c7a520 | |||
| 4dffceaf7e | |||
| 9f1e33e0c6 | |||
| 9a7d7e68e2 | |||
| ab18d5d1ca | |||
| 6e53e74742 | |||
| f910bd4fce | |||
| 93e475f3a4 | |||
| e5d8170037 | |||
| 861632f92b | |||
| 9cf75565b5 | |||
| 9368a6b85e | |||
| c8ac6b2271 | |||
| 28f5c2b974 | |||
| daa2522a52 | |||
| 863f8ec19b | |||
| 8f98fc4547 | |||
| 398afbe49f | |||
| ad8c0ab2fb | |||
| 37130576e9 | |||
| 486fea2227 | |||
| 6d7357b151 | |||
| 452d7577f8 | |||
| 124398115e | |||
| 541a7b28a7 | |||
| 947b0970ad | |||
| 447fd5b3eb | |||
| 064ffef462 | |||
| 05360ac284 | |||
| 08dabc7331 | |||
| d724df7db2 |
+39
-25
@@ -15,99 +15,113 @@ jobs:
|
|||||||
env: { CGO_ENABLED: 0 }
|
env: { CGO_ENABLED: 0 }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with: { go-version: '1.21' }
|
with: { go-version: '1.21' }
|
||||||
|
|
||||||
- name: Build go2rtc_win64
|
- name: Build go2rtc_win64
|
||||||
env: { GOOS: windows, GOARCH: amd64 }
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_win64
|
- name: Upload go2rtc_win64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||||
|
|
||||||
- name: Build go2rtc_win32
|
- name: Build go2rtc_win32
|
||||||
env: { GOOS: windows, GOARCH: 386 }
|
env: { GOOS: windows, GOARCH: 386 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_win32
|
- name: Upload go2rtc_win32
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_win32, path: go2rtc.exe }
|
with: { name: go2rtc_win32, path: go2rtc.exe }
|
||||||
|
|
||||||
- name: Build go2rtc_win_arm64
|
- name: Build go2rtc_win_arm64
|
||||||
env: { GOOS: windows, GOARCH: arm64 }
|
env: { GOOS: windows, GOARCH: arm64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_win_arm64
|
- name: Upload go2rtc_win_arm64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
|
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_amd64
|
- name: Build go2rtc_linux_amd64
|
||||||
env: { GOOS: linux, GOARCH: amd64 }
|
env: { GOOS: linux, GOARCH: amd64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_amd64
|
- name: Upload go2rtc_linux_amd64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_amd64, path: go2rtc }
|
with: { name: go2rtc_linux_amd64, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_i386
|
- name: Build go2rtc_linux_i386
|
||||||
env: { GOOS: linux, GOARCH: 386 }
|
env: { GOOS: linux, GOARCH: 386 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_i386
|
- name: Upload go2rtc_linux_i386
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_i386, path: go2rtc }
|
with: { name: go2rtc_linux_i386, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_arm64
|
- name: Build go2rtc_linux_arm64
|
||||||
env: { GOOS: linux, GOARCH: arm64 }
|
env: { GOOS: linux, GOARCH: arm64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_arm64
|
- name: Upload go2rtc_linux_arm64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_arm64, path: go2rtc }
|
with: { name: go2rtc_linux_arm64, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_arm
|
- name: Build go2rtc_linux_arm
|
||||||
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
|
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_arm
|
- name: Upload go2rtc_linux_arm
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_arm, path: go2rtc }
|
with: { name: go2rtc_linux_arm, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_armv6
|
- name: Build go2rtc_linux_armv6
|
||||||
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
|
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_armv6
|
- name: Upload go2rtc_linux_armv6
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_armv6, path: go2rtc }
|
with: { name: go2rtc_linux_armv6, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_mipsel
|
- name: Build go2rtc_linux_mipsel
|
||||||
env: { GOOS: linux, GOARCH: mipsle }
|
env: { GOOS: linux, GOARCH: mipsle }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_mipsel
|
- name: Upload go2rtc_linux_mipsel
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_mac_amd64
|
- name: Build go2rtc_mac_amd64
|
||||||
env: { GOOS: darwin, GOARCH: amd64 }
|
env: { GOOS: darwin, GOARCH: amd64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_mac_amd64
|
- name: Upload go2rtc_mac_amd64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_mac_amd64, path: go2rtc }
|
with: { name: go2rtc_mac_amd64, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_mac_arm64
|
- name: Build go2rtc_mac_arm64
|
||||||
env: { GOOS: darwin, GOARCH: arm64 }
|
env: { GOOS: darwin, GOARCH: arm64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_mac_arm64
|
- name: Upload go2rtc_mac_arm64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_mac_arm64, path: go2rtc }
|
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:
|
docker-master:
|
||||||
name: Build docker master
|
name: Build docker master
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: ${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -116,20 +130,20 @@ jobs:
|
|||||||
type=match,pattern=v(.*),group=1
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: |
|
platforms: |
|
||||||
@@ -148,11 +162,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-hw
|
id: meta-hw
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: ${{ github.repository }}
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -164,20 +178,20 @@ jobs:
|
|||||||
type=match,pattern=v(.*),group=1
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: hardware.Dockerfile
|
file: hardware.Dockerfile
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v3
|
uses: actions/configure-pages@v4
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: './website'
|
path: './website'
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v2
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ jobs:
|
|||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
|
|
||||||
@@ -70,13 +70,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/${{ matrix.platform }}
|
platforms: linux/${{ matrix.platform }}
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Hardware
|
- name: Build and push Hardware
|
||||||
if: matrix.platform == 'amd64'
|
if: matrix.platform == 'amd64'
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: hardware.Dockerfile
|
file: hardware.Dockerfile
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ go2rtc.yaml
|
|||||||
go2rtc.json
|
go2rtc.json
|
||||||
|
|
||||||
0_test.go
|
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)
|
<br>
|
||||||
[](https://github.com/AlexxIT/go2rtc/releases)
|
[](https://github.com/AlexxIT/go2rtc/stargazers)
|
||||||
[](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
|
[](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.
|
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
|
||||||
|
|
||||||
@@ -14,6 +17,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
|
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
|
||||||
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
|
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
|
||||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
|
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
|
||||||
|
- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram, etc.)
|
||||||
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
||||||
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
|
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
|
||||||
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||||
@@ -22,7 +26,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
- mixing tracks from different sources to single stream
|
- mixing tracks from different sources to single stream
|
||||||
- auto match client supported codecs
|
- auto match client supported codecs
|
||||||
- [2-way audio](#two-way-audio) for some cameras
|
- [2-way audio](#two-way-audio) for some cameras
|
||||||
- streaming from private networks via [Ngrok](#module-ngrok)
|
- streaming from private networks via [ngrok](#module-ngrok)
|
||||||
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
|
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
|
||||||
|
|
||||||
**Inspired by:**
|
**Inspired by:**
|
||||||
@@ -33,6 +37,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
|
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
|
||||||
- [MediaSoup](https://mediasoup.org/) framework routing idea
|
- [MediaSoup](https://mediasoup.org/) framework routing idea
|
||||||
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
|
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
|
||||||
|
- creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,11 +58,13 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
* [Source: FFmpeg Device](#source-ffmpeg-device)
|
* [Source: FFmpeg Device](#source-ffmpeg-device)
|
||||||
* [Source: Exec](#source-exec)
|
* [Source: Exec](#source-exec)
|
||||||
* [Source: Echo](#source-echo)
|
* [Source: Echo](#source-echo)
|
||||||
|
* [Source: Expr](#source-expr)
|
||||||
* [Source: HomeKit](#source-homekit)
|
* [Source: HomeKit](#source-homekit)
|
||||||
* [Source: Bubble](#source-bubble)
|
* [Source: Bubble](#source-bubble)
|
||||||
* [Source: DVRIP](#source-dvrip)
|
* [Source: DVRIP](#source-dvrip)
|
||||||
* [Source: Tapo](#source-tapo)
|
* [Source: Tapo](#source-tapo)
|
||||||
* [Source: Kasa](#source-kasa)
|
* [Source: Kasa](#source-kasa)
|
||||||
|
* [Source: GoPro](#source-gopro)
|
||||||
* [Source: Ivideon](#source-ivideon)
|
* [Source: Ivideon](#source-ivideon)
|
||||||
* [Source: Hass](#source-hass)
|
* [Source: Hass](#source-hass)
|
||||||
* [Source: ISAPI](#source-isapi)
|
* [Source: ISAPI](#source-isapi)
|
||||||
@@ -67,12 +74,14 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
* [Source: WebTorrent](#source-webtorrent)
|
* [Source: WebTorrent](#source-webtorrent)
|
||||||
* [Incoming sources](#incoming-sources)
|
* [Incoming sources](#incoming-sources)
|
||||||
* [Stream to camera](#stream-to-camera)
|
* [Stream to camera](#stream-to-camera)
|
||||||
|
* [Publish stream](#publish-stream)
|
||||||
* [Module: API](#module-api)
|
* [Module: API](#module-api)
|
||||||
* [Module: RTSP](#module-rtsp)
|
* [Module: RTSP](#module-rtsp)
|
||||||
|
* [Module: RTMP](#module-rtmp)
|
||||||
* [Module: WebRTC](#module-webrtc)
|
* [Module: WebRTC](#module-webrtc)
|
||||||
* [Module: HomeKit](#module-homekit)
|
* [Module: HomeKit](#module-homekit)
|
||||||
* [Module: WebTorrent](#module-webtorrent)
|
* [Module: WebTorrent](#module-webtorrent)
|
||||||
* [Module: Ngrok](#module-ngrok)
|
* [Module: ngrok](#module-ngrok)
|
||||||
* [Module: Hass](#module-hass)
|
* [Module: Hass](#module-hass)
|
||||||
* [Module: MP4](#module-mp4)
|
* [Module: MP4](#module-mp4)
|
||||||
* [Module: HLS](#module-hls)
|
* [Module: HLS](#module-hls)
|
||||||
@@ -122,7 +131,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
|||||||
|
|
||||||
### go2rtc: Docker
|
### 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).
|
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).
|
||||||
|
|
||||||
### go2rtc: Home Assistant Add-on
|
### go2rtc: Home Assistant Add-on
|
||||||
|
|
||||||
@@ -141,13 +150,13 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
|
|||||||
|
|
||||||
Latest, but maybe unstable version:
|
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
|
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
|
||||||
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
|
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
|
||||||
|
|
||||||
## Configuration
|
## 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)
|
- `api` server will start on default **1984 port** (TCP)
|
||||||
- `rtsp` server will start on default **8554 port** (TCP)
|
- `rtsp` server will start on default **8554 port** (TCP)
|
||||||
- `webrtc` will use port **8555** (TCP/UDP) for connections
|
- `webrtc` will use port **8555** (TCP/UDP) for connections
|
||||||
@@ -165,7 +174,7 @@ Available modules:
|
|||||||
- [hls](#module-hls) - HLS TS or fMP4 stream Server
|
- [hls](#module-hls) - HLS TS or fMP4 stream Server
|
||||||
- [mjpeg](#module-mjpeg) - MJPEG Server
|
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||||
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
- [ngrok](#module-ngrok) - ngrok integration (external access for private network)
|
||||||
- [hass](#module-hass) - Home Assistant integration
|
- [hass](#module-hass) - Home Assistant integration
|
||||||
- [log](#module-log) - logs config
|
- [log](#module-log) - logs config
|
||||||
|
|
||||||
@@ -183,11 +192,13 @@ Available source types:
|
|||||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||||
- [exec](#source-exec) - get media from external app output
|
- [exec](#source-exec) - get media from external app output
|
||||||
- [echo](#source-echo) - get stream link from bash or python
|
- [echo](#source-echo) - get stream link from bash or python
|
||||||
|
- [expr](#source-expr) - get stream link via built-in expression language
|
||||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||||
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
|
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
|
||||||
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
|
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
|
||||||
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
|
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
|
||||||
- [kasa](#source-tapo) - TP-Link Kasa cameras
|
- [kasa](#source-tapo) - TP-Link Kasa cameras
|
||||||
|
- [gopro](#source-gopro) - GoPro cameras
|
||||||
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
||||||
- [hass](#source-hass) - Home Assistant integration
|
- [hass](#source-hass) - Home Assistant integration
|
||||||
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
|
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
|
||||||
@@ -202,9 +213,11 @@ Read more about [incoming sources](#incoming-sources)
|
|||||||
Supported for sources:
|
Supported for sources:
|
||||||
|
|
||||||
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
- [RTSP cameras](#source-rtsp) with [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) (back channel connection)
|
||||||
|
- [DVRIP](#source-dvrip) cameras
|
||||||
- [TP-Link Tapo](#source-tapo) cameras
|
- [TP-Link Tapo](#source-tapo) cameras
|
||||||
- [Hikvision ISAPI](#source-isapi) cameras
|
- [Hikvision ISAPI](#source-isapi) cameras
|
||||||
- [Roborock vacuums](#source-roborock) models with cameras
|
- [Roborock vacuums](#source-roborock) models with cameras
|
||||||
|
- [Exec](#source-exec) audio on server
|
||||||
- [Any Browser](#incoming-browser) as IP-camera
|
- [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)).
|
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)).
|
||||||
@@ -222,7 +235,7 @@ streams:
|
|||||||
amcrest_doorbell:
|
amcrest_doorbell:
|
||||||
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
- 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
|
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**
|
**Recommendations**
|
||||||
@@ -257,7 +270,7 @@ streams:
|
|||||||
|
|
||||||
#### Source: RTMP
|
#### 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
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
@@ -297,6 +310,8 @@ streams:
|
|||||||
|
|
||||||
#### Source: ONVIF
|
#### Source: ONVIF
|
||||||
|
|
||||||
|
*[New in v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*
|
||||||
|
|
||||||
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
|
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
|
||||||
|
|
||||||
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
|
**WebUI > Add** webpage support ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use docker, you must use "network host".
|
||||||
@@ -389,24 +404,34 @@ streams:
|
|||||||
|
|
||||||
#### Source: Exec
|
#### Source: Exec
|
||||||
|
|
||||||
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** and **RTSP**.
|
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** (*from [v1.5.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)*) and **RTSP**.
|
||||||
|
|
||||||
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.
|
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:
|
The source can be used with:
|
||||||
|
|
||||||
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source just a shortcut to exec source
|
- [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/)
|
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||||
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
|
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
|
||||||
- any your own software
|
- 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
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
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_h264: exec:libcamera-vid -t 0 --inline -o -
|
||||||
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
|
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -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
|
#### Source: Echo
|
||||||
@@ -422,6 +447,12 @@ streams:
|
|||||||
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Source: Expr
|
||||||
|
|
||||||
|
*[New in v1.8.2](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.2)*
|
||||||
|
|
||||||
|
Like `echo` source, but uses the built-in [expr](https://github.com/antonmedv/expr) expression language ([read more](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md)).
|
||||||
|
|
||||||
#### Source: HomeKit
|
#### Source: HomeKit
|
||||||
|
|
||||||
**Important:**
|
**Important:**
|
||||||
@@ -457,6 +488,8 @@ RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g
|
|||||||
|
|
||||||
#### Source: Bubble
|
#### Source: Bubble
|
||||||
|
|
||||||
|
*[New in v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*
|
||||||
|
|
||||||
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
|
Other names: [ESeeCloud](http://www.eseecloud.com/), [dvr163](http://help.dvr163.com/).
|
||||||
|
|
||||||
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
|
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
|
||||||
@@ -469,6 +502,8 @@ streams:
|
|||||||
|
|
||||||
#### Source: DVRIP
|
#### Source: DVRIP
|
||||||
|
|
||||||
|
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
|
||||||
|
|
||||||
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
|
Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
|
||||||
|
|
||||||
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
|
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
|
||||||
@@ -478,27 +513,43 @@ Other names: DVR-IP, NetSurveillance, Sofia protocol (NETsurveillance ActiveX pl
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
camera1: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||||
|
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||||
|
two_way_audio:
|
||||||
|
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||||
|
- dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Source: Tapo
|
#### Source: Tapo
|
||||||
|
|
||||||
|
*[New in v1.2.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)*
|
||||||
|
|
||||||
[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two way audio** support.
|
[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two way audio** support.
|
||||||
|
|
||||||
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
|
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
|
||||||
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
|
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
|
||||||
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
|
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
|
||||||
|
- some new camera firmwares requires SHA256 instead of MD5
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
# cloud password without username
|
# cloud password without username
|
||||||
camera1: tapo://cloud-password@192.168.1.123
|
camera1: tapo://cloud-password@192.168.1.123
|
||||||
# admin username and UPPERCASE MD5 cloud-password hash
|
# admin username and UPPERCASE MD5 cloud-password hash
|
||||||
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
|
camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123
|
||||||
|
# admin username and UPPERCASE SHA256 cloud-password hash
|
||||||
|
camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo -n "cloud password" | md5 | awk '{print toupper($0)}'
|
||||||
|
echo -n "cloud password" | shasum -a 256 | awk '{print toupper($0)}'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Source: Kasa
|
#### Source: Kasa
|
||||||
|
|
||||||
|
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
|
||||||
|
|
||||||
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
|
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -506,6 +557,12 @@ streams:
|
|||||||
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
|
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Source: GoPro
|
||||||
|
|
||||||
|
*[New in v1.8.3](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)*
|
||||||
|
|
||||||
|
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows. [Read more](https://github.com/AlexxIT/go2rtc/tree/master/internal/gopro).
|
||||||
|
|
||||||
#### Source: Ivideon
|
#### Source: Ivideon
|
||||||
|
|
||||||
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
||||||
@@ -533,11 +590,11 @@ streams:
|
|||||||
aqara_g3: hass:Camera-Hub-G3-AB12
|
aqara_g3: hass:Camera-Hub-G3-AB12
|
||||||
```
|
```
|
||||||
|
|
||||||
**WebRTC Cameras**
|
**WebRTC Cameras** (*from [v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*)
|
||||||
|
|
||||||
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.
|
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
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
@@ -553,6 +610,8 @@ By default, the Home Assistant API does not allow you to get dynamic RTSP link t
|
|||||||
|
|
||||||
#### Source: ISAPI
|
#### Source: ISAPI
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
|
This source type support only backchannel audio for Hikvision ISAPI protocol. So it should be used as second source in addition to the RTSP protocol.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -564,7 +623,9 @@ streams:
|
|||||||
|
|
||||||
#### Source: Nest
|
#### Source: Nest
|
||||||
|
|
||||||
Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes.
|
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
|
||||||
|
|
||||||
|
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.
|
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.
|
||||||
|
|
||||||
@@ -575,6 +636,8 @@ streams:
|
|||||||
|
|
||||||
#### Source: Roborock
|
#### Source: Roborock
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
This source type support Roborock vacuums with cameras. Known working models:
|
This source type support Roborock vacuums with cameras. Known working models:
|
||||||
|
|
||||||
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
- Roborock S6 MaxV - only video (the vacuum has no microphone)
|
||||||
@@ -586,25 +649,27 @@ If you have graphic pin for your vacuum - add it as numeric pin (lines: 123, 456
|
|||||||
|
|
||||||
#### Source: WebRTC
|
#### Source: WebRTC
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
This source type support four connection formats.
|
This source type support four connection formats.
|
||||||
|
|
||||||
**whep**
|
**whep**
|
||||||
|
|
||||||
[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-01.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**
|
**go2rtc**
|
||||||
|
|
||||||
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
|
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
|
||||||
|
|
||||||
**openipc**
|
**openipc** (*from [v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*)
|
||||||
|
|
||||||
Support connection to [OpenIPC](https://openipc.org/) cameras.
|
Support connection to [OpenIPC](https://openipc.org/) cameras.
|
||||||
|
|
||||||
**wyze**
|
**wyze** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
|
||||||
|
|
||||||
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
|
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
|
||||||
|
|
||||||
**kinesis**
|
**kinesis** (*from [v1.6.1](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)*)
|
||||||
|
|
||||||
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).
|
||||||
|
|
||||||
@@ -621,6 +686,8 @@ streams:
|
|||||||
|
|
||||||
#### Source: WebTorrent
|
#### Source: WebTorrent
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
|
This source can get a stream from another go2rtc via [WebTorrent](#module-webtorrent) protocol.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -632,7 +699,7 @@ streams:
|
|||||||
|
|
||||||
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
|
By default, go2rtc establishes a connection to the source when any client requests it. Go2rtc drops the connection to the source when it has no clients left.
|
||||||
|
|
||||||
- Go2rtc also can accepts incoming sources in [RTSP](#source-rtsp), [HTTP](#source-http) and **WebRTC/WHIP** formats
|
- Go2rtc also can accepts incoming sources in [RTSP](#module-rtsp), [RTMP](#module-rtmp), [HTTP](#source-http) and **WebRTC/WHIP** formats
|
||||||
- Go2rtc won't stop such a source if it has no clients
|
- Go2rtc won't stop such a source if it has no clients
|
||||||
- You can push data only to existing stream (create stream with empty source in config)
|
- You can push data only to existing stream (create stream with empty source in config)
|
||||||
- You can push multiple incoming sources to same stream
|
- You can push multiple incoming sources to same stream
|
||||||
@@ -659,6 +726,8 @@ By default, go2rtc establishes a connection to the source when any client reques
|
|||||||
|
|
||||||
#### Incoming: Browser
|
#### Incoming: Browser
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
|
You can turn the browser of any PC or mobile into an IP-camera with support video and two way audio. Or even broadcast your PC screen:
|
||||||
|
|
||||||
1. Create empty stream in the `go2rtc.yaml`
|
1. Create empty stream in the `go2rtc.yaml`
|
||||||
@@ -669,12 +738,16 @@ You can turn the browser of any PC or mobile into an IP-camera with support vide
|
|||||||
|
|
||||||
#### Incoming: WebRTC/WHIP
|
#### Incoming: WebRTC/WHIP
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
You can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):
|
You can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):
|
||||||
|
|
||||||
- Settings > Stream > Service: WHIP > http://192.168.1.123:1984/api/webrtc?dst=camera1
|
- Settings > Stream > Service: WHIP > http://192.168.1.123:1984/api/webrtc?dst=camera1
|
||||||
|
|
||||||
#### Stream to camera
|
#### Stream to camera
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
|
go2rtc support play audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two way audio](#two-way-audio) support (RTSP/ONVIF cameras, TP-Link Tapo, Hikvision ISAPI, Roborock vacuums, any Browser).
|
||||||
|
|
||||||
API example:
|
API example:
|
||||||
@@ -693,10 +766,47 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
|
|||||||
- you can stop active playback by calling the API with the empty `src` parameter
|
- you can stop active playback by calling the API with the empty `src` parameter
|
||||||
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
|
- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming
|
||||||
|
|
||||||
|
### Publish stream
|
||||||
|
|
||||||
|
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
|
||||||
|
|
||||||
|
You can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:
|
||||||
|
|
||||||
|
- Supported codecs: H264 for video and AAC for audio
|
||||||
|
- Pixel format should be `yuv420p`, for cameras with `yuvj420p` format you SHOULD use [transcoding](#source-ffmpeg)
|
||||||
|
- You don't need to enable [RTMP module](#module-rtmp) listening for this task
|
||||||
|
|
||||||
|
You can use API:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...
|
||||||
|
```
|
||||||
|
|
||||||
|
Or config file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
publish:
|
||||||
|
# publish stream "tplink_tapo" to Telegram
|
||||||
|
tplink_tapo: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
# publish stream "other_camera" to Telegram and YouTube
|
||||||
|
other_camera:
|
||||||
|
- rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
- rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
|
||||||
|
|
||||||
|
streams:
|
||||||
|
# for TP-Link cameras it's important to use transcoding because of wrong pixel format
|
||||||
|
tplink_tapo: ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.
|
||||||
|
- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.
|
||||||
|
|
||||||
### Module: API
|
### Module: API
|
||||||
|
|
||||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
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).
|
[API description](https://github.com/AlexxIT/go2rtc/tree/master/api).
|
||||||
|
|
||||||
**Module config**
|
**Module config**
|
||||||
@@ -705,6 +815,7 @@ The HTTP API is the main part for interacting with the application. Default addr
|
|||||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||||
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
||||||
- all files from `static_dir` hosted on root path: `/`
|
- all files from `static_dir` hosted on root path: `/`
|
||||||
|
- you can use raw TLS cert/key content or path to files
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
api:
|
api:
|
||||||
@@ -723,6 +834,7 @@ api:
|
|||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
...
|
...
|
||||||
-----END PRIVATE KEY-----
|
-----END PRIVATE KEY-----
|
||||||
|
unix_listen: "/tmp/go2rtc.sock" # default "", unix socket listener for API
|
||||||
```
|
```
|
||||||
|
|
||||||
**PS:**
|
**PS:**
|
||||||
@@ -753,6 +865,19 @@ By default go2rtc provide RTSP-stream with only one first video and only one fir
|
|||||||
|
|
||||||
Read more about [codecs filters](#codecs-filters).
|
Read more about [codecs filters](#codecs-filters).
|
||||||
|
|
||||||
|
### Module: RTMP
|
||||||
|
|
||||||
|
*[New in v1.8.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)*
|
||||||
|
|
||||||
|
You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now.
|
||||||
|
|
||||||
|
[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rtmp:
|
||||||
|
listen: ":1935" # by default - disabled!
|
||||||
|
```
|
||||||
|
|
||||||
### Module: WebRTC
|
### Module: WebRTC
|
||||||
|
|
||||||
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
|
In most cases [WebRTC](https://en.wikipedia.org/wiki/WebRTC) uses direct peer-to-peer connection from your browser to go2rtc and sends media data via UDP.
|
||||||
@@ -796,7 +921,7 @@ webrtc:
|
|||||||
|
|
||||||
**Private IP**
|
**Private IP**
|
||||||
|
|
||||||
- setup integration with [Ngrok service](#module-ngrok)
|
- setup integration with [ngrok service](#module-ngrok)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ngrok:
|
ngrok:
|
||||||
@@ -822,6 +947,8 @@ webrtc:
|
|||||||
|
|
||||||
### Module: HomeKit
|
### Module: HomeKit
|
||||||
|
|
||||||
|
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
|
||||||
|
|
||||||
HomeKit module can work in two modes:
|
HomeKit module can work in two modes:
|
||||||
|
|
||||||
- export any H264 camera to Apple HomeKit
|
- export any H264 camera to Apple HomeKit
|
||||||
@@ -837,7 +964,7 @@ HomeKit module can work in two modes:
|
|||||||
streams:
|
streams:
|
||||||
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
|
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
|
||||||
homekit:
|
homekit:
|
||||||
dahua1: # same stream ID from streams list, default PIN - 195502224
|
dahua1: # same stream ID from streams list, default PIN - 19550224
|
||||||
```
|
```
|
||||||
|
|
||||||
**Full config**
|
**Full config**
|
||||||
@@ -851,7 +978,7 @@ streams:
|
|||||||
|
|
||||||
homekit:
|
homekit:
|
||||||
dahua1: # same stream ID from streams list
|
dahua1: # same stream ID from streams list
|
||||||
pin: 12345678 # custom PIN, default: 195502224
|
pin: 12345678 # custom PIN, default: 19550224
|
||||||
name: Dahua camera # custom camera name, default: generated from stream ID
|
name: Dahua camera # custom camera name, default: generated from stream ID
|
||||||
device_id: dahua1 # custom ID, default: generated from stream ID
|
device_id: dahua1 # custom ID, default: generated from stream ID
|
||||||
device_private: dahua1 # custom key, default: generated from stream ID
|
device_private: dahua1 # custom key, default: generated from stream ID
|
||||||
@@ -874,6 +1001,8 @@ homekit:
|
|||||||
|
|
||||||
### Module: WebTorrent
|
### Module: WebTorrent
|
||||||
|
|
||||||
|
*[New in v1.3.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)*
|
||||||
|
|
||||||
This module support:
|
This module support:
|
||||||
|
|
||||||
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
|
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
|
||||||
@@ -898,29 +1027,29 @@ Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&
|
|||||||
|
|
||||||
TODO: article how it works...
|
TODO: article how it works...
|
||||||
|
|
||||||
### Module: Ngrok
|
### Module: ngrok
|
||||||
|
|
||||||
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
|
With ngrok integration you can get external access to your streams in situations when you have Internet with private IP-address.
|
||||||
|
|
||||||
- Ngrok preistalled for **Docker** and **Hass Add-on** users
|
- ngrok is pre-installed for **Docker** and **Hass Add-on** users
|
||||||
- you may need external access for two different things:
|
- you may need external access for two different things:
|
||||||
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
|
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
|
||||||
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
|
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
|
||||||
- Ngrok support authorization for your web interface
|
- ngrok support authorization for your web interface
|
||||||
- Ngrok automatically adds HTTPS to your web interface
|
- ngrok automatically adds HTTPS to your web interface
|
||||||
|
|
||||||
Ngrok free subscription limitations:
|
The ngrok free subscription has the following limitations:
|
||||||
|
|
||||||
- you will always get random external address (not a problem for webrtc stream)
|
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and change with each restart of the ngrok agent (not a problem for webrtc stream)
|
||||||
- you can forward multiple ports but use only one Ngrok app
|
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
|
||||||
|
|
||||||
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
|
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
|
||||||
|
|
||||||
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
|
You need to manually download the [ngrok agent app](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
|
||||||
|
|
||||||
**Tunnel for only WebRTC Stream**
|
**Tunnel for only WebRTC Stream**
|
||||||
|
|
||||||
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ngrok:
|
ngrok:
|
||||||
@@ -936,7 +1065,7 @@ ngrok:
|
|||||||
command: ngrok start --all --config ngrok.yaml
|
command: ngrok start --all --config ngrok.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Ngrok config example:
|
ngrok config example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "2"
|
version: "2"
|
||||||
@@ -952,6 +1081,8 @@ tunnels:
|
|||||||
proto: tcp
|
proto: tcp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
|
||||||
|
|
||||||
### Module: Hass
|
### Module: Hass
|
||||||
|
|
||||||
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
|
The best and easiest way to use go2rtc inside the Home Assistant is to install the custom integration [WebRTC Camera](#go2rtc-home-assistant-integration) and custom lovelace card.
|
||||||
@@ -965,7 +1096,7 @@ You have several options on how to add a camera to Home Assistant:
|
|||||||
- Install any [go2rtc](#fast-start)
|
- Install any [go2rtc](#fast-start)
|
||||||
- Add your stream to [go2rtc config](#configuration)
|
- Add your stream to [go2rtc config](#configuration)
|
||||||
- Hass > Settings > Integrations > Add Integration > [ONVIF](https://my.home-assistant.io/redirect/config_flow_start/?domain=onvif) > Host: `127.0.0.1`, Port: `1984`
|
- Hass > Settings > Integrations > Add Integration > [ONVIF](https://my.home-assistant.io/redirect/config_flow_start/?domain=onvif) > Host: `127.0.0.1`, Port: `1984`
|
||||||
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1` (change to your stream name)
|
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > Stream Source URL: `rtsp://127.0.0.1:8554/camera1` (change to your stream name, leave everything else as is)
|
||||||
|
|
||||||
You have several options on how to watch the stream from the cameras in Home Assistant:
|
You have several options on how to watch the stream from the cameras in Home Assistant:
|
||||||
|
|
||||||
@@ -1015,6 +1146,8 @@ Read more about [codecs filters](#codecs-filters).
|
|||||||
|
|
||||||
### Module: HLS
|
### Module: HLS
|
||||||
|
|
||||||
|
*[New in v1.1.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.1.0)*
|
||||||
|
|
||||||
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
|
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. It can only be useful on devices that do not support more modern technology, like [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4).
|
||||||
|
|
||||||
The go2rtc implementation differs from the standards and may not work with all players.
|
The go2rtc implementation differs from the standards and may not work with all players.
|
||||||
@@ -1091,7 +1224,7 @@ webrtc:
|
|||||||
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
|
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
|
||||||
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
|
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
|
||||||
|
|
||||||
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
|
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [ngrok](https://ngrok.com/), etc.
|
||||||
|
|
||||||
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
|
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
|
||||||
|
|
||||||
@@ -1122,17 +1255,14 @@ Some examples:
|
|||||||
|
|
||||||
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
||||||
|
|
||||||
| Device | WebRTC | MSE | HTTP | HLS |
|
| Device | WebRTC | MSE | HTTP* | HLS |
|
||||||
|---------------------|-------------------------------|-------------------------------|------------------------------------|------------------------|
|
|--------------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|
|
||||||
| *latency* | best | medium | bad | bad |
|
| *latency* | best | medium | bad | bad |
|
||||||
| Desktop Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
|
| - Desktop Chrome 107+ <br/> - Desktop Edge <br/> - Android Chrome 107+ | H264 <br/> PCMU, PCMA <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS, MP3 | no |
|
||||||
| Desktop Edge | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
|
| Desktop Firefox | H264 <br/> PCMU, PCMA <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | H264 <br/> AAC, FLAC* <br/> OPUS | no |
|
||||||
| Android Chrome 107+ | H264, OPUS, PCMU, PCMA | H264, H265*, AAC, FLAC*, OPUS | H264, H265*, AAC, FLAC*, OPUS, MP3 | no |
|
| - Desktop Safari 14+ <br/> - iPad Safari 14+ <br/> - iPhone Safari 17.1+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265 <br/> AAC, FLAC* | **no!** | H264, H265 <br/> AAC, FLAC* |
|
||||||
| Desktop Firefox | H264, OPUS, PCMU, PCMA | H264, AAC, FLAC*, OPUS | H264, AAC, FLAC*, OPUS | no |
|
| iPhone Safari 14+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | **no!** | **no!** | H264, H265 <br/> AAC, FLAC* |
|
||||||
| Desktop Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
|
| macOS [Hass App][1] | no | no | no | H264, H265 <br/> AAC, FLAC* |
|
||||||
| iPad Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | H264, H265, AAC, FLAC* | **no!** | H264, H265, AAC, FLAC* |
|
|
||||||
| iPhone Safari 14+ | H264, H265*, OPUS, PCMU, PCMA | **no!** | **no!** | H264, H265, AAC, FLAC* |
|
|
||||||
| macOS [Hass App][1] | no | no | no | H264, H265, AAC, FLAC* |
|
|
||||||
|
|
||||||
[1]: https://apps.apple.com/app/home-assistant/id1099568401
|
[1]: https://apps.apple.com/app/home-assistant/id1099568401
|
||||||
|
|
||||||
@@ -1159,8 +1289,8 @@ Some examples:
|
|||||||
|
|
||||||
- H264 = H.264 = AVC (Advanced Video Coding)
|
- H264 = H.264 = AVC (Advanced Video Coding)
|
||||||
- H265 = H.265 = HEVC (High Efficiency Video Coding)
|
- H265 = H.265 = HEVC (High Efficiency Video Coding)
|
||||||
- PCMU = G.711 PCM (A-law) = PCM A-law (`alaw`)
|
- PCMA = G.711 PCM (A-law) = PCM A-law (`alaw`)
|
||||||
- PCMA = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
|
- PCMU = G.711 PCM (µ-law) = PCM mu-law (`mulaw`)
|
||||||
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
|
- PCM = L16 = PCM signed 16-bit big-endian (`s16be`)
|
||||||
- AAC = MPEG4-GENERIC
|
- AAC = MPEG4-GENERIC
|
||||||
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
|
- MP3 = MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
|
||||||
@@ -1227,14 +1357,22 @@ streams:
|
|||||||
|
|
||||||
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
|
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
|
||||||
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
|
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
|
||||||
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
|
- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - Alternative IP Camera firmware from an open community
|
||||||
|
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
|
||||||
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
|
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
|
||||||
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
|
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
|
||||||
- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - Custom firmware for Wyze cameras
|
|
||||||
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
|
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
|
||||||
|
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
|
||||||
|
|
||||||
|
**Distributions**
|
||||||
|
|
||||||
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
|
- [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)
|
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
|
||||||
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
||||||
|
- [QNAP](https://www.myqnap.org/product/go2rtc/)
|
||||||
|
- [Synology NAS](https://synocommunity.com/package/go2rtc)
|
||||||
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
|
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
|
||||||
|
|
||||||
## Cameras experience
|
## Cameras experience
|
||||||
|
|||||||
+75
-24
@@ -1,4 +1,4 @@
|
|||||||
openapi: 3.0.0
|
openapi: 3.1.0
|
||||||
|
|
||||||
info:
|
info:
|
||||||
title: go2rtc
|
title: go2rtc
|
||||||
@@ -111,9 +111,18 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema: { type: integer }
|
schema: { type: integer }
|
||||||
example: 100
|
example: 100
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
|
/api/restart:
|
||||||
|
post:
|
||||||
|
summary: Restart Daemon
|
||||||
|
description: Restarts the daemon.
|
||||||
|
tags: [ Application ]
|
||||||
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
/api/config:
|
/api/config:
|
||||||
get:
|
get:
|
||||||
@@ -130,14 +139,18 @@ paths:
|
|||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
"*/*": { example: "streams:..." }
|
"*/*": { example: "streams:..." }
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
patch:
|
patch:
|
||||||
summary: Merge changes to main config file
|
summary: Merge changes to main config file
|
||||||
tags: [ Config ]
|
tags: [ Config ]
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
"*/*": { example: "streams:..." }
|
"*/*": { example: "streams:..." }
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -166,7 +179,9 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema: { type: string }
|
schema: { type: string }
|
||||||
example: camera1
|
example: camera1
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
patch:
|
patch:
|
||||||
summary: Update stream source
|
summary: Update stream source
|
||||||
tags: [ Streams list ]
|
tags: [ Streams list ]
|
||||||
@@ -183,7 +198,9 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema: { type: string }
|
schema: { type: string }
|
||||||
example: camera1
|
example: camera1
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
delete:
|
delete:
|
||||||
summary: Delete stream
|
summary: Delete stream
|
||||||
tags: [ Streams list ]
|
tags: [ Streams list ]
|
||||||
@@ -194,7 +211,9 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema: { type: string }
|
schema: { type: string }
|
||||||
example: camera1
|
example: camera1
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
post:
|
post:
|
||||||
summary: Send stream from source to destination
|
summary: Send stream from source to destination
|
||||||
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
|
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
|
||||||
@@ -212,7 +231,9 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema: { type: string }
|
schema: { type: string }
|
||||||
example: camera1
|
example: camera1
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -347,7 +368,9 @@ paths:
|
|||||||
tags: [ Produce stream ]
|
tags: [ Produce stream ]
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/stream_dst_path"
|
- $ref: "#/components/parameters/stream_dst_path"
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/stream.flv?dst={dst}:
|
/api/stream.flv?dst={dst}:
|
||||||
post:
|
post:
|
||||||
summary: Post stream in FLV format
|
summary: Post stream in FLV format
|
||||||
@@ -355,7 +378,9 @@ paths:
|
|||||||
tags: [ Produce stream ]
|
tags: [ Produce stream ]
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/stream_dst_path"
|
- $ref: "#/components/parameters/stream_dst_path"
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/stream.ts?dst={dst}:
|
/api/stream.ts?dst={dst}:
|
||||||
post:
|
post:
|
||||||
summary: Post stream in MPEG-TS format
|
summary: Post stream in MPEG-TS format
|
||||||
@@ -363,7 +388,9 @@ paths:
|
|||||||
tags: [ Produce stream ]
|
tags: [ Produce stream ]
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/stream_dst_path"
|
- $ref: "#/components/parameters/stream_dst_path"
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/stream.mjpeg?dst={dst}:
|
/api/stream.mjpeg?dst={dst}:
|
||||||
post:
|
post:
|
||||||
summary: Post stream in MJPEG format
|
summary: Post stream in MJPEG format
|
||||||
@@ -371,7 +398,9 @@ paths:
|
|||||||
tags: [ Produce stream ]
|
tags: [ Produce stream ]
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/stream_dst_path"
|
- $ref: "#/components/parameters/stream_dst_path"
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -380,49 +409,65 @@ paths:
|
|||||||
summary: DVRIP cameras discovery
|
summary: DVRIP cameras discovery
|
||||||
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
|
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
/api/ffmpeg/devices:
|
/api/ffmpeg/devices:
|
||||||
get:
|
get:
|
||||||
summary: FFmpeg USB devices discovery
|
summary: FFmpeg USB devices discovery
|
||||||
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
|
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/ffmpeg/hardware:
|
/api/ffmpeg/hardware:
|
||||||
get:
|
get:
|
||||||
summary: FFmpeg hardware transcoding discovery
|
summary: FFmpeg hardware transcoding discovery
|
||||||
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
|
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/hass:
|
/api/hass:
|
||||||
get:
|
get:
|
||||||
summary: Home Assistant cameras discovery
|
summary: Home Assistant cameras discovery
|
||||||
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
|
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/homekit:
|
/api/homekit:
|
||||||
get:
|
get:
|
||||||
summary: HomeKit cameras discovery
|
summary: HomeKit cameras discovery
|
||||||
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
|
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/nest:
|
/api/nest:
|
||||||
get:
|
get:
|
||||||
summary: Nest cameras discovery
|
summary: Nest cameras discovery
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/onvif:
|
/api/onvif:
|
||||||
get:
|
get:
|
||||||
summary: ONVIF cameras discovery
|
summary: ONVIF cameras discovery
|
||||||
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
|
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
/api/roborock:
|
/api/roborock:
|
||||||
get:
|
get:
|
||||||
summary: Roborock vacuums discovery
|
summary: Roborock vacuums discovery
|
||||||
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
|
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
|
||||||
tags: [ Discovery ]
|
tags: [ Discovery ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -431,7 +476,9 @@ paths:
|
|||||||
summary: ONVIF server implementation
|
summary: ONVIF server implementation
|
||||||
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
|
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
|
||||||
tags: [ ONVIF ]
|
tags: [ ONVIF ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -440,7 +487,9 @@ paths:
|
|||||||
summary: RTSPtoWebRTC server implementation
|
summary: RTSPtoWebRTC server implementation
|
||||||
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
|
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
|
||||||
tags: [ RTSPtoWebRTC ]
|
tags: [ RTSPtoWebRTC ]
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -465,7 +514,9 @@ paths:
|
|||||||
tags: [ WebTorrent ]
|
tags: [ WebTorrent ]
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/stream_src_path"
|
- $ref: "#/components/parameters/stream_src_path"
|
||||||
responses: { }
|
responses:
|
||||||
|
default:
|
||||||
|
description: Default response
|
||||||
|
|
||||||
/api/webtorrent:
|
/api/webtorrent:
|
||||||
get:
|
get:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,123 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var servs = map[string]string{
|
||||||
|
"3E": "Accessory Information",
|
||||||
|
"7E": "Security System",
|
||||||
|
"85": "Motion Sensor",
|
||||||
|
"96": "Battery",
|
||||||
|
"A2": "Protocol Information",
|
||||||
|
"110": "Camera RTP Stream Management",
|
||||||
|
"112": "Microphone",
|
||||||
|
"113": "Speaker",
|
||||||
|
"121": "Doorbell",
|
||||||
|
"129": "Data Stream Transport Management",
|
||||||
|
"204": "Camera Recording Management",
|
||||||
|
"21A": "Camera Operating Mode",
|
||||||
|
"22A": "Wi-Fi Transport",
|
||||||
|
"239": "Accessory Runtime Information",
|
||||||
|
}
|
||||||
|
|
||||||
|
var chars = map[string]string{
|
||||||
|
"14": "Identify",
|
||||||
|
"20": "Manufacturer",
|
||||||
|
"21": "Model",
|
||||||
|
"23": "Name",
|
||||||
|
"30": "Serial Number",
|
||||||
|
"52": "Firmware Revision",
|
||||||
|
"53": "Hardware Revision",
|
||||||
|
"220": "Product Data",
|
||||||
|
"A6": "Accessory Flags",
|
||||||
|
|
||||||
|
"22": "Motion Detected",
|
||||||
|
"75": "Status Active",
|
||||||
|
|
||||||
|
"11A": "Mute",
|
||||||
|
"119": "Volume",
|
||||||
|
|
||||||
|
"B0": "Active",
|
||||||
|
"209": "Selected Camera Recording Configuration",
|
||||||
|
"207": "Supported Audio Recording Configuration",
|
||||||
|
"205": "Supported Camera Recording Configuration",
|
||||||
|
"206": "Supported Video Recording Configuration",
|
||||||
|
"226": "Recording Audio Active",
|
||||||
|
|
||||||
|
"223": "Event Snapshots Active",
|
||||||
|
"225": "Periodic Snapshots Active",
|
||||||
|
"21B": "HomeKit Camera Active",
|
||||||
|
"21C": "Third Party Camera Active",
|
||||||
|
"21D": "Camera Operating Mode Indicator",
|
||||||
|
"11B": "Night Vision",
|
||||||
|
"129": "Supported Data Stream Transport Configuration",
|
||||||
|
"37": "Version",
|
||||||
|
"131": "Setup Data Stream Transport",
|
||||||
|
"130": "Supported Data Stream Transport Configuration",
|
||||||
|
|
||||||
|
"120": "Streaming Status",
|
||||||
|
"115": "Supported Audio Stream Configuration",
|
||||||
|
"116": "Supported RTP Configuration",
|
||||||
|
"114": "Supported Video Stream Configuration",
|
||||||
|
"117": "Selected RTP Stream Configuration",
|
||||||
|
"118": "Setup Endpoints",
|
||||||
|
|
||||||
|
"22B": "Current Transport",
|
||||||
|
"22C": "Wi-Fi Capabilities",
|
||||||
|
"22D": "Wi-Fi Configuration Control",
|
||||||
|
|
||||||
|
"23C": "Ping",
|
||||||
|
|
||||||
|
"68": "Battery Level",
|
||||||
|
"79": "Status Low Battery",
|
||||||
|
"8F": "Charging State",
|
||||||
|
|
||||||
|
"73": "Programmable Switch Event",
|
||||||
|
"232": "Operating State Response",
|
||||||
|
|
||||||
|
"66": "Security System Current State",
|
||||||
|
"67": "Security System Target State",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
src := os.Args[1]
|
||||||
|
dst := os.Args[2]
|
||||||
|
|
||||||
|
f, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var v hap.JSONAccessories
|
||||||
|
if err = json.NewDecoder(f).Decode(&v); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, acc := range v.Value {
|
||||||
|
for _, srv := range acc.Services {
|
||||||
|
if srv.Desc == "" {
|
||||||
|
srv.Desc = servs[srv.Type]
|
||||||
|
}
|
||||||
|
for _, chr := range srv.Characters {
|
||||||
|
if chr.Desc == "" {
|
||||||
|
chr.Desc = chars[chr.Type]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err = os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err = enc.Encode(v); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,42 +3,46 @@ module github.com/AlexxIT/go2rtc
|
|||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/asticode/go-astits v1.13.0
|
||||||
github.com/miekg/dns v1.1.55
|
github.com/expr-lang/expr v1.16.5
|
||||||
github.com/pion/ice/v2 v2.3.10
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/pion/interceptor v0.1.17
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/pion/rtcp v1.2.10
|
github.com/pion/ice/v2 v2.3.19
|
||||||
github.com/pion/rtp v1.8.1
|
github.com/pion/interceptor v0.1.29
|
||||||
github.com/pion/sdp/v3 v3.0.6
|
github.com/pion/rtcp v1.2.14
|
||||||
github.com/pion/srtp/v2 v2.0.16
|
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/stun v0.6.1
|
||||||
github.com/pion/webrtc/v3 v3.2.17
|
github.com/pion/webrtc/v3 v3.2.39
|
||||||
github.com/rs/zerolog v1.30.0
|
github.com/rs/zerolog v1.32.0
|
||||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
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
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||||
golang.org/x/crypto v0.12.0
|
golang.org/x/crypto v0.22.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/uuid v1.3.1 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pion/datachannel v1.5.5 // indirect
|
github.com/pion/datachannel v1.5.6 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
github.com/pion/dtls/v2 v2.2.10 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.7 // indirect
|
github.com/pion/mdns v0.0.12 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.8 // indirect
|
github.com/pion/sctp v1.8.16 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
github.com/pion/transport/v2 v2.2.5 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.3 // indirect
|
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
golang.org/x/net v0.14.0 // indirect
|
golang.org/x/net v0.24.0 // indirect
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/tools v0.12.0 // indirect
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
golang.org/x/tools v0.20.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,231 +1,182 @@
|
|||||||
|
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||||
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
|
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||||
|
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/expr-lang/expr v1.16.5 h1:m2hvtguFeVaVNTHj8L7BoAyt7O0PAIBaSVbjdHgRXMs=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/expr-lang/expr v1.16.5/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
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.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
|
||||||
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/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
github.com/pion/ice/v2 v2.3.10 h1:T3bUJKqh7pGEdMyTngUcTeQd6io9X8JjgsVWZDannnY=
|
github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
|
||||||
github.com/pion/ice/v2 v2.3.10/go.mod h1:hHGCibDfmXGqukayQw979xEctASp2Pe5Oe0iDU8pRus=
|
github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||||
github.com/pion/interceptor v0.1.17 h1:prJtgwFh/gB8zMqGZoOgJPHivOwVAp61i2aG61Du/1w=
|
github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc=
|
||||||
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
|
github.com/pion/ice/v2 v2.3.19/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 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||||
github.com/pion/rtp v1.8.0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
|
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
|
||||||
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/rtp v1.8.6/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.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
|
||||||
github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U=
|
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
||||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||||
github.com/pion/srtp/v2 v2.0.16 h1:impT2XBrHKsDpXr1x5hHIRydwssrSWKpmw3KvSfXbso=
|
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||||
github.com/pion/srtp/v2 v2.0.16/go.mod h1:NCLCV+U+NpxQ+vXhfOETet4OgKioIgrFjZmIM3ldJYE=
|
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 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
|
||||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
|
||||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
|
||||||
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
|
||||||
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
|
||||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||||
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
|
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/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||||
|
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.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||||
github.com/pion/webrtc/v3 v3.2.17 h1:4ra4H3atxp02e891dz8ZOye2Rgfsv8E2VUksyS1EW28=
|
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||||
github.com/pion/webrtc/v3 v3.2.17/go.mod h1:stMj0DIIhmUF0yOSR02uPAoKapzYbDIthSwW/Uk+AGs=
|
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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
github.com/rs/zerolog v1.32.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-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/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 h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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.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 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
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=
|
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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.17.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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|
||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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-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-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.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.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
golang.org/x/term v0.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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.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.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-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-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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
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-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 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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.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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
+8
-4
@@ -1,8 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:labs
|
# syntax=docker/dockerfile:labs
|
||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
# only debian 12 (bookworm) has latest ffmpeg
|
# only debian 13 (trixie) has latest ffmpeg
|
||||||
ARG DEBIAN_VERSION="bookworm-slim"
|
# https://packages.debian.org/trixie/ffmpeg
|
||||||
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
ARG GO_VERSION="1.21-bookworm"
|
ARG GO_VERSION="1.21-bookworm"
|
||||||
ARG NGROK_VERSION="3"
|
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),
|
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||||
# and other common tools for the echo source.
|
# and other common tools for the echo source.
|
||||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||||
|
# mesa-va-drivers for AMD APU
|
||||||
# libasound2-plugins for ALSA support
|
# libasound2-plugins for ALSA support
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
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 \
|
apt-get -y update && apt-get -y install tini ffmpeg \
|
||||||
python3 curl jq \
|
python3 curl jq \
|
||||||
intel-media-va-driver-non-free \
|
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 / /
|
COPY --link --from=rootfs / /
|
||||||
|
|
||||||
|
|||||||
+120
-62
@@ -10,6 +10,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
@@ -18,25 +20,26 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
BasePath string `yaml:"base_path"`
|
BasePath string `yaml:"base_path"`
|
||||||
StaticDir string `yaml:"static_dir"`
|
StaticDir string `yaml:"static_dir"`
|
||||||
Origin string `yaml:"origin"`
|
Origin string `yaml:"origin"`
|
||||||
TLSListen string `yaml:"tls_listen"`
|
TLSListen string `yaml:"tls_listen"`
|
||||||
TLSCert string `yaml:"tls_cert"`
|
TLSCert string `yaml:"tls_cert"`
|
||||||
TLSKey string `yaml:"tls_key"`
|
TLSKey string `yaml:"tls_key"`
|
||||||
|
UnixListen string `yaml:"unix_listen"`
|
||||||
} `yaml:"api"`
|
} `yaml:"api"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// default config
|
// default config
|
||||||
cfg.Mod.Listen = "0.0.0.0:1984"
|
cfg.Mod.Listen = ":1984"
|
||||||
|
|
||||||
// load config from YAML
|
// load config from YAML
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
if cfg.Mod.Listen == "" {
|
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,16 +51,8 @@ func Init() {
|
|||||||
HandleFunc("api", apiHandler)
|
HandleFunc("api", apiHandler)
|
||||||
HandleFunc("api/config", configHandler)
|
HandleFunc("api/config", configHandler)
|
||||||
HandleFunc("api/exit", exitHandler)
|
HandleFunc("api/exit", exitHandler)
|
||||||
|
HandleFunc("api/restart", restartHandler)
|
||||||
// ensure we can listen without errors
|
HandleFunc("api/log", logHandler)
|
||||||
var err error
|
|
||||||
ln, err = net.Listen("tcp", cfg.Mod.Listen)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("[api] listen")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
|
||||||
|
|
||||||
Handler = http.DefaultServeMux // 4th
|
Handler = http.DefaultServeMux // 4th
|
||||||
|
|
||||||
@@ -73,52 +68,78 @@ func Init() {
|
|||||||
Handler = middlewareLog(Handler) // 1st
|
Handler = middlewareLog(Handler) // 1st
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
if cfg.Mod.Listen != "" {
|
||||||
s := http.Server{}
|
go listen("tcp", cfg.Mod.Listen)
|
||||||
s.Handler = Handler
|
}
|
||||||
if err = s.Serve(ln); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("[api] serve")
|
if cfg.Mod.UnixListen != "" {
|
||||||
}
|
_ = syscall.Unlink(cfg.Mod.UnixListen)
|
||||||
}()
|
go listen("unix", cfg.Mod.UnixListen)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the HTTPS server
|
// Initialize the HTTPS server
|
||||||
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
||||||
cert, err := tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
|
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsListener, err := net.Listen("tcp", cfg.Mod.TLSListen)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
|
|
||||||
|
|
||||||
tlsServer := &http.Server{
|
|
||||||
Handler: Handler,
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("[api] tls serve")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Port() int {
|
func listen(network, address string) {
|
||||||
if ln == nil {
|
ln, err := net.Listen(network, address)
|
||||||
return 0
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api] listen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("addr", address).Msg("[api] listen")
|
||||||
|
|
||||||
|
if network == "tcp" {
|
||||||
|
Port = ln.Addr().(*net.TCPAddr).Port
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
return ln.Addr().(*net.TCPAddr).Port
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tlsListen(network, address, certFile, keyFile string) {
|
||||||
|
var cert tls.Certificate
|
||||||
|
var err error
|
||||||
|
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
|
||||||
|
// check if file path
|
||||||
|
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
} else {
|
||||||
|
// if text file content
|
||||||
|
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen(network, address)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api] tls listen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("addr", address).Msg("[api] tls listen")
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Port int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MimeJSON = "application/json"
|
MimeJSON = "application/json"
|
||||||
MimeText = "text/plain"
|
MimeText = "text/plain"
|
||||||
@@ -178,7 +199,7 @@ func middlewareLog(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
func middlewareAuth(username, password string, next http.Handler) http.Handler {
|
func middlewareAuth(username, password string, next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
|
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
|
||||||
user, pass, ok := r.BasicAuth()
|
user, pass, ok := r.BasicAuth()
|
||||||
if !ok || user != username || pass != password {
|
if !ok || user != username || pass != password {
|
||||||
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||||
@@ -195,12 +216,11 @@ func middlewareCORS(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var ln net.Listener
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
|
|
||||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -218,10 +238,48 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := r.URL.Query().Get("code")
|
s := r.URL.Query().Get("code")
|
||||||
code, _ := strconv.Atoi(s)
|
code, err := strconv.Atoi(s)
|
||||||
|
|
||||||
|
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
|
||||||
|
if err != nil || code < 0 || code > 125 {
|
||||||
|
http.Error(w, "Code must be in the range [0, 125]", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
// Send current state of the log file immediately
|
||||||
|
w.Header().Set("Content-Type", "application/jsonlines")
|
||||||
|
_, _ = app.MemoryLog.WriteTo(w)
|
||||||
|
case "DELETE":
|
||||||
|
app.MemoryLog.Reset()
|
||||||
|
Response(w, "OK", "text/plain")
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
|||||||
+23
-40
@@ -4,20 +4,18 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.7.0"
|
var Version = "1.9.0"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
var ConfigPath string
|
var ConfigPath string
|
||||||
@@ -27,14 +25,34 @@ var Info = map[string]any{
|
|||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var confs Config
|
var confs Config
|
||||||
|
var daemon bool
|
||||||
var version bool
|
var version bool
|
||||||
|
|
||||||
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
flag.BoolVar(&daemon, "daemon", false, "Run program in background")
|
||||||
|
}
|
||||||
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
|
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if version {
|
if version {
|
||||||
fmt.Println("Current version: ", Version)
|
fmt.Println("Current version:", Version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if daemon {
|
||||||
|
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)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,26 +104,6 @@ func Init() {
|
|||||||
migrateStore()
|
migrateStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
|
||||||
var writer io.Writer = os.Stdout
|
|
||||||
|
|
||||||
if format != "json" {
|
|
||||||
writer = zerolog.ConsoleWriter{
|
|
||||||
Out: writer, TimeFormat: "15:04:05.000",
|
|
||||||
NoColor: writer != os.Stdout || format == "text",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = time.RFC3339Nano
|
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
|
||||||
lvl = zerolog.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(v any) {
|
func LoadConfig(v any) {
|
||||||
for _, data := range configs {
|
for _, data := range configs {
|
||||||
if err := yaml.Unmarshal(data, v); err != nil {
|
if err := yaml.Unmarshal(data, v); err != nil {
|
||||||
@@ -114,18 +112,6 @@ func LoadConfig(v any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLogger(module string) zerolog.Logger {
|
|
||||||
if s, ok := modules[module]; ok {
|
|
||||||
lvl, err := zerolog.ParseLevel(s)
|
|
||||||
if err == nil {
|
|
||||||
return log.Level(lvl)
|
|
||||||
}
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
return log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func PatchConfig(key string, value any, path ...string) error {
|
func PatchConfig(key string, value any, path ...string) error {
|
||||||
if ConfigPath == "" {
|
if ConfigPath == "" {
|
||||||
return errors.New("config file disabled")
|
return errors.New("config file disabled")
|
||||||
@@ -156,6 +142,3 @@ func (c *Config) Set(value string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var configs [][]byte
|
var configs [][]byte
|
||||||
|
|
||||||
// modules log levels
|
|
||||||
var modules map[string]string
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MemoryLog *circularBuffer
|
||||||
|
|
||||||
|
func NewLogger(format string, level string) zerolog.Logger {
|
||||||
|
var writer io.Writer = os.Stdout
|
||||||
|
|
||||||
|
if format != "json" {
|
||||||
|
writer = zerolog.ConsoleWriter{
|
||||||
|
Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryLog = newBuffer(16)
|
||||||
|
|
||||||
|
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
|
||||||
|
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||||
|
|
||||||
|
lvl, err := zerolog.ParseLevel(level)
|
||||||
|
if err != nil || lvl == zerolog.NoLevel {
|
||||||
|
lvl = zerolog.InfoLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogger(module string) zerolog.Logger {
|
||||||
|
if s, ok := modules[module]; ok {
|
||||||
|
lvl, err := zerolog.ParseLevel(s)
|
||||||
|
if err == nil {
|
||||||
|
return log.Level(lvl)
|
||||||
|
}
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
return log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// modules log levels
|
||||||
|
var modules map[string]string
|
||||||
|
|
||||||
|
const chunkSize = 1 << 16
|
||||||
|
|
||||||
|
type circularBuffer struct {
|
||||||
|
chunks [][]byte
|
||||||
|
r, w int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBuffer(chunks int) *circularBuffer {
|
||||||
|
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
|
||||||
|
// create first chunk
|
||||||
|
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
||||||
|
n = len(p)
|
||||||
|
|
||||||
|
// check if chunk has size
|
||||||
|
if len(b.chunks[b.w])+n > chunkSize {
|
||||||
|
// increase write chunk index
|
||||||
|
if b.w++; b.w == cap(b.chunks) {
|
||||||
|
b.w = 0
|
||||||
|
}
|
||||||
|
// check overflow
|
||||||
|
if b.r == b.w {
|
||||||
|
// increase read chunk index
|
||||||
|
if b.r++; b.r == cap(b.chunks) {
|
||||||
|
b.r = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if current chunk exists
|
||||||
|
if b.w == len(b.chunks) {
|
||||||
|
// allocate new chunk
|
||||||
|
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||||
|
} else {
|
||||||
|
// reset len of current chunk
|
||||||
|
b.chunks[b.w] = b.chunks[b.w][:0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.chunks[b.w] = append(b.chunks[b.w], p...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
for i := b.r; ; {
|
||||||
|
var nn int
|
||||||
|
if nn, err = w.Write(b.chunks[i]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n += int64(nn)
|
||||||
|
|
||||||
|
if i == b.w {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i++; i == cap(b.chunks) {
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *circularBuffer) Reset() {
|
||||||
|
b.chunks[0] = b.chunks[0][:0]
|
||||||
|
b.r = 0
|
||||||
|
b.w = 0
|
||||||
|
}
|
||||||
@@ -23,17 +23,11 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
func handle(url string) (core.Producer, error) {
|
||||||
conn := dvrip.NewClient(url)
|
client, err := dvrip.Dial(url)
|
||||||
if err := conn.Dial(); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := conn.Play(); err != nil {
|
return client, nil
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Handle(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Port = 34569 // UDP port number for dvrip discovery
|
const Port = 34569 // UDP port number for dvrip discovery
|
||||||
|
|||||||
+29
-12
@@ -5,8 +5,10 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/stdin"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,17 +47,19 @@ func Init() {
|
|||||||
log = app.GetLogger("exec")
|
log = app.GetLogger("exec")
|
||||||
}
|
}
|
||||||
|
|
||||||
func execHandle(url string) (core.Producer, error) {
|
func execHandle(rawURL string) (core.Producer, error) {
|
||||||
var path string
|
var path string
|
||||||
|
|
||||||
args := shell.QuoteSplit(url[5:]) // remove `exec:`
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
|
||||||
|
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
if arg == "{output}" {
|
if arg == "{output}" {
|
||||||
if rtsp.Port == "" {
|
if rtsp.Port == "" {
|
||||||
return nil, errors.New("rtsp module disabled")
|
return nil, errors.New("rtsp module disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
sum := md5.Sum([]byte(url))
|
sum := md5.Sum([]byte(rawURL))
|
||||||
path = "/" + hex.EncodeToString(sum[:])
|
path = "/" + hex.EncodeToString(sum[:])
|
||||||
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
|
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
|
||||||
break
|
break
|
||||||
@@ -67,14 +72,19 @@ func execHandle(url string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return handlePipe(url, cmd)
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
return handlePipe(rawURL, cmd, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleRTSP(url, path, cmd)
|
return handleRTSP(rawURL, path, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
func handlePipe(_ string, cmd *exec.Cmd, query url.Values) (core.Producer, error) {
|
||||||
r, err := PipeCloser(cmd)
|
if query.Get("backchannel") == "1" {
|
||||||
|
return stdin.NewClient(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := PipeCloser(cmd, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -83,7 +93,12 @@ func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return magic.Open(r)
|
prod, err := magic.Open(r)
|
||||||
|
if err != nil {
|
||||||
|
_ = r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return prod, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
||||||
@@ -103,7 +118,7 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
waitersMu.Unlock()
|
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()
|
ts := time.Now()
|
||||||
|
|
||||||
@@ -139,6 +154,8 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
|
|
||||||
// internal
|
// internal
|
||||||
|
|
||||||
var log zerolog.Logger
|
var (
|
||||||
var waiters = map[string]chan core.Producer{}
|
log zerolog.Logger
|
||||||
var waitersMu sync.Mutex
|
waiters = map[string]chan core.Producer{}
|
||||||
|
waitersMu sync.Mutex
|
||||||
|
)
|
||||||
|
|||||||
+31
-5
@@ -2,29 +2,55 @@ package exec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PipeCloser - return StdoutPipe that Kill cmd on Close call
|
// 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()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add buffer for pipe reader to reduce syscall
|
// 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 {
|
type pipeCloser struct {
|
||||||
io.Reader
|
io.Reader
|
||||||
io.Closer
|
io.Closer
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
|
query url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p pipeCloser) Close() error {
|
func (p *pipeCloser) Close() error {
|
||||||
return core.Any(p.Closer.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
|
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,91 @@
|
|||||||
|
# Expr
|
||||||
|
|
||||||
|
[Expr](https://github.com/antonmedv/expr) - expression language and expression evaluation for Go.
|
||||||
|
|
||||||
|
- [language definition](https://expr.medv.io/docs/Language-Definition) - takes best from JS, Python, Jinja2 syntax
|
||||||
|
- your expression should return a link of any supported source
|
||||||
|
- expression supports multiple operation, but:
|
||||||
|
- all operations must be separated by a semicolon
|
||||||
|
- all operations, except the last one, must declare a new variable (`let s = "abc";`)
|
||||||
|
- the last operation should return a string
|
||||||
|
- go2rtc supports additional functions:
|
||||||
|
- `fetch` - JS-like HTTP requests
|
||||||
|
- `match` - JS-like RegExp queries
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Two way audio for Dahua VTO**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
dahua_vto: |
|
||||||
|
expr: let host = "admin:password@192.168.1.123";
|
||||||
|
fetch("http://"+host+"/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000").ok
|
||||||
|
? "rtsp://"+host+"/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif" : ""
|
||||||
|
```
|
||||||
|
|
||||||
|
**dom.ru**
|
||||||
|
|
||||||
|
You can get credentials via:
|
||||||
|
|
||||||
|
- https://github.com/alexmorbo/domru (file `/share/domru/accounts`)
|
||||||
|
- https://github.com/ad/domru
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
dom_ru: |
|
||||||
|
expr: let camera = "99999999"; let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; let operator = 99;
|
||||||
|
fetch("https://myhome.novotelecom.ru/rest/v1/forpost/cameras/"+camera+"/video", {
|
||||||
|
headers: {Authorization: "Bearer "+token, Operator: operator}
|
||||||
|
}).json().data.URL
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parse HLS files from Apple**
|
||||||
|
|
||||||
|
Same example in two languages - python and expr.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
example_python: |
|
||||||
|
echo:python -c 'from urllib.request import urlopen; import re
|
||||||
|
|
||||||
|
# url1 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
|
||||||
|
html1 = urlopen("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").read().decode("utf-8")
|
||||||
|
url1 = re.search(r"https.+?m3u8", html1)[0]
|
||||||
|
|
||||||
|
# url2 = "gear1/prog_index.m3u8"
|
||||||
|
html2 = urlopen(url1).read().decode("utf-8")
|
||||||
|
url2 = re.search(r"^[a-z0-1/_]+\.m3u8$", html2, flags=re.MULTILINE)[0]
|
||||||
|
|
||||||
|
# url3 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8"
|
||||||
|
url3 = url1[:url1.rindex("/")+1] + url2
|
||||||
|
|
||||||
|
print("ffmpeg:" + url3 + "#video=copy")'
|
||||||
|
|
||||||
|
example_expr: |
|
||||||
|
expr:
|
||||||
|
|
||||||
|
let html1 = fetch("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").text;
|
||||||
|
let url1 = match(html1, "https.+?m3u8")[0];
|
||||||
|
|
||||||
|
let html2 = fetch(url1).text;
|
||||||
|
let url2 = match(html2, "^[a-z0-1/_]+\\.m3u8$", "m")[0];
|
||||||
|
|
||||||
|
let url3 = url1[:lastIndexOf(url1, "/")+1] + url2;
|
||||||
|
|
||||||
|
"ffmpeg:" + url3 + "#video=copy"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparsion
|
||||||
|
|
||||||
|
| expr | python | js |
|
||||||
|
|------------------------------|----------------------------|--------------------------------|
|
||||||
|
| let x = 1; | x = 1 | let x = 1 |
|
||||||
|
| {a: 1, b: 2} | {"a": 1, "b": 2} | {a: 1, b: 2} |
|
||||||
|
| let r = fetch(url, {method}) | r = request(method, url) | r = await fetch(url, {method}) |
|
||||||
|
| r.ok | r.ok | r.ok |
|
||||||
|
| r.status | r.status_code | r.status |
|
||||||
|
| r.text | r.text | await r.text() |
|
||||||
|
| r.json() | r.json() | await r.json() |
|
||||||
|
| r.headers | r.headers | r.headers |
|
||||||
|
| let m = match(text, "abc") | m = re.search("abc", text) | let m = text.match(/abc/) |
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
log := app.GetLogger("expr")
|
||||||
|
|
||||||
|
streams.RedirectFunc("expr", func(url string) (string, error) {
|
||||||
|
v, err := expr.Run(url[5:])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("[expr] url=%s", url)
|
||||||
|
|
||||||
|
if url = v.(string); url == "" {
|
||||||
|
return "", errors.New("expr: result is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,30 +45,20 @@ func queryToInput(query url.Values) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if video != "" {
|
if video != "" {
|
||||||
input += ` -i video="` + video + `"`
|
input += ` -i "video=` + video
|
||||||
|
|
||||||
if audio != "" {
|
if audio != "" {
|
||||||
input += `:audio="` + audio + `"`
|
input += `:audio=` + audio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input += `"`
|
||||||
} else {
|
} else {
|
||||||
input += ` -i audio="` + audio + `"`
|
input += ` -i "audio=` + audio + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceInputSuffix(video, audio string) string {
|
|
||||||
switch {
|
|
||||||
case video != "" && audio != "":
|
|
||||||
return `video="` + video + `":audio=` + audio + `"`
|
|
||||||
case video != "":
|
|
||||||
return `video="` + video + `"`
|
|
||||||
case audio != "":
|
|
||||||
return `audio="` + audio + `"`
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDevices() {
|
func initDevices() {
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ var defaults = map[string]string{
|
|||||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||||
// `-tune zerolatency` - for minimal latency
|
// `-tune zerolatency` - for minimal latency
|
||||||
// `-profile high -level 4.1` - most used streaming profile
|
// `-profile high -level 4.1` - most used streaming profile
|
||||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
|
// `-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",
|
||||||
"mjpeg": "-c:v mjpeg",
|
"mjpeg": "-c:v mjpeg",
|
||||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||||
@@ -60,47 +61,58 @@ var defaults = map[string]string{
|
|||||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||||
// https://github.com/pion/webrtc/issues/1514
|
// https://github.com/pion/webrtc/issues/1514
|
||||||
// https://ffmpeg.org/ffmpeg-resampler.html
|
// https://ffmpeg.org/ffmpeg-resampler.html
|
||||||
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
|
// `-async 1` or `-min_comp 0` - force resampling for static timestamp inc, important for WebRTC audio quality
|
||||||
"opus": "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
|
"opus": "-c:a libopus -application:a lowdelay -min_comp 0",
|
||||||
|
"opus/16000": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 16000 -ac:a 1",
|
||||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||||
|
"pcmu/8000": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
||||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
||||||
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||||
|
"pcma/8000": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||||
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
||||||
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
||||||
"aac": "-c:a aac", // keep sample rate and channels
|
"aac": "-c:a aac", // keep sample rate and channels
|
||||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||||
"mp3": "-c:a libmp3lame -q:a 8",
|
"mp3": "-c:a libmp3lame -q:a 8",
|
||||||
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||||
|
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||||
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||||
|
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||||
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
|
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
|
||||||
|
|
||||||
// hardware Intel and AMD on Linux
|
// hardware Intel and AMD on Linux
|
||||||
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
||||||
// `-bf 0` - disable B-frames is very important
|
// `-bf 0` - disable B-frames is very important
|
||||||
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
||||||
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
|
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0",
|
||||||
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
||||||
|
|
||||||
// hardware Raspberry
|
// hardware Raspberry
|
||||||
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
||||||
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
||||||
|
|
||||||
|
// hardware Rockchip
|
||||||
|
// important to use custom ffmpeg https://github.com/AlexxIT/go2rtc/issues/768
|
||||||
|
// hevc - doesn't have a profile setting
|
||||||
|
"h264/rkmpp": "-c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||||
|
"h265/rkmpp": "-c:v hevc_rkmpp_encoder -g 50 -bf 0 -level:v 5.1",
|
||||||
|
|
||||||
// hardware NVidia on Linux and Windows
|
// hardware NVidia on Linux and Windows
|
||||||
// preset=p2 - faster, tune=ll - low latency
|
// preset=p2 - faster, tune=ll - low latency
|
||||||
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
||||||
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto",
|
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v main -level:v auto",
|
||||||
|
|
||||||
// hardware Intel on Windows
|
// hardware Intel on Windows
|
||||||
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
||||||
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
|
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v main -level:v 5.1 -async_depth:v 1",
|
||||||
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
|
"mjpeg/dxva2": "-c:v mjpeg_qsv",
|
||||||
|
|
||||||
// hardware macOS
|
// hardware macOS
|
||||||
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
|
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||||
}
|
}
|
||||||
|
|
||||||
// configTemplate - return template from config (defaults) if exist or return raw template
|
// configTemplate - return template from config (defaults) if exist or return raw template
|
||||||
|
|||||||
+136
-51
@@ -7,39 +7,62 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParseArgsFile(t *testing.T) {
|
func TestParseArgsFile(t *testing.T) {
|
||||||
// [FILE] all tracks will be copied without transcoding codecs
|
tests := []struct {
|
||||||
args := parseArgs("/media/bbb.mp4")
|
name string
|
||||||
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())
|
source string
|
||||||
|
expect 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 yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[FILE] all tracks will be copied without transcoding codecs",
|
||||||
|
source: "/media/bbb.mp4",
|
||||||
// [FILE] video will be copied, audio will be transcoded to pcmu
|
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
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())
|
{
|
||||||
|
name: "[FILE] video will be transcoded to H264, audio will be skipped",
|
||||||
// [FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped
|
source: "/media/bbb.mp4#video=h264",
|
||||||
args = parseArgs("/media/bbb.mp4#video=h265#rotate=-90")
|
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}`,
|
||||||
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
|
name: "[FILE] video will be copied, audio will be transcoded to pcmu",
|
||||||
args = parseArgs("/media/bbb.mp4#video=mjpeg")
|
source: "/media/bbb.mp4#video=copy#audio=pcmu",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, args.String())
|
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}`,
|
||||||
|
},
|
||||||
// https://github.com/AlexxIT/go2rtc/issues/509
|
{
|
||||||
args = parseArgs("ffmpeg:test.mp4#raw=-ss 00:00:20")
|
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
|
||||||
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())
|
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) {
|
func TestParseArgsDevice(t *testing.T) {
|
||||||
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
|
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
|
||||||
args := parseArgs("device?video=0&video_size=1920x1080")
|
args := parseArgs("device?video=0&video_size=1920x1080")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
|
|
||||||
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
|
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
|
||||||
args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
//args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1280x720 -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
args = parseArgs("device?video=0&framerate=20#video=h265")
|
||||||
|
require.Equal(t, `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
|
|
||||||
|
args = parseArgs("device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)")
|
||||||
|
require.Equal(t, `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsIpCam(t *testing.T) {
|
func TestParseArgsIpCam(t *testing.T) {
|
||||||
@@ -49,7 +72,7 @@ func TestParseArgsIpCam(t *testing.T) {
|
|||||||
|
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args = parseArgs("http://example.com#video=h264")
|
args = parseArgs("http://example.com#video=h264")
|
||||||
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 yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
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 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
|
|
||||||
// [HLS] video will be copied, audio will be skipped
|
// [HLS] video will be copied, audio will be skipped
|
||||||
args = parseArgs("https://example.com#video=copy")
|
args = parseArgs("https://example.com#video=copy")
|
||||||
@@ -83,7 +106,7 @@ func TestParseArgsAudio(t *testing.T) {
|
|||||||
|
|
||||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
||||||
args = parseArgs("rtsp:///example.com#audio=opus")
|
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 -ar:a 48000 -ac:a 2 -application:a voip -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
|
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
||||||
@@ -111,28 +134,46 @@ func TestParseArgsAudio(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsHwVaapi(t *testing.T) {
|
func TestParseArgsHwVaapi(t *testing.T) {
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
tests := []struct {
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
|
name string
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -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" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source string
|
||||||
|
expect 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 -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" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[HTTP-MJPEG] video will be transcoded to H264",
|
||||||
|
source: "http:///example.com#video=h264#hardware=vaapi",
|
||||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
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}`,
|
||||||
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 -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" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
{
|
||||||
|
name: "[RTSP] video with rotation, should be transcoded, so select H264",
|
||||||
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
|
source: "rtsp://example.com#video=h264#rotate=180#hardware=vaapi",
|
||||||
args = parseArgs("/media/bbb.mp4#video=mjpeg#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}`,
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
|
},
|
||||||
|
{
|
||||||
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
|
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
|
||||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
|
source: "rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -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" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
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
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
|
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())
|
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())
|
||||||
@@ -150,7 +191,19 @@ func TestParseArgsHwV4l2m2m(t *testing.T) {
|
|||||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsHwCuda(t *testing.T) {
|
func TestParseArgsHwRKMPP(t *testing.T) {
|
||||||
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
|
args := parseArgs("http://example.com#video=h264#hardware=rkmpp")
|
||||||
|
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 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
|
|
||||||
|
args = parseArgs("http://example.com#video=h264#rotate=180#hardware=rkmpp")
|
||||||
|
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 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
|
|
||||||
|
args = parseArgs("http://example.com#video=h264#height=320#hardware=rkmpp")
|
||||||
|
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) {
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
|
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())
|
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())
|
||||||
@@ -168,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())
|
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
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
|
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())
|
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())
|
||||||
@@ -190,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())
|
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
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
|
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())
|
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())
|
||||||
@@ -207,3 +260,35 @@ func TestParseArgsHwVideotoolbox(t *testing.T) {
|
|||||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
|
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_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) {
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const (
|
|||||||
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
||||||
EngineDXVA2 = "dxva2" // Intel on Windows
|
EngineDXVA2 = "dxva2" // Intel on Windows
|
||||||
EngineVideoToolbox = "videotoolbox" // macOS
|
EngineVideoToolbox = "videotoolbox" // macOS
|
||||||
|
EngineRKMPP = "rkmpp" // Rockchip
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(bin string) {
|
func Init(bin string) {
|
||||||
@@ -61,6 +62,10 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
|||||||
if !args.HasFilters("drawtext=") {
|
if !args.HasFilters("drawtext=") {
|
||||||
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
|
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
|
||||||
|
|
||||||
|
if name == "h264" {
|
||||||
|
fixPixelFormat(args)
|
||||||
|
}
|
||||||
|
|
||||||
for i, filter := range args.Filters {
|
for i, filter := range args.Filters {
|
||||||
if strings.HasPrefix(filter, "scale=") {
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
args.Filters[i] = "scale_vaapi=" + filter[6:]
|
args.Filters[i] = "scale_vaapi=" + filter[6:]
|
||||||
@@ -121,6 +126,24 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
|||||||
|
|
||||||
case EngineV4L2M2M:
|
case EngineV4L2M2M:
|
||||||
args.Codecs[i] = defaults[name+"/"+engine]
|
args.Codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
case EngineRKMPP:
|
||||||
|
args.Codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
for j, filter := range args.Filters {
|
||||||
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
|
args.Filters = append(args.Filters[:j], args.Filters[j+1:]...)
|
||||||
|
|
||||||
|
width, height, _ := strings.Cut(filter[6:], ":")
|
||||||
|
if width != "-1" {
|
||||||
|
args.Codecs[i] += " -width " + width
|
||||||
|
}
|
||||||
|
if height != "-1" {
|
||||||
|
args.Codecs[i] += " -height " + height
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,3 +177,24 @@ func cut(s string, sep byte, pos int) string {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fixPixelFormat:
|
||||||
|
// - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)
|
||||||
|
// - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)
|
||||||
|
// - bad jpeg pixel: yuvj422p(pc, bt470bg)
|
||||||
|
func fixPixelFormat(args *ffmpeg.Args) {
|
||||||
|
// in my tests this filters has same CPU/GPU load:
|
||||||
|
// - "hwupload"
|
||||||
|
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv"
|
||||||
|
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12"
|
||||||
|
const fixPixFmt = "out_color_matrix=bt709:out_range=tv:format=nv12"
|
||||||
|
|
||||||
|
for i, filter := range args.Filters {
|
||||||
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
|
args.Filters[i] = filter + ":" + fixPixFmt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Filters = append(args.Filters, "scale="+fixPixFmt)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -6,13 +6,17 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
const (
|
||||||
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
||||||
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
|
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
||||||
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
|
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
|
||||||
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
|
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
|
||||||
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
|
||||||
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
|
||||||
|
ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
|
||||||
|
ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||||
|
ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||||
|
)
|
||||||
|
|
||||||
func ProbeAll(bin string) []*api.Source {
|
func ProbeAll(bin string) []*api.Source {
|
||||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||||
@@ -25,6 +29,14 @@ func ProbeAll(bin string) []*api.Source {
|
|||||||
Name: runToString(bin, ProbeV4L2M2MH265),
|
Name: runToString(bin, ProbeV4L2M2MH265),
|
||||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
|
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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +71,16 @@ func ProbeHardware(bin, name string) string {
|
|||||||
if run(bin, ProbeV4L2M2MH264) {
|
if run(bin, ProbeV4L2M2MH264) {
|
||||||
return EngineV4L2M2M
|
return EngineV4L2M2M
|
||||||
}
|
}
|
||||||
|
if run(bin, ProbeRKMPPH264) {
|
||||||
|
return EngineRKMPP
|
||||||
|
}
|
||||||
case "h265":
|
case "h265":
|
||||||
if run(bin, ProbeV4L2M2MH265) {
|
if run(bin, ProbeV4L2M2MH265) {
|
||||||
return EngineV4L2M2M
|
return EngineV4L2M2M
|
||||||
}
|
}
|
||||||
|
if run(bin, ProbeRKMPPH265) {
|
||||||
|
return EngineRKMPP
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return EngineSoftware
|
return EngineSoftware
|
||||||
|
|||||||
@@ -19,5 +19,5 @@ func TestParseQuery(t *testing.T) {
|
|||||||
query, err = url.ParseQuery("hw=vaapi")
|
query, err = url.ParseQuery("hw=vaapi")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
args = parseQuery(query)
|
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,25 @@
|
|||||||
|
# GoPro
|
||||||
|
|
||||||
|
Supported models: HERO9, HERO10, HERO11, HERO12.
|
||||||
|
Supported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)
|
||||||
|
|
||||||
|
The other camera models have different APIs. I will try to add them in the next versions.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
- USB-connected cameras create a new network interface in the system
|
||||||
|
- Linux users do not need to install anything
|
||||||
|
- Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)
|
||||||
|
- if the camera is detected but the stream does not start - you need to disable firewall
|
||||||
|
|
||||||
|
1. Discover camera address: WebUI > Add > GoPro
|
||||||
|
2. Add camera to config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
hero12: gopro://172.20.100.51
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://gopro.github.io/OpenGoPro/
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package gopro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/gopro"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.HandleFunc("gopro", handleGoPro)
|
||||||
|
|
||||||
|
api.HandleFunc("api/gopro", apiGoPro)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGoPro(rawURL string) (core.Producer, error) {
|
||||||
|
return gopro.Dial(rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var items []*api.Source
|
||||||
|
|
||||||
|
for _, host := range gopro.Discovery() {
|
||||||
|
items = append(items, &api.Source{Name: host, URL: "gopro://" + host})
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ResponseSources(w, items)
|
||||||
|
}
|
||||||
@@ -71,7 +71,8 @@ func discovery() ([]*api.Source, error) {
|
|||||||
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||||
log.Trace().Msgf("[homekit] mdns=%s", entry)
|
log.Trace().Msgf("[homekit] mdns=%s", entry)
|
||||||
|
|
||||||
if entry.Complete() && entry.Info[hap.TXTCategory] == hap.CategoryCamera {
|
category := entry.Info[hap.TXTCategory]
|
||||||
|
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
|
||||||
source := &api.Source{
|
source := &api.Source{
|
||||||
Name: entry.Name,
|
Name: entry.Name,
|
||||||
Info: entry.Info[hap.TXTModel],
|
Info: entry.Info[hap.TXTModel],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -97,7 +98,7 @@ func Init() {
|
|||||||
|
|
||||||
srv.mdns = &mdns.ServiceEntry{
|
srv.mdns = &mdns.ServiceEntry{
|
||||||
Name: name,
|
Name: name,
|
||||||
Port: uint16(api.Port()),
|
Port: uint16(api.Port),
|
||||||
Info: map[string]string{
|
Info: map[string]string{
|
||||||
hap.TXTConfigNumber: "1",
|
hap.TXTConfigNumber: "1",
|
||||||
hap.TXTFeatureFlags: "0",
|
hap.TXTFeatureFlags: "0",
|
||||||
@@ -121,7 +122,7 @@ func Init() {
|
|||||||
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
|
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
|
||||||
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
|
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
|
||||||
|
|
||||||
log.Trace().Msgf("[homekit] mnds: %s", entries)
|
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
||||||
@@ -134,6 +135,10 @@ var log zerolog.Logger
|
|||||||
var servers map[string]*server
|
var servers map[string]*server
|
||||||
|
|
||||||
func streamHandler(url string) (core.Producer, error) {
|
func streamHandler(url string) (core.Producer, error) {
|
||||||
|
if srtp.Server == nil {
|
||||||
|
return nil, errors.New("homekit: can't work without SRTP server")
|
||||||
|
}
|
||||||
|
|
||||||
return homekit.Dial(url, srtp.Server)
|
return homekit.Dial(url, srtp.Server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -198,9 +198,11 @@ func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions by
|
|||||||
"client_public": []string{hex.EncodeToString(public)},
|
"client_public": []string{hex.EncodeToString(public)},
|
||||||
"permissions": []string{string('0' + permissions)},
|
"permissions": []string{string('0' + permissions)},
|
||||||
}
|
}
|
||||||
s.pairings = append(s.pairings, query.Encode())
|
if s.GetPair(conn, id) == nil {
|
||||||
s.UpdateStatus()
|
s.pairings = append(s.pairings, query.Encode())
|
||||||
s.PatchConfig()
|
s.UpdateStatus()
|
||||||
|
s.PatchConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) DelPair(conn net.Conn, id string) {
|
func (s *server) DelPair(conn net.Conn, id string) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hls"
|
"github.com/AlexxIT/go2rtc/pkg/hls"
|
||||||
@@ -22,6 +23,8 @@ func Init() {
|
|||||||
streams.HandleFunc("httpx", handleHTTP)
|
streams.HandleFunc("httpx", handleHTTP)
|
||||||
|
|
||||||
streams.HandleFunc("tcp", handleTCP)
|
streams.HandleFunc("tcp", handleTCP)
|
||||||
|
|
||||||
|
api.HandleFunc("api/stream", apiStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHTTP(rawURL string) (core.Producer, error) {
|
func handleHTTP(rawURL string) (core.Producer, error) {
|
||||||
@@ -89,3 +92,26 @@ func handleTCP(rawURL string) (core.Producer, error) {
|
|||||||
|
|
||||||
return magic.Open(conn)
|
return magic.Open(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiStream(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dst := r.URL.Query().Get("dst")
|
||||||
|
stream := streams.Get(dst)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := magic.Open(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.AddProducer(client)
|
||||||
|
defer stream.RemoveProducer(client)
|
||||||
|
|
||||||
|
if err = client.Start(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-2
@@ -91,8 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
src := query.Get("src")
|
stream := streams.GetOrPatch(query)
|
||||||
stream := streams.Get(src)
|
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -56,19 +56,17 @@ func inputMpegTS(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Request: r}
|
client, err := mpegts.Open(r.Body)
|
||||||
client, err := mpegts.Open(res.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.AddProducer(client)
|
stream.AddProducer(client)
|
||||||
|
defer stream.RemoveProducer(client)
|
||||||
|
|
||||||
if err = client.Start(); err != nil {
|
if err = client.Start(); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.RemoveProducer(client)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package ngrok
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ngrok"
|
"github.com/AlexxIT/go2rtc/pkg/ngrok"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -39,7 +40,7 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Addr: "//localhost:8555", URL: "tcp://1.tcp.eu.ngrok.io:12345"
|
// Addr: "//localhost:8555", URL: "tcp://1.tcp.eu.ngrok.io:12345"
|
||||||
if msg.Addr == "//localhost:"+webrtc.Port && strings.HasPrefix(msg.URL, "tcp://") {
|
if strings.HasPrefix(msg.Addr, "//localhost:") && strings.HasPrefix(msg.URL, "tcp://") {
|
||||||
// don't know if really necessary use IP
|
// don't know if really necessary use IP
|
||||||
address, err := ConvertHostToIP(msg.URL[6:])
|
address, err := ConvertHostToIP(msg.URL[6:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,7 +50,7 @@ func Init() {
|
|||||||
|
|
||||||
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
|
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
|
||||||
|
|
||||||
webrtc.AddCandidate(address)
|
webrtc.AddCandidate(address, "tcp")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+152
-3
@@ -1,39 +1,188 @@
|
|||||||
package rtmp
|
package rtmp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/flv"
|
"github.com/AlexxIT/go2rtc/pkg/flv"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
|
var conf struct {
|
||||||
|
Mod struct {
|
||||||
|
Listen string `yaml:"listen" json:"listen"`
|
||||||
|
} `yaml:"rtmp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
app.LoadConfig(&conf)
|
||||||
|
|
||||||
|
log = app.GetLogger("rtmp")
|
||||||
|
|
||||||
streams.HandleFunc("rtmp", streamsHandle)
|
streams.HandleFunc("rtmp", streamsHandle)
|
||||||
streams.HandleFunc("rtmps", streamsHandle)
|
streams.HandleFunc("rtmps", streamsHandle)
|
||||||
streams.HandleFunc("rtmpx", streamsHandle)
|
streams.HandleFunc("rtmpx", streamsHandle)
|
||||||
|
|
||||||
api.HandleFunc("api/stream.flv", apiHandle)
|
api.HandleFunc("api/stream.flv", apiHandle)
|
||||||
|
|
||||||
|
streams.HandleConsumerFunc("rtmp", streamsConsumerHandle)
|
||||||
|
streams.HandleConsumerFunc("rtmps", streamsConsumerHandle)
|
||||||
|
streams.HandleConsumerFunc("rtmpx", streamsConsumerHandle)
|
||||||
|
|
||||||
|
address := conf.Mod.Listen
|
||||||
|
if address == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("addr", address).Msg("[rtmp] listen")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err = tcpHandle(conn); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tcpHandle(netConn net.Conn) error {
|
||||||
|
rtmpConn, err := rtmp.NewServer(netConn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rtmpConn.ReadCommands(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rtmpConn.Intent {
|
||||||
|
case rtmp.CommandPlay:
|
||||||
|
stream := streams.Get(rtmpConn.App)
|
||||||
|
if stream == nil {
|
||||||
|
return errors.New("stream not found: " + rtmpConn.App)
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := flv.NewConsumer()
|
||||||
|
if err = stream.AddConsumer(cons); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
|
if err = rtmpConn.WriteStart(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = cons.WriteTo(rtmpConn)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case rtmp.CommandPublish:
|
||||||
|
stream := streams.Get(rtmpConn.App)
|
||||||
|
if stream == nil {
|
||||||
|
return errors.New("stream not found: " + rtmpConn.App)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rtmpConn.WriteStart(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prod, err := rtmpConn.Producer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.AddProducer(prod)
|
||||||
|
|
||||||
|
defer stream.RemoveProducer(prod)
|
||||||
|
|
||||||
|
_ = prod.Start()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("rtmp: unknown command: " + rtmpConn.Intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
func streamsHandle(url string) (core.Producer, error) {
|
func streamsHandle(url string) (core.Producer, error) {
|
||||||
client, err := rtmp.Dial(url)
|
client, err := rtmp.DialPlay(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func streamsConsumerHandle(url string) (core.Consumer, func(), error) {
|
||||||
|
cons := flv.NewConsumer()
|
||||||
|
run := func() {
|
||||||
|
wr, err := rtmp.DialPublish(url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = cons.WriteTo(wr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cons, run, nil
|
||||||
|
}
|
||||||
|
|
||||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
outputFLV(w, r)
|
||||||
|
} else {
|
||||||
|
inputFLV(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputFLV(w http.ResponseWriter, r *http.Request) {
|
||||||
|
src := r.URL.Query().Get("src")
|
||||||
|
stream := streams.Get(src)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cons := flv.NewConsumer()
|
||||||
|
cons.Type = "HTTP-FLV consumer"
|
||||||
|
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
|
cons.UserAgent = r.UserAgent()
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h := w.Header()
|
||||||
|
h.Set("Content-Type", "video/x-flv")
|
||||||
|
|
||||||
|
_, _ = cons.WriteTo(w)
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inputFLV(w http.ResponseWriter, r *http.Request) {
|
||||||
dst := r.URL.Query().Get("dst")
|
dst := r.URL.Query().Get("dst")
|
||||||
stream := streams.Get(dst)
|
stream := streams.Get(dst)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// default config
|
// default config
|
||||||
conf.Mod.Listen = "0.0.0.0:8554"
|
conf.Mod.Listen = ":8554"
|
||||||
conf.Mod.DefaultQuery = "video&audio"
|
conf.Mod.DefaultQuery = "video&audio"
|
||||||
|
|
||||||
app.LoadConfig(&conf)
|
app.LoadConfig(&conf)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// default config
|
// default config
|
||||||
cfg.Mod.Listen = "0.0.0.0:8443"
|
cfg.Mod.Listen = ":8443"
|
||||||
|
|
||||||
// load config from YAML
|
// load config from YAML
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|||||||
@@ -73,3 +73,25 @@ func Location(url string) (string, error) {
|
|||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: rework
|
||||||
|
|
||||||
|
type ConsumerHandler func(url string) (core.Consumer, func(), error)
|
||||||
|
|
||||||
|
var consumerHandlers = map[string]ConsumerHandler{}
|
||||||
|
|
||||||
|
func HandleConsumerFunc(scheme string, handler ConsumerHandler) {
|
||||||
|
consumerHandlers[scheme] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConsumer(url string) (core.Consumer, func(), error) {
|
||||||
|
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||||
|
scheme := url[:i]
|
||||||
|
|
||||||
|
if handler, ok := consumerHandlers[scheme]; ok {
|
||||||
|
return handler(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, errors.New("streams: unsupported scheme: " + url)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ParseQuery(s string) url.Values {
|
func ParseQuery(s string) url.Values {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
for _, key := range strings.Split(s, "#") {
|
for _, key := range strings.Split(s, "#") {
|
||||||
var value string
|
var value string
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package streams
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func (s *Stream) Publish(url string) error {
|
||||||
|
cons, run, err := GetConsumer(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.AddConsumer(cons); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
run()
|
||||||
|
s.RemoveConsumer(cons)
|
||||||
|
|
||||||
|
// TODO: more smart retry
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
_ = s.Publish(url)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Publish(stream *Stream, destination any) {
|
||||||
|
switch v := destination.(type) {
|
||||||
|
case string:
|
||||||
|
if err := stream.Publish(v); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, v := range v {
|
||||||
|
Publish(stream, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,8 +22,13 @@ func NewStream(source any) *Stream {
|
|||||||
}
|
}
|
||||||
case []any:
|
case []any:
|
||||||
s := new(Stream)
|
s := new(Stream)
|
||||||
for _, source := range source {
|
for _, src := range source {
|
||||||
s.producers = append(s.producers, NewProducer(source.(string)))
|
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
|
return s
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
func TestRecursion(t *testing.T) {
|
func TestRecursion(t *testing.T) {
|
||||||
// create stream with some source
|
// create stream with some source
|
||||||
stream1 := New("from_yaml", "does not matter")
|
stream1 := New("from_yaml", "does_not_matter")
|
||||||
require.Len(t, streams, 1)
|
require.Len(t, streams, 1)
|
||||||
|
|
||||||
// ask another unnamed stream that links go2rtc
|
// ask another unnamed stream that links go2rtc
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
@@ -13,18 +14,31 @@ import (
|
|||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]any `yaml:"streams"`
|
Streams map[string]any `yaml:"streams"`
|
||||||
|
Publish map[string]any `yaml:"publish"`
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
log = app.GetLogger("streams")
|
log = app.GetLogger("streams")
|
||||||
|
|
||||||
for name, item := range cfg.Mod {
|
for name, item := range cfg.Streams {
|
||||||
streams[name] = NewStream(item)
|
streams[name] = NewStream(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.HandleFunc("api/streams", streamsHandler)
|
api.HandleFunc("api/streams", streamsHandler)
|
||||||
|
|
||||||
|
if cfg.Publish == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.AfterFunc(time.Second, func() {
|
||||||
|
for name, dst := range cfg.Publish {
|
||||||
|
if stream := Get(name); stream != nil {
|
||||||
|
Publish(stream, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(name string) *Stream {
|
func Get(name string) *Stream {
|
||||||
@@ -172,6 +186,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
api.ResponseJSON(w, stream)
|
api.ResponseJSON(w, stream)
|
||||||
}
|
}
|
||||||
|
} else if stream = Get(src); stream != nil {
|
||||||
|
if err := stream.Publish(dst); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "", http.StatusNotFound)
|
http.Error(w, "", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
## Config
|
||||||
|
|
||||||
|
- supported TCP: fixed port (default), disabled
|
||||||
|
- supported UDP: random port (default), fixed port
|
||||||
|
|
||||||
|
| Config examples | TCP | UDP |
|
||||||
|
|-----------------------|-------|--------|
|
||||||
|
| `listen: ":8555/tcp"` | fixed | random |
|
||||||
|
| `listen: ":8555"` | fixed | fixed |
|
||||||
|
| `listen: ""` | no | random |
|
||||||
|
|
||||||
## Userful links
|
## Userful links
|
||||||
|
|
||||||
- https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html
|
- https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html
|
||||||
|
|||||||
@@ -1,58 +1,66 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Address struct {
|
type Address struct {
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port string
|
||||||
|
Network string
|
||||||
|
Offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
var addresses []Address
|
func (a *Address) Marshal() string {
|
||||||
|
host := a.Host
|
||||||
func AddCandidate(address string) {
|
if host == "stun" {
|
||||||
var port int
|
ip, err := webrtc.GetCachedPublicIP()
|
||||||
|
if err != nil {
|
||||||
// try to get port from address string
|
return ""
|
||||||
if i := strings.LastIndexByte(address, ':'); i > 0 {
|
|
||||||
if v, _ := strconv.Atoi(address[i+1:]); v != 0 {
|
|
||||||
address = address[:i]
|
|
||||||
port = v
|
|
||||||
}
|
}
|
||||||
|
host = ip.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// use default WebRTC port
|
switch a.Network {
|
||||||
if port == 0 {
|
case "udp":
|
||||||
port, _ = strconv.Atoi(Port)
|
return webrtc.CandidateManualHostUDP(host, a.Port, a.Offset)
|
||||||
|
case "tcp":
|
||||||
|
return webrtc.CandidateManualHostTCPPassive(host, a.Port, a.Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses = append(addresses, Address{Host: address, Port: port})
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var addresses []*Address
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCandidates() (candidates []string) {
|
func GetCandidates() (candidates []string) {
|
||||||
for _, address := range addresses {
|
for _, address := range addresses {
|
||||||
// using stun server for receive public IP-address
|
if candidate := address.Marshal(); candidate != "" {
|
||||||
if address.Host == "stun" {
|
candidates = append(candidates, candidate)
|
||||||
ip, err := webrtc.GetCachedPublicIP()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// this is a copy, original host unchanged
|
|
||||||
address.Host = ip.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates = append(
|
|
||||||
candidates,
|
|
||||||
webrtc.CandidateManualHostUDP(address.Host, address.Port),
|
|
||||||
webrtc.CandidateManualHostTCPPassive(address.Host, address.Port),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
@@ -47,7 +49,9 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
if format == "wyze" {
|
if format == "milestone" {
|
||||||
|
return milestoneClient(rawURL, query)
|
||||||
|
} else if format == "wyze" {
|
||||||
// https://github.com/mrlt8/docker-wyze-bridge
|
// https://github.com/mrlt8/docker-wyze-bridge
|
||||||
return wyzeClient(rawURL)
|
return wyzeClient(rawURL)
|
||||||
} else {
|
} else {
|
||||||
@@ -62,7 +66,7 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
|||||||
// ex: ws://localhost:1984/api/ws?src=camera1
|
// ex: ws://localhost:1984/api/ws?src=camera1
|
||||||
func go2rtcClient(url string) (core.Producer, error) {
|
func go2rtcClient(url string) (core.Producer, error) {
|
||||||
// 1. Connect to signalign server
|
// 1. Connect to signalign server
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
|
conn, _, err := Dial(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -79,6 +83,7 @@ func go2rtcClient(url string) (core.Producer, error) {
|
|||||||
|
|
||||||
// waiter will wait PC error or WS error or nil (connection OK)
|
// waiter will wait PC error or WS error or nil (connection OK)
|
||||||
var connState core.Waiter
|
var connState core.Waiter
|
||||||
|
var connMu sync.Mutex
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
prod := webrtc.NewConn(pc)
|
||||||
prod.Desc = "WebRTC/WebSocket async"
|
prod.Desc = "WebRTC/WebSocket async"
|
||||||
@@ -87,8 +92,10 @@ func go2rtcClient(url string) (core.Producer, error) {
|
|||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case *pion.ICECandidate:
|
case *pion.ICECandidate:
|
||||||
s := msg.ToJSON().Candidate
|
s := msg.ToJSON().Candidate
|
||||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
log.Trace().Str("candidate", s).Msg("[webrtc] local ")
|
||||||
|
connMu.Lock()
|
||||||
_ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s})
|
_ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s})
|
||||||
|
connMu.Unlock()
|
||||||
|
|
||||||
case pion.PeerConnectionState:
|
case pion.PeerConnectionState:
|
||||||
switch msg {
|
switch msg {
|
||||||
@@ -115,9 +122,9 @@ func go2rtcClient(url string) (core.Producer, error) {
|
|||||||
|
|
||||||
// 4. Send offer
|
// 4. Send offer
|
||||||
msg := &ws.Message{Type: "webrtc/offer", Value: offer}
|
msg := &ws.Message{Type: "webrtc/offer", Value: offer}
|
||||||
if err = conn.WriteJSON(msg); err != nil {
|
connMu.Lock()
|
||||||
return nil, err
|
_ = conn.WriteJSON(msg)
|
||||||
}
|
connMu.Unlock()
|
||||||
|
|
||||||
// 5. Get answer
|
// 5. Get answer
|
||||||
if err = conn.ReadJSON(msg); err != nil {
|
if err = conn.ReadJSON(msg); err != nil {
|
||||||
@@ -188,10 +195,10 @@ func whepClient(url string) (core.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", url, strings.NewReader(offer))
|
req, err := http.NewRequest("POST", url, strings.NewReader(offer))
|
||||||
req.Header.Set("Content-Type", MimeSDP)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
req.Header.Set("Content-Type", MimeSDP)
|
||||||
|
|
||||||
client := http.Client{Timeout: time.Second * 5000}
|
client := http.Client{Timeout: time.Second * 5000}
|
||||||
defer client.CloseIdleConnections()
|
defer client.CloseIdleConnections()
|
||||||
@@ -212,3 +219,27 @@ func whepClient(url string) (core.Producer, error) {
|
|||||||
|
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dial - websocket.Dial with Basic auth support
|
||||||
|
func Dial(rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.User == nil {
|
||||||
|
return websocket.DefaultDialer.Dial(rawURL, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := u.User.Username()
|
||||||
|
pass, _ := u.User.Password()
|
||||||
|
u.User = nil
|
||||||
|
|
||||||
|
header := http.Header{
|
||||||
|
"Authorization": []string{
|
||||||
|
"Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return websocket.DefaultDialer.Dial(u.String(), header)
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer,
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// 3. Create Peer Connection
|
// 3. Create Peer Connection
|
||||||
api, err := webrtc.NewAPI("")
|
api, err := webrtc.NewAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// 3. Create Peer Connection
|
// 3. Create Peer Connection
|
||||||
api, err := webrtc.NewAPI("")
|
api, err := webrtc.NewAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "", http.StatusBadRequest)
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "OPTIONS":
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
@@ -195,9 +198,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
|
|||||||
case pion.PeerConnectionState:
|
case pion.PeerConnectionState:
|
||||||
if msg == pion.PeerConnectionStateClosed {
|
if msg == pion.PeerConnectionStateClosed {
|
||||||
stream.RemoveProducer(prod)
|
stream.RemoveProducer(prod)
|
||||||
if _, ok := sessions[id]; ok {
|
delete(sessions, id)
|
||||||
delete(sessions, id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+16
-12
@@ -2,7 +2,7 @@ package webrtc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
@@ -23,7 +23,7 @@ func Init() {
|
|||||||
} `yaml:"webrtc"`
|
} `yaml:"webrtc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Mod.Listen = "0.0.0.0:8555/tcp"
|
cfg.Mod.Listen = ":8555/tcp"
|
||||||
cfg.Mod.IceServers = []pion.ICEServer{
|
cfg.Mod.IceServers = []pion.ICEServer{
|
||||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||||
}
|
}
|
||||||
@@ -32,10 +32,20 @@ func Init() {
|
|||||||
|
|
||||||
log = app.GetLogger("webrtc")
|
log = app.GetLogger("webrtc")
|
||||||
|
|
||||||
address := cfg.Mod.Listen
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// create pionAPI with custom codecs list and custom network settings
|
// create pionAPI with custom codecs list and custom network settings
|
||||||
serverAPI, err := webrtc.NewAPI(address)
|
serverAPI, err := webrtc.NewServerAPI(address, network, candidateHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
@@ -46,9 +56,8 @@ func Init() {
|
|||||||
|
|
||||||
if address != "" {
|
if address != "" {
|
||||||
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
||||||
_, Port, _ = net.SplitHostPort(address)
|
|
||||||
|
|
||||||
clientAPI, _ = webrtc.NewAPI("")
|
clientAPI, _ = webrtc.NewAPI()
|
||||||
}
|
}
|
||||||
|
|
||||||
pionConf := pion.Configuration{
|
pionConf := pion.Configuration{
|
||||||
@@ -65,10 +74,6 @@ func Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, candidate := range cfg.Mod.Candidates {
|
|
||||||
AddCandidate(candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// async WebRTC server (two API versions)
|
// async WebRTC server (two API versions)
|
||||||
ws.HandleFunc("webrtc", asyncHandler)
|
ws.HandleFunc("webrtc", asyncHandler)
|
||||||
ws.HandleFunc("webrtc/offer", asyncHandler)
|
ws.HandleFunc("webrtc/offer", asyncHandler)
|
||||||
@@ -81,7 +86,6 @@ func Init() {
|
|||||||
streams.HandleFunc("webrtc", streamsHandler)
|
streams.HandleFunc("webrtc", streamsHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
var Port string
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
var PeerConnection func(active bool) (*pion.PeerConnection, error)
|
var PeerConnection func(active bool) (*pion.PeerConnection, error)
|
||||||
@@ -138,7 +142,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
_ = sendAnswer.Wait()
|
_ = sendAnswer.Wait()
|
||||||
|
|
||||||
s := msg.ToJSON().Candidate
|
s := msg.ToJSON().Candidate
|
||||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
log.Trace().Str("candidate", s).Msg("[webrtc] local ")
|
||||||
tr.Write(&ws.Message{Type: "webrtc/candidate", Value: s})
|
tr.Write(&ws.Message{Type: "webrtc/candidate", Value: s})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
||||||
"github.com/AlexxIT/go2rtc/internal/echo"
|
"github.com/AlexxIT/go2rtc/internal/echo"
|
||||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/expr"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/gopro"
|
||||||
"github.com/AlexxIT/go2rtc/internal/hass"
|
"github.com/AlexxIT/go2rtc/internal/hass"
|
||||||
"github.com/AlexxIT/go2rtc/internal/hls"
|
"github.com/AlexxIT/go2rtc/internal/hls"
|
||||||
"github.com/AlexxIT/go2rtc/internal/homekit"
|
"github.com/AlexxIT/go2rtc/internal/homekit"
|
||||||
@@ -76,10 +78,12 @@ func main() {
|
|||||||
homekit.Init() // homekit source
|
homekit.Init() // homekit source
|
||||||
nest.Init() // nest source
|
nest.Init() // nest source
|
||||||
bubble.Init() // bubble source
|
bubble.Init() // bubble source
|
||||||
|
expr.Init() // expr source
|
||||||
|
gopro.Init() // gopro source
|
||||||
|
|
||||||
// 6. Helper modules
|
// 6. Helper modules
|
||||||
|
|
||||||
ngrok.Init() // Ngrok module
|
ngrok.Init() // ngrok module
|
||||||
srtp.Init() // SRTP server
|
srtp.Init() // SRTP server
|
||||||
debug.Init() // debug API
|
debug.Init() // debug API
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
func IsADTS(b []byte) bool {
|
func IsADTS(b []byte) bool {
|
||||||
_ = b[1]
|
_ = b[1]
|
||||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
|
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
||||||
}
|
}
|
||||||
|
|
||||||
func ADTSToCodec(b []byte) *core.Codec {
|
func ADTSToCodec(b []byte) *core.Codec {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package aac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Producer struct {
|
||||||
|
core.SuperProducer
|
||||||
|
rd *bufio.Reader
|
||||||
|
cl io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(r io.Reader) (*Producer, error) {
|
||||||
|
rd := bufio.NewReader(r)
|
||||||
|
|
||||||
|
b, err := rd.Peek(8)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
codec := ADTSToCodec(b)
|
||||||
|
|
||||||
|
prod := &Producer{rd: rd, cl: r.(io.Closer)}
|
||||||
|
prod.Type = "ADTS producer"
|
||||||
|
prod.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{codec},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return prod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Producer) Start() error {
|
||||||
|
for {
|
||||||
|
b, err := c.rd.Peek(6)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
auSize := ReadADTSSize(b)
|
||||||
|
payload := make([]byte, 2+2+auSize)
|
||||||
|
if _, err = io.ReadFull(c.rd, payload[4:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Recv += int(auSize)
|
||||||
|
|
||||||
|
if len(c.Receivers) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
payload[1] = 16 // header size in bits
|
||||||
|
binary.BigEndian.PutUint16(payload[2:], auSize<<3)
|
||||||
|
|
||||||
|
pkt := &rtp.Packet{
|
||||||
|
Header: rtp.Header{Timestamp: core.Now90000()},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
c.Receivers[0].WriteRTP(pkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Producer) Stop() error {
|
||||||
|
_ = c.SuperProducer.Close()
|
||||||
|
return c.cl.Close()
|
||||||
|
}
|
||||||
+9
-1
@@ -21,12 +21,20 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
|||||||
|
|
||||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||||
|
|
||||||
|
if len(packet.Payload) < int(2+headersSize) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
headers := packet.Payload[2 : 2+headersSize]
|
headers := packet.Payload[2 : 2+headersSize]
|
||||||
units := packet.Payload[2+headersSize:]
|
units := packet.Payload[2+headersSize:]
|
||||||
|
|
||||||
for len(headers) > 0 {
|
for len(headers) >= 2 {
|
||||||
unitSize := binary.BigEndian.Uint16(headers) >> 3
|
unitSize := binary.BigEndian.Uint16(headers) >> 3
|
||||||
|
|
||||||
|
if len(units) < int(unitSize) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
unit := units[:unitSize]
|
unit := units[:unitSize]
|
||||||
|
|
||||||
headers = headers[2:]
|
headers = headers[2:]
|
||||||
|
|||||||
@@ -127,3 +127,7 @@ func (r *Reader) ReadSEGolomb() int32 {
|
|||||||
return int32(b >> 1)
|
return int32(b >> 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Left() []byte {
|
||||||
|
return r.buf[r.pos:]
|
||||||
|
}
|
||||||
|
|||||||
+1
-10
@@ -11,7 +11,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
BufferSize = 64 * 1024 // 64K
|
BufferSize = 64 * 1024 // 64K
|
||||||
ConnDialTimeout = time.Second * 3
|
ConnDialTimeout = time.Second * 3
|
||||||
ConnDeadline = time.Second * 3
|
ConnDeadline = time.Second * 5
|
||||||
ProbeTimeout = time.Second * 3
|
ProbeTimeout = time.Second * 3
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,15 +38,6 @@ func RandString(size, base byte) string {
|
|||||||
return string(b)
|
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 {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ProbeSize = 1024 * 1024 // 1MB
|
// ProbeSize
|
||||||
|
// in my tests MPEG-TS 40Mbit/s 4K-video require more than 1MB for probe
|
||||||
|
const ProbeSize = 5 * 1024 * 1024 // 5MB
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BufferDisable = 0
|
BufferDisable = 0
|
||||||
|
|||||||
+3
-5
@@ -117,9 +117,9 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
|||||||
|
|
||||||
if GetKind(track.Codec.Name) == KindVideo {
|
if GetKind(track.Codec.Name) == KindVideo {
|
||||||
if track.Codec.IsRTP() {
|
if track.Codec.IsRTP() {
|
||||||
// H.264 2560x1440 4096kbs can have 700+ packets between 25 frames
|
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
|
||||||
// H.265 5120x1440 can have 700+ packets between two keyframes
|
// for the h264.RTPDepay => RTPPay queue
|
||||||
bufferSize = 1000
|
bufferSize = 5000
|
||||||
} else {
|
} else {
|
||||||
bufferSize = 50
|
bufferSize = 50
|
||||||
}
|
}
|
||||||
@@ -140,9 +140,7 @@ func (s *Sender) HandleRTP(track *Receiver) {
|
|||||||
go func() {
|
go func() {
|
||||||
// read packets from buffer channel until it will be closed
|
// read packets from buffer channel until it will be closed
|
||||||
for packet := range buffer {
|
for packet := range buffer {
|
||||||
s.mu.Lock()
|
|
||||||
s.bytes += len(packet.Payload)
|
s.bytes += len(packet.Payload)
|
||||||
s.mu.Unlock()
|
|
||||||
s.Handler(packet)
|
s.Handler(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package core
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,6 +33,8 @@ func (w *WriteBuffer) Write(p []byte) (n int, err error) {
|
|||||||
} else if n, err = w.Writer.Write(p); err != nil {
|
} else if n, err = w.Writer.Write(p); err != nil {
|
||||||
w.err = err
|
w.err = err
|
||||||
w.done()
|
w.done()
|
||||||
|
} else if f, ok := w.Writer.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
}
|
}
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
return
|
return
|
||||||
|
|||||||
+131
-322
@@ -2,8 +2,8 @@ package dvrip
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -12,49 +12,29 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
const (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
Login = 1000
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
OPMonitorClaim = 1413
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
OPMonitorStart = 1410
|
||||||
"github.com/pion/rtp"
|
OPTalkClaim = 1434
|
||||||
|
OPTalkStart = 1430
|
||||||
|
OPTalkData = 1432
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
core.Listener
|
|
||||||
|
|
||||||
uri string
|
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
reader *bufio.Reader
|
|
||||||
session uint32
|
session uint32
|
||||||
seq uint32
|
seq uint32
|
||||||
stream string
|
stream string
|
||||||
|
|
||||||
medias []*core.Media
|
rd io.Reader
|
||||||
receivers []*core.Receiver
|
buf []byte
|
||||||
videoTrack *core.Receiver
|
|
||||||
audioTrack *core.Receiver
|
|
||||||
|
|
||||||
videoTS uint32
|
|
||||||
videoDT uint32
|
|
||||||
audioTS uint32
|
|
||||||
audioSeq uint16
|
|
||||||
|
|
||||||
recv uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response map[string]any
|
func (c *Client) Dial(rawURL string) (err error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
const Login = uint16(1000)
|
|
||||||
const OPMonitorClaim = uint16(1413)
|
|
||||||
const OPMonitorStart = uint16(1410)
|
|
||||||
|
|
||||||
func NewClient(url string) *Client {
|
|
||||||
return &Client{uri: url}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Dial() (err error) {
|
|
||||||
u, err := url.Parse(c.uri)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -69,26 +49,27 @@ func (c *Client) Dial() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.reader = bufio.NewReader(c.conn)
|
if query := u.Query(); query.Get("backchannel") != "1" {
|
||||||
|
channel := query.Get("channel")
|
||||||
|
if channel == "" {
|
||||||
|
channel = "0"
|
||||||
|
}
|
||||||
|
|
||||||
query := u.Query()
|
subtype := query.Get("subtype")
|
||||||
channel := query.Get("channel")
|
switch subtype {
|
||||||
if channel == "" {
|
case "", "0":
|
||||||
channel = "0"
|
subtype = "Main"
|
||||||
|
case "1":
|
||||||
|
subtype = "Extra1"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.stream = fmt.Sprintf(
|
||||||
|
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
|
||||||
|
channel, subtype,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
subtype := query.Get("subtype")
|
c.rd = bufio.NewReader(c.conn)
|
||||||
switch subtype {
|
|
||||||
case "", "0":
|
|
||||||
subtype = "Main"
|
|
||||||
case "1":
|
|
||||||
subtype = "Extra1"
|
|
||||||
}
|
|
||||||
|
|
||||||
c.stream = fmt.Sprintf(
|
|
||||||
`{"Channel":%s,"CombinMode":"NONE","StreamType":"%s","TransMode":"TCP"}`,
|
|
||||||
channel, subtype,
|
|
||||||
)
|
|
||||||
|
|
||||||
if u.User != nil {
|
if u.User != nil {
|
||||||
pass, _ := u.User.Password()
|
pass, _ := u.User.Password()
|
||||||
@@ -98,210 +79,84 @@ func (c *Client) Dial() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Login(user, pass string) (err error) {
|
|
||||||
data := fmt.Sprintf(
|
|
||||||
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`,
|
|
||||||
SofiaHash(pass), user,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.Request(Login, data); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.ResponseJSON()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Play() (err error) {
|
|
||||||
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}`
|
|
||||||
|
|
||||||
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
|
|
||||||
if err = c.Request(OPMonitorClaim, data); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err = c.ResponseJSON(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
|
||||||
return c.Request(OPMonitorStart, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Handle() error {
|
|
||||||
var buf []byte
|
|
||||||
var size int
|
|
||||||
|
|
||||||
var probe byte
|
|
||||||
if c.medias == nil {
|
|
||||||
probe = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
b, err := c.Response()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect data from multiple packets
|
|
||||||
if size > 0 {
|
|
||||||
buf = append(buf, b...)
|
|
||||||
if len(buf) < size {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(buf) > size {
|
|
||||||
return errors.New("wrong size")
|
|
||||||
}
|
|
||||||
b = buf
|
|
||||||
}
|
|
||||||
|
|
||||||
dataType := binary.BigEndian.Uint32(b)
|
|
||||||
switch dataType {
|
|
||||||
case 0x1FC, 0x1FE:
|
|
||||||
size = int(binary.LittleEndian.Uint32(b[12:])) + 16
|
|
||||||
case 0x1FD: // PFrame
|
|
||||||
size = int(binary.LittleEndian.Uint32(b[4:])) + 8
|
|
||||||
case 0x1FA, 0x1F9:
|
|
||||||
size = int(binary.LittleEndian.Uint16(b[6:])) + 8
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown type: %X", dataType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(b) < size {
|
|
||||||
buf = b
|
|
||||||
continue // need to collect data from next packets
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
|
||||||
|
|
||||||
switch dataType {
|
|
||||||
case 0x1FC, 0x1FE: // video IFrame
|
|
||||||
payload := annexb.EncodeToAVCC(b[16:], false)
|
|
||||||
|
|
||||||
if c.videoTrack == nil {
|
|
||||||
fps := b[5]
|
|
||||||
//width := uint16(b[6]) * 8
|
|
||||||
//height := uint16(b[7]) * 8
|
|
||||||
//println(width, height)
|
|
||||||
ts := b[8:]
|
|
||||||
|
|
||||||
// the exact value of the start TS does not matter
|
|
||||||
c.videoTS = binary.LittleEndian.Uint32(ts)
|
|
||||||
c.videoDT = 90000 / uint32(fps)
|
|
||||||
|
|
||||||
c.AddVideoTrack(b[4], payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.videoTrack != nil {
|
|
||||||
c.videoTS += c.videoDT
|
|
||||||
|
|
||||||
packet := &rtp.Packet{
|
|
||||||
Header: rtp.Header{Timestamp: c.videoTS},
|
|
||||||
Payload: payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
|
||||||
|
|
||||||
c.videoTrack.WriteRTP(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0x1FD: // PFrame
|
|
||||||
if c.videoTrack != nil {
|
|
||||||
c.videoTS += c.videoDT
|
|
||||||
|
|
||||||
packet := &rtp.Packet{
|
|
||||||
Header: rtp.Header{Timestamp: c.videoTS},
|
|
||||||
Payload: annexb.EncodeToAVCC(b[8:], false),
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
|
|
||||||
|
|
||||||
c.videoTrack.WriteRTP(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0x1FA, 0x1F9: // audio
|
|
||||||
if c.audioTrack == nil {
|
|
||||||
// the exact value of the start TS does not matter
|
|
||||||
c.audioTS = c.videoTS
|
|
||||||
|
|
||||||
c.AddAudioTrack(b[4], b[5])
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.audioTrack != nil {
|
|
||||||
for b != nil {
|
|
||||||
payload := b[8:size]
|
|
||||||
if len(b) > size {
|
|
||||||
b = b[size:]
|
|
||||||
} else {
|
|
||||||
b = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.audioTS += uint32(len(payload))
|
|
||||||
c.audioSeq++
|
|
||||||
|
|
||||||
packet := &rtp.Packet{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: true,
|
|
||||||
SequenceNumber: c.audioSeq,
|
|
||||||
Timestamp: c.audioTS,
|
|
||||||
},
|
|
||||||
Payload: payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
|
|
||||||
|
|
||||||
c.audioTrack.WriteRTP(packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if probe != 0 {
|
|
||||||
probe++
|
|
||||||
if (c.videoTS > 0 && c.audioTS > 0) || probe == 20 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
size = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
return c.conn.Close()
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Request(cmd uint16, data string) (err error) {
|
func (c *Client) Login(user, pass string) (err error) {
|
||||||
|
data := fmt.Sprintf(
|
||||||
|
`{"EncryptType":"MD5","LoginType":"DVRIP-Web","PassWord":"%s","UserName":"%s"}`+"\x0A\x00",
|
||||||
|
SofiaHash(pass), user,
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err = c.WriteCmd(Login, []byte(data)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.ReadJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Play() error {
|
||||||
|
format := `{"Name":"OPMonitor","SessionID":"0x%08X","OPMonitor":{"Action":"%s","Parameter":%s}}` + "\x0A\x00"
|
||||||
|
|
||||||
|
data := fmt.Sprintf(format, c.session, "Claim", c.stream)
|
||||||
|
if _, err := c.WriteCmd(OPMonitorClaim, []byte(data)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := c.ReadJSON(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data = fmt.Sprintf(format, c.session, "Start", c.stream)
|
||||||
|
_, err := c.WriteCmd(OPMonitorStart, []byte(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Talk() error {
|
||||||
|
format := `{"Name":"OPTalk","SessionID":"0x%08X","OPTalk":{"Action":"%s"}}` + "\x0A\x00"
|
||||||
|
|
||||||
|
data := fmt.Sprintf(format, c.session, "Claim")
|
||||||
|
if _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := c.ReadJSON(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data = fmt.Sprintf(format, c.session, "Start")
|
||||||
|
_, err := c.WriteCmd(OPTalkStart, []byte(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WriteCmd(cmd uint16, payload []byte) (n int, err error) {
|
||||||
b := make([]byte, 20, 128)
|
b := make([]byte, 20, 128)
|
||||||
b[0] = 255
|
b[0] = 255
|
||||||
binary.LittleEndian.PutUint32(b[4:], c.session)
|
binary.LittleEndian.PutUint32(b[4:], c.session)
|
||||||
binary.LittleEndian.PutUint32(b[8:], c.seq)
|
binary.LittleEndian.PutUint32(b[8:], c.seq)
|
||||||
binary.LittleEndian.PutUint16(b[14:], cmd)
|
binary.LittleEndian.PutUint16(b[14:], cmd)
|
||||||
binary.LittleEndian.PutUint32(b[16:], uint32(len(data))+2)
|
binary.LittleEndian.PutUint32(b[16:], uint32(len(payload)))
|
||||||
b = append(b, data...)
|
b = append(b, payload...)
|
||||||
b = append(b, 0x0A, 0x00)
|
|
||||||
|
|
||||||
c.seq++
|
c.seq++
|
||||||
|
|
||||||
if err = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
if err = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
||||||
return
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.conn.Write(b)
|
return c.conn.Write(b)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Response() (b []byte, err error) {
|
func (c *Client) ReadChunk() (b []byte, err error) {
|
||||||
if err = c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
if err = c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b = make([]byte, 20)
|
b = make([]byte, 20)
|
||||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.recv += 20
|
|
||||||
|
|
||||||
if b[0] != 255 {
|
if b[0] != 255 {
|
||||||
return nil, errors.New("read error")
|
return nil, errors.New("read error")
|
||||||
}
|
}
|
||||||
@@ -310,17 +165,59 @@ func (c *Client) Response() (b []byte, err error) {
|
|||||||
size := binary.LittleEndian.Uint32(b[16:])
|
size := binary.LittleEndian.Uint32(b[16:])
|
||||||
|
|
||||||
b = make([]byte, size)
|
b = make([]byte, size)
|
||||||
if _, err = io.ReadFull(c.reader, b); err != nil {
|
if _, err = io.ReadFull(c.rd, b); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.recv += size
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ResponseJSON() (res Response, err error) {
|
func (c *Client) ReadPacket() (pType byte, payload []byte, err error) {
|
||||||
b, err := c.Response()
|
var b []byte
|
||||||
|
|
||||||
|
// many cameras may split packet to multiple chunks
|
||||||
|
// some rare cameras may put multiple packets to single chunk
|
||||||
|
for len(c.buf) < 16 {
|
||||||
|
if b, err = c.ReadChunk(); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
c.buf = append(c.buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.HasPrefix(c.buf, []byte{0, 0, 1}) {
|
||||||
|
return 0, nil, fmt.Errorf("dvrip: wrong packet: %0.16x", c.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int
|
||||||
|
|
||||||
|
switch pType = c.buf[3]; pType {
|
||||||
|
case 0xFC, 0xFE:
|
||||||
|
size = int(binary.LittleEndian.Uint32(c.buf[12:])) + 16
|
||||||
|
case 0xFD: // PFrame
|
||||||
|
size = int(binary.LittleEndian.Uint32(c.buf[4:])) + 8
|
||||||
|
case 0xFA, 0xF9:
|
||||||
|
size = int(binary.LittleEndian.Uint16(c.buf[6:])) + 8
|
||||||
|
default:
|
||||||
|
return 0, nil, fmt.Errorf("dvrip: unknown packet type: %X", pType)
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(c.buf) < size {
|
||||||
|
if b, err = c.ReadChunk(); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
c.buf = append(c.buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = c.buf[:size]
|
||||||
|
c.buf = c.buf[size:]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response map[string]any
|
||||||
|
|
||||||
|
func (c *Client) ReadJSON() (res Response, err error) {
|
||||||
|
b, err := c.ReadChunk()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -336,94 +233,6 @@ func (c *Client) ResponseJSON() (res Response, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
|
|
||||||
var codec *core.Codec
|
|
||||||
switch mediaCode {
|
|
||||||
case 0x02, 0x12:
|
|
||||||
codec = &core.Codec{
|
|
||||||
Name: core.CodecH264,
|
|
||||||
ClockRate: 90000,
|
|
||||||
PayloadType: core.PayloadTypeRAW,
|
|
||||||
FmtpLine: h264.GetFmtpLine(payload),
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0x03, 0x13, 0x43, 0x53:
|
|
||||||
codec = &core.Codec{
|
|
||||||
Name: core.CodecH265,
|
|
||||||
ClockRate: 90000,
|
|
||||||
PayloadType: core.PayloadTypeRAW,
|
|
||||||
FmtpLine: "profile-id=1",
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
size := 4 + int(binary.BigEndian.Uint32(payload))
|
|
||||||
|
|
||||||
switch h265.NALUType(payload) {
|
|
||||||
case h265.NALUTypeVPS:
|
|
||||||
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
|
||||||
case h265.NALUTypeSPS:
|
|
||||||
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
|
||||||
case h265.NALUTypePPS:
|
|
||||||
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
|
||||||
}
|
|
||||||
|
|
||||||
if size < len(payload) {
|
|
||||||
payload = payload[size:]
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
println("[DVRIP] unsupported video codec:", mediaCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &core.Media{
|
|
||||||
Kind: core.KindVideo,
|
|
||||||
Direction: core.DirectionRecvonly,
|
|
||||||
Codecs: []*core.Codec{codec},
|
|
||||||
}
|
|
||||||
c.medias = append(c.medias, media)
|
|
||||||
|
|
||||||
c.videoTrack = core.NewReceiver(media, codec)
|
|
||||||
c.receivers = append(c.receivers, c.videoTrack)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
|
||||||
|
|
||||||
func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
|
|
||||||
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
|
|
||||||
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
|
|
||||||
var codec *core.Codec
|
|
||||||
switch mediaCode {
|
|
||||||
case 10: // G711U
|
|
||||||
codec = &core.Codec{
|
|
||||||
Name: core.CodecPCMU,
|
|
||||||
}
|
|
||||||
case 14: // G711A
|
|
||||||
codec = &core.Codec{
|
|
||||||
Name: core.CodecPCMA,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
println("[DVRIP] unsupported audio codec:", mediaCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleRate <= byte(len(sampleRates)) {
|
|
||||||
codec.ClockRate = sampleRates[sampleRate-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &core.Media{
|
|
||||||
Kind: core.KindAudio,
|
|
||||||
Direction: core.DirectionRecvonly,
|
|
||||||
Codecs: []*core.Codec{codec},
|
|
||||||
}
|
|
||||||
c.medias = append(c.medias, media)
|
|
||||||
|
|
||||||
c.audioTrack = core.NewReceiver(media, codec)
|
|
||||||
c.receivers = append(c.receivers, c.audioTrack)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SofiaHash(password string) string {
|
func SofiaHash(password string) string {
|
||||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package dvrip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Consumer struct {
|
||||||
|
core.SuperConsumer
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||||
|
return nil, core.ErrCantGetTrack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Start() error {
|
||||||
|
if err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
if _, err := c.client.rd.Read(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Stop() error {
|
||||||
|
_ = c.SuperConsumer.Close()
|
||||||
|
return c.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||||
|
if err := c.client.Talk(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const PacketSize = 320
|
||||||
|
|
||||||
|
buf := make([]byte, 8+PacketSize)
|
||||||
|
binary.BigEndian.PutUint32(buf, 0x1FA)
|
||||||
|
|
||||||
|
switch track.Codec.Name {
|
||||||
|
case core.CodecPCMU:
|
||||||
|
buf[4] = 10
|
||||||
|
case core.CodecPCMA:
|
||||||
|
buf[4] = 14
|
||||||
|
}
|
||||||
|
|
||||||
|
//for i, rate := range sampleRates {
|
||||||
|
// if rate == track.Codec.ClockRate {
|
||||||
|
// buf[5] = byte(i) + 1
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
buf[5] = 2 // ClockRate=8000
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint16(buf[6:], PacketSize)
|
||||||
|
|
||||||
|
var payload []byte
|
||||||
|
|
||||||
|
sender := core.NewSender(media, track.Codec)
|
||||||
|
sender.Handler = func(packet *rtp.Packet) {
|
||||||
|
payload = append(payload, packet.Payload...)
|
||||||
|
|
||||||
|
for len(payload) >= PacketSize {
|
||||||
|
buf = append(buf[:8], payload[:PacketSize]...)
|
||||||
|
if n, err := c.client.WriteCmd(OPTalkData, buf); err != nil {
|
||||||
|
c.Send += n
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = payload[PacketSize:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.HandleRTP(track)
|
||||||
|
c.Senders = append(c.Senders, sender)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package dvrip
|
||||||
|
|
||||||
|
import "github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
|
||||||
|
func Dial(url string) (core.Producer, error) {
|
||||||
|
client := &Client{}
|
||||||
|
if err := client.Dial(url); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.stream != "" {
|
||||||
|
prod := &Producer{client: client}
|
||||||
|
prod.Type = "DVRIP active producer"
|
||||||
|
if err := prod.probe(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return prod, nil
|
||||||
|
} else {
|
||||||
|
cons := &Consumer{client: client}
|
||||||
|
cons.Type = "DVRIP active consumer"
|
||||||
|
cons.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 8000, PayloadType: 0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cons, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
+247
-22
@@ -1,41 +1,266 @@
|
|||||||
package dvrip
|
package dvrip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) GetMedias() []*core.Media {
|
type Producer struct {
|
||||||
return c.medias
|
core.SuperProducer
|
||||||
|
|
||||||
|
client *Client
|
||||||
|
|
||||||
|
video, audio *core.Receiver
|
||||||
|
|
||||||
|
videoTS uint32
|
||||||
|
videoDT uint32
|
||||||
|
audioTS uint32
|
||||||
|
audioSeq uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
func (c *Producer) Start() error {
|
||||||
for _, track := range c.receivers {
|
for {
|
||||||
if track.Codec == codec {
|
pType, b, err := c.client.ReadPacket()
|
||||||
return track, nil
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("[DVR] type: %d, len: %d", dataType, len(b))
|
||||||
|
|
||||||
|
switch pType {
|
||||||
|
case 0xFC, 0xFE, 0xFD:
|
||||||
|
if c.video == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload []byte
|
||||||
|
if pType != 0xFD {
|
||||||
|
payload = b[16:] // iframe
|
||||||
|
} else {
|
||||||
|
payload = b[8:] // pframe
|
||||||
|
}
|
||||||
|
|
||||||
|
c.videoTS += c.videoDT
|
||||||
|
|
||||||
|
packet := &rtp.Packet{
|
||||||
|
Header: rtp.Header{Timestamp: c.videoTS},
|
||||||
|
Payload: annexb.EncodeToAVCC(payload, false),
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
|
||||||
|
|
||||||
|
c.video.WriteRTP(packet)
|
||||||
|
|
||||||
|
case 0xFA: // audio
|
||||||
|
if c.audio == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := b[8:]
|
||||||
|
|
||||||
|
c.audioTS += uint32(len(payload))
|
||||||
|
c.audioSeq++
|
||||||
|
|
||||||
|
packet := &rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
|
SequenceNumber: c.audioSeq,
|
||||||
|
Timestamp: c.audioTS,
|
||||||
|
},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
|
||||||
|
|
||||||
|
c.audio.WriteRTP(packet)
|
||||||
|
|
||||||
|
case 0xF9: // unknown
|
||||||
|
|
||||||
|
default:
|
||||||
|
println(fmt.Sprintf("dvrip: unknown packet type: %d", pType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, core.ErrCantGetTrack
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Start() error {
|
func (c *Producer) Stop() error {
|
||||||
return c.Handle()
|
return c.client.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Stop() error {
|
func (c *Producer) probe() error {
|
||||||
for _, receiver := range c.receivers {
|
if err := c.client.Play(); err != nil {
|
||||||
receiver.Close()
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := core.NewReadBuffer(c.client.rd)
|
||||||
|
rd.BufferSize = core.ProbeSize
|
||||||
|
defer func() {
|
||||||
|
c.client.buf = nil
|
||||||
|
rd.Reset()
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.client.rd = rd
|
||||||
|
|
||||||
|
// some awful cameras has VERY rare keyframes
|
||||||
|
// so we wait video+audio for default probe time
|
||||||
|
// and wait anything for 15 seconds
|
||||||
|
timeoutBoth := time.Now().Add(core.ProbeTimeout)
|
||||||
|
timeoutAny := time.Now().Add(time.Second * 15)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if now := time.Now(); now.Before(timeoutBoth) {
|
||||||
|
if c.video != nil && c.audio != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if now.Before(timeoutAny) {
|
||||||
|
if c.video != nil || c.audio != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("dvrip: can't probe medias")
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, b, err := c.client.ReadPacket()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case 0xFC, 0xFE: // video
|
||||||
|
if c.video != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fps := b[5]
|
||||||
|
//width := uint16(b[6]) * 8
|
||||||
|
//height := uint16(b[7]) * 8
|
||||||
|
//println(width, height)
|
||||||
|
ts := b[8:]
|
||||||
|
|
||||||
|
// the exact value of the start TS does not matter
|
||||||
|
c.videoTS = binary.LittleEndian.Uint32(ts)
|
||||||
|
c.videoDT = 90000 / uint32(fps)
|
||||||
|
|
||||||
|
payload := annexb.EncodeToAVCC(b[16:], false)
|
||||||
|
c.addVideoTrack(b[4], payload)
|
||||||
|
|
||||||
|
case 0xFA: // audio
|
||||||
|
if c.audio != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// the exact value of the start TS does not matter
|
||||||
|
c.audioTS = c.videoTS
|
||||||
|
|
||||||
|
c.addAudioTrack(b[4], b[5])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return c.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Producer) addVideoTrack(mediaCode byte, payload []byte) {
|
||||||
info := &core.Info{
|
var codec *core.Codec
|
||||||
Type: "DVRIP active producer",
|
switch mediaCode {
|
||||||
RemoteAddr: c.conn.RemoteAddr().String(),
|
case 0x02, 0x12:
|
||||||
Medias: c.medias,
|
codec = &core.Codec{
|
||||||
Receivers: c.receivers,
|
Name: core.CodecH264,
|
||||||
Recv: int(c.recv),
|
ClockRate: 90000,
|
||||||
|
PayloadType: core.PayloadTypeRAW,
|
||||||
|
FmtpLine: h264.GetFmtpLine(payload),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 0x03, 0x13, 0x43, 0x53:
|
||||||
|
codec = &core.Codec{
|
||||||
|
Name: core.CodecH265,
|
||||||
|
ClockRate: 90000,
|
||||||
|
PayloadType: core.PayloadTypeRAW,
|
||||||
|
FmtpLine: "profile-id=1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
size := 4 + int(binary.BigEndian.Uint32(payload))
|
||||||
|
|
||||||
|
switch h265.NALUType(payload) {
|
||||||
|
case h265.NALUTypeVPS:
|
||||||
|
codec.FmtpLine += ";sprop-vps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||||
|
case h265.NALUTypeSPS:
|
||||||
|
codec.FmtpLine += ";sprop-sps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||||
|
case h265.NALUTypePPS:
|
||||||
|
codec.FmtpLine += ";sprop-pps=" + base64.StdEncoding.EncodeToString(payload[4:size])
|
||||||
|
}
|
||||||
|
|
||||||
|
if size < len(payload) {
|
||||||
|
payload = payload[size:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
println("[DVRIP] unsupported video codec:", mediaCode)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return json.Marshal(info)
|
|
||||||
|
media := &core.Media{
|
||||||
|
Kind: core.KindVideo,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{codec},
|
||||||
|
}
|
||||||
|
c.Medias = append(c.Medias, media)
|
||||||
|
|
||||||
|
c.video = core.NewReceiver(media, codec)
|
||||||
|
c.Receivers = append(c.Receivers, c.video)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
|
||||||
|
|
||||||
|
func (c *Producer) addAudioTrack(mediaCode byte, sampleRate byte) {
|
||||||
|
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
|
||||||
|
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
|
||||||
|
var codec *core.Codec
|
||||||
|
switch mediaCode {
|
||||||
|
case 10: // G711U
|
||||||
|
codec = &core.Codec{
|
||||||
|
Name: core.CodecPCMU,
|
||||||
|
}
|
||||||
|
case 14: // G711A
|
||||||
|
codec = &core.Codec{
|
||||||
|
Name: core.CodecPCMA,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
println("[DVRIP] unsupported audio codec:", mediaCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleRate <= byte(len(sampleRates)) {
|
||||||
|
codec.ClockRate = sampleRates[sampleRate-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
media := &core.Media{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{codec},
|
||||||
|
}
|
||||||
|
c.Medias = append(c.Medias, media)
|
||||||
|
|
||||||
|
c.audio = core.NewReceiver(media, codec)
|
||||||
|
c.Receivers = append(c.Receivers, c.audio)
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
|
// info := &core.Info{
|
||||||
|
// Type: "DVRIP active producer",
|
||||||
|
// RemoteAddr: c.conn.RemoteAddr().String(),
|
||||||
|
// Medias: c.Medias,
|
||||||
|
// Receivers: c.Receivers,
|
||||||
|
// Recv: c.Recv,
|
||||||
|
// }
|
||||||
|
// return json.Marshal(info)
|
||||||
|
//}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
|
"github.com/expr-lang/expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRequest(method, url string, headers map[string]any) (*http.Request, error) {
|
||||||
|
if method == "" {
|
||||||
|
method = "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
req.Header.Set(k, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func regExp(params ...any) (*regexp.Regexp, error) {
|
||||||
|
exp := params[0].(string)
|
||||||
|
if len(params) >= 2 {
|
||||||
|
// support:
|
||||||
|
// i case-insensitive (default false)
|
||||||
|
// m multi-line mode: ^ and $ match begin/end line (default false)
|
||||||
|
// s let . match \n (default false)
|
||||||
|
// https://pkg.go.dev/regexp/syntax
|
||||||
|
flags := params[1].(string)
|
||||||
|
exp = "(?" + flags + ")" + exp
|
||||||
|
}
|
||||||
|
return regexp.Compile(exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Options = []expr.Option{
|
||||||
|
expr.Function(
|
||||||
|
"fetch",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
url := params[0].(string)
|
||||||
|
|
||||||
|
if len(params) == 2 {
|
||||||
|
options := params[1].(map[string]any)
|
||||||
|
method, _ := options["method"].(string)
|
||||||
|
headers, _ := options["headers"].(map[string]any)
|
||||||
|
req, err = newRequest(method, url, headers)
|
||||||
|
} else {
|
||||||
|
req, err = http.NewRequest("GET", url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := tcp.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := io.ReadAll(res.Body)
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"ok": res.StatusCode < 400,
|
||||||
|
"status": res.Status,
|
||||||
|
"text": string(b),
|
||||||
|
"json": func() (v any) {
|
||||||
|
_ = json.Unmarshal(b, &v)
|
||||||
|
return
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
//new(func(url string) map[string]any),
|
||||||
|
//new(func(url string, options map[string]any) map[string]any),
|
||||||
|
),
|
||||||
|
expr.Function(
|
||||||
|
"match",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
re, err := regExp(params[1:]...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
str := params[0].(string)
|
||||||
|
return re.FindStringSubmatch(str), nil
|
||||||
|
},
|
||||||
|
//new(func(str, expr string) []string),
|
||||||
|
//new(func(str, expr, flags string) []string),
|
||||||
|
),
|
||||||
|
expr.Function(
|
||||||
|
"RegExp",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
return regExp(params)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(input string) (any, error) {
|
||||||
|
program, err := expr.Compile(input, Options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr.Run(program, nil)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatchHost(t *testing.T) {
|
||||||
|
v, err := Run(`
|
||||||
|
let url = "rtsp://user:pass@192.168.1.123/cam/realmonitor?...";
|
||||||
|
let host = match(url, "//[^/]+")[0][2:];
|
||||||
|
host
|
||||||
|
`)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "user:pass@192.168.1.123", v)
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ func (a *Args) String() string {
|
|||||||
b.WriteString(codec)
|
b.WriteString(codec)
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.Filters != nil {
|
if len(a.Filters) > 0 {
|
||||||
for i, filter := range a.Filters {
|
for i, filter := range a.Filters {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
b.WriteString(` -vf "`)
|
b.WriteString(` -vf "`)
|
||||||
|
|||||||
+41
-2
@@ -60,6 +60,9 @@ func (a *AMF) ReadItem() (any, error) {
|
|||||||
case TypeObject:
|
case TypeObject:
|
||||||
return a.ReadObject()
|
return a.ReadObject()
|
||||||
|
|
||||||
|
case TypeEcmaArray:
|
||||||
|
return a.ReadEcmaArray()
|
||||||
|
|
||||||
case TypeNull:
|
case TypeNull:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
||||||
@@ -174,7 +177,18 @@ func (a *AMF) WriteString(s string) {
|
|||||||
|
|
||||||
func (a *AMF) WriteObject(obj map[string]any) {
|
func (a *AMF) WriteObject(obj map[string]any) {
|
||||||
a.buf = append(a.buf, TypeObject)
|
a.buf = append(a.buf, TypeObject)
|
||||||
|
a.writeKV(obj)
|
||||||
|
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF) WriteEcmaArray(obj map[string]any) {
|
||||||
|
n := len(obj)
|
||||||
|
a.buf = append(a.buf, TypeEcmaArray, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
|
||||||
|
a.writeKV(obj)
|
||||||
|
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF) writeKV(obj map[string]any) {
|
||||||
for k, v := range obj {
|
for k, v := range obj {
|
||||||
n := len(k)
|
n := len(k)
|
||||||
a.buf = append(a.buf, byte(n>>8), byte(n))
|
a.buf = append(a.buf, byte(n>>8), byte(n))
|
||||||
@@ -185,16 +199,41 @@ func (a *AMF) WriteObject(obj map[string]any) {
|
|||||||
a.WriteString(v)
|
a.WriteString(v)
|
||||||
case int:
|
case int:
|
||||||
a.WriteNumber(float64(v))
|
a.WriteNumber(float64(v))
|
||||||
|
case uint16:
|
||||||
|
a.WriteNumber(float64(v))
|
||||||
|
case uint32:
|
||||||
|
a.WriteNumber(float64(v))
|
||||||
|
case float64:
|
||||||
|
a.WriteNumber(v)
|
||||||
case bool:
|
case bool:
|
||||||
a.WriteBool(v)
|
a.WriteBool(v)
|
||||||
default:
|
default:
|
||||||
panic(v)
|
panic(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AMF) WriteNull() {
|
func (a *AMF) WriteNull() {
|
||||||
a.buf = append(a.buf, TypeNull)
|
a.buf = append(a.buf, TypeNull)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EncodeItems(items ...any) []byte {
|
||||||
|
a := &AMF{}
|
||||||
|
for _, item := range items {
|
||||||
|
switch v := item.(type) {
|
||||||
|
case float64:
|
||||||
|
a.WriteNumber(v)
|
||||||
|
case int:
|
||||||
|
a.WriteNumber(float64(v))
|
||||||
|
case string:
|
||||||
|
a.WriteString(v)
|
||||||
|
case map[string]any:
|
||||||
|
a.WriteObject(v)
|
||||||
|
case nil:
|
||||||
|
a.WriteNull()
|
||||||
|
default:
|
||||||
|
panic(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.Bytes()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package amf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewReader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
actual string
|
||||||
|
expect []any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ffmpeg-http",
|
||||||
|
actual: "02000a6f6e4d65746144617461080000001000086475726174696f6e000000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500409e62770000000000096672616d6572617465004038000000000000000c766964656f636f646563696400401c000000000000000d617564696f646174617261746500405ea93000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000673746572656f0101000c617564696f636f6465636964004024000000000000000b6d616a6f725f6272616e640200046d703432000d6d696e6f725f76657273696f6e020001300011636f6d70617469626c655f6272616e647302000c69736f6d617663316d7034320007656e636f64657202000c4c61766636302e352e313030000866696c6573697a65000000000000000000000009",
|
||||||
|
expect: []any{
|
||||||
|
"onMetaData",
|
||||||
|
map[string]any{
|
||||||
|
"compatible_brands": "isomavc1mp42",
|
||||||
|
"major_brand": "mp42",
|
||||||
|
"minor_version": "0",
|
||||||
|
"encoder": "Lavf60.5.100",
|
||||||
|
|
||||||
|
"filesize": float64(0),
|
||||||
|
"duration": float64(0),
|
||||||
|
|
||||||
|
"videocodecid": float64(7),
|
||||||
|
"width": float64(1280),
|
||||||
|
"height": float64(720),
|
||||||
|
"framerate": float64(24),
|
||||||
|
"videodatarate": 1944.6162109375,
|
||||||
|
|
||||||
|
"audiocodecid": float64(10),
|
||||||
|
"audiosamplerate": float64(44100),
|
||||||
|
"stereo": true,
|
||||||
|
"audiosamplesize": float64(16),
|
||||||
|
"audiodatarate": 122.6435546875,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ffmpeg-file",
|
||||||
|
actual: "02000a6f6e4d65746144617461080000000800086475726174696f6e004000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500000000000000000000096672616d6572617465004039000000000000000c766964656f636f646563696400401c0000000000000007656e636f64657202000c4c61766636302e352e313030000866696c6573697a6500411f541400000000000009",
|
||||||
|
expect: []any{
|
||||||
|
"onMetaData",
|
||||||
|
map[string]any{
|
||||||
|
"encoder": "Lavf60.5.100",
|
||||||
|
|
||||||
|
"filesize": float64(513285),
|
||||||
|
"duration": float64(2),
|
||||||
|
|
||||||
|
"videocodecid": float64(7),
|
||||||
|
"width": float64(1280),
|
||||||
|
"height": float64(720),
|
||||||
|
"framerate": float64(25),
|
||||||
|
"videodatarate": float64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reolink-1",
|
||||||
|
actual: "0200075f726573756c74003ff0000000000000030006666d7356657202000d464d532f332c302c312c313233000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009",
|
||||||
|
expect: []any{
|
||||||
|
"_result", float64(1),
|
||||||
|
map[string]any{
|
||||||
|
"capabilities": float64(31),
|
||||||
|
"fmsVer": "FMS/3,0,1,123",
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"code": "NetConnection.Connect.Success",
|
||||||
|
"description": "Connection succeeded.",
|
||||||
|
"level": "status",
|
||||||
|
"objectEncoding": float64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reolink-2",
|
||||||
|
actual: "0200075f726573756c7400400000000000000005003ff0000000000000",
|
||||||
|
expect: []any{
|
||||||
|
"_result", float64(2), nil, float64(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reolink-3",
|
||||||
|
actual: "0200086f6e537461747573000000000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e020015537461727420766964656f206f6e2064656d616e64000009",
|
||||||
|
expect: []any{
|
||||||
|
"onStatus", float64(0), nil,
|
||||||
|
map[string]any{
|
||||||
|
"code": "NetStream.Play.Start",
|
||||||
|
"description": "Start video on demand",
|
||||||
|
"level": "status",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reolink-4",
|
||||||
|
actual: "0200117c52746d7053616d706c6541636365737301010101",
|
||||||
|
expect: []any{
|
||||||
|
"|RtmpSampleAccess", true, true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reolink-5",
|
||||||
|
actual: "02000a6f6e4d6574614461746103000577696474680040a4000000000000000668656967687400409e000000000000000c646973706c617957696474680040a4000000000000000d646973706c617948656967687400409e00000000000000086475726174696f6e000000000000000000000c766964656f636f646563696400401c000000000000000c617564696f636f6465636964004024000000000000000f617564696f73616d706c65726174650040cf40000000000000096672616d657261746500403e000000000000000009",
|
||||||
|
expect: []any{
|
||||||
|
"onMetaData",
|
||||||
|
map[string]any{
|
||||||
|
"duration": float64(0),
|
||||||
|
|
||||||
|
"videocodecid": float64(7),
|
||||||
|
"width": float64(2560),
|
||||||
|
"height": float64(1920),
|
||||||
|
"displayWidth": float64(2560),
|
||||||
|
"displayHeight": float64(1920),
|
||||||
|
"framerate": float64(30),
|
||||||
|
|
||||||
|
"audiocodecid": float64(10),
|
||||||
|
"audiosamplerate": float64(16000),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mediamtx",
|
||||||
|
actual: "02000d40736574446174614672616d6502000a6f6e4d6574614461746103000d766964656f6461746172617465000000000000000000000c766964656f636f646563696400401c000000000000000d617564696f6461746172617465000000000000000000000c617564696f636f6465636964004024000000000000000009",
|
||||||
|
expect: []any{
|
||||||
|
"@setDataFrame",
|
||||||
|
"onMetaData",
|
||||||
|
map[string]any{
|
||||||
|
"videocodecid": float64(7),
|
||||||
|
"videodatarate": float64(0),
|
||||||
|
"audiocodecid": float64(10),
|
||||||
|
"audiodatarate": float64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "obs-connect",
|
||||||
|
actual: "020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009",
|
||||||
|
expect: []any{
|
||||||
|
"connect", float64(1),
|
||||||
|
map[string]any{
|
||||||
|
"app": "app1/stream1",
|
||||||
|
"flashVer": "FMLE/3.0 (compatible; FMSc/1.0)",
|
||||||
|
"supportsGoAway": true,
|
||||||
|
"swfUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||||
|
"tcUrl": "rtmp://192.168.10.101/app1/stream1",
|
||||||
|
"type": "nonprivate",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "obs-key",
|
||||||
|
actual: "02000d72656c6561736553747265616d004000000000000000050200046b657931",
|
||||||
|
expect: []any{
|
||||||
|
"releaseStream", float64(2), nil, "key1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "obs",
|
||||||
|
actual: "02000d40736574446174614672616d6502000a6f6e4d65746144617461080000001400086475726174696f6e000000000000000000000866696c6553697a65000000000000000000000577696474680040840000000000000006686569676874004076800000000000000c766964656f636f646563696400401c000000000000000d766964656f64617461726174650040a388000000000000096672616d6572617465004039000000000000000c617564696f636f6465636964004024000000000000000d617564696f6461746172617465004064000000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000d617564696f6368616e6e656c73004000000000000000000673746572656f01010003322e3101000003332e3101000003342e3001000003342e3101000003352e3101000003372e3101000007656e636f6465720200376f62732d6f7574707574206d6f64756c6520286c69626f62732076657273696f6e2032392e302e302d36322d6739303031323131663829000009",
|
||||||
|
expect: []any{
|
||||||
|
"@setDataFrame", "onMetaData", map[string]any{
|
||||||
|
"2.1": false,
|
||||||
|
"3.1": false,
|
||||||
|
"4.0": false,
|
||||||
|
"4.1": false,
|
||||||
|
"5.1": false,
|
||||||
|
"7.1": false,
|
||||||
|
"audiochannels": float64(2),
|
||||||
|
"audiocodecid": float64(10),
|
||||||
|
"audiodatarate": float64(160),
|
||||||
|
"audiosamplerate": float64(44100),
|
||||||
|
"audiosamplesize": float64(16),
|
||||||
|
"duration": float64(0),
|
||||||
|
"encoder": "obs-output module (libobs version 29.0.0-62-g9001211f8)",
|
||||||
|
"fileSize": float64(0),
|
||||||
|
"framerate": float64(25),
|
||||||
|
"height": float64(360),
|
||||||
|
"stereo": true,
|
||||||
|
"videocodecid": float64(7),
|
||||||
|
"videodatarate": float64(2500),
|
||||||
|
"width": float64(640),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "telegram-2",
|
||||||
|
actual: "0200075f726573756c7400400000000000000005",
|
||||||
|
expect: []any{
|
||||||
|
"_result", float64(2), nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "telegram-4",
|
||||||
|
actual: "0200075f726573756c7400401000000000000005003ff0000000000000",
|
||||||
|
expect: []any{
|
||||||
|
"_result", float64(4), nil, float64(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
b, err := hex.DecodeString(test.actual)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
rd := NewReader(b)
|
||||||
|
v, err := rd.ReadItems()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, test.expect, v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package flv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Consumer struct {
|
||||||
|
core.SuperConsumer
|
||||||
|
wr *core.WriteBuffer
|
||||||
|
muxer *Muxer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConsumer() *Consumer {
|
||||||
|
c := &Consumer{
|
||||||
|
wr: core.NewWriteBuffer(nil),
|
||||||
|
muxer: &Muxer{},
|
||||||
|
}
|
||||||
|
c.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindVideo,
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecH264},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecAAC},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
|
sender := core.NewSender(media, track.Codec)
|
||||||
|
|
||||||
|
switch track.Codec.Name {
|
||||||
|
case core.CodecH264:
|
||||||
|
payload := c.muxer.GetPayloader(track.Codec)
|
||||||
|
|
||||||
|
sender.Handler = func(pkt *rtp.Packet) {
|
||||||
|
b := payload(pkt)
|
||||||
|
if n, err := c.wr.Write(b); err == nil {
|
||||||
|
c.Send += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
|
||||||
|
} else {
|
||||||
|
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
case core.CodecAAC:
|
||||||
|
payload := c.muxer.GetPayloader(track.Codec)
|
||||||
|
|
||||||
|
sender.Handler = func(pkt *rtp.Packet) {
|
||||||
|
b := payload(pkt)
|
||||||
|
if n, err := c.wr.Write(b); err == nil {
|
||||||
|
c.Send += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
sender.Handler = aac.RTPDepay(sender.Handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.HandleRTP(track)
|
||||||
|
c.Senders = append(c.Senders, sender)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||||
|
b := c.muxer.GetInit()
|
||||||
|
if _, err := wr.Write(b); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return c.wr.WriteTo(wr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Stop() error {
|
||||||
|
_ = c.SuperConsumer.Close()
|
||||||
|
return c.wr.Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package flv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/flv/amf"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Muxer struct {
|
||||||
|
codecs []*core.Codec
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagsVideo = 0b001
|
||||||
|
FlagsAudio = 0b100
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Muxer) GetInit() []byte {
|
||||||
|
b := []byte{
|
||||||
|
'F', 'L', 'V', // signature
|
||||||
|
1, // version
|
||||||
|
0, // flags (has video/audio)
|
||||||
|
0, 0, 0, 9, // header size
|
||||||
|
0, 0, 0, 0, // tag 0 size
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := map[string]any{}
|
||||||
|
|
||||||
|
for _, codec := range m.codecs {
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecH264:
|
||||||
|
b[4] |= FlagsVideo
|
||||||
|
obj["videocodecid"] = CodecAVC
|
||||||
|
|
||||||
|
case core.CodecAAC:
|
||||||
|
b[4] |= FlagsAudio
|
||||||
|
obj["audiocodecid"] = CodecAAC
|
||||||
|
obj["audiosamplerate"] = codec.ClockRate
|
||||||
|
obj["audiosamplesize"] = 16
|
||||||
|
obj["stereo"] = codec.Channels == 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := amf.EncodeItems("@setDataFrame", "onMetaData", obj)
|
||||||
|
b = append(b, EncodeTag(TagData, 0, data)...)
|
||||||
|
|
||||||
|
for _, codec := range m.codecs {
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecH264:
|
||||||
|
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||||
|
if len(sps) == 0 {
|
||||||
|
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||||
|
}
|
||||||
|
if len(pps) == 0 {
|
||||||
|
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := h264.EncodeConfig(sps, pps)
|
||||||
|
video := append(encodeAVData(codec, 0), config...)
|
||||||
|
b = append(b, EncodeTag(TagVideo, 0, video)...)
|
||||||
|
|
||||||
|
case core.CodecAAC:
|
||||||
|
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||||
|
config, _ := hex.DecodeString(s)
|
||||||
|
audio := append(encodeAVData(codec, 0), config...)
|
||||||
|
b = append(b, EncodeTag(TagAudio, 0, audio)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Muxer) GetPayloader(codec *core.Codec) func(packet *rtp.Packet) []byte {
|
||||||
|
m.codecs = append(m.codecs, codec)
|
||||||
|
|
||||||
|
var ts0 uint32
|
||||||
|
var k = codec.ClockRate / 1000
|
||||||
|
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecH264:
|
||||||
|
buf := encodeAVData(codec, 1)
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) []byte {
|
||||||
|
if h264.IsKeyframe(packet.Payload) {
|
||||||
|
buf[0] = 1<<4 | 7
|
||||||
|
} else {
|
||||||
|
buf[0] = 2<<4 | 7
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = append(buf[:5], packet.Payload...) // reset buffer to previous place
|
||||||
|
|
||||||
|
if ts0 == 0 {
|
||||||
|
ts0 = packet.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
timeMS := (packet.Timestamp - ts0) / k
|
||||||
|
return EncodeTag(TagVideo, timeMS, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
case core.CodecAAC:
|
||||||
|
buf := encodeAVData(codec, 1)
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) []byte {
|
||||||
|
buf = append(buf[:2], packet.Payload...)
|
||||||
|
|
||||||
|
if ts0 == 0 {
|
||||||
|
ts0 = packet.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
timeMS := (packet.Timestamp - ts0) / k
|
||||||
|
return EncodeTag(TagAudio, timeMS, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeTag(tagType byte, timeMS uint32, payload []byte) []byte {
|
||||||
|
payloadSize := uint32(len(payload))
|
||||||
|
tagSize := payloadSize + 11
|
||||||
|
|
||||||
|
b := make([]byte, tagSize+4)
|
||||||
|
b[0] = tagType
|
||||||
|
b[1] = byte(payloadSize >> 16)
|
||||||
|
b[2] = byte(payloadSize >> 8)
|
||||||
|
b[3] = byte(payloadSize)
|
||||||
|
b[4] = byte(timeMS >> 16)
|
||||||
|
b[5] = byte(timeMS >> 8)
|
||||||
|
b[6] = byte(timeMS)
|
||||||
|
b[7] = byte(timeMS >> 24)
|
||||||
|
copy(b[11:], payload)
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(b[tagSize:], tagSize)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeAVData(codec *core.Codec, isFrame byte) []byte {
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecH264:
|
||||||
|
return []byte{
|
||||||
|
1<<4 | 7, // keyframe + AVC
|
||||||
|
isFrame, // 0 - config, 1 - frame
|
||||||
|
0, 0, 0, // composition time = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case core.CodecAAC:
|
||||||
|
var b0 byte = 10 << 4 // AAC
|
||||||
|
|
||||||
|
switch codec.ClockRate {
|
||||||
|
case 11025:
|
||||||
|
b0 |= 1 << 2
|
||||||
|
case 22050:
|
||||||
|
b0 |= 2 << 2
|
||||||
|
case 44100:
|
||||||
|
b0 |= 3 << 2
|
||||||
|
}
|
||||||
|
|
||||||
|
b0 |= 1 << 1 // 16 bits
|
||||||
|
|
||||||
|
if codec.Channels == 2 {
|
||||||
|
b0 |= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte{b0, isFrame} // 0 - config, 1 - frame
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+70
-14
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +41,21 @@ const (
|
|||||||
CodecAVC = 7
|
CodecAVC = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PacketTypeAVCHeader = iota
|
||||||
|
PacketTypeAVCNALU
|
||||||
|
PacketTypeAVCEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PacketTypeSequenceStart = iota
|
||||||
|
PacketTypeCodedFrames
|
||||||
|
PacketTypeSequenceEnd
|
||||||
|
PacketTypeCodedFramesX
|
||||||
|
PacketTypeMetadata
|
||||||
|
PacketTypeMPEG2TSSequenceStart
|
||||||
|
)
|
||||||
|
|
||||||
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||||
receiver, _ := c.SuperProducer.GetTrack(media, codec)
|
receiver, _ := c.SuperProducer.GetTrack(media, codec)
|
||||||
if media.Kind == core.KindVideo {
|
if media.Kind == core.KindVideo {
|
||||||
@@ -70,13 +86,32 @@ func (c *Producer) Start() error {
|
|||||||
c.audio.WriteRTP(pkt)
|
c.audio.WriteRTP(pkt)
|
||||||
|
|
||||||
case TagVideo:
|
case TagVideo:
|
||||||
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
|
if c.video == nil {
|
||||||
if c.video == nil || pkt.Payload[1] == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isExHeader(pkt.Payload) {
|
||||||
|
switch packetType := pkt.Payload[0] & 0b1111; packetType {
|
||||||
|
case PacketTypeCodedFrames:
|
||||||
|
// frame type 4b, packet type 4b, fourCC 32b, composition time 24b
|
||||||
|
pkt.Payload = pkt.Payload[8:]
|
||||||
|
case PacketTypeCodedFramesX:
|
||||||
|
// frame type 4b, packet type 4b, fourCC 32b
|
||||||
|
pkt.Payload = pkt.Payload[5:]
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch pkt.Payload[1] {
|
||||||
|
case PacketTypeAVCNALU:
|
||||||
|
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
|
||||||
|
pkt.Payload = pkt.Payload[5:]
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)
|
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)
|
||||||
pkt.Payload = pkt.Payload[5:]
|
|
||||||
c.video.WriteRTP(pkt)
|
c.video.WriteRTP(pkt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,20 +180,32 @@ func (c *Producer) probe() error {
|
|||||||
c.Medias = append(c.Medias, media)
|
c.Medias = append(c.Medias, media)
|
||||||
|
|
||||||
case TagVideo:
|
case TagVideo:
|
||||||
_ = pkt.Payload[1] // bounds
|
var codec *core.Codec
|
||||||
|
|
||||||
_ = pkt.Payload[0] >> 4 // FrameType
|
if isExHeader(pkt.Payload) {
|
||||||
codecID := pkt.Payload[0] & 0b1111 // CodecID
|
if string(pkt.Payload[1:5]) != "hvc1" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if codecID != CodecAVC {
|
if packetType := pkt.Payload[0] & 0b1111; packetType != PacketTypeSequenceStart {
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
codec = h265.ConfigToCodec(pkt.Payload[5:])
|
||||||
|
} else {
|
||||||
|
_ = pkt.Payload[0] >> 4 // FrameType
|
||||||
|
|
||||||
|
if codecID := pkt.Payload[0] & 0b1111; codecID != CodecAVC {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if packetType := pkt.Payload[1]; packetType != PacketTypeAVCHeader { // check if header
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
codec = h264.ConfigToCodec(pkt.Payload[5:])
|
||||||
}
|
}
|
||||||
|
|
||||||
if pkt.Payload[1] != 0 { // check if header
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
codec := h264.ConfigToCodec(pkt.Payload[5:])
|
|
||||||
media := &core.Media{
|
media := &core.Media{
|
||||||
Kind: core.KindVideo,
|
Kind: core.KindVideo,
|
||||||
Direction: core.DirectionRecvonly,
|
Direction: core.DirectionRecvonly,
|
||||||
@@ -170,7 +217,10 @@ func (c *Producer) probe() error {
|
|||||||
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
|
||||||
waitType = append(waitType, TagData)
|
waitType = append(waitType, TagData)
|
||||||
}
|
}
|
||||||
if bytes.Contains(pkt.Payload, []byte("videocodecid")) {
|
// Dahua cameras doesn't send videocodecid
|
||||||
|
if bytes.Contains(pkt.Payload, []byte("videocodecid")) ||
|
||||||
|
bytes.Contains(pkt.Payload, []byte("width")) ||
|
||||||
|
bytes.Contains(pkt.Payload, []byte("framerate")) {
|
||||||
waitType = append(waitType, TagVideo)
|
waitType = append(waitType, TagVideo)
|
||||||
}
|
}
|
||||||
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
|
||||||
@@ -226,9 +276,15 @@ func (c *Producer) readPacket() (*rtp.Packet, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//log.Printf("[FLV] %d %.40x", pkt.PayloadType, pkt.Payload)
|
||||||
|
|
||||||
return pkt, nil
|
return pkt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
|
func TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
|
||||||
return timeMS * clockRate / 1000
|
return timeMS * clockRate / 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isExHeader(data []byte) bool {
|
||||||
|
return data[0]&0b1000_0000 != 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package gopro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Discovery() (urls []string) {
|
||||||
|
ints, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The socket address for USB connections is 172.2X.1YZ.51:8080
|
||||||
|
// https://gopro.github.io/OpenGoPro/http_2_0#socket-address
|
||||||
|
re := regexp.MustCompile(`^172\.2\d\.1\d\d\.`)
|
||||||
|
|
||||||
|
for _, itf := range ints {
|
||||||
|
addrs, err := itf.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
host := addr.String()
|
||||||
|
if !re.MatchString(host) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
host = host[:11] + "51" // 172.2x.1xx.xxx
|
||||||
|
res, err := http.Get("http://" + host + ":8080/gopro/webcam/status")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = res.Body.Close()
|
||||||
|
|
||||||
|
urls = append(urls, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package gopro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dial(rawURL string) (core.Producer, error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &listener{host: u.Host}
|
||||||
|
|
||||||
|
if err = r.command("/gopro/webcam/stop"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.listen(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = r.command("/gopro/webcam/start"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mpegts.Open(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type listener struct {
|
||||||
|
conn net.PacketConn
|
||||||
|
host string
|
||||||
|
packet []byte
|
||||||
|
packets chan []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *listener) Read(p []byte) (n int, err error) {
|
||||||
|
if r.packet == nil {
|
||||||
|
var ok bool
|
||||||
|
if r.packet, ok = <-r.packets; !ok {
|
||||||
|
return 0, io.EOF // channel closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n = copy(p, r.packet)
|
||||||
|
|
||||||
|
if n < len(r.packet) {
|
||||||
|
r.packet = r.packet[n:]
|
||||||
|
} else {
|
||||||
|
r.packet = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *listener) Close() error {
|
||||||
|
return r.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *listener) command(api string) error {
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
res, err := client.Get("http://" + r.host + ":8080" + api)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return errors.New("gopro: wrong response: " + res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *listener) listen() (err error) {
|
||||||
|
if r.conn, err = net.ListenPacket("udp4", ":8554"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.packets = make(chan []byte, 1024)
|
||||||
|
go r.worker()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *listener) worker() {
|
||||||
|
b := make([]byte, 1500)
|
||||||
|
for {
|
||||||
|
if err := r.conn.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
n, _, err := r.conn.ReadFrom(b)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
packet := make([]byte, n)
|
||||||
|
copy(packet, b)
|
||||||
|
|
||||||
|
r.packets <- packet
|
||||||
|
}
|
||||||
|
|
||||||
|
close(r.packets)
|
||||||
|
|
||||||
|
_ = r.command("/gopro/webcam/stop")
|
||||||
|
}
|
||||||
@@ -139,3 +139,22 @@ func IndexFrame(b []byte) int {
|
|||||||
|
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FixAnnexBInAVCC(b []byte) []byte {
|
||||||
|
for i := 0; i < len(b); {
|
||||||
|
if i+4 >= len(b) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
size := bytes.Index(b[i+4:], []byte{0, 0, 0, 1})
|
||||||
|
if size < 0 {
|
||||||
|
size = len(b) - (i + 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(b[i:], uint32(size))
|
||||||
|
|
||||||
|
i += size + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,3 +84,12 @@ func TestGetProfileLevelID(t *testing.T) {
|
|||||||
profile = GetProfileLevelID(s)
|
profile = GetProfileLevelID(s)
|
||||||
require.Equal(t, "640029", profile)
|
require.Equal(t, "640029", profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecodeSPS2(t *testing.T) {
|
||||||
|
s := "6764001fad84010c20086100430802184010c200843b50740932"
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
sps := DecodeSPS(b)
|
||||||
|
assert.Nil(t, sps) // broken SPS?
|
||||||
|
}
|
||||||
|
|||||||
+8
-2
@@ -29,6 +29,12 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Memory overflow protection. Can happen if we miss a lot of packets with the marker.
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/675
|
||||||
|
if len(buf) > 5*1024*1024 {
|
||||||
|
buf = buf[: 0 : 512*1024]
|
||||||
|
}
|
||||||
|
|
||||||
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||||
// Reolink Duo 2: sends SPS with Marker and PPS without
|
// Reolink Duo 2: sends SPS with Marker and PPS without
|
||||||
if packet.Marker && len(payload) < PSMaxSize {
|
if packet.Marker && len(payload) < PSMaxSize {
|
||||||
@@ -83,10 +89,10 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
|||||||
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
|
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
|
||||||
// https://github.com/AlexxIT/WebRTC/issues/391
|
// https://github.com/AlexxIT/WebRTC/issues/391
|
||||||
// https://github.com/AlexxIT/WebRTC/issues/392
|
// https://github.com/AlexxIT/WebRTC/issues/392
|
||||||
payload = annexb.EncodeToAVCC(payload, false)
|
payload = annexb.FixAnnexBInAVCC(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", NALUTypes(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
||||||
|
|
||||||
clone := *packet
|
clone := *packet
|
||||||
clone.Version = RTPPacketVersionAVC
|
clone.Version = RTPPacketVersionAVC
|
||||||
|
|||||||
+23
-3
@@ -115,9 +115,14 @@ func DecodeSPS(sps []byte) *SPS {
|
|||||||
s.seq_scaling_matrix_present_flag = r.ReadBit()
|
s.seq_scaling_matrix_present_flag = r.ReadBit()
|
||||||
if s.seq_scaling_matrix_present_flag != 0 {
|
if s.seq_scaling_matrix_present_flag != 0 {
|
||||||
for i := byte(0); i < n; i++ {
|
for i := byte(0); i < n; i++ {
|
||||||
ssl := r.ReadBit() // seq_scaling_list_present_flag[i]
|
//goland:noinspection GoSnakeCaseUsage
|
||||||
if ssl != 0 {
|
seq_scaling_list_present_flag := r.ReadBit()
|
||||||
return nil // not implemented
|
if seq_scaling_list_present_flag != 0 {
|
||||||
|
if i < 6 {
|
||||||
|
s.scaling_list(r, 16)
|
||||||
|
} else {
|
||||||
|
s.scaling_list(r, 64)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,3 +214,18 @@ func DecodeSPS(sps []byte) *SPS {
|
|||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//goland:noinspection GoSnakeCaseUsage
|
||||||
|
func (s *SPS) scaling_list(r *bits.Reader, sizeOfScalingList int) {
|
||||||
|
lastScale := int32(8)
|
||||||
|
nextScale := int32(8)
|
||||||
|
for j := 0; j < sizeOfScalingList; j++ {
|
||||||
|
if nextScale != 0 {
|
||||||
|
delta_scale := r.ReadSEGolomb()
|
||||||
|
nextScale = (lastScale + delta_scale + 256) % 256
|
||||||
|
}
|
||||||
|
if nextScale != 0 {
|
||||||
|
lastScale = nextScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,26 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
|
vds, sps, pps := GetParameterSet(codec.FmtpLine)
|
||||||
|
ps := h264.JoinNALU(vds, sps, pps)
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) {
|
||||||
|
switch NALUType(packet.Payload) {
|
||||||
|
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = h264.Join(ps, packet.Payload)
|
||||||
|
handler(&clone)
|
||||||
|
default:
|
||||||
|
handler(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func AVCCToCodec(avcc []byte) *core.Codec {
|
func AVCCToCodec(avcc []byte) *core.Codec {
|
||||||
buf := bytes.NewBufferString("profile-id=1")
|
buf := bytes.NewBufferString("profile-id=1")
|
||||||
|
|
||||||
|
|||||||
+59
-1
@@ -1,7 +1,40 @@
|
|||||||
// Package h265 - MPEG4 format related functions
|
// Package h265 - MPEG4 format related functions
|
||||||
package h265
|
package h265
|
||||||
|
|
||||||
import "encoding/binary"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DecodeConfig(conf []byte) (profile, vps, sps, pps []byte) {
|
||||||
|
profile = conf[1:4]
|
||||||
|
|
||||||
|
b := conf[23:]
|
||||||
|
if binary.BigEndian.Uint16(b[1:]) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vpsSize := binary.BigEndian.Uint16(b[3:])
|
||||||
|
vps = b[5 : 5+vpsSize]
|
||||||
|
|
||||||
|
b = conf[23+5+vpsSize:]
|
||||||
|
if binary.BigEndian.Uint16(b[1:]) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spsSize := binary.BigEndian.Uint16(b[3:])
|
||||||
|
sps = b[5 : 5+spsSize]
|
||||||
|
|
||||||
|
b = conf[23+5+vpsSize+5+spsSize:]
|
||||||
|
if binary.BigEndian.Uint16(b[1:]) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ppsSize := binary.BigEndian.Uint16(b[3:])
|
||||||
|
pps = b[5 : 5+ppsSize]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func EncodeConfig(vps, sps, pps []byte) []byte {
|
func EncodeConfig(vps, sps, pps []byte) []byte {
|
||||||
vpsSize := uint16(len(vps))
|
vpsSize := uint16(len(vps))
|
||||||
@@ -38,3 +71,28 @@ func EncodeConfig(vps, sps, pps []byte) []byte {
|
|||||||
|
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConfigToCodec(conf []byte) *core.Codec {
|
||||||
|
buf := bytes.NewBufferString("profile-id=1")
|
||||||
|
|
||||||
|
_, vps, sps, pps := DecodeConfig(conf)
|
||||||
|
if vps != nil {
|
||||||
|
buf.WriteString(";sprop-vps=")
|
||||||
|
buf.WriteString(base64.StdEncoding.EncodeToString(vps))
|
||||||
|
}
|
||||||
|
if sps != nil {
|
||||||
|
buf.WriteString(";sprop-sps=")
|
||||||
|
buf.WriteString(base64.StdEncoding.EncodeToString(sps))
|
||||||
|
}
|
||||||
|
if pps != nil {
|
||||||
|
buf.WriteString(";sprop-pps=")
|
||||||
|
buf.WriteString(base64.StdEncoding.EncodeToString(pps))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &core.Codec{
|
||||||
|
Name: core.CodecH265,
|
||||||
|
ClockRate: 90000,
|
||||||
|
FmtpLine: buf.String(),
|
||||||
|
PayloadType: core.PayloadTypeRAW,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
|||||||
case 1: // end
|
case 1: // end
|
||||||
buf = append(buf, data[3:]...)
|
buf = append(buf, data[3:]...)
|
||||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
||||||
|
case 3: // wrong RFC 7798 realisation from OpenIPC project
|
||||||
|
// A non-fragmented NAL unit MUST NOT be transmitted in one FU; i.e.,
|
||||||
|
// the Start bit and End bit must not both be set to 1 in the same FU
|
||||||
|
// header.
|
||||||
|
nuType = data[2] & 0x3F
|
||||||
|
buf = binary.BigEndian.AppendUint32(buf, uint32(len(data))-1) // NAL unit size
|
||||||
|
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
||||||
|
buf = append(buf, data[3:]...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nuStart = len(buf)
|
nuStart = len(buf)
|
||||||
|
|||||||
+2
-1
@@ -50,4 +50,5 @@ Requires ffmpeg built with `--enable-libfdk-aac`
|
|||||||
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
|
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
|
||||||
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)
|
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)
|
||||||
- [HomeKit Secure Video Unofficial Specification](https://github.com/Supereg/secure-video-specification)
|
- [HomeKit Secure Video Unofficial Specification](https://github.com/Supereg/secure-video-specification)
|
||||||
- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)
|
- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)
|
||||||
|
- https://github.com/ljezny/Particle-HAP/blob/master/HAP-Specification-Non-Commercial-Version.pdf
|
||||||
@@ -93,6 +93,8 @@ func (a *Accessory) GetCharacterByID(iid uint64) *Character {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
Desc string `json:"description,omitempty"`
|
||||||
|
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
IID uint64 `json:"iid"`
|
IID uint64 `json:"iid"`
|
||||||
Primary bool `json:"primary,omitempty"`
|
Primary bool `json:"primary,omitempty"`
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func ServiceCameraRTPStreamManagement() *hap.Service {
|
|||||||
VideoAttrs: []VideoAttrs{
|
VideoAttrs: []VideoAttrs{
|
||||||
{Width: 1920, Height: 1080, Framerate: 30},
|
{Width: 1920, Height: 1080, Framerate: 30},
|
||||||
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
|
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
|
||||||
|
{Width: 320, Height: 240, Framerate: 15}, // apple watch
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import (
|
|||||||
// Value should be omit for PW
|
// Value should be omit for PW
|
||||||
// Value may be empty for PR
|
// Value may be empty for PR
|
||||||
type Character struct {
|
type Character struct {
|
||||||
|
Desc string `json:"description,omitempty"`
|
||||||
|
|
||||||
IID uint64 `json:"iid"`
|
IID uint64 `json:"iid"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
Value any `json:"value,omitempty"`
|
Value any `json:"value,omitempty"`
|
||||||
Perms []string `json:"perms"`
|
Perms []string `json:"perms"`
|
||||||
|
|
||||||
//Descr string `json:"description,omitempty"`
|
|
||||||
//MaxLen int `json:"maxLen,omitempty"`
|
//MaxLen int `json:"maxLen,omitempty"`
|
||||||
//Unit string `json:"unit,omitempty"`
|
//Unit string `json:"unit,omitempty"`
|
||||||
//MinValue any `json:"minValue,omitempty"`
|
//MinValue any `json:"minValue,omitempty"`
|
||||||
|
|||||||
+42
-6
@@ -41,6 +41,9 @@ type Client struct {
|
|||||||
|
|
||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
|
|
||||||
|
res chan *http.Response
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(rawURL string) (*Client, error) {
|
func NewClient(rawURL string) (*Client, error) {
|
||||||
@@ -80,6 +83,10 @@ func (c *Client) DeviceHost() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Dial() (err error) {
|
func (c *Client) Dial() (err error) {
|
||||||
|
if len(c.ClientID) == 0 || len(c.ClientPrivate) == 0 {
|
||||||
|
return errors.New("hap: can't dial witout client_id or client_private")
|
||||||
|
}
|
||||||
|
|
||||||
// update device address (host and/or port) before dial
|
// update device address (host and/or port) before dial
|
||||||
_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||||
if entry.Complete() && entry.Info["id"] == c.DeviceID {
|
if entry.Complete() && entry.Info["id"] == c.DeviceID {
|
||||||
@@ -214,7 +221,7 @@ func (c *Client) Dial() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// new reader for new conn
|
// new reader for new conn
|
||||||
c.reader = bufio.NewReaderSize(c.Conn, 32*1024) // 32K like default request body
|
c.reader = bufio.NewReader(c.Conn)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,9 +230,33 @@ func (c *Client) Close() error {
|
|||||||
if c.Conn == nil {
|
if c.Conn == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
conn := c.Conn
|
return c.Conn.Close()
|
||||||
c.Conn = nil
|
}
|
||||||
return conn.Close()
|
|
||||||
|
func (c *Client) eventsReader() {
|
||||||
|
c.res = make(chan *http.Response)
|
||||||
|
|
||||||
|
for {
|
||||||
|
var res *http.Response
|
||||||
|
if res, c.err = ReadResponse(c.reader, nil); c.err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
if body, c.err = io.ReadAll(res.Body); c.err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Body = io.NopCloser(bytes.NewReader(body))
|
||||||
|
|
||||||
|
if res.Proto != ProtoEvent {
|
||||||
|
c.res <- res
|
||||||
|
} else if c.OnEvent != nil {
|
||||||
|
c.OnEvent(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(c.res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAccessories() ([]*Accessory, error) {
|
func (c *Client) GetAccessories() ([]*Accessory, error) {
|
||||||
@@ -296,11 +327,13 @@ func (c *Client) PutCharacters(characters ...*Character) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
|
res, err := c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, _ = io.ReadAll(res.Body) // important to "clear" body
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,8 +350,11 @@ func (c *Client) GetImage(width, height int) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) LocalIP() string {
|
func (c *Client) LocalIP() string {
|
||||||
|
if c.Conn == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
addr := c.Conn.LocalAddr().(*net.TCPAddr)
|
addr := c.Conn.LocalAddr().(*net.TCPAddr)
|
||||||
return addr.IP.To4().String()
|
return addr.IP.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeKey(s string) []byte {
|
func DecodeKey(s string) []byte {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package hap
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -22,6 +23,9 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|||||||
if err := req.Write(c.Conn); err != nil {
|
if err := req.Write(c.Conn); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if c.res != nil {
|
||||||
|
return <-c.res, c.err
|
||||||
|
}
|
||||||
return http.ReadResponse(c.reader, req)
|
return http.ReadResponse(c.reader, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,3 +58,27 @@ func (c *Client) Post(path, contentType string, body io.Reader) (*http.Response,
|
|||||||
func (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) {
|
func (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) {
|
||||||
return c.Request("PUT", path, contentType, body)
|
return c.Request("PUT", path, contentType, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ProtoEvent = "EVENT/1.0"
|
||||||
|
|
||||||
|
func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) {
|
||||||
|
b, err := r.Peek(9)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(b) != ProtoEvent {
|
||||||
|
return http.ReadResponse(r, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(b, "HTTP/1.1 ")
|
||||||
|
|
||||||
|
res, err := http.ReadResponse(r, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Proto = ProtoEvent
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ func (c *Client) Pair(feature, pin string) (err error) {
|
|||||||
Proof string `tlv8:"4"` // server proof
|
Proof string `tlv8:"4"` // server proof
|
||||||
State byte `tlv8:"6"`
|
State byte `tlv8:"6"`
|
||||||
Error byte `tlv8:"7"`
|
Error byte `tlv8:"7"`
|
||||||
|
|
||||||
|
EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices)
|
||||||
}
|
}
|
||||||
if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil {
|
if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package hap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EventReader struct {
|
|
||||||
r io.Reader
|
|
||||||
ch chan []byte
|
|
||||||
err error
|
|
||||||
left []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEventReader(r io.Reader) *EventReader {
|
|
||||||
e := &EventReader{r: r, ch: make(chan []byte, 1)}
|
|
||||||
go e.background()
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EventReader) background() {
|
|
||||||
b := make([]byte, 32*1024)
|
|
||||||
for {
|
|
||||||
n, err := e.r.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
e.err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if n >= 6 && string(b[:6]) == "EVENT " {
|
|
||||||
panic("TODO")
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy because will be overwriten
|
|
||||||
buf := make([]byte, n)
|
|
||||||
copy(buf, b)
|
|
||||||
e.ch <- buf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EventReader) Read(p []byte) (n int, err error) {
|
|
||||||
if e.err != nil {
|
|
||||||
return 0, e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if something left after previous reading
|
|
||||||
if e.left != nil {
|
|
||||||
// if still something left
|
|
||||||
if n = copy(p, e.left); n < len(e.left) {
|
|
||||||
e.left = e.left[n:]
|
|
||||||
} else {
|
|
||||||
e.left = nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
return 0, os.ErrDeadlineExceeded
|
|
||||||
case b := <-e.ch:
|
|
||||||
if n = copy(p, b); n < len(b) {
|
|
||||||
e.left = b[n:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
+5
-3
@@ -31,8 +31,9 @@ const (
|
|||||||
StatusPaired = "0"
|
StatusPaired = "0"
|
||||||
StatusNotPaired = "1"
|
StatusNotPaired = "1"
|
||||||
|
|
||||||
CategoryBridge = "2"
|
CategoryBridge = "2"
|
||||||
CategoryCamera = "17"
|
CategoryCamera = "17"
|
||||||
|
CategoryDoorbell = "18"
|
||||||
|
|
||||||
StateM1 = 1
|
StateM1 = 1
|
||||||
StateM2 = 2
|
StateM2 = 2
|
||||||
@@ -65,7 +66,8 @@ type JSONCharacters struct {
|
|||||||
type JSONCharacter struct {
|
type JSONCharacter struct {
|
||||||
AID uint8 `json:"aid"`
|
AID uint8 `json:"aid"`
|
||||||
IID uint64 `json:"iid"`
|
IID uint64 `json:"iid"`
|
||||||
Value any `json:"value"`
|
Value any `json:"value,omitempty"`
|
||||||
|
Event any `json:"ev,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SanitizePin(pin string) (string, error) {
|
func SanitizePin(pin string) (string, error) {
|
||||||
|
|||||||
+49
-57
@@ -1,7 +1,9 @@
|
|||||||
package secure
|
package secure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -14,6 +16,9 @@ import (
|
|||||||
type Conn struct {
|
type Conn struct {
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
|
|
||||||
|
rd *bufio.Reader
|
||||||
|
wr *bufio.Writer
|
||||||
|
|
||||||
encryptKey []byte
|
encryptKey []byte
|
||||||
decryptKey []byte
|
decryptKey []byte
|
||||||
encryptCnt uint64
|
encryptCnt uint64
|
||||||
@@ -33,11 +38,19 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isClient {
|
c := &Conn{
|
||||||
return &Conn{conn: conn, encryptKey: key2, decryptKey: key1}, nil
|
conn: conn,
|
||||||
} else {
|
rd: bufio.NewReaderSize(conn, 32*1024),
|
||||||
return &Conn{conn: conn, encryptKey: key1, decryptKey: key2}, nil
|
wr: bufio.NewWriterSize(conn, 32*1024),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isClient {
|
||||||
|
c.encryptKey, c.decryptKey = key2, key1
|
||||||
|
} else {
|
||||||
|
c.encryptKey, c.decryptKey = key1, key2
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -50,84 +63,63 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *Conn) Read(b []byte) (n int, err error) {
|
func (c *Conn) Read(b []byte) (n int, err error) {
|
||||||
verify := make([]byte, VerifySize) // = packet length
|
if cap(b) < PacketSizeMax {
|
||||||
buf := make([]byte, PacketSizeMax+Overhead)
|
return 0, errors.New("hap: read buffer is too small")
|
||||||
nonce := make([]byte, NonceSize)
|
|
||||||
|
|
||||||
for {
|
|
||||||
if len(b) < PacketSizeMax {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = io.ReadFull(c.conn, verify); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
size := binary.LittleEndian.Uint16(verify)
|
|
||||||
ciphertext := buf[:size+Overhead]
|
|
||||||
|
|
||||||
if _, err = io.ReadFull(c.conn, ciphertext); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
|
||||||
c.decryptCnt++
|
|
||||||
|
|
||||||
// put decrypted text to b's end
|
|
||||||
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n += int(size) // plaintext size
|
|
||||||
|
|
||||||
// Finish when all bytes fit in b
|
|
||||||
if size < PacketSizeMax {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
b = b[size:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify := make([]byte, 2) // verify = plain message size
|
||||||
|
if _, err = io.ReadFull(c.rd, verify); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n = int(binary.LittleEndian.Uint16(verify))
|
||||||
|
ciphertext := make([]byte, n+Overhead)
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(c.rd, ciphertext); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, NonceSize)
|
||||||
|
binary.LittleEndian.PutUint64(nonce, c.decryptCnt)
|
||||||
|
c.decryptCnt++
|
||||||
|
|
||||||
|
_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||||
c.mx.Lock()
|
buf := make([]byte, 0, PacketSizeMax+Overhead)
|
||||||
defer c.mx.Unlock()
|
|
||||||
|
|
||||||
nonce := make([]byte, NonceSize)
|
nonce := make([]byte, NonceSize)
|
||||||
buf := make([]byte, NonceSize+PacketSizeMax+Overhead)
|
verify := make([]byte, VerifySize)
|
||||||
verify := buf[:VerifySize] // part of write buffer
|
|
||||||
|
|
||||||
for {
|
for len(b) > 0 {
|
||||||
size := len(b)
|
size := len(b)
|
||||||
if size > PacketSizeMax {
|
if size > PacketSizeMax {
|
||||||
size = PacketSizeMax
|
size = PacketSizeMax
|
||||||
}
|
}
|
||||||
|
|
||||||
binary.LittleEndian.PutUint16(verify, uint16(size))
|
binary.LittleEndian.PutUint16(verify, uint16(size))
|
||||||
|
if _, err = c.wr.Write(verify); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
|
binary.LittleEndian.PutUint64(nonce, c.encryptCnt)
|
||||||
c.encryptCnt++
|
c.encryptCnt++
|
||||||
|
|
||||||
// put encrypted text to writing buffer just after size (2 bytes)
|
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf, nonce, b[:size], verify)
|
||||||
_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[2:2], nonce, b[:size], verify)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = c.conn.Write(buf[:VerifySize+size+Overhead]); err != nil {
|
if _, err = c.wr.Write(buf[:size+Overhead]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
n += size // plaintext size
|
b = b[size:]
|
||||||
|
n += size
|
||||||
if size < PacketSizeMax {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
b = b[PacketSizeMax:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.wr.Flush()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -146,7 +146,7 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co
|
|||||||
|
|
||||||
clientPublic := s.GetPair(conn, plainM3.Identifier)
|
clientPublic := s.GetPair(conn, plainM3.Identifier)
|
||||||
if clientPublic == nil {
|
if clientPublic == nil {
|
||||||
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", plainM3.Identifier)
|
return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
|
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
|
||||||
|
|||||||
+3
-2
@@ -2,10 +2,11 @@ package hass
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
pion "github.com/pion/webrtc/v3"
|
pion "github.com/pion/webrtc/v3"
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -48,7 +49,7 @@ func NewClient(rawURL string) (*Client, error) {
|
|||||||
defer hassAPI.Close()
|
defer hassAPI.Close()
|
||||||
|
|
||||||
// 2. Create WebRTC client
|
// 2. Create WebRTC client
|
||||||
rtcAPI, err := webrtc.NewAPI("")
|
rtcAPI, err := webrtc.NewAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,10 +175,10 @@ func (c *Client) Start() error {
|
|||||||
func (c *Client) Stop() error {
|
func (c *Client) Stop() error {
|
||||||
_ = c.SuperProducer.Close()
|
_ = c.SuperProducer.Close()
|
||||||
|
|
||||||
if c.videoSession != nil {
|
if c.videoSession != nil && c.videoSession.Remote != nil {
|
||||||
c.srtp.DelSession(c.videoSession)
|
c.srtp.DelSession(c.videoSession)
|
||||||
}
|
}
|
||||||
if c.audioSession != nil {
|
if c.audioSession != nil && c.audioSession.Remote != nil {
|
||||||
c.srtp.DelSession(c.audioSession)
|
c.srtp.DelSession(c.audioSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/opus"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,7 @@ type Consumer struct {
|
|||||||
sessionID string
|
sessionID string
|
||||||
videoSession *srtp.Session
|
videoSession *srtp.Session
|
||||||
audioSession *srtp.Session
|
audioSession *srtp.Session
|
||||||
|
audioRTPTime byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
|
func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {
|
||||||
@@ -113,6 +115,7 @@ func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) bool {
|
|||||||
c.audioSession.Remote.SSRC = conf.AudioCodec.RTPParams[0].SSRC
|
c.audioSession.Remote.SSRC = conf.AudioCodec.RTPParams[0].SSRC
|
||||||
c.audioSession.PayloadType = conf.AudioCodec.RTPParams[0].PayloadType
|
c.audioSession.PayloadType = conf.AudioCodec.RTPParams[0].PayloadType
|
||||||
c.audioSession.RTCPInterval = toDuration(conf.AudioCodec.RTPParams[0].RTCPInterval)
|
c.audioSession.RTCPInterval = toDuration(conf.AudioCodec.RTPParams[0].RTCPInterval)
|
||||||
|
c.audioRTPTime = conf.AudioCodec.CodecParams[0].RTPTime[0]
|
||||||
|
|
||||||
c.srtp.AddSession(c.videoSession)
|
c.srtp.AddSession(c.videoSession)
|
||||||
c.srtp.AddSession(c.audioSession)
|
c.srtp.AddSession(c.audioSession)
|
||||||
@@ -155,6 +158,8 @@ func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Re
|
|||||||
} else {
|
} else {
|
||||||
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)
|
||||||
}
|
}
|
||||||
|
case core.CodecOpus:
|
||||||
|
sender.Handler = opus.RepackToHAP(c.audioRTPTime, sender.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
sender.HandleRTP(track)
|
sender.HandleRTP(track)
|
||||||
|
|||||||
+19
-14
@@ -2,6 +2,7 @@ package homekit
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
@@ -20,17 +21,16 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media {
|
|||||||
|
|
||||||
for _, codec := range codecs {
|
for _, codec := range codecs {
|
||||||
for _, param := range codec.CodecParams {
|
for _, param := range codec.CodecParams {
|
||||||
for _, profileID := range param.ProfileID {
|
// get best profile and level
|
||||||
for _, level := range param.Level {
|
profileID := slices.Max(param.ProfileID)
|
||||||
profile := videoProfiles[profileID] + videoLevels[level]
|
level := slices.Max(param.Level)
|
||||||
mediaCodec := &core.Codec{
|
profile := videoProfiles[profileID] + videoLevels[level]
|
||||||
Name: videoCodecs[codec.CodecType],
|
mediaCodec := &core.Codec{
|
||||||
ClockRate: 90000,
|
Name: videoCodecs[codec.CodecType],
|
||||||
FmtpLine: "profile-level-id=" + profile,
|
ClockRate: 90000,
|
||||||
}
|
FmtpLine: "profile-level-id=" + profile,
|
||||||
media.Codecs = append(media.Codecs, mediaCodec)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
media.Codecs = append(media.Codecs, mediaCodec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mediaCodec.Name == core.CodecELD {
|
if mediaCodec.Name == core.CodecELD {
|
||||||
// onli this version works with FFmpeg
|
// only this version works with FFmpeg
|
||||||
conf := aac.EncodeConfig(aac.TypeAACELD, 24000, 1, true)
|
conf := aac.EncodeConfig(aac.TypeAACELD, 24000, 1, true)
|
||||||
mediaCodec.FmtpLine = aac.FMTP + hex.EncodeToString(conf)
|
mediaCodec.FmtpLine = aac.FMTP + hex.EncodeToString(conf)
|
||||||
}
|
}
|
||||||
@@ -71,6 +71,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media {
|
|||||||
func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec {
|
func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec {
|
||||||
profileID := video0.CodecParams[0].ProfileID[0]
|
profileID := video0.CodecParams[0].ProfileID[0]
|
||||||
level := video0.CodecParams[0].Level[0]
|
level := video0.CodecParams[0].Level[0]
|
||||||
|
attrs := video0.VideoAttrs[0]
|
||||||
|
|
||||||
if track != nil {
|
if track != nil {
|
||||||
profile := h264.GetProfileLevelID(track.Codec.FmtpLine)
|
profile := h264.GetProfileLevelID(track.Codec.FmtpLine)
|
||||||
@@ -88,6 +89,12 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, s := range video0.VideoAttrs {
|
||||||
|
if s.Width > attrs.Width || s.Height > attrs.Height {
|
||||||
|
attrs = s
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &camera.VideoCodec{
|
return &camera.VideoCodec{
|
||||||
@@ -98,9 +105,7 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video
|
|||||||
Level: []byte{level},
|
Level: []byte{level},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
VideoAttrs: []camera.VideoAttrs{
|
VideoAttrs: []camera.VideoAttrs{attrs},
|
||||||
{Width: 1920, Height: 1080, Framerate: 30},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,8 @@ func proxy(r, w net.Conn, pair ServerPair) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
//if n > 512 {
|
//log.Printf("[hap] %d bytes => %s\n%.512s", n, w.RemoteAddr(), b[:n])
|
||||||
// log.Printf("[hap] %d bytes => %s\n%s...", n, w.RemoteAddr(), b[:512])
|
|
||||||
//} else {
|
|
||||||
// log.Printf("[hap] %d bytes => %s\n%s", n, w.RemoteAddr(), b[:n])
|
|
||||||
//}
|
|
||||||
if _, err = w.Write(b[:n]); err != nil {
|
if _, err = w.Write(b[:n]); err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user