Compare commits
436 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9f2b5158c | |||
| b9f984dad0 | |||
| 290e011061 | |||
| 09109e783e | |||
| 8ac834bdd4 | |||
| 06d8503fd0 | |||
| 4c3de3bbf4 | |||
| 4933c1415b | |||
| 322c332170 | |||
| 5d9c254282 | |||
| a03db503c3 | |||
| 2ea66deb08 | |||
| b3c5ef8c86 | |||
| fb1e7613cb | |||
| 8a7ab63b00 | |||
| 07f51e6929 | |||
| f64d279672 | |||
| 4185202496 | |||
| edbcd3e736 | |||
| 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 | |||
| fc1b6af436 | |||
| 88fb589d2e | |||
| 5c5357cd79 | |||
| 5ffd60c429 | |||
| 5645c73613 | |||
| 13a7957cf3 | |||
| c0d5a7c01a | |||
| d87cc9ddb6 | |||
| b1c4bcc508 | |||
| 6288c2a57f | |||
| 1c569e690d | |||
| 7fdc6b9472 | |||
| 60d7d525f2 | |||
| f6f2998e85 | |||
| 82d1f2cf0b | |||
| f00e646612 | |||
| a101387b26 | |||
| af31ab604d | |||
| ccdd6ed490 | |||
| 9f404d965f | |||
| 0621b82aff | |||
| 22787b979d | |||
| 7d65c60711 | |||
| 69da64a49c | |||
| 66c858e00e | |||
| ef63cec7a8 | |||
| 0ac505ba09 | |||
| d4444c6257 | |||
| c6d5bb4eeb | |||
| 7f232c5cf2 | |||
| dc2ab5fcc0 | |||
| 137b23da10 | |||
| 54e361e3b8 | |||
| c78da1a7a9 | |||
| 27673cb0c1 | |||
| c040a02fa8 | |||
| a664e3b838 | |||
| 317b3b5eeb | |||
| 9f14b30aae | |||
| 065a6f4f46 | |||
| 9f9dc7e844 | |||
| b1c0a28366 | |||
| fc963dfe5c | |||
| 6f5ba2ade6 | |||
| ea708bb606 | |||
| 0822326900 | |||
| 79fc0cd395 | |||
| 357e7c1b18 | |||
| 71f1e445e1 | |||
| 20efe22e60 | |||
| 75a3dad745 | |||
| f5cca50830 | |||
| 8cd977f7ad | |||
| 90f2a9e106 | |||
| e0ad358aa9 | |||
| 3db4002420 | |||
| bf248c49c3 | |||
| 69a3a30a0e | |||
| f80f179e4c | |||
| c1c1d84cef | |||
| c431d888f0 | |||
| 2ebb791eb7 | |||
| 00b818b4d7 | |||
| ce1b0d442c | |||
| 5283c9781c | |||
| 279d8bf799 | |||
| 7114d63ba6 | |||
| 120ae89578 | |||
| d1eb623fd6 | |||
| 873cf65317 | |||
| 2091dead3f | |||
| 2ffd859f0e | |||
| da02a97a00 | |||
| fb51dc781d | |||
| 32bf64028d | |||
| 2e4e75e386 | |||
| f67f6e5b9f | |||
| 24039218a1 | |||
| 1f447ef73c | |||
| 4509198eef | |||
| bc60cbefb8 | |||
| a9118562a9 | |||
| 24637be7c2 | |||
| d74be47696 | |||
| 76a00031cd | |||
| 063a192699 | |||
| b016b7dc2a | |||
| 42f6441512 | |||
| dd066ba040 | |||
| b3def6cfa2 | |||
| 4a82eb3503 | |||
| c3ba8db660 | |||
| 4e1a0e1ab9 | |||
| 1dd3dbbcd8 | |||
| e1be2d9e48 | |||
| 8fbfccd024 | |||
| de6bb33f01 | |||
| 3a40515a90 | |||
| 5d533338d0 | |||
| f412852d50 | |||
| 5fbec487e2 | |||
| 19c61e20c0 | |||
| 0b6fda2af5 | |||
| e9795e7521 | |||
| 3b8413a9dd | |||
| b2f9ad7efb | |||
| 4baa3f5588 | |||
| 9c5ae3260c | |||
| b7baef0a48 | |||
| 8778d7c9ab | |||
| d275997e54 | |||
| 2faea1bb69 | |||
| ba6c96412b | |||
| ed38122752 | |||
| 922587ed2e | |||
| 8e7c9d19e4 | |||
| 0f33ef0fc5 | |||
| a14c87ad60 | |||
| 6d82b1ce89 | |||
| d73e9f6bcf | |||
| e6a87fbd69 | |||
| 3defbd60db | |||
| 6e9574a1bd | |||
| 7005cd08f2 | |||
| e94f338b77 | |||
| d6172587b3 |
@@ -0,0 +1,203 @@
|
|||||||
|
name: Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-binaries:
|
||||||
|
name: Build binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env: { CGO_ENABLED: 0 }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with: { go-version: '1.21' }
|
||||||
|
|
||||||
|
- name: Build go2rtc_win64
|
||||||
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_win64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||||
|
|
||||||
|
- name: Build go2rtc_win32
|
||||||
|
env: { GOOS: windows, GOARCH: 386 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_win32
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_win32, path: go2rtc.exe }
|
||||||
|
|
||||||
|
- name: Build go2rtc_win_arm64
|
||||||
|
env: { GOOS: windows, GOARCH: arm64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_win_arm64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_amd64
|
||||||
|
env: { GOOS: linux, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_amd64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_amd64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_i386
|
||||||
|
env: { GOOS: linux, GOARCH: 386 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_i386
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_i386, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_arm64
|
||||||
|
env: { GOOS: linux, GOARCH: arm64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_arm64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_arm64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_arm
|
||||||
|
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_arm
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_arm, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_armv6
|
||||||
|
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_armv6
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_armv6, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_mipsel
|
||||||
|
env: { GOOS: linux, GOARCH: mipsle }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_mipsel
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_mac_amd64
|
||||||
|
env: { GOOS: darwin, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_mac_amd64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_mac_amd64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_mac_arm64
|
||||||
|
env: { GOOS: darwin, GOARCH: arm64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_mac_arm64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_mac_arm64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_freebsd_amd64
|
||||||
|
env: { GOOS: freebsd, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_freebsd_amd64
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_freebsd_arm64
|
||||||
|
env: { GOOS: freebsd, GOARCH: arm64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_freebsd_arm64
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
|
||||||
|
|
||||||
|
docker-master:
|
||||||
|
name: Build docker master
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}},enable=false
|
||||||
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/386
|
||||||
|
linux/arm/v7
|
||||||
|
linux/arm64/v8
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
docker-hardware:
|
||||||
|
name: Build docker hardware
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta-hw
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ github.repository }}
|
||||||
|
flavor: |
|
||||||
|
suffix=-hardware
|
||||||
|
latest=false
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}},enable=false
|
||||||
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: hardware.Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta-hw.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-hw.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
name: docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'master'
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ github.repository }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=semver,pattern={{version}},enable=false
|
|
||||||
type=match,pattern=v(.*),group=1
|
|
||||||
|
|
||||||
- name: Docker meta Hardware
|
|
||||||
id: meta-hw
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ github.repository }}
|
|
||||||
flavor: |
|
|
||||||
suffix=-hardware
|
|
||||||
latest=false
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=semver,pattern={{version}},enable=false
|
|
||||||
type=match,pattern=v(.*),group=1
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/386
|
|
||||||
linux/arm/v7
|
|
||||||
linux/arm64/v8
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
- name: Build and push Hardware
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: hardware.Dockerfile
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta-hw.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-hw.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# push:
|
|
||||||
# tags:
|
|
||||||
# - 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Generate changelog
|
|
||||||
run: |
|
|
||||||
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
|
|
||||||
- name: install lipo
|
|
||||||
run: |
|
|
||||||
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
|
|
||||||
chmod +x /tmp/lipo
|
|
||||||
mv /tmp/lipo /usr/local/bin
|
|
||||||
- name: Build Go binaries
|
|
||||||
run: |
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
export CGO_ENABLED=0
|
|
||||||
|
|
||||||
mkdir -p artifacts
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=amd64
|
|
||||||
export FILENAME=artifacts/go2rtc_win64.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=386
|
|
||||||
export FILENAME=artifacts/go2rtc_win32.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=arm64
|
|
||||||
export FILENAME=artifacts/go2rtc_win_arm64.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=amd64
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_amd64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=386
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_i386
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=arm64
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_arm64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=arm
|
|
||||||
export GOARM=7
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_arm
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=mipsle
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_mipsel
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=darwin
|
|
||||||
export GOARCH=amd64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
|
|
||||||
|
|
||||||
export GOOS=darwin
|
|
||||||
export GOARCH=arm64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
|
|
||||||
|
|
||||||
export FILENAME=artifacts/go2rtc_mac_universal.zip
|
|
||||||
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
|
||||||
|
|
||||||
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
|
|
||||||
- name: Setup tmate session
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
if: ${{ failure() }}
|
|
||||||
- name: Set env
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Create GitHub release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
files: artifacts/*
|
|
||||||
generate_release_notes: true
|
|
||||||
name: Release ${{ env.RELEASE_VERSION }}
|
|
||||||
body_path: CHANGELOG.md
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
+13
-13
@@ -1,11 +1,11 @@
|
|||||||
name: Test Build and Run
|
name: Test Build and Run
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
# push:
|
||||||
branches:
|
# branches:
|
||||||
- '*'
|
# - '*'
|
||||||
pull_request:
|
# pull_request:
|
||||||
merge_group:
|
# merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -21,12 +21,12 @@ 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.20'
|
go-version: '1.21'
|
||||||
|
|
||||||
- name: Build Go binary
|
- name: Build Go binary
|
||||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
@@ -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
|
||||||
|
|||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
.tmp/
|
.tmp/
|
||||||
|
|
||||||
go2rtc.yaml
|
go2rtc.yaml
|
||||||
|
|
||||||
go2rtc.json
|
go2rtc.json
|
||||||
|
|
||||||
|
0_test.go
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
ARG PYTHON_VERSION="3.11"
|
ARG PYTHON_VERSION="3.11"
|
||||||
ARG GO_VERSION="1.20"
|
ARG GO_VERSION="1.21"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||||
|
|||||||
@@ -1,8 +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://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.
|
||||||
|
|
||||||
@@ -12,16 +16,17 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
- zero-delay for many supported protocols (lowest possible streaming latency)
|
- zero-delay for many supported protocols (lowest possible streaming latency)
|
||||||
- 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), [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)
|
||||||
- first project in the World with 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)
|
||||||
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
|
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
|
||||||
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
||||||
- 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:**
|
||||||
@@ -32,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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,6 +46,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
* [go2rtc: Docker](#go2rtc-docker)
|
* [go2rtc: Docker](#go2rtc-docker)
|
||||||
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
|
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
|
||||||
* [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
|
* [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
|
||||||
|
* [go2rtc: Dev version](#go2rtc-dev-version)
|
||||||
* [Configuration](#configuration)
|
* [Configuration](#configuration)
|
||||||
* [Module: Streams](#module-streams)
|
* [Module: Streams](#module-streams)
|
||||||
* [Two way audio](#two-way-audio)
|
* [Two way audio](#two-way-audio)
|
||||||
@@ -51,10 +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: 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)
|
||||||
@@ -64,11 +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: 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)
|
||||||
@@ -110,7 +123,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
|||||||
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
|
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
|
||||||
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
||||||
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
|
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
|
||||||
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
|
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
|
||||||
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
|
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
|
||||||
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
|
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
|
||||||
|
|
||||||
@@ -118,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
|
||||||
|
|
||||||
@@ -133,9 +146,17 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
|
|||||||
|
|
||||||
[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.
|
[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.
|
||||||
|
|
||||||
|
### go2rtc: Dev version
|
||||||
|
|
||||||
|
Latest, but maybe unstable version:
|
||||||
|
|
||||||
|
- Binary: [latest nightly release](https://nightly.link/AlexxIT/go2rtc/workflows/build/master)
|
||||||
|
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
|
||||||
|
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
|
||||||
|
|
||||||
## Configuration
|
## 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
|
||||||
@@ -153,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
|
||||||
|
|
||||||
@@ -171,10 +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
|
||||||
|
- [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
|
||||||
@@ -189,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)).
|
||||||
@@ -209,19 +235,29 @@ 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**
|
||||||
|
|
||||||
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
|
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
|
||||||
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
|
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
|
||||||
|
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
|
||||||
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
|
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
|
||||||
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
|
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
|
||||||
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
|
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
|
||||||
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
|
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
|
||||||
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
|
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
|
||||||
|
|
||||||
|
**Other options**
|
||||||
|
|
||||||
|
Format: `rtsp...#{param1}#{param2}#{param3}`
|
||||||
|
|
||||||
|
- Add custom timeout `#timeout=30` (in seconds)
|
||||||
|
- Ignore audio - `#media=video` or ignore video - `#media=audio`
|
||||||
|
- Ignore two way audio API `#backchannel=0` - important for some glitchy cameras
|
||||||
|
- Use WebSocket transport `#transport=ws...`
|
||||||
|
|
||||||
**RTSP over WebSocket**
|
**RTSP over WebSocket**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -234,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:
|
||||||
@@ -265,12 +301,17 @@ streams:
|
|||||||
|
|
||||||
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
|
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
|
||||||
tcp_magic: tcp://192.168.1.123:12345
|
tcp_magic: tcp://192.168.1.123:12345
|
||||||
|
|
||||||
|
# Add custom header
|
||||||
|
custom_header: "https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX"
|
||||||
```
|
```
|
||||||
|
|
||||||
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
|
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
|
||||||
|
|
||||||
#### 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".
|
||||||
@@ -363,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
|
||||||
@@ -396,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:**
|
||||||
@@ -413,9 +470,8 @@ If you see a device but it does not have a pair button - it is paired to some ec
|
|||||||
**Important:**
|
**Important:**
|
||||||
|
|
||||||
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violation
|
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violation
|
||||||
- Audio can be transcoded by [ffmpeg](#source-ffmpeg) source with `#async` option
|
|
||||||
- Audio can be played by `ffplay` with `-use_wallclock_as_timestamps 1 -async 1` options
|
|
||||||
- Audio can't be played in `VLC` and probably any other player
|
- Audio can't be played in `VLC` and probably any other player
|
||||||
|
- Audio should be transcoded for using with MSE, WebRTC, etc.
|
||||||
|
|
||||||
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
|
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
|
||||||
|
|
||||||
@@ -423,7 +479,7 @@ Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
|
|||||||
streams:
|
streams:
|
||||||
aqara_g3:
|
aqara_g3:
|
||||||
- hass:Camera-Hub-G3-AB12
|
- hass:Camera-Hub-G3-AB12
|
||||||
- ffmpeg:aqara_g3#audio=aac#audio=opus#async
|
- ffmpeg:aqara_g3#audio=aac#audio=opus
|
||||||
```
|
```
|
||||||
|
|
||||||
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
|
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
|
||||||
@@ -432,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
|
||||||
@@ -444,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
|
||||||
@@ -453,25 +513,56 @@ 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
|
||||||
|
|
||||||
|
*[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).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
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/).
|
||||||
@@ -499,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:
|
||||||
@@ -519,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
|
||||||
@@ -530,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.
|
||||||
|
|
||||||
@@ -541,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)
|
||||||
@@ -552,21 +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.
|
||||||
|
|
||||||
**wyze**
|
**openipc** (*from [v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*)
|
||||||
|
|
||||||
|
Support connection to [OpenIPC](https://openipc.org/) cameras.
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
@@ -574,14 +677,17 @@ Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-str
|
|||||||
streams:
|
streams:
|
||||||
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
|
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
|
||||||
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
|
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
|
||||||
|
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
|
||||||
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
|
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
|
||||||
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
|
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
|
||||||
```
|
```
|
||||||
|
|
||||||
**PS.** For `wyze` and `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
|
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
|
||||||
|
|
||||||
#### 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
|
||||||
@@ -593,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
|
||||||
@@ -620,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`
|
||||||
@@ -630,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:
|
||||||
@@ -654,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**
|
||||||
@@ -666,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:
|
||||||
@@ -684,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:**
|
||||||
@@ -714,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.
|
||||||
@@ -757,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:
|
||||||
@@ -781,8 +945,64 @@ webrtc:
|
|||||||
credential: your_pass
|
credential: your_pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Module: HomeKit
|
||||||
|
|
||||||
|
*[New in v1.7.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)*
|
||||||
|
|
||||||
|
HomeKit module can work in two modes:
|
||||||
|
|
||||||
|
- export any H264 camera to Apple HomeKit
|
||||||
|
- transparent proxy any Apple HomeKit camera (Aqara, Eve, Eufy, etc.) back to Apple HomeKit, so you will have all camera features in Apple Home and also will have RTSP/WebRTC/MP4/etc. from your HomeKit camera
|
||||||
|
|
||||||
|
**Important**
|
||||||
|
|
||||||
|
- HomeKit cameras supports only H264 video and OPUS audio
|
||||||
|
|
||||||
|
**Minimal config**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
|
||||||
|
homekit:
|
||||||
|
dahua1: # same stream ID from streams list, default PIN - 19550224
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full config**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
dahua1:
|
||||||
|
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
|
||||||
|
- ffmpeg:dahua1#video=h264#hardware # if your camera doesn't support H264, important for HomeKit
|
||||||
|
- ffmpeg:dahua1#audio=opus # only OPUS audio supported by HomeKit
|
||||||
|
|
||||||
|
homekit:
|
||||||
|
dahua1: # same stream ID from streams list
|
||||||
|
pin: 12345678 # custom PIN, default: 19550224
|
||||||
|
name: Dahua camera # custom camera name, 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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Proxy HomeKit camera**
|
||||||
|
|
||||||
|
- Video stream from HomeKit camera to Apple device (iPhone, AppleTV) will be transmitted directly
|
||||||
|
- Video stream from HomeKit camera to RTSP/WebRTC/MP4/etc. will be transmitted via go2rtc
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
aqara1:
|
||||||
|
- homekit://...
|
||||||
|
- ffmpeg:aqara1#audio=aac#audio=opus # optional audio transcoding
|
||||||
|
|
||||||
|
homekit:
|
||||||
|
aqara1: # same stream ID from streams list
|
||||||
|
```
|
||||||
|
|
||||||
### 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
|
||||||
@@ -807,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:
|
||||||
@@ -845,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"
|
||||||
@@ -861,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.
|
||||||
@@ -874,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:
|
||||||
|
|
||||||
@@ -924,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.
|
||||||
@@ -1000,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.
|
||||||
|
|
||||||
@@ -1031,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
|
||||||
|
|
||||||
@@ -1068,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
|
||||||
@@ -1136,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
|
||||||
@@ -1151,7 +1380,7 @@ streams:
|
|||||||
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
|
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
|
||||||
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol realisation, many bugs in SDP
|
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol realisation, many bugs in SDP
|
||||||
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
|
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
|
||||||
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best HTTP-FLV alternative (I recommend that you contact Reolink support for new firmware), few settings
|
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings
|
||||||
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
|
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
|
||||||
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
|
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
|
||||||
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?
|
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?
|
||||||
|
|||||||
+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: 295 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,48 @@
|
|||||||
module github.com/AlexxIT/go2rtc
|
module github.com/AlexxIT/go2rtc
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brutella/hap v0.0.17
|
github.com/asticode/go-astits v1.13.0
|
||||||
github.com/deepch/vdk v0.0.19
|
github.com/expr-lang/expr v1.16.5
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/miekg/dns v1.1.55
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/pion/ice/v2 v2.3.9
|
github.com/pion/ice/v2 v2.3.24
|
||||||
github.com/pion/interceptor v0.1.17
|
github.com/pion/interceptor v0.1.29
|
||||||
github.com/pion/rtcp v1.2.10
|
github.com/pion/rtcp v1.2.14
|
||||||
github.com/pion/rtp v1.7.13
|
github.com/pion/rtp v1.8.6
|
||||||
github.com/pion/sdp/v3 v3.0.6
|
github.com/pion/sdp/v3 v3.0.9
|
||||||
github.com/pion/srtp/v2 v2.0.15
|
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.12
|
github.com/pion/webrtc/v3 v3.2.40
|
||||||
github.com/rs/zerolog v1.29.1
|
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.23.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brutella/dnssd v1.2.9 // indirect
|
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/go-chi/chi v1.5.4 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/google/uuid v1.3.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.11 // 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.7 // 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.2 // 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
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/net v0.12.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/tools v0.20.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
|
||||||
golang.org/x/tools v0.11.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
replace (
|
|
||||||
// RTP tlv8 fix
|
|
||||||
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
|
|
||||||
// fix reading AAC config bytes
|
|
||||||
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,253 +1,194 @@
|
|||||||
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
|
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||||
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
|
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
|
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
|
||||||
github.com/brutella/dnssd v1.2.9 h1:eUqO0qXZAMaFN4W4Ms1AAO/OtAbNoh9U87GAlN+1FCs=
|
|
||||||
github.com/brutella/dnssd v1.2.9/go.mod h1:yZ+GHHbGhtp5yJeKTnppdFGiy6OhiPoxs0WHW1KUcFA=
|
|
||||||
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-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
|
||||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
|
||||||
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/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.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.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
|
||||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
|
||||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
|
||||||
github.com/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.9 h1:7yZpHf3PhPxJGT4JkMj1Y8Rl5cQ6fB709iz99aeMd/U=
|
github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA=
|
||||||
github.com/pion/ice/v2 v2.3.9/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
|
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/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
|
||||||
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
|
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||||
|
github.com/pion/ice/v2 v2.3.19 h1:1GoMRTMnB6bCP4aGy2MjxK3w4laDkk+m7svJb/eqybc=
|
||||||
|
github.com/pion/ice/v2 v2.3.19/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||||
|
github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI=
|
||||||
|
github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||||
|
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||||
|
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.2 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 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
|
||||||
github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
|
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
|
github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
|
||||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
|
||||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||||
github.com/pion/srtp/v2 v2.0.15 h1:+tqRtXGsGwHC0G0IUIAzRmdkHvriF79IHVfZGfHrQoA=
|
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||||
github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw=
|
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||||
|
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||||
|
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
github.com/pion/stun v0.6.1 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.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
|
||||||
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.2 h1:wj0cAoGKltaZ790XEGW9HwoUewqjliwmhtxCuB2ApyM=
|
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||||
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
|
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||||
github.com/pion/webrtc/v3 v3.2.12 h1:pVqz5NdtTqyhKIhMcXR8bPp709kCf9blyAhDjoVRLvA=
|
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||||
github.com/pion/webrtc/v3 v3.2.12/go.mod h1:/Oz6K95CGWaN+3No+Z0NYvgOPOr3aY8UyTlMm/dec3A=
|
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.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||||
|
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||||
|
github.com/pion/webrtc/v3 v3.2.39 h1:Lf2SIMGdE3M9VNm48KpoX5pR8SJ6TsMnktzOkc/oB0o=
|
||||||
|
github.com/pion/webrtc/v3 v3.2.39/go.mod h1:AQ8p56OLbm3MjhYovYdgPuyX6oc+JcKx/HFoCGFcYzA=
|
||||||
|
github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU=
|
||||||
|
github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/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.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.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/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
|
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
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.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
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.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
|
||||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
|
||||||
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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
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.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-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-20210220032951-036812b2e83c/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/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/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-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 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
|
||||||
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/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/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-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.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||||
|
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
|
golang.org/x/term v0.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.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
|
||||||
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.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
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.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
|
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
||||||
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
|
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.2.8/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=
|
||||||
|
|||||||
+9
-5
@@ -1,9 +1,10 @@
|
|||||||
# 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 GO_VERSION="1.20-buster"
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
|
ARG GO_VERSION="1.21-bookworm"
|
||||||
ARG NGROK_VERSION="3"
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
@@ -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 / /
|
||||||
|
|
||||||
|
|||||||
+140
-64
@@ -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,15 +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)
|
||||||
listener, 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
|
||||||
|
|
||||||
@@ -72,45 +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(listener); 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 listen(network, address string) {
|
||||||
|
ln, err := net.Listen(network, address)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -170,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"`)
|
||||||
@@ -187,7 +216,7 @@ 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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -209,25 +238,72 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Stream struct {
|
func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Name string `json:"name"`
|
if r.Method != "POST" {
|
||||||
URL string `json:"url"`
|
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 ResponseStreams(w http.ResponseWriter, streams []Stream) {
|
func logHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if len(streams) == 0 {
|
switch r.Method {
|
||||||
http.Error(w, "no streams", http.StatusNotFound)
|
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 {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Info string `json:"info,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResponseSources(w http.ResponseWriter, sources []*Source) {
|
||||||
|
if len(sources) == 0 {
|
||||||
|
http.Error(w, "no sources", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = struct {
|
var response = struct {
|
||||||
Streams []Stream `json:"streams"`
|
Sources []*Source `json:"sources"`
|
||||||
}{
|
}{
|
||||||
Streams: streams,
|
Sources: sources,
|
||||||
}
|
}
|
||||||
ResponseJSON(w, response)
|
ResponseJSON(w, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Error(w http.ResponseWriter, err error) {
|
||||||
|
log.Error().Err(err).Caller(1).Send()
|
||||||
|
|
||||||
|
http.Error(w, err.Error(), http.StatusInsufficientStorage)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func configHandler(w http.ResponseWriter, r *http.Request) {
|
func configHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -40,8 +41,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// validate config
|
// validate config
|
||||||
var tmp struct{}
|
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
|
||||||
if err = yaml.Unmarshal(data, &tmp); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-10
@@ -1,15 +1,17 @@
|
|||||||
package ws
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"io"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -21,11 +23,15 @@ func Init() {
|
|||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
|
log = app.GetLogger("api")
|
||||||
|
|
||||||
initWS(cfg.Mod.Origin)
|
initWS(cfg.Mod.Origin)
|
||||||
|
|
||||||
api.HandleFunc("api/ws", apiWS)
|
api.HandleFunc("api/ws", apiWS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
// Message - struct for data exchange in Web API
|
// Message - struct for data exchange in Web API
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -101,13 +107,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr := &Transport{Request: r}
|
tr := &Transport{Request: r}
|
||||||
tr.OnWrite(func(msg any) {
|
tr.OnWrite(func(msg any) error {
|
||||||
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||||
|
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
_ = ws.WriteMessage(websocket.BinaryMessage, data)
|
return ws.WriteMessage(websocket.BinaryMessage, data)
|
||||||
} else {
|
} else {
|
||||||
_ = ws.WriteJSON(msg)
|
return ws.WriteJSON(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,11 +153,11 @@ type Transport struct {
|
|||||||
wrmx sync.Mutex
|
wrmx sync.Mutex
|
||||||
|
|
||||||
onChange func()
|
onChange func()
|
||||||
onWrite func(msg any)
|
onWrite func(msg any) error
|
||||||
onClose []func()
|
onClose []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) OnWrite(f func(msg any)) {
|
func (t *Transport) OnWrite(f func(msg any) error) {
|
||||||
t.mx.Lock()
|
t.mx.Lock()
|
||||||
if t.onChange != nil {
|
if t.onChange != nil {
|
||||||
t.onChange()
|
t.onChange()
|
||||||
@@ -162,7 +168,7 @@ func (t *Transport) OnWrite(f func(msg any)) {
|
|||||||
|
|
||||||
func (t *Transport) Write(msg any) {
|
func (t *Transport) Write(msg any) {
|
||||||
t.wrmx.Lock()
|
t.wrmx.Lock()
|
||||||
t.onWrite(msg)
|
_ = t.onWrite(msg)
|
||||||
t.wrmx.Unlock()
|
t.wrmx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,3 +206,20 @@ func (t *Transport) WithContext(f func(ctx map[any]any)) {
|
|||||||
f(t.ctx)
|
f(t.ctx)
|
||||||
t.mx.Unlock()
|
t.mx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Writer() io.Writer {
|
||||||
|
return &writer{t: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
type writer struct {
|
||||||
|
t *Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *writer) Write(p []byte) (n int, err error) {
|
||||||
|
w.t.wrmx.Lock()
|
||||||
|
if err = w.t.onWrite(p); err == nil {
|
||||||
|
n = len(p)
|
||||||
|
}
|
||||||
|
w.t.wrmx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
+38
-36
@@ -1,22 +1,21 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"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/rs/zerolog"
|
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.6.2"
|
var Version = "1.9.1"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
var ConfigPath string
|
var ConfigPath string
|
||||||
@@ -26,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,26 +100,8 @@ func Init() {
|
|||||||
modules = cfg.Mod
|
modules = cfg.Mod
|
||||||
|
|
||||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
migrateStore()
|
||||||
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) {
|
||||||
@@ -111,16 +112,20 @@ func LoadConfig(v any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLogger(module string) zerolog.Logger {
|
func PatchConfig(key string, value any, path ...string) error {
|
||||||
if s, ok := modules[module]; ok {
|
if ConfigPath == "" {
|
||||||
lvl, err := zerolog.ParseLevel(s)
|
return errors.New("config file disabled")
|
||||||
if err == nil {
|
|
||||||
return log.Level(lvl)
|
|
||||||
}
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return log.Logger
|
// empty config is OK
|
||||||
|
b, _ := os.ReadFile(ConfigPath)
|
||||||
|
|
||||||
|
b, err := yaml.Patch(b, key, value, path...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(ConfigPath, b, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
@@ -137,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
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func migrateStore() {
|
||||||
|
const name = "go2rtc.json"
|
||||||
|
|
||||||
|
data, _ := os.ReadFile(name)
|
||||||
|
if data == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var store struct {
|
||||||
|
Streams map[string]string `json:"streams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &store); err != nil {
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, url := range store.Streams {
|
||||||
|
if err := PatchConfig(id, url, "streams"); err != nil {
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.Remove(name)
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const name = "go2rtc.json"
|
|
||||||
|
|
||||||
var store map[string]any
|
|
||||||
|
|
||||||
func load() {
|
|
||||||
data, _ := os.ReadFile(name)
|
|
||||||
if data != nil {
|
|
||||||
if err := json.Unmarshal(data, &store); err != nil {
|
|
||||||
// TODO: log
|
|
||||||
log.Warn().Err(err).Msg("[app] read storage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if store == nil {
|
|
||||||
store = make(map[string]any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() error {
|
|
||||||
data, err := json.Marshal(store)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(name, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRaw(key string) any {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
return store[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDict(key string) map[string]any {
|
|
||||||
raw := GetRaw(key)
|
|
||||||
if raw != nil {
|
|
||||||
return raw.(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
return make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(key string, v any) error {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
store[key] = v
|
|
||||||
|
|
||||||
return save()
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,10 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stackSkip = [][]byte{
|
var stackSkip = [][]byte{
|
||||||
@@ -24,6 +25,9 @@ var stackSkip = [][]byte{
|
|||||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
|
||||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
|
||||||
|
|
||||||
|
// homekit
|
||||||
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
|
||||||
|
|
||||||
// webrtc/api.go
|
// webrtc/api.go
|
||||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
||||||
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
|
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
|
||||||
|
|||||||
+7
-13
@@ -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
|
||||||
@@ -45,10 +39,10 @@ func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func discover() ([]api.Stream, error) {
|
func discover() ([]*api.Source, error) {
|
||||||
addr := &net.UDPAddr{
|
addr := &net.UDPAddr{
|
||||||
Port: Port,
|
Port: Port,
|
||||||
IP: net.IP{239, 255, 255, 250},
|
IP: net.IP{239, 255, 255, 250},
|
||||||
@@ -63,7 +57,7 @@ func discover() ([]api.Stream, error) {
|
|||||||
|
|
||||||
go sendBroadcasts(conn)
|
go sendBroadcasts(conn)
|
||||||
|
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
|
|
||||||
for _, info := range getResponses(conn) {
|
for _, info := range getResponses(conn) {
|
||||||
if info.HostIP == "" || info.HostName == "" {
|
if info.HostIP == "" || info.HostName == "" {
|
||||||
@@ -75,7 +69,7 @@ func discover() ([]api.Stream, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
items = append(items, api.Stream{
|
items = append(items, &api.Source{
|
||||||
Name: info.HostName,
|
Name: info.HostName,
|
||||||
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
|
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,28 +2,28 @@ package echo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"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/shell"
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
"os/exec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
log := app.GetLogger("echo")
|
log := app.GetLogger("echo")
|
||||||
|
|
||||||
streams.HandleFunc("echo", func(url string) (core.Producer, error) {
|
streams.RedirectFunc("echo", func(url string) (string, error) {
|
||||||
args := shell.QuoteSplit(url[5:])
|
args := shell.QuoteSplit(url[5:])
|
||||||
|
|
||||||
b, err := exec.Command(args[0], args[1:]...).Output()
|
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
b = bytes.TrimSpace(b)
|
b = bytes.TrimSpace(b)
|
||||||
|
|
||||||
log.Debug().Str("url", url).Msgf("[echo] %s", b)
|
log.Debug().Str("url", url).Msgf("[echo] %s", b)
|
||||||
|
|
||||||
return streams.GetProducer(string(b))
|
return string(b), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-46
@@ -5,6 +5,14 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
@@ -12,11 +20,8 @@ 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"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -43,37 +48,44 @@ 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
|
||||||
|
var query url.Values
|
||||||
|
|
||||||
args := shell.QuoteSplit(url[5:]) // remove `exec:`
|
// RTSP flow should have `{output}` inside URL
|
||||||
for i, arg := range args {
|
// pipe flow may have `#{params}` inside URL
|
||||||
if arg == "{output}" {
|
if i := strings.Index(rawURL, "{output}"); i > 0 {
|
||||||
if rtsp.Port == "" {
|
if rtsp.Port == "" {
|
||||||
return nil, errors.New("rtsp module disabled")
|
return nil, errors.New("exec: rtsp module disabled")
|
||||||
}
|
|
||||||
|
|
||||||
sum := md5.Sum([]byte(url))
|
|
||||||
path = "/" + hex.EncodeToString(sum[:])
|
|
||||||
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sum := md5.Sum([]byte(rawURL))
|
||||||
|
path = "/" + hex.EncodeToString(sum[:])
|
||||||
|
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||||
|
} else if i = strings.IndexByte(rawURL, '#'); i > 0 {
|
||||||
|
query = streams.ParseQuery(rawURL[i+1:])
|
||||||
|
rawURL = rawURL[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
if log.Debug().Enabled() {
|
if log.Debug().Enabled() {
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return handlePipe(url, cmd)
|
return handlePipe(rawURL, cmd, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleRTSP(url, path, cmd)
|
return handleRTSP(rawURL, cmd, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
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
|
||||||
}
|
}
|
||||||
@@ -82,26 +94,31 @@ func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := magic.NewClient(r)
|
prod, err := magic.Open(r)
|
||||||
if err = client.Probe(); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
_ = r.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Desc = "exec active producer"
|
return prod, err
|
||||||
client.URL = url
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
func handleRTSP(url string, cmd *exec.Cmd, path string) (core.Producer, error) {
|
||||||
|
stderr := limitBuffer{buf: make([]byte, 512)}
|
||||||
|
|
||||||
|
if cmd.Stderr != nil {
|
||||||
|
cmd.Stderr = io.MultiWriter(cmd.Stderr, &stderr)
|
||||||
|
} else {
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
}
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := make(chan core.Producer)
|
waiter := make(chan core.Producer)
|
||||||
|
|
||||||
waitersMu.Lock()
|
waitersMu.Lock()
|
||||||
waiters[path] = ch
|
waiters[path] = waiter
|
||||||
waitersMu.Unlock()
|
waitersMu.Unlock()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -110,7 +127,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()
|
||||||
|
|
||||||
@@ -119,16 +136,9 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
chErr := make(chan error)
|
done := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
done <- cmd.Wait()
|
||||||
// unblocking write to channel
|
|
||||||
select {
|
|
||||||
case chErr <- err:
|
|
||||||
default:
|
|
||||||
log.Trace().Str("url", url).Msg("[exec] close")
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -136,9 +146,10 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
log.Error().Str("url", url).Msg("[exec] timeout")
|
||||||
return nil, errors.New("timeout")
|
return nil, errors.New("timeout")
|
||||||
case err := <-chErr:
|
case <-done:
|
||||||
return nil, fmt.Errorf("exec: %s", err)
|
// limit message size
|
||||||
case prod := <-ch:
|
return nil, errors.New("exec: " + stderr.String())
|
||||||
|
case prod := <-waiter:
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
@@ -146,6 +157,27 @@ 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
|
||||||
|
)
|
||||||
|
|
||||||
|
type limitBuffer struct {
|
||||||
|
buf []byte
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *limitBuffer) String() string {
|
||||||
|
if l.n == len(l.buf) {
|
||||||
|
return string(l.buf) + "..."
|
||||||
|
}
|
||||||
|
return string(l.buf[:l.n])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *limitBuffer) Write(p []byte) (int, error) {
|
||||||
|
if l.n < cap(l.buf) {
|
||||||
|
l.n += copy(l.buf[l.n:], p)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|||||||
+37
-7
@@ -1,26 +1,56 @@
|
|||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
return pipeCloser{stdout, cmd}, nil
|
// add buffer for pipe reader to reduce syscall
|
||||||
|
return &pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd, query}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type pipeCloser struct {
|
type pipeCloser struct {
|
||||||
io.ReadCloser
|
io.Reader
|
||||||
cmd *exec.Cmd
|
io.Closer
|
||||||
|
cmd *exec.Cmd
|
||||||
|
query url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p pipeCloser) Close() error {
|
func (p *pipeCloser) Close() error {
|
||||||
return core.Any(p.ReadCloser.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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func queryToInput(query url.Values) string {
|
func queryToInput(query url.Values) string {
|
||||||
@@ -78,7 +79,7 @@ func initDevices() {
|
|||||||
audios = append(audios, name)
|
audios = append(audios, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
streams = append(streams, api.Stream{
|
streams = append(streams, &api.Source{
|
||||||
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,8 +70,9 @@ func initDevices() {
|
|||||||
m := re.FindAllStringSubmatch(string(b), -1)
|
m := re.FindAllStringSubmatch(string(b), -1)
|
||||||
for _, i := range m {
|
for _, i := range m {
|
||||||
size, _, _ := strings.Cut(i[4], " ")
|
size, _, _ := strings.Cut(i[4], " ")
|
||||||
stream := api.Stream{
|
stream := &api.Source{
|
||||||
Name: i[3] + " | " + i[4],
|
Name: i[3],
|
||||||
|
Info: i[4],
|
||||||
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
|
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +87,9 @@ func initDevices() {
|
|||||||
|
|
||||||
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
|
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
stream := api.Stream{
|
stream := &api.Source{
|
||||||
Name: "ALSA default",
|
Name: "ALSA default",
|
||||||
|
Info: " ",
|
||||||
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
|
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func queryToInput(query url.Values) string {
|
func queryToInput(query url.Values) string {
|
||||||
@@ -44,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", "",
|
||||||
@@ -79,7 +70,7 @@ func initDevices() {
|
|||||||
name := m[1]
|
name := m[1]
|
||||||
kind := m[2]
|
kind := m[2]
|
||||||
|
|
||||||
stream := api.Stream{
|
stream := &api.Source{
|
||||||
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package device
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(bin string) {
|
func Init(bin string) {
|
||||||
@@ -39,13 +40,13 @@ func GetInput(src string) (string, error) {
|
|||||||
var Bin string
|
var Bin string
|
||||||
|
|
||||||
var videos, audios []string
|
var videos, audios []string
|
||||||
var streams []api.Stream
|
var streams []*api.Source
|
||||||
var runonce sync.Once
|
var runonce sync.Once
|
||||||
|
|
||||||
func apiDevices(w http.ResponseWriter, r *http.Request) {
|
func apiDevices(w http.ResponseWriter, r *http.Request) {
|
||||||
runonce.Do(initDevices)
|
runonce.Do(initDevices)
|
||||||
|
|
||||||
api.ResponseStreams(w, streams)
|
api.ResponseSources(w, streams)
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexToItem(items []string, index string) string {
|
func indexToItem(items []string, index string) string {
|
||||||
|
|||||||
+30
-17
@@ -1,16 +1,15 @@
|
|||||||
package ffmpeg
|
package ffmpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,12 +26,9 @@ func Init() {
|
|||||||
defaults["global"] += " -v error"
|
defaults["global"] += " -v error"
|
||||||
}
|
}
|
||||||
|
|
||||||
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
|
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
||||||
args := parseArgs(url[7:]) // remove `ffmpeg:`
|
args := parseArgs(url[7:])
|
||||||
if args == nil {
|
return "exec:" + args.String(), nil
|
||||||
return nil, errors.New("can't generate ffmpeg command")
|
|
||||||
}
|
|
||||||
return streams.GetProducer("exec:" + args.String())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
device.Init(defaults["bin"])
|
device.Init(defaults["bin"])
|
||||||
@@ -57,7 +53,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",
|
||||||
@@ -65,47 +62,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 -ar:a 48000 -ac:a 2 -application:a voip -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
|
||||||
@@ -138,7 +146,7 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var query url.Values
|
var query url.Values
|
||||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||||
query = streams.ParseQuery(s[i+1:])
|
query = streams.ParseQuery(s[i+1:])
|
||||||
args.Video = len(query["video"])
|
args.Video = len(query["video"])
|
||||||
args.Audio = len(query["audio"])
|
args.Audio = len(query["audio"])
|
||||||
@@ -186,6 +194,11 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
} else if strings.HasPrefix(s, "virtual?") {
|
||||||
|
var err error
|
||||||
|
if args.Input, err = virtual.GetInput(s[8:]); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
args.Input = inputTemplate("file", s, query)
|
args.Input = inputTemplate("file", s, query)
|
||||||
}
|
}
|
||||||
|
|||||||
+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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,11 +18,12 @@ 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) {
|
||||||
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
|
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
|
||||||
api.ResponseStreams(w, ProbeAll(bin))
|
api.ResponseSources(w, ProbeAll(bin))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +60,11 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
|||||||
args.Codecs[i] = defaults[name+"/"+engine]
|
args.Codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
if !args.HasFilters("drawtext=") {
|
if !args.HasFilters("drawtext=") {
|
||||||
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + 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=") {
|
||||||
@@ -78,7 +84,7 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
|||||||
args.InsertFilter("format=vaapi|nv12,hwupload")
|
args.InsertFilter("format=vaapi|nv12,hwupload")
|
||||||
} else {
|
} else {
|
||||||
// enable software pixel for drawtext, scale and transpose
|
// enable software pixel for drawtext, scale and transpose
|
||||||
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 " + args.Input
|
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch " + args.Input
|
||||||
|
|
||||||
args.AddFilter("hwupload")
|
args.AddFilter("hwupload")
|
||||||
}
|
}
|
||||||
@@ -120,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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
|
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
|
||||||
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
|
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
|
||||||
|
|
||||||
func ProbeAll(bin string) []api.Stream {
|
func ProbeAll(bin string) []*api.Source {
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeVideoToolboxH264),
|
Name: runToString(bin, ProbeVideoToolboxH264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"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.Stream {
|
func ProbeAll(bin string) []*api.Source {
|
||||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeV4L2M2MH264),
|
Name: runToString(bin, ProbeV4L2M2MH264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
|
||||||
@@ -24,10 +29,18 @@ func ProbeAll(bin string) []api.Stream {
|
|||||||
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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeVAAPIH264),
|
Name: runToString(bin, ProbeVAAPIH264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
|
||||||
@@ -58,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
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg
|
|||||||
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||||
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||||
|
|
||||||
func ProbeAll(bin string) []api.Stream {
|
func ProbeAll(bin string) []*api.Source {
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeDXVA2H264),
|
Name: runToString(bin, ProbeDXVA2H264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
|
||||||
|
|||||||
+19
-5
@@ -12,22 +12,36 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TranscodeToJPEG(b []byte, query url.Values) ([]byte, error) {
|
func JPEGWithQuery(b []byte, query url.Values) ([]byte, error) {
|
||||||
ffmpegArgs := parseQuery(query)
|
args := parseQuery(query)
|
||||||
cmdArgs := shell.QuoteSplit(ffmpegArgs.String())
|
return transcode(b, args.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func JPEGWithScale(b []byte, width, height int) ([]byte, error) {
|
||||||
|
args := defaultArgs()
|
||||||
|
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
|
||||||
|
return transcode(b, args.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func transcode(b []byte, args string) ([]byte, error) {
|
||||||
|
cmdArgs := shell.QuoteSplit(args)
|
||||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||||
cmd.Stdin = bytes.NewBuffer(b)
|
cmd.Stdin = bytes.NewBuffer(b)
|
||||||
return cmd.Output()
|
return cmd.Output()
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseQuery(query url.Values) *ffmpeg.Args {
|
func defaultArgs() *ffmpeg.Args {
|
||||||
args := &ffmpeg.Args{
|
return &ffmpeg.Args{
|
||||||
Bin: defaults["bin"],
|
Bin: defaults["bin"],
|
||||||
Global: defaults["global"],
|
Global: defaults["global"],
|
||||||
Input: "-i -",
|
Input: "-i -",
|
||||||
Codecs: []string{defaults["mjpeg"]},
|
Codecs: []string{defaults["mjpeg"]},
|
||||||
Output: defaults["output/mjpeg"],
|
Output: defaults["output/mjpeg"],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQuery(query url.Values) *ffmpeg.Args {
|
||||||
|
args := defaultArgs()
|
||||||
|
|
||||||
var width = -1
|
var width = -1
|
||||||
var height = -1
|
var height = -1
|
||||||
|
|||||||
@@ -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,59 @@
|
|||||||
|
package virtual
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetInput(src string) (string, error) {
|
||||||
|
query, err := url.ParseQuery(src)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set defaults (using Add instead of Set)
|
||||||
|
query.Add("source", "testsrc")
|
||||||
|
query.Add("size", "1920x1080")
|
||||||
|
query.Add("decimals", "2")
|
||||||
|
|
||||||
|
// https://ffmpeg.org/ffmpeg-filters.html
|
||||||
|
source := query.Get("source")
|
||||||
|
input := "-re -f lavfi -i " + source
|
||||||
|
|
||||||
|
sep := "=" // first separator
|
||||||
|
for key, values := range query {
|
||||||
|
value := values[0]
|
||||||
|
|
||||||
|
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
|
||||||
|
switch key {
|
||||||
|
case "color", "rate", "duration", "sar":
|
||||||
|
case "size":
|
||||||
|
switch value {
|
||||||
|
case "720":
|
||||||
|
value = "1280x720"
|
||||||
|
case "1080":
|
||||||
|
value = "1920x1080"
|
||||||
|
case "2K":
|
||||||
|
value = "2560x1440"
|
||||||
|
case "4K":
|
||||||
|
value = "3840x2160"
|
||||||
|
case "8K":
|
||||||
|
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
|
||||||
|
}
|
||||||
|
case "decimals":
|
||||||
|
if source != "testsrc" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
input += sep + key + "=" + value
|
||||||
|
sep = ":" // next separator
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := query.Get("format"); s != "" {
|
||||||
|
input += ",format=" + s
|
||||||
|
}
|
||||||
|
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+28
-22
@@ -4,6 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||||
@@ -11,10 +16,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hass"
|
"github.com/AlexxIT/go2rtc/pkg/hass"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -36,6 +37,24 @@ func Init() {
|
|||||||
api.HandleFunc("/streams", apiOK)
|
api.HandleFunc("/streams", apiOK)
|
||||||
api.HandleFunc("/stream/", apiStream)
|
api.HandleFunc("/stream/", apiStream)
|
||||||
|
|
||||||
|
streams.RedirectFunc("hass", func(url string) (string, error) {
|
||||||
|
if location := entities[url[5:]]; location != "" {
|
||||||
|
return location, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
||||||
|
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
||||||
|
client, err := hass.NewClient(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
})
|
||||||
|
|
||||||
// load static entries from Hass config
|
// load static entries from Hass config
|
||||||
if err := importConfig(conf.Mod.Config); err != nil {
|
if err := importConfig(conf.Mod.Config); err != nil {
|
||||||
log.Debug().Msgf("[hass] can't import config: %s", err)
|
log.Debug().Msgf("[hass] can't import config: %s", err)
|
||||||
@@ -56,26 +75,13 @@ func Init() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
for name, url := range entities {
|
for name, url := range entities {
|
||||||
items = append(items, api.Stream{Name: name, URL: url})
|
items = append(items, &api.Source{
|
||||||
|
Name: name, URL: "hass:" + name, Location: url,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
})
|
|
||||||
|
|
||||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
|
||||||
// check entity by name
|
|
||||||
if url2 := entities[url[5:]]; url2 != "" {
|
|
||||||
return streams.GetProducer(url2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
|
||||||
client, err := hass.NewClient(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// for Addon listen on hassio interface, so WebUI feature will work
|
// for Addon listen on hassio interface, so WebUI feature will work
|
||||||
|
|||||||
+21
-82
@@ -2,7 +2,6 @@ package hls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -33,21 +32,12 @@ func Init() {
|
|||||||
ws.HandleFunc("hls", handlerWSHLS)
|
ws.HandleFunc("hls", handlerWSHLS)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Consumer interface {
|
|
||||||
core.Consumer
|
|
||||||
Listen(f core.EventFunc)
|
|
||||||
Init() ([]byte, error)
|
|
||||||
MimeCodecs() string
|
|
||||||
Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
const keepalive = 5 * time.Second
|
const keepalive = 5 * time.Second
|
||||||
|
|
||||||
var sessions = map[string]*Session{}
|
|
||||||
|
|
||||||
// once I saw 404 on MP4 segment, so better to use mutex
|
// once I saw 404 on MP4 segment, so better to use mutex
|
||||||
|
var sessions = map[string]*Session{}
|
||||||
var sessionsMu sync.RWMutex
|
var sessionsMu sync.RWMutex
|
||||||
|
|
||||||
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -67,22 +57,22 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var cons Consumer
|
var cons core.Consumer
|
||||||
|
|
||||||
// use fMP4 with codecs filter and TS without
|
// use fMP4 with codecs filter and TS without
|
||||||
medias := mp4.ParseQuery(r.URL.Query())
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
if medias != nil {
|
if medias != nil {
|
||||||
cons = &mp4.Consumer{
|
c := mp4.NewConsumer(medias)
|
||||||
Desc: "HLS/HTTP",
|
c.Type = "HLS/fMP4 consumer"
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
c.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
UserAgent: r.UserAgent(),
|
c.UserAgent = r.UserAgent()
|
||||||
Medias: medias,
|
cons = c
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
cons = &mpegts.Consumer{
|
c := mpegts.NewConsumer()
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
c.Type = "HLS/TS consumer"
|
||||||
UserAgent: r.UserAgent(),
|
c.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
}
|
c.UserAgent = r.UserAgent()
|
||||||
|
cons = c
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
@@ -90,63 +80,22 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session := &Session{cons: cons}
|
session := NewSession(cons)
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
session.mu.Lock()
|
|
||||||
session.buffer = append(session.buffer, data...)
|
|
||||||
session.mu.Unlock()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sid := core.RandString(8, 62)
|
|
||||||
|
|
||||||
session.alive = time.AfterFunc(keepalive, func() {
|
session.alive = time.AfterFunc(keepalive, func() {
|
||||||
sessionsMu.Lock()
|
sessionsMu.Lock()
|
||||||
delete(sessions, sid)
|
delete(sessions, session.id)
|
||||||
sessionsMu.Unlock()
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
session.init, _ = cons.Init()
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
// two segments important for Chromecast
|
|
||||||
if medias != nil {
|
|
||||||
session.template = `#EXTM3U
|
|
||||||
#EXT-X-VERSION:6
|
|
||||||
#EXT-X-TARGETDURATION:1
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:%d
|
|
||||||
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d`
|
|
||||||
} else {
|
|
||||||
session.template = `#EXTM3U
|
|
||||||
#EXT-X-VERSION:3
|
|
||||||
#EXT-X-TARGETDURATION:1
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.ts?id=` + sid + `&n=%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.ts?id=` + sid + `&n=%d`
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionsMu.Lock()
|
sessionsMu.Lock()
|
||||||
sessions[sid] = session
|
sessions[session.id] = session
|
||||||
sessionsMu.Unlock()
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, "fLaC", 1)
|
go session.Run()
|
||||||
|
|
||||||
// bandwidth important for Safari, codecs useful for smooth playback
|
if _, err := w.Write(session.Main()); err != nil {
|
||||||
data := []byte(`#EXTM3U
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
|
|
||||||
hls/playlist.m3u8?id=` + sid)
|
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +118,7 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := w.Write([]byte(session.Playlist())); err != nil {
|
if _, err := w.Write(session.Playlist()); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,11 +173,8 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := session.init
|
data := session.Init()
|
||||||
session.init = nil
|
if data == nil {
|
||||||
|
|
||||||
session.segment0 = session.Segment()
|
|
||||||
if session.segment0 == nil {
|
|
||||||
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
|
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
@@ -261,14 +207,7 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
session.alive.Reset(keepalive)
|
session.alive.Reset(keepalive)
|
||||||
|
|
||||||
var data []byte
|
data := session.Segment()
|
||||||
|
|
||||||
if query.Get("n") != "0" {
|
|
||||||
data = session.Segment()
|
|
||||||
} else {
|
|
||||||
data = session.segment0
|
|
||||||
}
|
|
||||||
|
|
||||||
if data == nil {
|
if data == nil {
|
||||||
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
|
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|||||||
+92
-6
@@ -2,23 +2,105 @@ package hls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
cons Consumer
|
cons core.Consumer
|
||||||
|
id string
|
||||||
template string
|
template string
|
||||||
init []byte
|
init []byte
|
||||||
segment0 []byte
|
|
||||||
buffer []byte
|
buffer []byte
|
||||||
seq int
|
seq int
|
||||||
alive *time.Timer
|
alive *time.Timer
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) Playlist() string {
|
func NewSession(cons core.Consumer) *Session {
|
||||||
return fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1)
|
s := &Session{
|
||||||
|
id: core.RandString(8, 62),
|
||||||
|
cons: cons,
|
||||||
|
}
|
||||||
|
|
||||||
|
// two segments important for Chromecast
|
||||||
|
if _, ok := cons.(*mp4.Consumer); ok {
|
||||||
|
s.template = `#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
#EXT-X-TARGETDURATION:1
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:%d
|
||||||
|
#EXT-X-MAP:URI="init.mp4?id=` + s.id + `"
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.m4s?id=` + s.id + `&n=%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.m4s?id=` + s.id + `&n=%d`
|
||||||
|
} else {
|
||||||
|
s.template = `#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:1
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.ts?id=` + s.id + `&n=%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.ts?id=` + s.id + `&n=%d`
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Write(p []byte) (n int, err error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.init == nil {
|
||||||
|
s.init = p
|
||||||
|
} else {
|
||||||
|
s.buffer = append(s.buffer, p...)
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Run() {
|
||||||
|
_, _ = s.cons.(io.WriterTo).WriteTo(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Main() []byte {
|
||||||
|
type withCodecs interface {
|
||||||
|
Codecs() []*core.Codec
|
||||||
|
}
|
||||||
|
|
||||||
|
codecs := mp4.MimeCodecs(s.cons.(withCodecs).Codecs())
|
||||||
|
codecs = strings.Replace(codecs, mp4.MimeFlac, "fLaC", 1)
|
||||||
|
|
||||||
|
// bandwidth important for Safari, codecs useful for smooth playback
|
||||||
|
return []byte(`#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
|
||||||
|
hls/playlist.m3u8?id=` + s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Playlist() []byte {
|
||||||
|
return []byte(fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Init() (init []byte) {
|
||||||
|
for i := 0; i < 60 && init == nil; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
// return init only when have some buffer
|
||||||
|
if len(s.buffer) > 0 {
|
||||||
|
init = s.init
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) Segment() (segment []byte) {
|
func (s *Session) Segment() (segment []byte) {
|
||||||
@@ -30,8 +112,12 @@ func (s *Session) Segment() (segment []byte) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if len(s.buffer) > 0 {
|
if len(s.buffer) > 0 {
|
||||||
segment = s.buffer
|
segment = s.buffer
|
||||||
// for TS important to start new segment with init
|
if _, ok := s.cons.(*mp4.Consumer); ok {
|
||||||
s.buffer = s.init
|
s.buffer = nil
|
||||||
|
} else {
|
||||||
|
// for TS important to start new segment with init
|
||||||
|
s.buffer = s.init
|
||||||
|
}
|
||||||
s.seq++
|
s.seq++
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|||||||
+14
-42
@@ -2,13 +2,11 @@ package hls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
)
|
)
|
||||||
@@ -20,63 +18,37 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
codecs := msg.String()
|
codecs := msg.String()
|
||||||
|
medias := mp4.ParseCodecs(codecs, true)
|
||||||
|
cons := mp4.NewConsumer(medias)
|
||||||
|
cons.Type = "HLS/fMP4 consumer"
|
||||||
|
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||||
|
cons.UserAgent = tr.Request.UserAgent()
|
||||||
|
|
||||||
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
||||||
|
|
||||||
cons := &mp4.Consumer{
|
|
||||||
Desc: "HLS/WebSocket",
|
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
|
||||||
UserAgent: tr.Request.UserAgent(),
|
|
||||||
Medias: mp4.ParseCodecs(codecs, true),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
session := &Session{cons: cons}
|
session := NewSession(cons)
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
session.mu.Lock()
|
|
||||||
session.buffer = append(session.buffer, data...)
|
|
||||||
session.mu.Unlock()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
session.alive = time.AfterFunc(keepalive, func() {
|
session.alive = time.AfterFunc(keepalive, func() {
|
||||||
|
sessionsMu.Lock()
|
||||||
|
delete(sessions, session.id)
|
||||||
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
session.init, _ = cons.Init()
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
sid := core.RandString(8, 62)
|
|
||||||
|
|
||||||
// two segments important for Chromecast
|
|
||||||
session.template = `#EXTM3U
|
|
||||||
#EXT-X-VERSION:6
|
|
||||||
#EXT-X-TARGETDURATION:1
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:%d
|
|
||||||
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d`
|
|
||||||
|
|
||||||
sessionsMu.Lock()
|
sessionsMu.Lock()
|
||||||
sessions[sid] = session
|
sessions[session.id] = session
|
||||||
sessionsMu.Unlock()
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
codecs = strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, "fLaC", 1)
|
go session.Run()
|
||||||
|
|
||||||
// bandwidth important for Safari, codecs useful for smooth playback
|
main := session.Main()
|
||||||
data := `#EXTM3U
|
tr.Write(&ws.Message{Type: "hls", Value: string(main)})
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
|
|
||||||
hls/playlist.m3u8?id=` + sid
|
|
||||||
|
|
||||||
tr.Write(&ws.Message{Type: "hls", Value: data})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-100
@@ -1,136 +1,139 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app/store"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "GET":
|
case "GET":
|
||||||
items := make([]any, 0)
|
sources, err := discovery()
|
||||||
|
|
||||||
for name, src := range store.GetDict("streams") {
|
|
||||||
if src := src.(string); strings.HasPrefix(src, "homekit") {
|
|
||||||
u, err := url.Parse(src)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
device := Device{
|
|
||||||
Name: name,
|
|
||||||
Addr: u.Host,
|
|
||||||
Paired: true,
|
|
||||||
}
|
|
||||||
items = append(items, device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
|
||||||
if entry.Complete() {
|
|
||||||
device := Device{
|
|
||||||
Name: entry.Name,
|
|
||||||
Addr: entry.Addr(),
|
|
||||||
ID: entry.Info["id"],
|
|
||||||
Model: entry.Info["md"],
|
|
||||||
Paired: entry.Info["sf"] == "0",
|
|
||||||
}
|
|
||||||
items = append(items, device)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
api.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ResponseJSON(w, items)
|
urls := findHomeKitURLs()
|
||||||
|
for id, u := range urls {
|
||||||
|
deviceID := u.Query().Get("device_id")
|
||||||
|
for _, source := range sources {
|
||||||
|
if strings.Contains(source.URL, deviceID) {
|
||||||
|
source.Location = id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, source := range sources {
|
||||||
|
if source.Location == "" {
|
||||||
|
source.Location = " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ResponseSources(w, sources)
|
||||||
|
|
||||||
case "POST":
|
case "POST":
|
||||||
// TODO: post params...
|
if err := r.ParseMultipartForm(1024); err != nil {
|
||||||
|
api.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
id := r.URL.Query().Get("id")
|
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
|
||||||
pin := r.URL.Query().Get("pin")
|
api.Error(w, err)
|
||||||
name := r.URL.Query().Get("name")
|
|
||||||
if err := hkPair(id, pin, name); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
src := r.URL.Query().Get("src")
|
if err := r.ParseMultipartForm(1024); err != nil {
|
||||||
if err := hkDelete(src); err != nil {
|
api.Error(w, err)
|
||||||
log.Error().Err(err).Caller().Send()
|
return
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
}
|
||||||
|
|
||||||
|
if err := apiUnpair(r.Form.Get("id")); err != nil {
|
||||||
|
api.Error(w, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hkPair(deviceID, pin, name string) (err error) {
|
func discovery() ([]*api.Source, error) {
|
||||||
var conn *hap.Conn
|
var sources []*api.Source
|
||||||
|
|
||||||
if conn, err = hap.Pair(deviceID, pin); err != nil {
|
// 1. Get streams from Discovery
|
||||||
return
|
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||||
}
|
log.Trace().Msgf("[homekit] mdns=%s", entry)
|
||||||
|
|
||||||
streams.New(name, conn.URL())
|
category := entry.Info[hap.TXTCategory]
|
||||||
|
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
|
||||||
dict := store.GetDict("streams")
|
source := &api.Source{
|
||||||
dict[name] = conn.URL()
|
Name: entry.Name,
|
||||||
|
Info: entry.Info[hap.TXTModel],
|
||||||
return store.Set("streams", dict)
|
URL: fmt.Sprintf(
|
||||||
}
|
"homekit://%s:%d?device_id=%s&feature=%s&status=%s",
|
||||||
|
entry.IP, entry.Port, entry.Info[hap.TXTDeviceID],
|
||||||
func hkDelete(name string) (err error) {
|
entry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags],
|
||||||
dict := store.GetDict("streams")
|
),
|
||||||
for key, rawURL := range dict {
|
|
||||||
if key != name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var conn *hap.Conn
|
|
||||||
|
|
||||||
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err = conn.Handle(); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
if err = conn.ListPairings(); err != nil {
|
sources = append(sources, source)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
if err = conn.DeletePairing(conn.ClientID); err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
delete(dict, name)
|
|
||||||
|
|
||||||
return store.Set("streams", dict)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return sources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
func apiPair(id, url string) error {
|
||||||
ID string `json:"id"`
|
conn, err := hap.Pair(url)
|
||||||
Name string `json:"name"`
|
if err != nil {
|
||||||
Addr string `json:"addr"`
|
return err
|
||||||
Model string `json:"model"`
|
}
|
||||||
Paired bool `json:"paired"`
|
|
||||||
//Type string `json:"type"`
|
streams.New(id, conn.URL())
|
||||||
|
|
||||||
|
return app.PatchConfig(id, conn.URL(), "streams")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiUnpair(id string) error {
|
||||||
|
stream := streams.Get(id)
|
||||||
|
if stream == nil {
|
||||||
|
return errors.New(api.StreamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawURL := findHomeKitURL(stream)
|
||||||
|
if rawURL == "" {
|
||||||
|
return errors.New("not homekit source")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hap.Unpair(rawURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.Delete(id)
|
||||||
|
|
||||||
|
return app.PatchConfig(id, nil, "streams")
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHomeKitURLs() map[string]*url.URL {
|
||||||
|
urls := map[string]*url.URL{}
|
||||||
|
for id, stream := range streams.Streams() {
|
||||||
|
if rawURL := findHomeKitURL(stream); rawURL != "" {
|
||||||
|
if u, err := url.Parse(rawURL); err == nil {
|
||||||
|
urls[id] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-7
@@ -1,32 +1,202 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||||
"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/hap"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod map[string]struct {
|
||||||
|
Pin string `json:"pin"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
DevicePrivate string `json:"device_private"`
|
||||||
|
Pairings []string `json:"pairings"`
|
||||||
|
//Listen string `json:"listen"`
|
||||||
|
} `yaml:"homekit"`
|
||||||
|
}
|
||||||
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
log = app.GetLogger("homekit")
|
log = app.GetLogger("homekit")
|
||||||
|
|
||||||
streams.HandleFunc("homekit", streamHandler)
|
streams.HandleFunc("homekit", streamHandler)
|
||||||
|
|
||||||
api.HandleFunc("api/homekit", apiHandler)
|
api.HandleFunc("api/homekit", apiHandler)
|
||||||
|
|
||||||
|
if cfg.Mod == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
servers = map[string]*server{}
|
||||||
|
var entries []*mdns.ServiceEntry
|
||||||
|
|
||||||
|
for id, conf := range cfg.Mod {
|
||||||
|
stream := streams.Get(id)
|
||||||
|
if stream == nil {
|
||||||
|
log.Warn().Msgf("[homekit] missing stream: %s", id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Pin == "" {
|
||||||
|
conf.Pin = "19550224" // default PIN
|
||||||
|
}
|
||||||
|
|
||||||
|
pin, err := hap.SanitizePin(conf.Pin)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
|
||||||
|
name := calcName(conf.Name, deviceID)
|
||||||
|
|
||||||
|
srv := &server{
|
||||||
|
stream: id,
|
||||||
|
srtp: srtp.Server,
|
||||||
|
pairings: conf.Pairings,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.hap = &hap.Server{
|
||||||
|
Pin: pin,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
|
||||||
|
GetPair: srv.GetPair,
|
||||||
|
AddPair: srv.AddPair,
|
||||||
|
Handler: homekit.ServerHandler(srv),
|
||||||
|
}
|
||||||
|
|
||||||
|
if url := findHomeKitURL(stream); url != "" {
|
||||||
|
// 1. Act as transparent proxy for HomeKit camera
|
||||||
|
dial := func() (net.Conn, error) {
|
||||||
|
client, err := homekit.Dial(url, srtp.Server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.Conn(), nil
|
||||||
|
}
|
||||||
|
srv.hap.Handler = homekit.ProxyHandler(srv, dial)
|
||||||
|
} else {
|
||||||
|
// 2. Act as basic HomeKit camera
|
||||||
|
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
|
||||||
|
srv.hap.Handler = homekit.ServerHandler(srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.mdns = &mdns.ServiceEntry{
|
||||||
|
Name: name,
|
||||||
|
Port: uint16(api.Port),
|
||||||
|
Info: map[string]string{
|
||||||
|
hap.TXTConfigNumber: "1",
|
||||||
|
hap.TXTFeatureFlags: "0",
|
||||||
|
hap.TXTDeviceID: deviceID,
|
||||||
|
hap.TXTModel: app.UserAgent,
|
||||||
|
hap.TXTProtoVersion: "1.1",
|
||||||
|
hap.TXTStateNumber: "1",
|
||||||
|
hap.TXTStatusFlags: hap.StatusNotPaired,
|
||||||
|
hap.TXTCategory: hap.CategoryCamera,
|
||||||
|
hap.TXTSetupHash: srv.hap.SetupHash(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
entries = append(entries, srv.mdns)
|
||||||
|
|
||||||
|
srv.UpdateStatus()
|
||||||
|
|
||||||
|
host := srv.mdns.Host(mdns.ServiceHAP)
|
||||||
|
servers[host] = srv
|
||||||
|
}
|
||||||
|
|
||||||
|
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
|
||||||
|
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
|
||||||
|
|
||||||
|
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
var servers map[string]*server
|
||||||
|
|
||||||
func streamHandler(url string) (core.Producer, error) {
|
func streamHandler(url string) (core.Producer, error) {
|
||||||
conn, err := homekit.NewClient(url, srtp.Server)
|
if srtp.Server == nil {
|
||||||
if err != nil {
|
return nil, errors.New("homekit: can't work without SRTP server")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
return homekit.Dial(url, srtp.Server)
|
||||||
}
|
}
|
||||||
return conn, nil
|
|
||||||
|
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
srv, ok := servers[r.Host]
|
||||||
|
if !ok {
|
||||||
|
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if err = srv.hap.PairSetup(r, rw, conn); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hapPairVerify(w http.ResponseWriter, r *http.Request) {
|
||||||
|
srv, ok := servers[r.Host]
|
||||||
|
if !ok {
|
||||||
|
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHomeKitURL(stream *streams.Stream) string {
|
||||||
|
sources := stream.Sources()
|
||||||
|
if len(sources) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
url := sources[0]
|
||||||
|
if strings.HasPrefix(url, "homekit") {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(url, "hass") {
|
||||||
|
location, _ := streams.Location(url)
|
||||||
|
if strings.HasPrefix(location, "homekit") {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package homekit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||||
|
srtp2 "github.com/AlexxIT/go2rtc/internal/srtp"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
stream string // stream name from YAML
|
||||||
|
hap *hap.Server // server for HAP connection and encryption
|
||||||
|
mdns *mdns.ServiceEntry
|
||||||
|
srtp *srtp.Server
|
||||||
|
accessory *hap.Accessory // HAP accessory
|
||||||
|
pairings []string // pairings list
|
||||||
|
|
||||||
|
streams map[string]*homekit.Consumer
|
||||||
|
consumer *homekit.Consumer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) UpdateStatus() {
|
||||||
|
// true status is important, or device may be offline in Apple Home
|
||||||
|
if len(s.pairings) == 0 {
|
||||||
|
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired
|
||||||
|
} else {
|
||||||
|
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {
|
||||||
|
return []*hap.Accessory{s.accessory}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
|
||||||
|
log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid)
|
||||||
|
|
||||||
|
char := s.accessory.GetCharacterByID(iid)
|
||||||
|
if char == nil {
|
||||||
|
log.Warn().Msgf("[homekit] get unknown characteristic: %d", iid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch char.Type {
|
||||||
|
case camera.TypeSetupEndpoints:
|
||||||
|
if s.consumer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
answer := s.consumer.GetAnswer()
|
||||||
|
v, err := tlv8.MarshalBase64(answer)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return char.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
|
||||||
|
log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value)
|
||||||
|
|
||||||
|
char := s.accessory.GetCharacterByID(iid)
|
||||||
|
if char == nil {
|
||||||
|
log.Warn().Msgf("[homekit] set unknown characteristic: %d", iid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch char.Type {
|
||||||
|
case camera.TypeSetupEndpoints:
|
||||||
|
var offer camera.SetupEndpoints
|
||||||
|
if err := tlv8.UnmarshalBase64(value.(string), &offer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
|
||||||
|
s.consumer.SetOffer(&offer)
|
||||||
|
|
||||||
|
case camera.TypeSelectedStreamConfiguration:
|
||||||
|
var conf camera.SelectedStreamConfig
|
||||||
|
if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command)
|
||||||
|
|
||||||
|
switch conf.Control.Command {
|
||||||
|
case camera.SessionCommandEnd:
|
||||||
|
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
|
||||||
|
_ = consumer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
case camera.SessionCommandStart:
|
||||||
|
if s.consumer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.consumer.SetConfig(&conf) {
|
||||||
|
log.Warn().Msgf("[homekit] wrong config")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.streams == nil {
|
||||||
|
s.streams = map[string]*homekit.Consumer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.streams[conf.Control.SessionID] = s.consumer
|
||||||
|
|
||||||
|
stream := streams.Get(s.stream)
|
||||||
|
if err := stream.AddConsumer(s.consumer); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, _ = s.consumer.WriteTo(nil)
|
||||||
|
stream.RemoveConsumer(s.consumer)
|
||||||
|
|
||||||
|
delete(s.streams, conf.Control.SessionID)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
|
||||||
|
log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height)
|
||||||
|
|
||||||
|
stream := streams.Get(s.stream)
|
||||||
|
cons := magic.NewKeyframe()
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
once := &core.OnceBuffer{} // init and first frame
|
||||||
|
_, _ = cons.WriteTo(once)
|
||||||
|
b := once.Buffer()
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
|
switch cons.CodecName() {
|
||||||
|
case core.CodecH264, core.CodecH265:
|
||||||
|
var err error
|
||||||
|
if b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) GetPair(conn net.Conn, id string) []byte {
|
||||||
|
log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id)
|
||||||
|
|
||||||
|
for _, pairing := range s.pairings {
|
||||||
|
if !strings.Contains(pairing, id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := url.ParseQuery(pairing)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Get("client_id") != id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s := query.Get("client_public")
|
||||||
|
b, _ := hex.DecodeString(s)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) {
|
||||||
|
log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions)
|
||||||
|
|
||||||
|
query := url.Values{
|
||||||
|
"client_id": []string{id},
|
||||||
|
"client_public": []string{hex.EncodeToString(public)},
|
||||||
|
"permissions": []string{string('0' + permissions)},
|
||||||
|
}
|
||||||
|
if s.GetPair(conn, id) == nil {
|
||||||
|
s.pairings = append(s.pairings, query.Encode())
|
||||||
|
s.UpdateStatus()
|
||||||
|
s.PatchConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) DelPair(conn net.Conn, id string) {
|
||||||
|
log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id)
|
||||||
|
|
||||||
|
id = "client_id=" + id
|
||||||
|
for i, pairing := range s.pairings {
|
||||||
|
if !strings.Contains(pairing, id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
|
||||||
|
s.UpdateStatus()
|
||||||
|
s.PatchConfig()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) PatchConfig() {
|
||||||
|
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
|
||||||
|
log.Error().Err(err).Msgf(
|
||||||
|
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcName(name, seed string) string {
|
||||||
|
if name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
b := sha512.Sum512([]byte(seed))
|
||||||
|
return fmt.Sprintf("go2rtc-%02X%02X", b[0], b[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcDeviceID(deviceID, seed string) string {
|
||||||
|
if deviceID != "" {
|
||||||
|
if len(deviceID) >= 17 {
|
||||||
|
// 1. Returd device_id as is (ex. AA:BB:CC:DD:EE:FF)
|
||||||
|
return deviceID
|
||||||
|
}
|
||||||
|
// 2. Use device_id as seed if not zero
|
||||||
|
seed = deviceID
|
||||||
|
}
|
||||||
|
b := sha512.Sum512([]byte(seed))
|
||||||
|
return fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", b[32], b[34], b[36], b[38], b[40], b[42])
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcDevicePrivate(private, seed string) []byte {
|
||||||
|
if private != "" {
|
||||||
|
// 1. Decode private from HEX string
|
||||||
|
if b, _ := hex.DecodeString(private); len(b) == ed25519.PrivateKeySize {
|
||||||
|
// 2. Return if OK
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
// 3. Use private as seed if not zero
|
||||||
|
seed = private
|
||||||
|
}
|
||||||
|
b := sha512.Sum512([]byte(seed))
|
||||||
|
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
|
||||||
|
}
|
||||||
+59
-37
@@ -2,17 +2,19 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hls"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/multipart"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -21,15 +23,28 @@ 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(url string) (core.Producer, error) {
|
func handleHTTP(rawURL string) (core.Producer, error) {
|
||||||
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
|
||||||
// first we get the Content-Type to define supported producer
|
// first we get the Content-Type to define supported producer
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", rawURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rawQuery != "" {
|
||||||
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
|
||||||
|
for _, header := range query["header"] {
|
||||||
|
key, value, _ := strings.Cut(header, ":")
|
||||||
|
req.Header.Add(key, strings.TrimSpace(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res, err := tcp.Do(req)
|
res, err := tcp.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -39,37 +54,29 @@ func handleHTTP(url string) (core.Producer, error) {
|
|||||||
return nil, errors.New(res.Status)
|
return nil, errors.New(res.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Guess format from content type
|
||||||
ct := res.Header.Get("Content-Type")
|
ct := res.Header.Get("Content-Type")
|
||||||
if i := strings.IndexByte(ct, ';'); i > 0 {
|
if i := strings.IndexByte(ct, ';'); i > 0 {
|
||||||
ct = ct[:i]
|
ct = ct[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ct {
|
var ext string
|
||||||
case "image/jpeg", "multipart/x-mixed-replace":
|
if i := strings.LastIndexByte(req.URL.Path, '.'); i > 0 {
|
||||||
|
ext = req.URL.Path[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ct == "image/jpeg":
|
||||||
return mjpeg.NewClient(res), nil
|
return mjpeg.NewClient(res), nil
|
||||||
|
|
||||||
case "video/x-flv":
|
case ct == "multipart/x-mixed-replace":
|
||||||
var conn *rtmp.Client
|
return multipart.Open(res.Body)
|
||||||
if conn, err = rtmp.Accept(res); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = conn.Describe(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
|
|
||||||
default: // "video/mpeg":
|
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
|
||||||
|
return hls.OpenURL(req.URL, res.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := magic.NewClient(res.Body)
|
return magic.Open(res.Body)
|
||||||
if err = client.Probe(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Desc = "HTTP active producer"
|
|
||||||
client.URL = url
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTCP(rawURL string) (core.Producer, error) {
|
func handleTCP(rawURL string) (core.Producer, error) {
|
||||||
@@ -78,18 +85,33 @@ func handleTCP(rawURL string) (core.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.DialTimeout("tcp", u.Host, time.Second*3)
|
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := magic.NewClient(conn)
|
return magic.Open(conn)
|
||||||
if err = client.Probe(); err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
|
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.Desc = "TCP active producer"
|
client, err := magic.Open(r.Body)
|
||||||
client.URL = rawURL
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
return client, nil
|
stream.AddProducer(client)
|
||||||
|
defer stream.RemoveProducer(client)
|
||||||
|
|
||||||
|
if err = client.Start(); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-52
@@ -33,27 +33,18 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := make(chan []byte)
|
cons := magic.NewKeyframe()
|
||||||
|
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
cons := &magic.Keyframe{
|
cons.UserAgent = r.UserAgent()
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
|
||||||
UserAgent: r.UserAgent(),
|
|
||||||
}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if b, ok := msg.([]byte); ok {
|
|
||||||
select {
|
|
||||||
case exit <- b:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := <-exit
|
once := &core.OnceBuffer{} // init and first frame
|
||||||
|
_, _ = cons.WriteTo(once)
|
||||||
|
b := once.Buffer()
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
@@ -61,27 +52,27 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
case core.CodecH264, core.CodecH265:
|
case core.CodecH264, core.CodecH265:
|
||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
var err error
|
var err error
|
||||||
if data, err = ffmpeg.TranscodeToJPEG(data, r.URL.Query()); err != nil {
|
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
|
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
|
||||||
|
case core.CodecJPEG:
|
||||||
|
b = mjpeg.FixJPEG(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Set("Content-Type", "image/jpeg")
|
h.Set("Content-Type", "image/jpeg")
|
||||||
h.Set("Content-Length", strconv.Itoa(len(data)))
|
h.Set("Content-Length", strconv.Itoa(len(b)))
|
||||||
h.Set("Cache-Control", "no-cache")
|
h.Set("Cache-Control", "no-cache")
|
||||||
h.Set("Connection", "close")
|
h.Set("Connection", "close")
|
||||||
h.Set("Pragma", "no-cache")
|
h.Set("Pragma", "no-cache")
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
if _, err := w.Write(b); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
|
||||||
|
|
||||||
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
outputMjpeg(w, r)
|
outputMjpeg(w, r)
|
||||||
@@ -98,26 +89,9 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
flusher := w.(http.Flusher)
|
cons := mjpeg.NewConsumer()
|
||||||
|
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
cons := &mjpeg.Consumer{
|
cons.UserAgent = r.UserAgent()
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
|
||||||
UserAgent: r.UserAgent(),
|
|
||||||
}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case []byte:
|
|
||||||
data := []byte(header + strconv.Itoa(len(msg)))
|
|
||||||
data = append(data, '\r', '\n', '\r', '\n')
|
|
||||||
data = append(data, msg...)
|
|
||||||
data = append(data, '\r', '\n')
|
|
||||||
|
|
||||||
// Chrome bug: mjpeg image always shows the second to last image
|
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
|
||||||
_, _ = w.Write(data)
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
||||||
@@ -130,11 +104,33 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.Set("Connection", "close")
|
h.Set("Connection", "close")
|
||||||
h.Set("Pragma", "no-cache")
|
h.Set("Pragma", "no-cache")
|
||||||
|
|
||||||
<-r.Context().Done()
|
wr := &writer{wr: w, buf: []byte(header)}
|
||||||
|
_, _ = cons.WriteTo(wr)
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
}
|
||||||
|
|
||||||
//log.Trace().Msg("[api.mjpeg] close")
|
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
||||||
|
|
||||||
|
type writer struct {
|
||||||
|
wr io.Writer
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *writer) Write(p []byte) (n int, err error) {
|
||||||
|
w.buf = w.buf[:len(header)]
|
||||||
|
w.buf = append(w.buf, strconv.Itoa(len(p))...)
|
||||||
|
w.buf = append(w.buf, "\r\n\r\n"...)
|
||||||
|
w.buf = append(w.buf, p...)
|
||||||
|
w.buf = append(w.buf, "\r\n"...)
|
||||||
|
|
||||||
|
// Chrome bug: mjpeg image always shows the second to last image
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
||||||
|
if n, err = w.wr.Write(w.buf); err == nil {
|
||||||
|
w.wr.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -164,23 +160,19 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{
|
cons := mjpeg.NewConsumer()
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||||
UserAgent: tr.Request.UserAgent(),
|
cons.UserAgent = tr.Request.UserAgent()
|
||||||
}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
tr.Write(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Debug().Err(err).Msg("[mjpeg] add consumer")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.Write(&ws.Message{Type: "mjpeg"})
|
tr.Write(&ws.Message{Type: "mjpeg"})
|
||||||
|
|
||||||
|
go cons.WriteTo(tr.Writer())
|
||||||
|
|
||||||
tr.OnClose(func() {
|
tr.OnClose(func() {
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
|
|||||||
+24
-76
@@ -47,18 +47,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := make(chan []byte, 1)
|
cons := mp4.NewKeyframe(nil)
|
||||||
|
|
||||||
cons := &mp4.Segment{OnlyKeyframe: true}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok && exit != nil {
|
|
||||||
select {
|
|
||||||
case exit <- data:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
exit = nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -66,20 +55,21 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := <-exit
|
once := &core.OnceBuffer{} // init and first frame
|
||||||
|
_, _ = cons.WriteTo(once)
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
// Apple Safari won't show frame without length
|
// Apple Safari won't show frame without length
|
||||||
header := w.Header()
|
header := w.Header()
|
||||||
header.Set("Content-Length", strconv.Itoa(len(data)))
|
header.Set("Content-Length", strconv.Itoa(once.Len()))
|
||||||
header.Set("Content-Type", cons.MimeType)
|
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
|
||||||
|
|
||||||
if filename := query.Get("filename"); filename != "" {
|
if filename := query.Get("filename"); filename != "" {
|
||||||
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
if _, err := once.WriteTo(w); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,36 +91,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := make(chan error, 1) // Add buffer to prevent blocking
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
|
cons := mp4.NewConsumer(medias)
|
||||||
cons := &mp4.Consumer{
|
cons.Type = "MP4/HTTP active consumer"
|
||||||
Desc: "MP4/HTTP",
|
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
cons.UserAgent = r.UserAgent()
|
||||||
UserAgent: r.UserAgent(),
|
|
||||||
Medias: mp4.ParseQuery(r.URL.Query()),
|
|
||||||
}
|
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if exit == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
select {
|
|
||||||
case exit <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
exit = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -138,59 +109,36 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer stream.RemoveConsumer(cons)
|
if rotate := query.Get("rotate"); rotate != "" {
|
||||||
|
cons.Rotate = core.Atoi(rotate)
|
||||||
|
}
|
||||||
|
|
||||||
data, err := cons.Init()
|
if scale := query.Get("scale"); scale != "" {
|
||||||
if err != nil {
|
if sx, sy, ok := strings.Cut(scale, ":"); ok {
|
||||||
log.Error().Err(err).Caller().Send()
|
cons.ScaleX = core.Atoi(sx)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
cons.ScaleY = core.Atoi(sy)
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header := w.Header()
|
header := w.Header()
|
||||||
header.Set("Content-Type", cons.MimeType())
|
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
|
||||||
|
|
||||||
if filename := query.Get("filename"); filename != "" {
|
if filename := query.Get("filename"); filename != "" {
|
||||||
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rotate := query.Get("rotate"); rotate != "" {
|
|
||||||
mp4.PatchVideoRotate(data, core.Atoi(rotate))
|
|
||||||
}
|
|
||||||
|
|
||||||
if scale := query.Get("scale"); scale != "" {
|
|
||||||
if sx, sy, ok := strings.Cut(scale, ":"); ok {
|
|
||||||
mp4.PatchVideoScale(data, core.Atoi(sx), core.Atoi(sy))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
var duration *time.Timer
|
var duration *time.Timer
|
||||||
if s := query.Get("duration"); s != "" {
|
if s := query.Get("duration"); s != "" {
|
||||||
if i, _ := strconv.Atoi(s); i > 0 {
|
if i, _ := strconv.Atoi(s); i > 0 {
|
||||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
||||||
if exit != nil {
|
_ = cons.Stop()
|
||||||
select {
|
|
||||||
case exit <- nil:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
exit = nil
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = <-exit
|
_, _ = cons.WriteTo(w)
|
||||||
exit = nil
|
|
||||||
|
|
||||||
log.Trace().Err(err).Caller().Send()
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
if duration != nil {
|
if duration != nil {
|
||||||
duration.Stop()
|
duration.Stop()
|
||||||
|
|||||||
+20
-37
@@ -6,6 +6,7 @@ import (
|
|||||||
"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"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
)
|
)
|
||||||
@@ -16,44 +17,30 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons := &mp4.Consumer{
|
var medias []*core.Media
|
||||||
Desc: "MSE/WebSocket",
|
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
|
||||||
UserAgent: tr.Request.UserAgent(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if codecs := msg.String(); codecs != "" {
|
if codecs := msg.String(); codecs != "" {
|
||||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
||||||
cons.Medias = mp4.ParseCodecs(codecs, true)
|
medias = mp4.ParseCodecs(codecs, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
cons := mp4.NewConsumer(medias)
|
||||||
if data, ok := msg.([]byte); ok {
|
cons.Type = "MSE/WebSocket active consumer"
|
||||||
tr.Write(data)
|
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||||
}
|
cons.UserAgent = tr.Request.UserAgent()
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Debug().Err(err).Msg("[mp4] add consumer")
|
log.Debug().Err(err).Msg("[mp4] add consumer")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
|
||||||
|
|
||||||
|
go cons.WriteTo(tr.Writer())
|
||||||
|
|
||||||
tr.OnClose(func() {
|
tr.OnClose(func() {
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
|
|
||||||
tr.Write(&ws.Message{Type: "mse", Value: cons.MimeType()})
|
|
||||||
|
|
||||||
data, err := cons.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.Write(data)
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,29 +50,25 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons := &mp4.Segment{
|
var medias []*core.Media
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
|
||||||
UserAgent: tr.Request.UserAgent(),
|
|
||||||
OnlyKeyframe: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if codecs := msg.String(); codecs != "" {
|
if codecs := msg.String(); codecs != "" {
|
||||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
||||||
cons.Medias = mp4.ParseCodecs(codecs, false)
|
medias = mp4.ParseCodecs(codecs, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
cons := mp4.NewKeyframe(medias)
|
||||||
if data, ok := msg.([]byte); ok {
|
cons.Type = "MP4/WebSocket active consumer"
|
||||||
tr.Write(data)
|
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
|
||||||
}
|
cons.UserAgent = tr.Request.UserAgent()
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.Write(&ws.Message{Type: "mp4", Value: cons.MimeType})
|
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
|
||||||
|
|
||||||
|
go cons.WriteTo(tr.Writer())
|
||||||
|
|
||||||
tr.OnClose(func() {
|
tr.OnClose(func() {
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package mpegts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiStreamAAC(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
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := aac.NewConsumer()
|
||||||
|
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
|
cons.UserAgent = r.UserAgent()
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "audio/aac")
|
||||||
|
|
||||||
|
_, _ = cons.WriteTo(w)
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
}
|
||||||
@@ -1,22 +1,54 @@
|
|||||||
package mpegts
|
package mpegts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
"net/http"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/stream.ts", apiHandle)
|
api.HandleFunc("api/stream.ts", apiHandle)
|
||||||
|
api.HandleFunc("api/stream.aac", apiStreamAAC)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
outputMpegTS(w, r)
|
||||||
|
} else {
|
||||||
|
inputMpegTS(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputMpegTS(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 := mpegts.NewConsumer()
|
||||||
|
cons.RemoteAddr = tcp.RemoteAddr(r)
|
||||||
|
cons.UserAgent = r.UserAgent()
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "video/mp2t")
|
||||||
|
|
||||||
|
_, _ = cons.WriteTo(w)
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inputMpegTS(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 {
|
||||||
@@ -24,20 +56,17 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Request: r}
|
client, err := mpegts.Open(r.Body)
|
||||||
client := mpegts.NewClient(res)
|
if err != nil {
|
||||||
|
|
||||||
if err := client.Handle(); 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.Handle(); 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package nest
|
package nest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"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/nest"
|
"github.com/AlexxIT/go2rtc/pkg/nest"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -41,15 +42,15 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
|
|
||||||
for name, deviceID := range devices {
|
for name, deviceID := range devices {
|
||||||
query.Set("device_id", deviceID)
|
query.Set("device_id", deviceID)
|
||||||
|
|
||||||
items = append(items, api.Stream{
|
items = append(items, &api.Source{
|
||||||
Name: name, URL: "nest:?" + query.Encode(),
|
Name: name, URL: "nest:?" + query.Encode(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+13
-12
@@ -1,13 +1,6 @@
|
|||||||
package onvif
|
package onvif
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/onvif"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,6 +8,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/onvif"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -121,7 +122,7 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
|||||||
func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
|
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
|
|
||||||
if src == "" {
|
if src == "" {
|
||||||
urls, err := onvif.DiscoveryStreamingURLs()
|
urls, err := onvif.DiscoveryStreamingURLs()
|
||||||
@@ -149,7 +150,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
|||||||
u.Path = ""
|
u.Path = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
items = append(items, api.Stream{Name: u.Host, URL: u.String()})
|
items = append(items, &api.Source{Name: u.Host, URL: u.String()})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
client, err := onvif.NewClient(src)
|
client, err := onvif.NewClient(src)
|
||||||
@@ -176,19 +177,19 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, token := range tokens {
|
for i, token := range tokens {
|
||||||
items = append(items, api.Stream{
|
items = append(items, &api.Source{
|
||||||
Name: name + " stream" + strconv.Itoa(i),
|
Name: name + " stream" + strconv.Itoa(i),
|
||||||
URL: src + "?subtype=" + token,
|
URL: src + "?subtype=" + token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tokens) > 0 && client.HasSnapshots() {
|
if len(tokens) > 0 && client.HasSnapshots() {
|
||||||
items = append(items, api.Stream{
|
items = append(items, &api.Source{
|
||||||
Name: name + " snapshot",
|
Name: name + " snapshot",
|
||||||
URL: src + "?subtype=" + tokens[0] + "&snapshot",
|
URL: src + "?subtype=" + tokens[0] + "&snapshot",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package roborock
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"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/roborock"
|
"github.com/AlexxIT/go2rtc/pkg/roborock"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -84,7 +85,7 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
|
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
source := fmt.Sprintf(
|
source := fmt.Sprintf(
|
||||||
@@ -93,8 +94,8 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
|
|||||||
Auth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,
|
Auth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,
|
||||||
device.DID, device.Key,
|
device.DID, device.Key,
|
||||||
)
|
)
|
||||||
items = append(items, api.Stream{Name: device.Name, URL: source})
|
items = append(items, &api.Source{Name: device.Name, URL: source})
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
}
|
}
|
||||||
|
|||||||
+160
-16
@@ -1,38 +1,188 @@
|
|||||||
package rtmp
|
package rtmp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"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/rtmp"
|
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
"io"
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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("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) {
|
||||||
conn := rtmp.NewClient(url)
|
client, err := rtmp.DialPlay(url)
|
||||||
if err := conn.Dial(); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := conn.Describe(); err != nil {
|
return client, nil
|
||||||
return nil, err
|
}
|
||||||
|
|
||||||
|
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 conn, nil
|
|
||||||
|
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 {
|
||||||
@@ -40,18 +190,12 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Request: r}
|
client, err := flv.Open(r.Body)
|
||||||
client, err := rtmp.Accept(res)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = client.Describe(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AddProducer(client)
|
stream.AddProducer(client)
|
||||||
|
|
||||||
if err = client.Start(); err != nil && err != io.EOF {
|
if err = client.Start(); err != nil && err != io.EOF {
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -101,6 +101,8 @@ func rtspHandler(rawURL string) (core.Producer, error) {
|
|||||||
if rawQuery != "" {
|
if rawQuery != "" {
|
||||||
query := streams.ParseQuery(rawQuery)
|
query := streams.ParseQuery(rawQuery)
|
||||||
conn.Backchannel = query.Get("backchannel") == "1"
|
conn.Backchannel = query.Get("backchannel") == "1"
|
||||||
|
conn.Media = query.Get("media")
|
||||||
|
conn.Timeout = core.Atoi(query.Get("timeout"))
|
||||||
conn.Transport = query.Get("transport")
|
conn.Transport = query.Get("transport")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +239,7 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
|
|
||||||
if closer != nil {
|
if closer != nil {
|
||||||
if err := conn.Handle(); err != nil {
|
if err := conn.Handle(); err != nil {
|
||||||
log.Debug().Msgf("[rtsp] handle=%s", err)
|
log.Debug().Err(err).Msg("[rtsp] handle")
|
||||||
}
|
}
|
||||||
|
|
||||||
closer()
|
closer()
|
||||||
|
|||||||
+3
-20
@@ -1,8 +1,6 @@
|
|||||||
package srtp
|
package srtp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||||
)
|
)
|
||||||
@@ -15,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)
|
||||||
@@ -24,23 +22,8 @@ func Init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log := app.GetLogger("srtp")
|
// create SRTP server (endpoint) for receiving video from HomeKit cameras
|
||||||
|
Server = srtp.NewServer(cfg.Mod.Listen)
|
||||||
// create SRTP server (endpoint) for receiving video from HomeKit camera
|
|
||||||
conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
|
|
||||||
|
|
||||||
// run server
|
|
||||||
go func() {
|
|
||||||
Server = &srtp.Server{}
|
|
||||||
if err = Server.Serve(conn); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var Server *srtp.Server
|
var Server *srtp.Server
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package streams
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
||||||
|
// support for multiple simultaneous pending from different consumers
|
||||||
|
consN := s.pending.Add(1) - 1
|
||||||
|
|
||||||
|
var prodErrors = make([]error, len(s.producers))
|
||||||
|
var prodMedias []*core.Media
|
||||||
|
var prodStarts []*Producer
|
||||||
|
|
||||||
|
// Step 1. Get consumer medias
|
||||||
|
consMedias := cons.GetMedias()
|
||||||
|
for _, consMedia := range consMedias {
|
||||||
|
log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia)
|
||||||
|
|
||||||
|
producers:
|
||||||
|
for prodN, prod := range s.producers {
|
||||||
|
if prodErrors[prodN] != nil {
|
||||||
|
log.Trace().Msgf("[streams] skip cons=%d prod=%d", consN, prodN)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = prod.Dial(); err != nil {
|
||||||
|
log.Trace().Err(err).Msgf("[streams] dial cons=%d prod=%d", consN, prodN)
|
||||||
|
prodErrors[prodN] = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2. Get producer medias (not tracks yet)
|
||||||
|
for _, prodMedia := range prod.GetMedias() {
|
||||||
|
log.Trace().Msgf("[streams] check cons=%d prod=%d media=%s", consN, prodN, prodMedia)
|
||||||
|
prodMedias = append(prodMedias, prodMedia)
|
||||||
|
|
||||||
|
// Step 3. Match consumer/producer codecs list
|
||||||
|
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||||
|
if prodCodec == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var track *core.Receiver
|
||||||
|
|
||||||
|
switch prodMedia.Direction {
|
||||||
|
case core.DirectionRecvonly:
|
||||||
|
log.Trace().Msgf("[streams] match cons=%d <= prod=%d", consN, prodN)
|
||||||
|
|
||||||
|
// Step 4. Get recvonly track from producer
|
||||||
|
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
|
||||||
|
log.Info().Err(err).Msg("[streams] can't get track")
|
||||||
|
prodErrors[prodN] = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Step 5. Add track to consumer
|
||||||
|
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||||
|
log.Info().Err(err).Msg("[streams] can't add track")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
case core.DirectionSendonly:
|
||||||
|
log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN)
|
||||||
|
|
||||||
|
// Step 4. Get recvonly track from consumer (backchannel)
|
||||||
|
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
|
||||||
|
log.Info().Err(err).Msg("[streams] can't get track")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Step 5. Add track to producer
|
||||||
|
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
|
||||||
|
log.Info().Err(err).Msg("[streams] can't add track")
|
||||||
|
prodErrors[prodN] = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prodStarts = append(prodStarts, prod)
|
||||||
|
|
||||||
|
if !consMedia.MatchAll() {
|
||||||
|
break producers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop producers if they don't have readers
|
||||||
|
if s.pending.Add(-1) == 0 {
|
||||||
|
s.stopProducers()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(prodStarts) == 0 {
|
||||||
|
return formatError(consMedias, prodMedias, prodErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.consumers = append(s.consumers, cons)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
// there may be duplicates, but that's not a problem
|
||||||
|
for _, prod := range prodStarts {
|
||||||
|
prod.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error {
|
||||||
|
// 1. Return errors if any not nil
|
||||||
|
var text string
|
||||||
|
|
||||||
|
for _, err := range prodErrors {
|
||||||
|
if err != nil {
|
||||||
|
text = appendString(text, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(text) != 0 {
|
||||||
|
return errors.New("streams: " + text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Return "codecs not matched"
|
||||||
|
if prodMedias != nil {
|
||||||
|
var prod, cons string
|
||||||
|
|
||||||
|
for _, media := range prodMedias {
|
||||||
|
if media.Direction == core.DirectionRecvonly {
|
||||||
|
for _, codec := range media.Codecs {
|
||||||
|
prod = appendString(prod, codec.PrintName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, media := range consMedias {
|
||||||
|
if media.Direction == core.DirectionSendonly {
|
||||||
|
for _, codec := range media.Codecs {
|
||||||
|
cons = appendString(cons, codec.PrintName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("streams: codecs not matched: " + prod + " => " + cons)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Return unknown error
|
||||||
|
return errors.New("streams: unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendString(s, elem string) string {
|
||||||
|
if strings.Contains(s, elem) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if len(s) == 0 {
|
||||||
|
return elem
|
||||||
|
}
|
||||||
|
return s + ", " + elem
|
||||||
|
}
|
||||||
@@ -1,41 +1,97 @@
|
|||||||
package streams
|
package streams
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler func(url string) (core.Producer, error)
|
type Handler func(url string) (core.Producer, error)
|
||||||
|
|
||||||
var handlers = map[string]Handler{}
|
var handlers = map[string]Handler{}
|
||||||
var handlersMu sync.Mutex
|
|
||||||
|
|
||||||
func HandleFunc(scheme string, handler Handler) {
|
func HandleFunc(scheme string, handler Handler) {
|
||||||
handlersMu.Lock()
|
|
||||||
handlers[scheme] = handler
|
handlers[scheme] = handler
|
||||||
handlersMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHandler(url string) Handler {
|
|
||||||
i := strings.IndexByte(url, ':')
|
|
||||||
if i <= 0 { // TODO: i < 4 ?
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
handlersMu.Lock()
|
|
||||||
defer handlersMu.Unlock()
|
|
||||||
return handlers[url[:i]]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasProducer(url string) bool {
|
func HasProducer(url string) bool {
|
||||||
return getHandler(url) != nil
|
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||||
|
scheme := url[:i]
|
||||||
|
|
||||||
|
if _, ok := handlers[scheme]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := redirects[scheme]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetProducer(url string) (core.Producer, error) {
|
func GetProducer(url string) (core.Producer, error) {
|
||||||
handler := getHandler(url)
|
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||||
if handler == nil {
|
scheme := url[:i]
|
||||||
return nil, fmt.Errorf("unsupported scheme: %s", url)
|
|
||||||
|
if redirect, ok := redirects[scheme]; ok {
|
||||||
|
location, err := redirect(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if location != "" {
|
||||||
|
return GetProducer(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler, ok := handlers[scheme]; ok {
|
||||||
|
return handler(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return handler(url)
|
|
||||||
|
return nil, errors.New("streams: unsupported scheme: " + url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect can return: location URL or error or empty URL and error
|
||||||
|
type Redirect func(url string) (string, error)
|
||||||
|
|
||||||
|
var redirects = map[string]Redirect{}
|
||||||
|
|
||||||
|
func RedirectFunc(scheme string, redirect Redirect) {
|
||||||
|
redirects[scheme] = redirect
|
||||||
|
}
|
||||||
|
|
||||||
|
func Location(url string) (string, error) {
|
||||||
|
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||||
|
scheme := url[:i]
|
||||||
|
|
||||||
|
if redirect, ok := redirects[scheme]; ok {
|
||||||
|
return redirect(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -245,10 +245,10 @@ func (p *Producer) stop() {
|
|||||||
|
|
||||||
switch p.state {
|
switch p.state {
|
||||||
case stateExternal:
|
case stateExternal:
|
||||||
log.Debug().Msgf("[streams] can't stop external producer")
|
log.Trace().Msgf("[streams] skip stop external producer")
|
||||||
return
|
return
|
||||||
case stateNone:
|
case stateNone:
|
||||||
log.Debug().Msgf("[streams] can't stop none producer")
|
log.Trace().Msgf("[streams] skip stop none producer")
|
||||||
return
|
return
|
||||||
case stateStart:
|
case stateStart:
|
||||||
p.workerID++
|
p.workerID++
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-143
@@ -2,8 +2,6 @@ package streams
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@@ -14,7 +12,7 @@ type Stream struct {
|
|||||||
producers []*Producer
|
producers []*Producer
|
||||||
consumers []core.Consumer
|
consumers []core.Consumer
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
requests int32
|
pending atomic.Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStream(source any) *Stream {
|
func NewStream(source any) *Stream {
|
||||||
@@ -25,8 +23,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:
|
||||||
@@ -38,105 +41,19 @@ func NewStream(source any) *Stream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Stream) Sources() (sources []string) {
|
||||||
|
for _, prod := range s.producers {
|
||||||
|
sources = append(sources, prod.url)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Stream) SetSource(source string) {
|
func (s *Stream) SetSource(source string) {
|
||||||
for _, prod := range s.producers {
|
for _, prod := range s.producers {
|
||||||
prod.SetSource(source)
|
prod.SetSource(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
|
||||||
// support for multiple simultaneous requests from different consumers
|
|
||||||
consN := atomic.AddInt32(&s.requests, 1) - 1
|
|
||||||
|
|
||||||
var statErrors []error
|
|
||||||
var statMedias []*core.Media
|
|
||||||
var statProds []*Producer // matched producers for consumer
|
|
||||||
|
|
||||||
// Step 1. Get consumer medias
|
|
||||||
for _, consMedia := range cons.GetMedias() {
|
|
||||||
log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia)
|
|
||||||
|
|
||||||
producers:
|
|
||||||
for prodN, prod := range s.producers {
|
|
||||||
if err = prod.Dial(); err != nil {
|
|
||||||
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
|
|
||||||
statErrors = append(statErrors, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2. Get producer medias (not tracks yet)
|
|
||||||
for _, prodMedia := range prod.GetMedias() {
|
|
||||||
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
|
|
||||||
statMedias = append(statMedias, prodMedia)
|
|
||||||
|
|
||||||
// Step 3. Match consumer/producer codecs list
|
|
||||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
|
||||||
if prodCodec == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var track *core.Receiver
|
|
||||||
|
|
||||||
switch prodMedia.Direction {
|
|
||||||
case core.DirectionRecvonly:
|
|
||||||
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
|
|
||||||
|
|
||||||
// Step 4. Get recvonly track from producer
|
|
||||||
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't get track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Step 5. Add track to consumer
|
|
||||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't add track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
case core.DirectionSendonly:
|
|
||||||
log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN)
|
|
||||||
|
|
||||||
// Step 4. Get recvonly track from consumer (backchannel)
|
|
||||||
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't get track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Step 5. Add track to producer
|
|
||||||
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't add track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
statProds = append(statProds, prod)
|
|
||||||
|
|
||||||
if !consMedia.MatchAll() {
|
|
||||||
break producers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop producers if they don't have readers
|
|
||||||
if atomic.AddInt32(&s.requests, -1) == 0 {
|
|
||||||
s.stopProducers()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(statProds) == 0 {
|
|
||||||
return formatError(statMedias, statErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
s.consumers = append(s.consumers, cons)
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
// there may be duplicates, but that's not a problem
|
|
||||||
for _, prod := range statProds {
|
|
||||||
prod.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) RemoveConsumer(cons core.Consumer) {
|
func (s *Stream) RemoveConsumer(cons core.Consumer) {
|
||||||
_ = cons.Stop()
|
_ = cons.Stop()
|
||||||
|
|
||||||
@@ -206,48 +123,3 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
|
|||||||
|
|
||||||
return json.Marshal(info)
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatError(statMedias []*core.Media, statErrors []error) error {
|
|
||||||
var text string
|
|
||||||
|
|
||||||
for _, media := range statMedias {
|
|
||||||
if media.Direction == core.DirectionRecvonly {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, codec := range media.Codecs {
|
|
||||||
name := codec.Name
|
|
||||||
if name == core.CodecAAC {
|
|
||||||
name = "AAC"
|
|
||||||
}
|
|
||||||
if strings.Contains(text, name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(text) > 0 {
|
|
||||||
text += ","
|
|
||||||
}
|
|
||||||
text += name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if text != "" {
|
|
||||||
return errors.New(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, err := range statErrors {
|
|
||||||
s := err.Error()
|
|
||||||
if strings.Contains(text, s) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(text) > 0 {
|
|
||||||
text += ","
|
|
||||||
}
|
|
||||||
text += s
|
|
||||||
}
|
|
||||||
|
|
||||||
if text != "" {
|
|
||||||
return errors.New(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("unknown error")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,31 +5,40 @@ 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"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app/store"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, item := range store.GetDict("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 {
|
||||||
@@ -118,6 +127,14 @@ func GetAll() (names []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Streams() map[string]*Stream {
|
||||||
|
return streams
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delete(id string) {
|
||||||
|
delete(streams, id)
|
||||||
|
}
|
||||||
|
|
||||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
src := query.Get("src")
|
src := query.Get("src")
|
||||||
@@ -141,6 +158,11 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if New(name, src) == nil {
|
if New(name, src) == nil {
|
||||||
http.Error(w, "", http.StatusBadRequest)
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.PatchConfig(name, src, "streams"); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "PATCH":
|
case "PATCH":
|
||||||
@@ -164,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)
|
||||||
}
|
}
|
||||||
@@ -173,6 +199,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
delete(streams, src)
|
delete(streams, src)
|
||||||
|
|
||||||
|
if err := app.PatchConfig(src, nil, "streams"); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ package tapo
|
|||||||
import (
|
import (
|
||||||
"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/kasa"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tapo"
|
"github.com/AlexxIT/go2rtc/pkg/tapo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("tapo", handle)
|
streams.HandleFunc("kasa", func(url string) (core.Producer, error) {
|
||||||
}
|
return kasa.Dial(url)
|
||||||
|
})
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
streams.HandleFunc("tapo", func(url string) (core.Producer, error) {
|
||||||
conn := tapo.NewClient(url)
|
return tapo.Dial(url)
|
||||||
if err := conn.Dial(); err != nil {
|
})
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-203
@@ -1,13 +1,13 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"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"
|
||||||
@@ -42,12 +42,16 @@ func streamsHandler(rawURL string) (core.Producer, error) {
|
|||||||
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
|
||||||
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
|
||||||
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
|
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
|
||||||
|
} else if format == "openipc" {
|
||||||
|
return openIPCClient(rawURL, query)
|
||||||
} else {
|
} else {
|
||||||
return go2rtcClient(rawURL)
|
return go2rtcClient(rawURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
@@ -213,206 +220,26 @@ func whepClient(url string) (core.Producer, error) {
|
|||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type KinesisRequest struct {
|
// Dial - websocket.Dial with Basic auth support
|
||||||
Action string `json:"action"`
|
func Dial(rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||||
ClientID string `json:"recipientClientId"`
|
u, err := url.Parse(rawURL)
|
||||||
Payload []byte `json:"messagePayload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KinesisRequest) String() string {
|
|
||||||
return fmt.Sprintf("action=%s, payload=%s", k.Action, k.Payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
type KinesisResponse struct {
|
|
||||||
Payload []byte `json:"messagePayload"`
|
|
||||||
Type string `json:"messageType"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KinesisResponse) String() string {
|
|
||||||
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
|
|
||||||
// 1. Connect to signalign server
|
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Load ICEServers from query param (base64 json)
|
if u.User == nil {
|
||||||
conf := pion.Configuration{}
|
return websocket.DefaultDialer.Dial(rawURL, nil)
|
||||||
|
|
||||||
if s := query.Get("ice_servers"); s != "" {
|
|
||||||
conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// close websocket when we ready return Producer or connection error
|
user := u.User.Username()
|
||||||
defer conn.Close()
|
pass, _ := u.User.Password()
|
||||||
|
u.User = nil
|
||||||
|
|
||||||
// 3. Create Peer Connection
|
header := http.Header{
|
||||||
api, err := webrtc.NewAPI("")
|
"Authorization": []string{
|
||||||
if err != nil {
|
"Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass)),
|
||||||
return nil, err
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pc, err := api.NewPeerConnection(conf)
|
return websocket.DefaultDialer.Dial(u.String(), header)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// protect from sending ICE candidate before Offer
|
|
||||||
var sendOffer core.Waiter
|
|
||||||
|
|
||||||
// protect from blocking on errors
|
|
||||||
defer sendOffer.Done(nil)
|
|
||||||
|
|
||||||
// waiter will wait PC error or WS error or nil (connection OK)
|
|
||||||
var connState core.Waiter
|
|
||||||
|
|
||||||
req := KinesisRequest{
|
|
||||||
ClientID: query.Get("client_id"),
|
|
||||||
}
|
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
|
||||||
prod.Desc = desc
|
|
||||||
prod.Mode = core.ModeActiveProducer
|
|
||||||
prod.Listen(func(msg any) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *pion.ICECandidate:
|
|
||||||
_ = sendOffer.Wait()
|
|
||||||
|
|
||||||
req.Action = "ICE_CANDIDATE"
|
|
||||||
req.Payload, _ = json.Marshal(msg.ToJSON())
|
|
||||||
if err = conn.WriteJSON(&req); err != nil {
|
|
||||||
connState.Done(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
|
|
||||||
|
|
||||||
case pion.PeerConnectionState:
|
|
||||||
switch msg {
|
|
||||||
case pion.PeerConnectionStateConnecting:
|
|
||||||
case pion.PeerConnectionStateConnected:
|
|
||||||
connState.Done(nil)
|
|
||||||
default:
|
|
||||||
connState.Done(errors.New("webrtc: " + msg.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
medias := []*core.Media{
|
|
||||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
|
||||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Create offer
|
|
||||||
offer, err := prod.CreateOffer(medias)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Send offer
|
|
||||||
req.Action = "SDP_OFFER"
|
|
||||||
req.Payload, _ = json.Marshal(pion.SessionDescription{
|
|
||||||
Type: pion.SDPTypeOffer,
|
|
||||||
SDP: offer,
|
|
||||||
})
|
|
||||||
if err = conn.WriteJSON(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
|
|
||||||
|
|
||||||
sendOffer.Done(nil)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// will be closed when conn will be closed
|
|
||||||
for {
|
|
||||||
var res KinesisResponse
|
|
||||||
if err = conn.ReadJSON(&res); err != nil {
|
|
||||||
// some buggy messages from Amazon servers
|
|
||||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().Msgf("[webrtc] kinesis recv: %s", res)
|
|
||||||
|
|
||||||
switch res.Type {
|
|
||||||
case "SDP_ANSWER":
|
|
||||||
// 6. Get answer
|
|
||||||
var sd pion.SessionDescription
|
|
||||||
if err = json.Unmarshal(res.Payload, &sd); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = prod.SetAnswer(sd.SDP); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "ICE_CANDIDATE":
|
|
||||||
// 7. Continue to receiving candidates
|
|
||||||
var ci pion.ICECandidateInit
|
|
||||||
if err = json.Unmarshal(res.Payload, &ci); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = prod.AddCandidate(ci.Candidate); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connState.Done(err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = connState.Wait(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return prod, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type WyzeKVS struct {
|
|
||||||
ClientId string `json:"ClientId"`
|
|
||||||
Cam string `json:"cam"`
|
|
||||||
Result string `json:"result"`
|
|
||||||
Servers json.RawMessage `json:"servers"`
|
|
||||||
URL string `json:"signalingUrl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func wyzeClient(rawURL string) (core.Producer, error) {
|
|
||||||
client := http.Client{Timeout: 5 * time.Second}
|
|
||||||
res, err := client.Get(rawURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var kvs WyzeKVS
|
|
||||||
if err = json.Unmarshal(b, &kvs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if kvs.Result != "ok" {
|
|
||||||
return nil, errors.New("wyse: wrong result: " + kvs.Result)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := url.Values{
|
|
||||||
"client_id": []string{kvs.ClientId},
|
|
||||||
"ice_servers": []string{string(kvs.Servers)},
|
|
||||||
}
|
|
||||||
|
|
||||||
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
pion "github.com/pion/webrtc/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type kinesisRequest struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
ClientID string `json:"recipientClientId"`
|
||||||
|
Payload []byte `json:"messagePayload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k kinesisRequest) String() string {
|
||||||
|
return fmt.Sprintf("action=%s, payload=%s", k.Action, k.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
type kinesisResponse struct {
|
||||||
|
Payload []byte `json:"messagePayload"`
|
||||||
|
Type string `json:"messageType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k kinesisResponse) String() string {
|
||||||
|
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
|
||||||
|
// 1. Connect to signalign server
|
||||||
|
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Load ICEServers from query param (base64 json)
|
||||||
|
conf := pion.Configuration{}
|
||||||
|
|
||||||
|
if s := query.Get("ice_servers"); s != "" {
|
||||||
|
conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close websocket when we ready return Producer or connection error
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// 3. Create Peer Connection
|
||||||
|
api, err := webrtc.NewAPI()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := api.NewPeerConnection(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// protect from sending ICE candidate before Offer
|
||||||
|
var sendOffer core.Waiter
|
||||||
|
|
||||||
|
// protect from blocking on errors
|
||||||
|
defer sendOffer.Done(nil)
|
||||||
|
|
||||||
|
// waiter will wait PC error or WS error or nil (connection OK)
|
||||||
|
var connState core.Waiter
|
||||||
|
|
||||||
|
req := kinesisRequest{
|
||||||
|
ClientID: query.Get("client_id"),
|
||||||
|
}
|
||||||
|
|
||||||
|
prod := webrtc.NewConn(pc)
|
||||||
|
prod.Desc = desc
|
||||||
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Listen(func(msg any) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case *pion.ICECandidate:
|
||||||
|
_ = sendOffer.Wait()
|
||||||
|
|
||||||
|
req.Action = "ICE_CANDIDATE"
|
||||||
|
req.Payload, _ = json.Marshal(msg.ToJSON())
|
||||||
|
if err = conn.WriteJSON(&req); err != nil {
|
||||||
|
connState.Done(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
|
||||||
|
|
||||||
|
case pion.PeerConnectionState:
|
||||||
|
switch msg {
|
||||||
|
case pion.PeerConnectionStateConnecting:
|
||||||
|
case pion.PeerConnectionStateConnected:
|
||||||
|
connState.Done(nil)
|
||||||
|
default:
|
||||||
|
connState.Done(errors.New("webrtc: " + msg.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
medias := []*core.Media{
|
||||||
|
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||||
|
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create offer
|
||||||
|
offer, err := prod.CreateOffer(medias)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Send offer
|
||||||
|
req.Action = "SDP_OFFER"
|
||||||
|
req.Payload, _ = json.Marshal(pion.SessionDescription{
|
||||||
|
Type: pion.SDPTypeOffer,
|
||||||
|
SDP: offer,
|
||||||
|
})
|
||||||
|
if err = conn.WriteJSON(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
|
||||||
|
|
||||||
|
sendOffer.Done(nil)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// will be closed when conn will be closed
|
||||||
|
for {
|
||||||
|
var res kinesisResponse
|
||||||
|
if err = conn.ReadJSON(&res); err != nil {
|
||||||
|
// some buggy messages from Amazon servers
|
||||||
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[webrtc] kinesis recv: %s", res)
|
||||||
|
|
||||||
|
switch res.Type {
|
||||||
|
case "SDP_ANSWER":
|
||||||
|
// 6. Get answer
|
||||||
|
var sd pion.SessionDescription
|
||||||
|
if err = json.Unmarshal(res.Payload, &sd); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = prod.SetAnswer(sd.SDP); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ICE_CANDIDATE":
|
||||||
|
// 7. Continue to receiving candidates
|
||||||
|
var ci pion.ICECandidateInit
|
||||||
|
if err = json.Unmarshal(res.Payload, &ci); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = prod.AddCandidate(ci.Candidate); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connState.Done(err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = connState.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type wyzeKVS struct {
|
||||||
|
ClientId string `json:"ClientId"`
|
||||||
|
Cam string `json:"cam"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
Servers json.RawMessage `json:"servers"`
|
||||||
|
URL string `json:"signalingUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func wyzeClient(rawURL string) (core.Producer, error) {
|
||||||
|
client := http.Client{Timeout: 5 * time.Second}
|
||||||
|
res, err := client.Get(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kvs wyzeKVS
|
||||||
|
if err = json.Unmarshal(b, &kvs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if kvs.Result != "ok" {
|
||||||
|
return nil, errors.New("wyse: wrong result: " + kvs.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{
|
||||||
|
"client_id": []string{kvs.ClientId},
|
||||||
|
"ice_servers": []string{string(kvs.Servers)},
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package webrtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
pion "github.com/pion/webrtc/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||||
|
// 1. Connect to signalign server
|
||||||
|
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Load ICEServers from query param (base64 json)
|
||||||
|
var conf pion.Configuration
|
||||||
|
|
||||||
|
if s := query.Get("ice_servers"); s != "" {
|
||||||
|
conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close websocket when we ready return Producer or connection error
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// 3. Create Peer Connection
|
||||||
|
api, err := webrtc.NewAPI()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := api.NewPeerConnection(conf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// protect from sending ICE candidate before Offer
|
||||||
|
var sendAnswer core.Waiter
|
||||||
|
|
||||||
|
// protect from blocking on errors
|
||||||
|
defer sendAnswer.Done(nil)
|
||||||
|
|
||||||
|
// waiter will wait PC error or WS error or nil (connection OK)
|
||||||
|
var connState core.Waiter
|
||||||
|
|
||||||
|
prod := webrtc.NewConn(pc)
|
||||||
|
prod.Desc = "WebRTC/OpenIPC"
|
||||||
|
prod.Mode = core.ModeActiveProducer
|
||||||
|
prod.Listen(func(msg any) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case *pion.ICECandidate:
|
||||||
|
_ = sendAnswer.Wait()
|
||||||
|
|
||||||
|
req := openIPCReq{
|
||||||
|
Data: msg.ToJSON().Candidate,
|
||||||
|
Req: "candidate",
|
||||||
|
}
|
||||||
|
if err = conn.WriteJSON(&req); err != nil {
|
||||||
|
connState.Done(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[webrtc] openipc send: %s", req)
|
||||||
|
|
||||||
|
case pion.PeerConnectionState:
|
||||||
|
switch msg {
|
||||||
|
case pion.PeerConnectionStateConnecting:
|
||||||
|
case pion.PeerConnectionStateConnected:
|
||||||
|
connState.Done(nil)
|
||||||
|
default:
|
||||||
|
connState.Done(errors.New("webrtc: " + msg.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// will be closed when conn will be closed
|
||||||
|
for err == nil {
|
||||||
|
var rep openIPCReply
|
||||||
|
if err = conn.ReadJSON(&rep); err != nil {
|
||||||
|
// some buggy messages from Amazon servers
|
||||||
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[webrtc] openipc recv: %s", rep)
|
||||||
|
|
||||||
|
switch rep.Reply {
|
||||||
|
case "webrtc_answer":
|
||||||
|
// 6. Get answer
|
||||||
|
var sd pion.SessionDescription
|
||||||
|
if err = json.Unmarshal(rep.Data, &sd); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = prod.SetOffer(sd.SDP); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var answer string
|
||||||
|
if answer, err = prod.GetAnswer(); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
req := openIPCReq{Data: answer, Req: "answer"}
|
||||||
|
if err = conn.WriteJSON(req); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
|
||||||
|
|
||||||
|
sendAnswer.Done(nil)
|
||||||
|
|
||||||
|
case "webrtc_candidate":
|
||||||
|
// 7. Continue to receiving candidates
|
||||||
|
var ci pion.ICECandidateInit
|
||||||
|
if err = json.Unmarshal(rep.Data, &ci); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = prod.AddCandidate(ci.Candidate); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connState.Done(err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = connState.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type openIPCReply struct {
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Reply string `json:"reply"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r openIPCReply) String() string {
|
||||||
|
b, _ := json.Marshal(r)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type openIPCReq struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
Req string `json:"req"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r openIPCReq) String() string {
|
||||||
|
b, _ := json.Marshal(r)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package webtorrent
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
@@ -10,8 +13,6 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -110,13 +111,13 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// response all shares
|
// response all shares
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
for src, share := range shares {
|
for src, share := range shares {
|
||||||
pwd := srv.GetSharePwd(share)
|
pwd := srv.GetSharePwd(share)
|
||||||
source := fmt.Sprintf("webtorrent:?share=%s&pwd=%s", share, pwd)
|
source := fmt.Sprintf("webtorrent:?share=%s&pwd=%s", share, pwd)
|
||||||
items = append(items, api.Stream{Name: src, URL: source})
|
items = append(items, &api.Source{ID: src, URL: source})
|
||||||
}
|
}
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "POST":
|
case "POST":
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -1,17 +1,17 @@
|
|||||||
## AAC-LD and AAC-ELD
|
## AAC-LD and AAC-ELD
|
||||||
|
|
||||||
Codec | Rate | QuickTime | ffmpeg | VLC
|
| Codec | Rate | QuickTime | ffmpeg | VLC |
|
||||||
------|------|-----------|--------|----
|
|---------|-------|-----------|--------|-----|
|
||||||
AAC-LD | 8000 | yes | no | no
|
| AAC-LD | 8000 | yes | no | no |
|
||||||
AAC-LD | 16000 | yes | no | no
|
| AAC-LD | 16000 | yes | no | no |
|
||||||
AAC-LD | 22050 | yes | yes | no
|
| AAC-LD | 22050 | yes | yes | no |
|
||||||
AAC-LD | 24000 | yes | yes | no
|
| AAC-LD | 24000 | yes | yes | no |
|
||||||
AAC-LD | 32000 | yes | yes | no
|
| AAC-LD | 32000 | yes | yes | no |
|
||||||
AAC-ELD | 8000 | yes | no | no
|
| AAC-ELD | 8000 | yes | no | no |
|
||||||
AAC-ELD | 16000 | yes | no | no
|
| AAC-ELD | 16000 | yes | no | no |
|
||||||
AAC-ELD | 22050 | yes | yes | yes
|
| AAC-ELD | 22050 | yes | yes | yes |
|
||||||
AAC-ELD | 24000 | yes | yes | yes
|
| AAC-ELD | 24000 | yes | yes | yes |
|
||||||
AAC-ELD | 32000 | yes | yes | yes
|
| AAC-ELD | 32000 | yes | yes | yes |
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
|
|||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
package aac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/bits"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeAACMain = 1
|
||||||
|
TypeAACLC = 2 // Low Complexity
|
||||||
|
TypeAACLD = 23 // Low Delay (48000, 44100, 32000, 24000, 22050)
|
||||||
|
TypeESCAPE = 31
|
||||||
|
TypeAACELD = 39 // Enhanced Low Delay
|
||||||
|
|
||||||
|
AUTime = 1024
|
||||||
|
|
||||||
|
// FMTP streamtype=5 - audio stream
|
||||||
|
FMTP = "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config="
|
||||||
|
)
|
||||||
|
|
||||||
|
var sampleRates = [16]uint32{
|
||||||
|
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
|
||||||
|
0, 0, 0, // protection from request sampleRates[15]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigToCodec(conf []byte) *core.Codec {
|
||||||
|
// https://en.wikipedia.org/wiki/MPEG-4_Part_3#MPEG-4_Audio_Object_Types
|
||||||
|
rd := bits.NewReader(conf)
|
||||||
|
|
||||||
|
codec := &core.Codec{
|
||||||
|
FmtpLine: FMTP + hex.EncodeToString(conf),
|
||||||
|
PayloadType: core.PayloadTypeRAW,
|
||||||
|
}
|
||||||
|
|
||||||
|
objType := rd.ReadBits(5)
|
||||||
|
if objType == TypeESCAPE {
|
||||||
|
objType = 32 + rd.ReadBits(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch objType {
|
||||||
|
case TypeAACLC, TypeAACLD, TypeAACELD:
|
||||||
|
codec.Name = core.CodecAAC
|
||||||
|
default:
|
||||||
|
codec.Name = fmt.Sprintf("AAC-%X", objType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleRateIdx := rd.ReadBits8(4); sampleRateIdx < 0x0F {
|
||||||
|
codec.ClockRate = sampleRates[sampleRateIdx]
|
||||||
|
} else {
|
||||||
|
codec.ClockRate = rd.ReadBits(24)
|
||||||
|
}
|
||||||
|
|
||||||
|
codec.Channels = rd.ReadBits16(4)
|
||||||
|
|
||||||
|
return codec
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate uint32) {
|
||||||
|
rd := bits.NewReader(b)
|
||||||
|
|
||||||
|
objType = rd.ReadBits8(5)
|
||||||
|
if objType == 0b11111 {
|
||||||
|
objType = 32 + rd.ReadBits8(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleFreqIdx = rd.ReadBits8(4)
|
||||||
|
if sampleFreqIdx == 0b1111 {
|
||||||
|
sampleRate = rd.ReadBits(24)
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = rd.ReadBits8(4)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeConfig(objType byte, sampleRate uint32, channels byte, shortFrame bool) []byte {
|
||||||
|
wr := bits.NewWriter(nil)
|
||||||
|
|
||||||
|
if objType < TypeESCAPE {
|
||||||
|
wr.WriteBits8(objType, 5)
|
||||||
|
} else {
|
||||||
|
wr.WriteBits8(TypeESCAPE, 5)
|
||||||
|
wr.WriteBits8(objType-32, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := indexUint32(sampleRates[:], sampleRate)
|
||||||
|
if i >= 0 {
|
||||||
|
wr.WriteBits8(byte(i), 4)
|
||||||
|
} else {
|
||||||
|
wr.WriteBits8(0xF, 4)
|
||||||
|
wr.WriteBits(sampleRate, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
wr.WriteBits8(channels, 4)
|
||||||
|
|
||||||
|
switch objType {
|
||||||
|
case TypeAACLD:
|
||||||
|
// https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L841
|
||||||
|
wr.WriteBool(shortFrame)
|
||||||
|
wr.WriteBit(0) // dependsOnCoreCoder
|
||||||
|
wr.WriteBit(0) // extension_flag
|
||||||
|
wr.WriteBits8(0, 2) // ep_config
|
||||||
|
case TypeAACELD:
|
||||||
|
// https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L922
|
||||||
|
wr.WriteBool(shortFrame)
|
||||||
|
wr.WriteBits8(0, 3) // res_flags
|
||||||
|
wr.WriteBit(0) // ldSbrPresentFlag
|
||||||
|
wr.WriteBits8(0, 4) // ELDEXT_TERM
|
||||||
|
wr.WriteBits8(0, 2) // ep_config
|
||||||
|
}
|
||||||
|
|
||||||
|
return wr.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexUint32(s []uint32, v uint32) int {
|
||||||
|
for i := range s {
|
||||||
|
if v == s[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package aac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigToCodec(t *testing.T) {
|
||||||
|
s := "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
|
||||||
|
s = core.Between(s, "config=", ";")
|
||||||
|
src, err := hex.DecodeString(s)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
codec := ConfigToCodec(src)
|
||||||
|
require.Equal(t, core.CodecAAC, codec.Name)
|
||||||
|
require.Equal(t, uint32(24000), codec.ClockRate)
|
||||||
|
require.Equal(t, uint16(1), codec.Channels)
|
||||||
|
|
||||||
|
dst := EncodeConfig(TypeAACELD, 24000, 1, true)
|
||||||
|
require.Equal(t, src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestADTS(t *testing.T) {
|
||||||
|
// FFmpeg MPEG-TS AAC (one packet)
|
||||||
|
s := "fff15080021ffc210049900219002380fff15080021ffc212049900219002380" //...
|
||||||
|
src, err := hex.DecodeString(s)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
codec := ADTSToCodec(src)
|
||||||
|
require.Equal(t, uint32(44100), codec.ClockRate)
|
||||||
|
require.Equal(t, uint16(2), codec.Channels)
|
||||||
|
|
||||||
|
size := ReadADTSSize(src)
|
||||||
|
require.Equal(t, uint16(16), size)
|
||||||
|
|
||||||
|
dst := CodecToADTS(codec)
|
||||||
|
WriteADTSSize(dst, size)
|
||||||
|
|
||||||
|
require.Equal(t, src[:len(dst)], dst)
|
||||||
|
}
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
package aac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/bits"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsADTS(b []byte) bool {
|
||||||
|
_ = b[1]
|
||||||
|
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
||||||
|
}
|
||||||
|
|
||||||
|
func ADTSToCodec(b []byte) *core.Codec {
|
||||||
|
// 1. Check ADTS header
|
||||||
|
if !IsADTS(b) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Decode ADTS params
|
||||||
|
// https://wiki.multimedia.cx/index.php/ADTS
|
||||||
|
rd := bits.NewReader(b)
|
||||||
|
_ = rd.ReadBits(12) // Syncword, all bits must be set to 1
|
||||||
|
_ = rd.ReadBit() // MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2
|
||||||
|
_ = rd.ReadBits(2) // Layer, always set to 0
|
||||||
|
_ = rd.ReadBit() // Protection absence, set to 1 if there is no CRC and 0 if there is CRC
|
||||||
|
objType := rd.ReadBits8(2) + 1 // Profile, the MPEG-4 Audio Object Type minus 1
|
||||||
|
sampleRateIdx := rd.ReadBits8(4) // MPEG-4 Sampling Frequency Index
|
||||||
|
_ = rd.ReadBit() // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
|
||||||
|
channels := rd.ReadBits16(3) // MPEG-4 Channel Configuration
|
||||||
|
|
||||||
|
//_ = rd.ReadBit() // Originality, set to 1 to signal originality of the audio and 0 otherwise
|
||||||
|
//_ = rd.ReadBit() // Home, set to 1 to signal home usage of the audio and 0 otherwise
|
||||||
|
//_ = rd.ReadBit() // Copyright ID bit
|
||||||
|
//_ = rd.ReadBit() // Copyright ID start
|
||||||
|
//_ = rd.ReadBits(13) // Frame length
|
||||||
|
//_ = rd.ReadBits(11) // Buffer fullness
|
||||||
|
//_ = rd.ReadBits(2) // Number of AAC frames (Raw Data Blocks) in ADTS frame minus 1
|
||||||
|
//_ = rd.ReadBits(16) // CRC check
|
||||||
|
|
||||||
|
// 3. Encode RTP config
|
||||||
|
wr := bits.NewWriter(nil)
|
||||||
|
wr.WriteBits8(objType, 5)
|
||||||
|
wr.WriteBits8(sampleRateIdx, 4)
|
||||||
|
wr.WriteBits16(channels, 4)
|
||||||
|
conf := wr.Bytes()
|
||||||
|
|
||||||
|
codec := &core.Codec{
|
||||||
|
Name: core.CodecAAC,
|
||||||
|
ClockRate: sampleRates[sampleRateIdx],
|
||||||
|
Channels: channels,
|
||||||
|
FmtpLine: FMTP + hex.EncodeToString(conf),
|
||||||
|
}
|
||||||
|
return codec
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadADTSSize(b []byte) uint16 {
|
||||||
|
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
|
||||||
|
_ = b[5] // bounds
|
||||||
|
return uint16(b[3]&0x03)<<(8+3) | uint16(b[4])<<3 | uint16(b[5]>>5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteADTSSize(b []byte, size uint16) {
|
||||||
|
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
|
||||||
|
_ = b[5] // bounds
|
||||||
|
b[3] |= byte(size >> (8 + 3))
|
||||||
|
b[4] = byte(size >> 3)
|
||||||
|
b[5] |= byte(size << 5)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ADTSTimeSize(b []byte) uint32 {
|
||||||
|
var units uint32
|
||||||
|
for len(b) > ADTSHeaderSize {
|
||||||
|
auSize := ReadADTSSize(b)
|
||||||
|
b = b[auSize:]
|
||||||
|
units++
|
||||||
|
}
|
||||||
|
return units * AUTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func CodecToADTS(codec *core.Codec) []byte {
|
||||||
|
s := core.Between(codec.FmtpLine, "config=", ";")
|
||||||
|
conf, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
objType, sampleFreqIdx, channels, _ := DecodeConfig(conf)
|
||||||
|
profile := objType - 1
|
||||||
|
|
||||||
|
wr := bits.NewWriter(nil)
|
||||||
|
wr.WriteAllBits(1, 12) // Syncword, all bits must be set to 1
|
||||||
|
wr.WriteBit(0) // MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2
|
||||||
|
wr.WriteBits8(0, 2) // Layer, always set to 0
|
||||||
|
wr.WriteBit(1) // Protection absence, set to 1 if there is no CRC and 0 if there is CRC
|
||||||
|
wr.WriteBits8(profile, 2) // Profile, the MPEG-4 Audio Object Type minus 1
|
||||||
|
wr.WriteBits8(sampleFreqIdx, 4) // MPEG-4 Sampling Frequency Index
|
||||||
|
wr.WriteBit(0) // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
|
||||||
|
wr.WriteBits8(channels, 3) // MPEG-4 Channel Configuration
|
||||||
|
wr.WriteBit(0) // Originality, set to 1 to signal originality of the audio and 0 otherwise
|
||||||
|
wr.WriteBit(0) // Home, set to 1 to signal home usage of the audio and 0 otherwise
|
||||||
|
wr.WriteBit(0) // Copyright ID bit
|
||||||
|
wr.WriteBit(0) // Copyright ID start
|
||||||
|
wr.WriteBits16(0, 13) // Frame length
|
||||||
|
wr.WriteAllBits(1, 11) // Buffer fullness (variable bitrate)
|
||||||
|
wr.WriteBits8(0, 2) // Number of AAC frames (Raw Data Blocks) in ADTS frame minus 1
|
||||||
|
|
||||||
|
return wr.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeToADTS(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
|
adts := CodecToADTS(codec)
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) {
|
||||||
|
if !IsADTS(packet.Payload) {
|
||||||
|
b := make([]byte, ADTSHeaderSize+len(packet.Payload))
|
||||||
|
copy(b, adts)
|
||||||
|
copy(b[ADTSHeaderSize:], packet.Payload)
|
||||||
|
WriteADTSSize(b, uint16(len(b)))
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = b
|
||||||
|
handler(&clone)
|
||||||
|
} else {
|
||||||
|
handler(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package aac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Consumer struct {
|
||||||
|
core.SuperConsumer
|
||||||
|
wr *core.WriteBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConsumer() *Consumer {
|
||||||
|
cons := &Consumer{
|
||||||
|
wr: core.NewWriteBuffer(nil),
|
||||||
|
}
|
||||||
|
cons.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionSendonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecAAC},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cons
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||||
|
sender := core.NewSender(media, track.Codec)
|
||||||
|
|
||||||
|
sender.Handler = func(pkt *rtp.Packet) {
|
||||||
|
if n, err := c.wr.Write(pkt.Payload); err == nil {
|
||||||
|
c.Send += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
sender.Handler = RTPToADTS(track.Codec, sender.Handler)
|
||||||
|
} else {
|
||||||
|
sender.Handler = EncodeToADTS(track.Codec, sender.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.HandleRTP(track)
|
||||||
|
c.Senders = append(c.Senders, sender)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
|
||||||
|
return c.wr.WriteTo(wr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Stop() error {
|
||||||
|
_ = c.SuperConsumer.Close()
|
||||||
|
return c.wr.Close()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
+71
-8
@@ -2,11 +2,13 @@ package aac
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const RTPPacketVersionAAC = 0
|
const RTPPacketVersionAAC = 0
|
||||||
|
const ADTSHeaderSize = 7
|
||||||
|
|
||||||
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||||
var timestamp uint32
|
var timestamp uint32
|
||||||
@@ -14,28 +16,37 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
|||||||
return func(packet *rtp.Packet) {
|
return func(packet *rtp.Packet) {
|
||||||
// support ONLY 2 bytes header size!
|
// support ONLY 2 bytes header size!
|
||||||
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
|
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3640
|
||||||
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
|
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
|
||||||
|
|
||||||
//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:]
|
||||||
units = units[unitSize:]
|
units = units[unitSize:]
|
||||||
|
|
||||||
timestamp += 1024
|
timestamp += AUTime
|
||||||
|
|
||||||
clone := *packet
|
clone := *packet
|
||||||
clone.Version = RTPPacketVersionAAC
|
clone.Version = RTPPacketVersionAAC
|
||||||
clone.Timestamp = timestamp
|
clone.Timestamp = timestamp
|
||||||
if IsADTS(unit) {
|
if IsADTS(unit) {
|
||||||
clone.Payload = unit[7:]
|
clone.Payload = unit[ADTSHeaderSize:]
|
||||||
} else {
|
} else {
|
||||||
clone.Payload = unit
|
clone.Payload = unit
|
||||||
}
|
}
|
||||||
@@ -54,11 +65,11 @@ func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// support ONLY one unit in payload
|
// support ONLY one unit in payload
|
||||||
size := uint16(len(packet.Payload))
|
auSize := uint16(len(packet.Payload))
|
||||||
// 2 bytes header size + 2 bytes first payload size
|
// 2 bytes header size + 2 bytes first payload size
|
||||||
payload := make([]byte, 2+2+size)
|
payload := make([]byte, 2+2+auSize)
|
||||||
payload[1] = 16 // header size in bits
|
payload[1] = 16 // header size in bits
|
||||||
binary.BigEndian.PutUint16(payload[2:], size<<3)
|
binary.BigEndian.PutUint16(payload[2:], auSize<<3)
|
||||||
copy(payload[4:], packet.Payload)
|
copy(payload[4:], packet.Payload)
|
||||||
|
|
||||||
clone := rtp.Packet{
|
clone := rtp.Packet{
|
||||||
@@ -74,6 +85,58 @@ func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsADTS(b []byte) bool {
|
func ADTStoRTP(src []byte) (dst []byte) {
|
||||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
|
dst = make([]byte, 2) // header bytes
|
||||||
|
for i, n := 0, len(src)-ADTSHeaderSize; i < n; {
|
||||||
|
auSize := ReadADTSSize(src[i:])
|
||||||
|
dst = append(dst, byte(auSize>>5), byte(auSize<<3)) // size in bits
|
||||||
|
i += int(auSize)
|
||||||
|
}
|
||||||
|
hdrSize := uint16(len(dst) - 2)
|
||||||
|
binary.BigEndian.PutUint16(dst, hdrSize<<3) // size in bits
|
||||||
|
return append(dst, src...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RTPTimeSize(b []byte) uint32 {
|
||||||
|
// convert RTP header size to units count
|
||||||
|
units := binary.BigEndian.Uint16(b) >> 4
|
||||||
|
return uint32(units) * AUTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func RTPToADTS(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
|
adts := CodecToADTS(codec)
|
||||||
|
|
||||||
|
return func(packet *rtp.Packet) {
|
||||||
|
src := packet.Payload
|
||||||
|
dst := make([]byte, 0, len(src))
|
||||||
|
|
||||||
|
headersSize := binary.BigEndian.Uint16(src) >> 3
|
||||||
|
headers := src[2 : 2+headersSize]
|
||||||
|
units := src[2+headersSize:]
|
||||||
|
|
||||||
|
for len(headers) > 0 {
|
||||||
|
unitSize := binary.BigEndian.Uint16(headers) >> 3
|
||||||
|
headers = headers[2:]
|
||||||
|
unit := units[:unitSize]
|
||||||
|
units = units[unitSize:]
|
||||||
|
|
||||||
|
if !IsADTS(unit) {
|
||||||
|
i := len(dst)
|
||||||
|
dst = append(dst, adts...)
|
||||||
|
WriteADTSSize(dst[i:], ADTSHeaderSize+uint16(len(unit)))
|
||||||
|
}
|
||||||
|
|
||||||
|
dst = append(dst, unit...)
|
||||||
|
}
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Version = RTPPacketVersionAAC
|
||||||
|
clone.Payload = dst
|
||||||
|
handler(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RTPToCodec(b []byte) *core.Codec {
|
||||||
|
hdrSize := binary.BigEndian.Uint16(b) / 8
|
||||||
|
return ADTSToCodec(b[2+hdrSize:])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package bits
|
||||||
|
|
||||||
|
type Reader struct {
|
||||||
|
EOF bool // if end of buffer raised during reading
|
||||||
|
|
||||||
|
buf []byte // total buf
|
||||||
|
byte byte // current byte
|
||||||
|
bits byte // bits left in byte
|
||||||
|
pos int // current pos in buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReader(b []byte) *Reader {
|
||||||
|
return &Reader{buf: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
//goland:noinspection GoStandardMethods
|
||||||
|
func (r *Reader) ReadByte() byte {
|
||||||
|
if r.bits != 0 {
|
||||||
|
return r.ReadBits8(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.pos >= len(r.buf) {
|
||||||
|
r.EOF = true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
b := r.buf[r.pos]
|
||||||
|
r.pos++
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadUint16() uint16 {
|
||||||
|
if r.bits != 0 {
|
||||||
|
return r.ReadBits16(16)
|
||||||
|
}
|
||||||
|
return uint16(r.ReadByte())<<8 | uint16(r.ReadByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadUint24() uint32 {
|
||||||
|
if r.bits != 0 {
|
||||||
|
return r.ReadBits(24)
|
||||||
|
}
|
||||||
|
return uint32(r.ReadByte())<<16 | uint32(r.ReadByte())<<8 | uint32(r.ReadByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadUint32() uint32 {
|
||||||
|
if r.bits != 0 {
|
||||||
|
return r.ReadBits(32)
|
||||||
|
}
|
||||||
|
return uint32(r.ReadByte())<<24 | uint32(r.ReadByte())<<16 | uint32(r.ReadByte())<<8 | uint32(r.ReadByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadBit() byte {
|
||||||
|
if r.bits == 0 {
|
||||||
|
r.byte = r.ReadByte()
|
||||||
|
r.bits = 7
|
||||||
|
} else {
|
||||||
|
r.bits--
|
||||||
|
}
|
||||||
|
|
||||||
|
return (r.byte >> r.bits) & 0b1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadBits(n byte) (res uint32) {
|
||||||
|
for i := n - 1; i != 255; i-- {
|
||||||
|
res |= uint32(r.ReadBit()) << i
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadBits8(n byte) (res uint8) {
|
||||||
|
for i := n - 1; i != 255; i-- {
|
||||||
|
res |= r.ReadBit() << i
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadBits16(n byte) (res uint16) {
|
||||||
|
for i := n - 1; i != 255; i-- {
|
||||||
|
res |= uint16(r.ReadBit()) << i
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadBits64(n byte) (res uint64) {
|
||||||
|
for i := n - 1; i != 255; i-- {
|
||||||
|
res |= uint64(r.ReadBit()) << i
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ReadBytes(n int) (b []byte) {
|
||||||
|
if r.bits == 0 {
|
||||||
|
if r.pos+n > len(r.buf) {
|
||||||
|
r.EOF = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b = r.buf[r.pos : r.pos+n]
|
||||||
|
r.pos += n
|
||||||
|
} else {
|
||||||
|
b = make([]byte, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
b[i] = r.ReadByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadUEGolomb - ReadExponentialGolomb (unsigned)
|
||||||
|
func (r *Reader) ReadUEGolomb() uint32 {
|
||||||
|
var size byte
|
||||||
|
for size = 0; size < 32; size++ {
|
||||||
|
if b := r.ReadBit(); b != 0 || r.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.ReadBits(size) + (1 << size) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadSEGolomb - ReadSignedExponentialGolomb
|
||||||
|
func (r *Reader) ReadSEGolomb() int32 {
|
||||||
|
if b := r.ReadUEGolomb(); b%2 == 0 {
|
||||||
|
return -int32(b >> 1)
|
||||||
|
} else {
|
||||||
|
return int32(b >> 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Left() []byte {
|
||||||
|
return r.buf[r.pos:]
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package bits
|
||||||
|
|
||||||
|
type Writer struct {
|
||||||
|
buf []byte // total buf
|
||||||
|
byte *byte // pointer to current byte
|
||||||
|
bits byte // bits left in byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWriter(buf []byte) *Writer {
|
||||||
|
return &Writer{buf: buf}
|
||||||
|
}
|
||||||
|
|
||||||
|
//goland:noinspection GoStandardMethods
|
||||||
|
func (w *Writer) WriteByte(b byte) {
|
||||||
|
if w.bits != 0 {
|
||||||
|
w.WriteBits8(b, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.buf = append(w.buf, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteBit(b byte) {
|
||||||
|
if w.bits == 0 {
|
||||||
|
w.buf = append(w.buf, 0)
|
||||||
|
w.byte = &w.buf[len(w.buf)-1]
|
||||||
|
w.bits = 7
|
||||||
|
} else {
|
||||||
|
w.bits--
|
||||||
|
}
|
||||||
|
|
||||||
|
*w.byte |= (b & 1) << w.bits
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteBits(v uint32, n byte) {
|
||||||
|
for i := n - 1; i != 255; i-- {
|
||||||
|
w.WriteBit(byte(v>>i) & 0b1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteBits16(v uint16, n byte) {
|
||||||
|
for i := n - 1; i != 255; i-- {
|
||||||
|
w.WriteBit(byte(v>>i) & 0b1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteBits8(v, n byte) {
|
||||||
|
for i := n - 1; i != 255; i-- {
|
||||||
|
w.WriteBit((v >> i) & 0b1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteAllBits(bit, n byte) {
|
||||||
|
for i := byte(0); i < n; i++ {
|
||||||
|
w.WriteBit(bit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteBool(b bool) {
|
||||||
|
if b {
|
||||||
|
w.WriteBit(1)
|
||||||
|
} else {
|
||||||
|
w.WriteBit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteUint16(v uint16) {
|
||||||
|
if w.bits != 0 {
|
||||||
|
w.WriteBits16(v, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.buf = append(w.buf, byte(v>>8), byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) WriteBytes(bytes ...byte) {
|
||||||
|
if w.bits != 0 {
|
||||||
|
for _, b := range bytes {
|
||||||
|
w.WriteByte(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.buf = append(w.buf, bytes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Bytes() []byte {
|
||||||
|
return w.buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Len() int {
|
||||||
|
return len(w.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Reset() {
|
||||||
|
w.buf = w.buf[:0]
|
||||||
|
w.bits = 0
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"time"
|
"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/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
@@ -132,7 +132,7 @@ func (c *Client) Dial() (err error) {
|
|||||||
// <stream1 name="360p.265" size="640x360" x1="yes" x2="yes" x4="yes" />
|
// <stream1 name="360p.265" size="640x360" x1="yes" x2="yes" x4="yes" />
|
||||||
// <vin0>
|
// <vin0>
|
||||||
// </bubble>
|
// </bubble>
|
||||||
re := regexp.MustCompile("<stream " + stream + `[^>]+`)
|
re := regexp.MustCompile("<stream" + stream + " [^>]+")
|
||||||
stream = re.FindString(string(xml))
|
stream = re.FindString(string(xml))
|
||||||
if strings.Contains(stream, ".265") {
|
if strings.Contains(stream, ".265") {
|
||||||
c.videoCodec = core.CodecH265
|
c.videoCodec = core.CodecH265
|
||||||
@@ -226,7 +226,7 @@ func (c *Client) Handle() error {
|
|||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Timestamp: core.Now90000(),
|
Timestamp: core.Now90000(),
|
||||||
},
|
},
|
||||||
Payload: h264.AnnexB2AVC(b[6:]),
|
Payload: annexb.EncodeToAVCC(b[6:], false),
|
||||||
}
|
}
|
||||||
c.videoTrack.WriteRTP(pkt)
|
c.videoTrack.WriteRTP(pkt)
|
||||||
} else {
|
} else {
|
||||||
@@ -245,6 +245,7 @@ func (c *Client) Handle() error {
|
|||||||
pkt := &rtp.Packet{
|
pkt := &rtp.Packet{
|
||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Version: 2,
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
Timestamp: audioTS,
|
Timestamp: audioTS,
|
||||||
},
|
},
|
||||||
Payload: b[6+36:],
|
Payload: b[6+36:],
|
||||||
|
|||||||
@@ -52,6 +52,30 @@ func (c *Codec) IsRTP() bool {
|
|||||||
return c.PayloadType != PayloadTypeRAW
|
return c.PayloadType != PayloadTypeRAW
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Codec) IsVideo() bool {
|
||||||
|
return c.Kind() == KindVideo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) IsAudio() bool {
|
||||||
|
return c.Kind() == KindAudio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) Kind() string {
|
||||||
|
return GetKind(c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Codec) PrintName() string {
|
||||||
|
switch c.Name {
|
||||||
|
case CodecAAC:
|
||||||
|
return "AAC"
|
||||||
|
case CodecPCM:
|
||||||
|
return "S16B"
|
||||||
|
case CodecPCML:
|
||||||
|
return "S16L"
|
||||||
|
}
|
||||||
|
return c.Name
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Codec) Clone() *Codec {
|
func (c *Codec) Clone() *Codec {
|
||||||
clone := *c
|
clone := *c
|
||||||
return &clone
|
return &clone
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ type Producer interface {
|
|||||||
// GetTrack - return Receiver, that can only produce rtp.Packet(s)
|
// GetTrack - return Receiver, that can only produce rtp.Packet(s)
|
||||||
GetTrack(media *Media, codec *Codec) (*Receiver, error)
|
GetTrack(media *Media, codec *Codec) (*Receiver, error)
|
||||||
|
|
||||||
|
// Deprecated: rename to Run()
|
||||||
Start() error
|
Start() error
|
||||||
|
|
||||||
|
// Deprecated: rename to Close()
|
||||||
Stop() error
|
Stop() error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ type Consumer interface {
|
|||||||
|
|
||||||
AddTrack(media *Media, codec *Codec, track *Receiver) error
|
AddTrack(media *Media, codec *Codec, track *Receiver) error
|
||||||
|
|
||||||
|
// Deprecated: rename to Close()
|
||||||
Stop() error
|
Stop() error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +94,7 @@ type Info struct {
|
|||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||||
UserAgent string `json:"user_agent,omitempty"`
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
|
SDP string `json:"sdp,omitempty"`
|
||||||
Medias []*Media `json:"medias,omitempty"`
|
Medias []*Media `json:"medias,omitempty"`
|
||||||
Receivers []*Receiver `json:"receivers,omitempty"`
|
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||||
Senders []*Sender `json:"senders,omitempty"`
|
Senders []*Sender `json:"senders,omitempty"`
|
||||||
@@ -101,3 +106,72 @@ const (
|
|||||||
UnsupportedCodec = "unsupported codec"
|
UnsupportedCodec = "unsupported codec"
|
||||||
WrongMediaDirection = "wrong media direction"
|
WrongMediaDirection = "wrong media direction"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SuperProducer struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
SDP string `json:"sdp,omitempty"`
|
||||||
|
Medias []*Media `json:"medias,omitempty"`
|
||||||
|
Receivers []*Receiver `json:"receivers,omitempty"`
|
||||||
|
Recv int `json:"recv,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SuperProducer) GetMedias() []*Media {
|
||||||
|
return s.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
|
||||||
|
for _, receiver := range s.Receivers {
|
||||||
|
if receiver.Codec == codec {
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiver := NewReceiver(media, codec)
|
||||||
|
s.Receivers = append(s.Receivers, receiver)
|
||||||
|
return receiver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SuperProducer) Close() error {
|
||||||
|
for _, receiver := range s.Receivers {
|
||||||
|
receiver.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SuperConsumer struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||||
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
|
SDP string `json:"sdp,omitempty"`
|
||||||
|
Medias []*Media `json:"medias,omitempty"`
|
||||||
|
Senders []*Sender `json:"senders,omitempty"`
|
||||||
|
Send int `json:"send,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SuperConsumer) GetMedias() []*Media {
|
||||||
|
return s.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
// return 0, nil
|
||||||
|
//}
|
||||||
|
|
||||||
|
func (s *SuperConsumer) Close() error {
|
||||||
|
for _, sender := range s.Senders {
|
||||||
|
sender.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SuperConsumer) Codecs() []*Codec {
|
||||||
|
codecs := make([]*Codec, len(s.Senders))
|
||||||
|
for i, sender := range s.Senders {
|
||||||
|
codecs[i] = sender.Codec
|
||||||
|
}
|
||||||
|
return codecs
|
||||||
|
}
|
||||||
|
|||||||
+18
-21
@@ -1,14 +1,20 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
cryptorand "crypto/rand"
|
"crypto/rand"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BufferSize = 64 * 1024 // 64K
|
||||||
|
ConnDialTimeout = time.Second * 3
|
||||||
|
ConnDeadline = time.Second * 5
|
||||||
|
ProbeTimeout = time.Second * 3
|
||||||
|
)
|
||||||
|
|
||||||
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
|
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
|
||||||
func Now90000() uint32 {
|
func Now90000() uint32 {
|
||||||
return uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
|
return uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
|
||||||
@@ -16,27 +22,22 @@ func Now90000() uint32 {
|
|||||||
|
|
||||||
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
|
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
|
||||||
|
|
||||||
// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols
|
// RandString base10 - numbers, base16 - hex, base36 - digits+letters
|
||||||
|
// base64 - URL safe symbols, base0 - crypto random
|
||||||
func RandString(size, base byte) string {
|
func RandString(size, base byte) string {
|
||||||
b := make([]byte, size)
|
b := make([]byte, size)
|
||||||
if _, err := cryptorand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if base == 0 {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
for i := byte(0); i < size; i++ {
|
for i := byte(0); i < size; i++ {
|
||||||
b[i] = symbols[b[i]%base]
|
b[i] = symbols[b[i]%base]
|
||||||
}
|
}
|
||||||
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 {
|
||||||
@@ -44,12 +45,7 @@ func Between(s, sub1, sub2 string) string {
|
|||||||
}
|
}
|
||||||
s = s[i+len(sub1):]
|
s = s[i+len(sub1):]
|
||||||
|
|
||||||
if len(sub2) == 1 {
|
if i = strings.Index(s, sub2); i >= 0 {
|
||||||
i = strings.IndexByte(s, sub2[0])
|
|
||||||
} else {
|
|
||||||
i = strings.Index(s, sub2)
|
|
||||||
}
|
|
||||||
if i >= 0 {
|
|
||||||
return s[:i]
|
return s[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +53,9 @@ func Between(s, sub1, sub2 string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Atoi(s string) (i int) {
|
func Atoi(s string) (i int) {
|
||||||
i, _ = strconv.Atoi(s)
|
if s != "" {
|
||||||
|
i, _ = strconv.Atoi(s)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +67,6 @@ func Assert(ok bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Caller() string {
|
func Caller() string {
|
||||||
log.Error().Caller(0).Send()
|
|
||||||
_, file, line, _ := runtime.Caller(1)
|
_, file, line, _ := runtime.Caller(1)
|
||||||
return file + ":" + strconv.Itoa(line)
|
return file + ":" + strconv.Itoa(line)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type Probe struct {
|
|
||||||
deadline time.Time
|
|
||||||
items map[any]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProbe(enable bool) *Probe {
|
|
||||||
if enable {
|
|
||||||
return &Probe{
|
|
||||||
deadline: time.Now().Add(time.Second * 3),
|
|
||||||
items: map[any]struct{}{},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active return true if probe enabled and not finish
|
|
||||||
func (p *Probe) Active() bool {
|
|
||||||
return len(p.items) < 2 && time.Now().Before(p.deadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append safe to run if Probe is nil
|
|
||||||
func (p *Probe) Append(v any) {
|
|
||||||
if p != nil {
|
|
||||||
p.items[v] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProbeSize
|
||||||
|
// in my tests MPEG-TS 40Mbit/s 4K-video require more than 1MB for probe
|
||||||
|
const ProbeSize = 5 * 1024 * 1024 // 5MB
|
||||||
|
|
||||||
|
const (
|
||||||
|
BufferDisable = 0
|
||||||
|
BufferDrainAndClear = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadBuffer support buffering and Seek over buffer
|
||||||
|
// positive BufferSize will enable buffering mode
|
||||||
|
// Seek to negative offset will clear buffer
|
||||||
|
// Seek with a positive BufferSize will continue buffering after the last read from the buffer
|
||||||
|
// Seek with a negative BufferSize will clear buffer after the last read from the buffer
|
||||||
|
// Read more than BufferSize will raise error
|
||||||
|
type ReadBuffer struct {
|
||||||
|
io.Reader
|
||||||
|
|
||||||
|
BufferSize int
|
||||||
|
|
||||||
|
buf []byte
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReadBuffer(rd io.Reader) *ReadBuffer {
|
||||||
|
if rs, ok := rd.(*ReadBuffer); ok {
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
return &ReadBuffer{Reader: rd}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadBuffer) Read(p []byte) (n int, err error) {
|
||||||
|
// with zero buffer - read as usual
|
||||||
|
if r.BufferSize == BufferDisable {
|
||||||
|
return r.Reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if buffer not empty - read from it
|
||||||
|
if r.pos < len(r.buf) {
|
||||||
|
n = copy(p, r.buf[r.pos:])
|
||||||
|
r.pos += n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// with negative buffer - empty it and read as usual
|
||||||
|
if r.BufferSize < 0 {
|
||||||
|
r.BufferSize = BufferDisable
|
||||||
|
r.buf = nil
|
||||||
|
r.pos = 0
|
||||||
|
|
||||||
|
return r.Reader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Reader.Read(p)
|
||||||
|
if len(r.buf)+n > r.BufferSize {
|
||||||
|
return 0, errors.New("probe reader overflow")
|
||||||
|
}
|
||||||
|
r.buf = append(r.buf, p[:n]...)
|
||||||
|
r.pos += n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadBuffer) Close() error {
|
||||||
|
if closer, ok := r.Reader.(io.Closer); ok {
|
||||||
|
return closer.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadBuffer) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
var pos int
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
pos = int(offset)
|
||||||
|
case io.SeekCurrent:
|
||||||
|
pos = r.pos + int(offset)
|
||||||
|
case io.SeekEnd:
|
||||||
|
pos = len(r.buf) + int(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// negative offset - empty buffer
|
||||||
|
if pos < 0 {
|
||||||
|
r.buf = nil
|
||||||
|
r.pos = 0
|
||||||
|
} else if pos >= len(r.buf) {
|
||||||
|
r.pos = len(r.buf)
|
||||||
|
} else {
|
||||||
|
r.pos = pos
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(r.pos), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadBuffer) Peek(n int) ([]byte, error) {
|
||||||
|
r.BufferSize = n
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := io.ReadAtLeast(r, b, n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.Reset()
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadBuffer) Reset() {
|
||||||
|
r.BufferSize = BufferDrainAndClear
|
||||||
|
r.pos = 0
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user