Compare commits
363 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfe47559d1 | |||
| eabd7d60cd | |||
| dfda4b11ff | |||
| 353262307b | |||
| d734140eaf | |||
| 2409bb56d7 | |||
| df484cc904 | |||
| 7e0c7a8173 | |||
| 7eaa4a1b55 | |||
| 03941a5691 | |||
| 28821c41e0 | |||
| 8636e96379 | |||
| 7119384184 | |||
| 57b0ace802 | |||
| b0f46bc919 | |||
| a4d4598a13 | |||
| 17c1f69f66 | |||
| fb31a251b8 | |||
| c5277daa45 | |||
| 76a5e160c2 | |||
| 494daed937 | |||
| a86e10446a | |||
| 209b73a0f1 | |||
| 54473ff1de | |||
| 7eb5fe0355 | |||
| fbd5215669 | |||
| 3ebc115cc5 | |||
| 6d1a95a4e3 | |||
| 4ec2849008 | |||
| 4ef6a147a6 | |||
| 94df080bf7 | |||
| 86edd814c9 | |||
| 5a259993d8 | |||
| b6fe8871df | |||
| b47a2ba73c | |||
| 4934fa4cc1 | |||
| 61f74820bc | |||
| 248fc7a11a | |||
| aa0ece2d1e | |||
| 68036b68c1 | |||
| 319dbf2c63 | |||
| 4b419309a8 | |||
| 42e7a03534 | |||
| 290e8fcfda | |||
| 6c78b5cb53 | |||
| c72b205d87 | |||
| 2cd009646a | |||
| 8f5fce4d73 | |||
| b705cadc04 | |||
| 2523a5ac38 | |||
| e246e2e756 | |||
| 2dc0d58ba7 | |||
| cb22ae7833 | |||
| c98b0a83c4 | |||
| 0bae158e41 | |||
| e2b63a4f6c | |||
| 3897f10a4d | |||
| ac80f1470e | |||
| 1fe602679e | |||
| e2c7d06730 | |||
| 2133f5323c | |||
| c10a06d199 | |||
| d053d88ce9 | |||
| 2ce38b4486 | |||
| 44d59b1696 | |||
| 15ec995ecc | |||
| 231cab36b2 | |||
| 640db3029e | |||
| 2836fdae13 | |||
| 964bb225fa | |||
| 5cc32197b8 | |||
| bc1a4ac8e4 | |||
| 158f9d3a08 | |||
| 81cfcf877a | |||
| 96919bf9e3 | |||
| e4359ac217 | |||
| 56d7a6fee4 | |||
| 292b32af99 | |||
| 33e4527042 | |||
| 62a9046f01 | |||
| 25e7ac531e | |||
| 0f27bb1124 | |||
| 0ff3bf67e1 | |||
| 6d3d45e337 | |||
| c6940eb0f3 | |||
| 8142d2fc43 | |||
| 721ed98afb | |||
| fb8c6e1b1b | |||
| 80ab32379c | |||
| 863174839c | |||
| ff18283d11 | |||
| 994e0dc526 | |||
| 7254bd4fbc | |||
| 9f407a754d | |||
| cc97bc33c4 | |||
| bd8e4fa298 | |||
| 6db4dda535 | |||
| be80eb1ac9 | |||
| a8d2312cb0 | |||
| 5bbc2aaf2e | |||
| 7abc963a50 | |||
| f8c88cfbe0 | |||
| cf4acd5a8d | |||
| 19226df7d6 | |||
| c415c8f2af | |||
| 2751f41afb | |||
| d59cb99f0d | |||
| 007e8dbc75 | |||
| dae396a1ed | |||
| d894483166 | |||
| 60ef52f44b | |||
| 240e1960f8 | |||
| a107d13e61 | |||
| f911936d79 | |||
| ea23957f2a | |||
| f1971cec7c | |||
| 7291c03cea | |||
| fdb3116027 | |||
| c87885be48 | |||
| 98f88d037e | |||
| fde1fdc592 | |||
| cca216ace5 | |||
| e953e949ef | |||
| fe2cc4b525 | |||
| 6a67fc3712 | |||
| 94b7c33485 | |||
| 887f0f4890 | |||
| ec08dfee9c | |||
| 54b95dced4 | |||
| 37d7409e82 | |||
| 4dd1f73a18 | |||
| 22cc8ac2c4 | |||
| a667acad07 | |||
| ee5e31d3b3 | |||
| c196b82a72 | |||
| 670370056c | |||
| 0c5a2bf02b | |||
| 8f9e78be0c | |||
| 50d5fa93b6 | |||
| 3a0e4078fd | |||
| 549da0257e | |||
| 2b5f9429a8 | |||
| c7119f4403 | |||
| 7d9862202a | |||
| dd7130d2d4 | |||
| d697bdcf05 | |||
| abd61919cf | |||
| ad2383de80 | |||
| 9f3ff98951 | |||
| d286980548 | |||
| 87533ac5a1 | |||
| d05451416d | |||
| 5c01cbad9e | |||
| df95ce39d0 | |||
| 26f16e392f | |||
| fcc837e570 | |||
| fd682306e7 | |||
| e072842b7a | |||
| 3b976c6812 | |||
| 40269328fb | |||
| 45cbbaf1cf | |||
| 3f542a642c | |||
| 8b4df5f02c | |||
| 788afb7189 | |||
| cd7fa5d09c | |||
| beb82045ff | |||
| 33f0fd5fe6 | |||
| 850051a299 | |||
| 8f7cbdf60a | |||
| 4577390130 | |||
| f2242e31c8 | |||
| 975a43d392 | |||
| 3d38e5e567 | |||
| 7d2ad92c4b | |||
| b82023bc32 | |||
| a92e04b6e0 | |||
| 56e61a85ee | |||
| 7d83b0d6c8 | |||
| 708277230a | |||
| 06e6e31443 | |||
| 8474f2f571 | |||
| 9ddea7d9d6 | |||
| c2fbd372b3 | |||
| e9611769be | |||
| c5dfa84ff2 | |||
| da68101c09 | |||
| 30c418542c | |||
| b58c1a7ed6 | |||
| 47e87281a1 | |||
| 60b6b93ff8 | |||
| 3149b6f750 | |||
| e44f1ad53e | |||
| c38c8a7fce | |||
| 9efc717633 | |||
| a2d422f5cb | |||
| 3036dd7cfe | |||
| 5be5d9247c | |||
| 42b7eea852 | |||
| 7ee3f6e4f7 | |||
| fbd8d995ed | |||
| 998c85d6f5 | |||
| 67dfc942a0 | |||
| 1cc8b373de | |||
| f045f3fccd | |||
| 691f6d9cdd | |||
| 8a8fb66eeb | |||
| e7044a93f6 | |||
| a9bcb46f38 | |||
| 16a812c8b8 | |||
| 27fe2622ec | |||
| 3d222136f9 | |||
| 524cdb7176 | |||
| 499dc10390 | |||
| e7bd3d401f | |||
| 5ec942cb5e | |||
| 6c255cd2f2 | |||
| 4f969d750a | |||
| bd2cbe20e0 | |||
| 6d8d6a91ef | |||
| 05c12b34e5 | |||
| 43b7a662c1 | |||
| a7e76db464 | |||
| b797a2fcd1 | |||
| 6e35f1a389 | |||
| 30d48e139c | |||
| e74fc6f198 | |||
| 34b103bbcb | |||
| 22fbd8bc9e | |||
| d175213369 | |||
| e00d211619 | |||
| c68e3cafe4 | |||
| 5a34483513 | |||
| 7bb0f0d2e6 | |||
| 96d7066085 | |||
| ae49946740 | |||
| fcc91c9b8a | |||
| 994fd41826 | |||
| 3282b38900 | |||
| 647b2acf48 | |||
| ef318f663e | |||
| 5771454400 | |||
| 6732e726d5 | |||
| 230c80c70e | |||
| 45503aa8c5 | |||
| a4d7fd0d95 | |||
| b6579122d1 | |||
| 42a67f8ad5 | |||
| 91eeefec68 | |||
| 8ab7aeb8b2 | |||
| 493fa1ef6a | |||
| 020549ef60 | |||
| dfc1f45f97 | |||
| 641e65ee95 | |||
| 24ca87e00d | |||
| 859cd1cbe6 | |||
| 79656d1344 | |||
| 759f979182 | |||
| 7c17e64090 | |||
| bf45f64a7e | |||
| 72890d5983 | |||
| e8e798d955 | |||
| 8a8b379bfc | |||
| ca491def83 | |||
| 5a597277a9 | |||
| ae8145f266 | |||
| c90fcd1ce1 | |||
| e0687db9e2 | |||
| 24310e2f7a | |||
| a1f0b86ab3 | |||
| 7f87c6e478 | |||
| a0145b4b24 | |||
| 2fcbb1d836 | |||
| a2beea1bbd | |||
| e5e55b7a50 | |||
| 0830d8342e | |||
| adb1b21e81 | |||
| edfa09bb9f | |||
| 2eef7bdbd3 | |||
| 124556f4db | |||
| d528e167db | |||
| f151969fe1 | |||
| 7107508286 | |||
| cd2f90a7a1 | |||
| e1577b5ad3 | |||
| 251686608a | |||
| 993aa613fd | |||
| 3c1f7e4181 | |||
| 2ed67648c3 | |||
| 2fdfec6f21 | |||
| a0d8f3ae81 | |||
| 6d37cceb91 | |||
| fce41f4fc1 | |||
| c50e894a42 | |||
| 890fd78a6a | |||
| 518cae1476 | |||
| 545a105ba0 | |||
| 70b4bf779e | |||
| 7cf672da84 | |||
| fd5746a954 | |||
| 2c3813deb9 | |||
| 80f57a0292 | |||
| 3b7309d9f7 | |||
| 6df1e68a5f | |||
| df2e982090 | |||
| 902af5e5d7 | |||
| 7fe23c7bc5 | |||
| d0c3cb066c | |||
| 5666943559 | |||
| 1b41f61247 | |||
| e1342f06b7 | |||
| f535595d1f | |||
| 7415776e4d | |||
| bad7caa187 | |||
| 2473eee66b | |||
| a8b51ad619 | |||
| 49c4d45731 | |||
| f45fef29d8 | |||
| 699a995e8c | |||
| ce02b03a73 | |||
| 3e1b01073b | |||
| 3e4dce2413 | |||
| fef3091ecc | |||
| af7509ebaf | |||
| 487527f5a5 | |||
| bfd26560b1 | |||
| be3a1c5b5f | |||
| 1282b23c57 | |||
| 54afd0b50b | |||
| 0669cfbebf | |||
| d99bf122ea | |||
| 6ee748a87a | |||
| 0ebda76cc8 | |||
| ed5581d1d9 | |||
| 71c59cfe50 | |||
| 68b3dc6f37 | |||
| 3c83102e91 | |||
| 0e49a066ba | |||
| fcb786cf60 | |||
| c56b2cdd62 | |||
| 6309d323dc | |||
| db2937d4a3 | |||
| fe10a7e55f | |||
| dd77c3e1f0 | |||
| e5e1f6bd05 | |||
| c52f3ebdd6 | |||
| ac96b64c64 | |||
| fa8fd60ac6 | |||
| 9c9393e0cf | |||
| 47f32a5f55 | |||
| 8b26f9309f | |||
| 4027809f32 | |||
| 60250a32c2 | |||
| 6a4c73db03 | |||
| b28ffa9543 | |||
| 7836f2e47f | |||
| 648873978c | |||
| c51c13b4b8 | |||
| 9a7c7d2a4b | |||
| 5f17474ff4 | |||
| 3376bf8b99 | |||
| ad1bea088e | |||
| d0ac99fc69 | |||
| 2b69eb2fd0 |
+67
-11
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with: { go-version: '1.24' }
|
||||
with: { go-version: '1.25' }
|
||||
|
||||
- name: Build go2rtc_win64
|
||||
env: { GOOS: windows, GOARCH: amd64 }
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_win32
|
||||
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
|
||||
env: { GOOS: windows, GOARCH: 386 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win32
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_mac_amd64
|
||||
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
|
||||
env: { GOOS: darwin, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_mac_amd64
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
@@ -138,14 +138,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -156,6 +156,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/386
|
||||
@@ -180,7 +181,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-hardware,onlatest=true
|
||||
@@ -197,14 +198,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -215,10 +216,65 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
file: docker/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
|
||||
|
||||
docker-rockchip:
|
||||
name: Build docker rockchip
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta-rk
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-rockchip,onlatest=true
|
||||
latest=auto
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}},enable=false
|
||||
type=match,pattern=v(.*),group=1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/rockchip.Dockerfile
|
||||
platforms: linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-rk.outputs.tags }}
|
||||
labels: ${{ steps.meta-rk.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -79,6 +79,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/${{ matrix.platform }}
|
||||
push: false
|
||||
load: true
|
||||
@@ -92,7 +93,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
file: docker/hardware.Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
|
||||
@@ -9,6 +9,9 @@ go2rtc_linux*
|
||||
go2rtc_mac*
|
||||
go2rtc_win*
|
||||
|
||||
/go2rtc
|
||||
/go2rtc.exe
|
||||
|
||||
0_test.go
|
||||
|
||||
.DS_Store
|
||||
|
||||
@@ -237,6 +237,62 @@ paths:
|
||||
|
||||
|
||||
|
||||
/api/preload:
|
||||
get:
|
||||
summary: Get all preloaded streams
|
||||
tags: [ Streams list ]
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json: { example: { camera1: "video&audio", camera2: "video" } }
|
||||
put:
|
||||
summary: Preload new stream
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream source (name)
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: "camera1"
|
||||
- name: video
|
||||
in: query
|
||||
description: Video codecs filter
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: all,h264,h265,...
|
||||
- name: audio
|
||||
in: query
|
||||
description: Audio codecs filter
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: all,aac,opus,...
|
||||
- name: microphone
|
||||
in: query
|
||||
description: Microphone codecs filter
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: all,aac,opus,...
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
delete:
|
||||
summary: Delete preloaded stream
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream source (name)
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: "camera1"
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/api/streams?src={src}:
|
||||
get:
|
||||
summary: Get stream info in JSON format
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG GO_VERSION="1.24"
|
||||
ARG PYTHON_VERSION="3.13"
|
||||
ARG GO_VERSION="1.25"
|
||||
|
||||
|
||||
# 1. Download ngrok binary (for support arm/v6)
|
||||
FROM alpine AS ngrok
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz /
|
||||
RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin
|
||||
|
||||
|
||||
# 2. Build go2rtc binary
|
||||
# 1. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
@@ -35,7 +26,7 @@ COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 3. Final image
|
||||
# 2. Final image
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
@@ -55,7 +46,6 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
VOLUME /config
|
||||
@@ -0,0 +1,50 @@
|
||||
## Versions
|
||||
|
||||
- `alexxit/go2rtc:latest` - latest release based on `alpine` (`amd64`, `386`, `arm/v6`, `arm/v7`, `arm64`) with support hardware transcoding for Intel iGPU and Raspberry
|
||||
- `alexxit/go2rtc:latest-hardware` - latest release based on `debian 13` (`amd64`) with support hardware transcoding for Intel iGPU, AMD GPU and NVidia GPU
|
||||
- `alexxit/go2rtc:latest-rockchip` - latest release based on `debian 12` (`arm64`) with support hardware transcoding for Rockchip RK35xx
|
||||
- `alexxit/go2rtc:master` - latest unstable version based on `alpine`
|
||||
- `alexxit/go2rtc:master-hardware` - latest unstable version based on `debian 13` (`amd64`)
|
||||
- `alexxit/go2rtc:master-rockchip` - latest unstable version based on `debian 12` (`arm64`)
|
||||
|
||||
## Docker compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
go2rtc:
|
||||
image: alexxit/go2rtc
|
||||
network_mode: host # important for WebRTC, HomeKit, UDP cameras
|
||||
privileged: true # only for FFmpeg hardware transcoding
|
||||
restart: unless-stopped # autorestart on fail or config change from WebUI
|
||||
environment:
|
||||
- TZ=Atlantic/Bermuda # timezone in logs
|
||||
volumes:
|
||||
- "~/go2rtc:/config" # folder for go2rtc.yaml file (edit from WebUI)
|
||||
```
|
||||
|
||||
## Basic Deployment
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name go2rtc \
|
||||
--network host \
|
||||
--privileged \
|
||||
--restart unless-stopped \
|
||||
-e TZ=Atlantic/Bermuda \
|
||||
-v ~/go2rtc:/config \
|
||||
alexxit/go2rtc
|
||||
```
|
||||
|
||||
## Deployment with GPU Acceleration
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name go2rtc \
|
||||
--network host \
|
||||
--privileged \
|
||||
--restart unless-stopped \
|
||||
-e TZ=Atlantic/Bermuda \
|
||||
--gpus all \
|
||||
-v ~/go2rtc:/config \
|
||||
alexxit/go2rtc:latest-hardware
|
||||
```
|
||||
@@ -4,16 +4,11 @@
|
||||
# only debian 13 (trixie) has latest ffmpeg
|
||||
# https://packages.debian.org/trixie/ffmpeg
|
||||
ARG DEBIAN_VERSION="trixie-slim"
|
||||
ARG GO_VERSION="1.24-bookworm"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM debian:${DEBIAN_VERSION} AS base
|
||||
FROM golang:${GO_VERSION} AS go
|
||||
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
|
||||
ARG GO_VERSION="1.25-bookworm"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM go AS build
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -31,34 +26,28 @@ COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Collect all files
|
||||
FROM scratch AS rootfs
|
||||
# 2. Final image
|
||||
FROM debian:${DEBIAN_VERSION}
|
||||
|
||||
COPY --link --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --link --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
# 3. Final image
|
||||
FROM base
|
||||
# Prepare apt for buildkit cache
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
|
||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||
# mesa-va-drivers for AMD APU
|
||||
# libasound2-plugins for ALSA support
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
echo 'deb http://deb.debian.org/debian 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 ffmpeg tini \
|
||||
python3 curl jq \
|
||||
intel-media-va-driver-non-free \
|
||||
mesa-va-drivers \
|
||||
libasound2-plugins && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --link --from=rootfs / /
|
||||
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
VOLUME /config
|
||||
@@ -0,0 +1,50 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.13-slim-bookworm"
|
||||
ARG GO_VERSION="1.25-bookworm"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GOOS=${TARGETOS}
|
||||
ENV GOARCH=${TARGETARCH}
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Final image
|
||||
FROM python:${PYTHON_VERSION}
|
||||
|
||||
# Prepare apt for buildkit cache
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# libasound2-plugins for ALSA support
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get -y update && apt-get -y install tini \
|
||||
curl jq \
|
||||
libasound2-plugins && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
ADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /usr/local/bin
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
|
||||
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
|
||||
@@ -54,7 +54,7 @@ var chars = map[string]string{
|
||||
"21C": "Third Party Camera Active",
|
||||
"21D": "Camera Operating Mode Indicator",
|
||||
"11B": "Night Vision",
|
||||
"129": "Supported Data Stream Transport Configuration",
|
||||
//"129": "Supported Data Stream Transport Configuration",
|
||||
"37": "Version",
|
||||
"131": "Setup Data Stream Transport",
|
||||
"130": "Supported Data Stream Transport Configuration",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module pinggy
|
||||
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 h1:lzZ00JK6BUGQXnpkJZ+cVj8kIkXsmiVBUci9uEkSwEY=
|
||||
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9/go.mod h1:V1Sxb+4zyr36o9atZiqtT4XhsKtW1RSb2GvsbTbTJYw=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/Pinggy-io/pinggy-go/pinggy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tunType := os.Args[1]
|
||||
address := os.Args[2]
|
||||
|
||||
log.SetFlags(log.Llongfile | log.LstdFlags)
|
||||
|
||||
config := pinggy.Config{
|
||||
Type: pinggy.TunnelType(tunType),
|
||||
TcpForwardingAddr: address,
|
||||
|
||||
//SshOverSsl: true,
|
||||
//Stdout: os.Stderr,
|
||||
//Stderr: os.Stderr,
|
||||
}
|
||||
|
||||
if tunType == "http" {
|
||||
hman := pinggy.CreateHeaderManipulationAndAuthConfig()
|
||||
//hman.SetReverseProxy(address)
|
||||
//hman.SetPassPreflight(true)
|
||||
//hman.SetNoReverseProxy()
|
||||
config.HeaderManipulationAndAuth = hman
|
||||
}
|
||||
|
||||
pl, err := pinggy.ConnectWithConfig(config)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
log.Println("Addrs: ", pl.RemoteUrls())
|
||||
//err = pl.InitiateWebDebug("localhost:3424")
|
||||
//log.Println(err)
|
||||
pl.StartForwarding()
|
||||
}
|
||||
@@ -1,50 +1,50 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.20
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/asticode/go-astits v1.14.0
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||
github.com/expr-lang/expr v1.17.6
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/miekg/dns v1.1.63
|
||||
github.com/pion/ice/v2 v2.3.37
|
||||
github.com/pion/interceptor v0.1.37
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.11
|
||||
github.com/pion/sdp/v3 v3.0.10
|
||||
github.com/pion/srtp/v2 v2.0.20
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.3.5
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/miekg/dns v1.1.69
|
||||
github.com/pion/ice/v4 v4.1.0
|
||||
github.com/pion/interceptor v0.1.42
|
||||
github.com/pion/rtcp v1.2.16
|
||||
github.com/pion/rtp v1.8.26
|
||||
github.com/pion/sdp/v3 v3.0.16
|
||||
github.com/pion/srtp/v3 v3.0.9
|
||||
github.com/pion/stun/v3 v3.0.2
|
||||
github.com/pion/webrtc/v4 v4.1.8
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astikit v0.52.0 // indirect
|
||||
github.com/asticode/go-astikit v0.57.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.9 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.36 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/sctp v1.8.41 // indirect
|
||||
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astikit v0.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y=
|
||||
github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/asticode/go-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA=
|
||||
github.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00=
|
||||
github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
|
||||
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -30,51 +30,40 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0=
|
||||
github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk=
|
||||
github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
|
||||
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
|
||||
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
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.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg=
|
||||
github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
|
||||
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
|
||||
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
|
||||
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
|
||||
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
|
||||
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
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=
|
||||
@@ -82,96 +71,41 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
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.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.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/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !(linux && (386 || amd64 || arm || arm64 || mipsle))
|
||||
|
||||
package alsa
|
||||
|
||||
func Init() {
|
||||
// not supported
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//go:build linux && (386 || amd64 || arm || arm64 || mipsle)
|
||||
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa"
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("alsa", alsa.Open)
|
||||
|
||||
api.HandleFunc("api/alsa", apiAlsa)
|
||||
}
|
||||
|
||||
func apiAlsa(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir("/dev/snd/")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sources []*api.Source
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), "pcm") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := "/dev/snd/" + file.Name()
|
||||
|
||||
dev, err := device.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := dev.Info()
|
||||
if err == nil {
|
||||
formats := formatsToString(dev.ListFormats())
|
||||
r1, r2 := dev.RangeRates()
|
||||
c1, c2 := dev.RangeChannels()
|
||||
source := &api.Source{
|
||||
Name: info.ID,
|
||||
Info: fmt.Sprintf("Formats: %s, Rates: %d-%d, Channels: %d-%d", formats, r1, r2, c1, c2),
|
||||
URL: "alsa:device?audio=" + path,
|
||||
}
|
||||
if !strings.Contains(source.Name, info.Name) {
|
||||
source.Name += ", " + info.Name
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
|
||||
_ = dev.Close()
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
}
|
||||
|
||||
func formatsToString(formats []byte) string {
|
||||
var s string
|
||||
for i, format := range formats {
|
||||
if i > 0 {
|
||||
s += " "
|
||||
}
|
||||
switch format {
|
||||
case 2:
|
||||
s += "s16le"
|
||||
case 10:
|
||||
s += "s32le"
|
||||
default:
|
||||
s += strconv.Itoa(int(format))
|
||||
}
|
||||
|
||||
}
|
||||
return s
|
||||
}
|
||||
+17
-3
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -23,6 +24,7 @@ func Init() {
|
||||
Listen string `yaml:"listen"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
LocalAuth bool `yaml:"local_auth"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
StaticDir string `yaml:"static_dir"`
|
||||
Origin string `yaml:"origin"`
|
||||
@@ -30,6 +32,8 @@ func Init() {
|
||||
TLSCert string `yaml:"tls_cert"`
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
UnixListen string `yaml:"unix_listen"`
|
||||
|
||||
AllowPaths []string `yaml:"allow_paths"`
|
||||
} `yaml:"api"`
|
||||
}
|
||||
|
||||
@@ -43,6 +47,7 @@ func Init() {
|
||||
return
|
||||
}
|
||||
|
||||
allowPaths = cfg.Mod.AllowPaths
|
||||
basePath = cfg.Mod.BasePath
|
||||
log = app.GetLogger("api")
|
||||
|
||||
@@ -61,7 +66,7 @@ func Init() {
|
||||
}
|
||||
|
||||
if cfg.Mod.Username != "" {
|
||||
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
|
||||
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd
|
||||
}
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
@@ -152,6 +157,10 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||
if len(pattern) == 0 || pattern[0] != '/' {
|
||||
pattern = basePath + "/" + pattern
|
||||
}
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, pattern) {
|
||||
log.Trace().Str("path", pattern).Msg("[api] ignore path not in allow_paths")
|
||||
return
|
||||
}
|
||||
log.Trace().Str("path", pattern).Msg("[api] register path")
|
||||
http.HandleFunc(pattern, handler)
|
||||
}
|
||||
@@ -185,6 +194,7 @@ func Response(w http.ResponseWriter, body any, contentType string) {
|
||||
|
||||
const StreamNotFound = "stream not found"
|
||||
|
||||
var allowPaths []string
|
||||
var basePath string
|
||||
var log zerolog.Logger
|
||||
|
||||
@@ -195,9 +205,13 @@ func middlewareLog(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func middlewareAuth(username, password string, next http.Handler) http.Handler {
|
||||
func isLoopback(remoteAddr string) bool {
|
||||
return strings.HasPrefix(remoteAddr, "127.") || strings.HasPrefix(remoteAddr, "[::1]") || remoteAddr == "@"
|
||||
}
|
||||
|
||||
func middlewareAuth(username, password string, localAuth bool, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
|
||||
if localAuth || !isLoopback(r.RemoteAddr) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != username || pass != password {
|
||||
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -132,7 +133,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
go func() {
|
||||
if err = handler(tr, msg); err != nil {
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
|
||||
errMsg := core.StripUserinfo(err.Error())
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
var (
|
||||
Version string
|
||||
Modules []string
|
||||
UserAgent string
|
||||
ConfigPath string
|
||||
Info = make(map[string]any)
|
||||
@@ -76,6 +77,16 @@ func Init() {
|
||||
if ConfigPath != "" {
|
||||
Logger.Info().Str("path", ConfigPath).Msg("config")
|
||||
}
|
||||
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Modules []string `yaml:"modules"`
|
||||
} `yaml:"app"`
|
||||
}
|
||||
|
||||
LoadConfig(&cfg)
|
||||
|
||||
Modules = cfg.Mod.Modules
|
||||
}
|
||||
|
||||
func readRevisionTime() (revision, vcsTime string) {
|
||||
|
||||
+10
-2
@@ -5,8 +5,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
)
|
||||
|
||||
@@ -18,11 +19,16 @@ func LoadConfig(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
var configMu sync.Mutex
|
||||
|
||||
func PatchConfig(path []string, value any) error {
|
||||
if ConfigPath == "" {
|
||||
return errors.New("config file disabled")
|
||||
}
|
||||
|
||||
configMu.Lock()
|
||||
defer configMu.Unlock()
|
||||
|
||||
// empty config is OK
|
||||
b, _ := os.ReadFile(ConfigPath)
|
||||
|
||||
@@ -65,13 +71,15 @@ func initConfig(confs flagConfig) {
|
||||
// config as file
|
||||
if ConfigPath == "" {
|
||||
ConfigPath = conf
|
||||
initStorage()
|
||||
}
|
||||
|
||||
if data, _ = os.ReadFile(conf); data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
data = []byte(shell.ReplaceEnvVars(string(data)))
|
||||
loadEnv(data)
|
||||
data = creds.ReplaceVars(data)
|
||||
configs = append(configs, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -88,6 +89,8 @@ func initLogger() {
|
||||
writer = MemoryLog
|
||||
}
|
||||
|
||||
writer = creds.SecretWriter(writer)
|
||||
|
||||
lvl, _ := zerolog.ParseLevel(modules["level"])
|
||||
Logger = zerolog.New(writer).Level(lvl)
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
)
|
||||
|
||||
func initStorage() {
|
||||
storage = &envStorage{data: make(map[string]string)}
|
||||
creds.SetStorage(storage)
|
||||
}
|
||||
|
||||
func loadEnv(data []byte) {
|
||||
var cfg struct {
|
||||
Env map[string]string `yaml:"env"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
storage.mu.Lock()
|
||||
for name, value := range cfg.Env {
|
||||
storage.data[name] = value
|
||||
creds.AddSecret(value)
|
||||
}
|
||||
storage.mu.Unlock()
|
||||
}
|
||||
|
||||
var storage *envStorage
|
||||
|
||||
type envStorage struct {
|
||||
data map[string]string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *envStorage) SetValue(name, value string) error {
|
||||
if err := PatchConfig([]string{"env", name}, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.data[name] = value
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *envStorage) GetValue(name string) (value string, ok bool) {
|
||||
s.mu.Lock()
|
||||
value, ok = s.data[name]
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -29,8 +29,8 @@ var stackSkip = [][]byte{
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
|
||||
|
||||
// webrtc/api.go
|
||||
[]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/v4.NewTCPMuxDefault"),
|
||||
[]byte("created by github.com/pion/ice/v4.NewUDPMuxDefault"),
|
||||
}
|
||||
|
||||
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -2,7 +2,9 @@ package echo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"slices"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
@@ -10,11 +12,25 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
AllowPaths []string `yaml:"allow_paths"`
|
||||
} `yaml:"echo"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
allowPaths := cfg.Mod.AllowPaths
|
||||
|
||||
log := app.GetLogger("echo")
|
||||
|
||||
streams.RedirectFunc("echo", func(url string) (string, error) {
|
||||
args := shell.QuoteSplit(url[5:])
|
||||
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, args[0]) {
|
||||
return "", errors.New("echo: bin not in allow_paths: " + args[0])
|
||||
}
|
||||
|
||||
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -26,4 +42,5 @@ func Init() {
|
||||
|
||||
return string(b), nil
|
||||
})
|
||||
streams.MarkInsecure("echo")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package eseecloud
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/eseecloud"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("eseecloud", eseecloud.Dial)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
## Backchannel
|
||||
|
||||
- You can check audio card names in the **Go2rtc > WebUI > Add**
|
||||
- You can specify multiple backchannel lines with different codecs
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
two_way_audio_win:
|
||||
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
|
||||
- exec:ffplay -nodisp -probesize 32 -f s16le -ar 16000 -#backchannel=1#audio=s16le/16000
|
||||
- exec:ffplay -nodisp -probesize 32 -f alaw -ar 8000 -#backchannel=1#audio=alaw/8000
|
||||
```
|
||||
+20
-2
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -19,13 +20,23 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/stdin"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
AllowPaths []string `yaml:"allow_paths"`
|
||||
} `yaml:"exec"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
allowPaths = cfg.Mod.AllowPaths
|
||||
|
||||
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
|
||||
waitersMu.Lock()
|
||||
waiter := waiters[conn.URL.Path]
|
||||
@@ -45,10 +56,13 @@ func Init() {
|
||||
})
|
||||
|
||||
streams.HandleFunc("exec", execHandle)
|
||||
streams.MarkInsecure("exec")
|
||||
|
||||
log = app.GetLogger("exec")
|
||||
}
|
||||
|
||||
var allowPaths []string
|
||||
|
||||
func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
query := streams.ParseQuery(rawQuery)
|
||||
@@ -73,6 +87,10 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
debug: log.Debug().Enabled(),
|
||||
}
|
||||
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) {
|
||||
return nil, errors.New("exec: bin not in allow_paths: " + cmd.Args[0])
|
||||
}
|
||||
|
||||
if s := query.Get("killsignal"); s != "" {
|
||||
sig := syscall.Signal(core.Atoi(s))
|
||||
cmd.Cancel = func() error {
|
||||
@@ -86,7 +104,7 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
}
|
||||
|
||||
if query.Get("backchannel") == "1" {
|
||||
return stdin.NewClient(cmd)
|
||||
return pcm.NewBackchannel(cmd, query.Get("audio"))
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
|
||||
+71
-11
@@ -12,34 +12,94 @@
|
||||
- `fetch` - JS-like HTTP requests
|
||||
- `match` - JS-like RegExp queries
|
||||
|
||||
## Examples
|
||||
## Fetch examples
|
||||
|
||||
Multiple fetch requests are executed within a single session. They share the same cookie.
|
||||
|
||||
**HTTP GET**
|
||||
|
||||
```js
|
||||
var r = fetch('https://example.org/products.json');
|
||||
```
|
||||
|
||||
**HTTP POST JSON**
|
||||
|
||||
```js
|
||||
var r = fetch('https://example.org/post', {
|
||||
method: 'POST',
|
||||
// Content-Type: application/json will be set automatically
|
||||
json: {username: 'example'}
|
||||
});
|
||||
```
|
||||
|
||||
**HTTP POST Form**
|
||||
|
||||
```js
|
||||
var r = fetch('https://example.org/post', {
|
||||
method: 'POST',
|
||||
// Content-Type: application/x-www-form-urlencoded will be set automatically
|
||||
data: {username: 'example', password: 'password'}
|
||||
});
|
||||
```
|
||||
|
||||
## Script 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" : ""
|
||||
expr:
|
||||
let host = 'admin:password@192.168.1.123';
|
||||
|
||||
var r = fetch('http://' + host + '/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000');
|
||||
|
||||
'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
|
||||
You can get credentials from 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}
|
||||
expr:
|
||||
let camera = '***';
|
||||
let token = '***';
|
||||
let operator = '***';
|
||||
|
||||
fetch('https://myhome.proptech.ru/rest/v1/forpost/cameras/' + camera + '/video', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'User-Agent': 'Google sdkgphone64x8664 | Android 14 | erth | 8.26.0 (82600010) | 0 | 0 | 0',
|
||||
'Operator': operator
|
||||
}
|
||||
}).json().data.URL
|
||||
```
|
||||
|
||||
**dom.ufanet.ru**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
ufanet_ru: |
|
||||
expr:
|
||||
let username = '***';
|
||||
let password = '***';
|
||||
let cameraid = '***';
|
||||
|
||||
let r1 = fetch('https://ucams.ufanet.ru/api/internal/login/', {
|
||||
method: 'POST',
|
||||
data: {username: username, password: password}
|
||||
});
|
||||
let r2 = fetch('https://ucams.ufanet.ru/api/v0/cameras/this/?lang=ru', {
|
||||
method: 'POST',
|
||||
json: {'fields': ['token_l', 'server'], 'token_l_ttl': 3600, 'numbers': [cameraid]},
|
||||
}).json().results[0];
|
||||
|
||||
'rtsp://' + r2.server.domain + '/' + r2.number + '?token=' + r2.token_l
|
||||
```
|
||||
|
||||
**Parse HLS files from Apple**
|
||||
|
||||
Same example in two languages - python and expr.
|
||||
|
||||
@@ -12,7 +12,7 @@ func Init() {
|
||||
log := app.GetLogger("expr")
|
||||
|
||||
streams.RedirectFunc("expr", func(url string) (string, error) {
|
||||
v, err := expr.Run(url[5:])
|
||||
v, err := expr.Eval(url[5:], nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -25,4 +25,5 @@ func Init() {
|
||||
|
||||
return url, nil
|
||||
})
|
||||
streams.MarkInsecure("expr")
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ var defaults = map[string]string{
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -x265-params level=5.1:high-tier=0 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"mjpeg": "-c:v mjpeg",
|
||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
@@ -113,6 +113,7 @@ var defaults = map[string]string{
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -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/16000": "-c:a pcm_s16le -ar:a 16000 -ac:a 1",
|
||||
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
|
||||
|
||||
// hardware Intel and AMD on Linux
|
||||
@@ -129,8 +130,9 @@ var defaults = map[string]string{
|
||||
// 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",
|
||||
"h264/rkmpp": "-c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||
"h265/rkmpp": "-c:v hevc_rkmpp -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||
"mjpeg/rkmpp": "-c:v mjpeg_rkmpp",
|
||||
|
||||
// hardware NVidia on Linux and Windows
|
||||
// preset=p2 - faster, tune=ll - low latency
|
||||
|
||||
@@ -251,15 +251,33 @@ func _TestParseArgsHwV4l2m2m(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())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[FILE] transcoding to H264",
|
||||
source: "bbb.mp4#video=h264#hardware=rkmpp",
|
||||
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] transcoding with rotation",
|
||||
source: "bbb.mp4#video=h264#rotate=180#hardware=rkmpp",
|
||||
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,vpp_rkrga=transpose=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] transcoding with scaling",
|
||||
source: "bbb.mp4#video=h264#height=320#hardware=rkmpp",
|
||||
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,scale_rkrga=-1:320:force_original_aspect_ratio=0" -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 _TestParseArgsHwCuda(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Hardware
|
||||
|
||||
You **DON'T** need hardware acceleration if:
|
||||
|
||||
- you not using [FFmpeg source](https://github.com/AlexxIT/go2rtc#source-ffmpeg)
|
||||
- you using only `#video=copy` for FFmpeg source
|
||||
- you using only `#audio=...` (any audio) transcoding for FFmpeg source
|
||||
|
||||
You **NEED** hardware acceleration if you using `#video=h264`, `#video=h265`, `#video=mjpeg` (video) transcoding.
|
||||
|
||||
## Important
|
||||
|
||||
- Acceleration is disabled by default because it can be unstable (it can be changed in future)
|
||||
- go2rtc can automatically detect supported hardware acceleration if enabled
|
||||
- go2rtc will enable hardware decoding only if hardware encoding supported
|
||||
- go2rtc will use the same GPU for decoder and encoder
|
||||
- Intel and AMD will switch to software decoder if input codec is not supported with hardware decoder
|
||||
- NVidia will fail if input codec is not supported with hardware decoder
|
||||
- Raspberry always uses software decoder
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# auto select hardware encoder
|
||||
camera1_hw: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware
|
||||
|
||||
# manual select hardware encoder (vaapi, cuda, v4l2m2m, dxva2, videotoolbox)
|
||||
camera1_vaapi: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware=vaapi
|
||||
```
|
||||
|
||||
## Docker and Hass Addon
|
||||
|
||||
There are two versions of the Docker container and Hass Add-on:
|
||||
|
||||
- Latest (alpine) support hardware acceleration for Intel iGPU (CPU with Graphics) and Raspberry.
|
||||
- Hardware (debian 12) support Intel iGPU, AMD GPU, NVidia GPU.
|
||||
|
||||
## Intel iGPU
|
||||
|
||||
**Supported on:** Windows binary, Linux binary, Docker, Hass Addon.
|
||||
|
||||
If you have Intel CPU Sandy Bridge (2011) with Graphics, you already have support hardware decoding/encoding for `AVC/H.264`.
|
||||
|
||||
If you have Intel CPU Skylake (2015) with Graphics, you already have support hardware decoding/encoding for `AVC/H.264`, `HEVC/H.265` and `MJPEG`.
|
||||
|
||||
Read more [here](https://en.wikipedia.org/wiki/Intel_Quick_Sync_Video#Hardware_decoding_and_encoding) and [here](https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)).
|
||||
|
||||
Linux and Docker:
|
||||
|
||||
- It may be important to have the latest version of the OS with the latest version of the Linux kernel. For example, on my **Debian 10 (kernel 4.19)** it did not work, but after update to **Debian 11 (kernel 5.10)** all was fine.
|
||||
- In case of troube check you have `/dev/dri/` folder on your host.
|
||||
|
||||
Docker users should add `--privileged` option to container for access to Hardware.
|
||||
|
||||
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine on Linux and [DXVA2+QSV](https://trac.ffmpeg.org/wiki/Hardware/QuickSync) engine on Windows.
|
||||
|
||||
## AMD GPU
|
||||
|
||||
*I don't have the hardware for test support!!!*
|
||||
|
||||
**Supported on:** Linux binary, Docker, Hass Addon.
|
||||
|
||||
Docker users should install: `alexxit/go2rtc:master-hardware`. Docker users should add `--privileged` option to container for access to Hardware.
|
||||
|
||||
Hass Addon users should install **go2rtc master hardware** version.
|
||||
|
||||
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine.
|
||||
|
||||
## NVidia GPU
|
||||
|
||||
**Supported on:** Windows binary, Linux binary, Docker.
|
||||
|
||||
Docker users should install: `alexxit/go2rtc:master-hardware`.
|
||||
|
||||
Read more [here](https://docs.frigate.video/configuration/hardware_acceleration) and [here](https://jellyfin.org/docs/general/administration/hardware-acceleration/#nvidia-hardware-acceleration-on-docker-linux).
|
||||
|
||||
**PS.** Supported via [CUDA](https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC) engine.
|
||||
|
||||
## Raspberry Pi 3
|
||||
|
||||
**Supported on:** Linux binary, Docker, Hass Addon.
|
||||
|
||||
I don't recommend using transcoding on the Raspberry Pi 3. It's extreamly slow, even with hardware acceleration. Also it may fail when transcoding 2K+ stream.
|
||||
|
||||
## Raspberry Pi 4
|
||||
|
||||
*I don't have the hardware for test support!!!*
|
||||
|
||||
**Supported on:** Linux binary, Docker, Hass Addon.
|
||||
|
||||
**PS.** Supported via [v4l2m2m](https://lalitm.com/hw-encoding-raspi/) engine.
|
||||
|
||||
## macOS
|
||||
|
||||
In my tests, transcoding is faster on the M1 CPU than on the M1 GPU. Transcoding time on M1 CPU better than any Intel iGPU and comparable to NVidia RTX 2070.
|
||||
|
||||
**PS.** Supported via [videotoolbox](https://trac.ffmpeg.org/wiki/HWAccelIntro#VideoToolbox) engine.
|
||||
|
||||
## Rockchip
|
||||
|
||||
- Important to use custom FFmpeg with Rockchip support from [@nyanmisaka](https://github.com/nyanmisaka/ffmpeg-rockchip)
|
||||
- Static binaries from [@MarcA711](https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/)
|
||||
- Important to have Linux kernel 5.10 or 6.1
|
||||
|
||||
**Tested**
|
||||
|
||||
- [Orange Pi 3B](https://www.armbian.com/orangepi3b/) with Armbian 6.1, support transcoding H264, H265, MJPEG
|
||||
@@ -128,19 +128,32 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
||||
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:]...)
|
||||
if !args.HasFilters("drawtext=") {
|
||||
args.Input = "-hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga " + args.Input
|
||||
|
||||
width, height, _ := strings.Cut(filter[6:], ":")
|
||||
if width != "-1" {
|
||||
args.Codecs[i] += " -width " + width
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters[i] = "scale_rkrga=" + filter[6:] + ":force_original_aspect_ratio=0"
|
||||
}
|
||||
if height != "-1" {
|
||||
args.Codecs[i] += " -height " + height
|
||||
if strings.HasPrefix(filter, "transpose=") {
|
||||
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
|
||||
args.Filters[i] = "vpp_rkrga=transpose=4" // reversal
|
||||
} else {
|
||||
args.Filters[i] = "vpp_rkrga=transpose=" + filter[10:]
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if len(args.Filters) > 0 {
|
||||
// fix if input doesn't support hwaccel, do nothing when support
|
||||
// insert as first filter before hardware scale and transpose
|
||||
args.InsertFilter("format=drm_prime|nv12,hwupload")
|
||||
}
|
||||
} else {
|
||||
// enable software pixel for drawtext, scale and transpose
|
||||
args.Input = "-hwaccel rkmpp -hwaccel_output_format nv12 -afbc rga " + args.Input
|
||||
|
||||
args.AddFilter("hwupload")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
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 -"
|
||||
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp -f null -"
|
||||
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp -f null -"
|
||||
ProbeRKMPPJPEG = "-f lavfi -i testsrc2 -t 1 -c mjpeg_rkmpp -f null -"
|
||||
ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -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 -"
|
||||
@@ -39,6 +40,10 @@ func ProbeAll(bin string) []*api.Source {
|
||||
Name: runToString(bin, ProbeRKMPPH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeRKMPPJPEG),
|
||||
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineRKMPP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +88,10 @@ func ProbeHardware(bin, name string) string {
|
||||
if run(bin, ProbeRKMPPH265) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
case "mjpeg":
|
||||
if run(bin, ProbeRKMPPJPEG) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
|
||||
@@ -42,9 +42,11 @@ func NewProducer(url string) (core.Producer, error) {
|
||||
Codecs: []*core.Codec{
|
||||
// OPUS will always marked as OPUS/48000/2
|
||||
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
|
||||
{Name: core.CodecPCML, ClockRate: 16000},
|
||||
{Name: core.CodecPCM, ClockRate: 16000},
|
||||
{Name: core.CodecPCMA, ClockRate: 16000},
|
||||
{Name: core.CodecPCMU, ClockRate: 16000},
|
||||
{Name: core.CodecPCML, ClockRate: 8000},
|
||||
{Name: core.CodecPCM, ClockRate: 8000},
|
||||
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||
@@ -94,9 +96,11 @@ func (p *Producer) newURL() string {
|
||||
codec := receiver.Codec
|
||||
switch codec.Name {
|
||||
case core.CodecOpus:
|
||||
s += "#audio=opus"
|
||||
s += "#audio=opus/16000"
|
||||
case core.CodecAAC:
|
||||
s += "#audio=aac/16000"
|
||||
case core.CodecPCML:
|
||||
s += "#audio=pcml/" + strconv.Itoa(int(codec.ClockRate))
|
||||
case core.CodecPCM:
|
||||
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
|
||||
case core.CodecPCMA:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package flussonic
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flussonic"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("flussonic", flussonic.Dial)
|
||||
}
|
||||
@@ -30,10 +30,10 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
||||
// 2. static link to Hass camera
|
||||
// 3. dynamic link to Hass camera
|
||||
if streams.Patch(v.Name, v.Channels.First.Url) != nil {
|
||||
if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil {
|
||||
apiOK(w, r)
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// /stream/{id}/channel/0/webrtc
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -37,8 +38,13 @@ func Init() {
|
||||
api.HandleFunc("/streams", apiOK)
|
||||
api.HandleFunc("/stream/", apiStream)
|
||||
|
||||
streams.RedirectFunc("hass", func(url string) (string, error) {
|
||||
if location := entities[url[5:]]; location != "" {
|
||||
streams.RedirectFunc("hass", func(rawURL string) (string, error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
|
||||
if location := entities[rawURL[5:]]; location != "" {
|
||||
if rawQuery != "" {
|
||||
return location + "#" + rawQuery, nil
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
+79
-37
@@ -3,6 +3,7 @@ package homekit
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -14,56 +15,97 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
)
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func apiDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
sources, err := discovery()
|
||||
if err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func apiHomekit(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
sources, err := discovery()
|
||||
if err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if id := r.Form.Get("id"); id != "" {
|
||||
if srv := servers[id]; srv != nil {
|
||||
api.ResponsePrettyJSON(w, srv)
|
||||
} else {
|
||||
http.Error(w, "server not found", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
api.ResponsePrettyJSON(w, servers)
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if source.Location == "" {
|
||||
source.Location = " "
|
||||
}
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
|
||||
case "POST":
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
|
||||
api.Error(w, err)
|
||||
id := r.Form.Get("id")
|
||||
rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin")
|
||||
if err := apiPair(id, rawURL); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := apiUnpair(r.Form.Get("id")); err != nil {
|
||||
api.Error(w, err)
|
||||
id := r.Form.Get("id")
|
||||
if err := apiUnpair(id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
stream := streams.Get(id)
|
||||
if stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
rawURL := findHomeKitURL(stream.Sources())
|
||||
if rawURL == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := hap.Dial(rawURL)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
res, err := client.Get(hap.PathAccessories)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", api.MimeJSON)
|
||||
_, _ = io.Copy(w, res.Body)
|
||||
}
|
||||
|
||||
func discovery() ([]*api.Source, error) {
|
||||
var sources []*api.Source
|
||||
|
||||
|
||||
+33
-54
@@ -2,8 +2,6 @@ package homekit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -26,6 +24,7 @@ func Init() {
|
||||
Name string `yaml:"name"`
|
||||
DeviceID string `yaml:"device_id"`
|
||||
DevicePrivate string `yaml:"device_private"`
|
||||
CategoryID string `yaml:"category_id"`
|
||||
Pairings []string `yaml:"pairings"`
|
||||
} `yaml:"homekit"`
|
||||
}
|
||||
@@ -35,12 +34,15 @@ func Init() {
|
||||
|
||||
streams.HandleFunc("homekit", streamHandler)
|
||||
|
||||
api.HandleFunc("api/homekit", apiHandler)
|
||||
api.HandleFunc("api/homekit", apiHomekit)
|
||||
api.HandleFunc("api/homekit/accessories", apiHomekitAccessories)
|
||||
api.HandleFunc("api/discovery/homekit", apiDiscovery)
|
||||
|
||||
if cfg.Mod == nil {
|
||||
return
|
||||
}
|
||||
|
||||
hosts = map[string]*server{}
|
||||
servers = map[string]*server{}
|
||||
var entries []*mdns.ServiceEntry
|
||||
|
||||
@@ -63,36 +65,19 @@ func Init() {
|
||||
|
||||
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
|
||||
name := calcName(conf.Name, deviceID)
|
||||
setupID := calcSetupID(id)
|
||||
|
||||
srv := &server{
|
||||
stream: id,
|
||||
srtp: srtp.Server,
|
||||
pairings: conf.Pairings,
|
||||
setupID: setupID,
|
||||
}
|
||||
|
||||
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.Sources()); 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)
|
||||
Pin: pin,
|
||||
DeviceID: deviceID,
|
||||
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
|
||||
GetClientPublic: srv.GetPair,
|
||||
}
|
||||
|
||||
srv.mdns = &mdns.ServiceEntry{
|
||||
@@ -106,23 +91,32 @@ func Init() {
|
||||
hap.TXTProtoVersion: "1.1",
|
||||
hap.TXTStateNumber: "1",
|
||||
hap.TXTStatusFlags: hap.StatusNotPaired,
|
||||
hap.TXTCategory: hap.CategoryCamera,
|
||||
hap.TXTSetupHash: srv.hap.SetupHash(),
|
||||
hap.TXTCategory: calcCategoryID(conf.CategoryID),
|
||||
hap.TXTSetupHash: hap.SetupHash(setupID, deviceID),
|
||||
},
|
||||
}
|
||||
entries = append(entries, srv.mdns)
|
||||
|
||||
srv.UpdateStatus()
|
||||
|
||||
if url := findHomeKitURL(stream.Sources()); url != "" {
|
||||
// 1. Act as transparent proxy for HomeKit camera
|
||||
srv.proxyURL = url
|
||||
} else {
|
||||
// 2. Act as basic HomeKit camera
|
||||
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
|
||||
}
|
||||
|
||||
host := srv.mdns.Host(mdns.ServiceHAP)
|
||||
servers[host] = srv
|
||||
hosts[host] = srv
|
||||
servers[id] = srv
|
||||
|
||||
log.Trace().Msgf("[homekit] new server: %s", srv.mdns)
|
||||
}
|
||||
|
||||
api.HandleFunc(hap.PathPairSetup, hapHandler)
|
||||
api.HandleFunc(hap.PathPairVerify, hapHandler)
|
||||
|
||||
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
||||
|
||||
go func() {
|
||||
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
@@ -131,6 +125,7 @@ func Init() {
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
var hosts map[string]*server
|
||||
var servers map[string]*server
|
||||
|
||||
func streamHandler(rawURL string) (core.Producer, error) {
|
||||
@@ -142,6 +137,8 @@ func streamHandler(rawURL string) (core.Producer, error) {
|
||||
client, err := homekit.Dial(rawURL, srtp.Server)
|
||||
if client != nil && rawQuery != "" {
|
||||
query := streams.ParseQuery(rawQuery)
|
||||
client.MaxWidth = core.Atoi(query.Get("maxwidth"))
|
||||
client.MaxHeight = core.Atoi(query.Get("maxheight"))
|
||||
client.Bitrate = parseBitrate(query.Get("bitrate"))
|
||||
}
|
||||
|
||||
@@ -149,45 +146,27 @@ func streamHandler(rawURL string) (core.Producer, error) {
|
||||
}
|
||||
|
||||
func resolve(host string) *server {
|
||||
if len(servers) == 1 {
|
||||
for _, srv := range servers {
|
||||
if len(hosts) == 1 {
|
||||
for _, srv := range hosts {
|
||||
return srv
|
||||
}
|
||||
}
|
||||
if srv, ok := servers[host]; ok {
|
||||
if srv, ok := hosts[host]; ok {
|
||||
return srv
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hapHandler(w http.ResponseWriter, r *http.Request) {
|
||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
// Can support multiple HomeKit cameras on single port ONLY for Apple devices.
|
||||
// Doesn't support Home Assistant and any other open source projects
|
||||
// because they don't send the host header in requests.
|
||||
srv := resolve(r.Host)
|
||||
if srv == nil {
|
||||
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||
_ = hap.WriteBackoff(rw)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.RequestURI {
|
||||
case hap.PathPairSetup:
|
||||
err = srv.hap.PairSetup(r, rw, conn)
|
||||
case hap.PathPairVerify:
|
||||
err = srv.hap.PairVerify(r, rw, conn)
|
||||
}
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
srv.Handle(w, r)
|
||||
}
|
||||
|
||||
func findHomeKitURL(sources []string) string {
|
||||
@@ -203,7 +182,7 @@ func findHomeKitURL(sources []string) string {
|
||||
if strings.HasPrefix(url, "hass") {
|
||||
location, _ := streams.Location(url)
|
||||
if strings.HasPrefix(location, "homekit") {
|
||||
return url
|
||||
return location
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+237
-97
@@ -4,10 +4,16 @@ import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
@@ -16,23 +22,142 @@ import (
|
||||
"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/hds"
|
||||
"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
|
||||
hap *hap.Server // server for HAP connection and encryption
|
||||
mdns *mdns.ServiceEntry
|
||||
|
||||
streams map[string]*homekit.Consumer
|
||||
consumer *homekit.Consumer
|
||||
pairings []string // pairings list
|
||||
conns []any
|
||||
mu sync.Mutex
|
||||
|
||||
accessory *hap.Accessory // HAP accessory
|
||||
consumer *homekit.Consumer
|
||||
proxyURL string
|
||||
setupID string
|
||||
stream string // stream name from YAML
|
||||
}
|
||||
|
||||
func (s *server) MarshalJSON() ([]byte, error) {
|
||||
v := struct {
|
||||
Name string `json:"name"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Paired int `json:"paired,omitempty"`
|
||||
CategoryID string `json:"category_id,omitempty"`
|
||||
SetupCode string `json:"setup_code,omitempty"`
|
||||
SetupID string `json:"setup_id,omitempty"`
|
||||
Conns []any `json:"connections,omitempty"`
|
||||
}{
|
||||
Name: s.mdns.Name,
|
||||
DeviceID: s.mdns.Info[hap.TXTDeviceID],
|
||||
CategoryID: s.mdns.Info[hap.TXTCategory],
|
||||
Paired: len(s.pairings),
|
||||
Conns: s.conns,
|
||||
}
|
||||
if v.Paired == 0 {
|
||||
v.SetupCode = s.hap.Pin
|
||||
v.SetupID = s.setupID
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (s *server) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
// Fix reading from Body after Hijack.
|
||||
r.Body = io.NopCloser(rw)
|
||||
|
||||
switch r.RequestURI {
|
||||
case hap.PathPairSetup:
|
||||
id, key, err := s.hap.PairSetup(r, rw)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
s.AddPair(id, key, hap.PermissionAdmin)
|
||||
|
||||
case hap.PathPairVerify:
|
||||
id, key, err := s.hap.PairVerify(r, rw)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr())
|
||||
|
||||
controller, err := hap.NewConn(conn, rw, key, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
s.AddConn(controller)
|
||||
defer s.DelConn(controller)
|
||||
|
||||
var handler homekit.HandlerFunc
|
||||
|
||||
switch {
|
||||
case s.accessory != nil:
|
||||
handler = homekit.ServerHandler(s)
|
||||
case s.proxyURL != "":
|
||||
client, err := hap.Dial(s.proxyURL)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
handler = homekit.ProxyHandler(s, client.Conn)
|
||||
}
|
||||
|
||||
// If your iPhone goes to sleep, it will be an EOF error.
|
||||
if err = handler(controller); err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
v any
|
||||
}
|
||||
|
||||
func (l logger) String() string {
|
||||
switch v := l.v.(type) {
|
||||
case *hap.Conn:
|
||||
return "hap " + v.RemoteAddr().String()
|
||||
case *hds.Conn:
|
||||
return "hds " + v.RemoteAddr().String()
|
||||
case *homekit.Consumer:
|
||||
return "rtp " + v.RemoteAddr
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (s *server) AddConn(v any) {
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v})
|
||||
s.mu.Lock()
|
||||
s.conns = append(s.conns, v)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) DelConn(v any) {
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v})
|
||||
s.mu.Lock()
|
||||
if i := slices.Index(s.conns, v); i >= 0 {
|
||||
s.conns = slices.Delete(s.conns, i, i+1)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) UpdateStatus() {
|
||||
@@ -44,12 +169,68 @@ func (s *server) UpdateStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) pairIndex(id string) int {
|
||||
id = "client_id=" + id
|
||||
for i, pairing := range s.pairings {
|
||||
if strings.HasPrefix(pairing, id) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (s *server) GetPair(id string) []byte {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if i := s.pairIndex(id); i >= 0 {
|
||||
query, _ := url.ParseQuery(s.pairings[i])
|
||||
b, _ := hex.DecodeString(query.Get("client_public"))
|
||||
return b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) AddPair(id string, public []byte, permissions byte) {
|
||||
log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions)
|
||||
|
||||
s.mu.Lock()
|
||||
if s.pairIndex(id) < 0 {
|
||||
s.pairings = append(s.pairings, fmt.Sprintf(
|
||||
"client_id=%s&client_public=%x&permissions=%d", id, public, permissions,
|
||||
))
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) DelPair(id string) {
|
||||
log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id)
|
||||
|
||||
s.mu.Lock()
|
||||
if i := s.pairIndex(id); i >= 0 {
|
||||
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) PatchConfig() {
|
||||
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
|
||||
log.Error().Err(err).Msgf(
|
||||
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", aid, iid)
|
||||
|
||||
char := s.accessory.GetCharacterByID(iid)
|
||||
if char == nil {
|
||||
@@ -59,11 +240,12 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
|
||||
|
||||
switch char.Type {
|
||||
case camera.TypeSetupEndpoints:
|
||||
if s.consumer == nil {
|
||||
consumer := s.consumer
|
||||
if consumer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
answer := s.consumer.GetAnswer()
|
||||
answer := consumer.GetAnswer()
|
||||
v, err := tlv8.MarshalBase64(answer)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -76,7 +258,7 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
|
||||
}
|
||||
|
||||
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)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", aid, iid, value)
|
||||
|
||||
char := s.accessory.GetCharacterByID(iid)
|
||||
if char == nil {
|
||||
@@ -86,61 +268,64 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a
|
||||
|
||||
switch char.Type {
|
||||
case camera.TypeSetupEndpoints:
|
||||
var offer camera.SetupEndpoints
|
||||
if err := tlv8.UnmarshalBase64(value.(string), &offer); err != nil {
|
||||
var offer camera.SetupEndpointsRequest
|
||||
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
|
||||
s.consumer.SetOffer(&offer)
|
||||
consumer := homekit.NewConsumer(conn, srtp2.Server)
|
||||
consumer.SetOffer(&offer)
|
||||
s.consumer = consumer
|
||||
|
||||
case camera.TypeSelectedStreamConfiguration:
|
||||
var conf camera.SelectedStreamConfig
|
||||
if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil {
|
||||
var conf camera.SelectedStreamConfiguration
|
||||
if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command)
|
||||
|
||||
switch conf.Control.Command {
|
||||
case camera.SessionCommandEnd:
|
||||
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
|
||||
_ = consumer.Stop()
|
||||
for _, consumer := range s.conns {
|
||||
if consumer, ok := consumer.(*homekit.Consumer); ok {
|
||||
if consumer.SessionID() == conf.Control.SessionID {
|
||||
_ = consumer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case camera.SessionCommandStart:
|
||||
if s.consumer == nil {
|
||||
consumer := s.consumer
|
||||
if consumer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.consumer.SetConfig(&conf) {
|
||||
if !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
|
||||
s.AddConn(consumer)
|
||||
|
||||
stream := streams.Get(s.stream)
|
||||
if err := stream.AddConsumer(s.consumer); err != nil {
|
||||
if err := stream.AddConsumer(consumer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
_, _ = s.consumer.WriteTo(nil)
|
||||
stream.RemoveConsumer(s.consumer)
|
||||
_, _ = consumer.WriteTo(nil)
|
||||
stream.RemoveConsumer(consumer)
|
||||
|
||||
delete(s.streams, conf.Control.SessionID)
|
||||
s.DelConn(consumer)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height)
|
||||
|
||||
stream := streams.Get(s.stream)
|
||||
cons := magic.NewKeyframe()
|
||||
@@ -166,69 +351,6 @@ func (s *server) GetImage(conn net.Conn, width, height int) []byte {
|
||||
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([]string{"homekit", s.stream, "pairings"}, s.pairings); 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
|
||||
@@ -263,3 +385,21 @@ func calcDevicePrivate(private, seed string) []byte {
|
||||
b := sha512.Sum512([]byte(seed))
|
||||
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
|
||||
}
|
||||
|
||||
func calcSetupID(seed string) string {
|
||||
b := sha512.Sum512([]byte(seed))
|
||||
return fmt.Sprintf("%02X%02X", b[44], b[46])
|
||||
}
|
||||
|
||||
func calcCategoryID(categoryID string) string {
|
||||
switch categoryID {
|
||||
case "bridge":
|
||||
return hap.CategoryBridge
|
||||
case "doorbell":
|
||||
return hap.CategoryDoorbell
|
||||
}
|
||||
if core.Atoi(categoryID) > 0 {
|
||||
return categoryID
|
||||
}
|
||||
return hap.CategoryCamera
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@ package ivideon
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ivideon", func(source string) (core.Producer, error) {
|
||||
return ivideon.Dial(source)
|
||||
})
|
||||
streams.HandleFunc("ivideon", ivideon.Dial)
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ func Init() {
|
||||
var log zerolog.Logger
|
||||
|
||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
stream, _ := streams.GetOrPatch(r.URL.Query())
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
@@ -146,7 +145,7 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
stream := streams.GetOrPatch(query)
|
||||
stream, _ := streams.GetOrPatch(query)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
|
||||
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
With ngrok integration, you can get external access to your streams in situations when you have Internet with a private IP address.
|
||||
|
||||
- you may need external access for two different things:
|
||||
- WebRTC stream, so you need a tunnel WebRTC TCP port (ex. 8555)
|
||||
- go2rtc web interface, so you need a tunnel API HTTP port (ex. 1984)
|
||||
- ngrok supports authorization for your web interface
|
||||
- ngrok automatically adds HTTPS to your web interface
|
||||
|
||||
The ngrok free subscription has the following limitations:
|
||||
|
||||
- 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 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).
|
||||
|
||||
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**
|
||||
|
||||
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
```
|
||||
|
||||
**Tunnel for WebRTC and Web interface**
|
||||
|
||||
You need to create `ngrok.yaml` config file and add it to the go2rtc config:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok start --all --config ngrok.yaml
|
||||
```
|
||||
|
||||
ngrok config example:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
tunnels:
|
||||
api:
|
||||
addr: 1984 # use the same port as in the go2rtc config
|
||||
proto: http
|
||||
basic_auth:
|
||||
- admin:password # you can set login/pass for your web interface
|
||||
webrtc:
|
||||
addr: 8555 # use the same port as in the go2rtc config
|
||||
proto: tcp
|
||||
```
|
||||
|
||||
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
|
||||
@@ -45,6 +45,10 @@ func streamOnvif(rawURL string) (core.Producer, error) {
|
||||
|
||||
log.Debug().Msgf("[onvif] new uri=%s", uri)
|
||||
|
||||
if err = streams.Validate(uri); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return streams.GetProducer(uri)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Pinggy
|
||||
|
||||
[Pinggy](https://pinggy.io/) - nice service for public tunnels to your local services.
|
||||
|
||||
**Features:**
|
||||
|
||||
- A free account does not require registration.
|
||||
- It does not require downloading third-party binaries and works over the SSH protocol.
|
||||
- Works with HTTP, TCP and UDP protocols.
|
||||
- Creates HTTPS for your HTTP services.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> A free account creates a tunnel with a random address that only works for an hour. It is suitable for testing purposes ONLY.
|
||||
|
||||
> [!CAUTION]
|
||||
> Public access to go2rtc without authorization puts your entire home network at risk. Use with caution.
|
||||
|
||||
**Why:**
|
||||
|
||||
- It's easy to set up HTTPS for testing two-way audio.
|
||||
- It's easy to check whether external access via WebRTC technology will work.
|
||||
- It's easy to share direct access to your RTSP or HTTP camera with the go2rtc developer. If such access is necessary to debug your problem.
|
||||
|
||||
## Configuration
|
||||
|
||||
You will find public links in the go2rtc log after startup.
|
||||
|
||||
**Tunnel to go2rtc WebUI.**
|
||||
|
||||
```yaml
|
||||
pinggy:
|
||||
tunnel: http://localhost:1984
|
||||
```
|
||||
|
||||
**Tunnel to RTSP camera.**
|
||||
|
||||
For example, you have camera: `rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0`
|
||||
|
||||
```yaml
|
||||
pinggy:
|
||||
tunnel: tcp://192.168.10.91:554
|
||||
```
|
||||
|
||||
In go2rtc logs you will get similar output:
|
||||
|
||||
```
|
||||
16:17:43.167 INF [pinggy] proxy url=tcp://abcde-123-123-123-123.a.free.pinggy.link:12345
|
||||
```
|
||||
|
||||
Now you have working stream:
|
||||
|
||||
```
|
||||
rtsp://admin:password@abcde-123-123-123-123.a.free.pinggy.link:12345/cam/realmonitor?channel=1&subtype=0
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
package pinggy
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pinggy"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Tunnel string `yaml:"tunnel"`
|
||||
} `yaml:"pinggy"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
if cfg.Mod.Tunnel == "" {
|
||||
return
|
||||
}
|
||||
|
||||
log = app.GetLogger("pinggy")
|
||||
|
||||
u, err := url.Parse(cfg.Mod.Tunnel)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
go proxy(u.Scheme, u.Host)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func proxy(proto, address string) {
|
||||
client, err := pinggy.NewClient(proto)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
urls, err := client.GetURLs()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range urls {
|
||||
log.Info().Str("url", s).Msgf("[pinggy] proxy")
|
||||
}
|
||||
|
||||
err = client.Proxy(address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
+14
-10
@@ -1,10 +1,11 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -21,8 +22,7 @@ func Init() {
|
||||
|
||||
func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
var ringAPI *ring.RingRestClient
|
||||
var err error
|
||||
var ringAPI *ring.RingApi
|
||||
|
||||
// Check auth method
|
||||
if email := query.Get("email"); email != "" {
|
||||
@@ -30,7 +30,8 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
password := query.Get("password")
|
||||
code := query.Get("code")
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.EmailAuth{
|
||||
var err error
|
||||
ringAPI, err = ring.NewRestClient(ring.EmailAuth{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}, nil)
|
||||
@@ -44,7 +45,7 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err = ringAPI.GetAuth(code); err != nil {
|
||||
if ringAPI.Using2FA {
|
||||
// Return 2FA prompt
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
api.ResponseJSON(w, map[string]interface{}{
|
||||
"needs_2fa": true,
|
||||
"prompt": ringAPI.PromptFor2FA,
|
||||
})
|
||||
@@ -53,36 +54,39 @@ func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
} else if refreshToken := query.Get("refresh_token"); refreshToken != "" {
|
||||
// Refresh Token Flow
|
||||
refreshToken := query.Get("refresh_token")
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ringAPI, err = ring.NewRingRestClient(ring.RefreshTokenAuth{
|
||||
var err error
|
||||
ringAPI, err = ring.NewRestClient(ring.RefreshTokenAuth{
|
||||
RefreshToken: refreshToken,
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "either email/password or refresh token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch devices
|
||||
devices, err := ringAPI.FetchRingDevices()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create clean query with only required parameters
|
||||
cleanQuery := url.Values{}
|
||||
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
|
||||
|
||||
var items []*api.Source
|
||||
for _, camera := range devices.AllCameras {
|
||||
cleanQuery.Set("camera_id", fmt.Sprint(camera.ID))
|
||||
cleanQuery.Set("device_id", camera.DeviceID)
|
||||
|
||||
// Stream source
|
||||
|
||||
+62
-5
@@ -5,10 +5,14 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
)
|
||||
|
||||
func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
w = creds.SecretResponse(w)
|
||||
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
|
||||
@@ -27,7 +31,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cons := probe.NewProbe(query)
|
||||
cons := probe.Create("probe", query)
|
||||
if len(cons.Medias) != 0 {
|
||||
cons.WithRequest(r)
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
@@ -48,8 +52,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
name = src
|
||||
}
|
||||
|
||||
if New(name, query["src"]...) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
if _, err := New(name, query["src"]...); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,8 +69,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass
|
||||
if Patch(name, src) == nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
if _, err := Patch(name, src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
case "POST":
|
||||
@@ -120,5 +124,58 @@ func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
dot = append(dot, '}')
|
||||
|
||||
dot = []byte(creds.SecretString(string(dot)))
|
||||
|
||||
api.Response(w, dot, "text/vnd.graphviz")
|
||||
}
|
||||
|
||||
func apiPreload(w http.ResponseWriter, r *http.Request) {
|
||||
// GET - return all preloads
|
||||
if r.Method == "GET" {
|
||||
api.ResponseJSON(w, GetPreloads())
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
src := query.Get("src")
|
||||
|
||||
switch r.Method {
|
||||
case "PUT":
|
||||
// it's safe to delete from map while iterating
|
||||
for k := range query {
|
||||
switch k {
|
||||
case core.KindVideo, core.KindAudio, "microphone":
|
||||
default:
|
||||
delete(query, k)
|
||||
}
|
||||
}
|
||||
|
||||
rawQuery := query.Encode()
|
||||
|
||||
if err := AddPreload(src, rawQuery); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig([]string{"preload", src}, rawQuery); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
if err := DelPreload(src); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.PatchConfig([]string{"preload", src}, nil); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
default:
|
||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func apiSchemes(w http.ResponseWriter, r *http.Request) {
|
||||
api.ResponseJSON(w, SupportedSchemes())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApiSchemes(t *testing.T) {
|
||||
// Setup: Register some test handlers and redirects
|
||||
HandleFunc("rtsp", func(url string) (core.Producer, error) { return nil, nil })
|
||||
HandleFunc("rtmp", func(url string) (core.Producer, error) { return nil, nil })
|
||||
RedirectFunc("http", func(url string) (string, error) { return "", nil })
|
||||
|
||||
t.Run("GET request returns schemes", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/schemes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
apiSchemes(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
||||
|
||||
var schemes []string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &schemes)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, schemes)
|
||||
|
||||
// Check that our test schemes are in the response
|
||||
require.Contains(t, schemes, "rtsp")
|
||||
require.Contains(t, schemes, "rtmp")
|
||||
require.Contains(t, schemes, "http")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApiSchemesNoDuplicates(t *testing.T) {
|
||||
// Setup: Register a scheme in both handlers and redirects
|
||||
HandleFunc("duplicate", func(url string) (core.Producer, error) { return nil, nil })
|
||||
RedirectFunc("duplicate", func(url string) (string, error) { return "", nil })
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/schemes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
apiSchemes(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var schemes []string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &schemes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count occurrences of "duplicate"
|
||||
count := 0
|
||||
for _, scheme := range schemes {
|
||||
if scheme == "duplicate" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// Should only appear once
|
||||
require.Equal(t, 1, count, "scheme 'duplicate' should appear exactly once")
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package streams
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -15,6 +16,21 @@ func HandleFunc(scheme string, handler Handler) {
|
||||
handlers[scheme] = handler
|
||||
}
|
||||
|
||||
func SupportedSchemes() []string {
|
||||
uniqueKeys := make(map[string]struct{}, len(handlers)+len(redirects))
|
||||
for scheme := range handlers {
|
||||
uniqueKeys[scheme] = struct{}{}
|
||||
}
|
||||
for scheme := range redirects {
|
||||
uniqueKeys[scheme] = struct{}{}
|
||||
}
|
||||
resultKeys := make([]string, 0, len(uniqueKeys))
|
||||
for key := range uniqueKeys {
|
||||
resultKeys = append(resultKeys, key)
|
||||
}
|
||||
return resultKeys
|
||||
}
|
||||
|
||||
func HasProducer(url string) bool {
|
||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||
scheme := url[:i]
|
||||
@@ -95,3 +111,24 @@ func GetConsumer(url string) (core.Consumer, func(), error) {
|
||||
|
||||
return nil, nil, errors.New("streams: unsupported scheme: " + url)
|
||||
}
|
||||
|
||||
var insecure = map[string]bool{}
|
||||
|
||||
func MarkInsecure(scheme string) {
|
||||
insecure[scheme] = true
|
||||
}
|
||||
|
||||
var sanitize = regexp.MustCompile(`\s`)
|
||||
|
||||
func Validate(source string) error {
|
||||
// TODO: Review the entire logic of insecure sources
|
||||
if i := strings.IndexByte(source, ':'); i > 0 {
|
||||
if insecure[source[:i]] {
|
||||
return errors.New("streams: source from insecure producer")
|
||||
}
|
||||
}
|
||||
if sanitize.MatchString(source) {
|
||||
return errors.New("streams: source with spaces may be insecure")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func (s *Stream) Play(source string) error {
|
||||
func (s *Stream) Play(urlOrProd any) error {
|
||||
s.mu.Lock()
|
||||
for _, producer := range s.producers {
|
||||
if producer.state == stateInternal && producer.conn != nil {
|
||||
@@ -16,12 +16,18 @@ func (s *Stream) Play(source string) error {
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if source == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var source string
|
||||
var src core.Producer
|
||||
|
||||
switch urlOrProd.(type) {
|
||||
case string:
|
||||
if source = urlOrProd.(string); source == "" {
|
||||
return nil
|
||||
}
|
||||
case core.Producer:
|
||||
src = urlOrProd.(core.Producer)
|
||||
}
|
||||
|
||||
for _, producer := range s.producers {
|
||||
if producer.conn == nil {
|
||||
continue
|
||||
@@ -140,10 +146,12 @@ func matchMedia(prod core.Producer, cons core.Consumer) bool {
|
||||
|
||||
track, err := prod.GetTrack(prodMedia, prodCodec)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[streams] can't get track")
|
||||
continue
|
||||
}
|
||||
|
||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
||||
log.Warn().Err(err).Msg("[streams] can't add track")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package streams
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/probe"
|
||||
)
|
||||
|
||||
type Preload struct {
|
||||
stream *Stream // Don't output the stream to JSON to not worry about its secrets.
|
||||
Cons *probe.Probe `json:"consumer"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
var preloads = map[string]*Preload{}
|
||||
var preloadsMu sync.Mutex
|
||||
|
||||
func AddPreload(name, rawQuery string) error {
|
||||
if rawQuery == "" {
|
||||
rawQuery = "video&audio"
|
||||
}
|
||||
|
||||
query, err := url.ParseQuery(rawQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preloadsMu.Lock()
|
||||
defer preloadsMu.Unlock()
|
||||
|
||||
if p := preloads[name]; p != nil {
|
||||
p.stream.RemoveConsumer(p.Cons)
|
||||
}
|
||||
|
||||
stream := Get(name)
|
||||
if stream == nil {
|
||||
return fmt.Errorf("streams: stream not found: %s", name)
|
||||
}
|
||||
cons := probe.Create("preload", query)
|
||||
|
||||
if err = stream.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preloads[name] = &Preload{stream: stream, Cons: cons, Query: rawQuery}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DelPreload(name string) error {
|
||||
preloadsMu.Lock()
|
||||
defer preloadsMu.Unlock()
|
||||
|
||||
if p := preloads[name]; p != nil {
|
||||
p.stream.RemoveConsumer(p.Cons)
|
||||
delete(preloads, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("streams: preload not found: %s", name)
|
||||
}
|
||||
|
||||
func GetPreloads() map[string]*Preload {
|
||||
preloadsMu.Lock()
|
||||
defer preloadsMu.Unlock()
|
||||
return maps.Clone(preloads)
|
||||
}
|
||||
+31
-31
@@ -3,7 +3,6 @@ package streams
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -14,8 +13,9 @@ import (
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Streams map[string]any `yaml:"streams"`
|
||||
Publish map[string]any `yaml:"publish"`
|
||||
Streams map[string]any `yaml:"streams"`
|
||||
Publish map[string]any `yaml:"publish"`
|
||||
Preload map[string]string `yaml:"preload"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
@@ -28,34 +28,36 @@ func Init() {
|
||||
|
||||
api.HandleFunc("api/streams", apiStreams)
|
||||
api.HandleFunc("api/streams.dot", apiStreamsDOT)
|
||||
api.HandleFunc("api/preload", apiPreload)
|
||||
api.HandleFunc("api/schemes", apiSchemes)
|
||||
|
||||
if cfg.Publish == nil {
|
||||
if cfg.Publish == nil && cfg.Preload == nil {
|
||||
return
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
// range for nil map is OK
|
||||
for name, dst := range cfg.Publish {
|
||||
if stream := Get(name); stream != nil {
|
||||
Publish(stream, dst)
|
||||
}
|
||||
}
|
||||
for name, rawQuery := range cfg.Preload {
|
||||
if err := AddPreload(name, rawQuery); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var sanitize = regexp.MustCompile(`\s`)
|
||||
|
||||
// Validate - not allow creating dynamic streams with spaces in the source
|
||||
func Validate(source string) error {
|
||||
if sanitize.MatchString(source) {
|
||||
return errors.New("streams: invalid dynamic source")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(name string, sources ...string) *Stream {
|
||||
func New(name string, sources ...string) (*Stream, error) {
|
||||
for _, source := range sources {
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
if !HasProducer(source) {
|
||||
return nil, errors.New("streams: source not supported")
|
||||
}
|
||||
|
||||
if err := Validate(source); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +67,10 @@ func New(name string, sources ...string) *Stream {
|
||||
streams[name] = stream
|
||||
streamsMu.Unlock()
|
||||
|
||||
return stream
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func Patch(name string, source string) *Stream {
|
||||
func Patch(name string, source string) (*Stream, error) {
|
||||
streamsMu.Lock()
|
||||
defer streamsMu.Unlock()
|
||||
|
||||
@@ -80,7 +82,7 @@ func Patch(name string, source string) *Stream {
|
||||
// link (alias) streams[name] to streams[rtspName]
|
||||
streams[name] = stream
|
||||
}
|
||||
return stream
|
||||
return stream, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,46 +91,44 @@ func Patch(name string, source string) *Stream {
|
||||
// link (alias) streams[name] to streams[source]
|
||||
streams[name] = stream
|
||||
}
|
||||
return stream
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// check if src has supported scheme
|
||||
if !HasProducer(source) {
|
||||
return nil
|
||||
return nil, errors.New("streams: source not supported")
|
||||
}
|
||||
|
||||
if Validate(source) != nil {
|
||||
return nil
|
||||
if err := Validate(source); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check an existing stream with this name
|
||||
if stream, ok := streams[name]; ok {
|
||||
stream.SetSource(source)
|
||||
return stream
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// create new stream with this name
|
||||
stream := NewStream(source)
|
||||
streams[name] = stream
|
||||
return stream
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func GetOrPatch(query url.Values) *Stream {
|
||||
func GetOrPatch(query url.Values) (*Stream, error) {
|
||||
// check if src param exists
|
||||
source := query.Get("src")
|
||||
if source == "" {
|
||||
return nil
|
||||
return nil, errors.New("streams: source empty")
|
||||
}
|
||||
|
||||
// check if src is stream name
|
||||
if stream := Get(source); stream != nil {
|
||||
return stream
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// check if name param provided
|
||||
if name := query.Get("name"); name != "" {
|
||||
log.Info().Msgf("[streams] create new stream url=%s", source)
|
||||
|
||||
return Patch(name, source)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Tuya
|
||||
|
||||
*[New in v1.9.13](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13)*
|
||||
|
||||
[Tuya](https://www.tuya.com/) proprietary camera protocol with **two way audio** support. Go2rtc supports `Tuya Smart API` and `Tuya Cloud API`.
|
||||
|
||||
**Tuya Smart API (recommended)**:
|
||||
- Cameras can be discovered through the go2rtc web interface via Tuya Smart account (Add > Tuya > Select region and fill in email and password > Login).
|
||||
- **Smart Life accounts are not supported**, you need to create a Tuya Smart account. If the cameras are already added to the Smart Life app, you need to remove them and add them again to the Tuya Smart app.
|
||||
|
||||
**Tuya Cloud API**:
|
||||
- Requires setting up a cloud project in the Tuya Developer Platform.
|
||||
- Obtain `device_id`, `client_id`, `client_secret`, and `uid` from [Tuya IoT Platform](https://iot.tuya.com/). [Here's a guide](https://xzetsubou.github.io/hass-localtuya/cloud_api/).
|
||||
- Please ensure that you have subscribed to the `IoT Video Live Stream` service (Free Trial) in the Tuya Developer Platform, otherwise the stream will not work (Tuya Developer Platform > Service API > Authorize > IoT Video Live Stream).
|
||||
|
||||
## Configuration
|
||||
|
||||
Use `resolution` parameter to select the stream (not all cameras support `hd` stream through WebRTC even if the camera has it):
|
||||
- `hd` - HD stream (default)
|
||||
- `sd` - SD stream
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# Tuya Smart API: WebRTC main stream (use Add > Tuya to discover the URL)
|
||||
tuya_main:
|
||||
- tuya://protect-us.ismartlife.me?device_id=XXX&email=XXX&password=XXX
|
||||
|
||||
# Tuya Smart API: WebRTC sub stream (use Add > Tuya to discover the URL)
|
||||
tuya_sub:
|
||||
- tuya://protect-us.ismartlife.me?device_id=XXX&email=XXX&password=XXX&resolution=sd
|
||||
|
||||
# Tuya Cloud API: WebRTC main stream
|
||||
tuya_webrtc:
|
||||
- tuya://openapi.tuyaus.com?device_id=XXX&uid=XXX&client_id=XXX&client_secret=XXX
|
||||
|
||||
# Tuya Cloud API: WebRTC sub stream
|
||||
tuya_webrtc_sd:
|
||||
- tuya://openapi.tuyaus.com?device_id=XXX&uid=XXX&client_id=XXX&client_secret=XXX&resolution=sd
|
||||
```
|
||||
@@ -0,0 +1,248 @@
|
||||
package tuya
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tuya"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("tuya", func(source string) (core.Producer, error) {
|
||||
return tuya.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/tuya", apiTuya)
|
||||
}
|
||||
|
||||
func apiTuya(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
region := query.Get("region")
|
||||
email := query.Get("email")
|
||||
password := query.Get("password")
|
||||
|
||||
if email == "" || password == "" || region == "" {
|
||||
http.Error(w, "email, password and region are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var tuyaRegion *tuya.Region
|
||||
for _, r := range tuya.AvailableRegions {
|
||||
if r.Host == region {
|
||||
tuyaRegion = &r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if tuyaRegion == nil {
|
||||
http.Error(w, fmt.Sprintf("invalid region: %s", region), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
httpClient := tuya.CreateHTTPClientWithSession()
|
||||
|
||||
_, err := login(httpClient, tuyaRegion.Host, email, password, tuyaRegion.Continent)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("login failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tuyaAPI, err := tuya.NewTuyaSmartApiClient(
|
||||
httpClient,
|
||||
tuyaRegion.Host,
|
||||
email,
|
||||
password,
|
||||
"",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var devices []tuya.Device
|
||||
|
||||
homes, _ := tuyaAPI.GetHomeList()
|
||||
if homes != nil && len(homes.Result) > 0 {
|
||||
for _, home := range homes.Result {
|
||||
roomList, err := tuyaAPI.GetRoomList(strconv.Itoa(home.Gid))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, room := range roomList.Result {
|
||||
for _, device := range room.DeviceList {
|
||||
if (device.Category == "sp" || device.Category == "dghsxj") && !containsDevice(devices, device.DeviceId) {
|
||||
devices = append(devices, device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedHomes, _ := tuyaAPI.GetSharedHomeList()
|
||||
if sharedHomes != nil && len(sharedHomes.Result.SecurityWebCShareInfoList) > 0 {
|
||||
for _, sharedHome := range sharedHomes.Result.SecurityWebCShareInfoList {
|
||||
for _, device := range sharedHome.DeviceInfoList {
|
||||
if (device.Category == "sp" || device.Category == "dghsxj") && !containsDevice(devices, device.DeviceId) {
|
||||
devices = append(devices, device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
http.Error(w, "no cameras found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var items []*api.Source
|
||||
for _, device := range devices {
|
||||
cleanQuery := url.Values{}
|
||||
cleanQuery.Set("device_id", device.DeviceId)
|
||||
cleanQuery.Set("email", email)
|
||||
cleanQuery.Set("password", password)
|
||||
url := fmt.Sprintf("tuya://%s?%s", tuyaRegion.Host, cleanQuery.Encode())
|
||||
|
||||
items = append(items, &api.Source{
|
||||
Name: device.DeviceName,
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
|
||||
api.ResponseSources(w, items)
|
||||
}
|
||||
|
||||
func login(client *http.Client, serverHost, email, password, countryCode string) (*tuya.LoginResult, error) {
|
||||
tokenResp, err := getLoginToken(client, serverHost, email, countryCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedPassword, err := tuya.EncryptPassword(password, tokenResp.Result.PbKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt password: %v", err)
|
||||
}
|
||||
|
||||
var loginResp *tuya.PasswordLoginResponse
|
||||
var url string
|
||||
|
||||
loginReq := tuya.PasswordLoginRequest{
|
||||
CountryCode: countryCode,
|
||||
Passwd: encryptedPassword,
|
||||
Token: tokenResp.Result.Token,
|
||||
IfEncrypt: 1,
|
||||
Options: `{"group":1}`,
|
||||
}
|
||||
|
||||
if tuya.IsEmailAddress(email) {
|
||||
url = fmt.Sprintf("https://%s/api/private/email/login", serverHost)
|
||||
loginReq.Email = email
|
||||
} else {
|
||||
url = fmt.Sprintf("https://%s/api/private/phone/login", serverHost)
|
||||
loginReq.Mobile = email
|
||||
}
|
||||
|
||||
loginResp, err = performLogin(client, url, loginReq, serverHost)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !loginResp.Success {
|
||||
return nil, errors.New(loginResp.ErrorMsg)
|
||||
}
|
||||
|
||||
return &loginResp.Result, nil
|
||||
}
|
||||
|
||||
func getLoginToken(client *http.Client, serverHost, username, countryCode string) (*tuya.LoginTokenResponse, error) {
|
||||
url := fmt.Sprintf("https://%s/api/login/token", serverHost)
|
||||
|
||||
tokenReq := tuya.LoginTokenRequest{
|
||||
CountryCode: countryCode,
|
||||
Username: username,
|
||||
IsUid: false,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(tokenReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Origin", fmt.Sprintf("https://%s", serverHost))
|
||||
req.Header.Set("Referer", fmt.Sprintf("https://%s/login", serverHost))
|
||||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var tokenResp tuya.LoginTokenResponse
|
||||
if err = json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !tokenResp.Success {
|
||||
return nil, errors.New("tuya: " + tokenResp.Msg)
|
||||
}
|
||||
|
||||
return &tokenResp, nil
|
||||
}
|
||||
|
||||
func performLogin(client *http.Client, url string, loginReq tuya.PasswordLoginRequest, serverHost string) (*tuya.PasswordLoginResponse, error) {
|
||||
jsonData, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Origin", fmt.Sprintf("https://%s", serverHost))
|
||||
req.Header.Set("Referer", fmt.Sprintf("https://%s/login", serverHost))
|
||||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var loginResp tuya.PasswordLoginResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &loginResp, nil
|
||||
}
|
||||
|
||||
func containsDevice(devices []tuya.Device, deviceID string) bool {
|
||||
for _, device := range devices {
|
||||
if device.DeviceId == deviceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !linux
|
||||
//go:build !(linux && (386 || arm || mipsle || amd64 || arm64))
|
||||
|
||||
package v4l2
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build linux && (386 || arm || mipsle || amd64 || arm64)
|
||||
|
||||
package v4l2
|
||||
|
||||
import (
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
@@ -18,9 +18,11 @@ type Address struct {
|
||||
Priority uint32
|
||||
}
|
||||
|
||||
var stuns []string
|
||||
|
||||
func (a *Address) Host() string {
|
||||
if a.host == "stun" {
|
||||
ip, err := webrtc.GetCachedPublicIP()
|
||||
ip, err := webrtc.GetCachedPublicIP(stuns...)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// streamsHandler supports:
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
)
|
||||
|
||||
// https://github.com/AlexxIT/go2rtc/issues/1600
|
||||
@@ -27,7 +28,6 @@ func crealityClient(url string) (core.Producer, error) {
|
||||
|
||||
medias := []*core.Media{
|
||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
||||
}
|
||||
|
||||
// TODO: return webrtc.SessionDescription
|
||||
@@ -36,6 +36,8 @@ func crealityClient(url string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
body, err := offerToB64(offer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -61,6 +63,12 @@ func crealityClient(url string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[webrtc] answer:\n%s", answer)
|
||||
|
||||
if answer, err = fixCrealitySDP(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = prod.SetAnswer(answer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -108,3 +116,37 @@ func answerFromB64(r io.Reader) (string, error) {
|
||||
// string "v=0..."
|
||||
return v["sdp"], nil
|
||||
}
|
||||
|
||||
func fixCrealitySDP(value string) (string, error) {
|
||||
var sd sdp.SessionDescription
|
||||
if err := sd.UnmarshalString(value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
md := sd.MediaDescriptions[0]
|
||||
|
||||
// important to skip first codec, because second codec will be used
|
||||
skip := md.MediaName.Formats[0]
|
||||
md.MediaName.Formats = md.MediaName.Formats[1:]
|
||||
|
||||
attrs := make([]sdp.Attribute, 0, len(md.Attributes))
|
||||
for _, attr := range md.Attributes {
|
||||
switch attr.Key {
|
||||
case "fmtp", "rtpmap":
|
||||
// important to skip fmtp with x-google, because this is second fmtp for same codec
|
||||
// and pion library will fail parsing this SDP
|
||||
if strings.HasPrefix(attr.Value, skip) || strings.Contains(attr.Value, "x-google") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
|
||||
md.Attributes = attrs
|
||||
|
||||
b, err := sd.Marshal()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type kinesisRequest struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// This package handles the Milestone WebRTC session lifecycle, including authentication,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
const MimeSDP = "application/sdp"
|
||||
|
||||
@@ -33,8 +33,12 @@ func switchbotClient(rawURL string, query url.Values) (core.Producer, error) {
|
||||
v.Resolution = 0
|
||||
case "sd":
|
||||
v.Resolution = 1
|
||||
case "auto":
|
||||
v.Resolution = 2
|
||||
}
|
||||
|
||||
v.PlayType = core.Atoi(query.Get("play_type")) // zero by default
|
||||
|
||||
return v, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ func Init() {
|
||||
|
||||
cfg.Mod.Listen = ":8555"
|
||||
cfg.Mod.IceServers = []pion.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
{URLs: []string{"stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302"}},
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
@@ -38,6 +38,16 @@ func Init() {
|
||||
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
|
||||
for _, candidate := range cfg.Mod.Candidates {
|
||||
AddCandidate(network, candidate)
|
||||
|
||||
if strings.HasPrefix(candidate, "stun:") && stuns == nil {
|
||||
for _, ice := range cfg.Mod.IceServers {
|
||||
for _, url := range ice.URLs {
|
||||
if strings.HasPrefix(url, "stun:") {
|
||||
stuns = append(stuns, url[5:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -95,7 +105,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) {
|
||||
|
||||
query := tr.Request.URL.Query()
|
||||
if name := query.Get("src"); name != "" {
|
||||
stream = streams.GetOrPatch(query)
|
||||
stream, _ = streams.GetOrPatch(query)
|
||||
mode = core.ModePassiveConsumer
|
||||
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
|
||||
} else if name = query.Get("dst"); name != "" {
|
||||
|
||||
@@ -2,10 +2,11 @@ package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -36,3 +37,37 @@ func TestWebRTCAPIv2(t *testing.T) {
|
||||
require.Equal(t, "v=0\n...", offer.SDP)
|
||||
require.Equal(t, "stun:stun.l.google.com:19302", offer.ICEServers[0].URLs[0])
|
||||
}
|
||||
|
||||
func TestCrealitySDP(t *testing.T) {
|
||||
sdp := `v=0
|
||||
o=- 1495799811084970 1495799811084970 IN IP4 0.0.0.0
|
||||
s=-
|
||||
t=0 0
|
||||
a=msid-semantic:WMS *
|
||||
a=group:BUNDLE 0
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 96 98
|
||||
a=rtcp-fb:98 nack
|
||||
a=rtcp-fb:98 nack pli
|
||||
a=fmtp:96 profile-level-id=42e01f;level-asymmetry-allowed=1
|
||||
a=fmtp:98 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1
|
||||
a=fmtp:98 x-google-max-bitrate=6000;x-google-min-bitrate=2000;x-google-start-bitrate=4000
|
||||
a=rtpmap:96 H264/90000
|
||||
a=rtpmap:98 H264/90000
|
||||
a=ssrc:1 cname:pear
|
||||
c=IN IP4 0.0.0.0
|
||||
a=sendonly
|
||||
a=mid:0
|
||||
a=rtcp-mux
|
||||
a=ice-ufrag:7AVa
|
||||
a=ice-pwd:T+F/5y05Paw+mtG5Jrd8N3
|
||||
a=ice-options:trickle
|
||||
a=fingerprint:sha-256 A5:AB:C0:4E:29:5B:BD:3B:7D:88:24:6C:56:6B:2A:79:A3:76:99:35:57:75:AD:C8:5A:A6:34:20:88:1B:68:EF
|
||||
a=setup:passive
|
||||
a=candidate:1 1 UDP 2015363327 172.22.233.10 48929 typ host
|
||||
a=candidate:2 1 TCP 1015021823 172.22.233.10 0 typ host tcptype active
|
||||
a=candidate:3 1 TCP 1010827519 172.22.233.10 60677 typ host tcptype passive
|
||||
`
|
||||
sdp, err := fixCrealitySDP(sdp)
|
||||
require.Nil(t, err)
|
||||
require.False(t, strings.Contains(sdp, "x-google-max-bitrate"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
# Wyoming
|
||||
|
||||
This module provide [Wyoming Protocol](https://www.home-assistant.io/integrations/wyoming/) support to create local voice assistants using [Home Assistant](https://www.home-assistant.io/).
|
||||
|
||||
- go2rtc can act as [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite)
|
||||
- go2rtc can act as [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external)
|
||||
- go2rtc can act as [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external)
|
||||
- any supported audio source with PCM codec can be used as audio input
|
||||
- any supported two-way audio source with PCM codec can be used as audio output
|
||||
- any desktop/server microphone/speaker can be used as two-way audio source
|
||||
- supported any OS via FFmpeg or any similar software
|
||||
- supported Linux via alsa source
|
||||
- you can change the behavior using the built-in scripting engine
|
||||
|
||||
## Typical Voice Pipeline
|
||||
|
||||
1. Audio stream (MIC)
|
||||
- any audio source with PCM codec support (include PCMA/PCMU)
|
||||
2. Voice Activity Detector (VAD)
|
||||
3. Wake Word (WAKE)
|
||||
- [OpenWakeWord](https://www.home-assistant.io/voice_control/create_wake_word/)
|
||||
4. Speech-to-Text (STT)
|
||||
- [Whisper](https://github.com/home-assistant/addons/blob/master/whisper/README.md)
|
||||
- [Vosk](https://github.com/rhasspy/hassio-addons/blob/master/vosk/README.md)
|
||||
5. Conversation agent (INTENT)
|
||||
- [Home Assistant](https://www.home-assistant.io/integrations/conversation/)
|
||||
6. Text-to-speech (TTS)
|
||||
- [Google Translate](https://www.home-assistant.io/integrations/google_translate/)
|
||||
- [Piper](https://github.com/home-assistant/addons/blob/master/piper/README.md)
|
||||
7. Audio stream (SND)
|
||||
- any source with two-way audio (backchannel) and PCM codec support (include PCMA/PCMU)
|
||||
|
||||
You can use a large number of different projects for WAKE, STT, INTENT and TTS thanks to the Home Assistant.
|
||||
|
||||
And you can use a large number of different technologies for MIC and SND thanks to Go2rtc.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can optionally specify WAKE service. So go2rtc will start transmitting audio to Home Assistant only after WAKE word. If the WAKE service cannot be connected to or not specified - go2rtc will pass all audio to Home Assistant. In this case WAKE service must be configured in your Voice Assistant pipeline.
|
||||
|
||||
You can optionally specify VAD threshold. So go2rtc will start transmitting audio to WAKE service only after some audio noise.
|
||||
|
||||
Your stream must support audio transmission in PCM codec (include PCMA/PCMU).
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
stream_name_from_streams_section:
|
||||
listen: :10700
|
||||
name: "My Satellite" # optional name
|
||||
wake_uri: tcp://192.168.1.23:10400 # optional WAKE service
|
||||
vad_threshold: 1 # optional VAD threshold (from 0.1 to 3.5)
|
||||
```
|
||||
|
||||
Home Assistant -> Settings -> Integrations -> Add -> Wyoming Protocol -> Host + Port from `go2rtc.yaml`
|
||||
|
||||
Select one or multiple wake words:
|
||||
```yaml
|
||||
wake_uri: tcp://192.168.1.23:10400?name=alexa_v0.1&name=hey_jarvis_v0.1&name=hey_mycroft_v0.1&name=hey_rhasspy_v0.1&name=ok_nabu_v0.1
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
You can add wyoming event handling using the [expr](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md) language. For example, to pronounce TTS on some media player from HA.
|
||||
|
||||
Turn on the logs to see what kind of events happens.
|
||||
|
||||
This is what the default scripts look like:
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
script_example:
|
||||
event:
|
||||
run-satellite: Detect()
|
||||
pause-satellite: Stop()
|
||||
voice-stopped: Pause()
|
||||
audio-stop: PlayAudio() && WriteEvent("played") && Detect()
|
||||
error: Detect()
|
||||
internal-run: WriteEvent("run-pipeline", '{"start_stage":"wake","end_stage":"tts"}') && Stream()
|
||||
internal-detection: WriteEvent("run-pipeline", '{"start_stage":"asr","end_stage":"tts"}') && Stream()
|
||||
```
|
||||
|
||||
Supported functions and variables:
|
||||
|
||||
- `Detect()` - start the VAD and WAKE word detection process
|
||||
- `Stream()` - start transmission of audio data to the client (Home Assistant)
|
||||
- `Stop()` - stop and disconnect stream without disconnecting client (Home Assistant)
|
||||
- `Pause()` - temporary pause of audio transfer, without disconnecting the stream
|
||||
- `PlayAudio()` - playing the last audio that was sent from client (Home Assistant)
|
||||
- `WriteEvent(type, data)` - send event to client (Home Assistant)
|
||||
- `Sleep(duration)` - temporary script pause (ex. `Sleep('1.5s')`)
|
||||
- `PlayFile(path)` - play audio from `wav` file
|
||||
- `Type` - type (name) of event
|
||||
- `Data` - event data in JSON format (ex. `{"text":"how are you"}`)
|
||||
- also available other functions from [expr](https://github.com/AlexxIT/go2rtc/blob/master/internal/expr/README.md) module (ex. `fetch`)
|
||||
|
||||
If you write a script for an event - the default action is no longer executed. You need to repeat the necessary steps yourself.
|
||||
|
||||
In addition to the standard events, there are two additional events:
|
||||
|
||||
- `internal-run` - called after `Detect()` when VAD detected, but WAKE service unavailable
|
||||
- `internal-detection` - called after `Detect()` when WAKE word detected
|
||||
|
||||
**Example 1.** You want to play a sound file when a wake word detected (only `wav` supported):
|
||||
|
||||
- `PlayFile` and `PlayAudio` functions are executed synchronously, the following steps will be executed only after they are completed
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
script_example:
|
||||
event:
|
||||
internal-detection: PlayFile('/media/beep.wav') && WriteEvent("run-pipeline", '{"start_stage":"asr","end_stage":"tts"}') && Stream()
|
||||
```
|
||||
|
||||
**Example 2.** You want to play TTS on a Home Assistant media player:
|
||||
|
||||
Each event has a `Type` and `Data` in JSON format. You can use their values in scripts.
|
||||
|
||||
- in the `synthesize` step, we get the value of the `text` and call the HA REST API
|
||||
- in the `audio-stop` step we get the duration of the TTS in seconds, wait for this time and start the pipeline again
|
||||
|
||||
```yaml
|
||||
wyoming:
|
||||
script_example:
|
||||
event:
|
||||
synthesize: |
|
||||
let text = fromJSON(Data).text;
|
||||
let token = 'eyJhbGci...';
|
||||
fetch('http://localhost:8123/api/services/tts/speak', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': 'Bearer '+token,'Content-Type': 'application/json'},
|
||||
body: toJSON({
|
||||
entity_id: 'tts.google_translate_com',
|
||||
media_player_entity_id: 'media_player.google_nest',
|
||||
message: text,
|
||||
language: 'en',
|
||||
}),
|
||||
}).ok
|
||||
audio-stop: |
|
||||
let timestamp = fromJSON(Data).timestamp;
|
||||
let delay = string(timestamp)+'s';
|
||||
Sleep(delay) && WriteEvent("played") && Detect()
|
||||
```
|
||||
|
||||
## Config examples
|
||||
|
||||
Satellite on Windows server using FFmpeg and FFplay.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
satellite_win:
|
||||
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
|
||||
- exec:ffplay -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050
|
||||
|
||||
wyoming:
|
||||
satellite_win:
|
||||
listen: :10700
|
||||
name: "Windows Satellite"
|
||||
wake_uri: tcp://192.168.1.23:10400
|
||||
vad_threshold: 1
|
||||
```
|
||||
|
||||
Satellite on Dahua camera with two-way audio support.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua_camera:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
|
||||
|
||||
wyoming:
|
||||
dahua_camera:
|
||||
listen: :10700
|
||||
name: "Dahua Satellite"
|
||||
wake_uri: tcp://192.168.1.23:10400
|
||||
vad_threshold: 1
|
||||
```
|
||||
|
||||
Satellite on external wyoming Microphone and Sound.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
wyoming_external:
|
||||
- wyoming://192.168.1.23:10600 # wyoming-mic-external
|
||||
- wyoming://192.168.1.23:10601?backchannel=1 # wyoming-snd-external
|
||||
|
||||
wyoming:
|
||||
wyoming_external:
|
||||
listen: :10700
|
||||
name: "Wyoming Satellite"
|
||||
wake_uri: tcp://192.168.1.23:10400
|
||||
vad_threshold: 1
|
||||
```
|
||||
|
||||
## Wyoming External Microphone and Sound
|
||||
|
||||
Advanced users, who want to enjoy the [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite) project, can use go2rtc as a [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external) or [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external).
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
wyoming_mic_external:
|
||||
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
|
||||
wyoming_snd_external:
|
||||
- exec:ffplay -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050
|
||||
|
||||
wyoming:
|
||||
wyoming_mic_external:
|
||||
listen: :10600
|
||||
mode: mic
|
||||
wyoming_snd_external:
|
||||
listen: :10601
|
||||
mode: snd
|
||||
```
|
||||
|
||||
**docker-compose.yml**
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
satellite:
|
||||
build: wyoming-satellite # https://github.com/rhasspy/wyoming-satellite
|
||||
ports:
|
||||
- "10700:10700"
|
||||
command:
|
||||
- "--name"
|
||||
- "my satellite"
|
||||
- "--mic-uri"
|
||||
- "tcp://192.168.1.23:10600"
|
||||
- "--snd-uri"
|
||||
- "tcp://192.168.1.23:10601"
|
||||
- "--debug"
|
||||
```
|
||||
|
||||
## Wyoming External Source
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
wyoming_external:
|
||||
- wyoming://192.168.1.23:10600
|
||||
- wyoming://192.168.1.23:10601?backchannel=1
|
||||
```
|
||||
|
||||
**docker-compose.yml**
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
microphone:
|
||||
build: wyoming-mic-external # https://github.com/rhasspy/wyoming-mic-external
|
||||
ports:
|
||||
- "10600:10600"
|
||||
devices:
|
||||
- /dev/snd:/dev/snd
|
||||
group_add:
|
||||
- audio
|
||||
command:
|
||||
- "--device"
|
||||
- "sysdefault"
|
||||
- "--debug"
|
||||
playback:
|
||||
build: wyoming-snd-external # https://github.com/rhasspy/wyoming-snd-external
|
||||
ports:
|
||||
- "10601:10601"
|
||||
devices:
|
||||
- /dev/snd:/dev/snd
|
||||
group_add:
|
||||
- audio
|
||||
command:
|
||||
- "--device"
|
||||
- "sysdefault"
|
||||
- "--debug"
|
||||
```
|
||||
|
||||
## Debug
|
||||
|
||||
```yaml
|
||||
log:
|
||||
wyoming: trace
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
package wyoming
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/wyoming"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("wyoming", wyoming.Dial)
|
||||
|
||||
// server
|
||||
var cfg struct {
|
||||
Mod map[string]struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Name string `yaml:"name"`
|
||||
Mode string `yaml:"mode"`
|
||||
Event map[string]string `yaml:"event"`
|
||||
WakeURI string `yaml:"wake_uri"`
|
||||
VADThreshold float32 `yaml:"vad_threshold"`
|
||||
} `yaml:"wyoming"`
|
||||
}
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
log = app.GetLogger("wyoming")
|
||||
|
||||
for name, cfg := range cfg.Mod {
|
||||
stream := streams.Get(name)
|
||||
if stream == nil {
|
||||
log.Warn().Msgf("[wyoming] missing stream: %s", name)
|
||||
continue
|
||||
}
|
||||
|
||||
if cfg.Name == "" {
|
||||
cfg.Name = name
|
||||
}
|
||||
|
||||
srv := &wyoming.Server{
|
||||
Name: cfg.Name,
|
||||
Event: cfg.Event,
|
||||
VADThreshold: int16(1000 * cfg.VADThreshold), // 1.0 => 1000
|
||||
WakeURI: cfg.WakeURI,
|
||||
MicHandler: func(cons core.Consumer) error {
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
return err
|
||||
}
|
||||
// not best solution
|
||||
if i, ok := cons.(interface{ OnClose(func()) }); ok {
|
||||
i.OnClose(func() {
|
||||
stream.RemoveConsumer(cons)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
},
|
||||
SndHandler: func(prod core.Producer) error {
|
||||
return stream.Play(prod)
|
||||
},
|
||||
Trace: func(format string, v ...any) {
|
||||
log.Trace().Msgf("[wyoming] "+format, v...)
|
||||
},
|
||||
Error: func(format string, v ...any) {
|
||||
log.Error().Msgf("[wyoming] "+format, v...)
|
||||
},
|
||||
}
|
||||
go serve(srv, cfg.Mode, cfg.Listen)
|
||||
}
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func serve(srv *wyoming.Server, mode, address string) {
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("[wyoming] listen")
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go handle(srv, mode, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func handle(srv *wyoming.Server, mode string, conn net.Conn) {
|
||||
addr := conn.RemoteAddr()
|
||||
|
||||
log.Trace().Msgf("[wyoming] %s connected", addr)
|
||||
|
||||
switch mode {
|
||||
case "mic":
|
||||
srv.HandleMic(conn)
|
||||
case "snd":
|
||||
srv.HandleSnd(conn)
|
||||
default:
|
||||
srv.Handle(conn)
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[wyoming] %s disconnected", addr)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Xiaomi
|
||||
|
||||
This source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem.
|
||||
|
||||
**Important:**
|
||||
|
||||
1. **Not all cameras are supported**. There are several P2P protocol vendors in the Xiaomi ecosystem.
|
||||
Currently, the **CS2** vendor is supported. However, the **TUTK** vendor is not supported.
|
||||
2. Each time you connect to the camera, you need internet access to obtain encryption keys.
|
||||
3. Connection to the camera is local only.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Multiple Xiaomi accounts supported
|
||||
- Cameras from multiple regions are supported for a single account
|
||||
- Two-way audio is supported
|
||||
- Cameras with multiple lenses are supported
|
||||
|
||||
## Setup
|
||||
|
||||
1. Goto go2rtc WebUI > Add > Xiaomi > Login with username and password
|
||||
2. Receive verification code by email or phone if required.
|
||||
3. Complete the captcha if required.
|
||||
4. If everything is OK, your account will be added and you can load cameras from it.
|
||||
|
||||
**Example**
|
||||
|
||||
```yaml
|
||||
xiaomi:
|
||||
1234567890: V1:***
|
||||
|
||||
streams:
|
||||
xiaomi1: xiaomi://1234567890:cn@192.168.1.123?did=9876543210&model=isa.camera.hlc7
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
You can change camera's quality: `subtype=hd/sd/auto`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
xiaomi1: xiaomi://***&subtype=sd
|
||||
```
|
||||
|
||||
You can use second channel for Dual cameras: `channel=1`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
xiaomi1: xiaomi://***&channel=1
|
||||
```
|
||||
@@ -0,0 +1,267 @@
|
||||
package xiaomi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xiaomi"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var v struct {
|
||||
Cfg map[string]string `yaml:"xiaomi"`
|
||||
}
|
||||
app.LoadConfig(&v)
|
||||
|
||||
tokens = v.Cfg
|
||||
|
||||
log := app.GetLogger("xiaomi")
|
||||
|
||||
streams.HandleFunc("xiaomi", func(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
rawURL, err = getCameraURL(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msgf("xiaomi: dial %s", rawURL)
|
||||
|
||||
return xiaomi.Dial(rawURL)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/xiaomi", apiXiaomi)
|
||||
}
|
||||
|
||||
var tokens map[string]string
|
||||
var tokensMu sync.Mutex
|
||||
|
||||
func getCloud(userID string) (*xiaomi.Cloud, error) {
|
||||
tokensMu.Lock()
|
||||
defer tokensMu.Unlock()
|
||||
|
||||
token := tokens[userID]
|
||||
cloud := xiaomi.NewCloud(AppXiaomiHome)
|
||||
if err := cloud.LoginWithToken(userID, token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cloud, nil
|
||||
}
|
||||
|
||||
func getCameraURL(url *url.URL) (string, error) {
|
||||
clientPublic, clientPrivate, err := miss.GenerateKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
query := url.Query()
|
||||
|
||||
params := fmt.Sprintf(
|
||||
`{"app_pubkey":"%x","did":"%s","support_vendors":"CS2"}`,
|
||||
clientPublic, query.Get("did"),
|
||||
)
|
||||
|
||||
cloud, err := getCloud(url.User.Username())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
region, _ := url.User.Password()
|
||||
|
||||
res, err := cloud.Request(GetBaseURL(region), "/v2/device/miss_get_vendor", params, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Vendor struct {
|
||||
VendorID byte `json:"vendor"`
|
||||
} `json:"vendor"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
if err = json.Unmarshal(res, &v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
query.Set("client_public", hex.EncodeToString(clientPublic))
|
||||
query.Set("client_private", hex.EncodeToString(clientPrivate))
|
||||
query.Set("device_public", v.PublicKey)
|
||||
query.Set("sign", v.Sign)
|
||||
query.Set("vendor", getVendorName(v.Vendor.VendorID))
|
||||
|
||||
url.RawQuery = query.Encode()
|
||||
return url.String(), nil
|
||||
}
|
||||
|
||||
func getVendorName(i byte) string {
|
||||
switch i {
|
||||
case 1:
|
||||
return "tutk"
|
||||
case 3:
|
||||
return "agora"
|
||||
case 4:
|
||||
return "cs2"
|
||||
case 6:
|
||||
return "mtp"
|
||||
}
|
||||
return fmt.Sprintf("%d", i)
|
||||
}
|
||||
|
||||
func apiXiaomi(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
apiDeviceList(w, r)
|
||||
case "POST":
|
||||
apiAuth(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func apiDeviceList(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
user := query.Get("id")
|
||||
if user == "" {
|
||||
tokensMu.Lock()
|
||||
users := make([]string, 0, len(tokens))
|
||||
for s := range tokens {
|
||||
users = append(users, s)
|
||||
}
|
||||
tokensMu.Unlock()
|
||||
|
||||
api.ResponseJSON(w, users)
|
||||
return
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
cloud, err := getCloud(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
region := query.Get("region")
|
||||
|
||||
res, err := cloud.Request(GetBaseURL(region), "/v2/home/device_list_page", "{}", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v struct {
|
||||
List []*Device `json:"list"`
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(res, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var items []*api.Source
|
||||
|
||||
for _, device := range v.List {
|
||||
if !strings.Contains(device.Model, ".camera.") {
|
||||
continue
|
||||
}
|
||||
items = append(items, &api.Source{
|
||||
Name: device.Name,
|
||||
Info: fmt.Sprintf("ip: %s, mac: %s", device.IP, device.MAC),
|
||||
URL: fmt.Sprintf("xiaomi://%s:%s@%s?did=%s&model=%s", user, region, device.IP, device.Did, device.Model),
|
||||
})
|
||||
}
|
||||
|
||||
api.ResponseSources(w, items)
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Did string `json:"did"`
|
||||
Name string `json:"name"`
|
||||
Model string `json:"model"`
|
||||
MAC string `json:"mac"`
|
||||
IP string `json:"localip"`
|
||||
}
|
||||
|
||||
var auth *xiaomi.Cloud
|
||||
|
||||
func apiAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
username := r.Form.Get("username")
|
||||
password := r.Form.Get("password")
|
||||
captcha := r.Form.Get("captcha")
|
||||
verify := r.Form.Get("verify")
|
||||
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case username != "" || password != "":
|
||||
auth = xiaomi.NewCloud(AppXiaomiHome)
|
||||
err = auth.Login(username, password)
|
||||
case captcha != "":
|
||||
err = auth.LoginWithCaptcha(captcha)
|
||||
case verify != "":
|
||||
err = auth.LoginWithVerify(verify)
|
||||
default:
|
||||
http.Error(w, "wrong request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
userID, token := auth.UserToken()
|
||||
auth = nil
|
||||
|
||||
tokensMu.Lock()
|
||||
if tokens == nil {
|
||||
tokens = map[string]string{userID: token}
|
||||
} else {
|
||||
tokens[userID] = token
|
||||
}
|
||||
tokensMu.Unlock()
|
||||
|
||||
err = app.PatchConfig([]string{"xiaomi", userID}, token)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
var login *xiaomi.LoginError
|
||||
if errors.As(err, &login) {
|
||||
w.Header().Set("Content-Type", api.MimeJSON)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(err)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
const AppXiaomiHome = "xiaomiio"
|
||||
|
||||
func GetBaseURL(region string) string {
|
||||
switch region {
|
||||
case "de", "i2", "ru", "sg", "us":
|
||||
return "https://" + region + ".api.io.mi.com/app"
|
||||
}
|
||||
return "https://api.io.mi.com/app"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Yandex
|
||||
|
||||
Source for receiving stream from new [Yandex IP camera](https://alice.yandex.ru/smart-home/security/ipcamera).
|
||||
|
||||
## Get Yandex token
|
||||
|
||||
1. Install HomeAssistant integration [YandexStation](https://github.com/AlexxIT/YandexStation).
|
||||
2. Copy token from HomeAssistant config folder: `/config/.storage/core.config_entries`, key: `"x_token"`.
|
||||
|
||||
## Get device ID
|
||||
|
||||
1. Open this link in any browser: https://iot.quasar.yandex.ru/m/v3/user/devices
|
||||
2. Copy ID of your camera, key: `"id"`.
|
||||
|
||||
## Config examples
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
yandex_stream: yandex:?x_token=XXXX&device_id=XXXX
|
||||
yandex_snapshot: yandex:?x_token=XXXX&device_id=XXXX&snapshot
|
||||
yandex_snapshot_custom_size: yandex:?x_token=XXXX&device_id=XXXX&snapshot=h=540
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
xwebrtc "github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
pion "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func goloomClient(serviceURL, serviceName, roomId, participantId, credentials string) (core.Producer, error) {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(serviceURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
time.Sleep(time.Second)
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
s := fmt.Sprintf(`{"hello": {
|
||||
"credentials":"%s","participantId":"%s","roomId":"%s","serviceName":"%s","sdkInitializationId":"%s",
|
||||
"capabilitiesOffer":{},"sendAudio":false,"sendSharing":false,"sendVideo":false,
|
||||
"sdkInfo":{"hwConcurrency":4,"implementation":"browser","version":"5.4.0"},
|
||||
"participantAttributes":{"description":"","name":"mike","role":"SPEAKER"},
|
||||
"participantMeta":{"description":"","name":"mike","role":"SPEAKER","sendAudio":false,"sendVideo":false}
|
||||
},"uid":"%s"}`,
|
||||
credentials, participantId, roomId, serviceName,
|
||||
uuid.NewString(), uuid.NewString(),
|
||||
)
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage, []byte(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, _, err = conn.ReadMessage(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc, err := webrtc.PeerConnection(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod := xwebrtc.NewConn(pc)
|
||||
prod.FormatName = "yandex"
|
||||
prod.Mode = core.ModeActiveProducer
|
||||
prod.Protocol = "wss"
|
||||
|
||||
var connState core.Waiter
|
||||
|
||||
prod.Listen(func(msg any) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
switch msg {
|
||||
case pion.PeerConnectionStateConnecting:
|
||||
case pion.PeerConnectionStateConnected:
|
||||
connState.Done(nil)
|
||||
default:
|
||||
connState.Done(errors.New("webrtc: " + msg.String()))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
var msg map[string]json.RawMessage
|
||||
if err = conn.ReadJSON(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range msg {
|
||||
switch k {
|
||||
case "uid":
|
||||
continue
|
||||
case "serverHello":
|
||||
case "subscriberSdpOffer":
|
||||
var sdp subscriberSdp
|
||||
if err = json.Unmarshal(v, &sdp); err != nil {
|
||||
return
|
||||
}
|
||||
//log.Trace().Msgf("offer:\n%s", sdp.Sdp)
|
||||
if err = prod.SetOffer(sdp.Sdp); err != nil {
|
||||
return
|
||||
}
|
||||
if sdp.Sdp, err = prod.GetAnswer(); err != nil {
|
||||
return
|
||||
}
|
||||
//log.Trace().Msgf("answer:\n%s", sdp.Sdp)
|
||||
|
||||
var raw []byte
|
||||
if raw, err = json.Marshal(sdp); err != nil {
|
||||
return
|
||||
}
|
||||
s = fmt.Sprintf(`{"uid":"%s","subscriberSdpAnswer":%s}`, uuid.NewString(), raw)
|
||||
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return
|
||||
}
|
||||
case "webrtcIceCandidate":
|
||||
var candidate webrtcIceCandidate
|
||||
if err = json.Unmarshal(v, &candidate); err != nil {
|
||||
return
|
||||
}
|
||||
if err = prod.AddCandidate(candidate.Candidate); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
//log.Trace().Msgf("%s : %s", k, v)
|
||||
}
|
||||
|
||||
if msg["ack"] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s = fmt.Sprintf(`{"uid":%s,"ack":{"status":{"code":"OK"}}}`, msg["uid"])
|
||||
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = connState.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s = fmt.Sprintf(`{"uid":"%s","setSlots":{"slots":[{"width":0,"height":0}],"audioSlotsCount":0,"key":1,"shutdownAllVideo":false,"withSelfView":false,"selfViewVisibility":"ON_LOADING_THEN_HIDE","gridConfig":{}}}`, uuid.NewString())
|
||||
if err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
type subscriberSdp struct {
|
||||
PcSeq int `json:"pcSeq"`
|
||||
Sdp string `json:"sdp"`
|
||||
}
|
||||
|
||||
type webrtcIceCandidate struct {
|
||||
PcSeq int `json:"pcSeq"`
|
||||
Target string `json:"target"`
|
||||
Candidate string `json:"candidate"`
|
||||
SdpMid string `json:"sdpMid"`
|
||||
SdpMlineIndex int `json:"sdpMlineIndex"`
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package yandex
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yandex"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("yandex", func(source string) (core.Producer, error) {
|
||||
u, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
token := query.Get("x_token")
|
||||
|
||||
session, err := yandex.GetSession(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceID := query.Get("device_id")
|
||||
|
||||
if query.Has("snapshot") {
|
||||
rawURL, err := session.GetSnapshotURL(deviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawURL += "/current.jpg?" + query.Get("snapshot") + "#header=Cookie:" + session.GetCookieString(rawURL)
|
||||
return streams.GetProducer(rawURL)
|
||||
}
|
||||
|
||||
room, err := session.WebrtcCreateRoom(deviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return goloomClient(room.ServiceUrl, room.ServiceName, room.RoomId, room.ParticipantId, room.Credentials)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/alsa"
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
@@ -9,9 +12,11 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/doorbird"
|
||||
"github.com/AlexxIT/go2rtc/internal/dvrip"
|
||||
"github.com/AlexxIT/go2rtc/internal/echo"
|
||||
"github.com/AlexxIT/go2rtc/internal/eseecloud"
|
||||
"github.com/AlexxIT/go2rtc/internal/exec"
|
||||
"github.com/AlexxIT/go2rtc/internal/expr"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/flussonic"
|
||||
"github.com/AlexxIT/go2rtc/internal/gopro"
|
||||
"github.com/AlexxIT/go2rtc/internal/hass"
|
||||
"github.com/AlexxIT/go2rtc/internal/hls"
|
||||
@@ -25,6 +30,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/nest"
|
||||
"github.com/AlexxIT/go2rtc/internal/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/internal/onvif"
|
||||
"github.com/AlexxIT/go2rtc/internal/pinggy"
|
||||
"github.com/AlexxIT/go2rtc/internal/ring"
|
||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||
"github.com/AlexxIT/go2rtc/internal/rtmp"
|
||||
@@ -32,70 +38,81 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/tapo"
|
||||
"github.com/AlexxIT/go2rtc/internal/tuya"
|
||||
"github.com/AlexxIT/go2rtc/internal/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/internal/webtorrent"
|
||||
"github.com/AlexxIT/go2rtc/internal/wyoming"
|
||||
"github.com/AlexxIT/go2rtc/internal/xiaomi"
|
||||
"github.com/AlexxIT/go2rtc/internal/yandex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Version = "1.9.9"
|
||||
app.Version = "1.9.13"
|
||||
|
||||
// 1. Core modules: app, api/ws, streams
|
||||
type module struct {
|
||||
name string
|
||||
init func()
|
||||
}
|
||||
|
||||
app.Init() // init config and logs
|
||||
modules := []module{
|
||||
{"", app.Init}, // init config and logs
|
||||
{"api", api.Init}, // init API before all others
|
||||
{"ws", ws.Init}, // init WS API endpoint
|
||||
{"", streams.Init},
|
||||
// Main sources and servers
|
||||
{"http", http.Init}, // rtsp source, HTTP server
|
||||
{"rtsp", rtsp.Init}, // rtsp source, RTSP server
|
||||
{"webrtc", webrtc.Init}, // webrtc source, WebRTC server
|
||||
// Main API
|
||||
{"mp4", mp4.Init}, // MP4 API
|
||||
{"hls", hls.Init}, // HLS API
|
||||
{"mjpeg", mjpeg.Init}, // MJPEG API
|
||||
// Other sources and servers
|
||||
{"hass", hass.Init}, // hass source, Hass API server
|
||||
{"homekit", homekit.Init}, // homekit source, HomeKit server
|
||||
{"onvif", onvif.Init}, // onvif source, ONVIF API server
|
||||
{"rtmp", rtmp.Init}, // rtmp source, RTMP server
|
||||
{"webtorrent", webtorrent.Init}, // webtorrent source, WebTorrent module
|
||||
{"wyoming", wyoming.Init},
|
||||
// Exec and script sources
|
||||
{"echo", echo.Init},
|
||||
{"exec", exec.Init},
|
||||
{"expr", expr.Init},
|
||||
{"ffmpeg", ffmpeg.Init},
|
||||
// Hardware sources
|
||||
{"alsa", alsa.Init},
|
||||
{"v4l2", v4l2.Init},
|
||||
// Other sources
|
||||
{"bubble", bubble.Init},
|
||||
{"doorbird", doorbird.Init},
|
||||
{"dvrip", dvrip.Init},
|
||||
{"eseecloud", eseecloud.Init},
|
||||
{"flussonic", flussonic.Init},
|
||||
{"gopro", gopro.Init},
|
||||
{"isapi", isapi.Init},
|
||||
{"ivideon", ivideon.Init},
|
||||
{"mpegts", mpegts.Init},
|
||||
{"nest", nest.Init},
|
||||
{"ring", ring.Init},
|
||||
{"roborock", roborock.Init},
|
||||
{"tapo", tapo.Init},
|
||||
{"tuya", tuya.Init},
|
||||
{"xiaomi", xiaomi.Init},
|
||||
{"yandex", yandex.Init},
|
||||
// Helper modules
|
||||
{"debug", debug.Init},
|
||||
{"ngrok", ngrok.Init},
|
||||
{"pinggy", pinggy.Init},
|
||||
{"srtp", srtp.Init},
|
||||
}
|
||||
|
||||
api.Init() // init API before all others
|
||||
ws.Init() // init WS API endpoint
|
||||
|
||||
streams.Init() // streams module
|
||||
|
||||
// 2. Main sources and servers
|
||||
|
||||
rtsp.Init() // rtsp source, RTSP server
|
||||
webrtc.Init() // webrtc source, WebRTC server
|
||||
|
||||
// 3. Main API
|
||||
|
||||
mp4.Init() // MP4 API
|
||||
hls.Init() // HLS API
|
||||
mjpeg.Init() // MJPEG API
|
||||
|
||||
// 4. Other sources and servers
|
||||
|
||||
hass.Init() // hass source, Hass API server
|
||||
onvif.Init() // onvif source, ONVIF API server
|
||||
webtorrent.Init() // webtorrent source, WebTorrent module
|
||||
|
||||
// 5. Other sources
|
||||
|
||||
rtmp.Init() // rtmp source
|
||||
exec.Init() // exec source
|
||||
ffmpeg.Init() // ffmpeg source
|
||||
echo.Init() // echo source
|
||||
ivideon.Init() // ivideon source
|
||||
http.Init() // http/tcp source
|
||||
dvrip.Init() // dvrip source
|
||||
tapo.Init() // tapo source
|
||||
isapi.Init() // isapi source
|
||||
mpegts.Init() // mpegts passive source
|
||||
roborock.Init() // roborock source
|
||||
homekit.Init() // homekit source
|
||||
ring.Init() // ring source
|
||||
nest.Init() // nest source
|
||||
bubble.Init() // bubble source
|
||||
expr.Init() // expr source
|
||||
gopro.Init() // gopro source
|
||||
doorbird.Init() // doorbird source
|
||||
v4l2.Init() // v4l2 source
|
||||
|
||||
// 6. Helper modules
|
||||
|
||||
ngrok.Init() // ngrok module
|
||||
srtp.Init() // SRTP server
|
||||
debug.Init() // debug API
|
||||
|
||||
// 7. Go
|
||||
for _, m := range modules {
|
||||
if app.Modules == nil || m.name == "" || slices.Contains(app.Modules, m.name) {
|
||||
m.init()
|
||||
}
|
||||
}
|
||||
|
||||
shell.RunUntilSignal()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ Some formats and protocols go2rtc supports exclusively. They have no equivalent
|
||||
| Format | Source protocols | Ingress protocols | Recevers codecs | Senders codecs | Example |
|
||||
|--------------|------------------|-------------------|------------------------------|--------------------|---------------|
|
||||
| adts | http,tcp,pipe | http | aac | | `http:` |
|
||||
| alsa | pipe | | | pcm | `alsa:` |
|
||||
| bubble | http | | h264,hevc,pcm_alaw | | `bubble:` |
|
||||
| dvrip | tcp | | h264,hevc,pcm_alaw,pcm_mulaw | pcm_alaw | `dvrip:` |
|
||||
| flv | http,tcp,pipe | http | h264,aac | | `http:` |
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ func ConfigToCodec(conf []byte) *core.Codec {
|
||||
codec.ClockRate = rd.ReadBits(24)
|
||||
}
|
||||
|
||||
codec.Channels = rd.ReadBits16(4)
|
||||
codec.Channels = rd.ReadBits8(4)
|
||||
|
||||
return codec
|
||||
}
|
||||
|
||||
+15
-4
@@ -8,8 +8,19 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
const ADTSHeaderSize = 7
|
||||
|
||||
func IsADTS(b []byte) bool {
|
||||
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF6 == 0xF0
|
||||
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
|
||||
// A 12 Syncword, all bits must be set to 1.
|
||||
// C 2 Layer, always set to 0.
|
||||
return len(b) >= ADTSHeaderSize && b[0] == 0xFF && b[1]&0b1111_0110 == 0xF0
|
||||
}
|
||||
|
||||
func HasCRC(b []byte) bool {
|
||||
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
|
||||
// D 1 Protection absence, set to 1 if there is no CRC and 0 if there is CRC.
|
||||
return b[1]&0b1 == 0
|
||||
}
|
||||
|
||||
func ADTSToCodec(b []byte) *core.Codec {
|
||||
@@ -28,7 +39,7 @@ func ADTSToCodec(b []byte) *core.Codec {
|
||||
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
|
||||
channels := rd.ReadBits8(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
|
||||
@@ -43,7 +54,7 @@ func ADTSToCodec(b []byte) *core.Codec {
|
||||
wr := bits.NewWriter(nil)
|
||||
wr.WriteBits8(objType, 5)
|
||||
wr.WriteBits8(sampleRateIdx, 4)
|
||||
wr.WriteBits16(channels, 4)
|
||||
wr.WriteBits8(channels, 4)
|
||||
conf := wr.Bytes()
|
||||
|
||||
codec := &core.Codec{
|
||||
@@ -58,7 +69,7 @@ func ADTSToCodec(b []byte) *core.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)
|
||||
return uint16(b[3]&0b11)<<11 | uint16(b[4])<<3 | uint16(b[5]>>5)
|
||||
}
|
||||
|
||||
func WriteADTSSize(b []byte, size uint16) {
|
||||
|
||||
+25
-11
@@ -2,7 +2,7 @@ package aac
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
@@ -17,16 +17,22 @@ type Producer struct {
|
||||
func Open(r io.Reader) (*Producer, error) {
|
||||
rd := bufio.NewReader(r)
|
||||
|
||||
b, err := rd.Peek(8)
|
||||
b, err := rd.Peek(ADTSHeaderSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codec := ADTSToCodec(b)
|
||||
if codec == nil {
|
||||
return nil, errors.New("adts: wrong header")
|
||||
}
|
||||
codec.PayloadType = core.PayloadTypeRAW
|
||||
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{ADTSToCodec(b)},
|
||||
Codecs: []*core.Codec{codec},
|
||||
},
|
||||
}
|
||||
return &Producer{
|
||||
@@ -42,14 +48,25 @@ func Open(r io.Reader) (*Producer, error) {
|
||||
|
||||
func (c *Producer) Start() error {
|
||||
for {
|
||||
b, err := c.rd.Peek(6)
|
||||
if err != nil {
|
||||
// read ADTS header
|
||||
adts := make([]byte, ADTSHeaderSize)
|
||||
if _, err := io.ReadFull(c.rd, adts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auSize := ReadADTSSize(b)
|
||||
payload := make([]byte, 2+2+auSize)
|
||||
if _, err = io.ReadFull(c.rd, payload[4:]); err != nil {
|
||||
auSize := ReadADTSSize(adts) - ADTSHeaderSize
|
||||
|
||||
if HasCRC(adts) {
|
||||
// skip CRC after header
|
||||
if _, err := c.rd.Discard(2); err != nil {
|
||||
return err
|
||||
}
|
||||
auSize -= 2
|
||||
}
|
||||
|
||||
// read AAC payload after header
|
||||
payload := make([]byte, auSize)
|
||||
if _, err := io.ReadFull(c.rd, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -59,9 +76,6 @@ func (c *Producer) Start() error {
|
||||
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,
|
||||
|
||||
+7
-4
@@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
const RTPPacketVersionAAC = 0
|
||||
const ADTSHeaderSize = 7
|
||||
|
||||
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
var timestamp uint32
|
||||
@@ -65,7 +64,8 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
}
|
||||
|
||||
func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
var seq uint16
|
||||
var ts uint32
|
||||
|
||||
return func(packet *rtp.Packet) {
|
||||
if packet.Version != RTPPacketVersionAAC {
|
||||
@@ -85,12 +85,15 @@ func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
SequenceNumber: seq,
|
||||
Timestamp: ts,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
handler(&clone)
|
||||
|
||||
seq++
|
||||
ts += AUTime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
## Build
|
||||
|
||||
```shell
|
||||
x86_64-linux-gnu-gcc -w -static asound_arch.c -o asound_amd64
|
||||
i686-linux-gnu-gcc -w -static asound_arch.c -o asound_i386
|
||||
aarch64-linux-gnu-gcc -w -static asound_arch.c -o asound_arm64
|
||||
arm-linux-gnueabihf-gcc -w -static asound_arch.c -o asound_arm
|
||||
mipsel-linux-gnu-gcc -w -static asound_arch.c -o asound_mipsle -D_TIME_BITS=32
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h
|
||||
- https://github.com/yobert/alsa
|
||||
- https://github.com/Narsil/alsa-go
|
||||
- https://github.com/alsa-project/alsa-lib
|
||||
- https://github.com/anisse/alsa
|
||||
- https://github.com/tinyalsa/tinyalsa
|
||||
|
||||
**Broken pipe**
|
||||
|
||||
- https://stackoverflow.com/questions/26545139/alsa-cannot-recovery-from-underrun-prepare-failed-broken-pipe
|
||||
- https://klipspringer.avadeaux.net/alsa-broken-pipe-errors/
|
||||
@@ -0,0 +1,90 @@
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Capture struct {
|
||||
core.Connection
|
||||
dev *device.Device
|
||||
closed core.Waiter
|
||||
}
|
||||
|
||||
func newCapture(dev *device.Device) (*Capture, error) {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionRecvonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCML, ClockRate: 16000},
|
||||
},
|
||||
},
|
||||
}
|
||||
return &Capture{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "alsa",
|
||||
Medias: medias,
|
||||
Transport: dev,
|
||||
},
|
||||
dev: dev,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Capture) Start() error {
|
||||
dst := c.Medias[0].Codecs[0]
|
||||
src := &core.Codec{
|
||||
Name: dst.Name,
|
||||
ClockRate: c.dev.GetRateNear(dst.ClockRate),
|
||||
Channels: c.dev.GetChannelsNear(dst.Channels),
|
||||
}
|
||||
|
||||
if err := c.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, src.ClockRate, src.Channels); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcode := transcodeFunc(dst, src)
|
||||
frameBytes := int(pcm.BytesPerFrame(src))
|
||||
|
||||
var ts uint32
|
||||
|
||||
// readBufferSize for 20ms interval
|
||||
readBufferSize := 20 * frameBytes * int(src.ClockRate) / 1000
|
||||
b := make([]byte, readBufferSize)
|
||||
for {
|
||||
n, err := c.dev.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Recv += n
|
||||
|
||||
if len(c.Receivers) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pkt := &rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
Timestamp: ts,
|
||||
},
|
||||
Payload: transcode(b[:n]),
|
||||
}
|
||||
c.Receivers[0].WriteRTP(pkt)
|
||||
|
||||
ts += uint32(n / frameBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func transcodeFunc(dst, src *core.Codec) func([]byte) []byte {
|
||||
if dst.ClockRate == src.ClockRate && dst.Channels == src.Channels {
|
||||
return func(b []byte) []byte {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return pcm.Transcode(dst, src)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//go:build 386 || arm
|
||||
|
||||
package device
|
||||
|
||||
type unsigned_char = byte
|
||||
type signed_int = int32
|
||||
type unsigned_int = uint32
|
||||
type signed_long = int64
|
||||
type unsigned_long = uint64
|
||||
type __u32 = uint32
|
||||
type void__user = uintptr
|
||||
|
||||
const (
|
||||
SNDRV_PCM_STREAM_PLAYBACK = 0
|
||||
SNDRV_PCM_STREAM_CAPTURE = 1
|
||||
|
||||
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
|
||||
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
|
||||
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
|
||||
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
|
||||
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
|
||||
|
||||
SNDRV_PCM_FORMAT_S8 = 0
|
||||
SNDRV_PCM_FORMAT_U8 = 1
|
||||
SNDRV_PCM_FORMAT_S16_LE = 2
|
||||
SNDRV_PCM_FORMAT_S16_BE = 3
|
||||
SNDRV_PCM_FORMAT_U16_LE = 4
|
||||
SNDRV_PCM_FORMAT_U16_BE = 5
|
||||
SNDRV_PCM_FORMAT_S24_LE = 6
|
||||
SNDRV_PCM_FORMAT_S24_BE = 7
|
||||
SNDRV_PCM_FORMAT_U24_LE = 8
|
||||
SNDRV_PCM_FORMAT_U24_BE = 9
|
||||
SNDRV_PCM_FORMAT_S32_LE = 10
|
||||
SNDRV_PCM_FORMAT_S32_BE = 11
|
||||
SNDRV_PCM_FORMAT_U32_LE = 12
|
||||
SNDRV_PCM_FORMAT_U32_BE = 13
|
||||
SNDRV_PCM_FORMAT_FLOAT_LE = 14
|
||||
SNDRV_PCM_FORMAT_FLOAT_BE = 15
|
||||
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
|
||||
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
|
||||
SNDRV_PCM_FORMAT_MU_LAW = 20
|
||||
SNDRV_PCM_FORMAT_A_LAW = 21
|
||||
SNDRV_PCM_FORMAT_MPEG = 23
|
||||
|
||||
SNDRV_PCM_IOCTL_PVERSION = 0x80044100
|
||||
SNDRV_PCM_IOCTL_INFO = 0x81204101
|
||||
SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110
|
||||
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111
|
||||
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113
|
||||
SNDRV_PCM_IOCTL_PREPARE = 0x00004140
|
||||
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x400c4150
|
||||
SNDRV_PCM_IOCTL_READI_FRAMES = 0x800c4151
|
||||
)
|
||||
|
||||
type snd_pcm_info struct { // size 288
|
||||
device unsigned_int // offset 0, size 4
|
||||
subdevice unsigned_int // offset 4, size 4
|
||||
stream signed_int // offset 8, size 4
|
||||
card signed_int // offset 12, size 4
|
||||
id [64]unsigned_char // offset 16, size 64
|
||||
name [80]unsigned_char // offset 80, size 80
|
||||
subname [32]unsigned_char // offset 160, size 32
|
||||
dev_class signed_int // offset 192, size 4
|
||||
dev_subclass signed_int // offset 196, size 4
|
||||
subdevices_count unsigned_int // offset 200, size 4
|
||||
subdevices_avail unsigned_int // offset 204, size 4
|
||||
pad1 [16]unsigned_char
|
||||
reserved [64]unsigned_char // offset 224, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_uframes_t = unsigned_long
|
||||
type snd_pcm_sframes_t = signed_long
|
||||
|
||||
type snd_xferi struct { // size 12
|
||||
result snd_pcm_sframes_t // offset 0, size 4
|
||||
buf void__user // offset 4, size 4
|
||||
frames snd_pcm_uframes_t // offset 8, size 4
|
||||
}
|
||||
|
||||
const (
|
||||
SNDRV_PCM_HW_PARAM_ACCESS = 0
|
||||
SNDRV_PCM_HW_PARAM_FORMAT = 1
|
||||
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
|
||||
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
|
||||
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
|
||||
|
||||
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
|
||||
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
|
||||
SNDRV_PCM_HW_PARAM_CHANNELS = 10
|
||||
SNDRV_PCM_HW_PARAM_RATE = 11
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
|
||||
SNDRV_PCM_HW_PARAM_PERIODS = 15
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
|
||||
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
|
||||
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
|
||||
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
|
||||
|
||||
SNDRV_MASK_MAX = 256
|
||||
|
||||
SNDRV_PCM_TSTAMP_NONE = 0
|
||||
SNDRV_PCM_TSTAMP_ENABLE = 1
|
||||
)
|
||||
|
||||
type snd_mask struct { // size 32
|
||||
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
|
||||
}
|
||||
|
||||
type snd_interval struct { // size 12
|
||||
min unsigned_int // offset 0, size 4
|
||||
max unsigned_int // offset 4, size 4
|
||||
bit unsigned_int
|
||||
}
|
||||
|
||||
type snd_pcm_hw_params struct { // size 604
|
||||
flags unsigned_int // offset 0, size 4
|
||||
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
|
||||
mres [5]snd_mask // offset 100, size 160
|
||||
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
|
||||
ires [9]snd_interval // offset 404, size 108
|
||||
rmask unsigned_int // offset 512, size 4
|
||||
cmask unsigned_int // offset 516, size 4
|
||||
info unsigned_int // offset 520, size 4
|
||||
msbits unsigned_int // offset 524, size 4
|
||||
rate_num unsigned_int // offset 528, size 4
|
||||
rate_den unsigned_int // offset 532, size 4
|
||||
fifo_size snd_pcm_uframes_t // offset 536, size 4
|
||||
reserved [64]unsigned_char // offset 540, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_sw_params struct { // size 104
|
||||
tstamp_mode signed_int // offset 0, size 4
|
||||
period_step unsigned_int // offset 4, size 4
|
||||
sleep_min unsigned_int // offset 8, size 4
|
||||
avail_min snd_pcm_uframes_t // offset 12, size 4
|
||||
xfer_align snd_pcm_uframes_t // offset 16, size 4
|
||||
start_threshold snd_pcm_uframes_t // offset 20, size 4
|
||||
stop_threshold snd_pcm_uframes_t // offset 24, size 4
|
||||
silence_threshold snd_pcm_uframes_t // offset 28, size 4
|
||||
silence_size snd_pcm_uframes_t // offset 32, size 4
|
||||
boundary snd_pcm_uframes_t // offset 36, size 4
|
||||
proto unsigned_int // offset 40, size 4
|
||||
tstamp_type unsigned_int // offset 44, size 4
|
||||
reserved [56]unsigned_char // offset 48, size 56
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package device
|
||||
|
||||
type unsigned_char = byte
|
||||
type signed_int = int32
|
||||
type unsigned_int = uint32
|
||||
type signed_long = int64
|
||||
type unsigned_long = uint64
|
||||
type __u32 = uint32
|
||||
type void__user = uintptr
|
||||
|
||||
const (
|
||||
SNDRV_PCM_STREAM_PLAYBACK = 0
|
||||
SNDRV_PCM_STREAM_CAPTURE = 1
|
||||
|
||||
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
|
||||
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
|
||||
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
|
||||
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
|
||||
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
|
||||
|
||||
SNDRV_PCM_FORMAT_S8 = 0
|
||||
SNDRV_PCM_FORMAT_U8 = 1
|
||||
SNDRV_PCM_FORMAT_S16_LE = 2
|
||||
SNDRV_PCM_FORMAT_S16_BE = 3
|
||||
SNDRV_PCM_FORMAT_U16_LE = 4
|
||||
SNDRV_PCM_FORMAT_U16_BE = 5
|
||||
SNDRV_PCM_FORMAT_S24_LE = 6
|
||||
SNDRV_PCM_FORMAT_S24_BE = 7
|
||||
SNDRV_PCM_FORMAT_U24_LE = 8
|
||||
SNDRV_PCM_FORMAT_U24_BE = 9
|
||||
SNDRV_PCM_FORMAT_S32_LE = 10
|
||||
SNDRV_PCM_FORMAT_S32_BE = 11
|
||||
SNDRV_PCM_FORMAT_U32_LE = 12
|
||||
SNDRV_PCM_FORMAT_U32_BE = 13
|
||||
SNDRV_PCM_FORMAT_FLOAT_LE = 14
|
||||
SNDRV_PCM_FORMAT_FLOAT_BE = 15
|
||||
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
|
||||
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
|
||||
SNDRV_PCM_FORMAT_MU_LAW = 20
|
||||
SNDRV_PCM_FORMAT_A_LAW = 21
|
||||
SNDRV_PCM_FORMAT_MPEG = 23
|
||||
|
||||
SNDRV_PCM_IOCTL_PVERSION = 0x80044100
|
||||
SNDRV_PCM_IOCTL_INFO = 0x81204101
|
||||
SNDRV_PCM_IOCTL_HW_REFINE = 0xc2604110
|
||||
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc2604111
|
||||
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0884113
|
||||
SNDRV_PCM_IOCTL_PREPARE = 0x00004140
|
||||
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x40184150
|
||||
SNDRV_PCM_IOCTL_READI_FRAMES = 0x80184151
|
||||
)
|
||||
|
||||
type snd_pcm_info struct { // size 288
|
||||
device unsigned_int // offset 0, size 4
|
||||
subdevice unsigned_int // offset 4, size 4
|
||||
stream signed_int // offset 8, size 4
|
||||
card signed_int // offset 12, size 4
|
||||
id [64]unsigned_char // offset 16, size 64
|
||||
name [80]unsigned_char // offset 80, size 80
|
||||
subname [32]unsigned_char // offset 160, size 32
|
||||
dev_class signed_int // offset 192, size 4
|
||||
dev_subclass signed_int // offset 196, size 4
|
||||
subdevices_count unsigned_int // offset 200, size 4
|
||||
subdevices_avail unsigned_int // offset 204, size 4
|
||||
pad1 [16]unsigned_char
|
||||
reserved [64]unsigned_char // offset 224, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_uframes_t = unsigned_long
|
||||
type snd_pcm_sframes_t = signed_long
|
||||
|
||||
type snd_xferi struct { // size 24
|
||||
result snd_pcm_sframes_t // offset 0, size 8
|
||||
buf void__user // offset 8, size 8
|
||||
frames snd_pcm_uframes_t // offset 16, size 8
|
||||
}
|
||||
|
||||
const (
|
||||
SNDRV_PCM_HW_PARAM_ACCESS = 0
|
||||
SNDRV_PCM_HW_PARAM_FORMAT = 1
|
||||
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
|
||||
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
|
||||
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
|
||||
|
||||
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
|
||||
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
|
||||
SNDRV_PCM_HW_PARAM_CHANNELS = 10
|
||||
SNDRV_PCM_HW_PARAM_RATE = 11
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
|
||||
SNDRV_PCM_HW_PARAM_PERIODS = 15
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
|
||||
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
|
||||
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
|
||||
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
|
||||
|
||||
SNDRV_MASK_MAX = 256
|
||||
|
||||
SNDRV_PCM_TSTAMP_NONE = 0
|
||||
SNDRV_PCM_TSTAMP_ENABLE = 1
|
||||
)
|
||||
|
||||
type snd_mask struct { // size 32
|
||||
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
|
||||
}
|
||||
|
||||
type snd_interval struct { // size 12
|
||||
min unsigned_int // offset 0, size 4
|
||||
max unsigned_int // offset 4, size 4
|
||||
bit unsigned_int
|
||||
}
|
||||
|
||||
type snd_pcm_hw_params struct { // size 608
|
||||
flags unsigned_int // offset 0, size 4
|
||||
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
|
||||
mres [5]snd_mask // offset 100, size 160
|
||||
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
|
||||
ires [9]snd_interval // offset 404, size 108
|
||||
rmask unsigned_int // offset 512, size 4
|
||||
cmask unsigned_int // offset 516, size 4
|
||||
info unsigned_int // offset 520, size 4
|
||||
msbits unsigned_int // offset 524, size 4
|
||||
rate_num unsigned_int // offset 528, size 4
|
||||
rate_den unsigned_int // offset 532, size 4
|
||||
fifo_size snd_pcm_uframes_t // offset 536, size 8
|
||||
reserved [64]unsigned_char // offset 544, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_sw_params struct { // size 136
|
||||
tstamp_mode signed_int // offset 0, size 4
|
||||
period_step unsigned_int // offset 4, size 4
|
||||
sleep_min unsigned_int // offset 8, size 4
|
||||
avail_min snd_pcm_uframes_t // offset 16, size 8
|
||||
xfer_align snd_pcm_uframes_t // offset 24, size 8
|
||||
start_threshold snd_pcm_uframes_t // offset 32, size 8
|
||||
stop_threshold snd_pcm_uframes_t // offset 40, size 8
|
||||
silence_threshold snd_pcm_uframes_t // offset 48, size 8
|
||||
silence_size snd_pcm_uframes_t // offset 56, size 8
|
||||
boundary snd_pcm_uframes_t // offset 64, size 8
|
||||
proto unsigned_int // offset 72, size 4
|
||||
tstamp_type unsigned_int // offset 76, size 4
|
||||
reserved [56]unsigned_char // offset 80, size 56
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sound/asound.h>
|
||||
|
||||
#define print_line(text) printf("%s\n", text)
|
||||
#define print_hex_const(name) printf("\t%s = 0x%08lx\n", #name, name)
|
||||
#define print_int_const(con) printf("\t%s = %d\n", #con, con)
|
||||
|
||||
#define print_struct_header(str) printf("type %s struct { // size %lu\n", #str, sizeof(struct str))
|
||||
#define print_struct_member(str, mem, typ) printf("\t%s %s // offset %lu, size %lu\n", #mem == "type" ? "typ" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem))
|
||||
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h
|
||||
int main() {
|
||||
print_line("package device\n");
|
||||
|
||||
print_line("type unsigned_char = byte");
|
||||
print_line("type signed_int = int32");
|
||||
print_line("type unsigned_int = uint32");
|
||||
print_line("type signed_long = int64");
|
||||
print_line("type unsigned_long = uint64");
|
||||
print_line("type __u32 = uint32");
|
||||
print_line("type void__user = uintptr\n");
|
||||
|
||||
print_line("const (");
|
||||
print_int_const(SNDRV_PCM_STREAM_PLAYBACK);
|
||||
print_int_const(SNDRV_PCM_STREAM_CAPTURE);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
|
||||
print_int_const(SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED);
|
||||
print_int_const(SNDRV_PCM_ACCESS_MMAP_COMPLEX);
|
||||
print_int_const(SNDRV_PCM_ACCESS_RW_INTERLEAVED);
|
||||
print_int_const(SNDRV_PCM_ACCESS_RW_NONINTERLEAVED);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_FORMAT_S8);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U8);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S16_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S16_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U16_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U16_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S24_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S24_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U24_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U24_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S32_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_S32_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U32_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_U32_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT64_LE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_FLOAT64_BE);
|
||||
print_int_const(SNDRV_PCM_FORMAT_MU_LAW);
|
||||
print_int_const(SNDRV_PCM_FORMAT_A_LAW);
|
||||
print_int_const(SNDRV_PCM_FORMAT_MPEG);
|
||||
print_line("");
|
||||
print_hex_const(SNDRV_PCM_IOCTL_PVERSION); // A 0x00
|
||||
print_hex_const(SNDRV_PCM_IOCTL_INFO); // A 0x01
|
||||
print_hex_const(SNDRV_PCM_IOCTL_HW_REFINE); // A 0x10
|
||||
print_hex_const(SNDRV_PCM_IOCTL_HW_PARAMS); // A 0x11
|
||||
print_hex_const(SNDRV_PCM_IOCTL_SW_PARAMS); // A 0x13
|
||||
print_hex_const(SNDRV_PCM_IOCTL_PREPARE); // A 0x40
|
||||
print_hex_const(SNDRV_PCM_IOCTL_WRITEI_FRAMES); // A 0x50
|
||||
print_hex_const(SNDRV_PCM_IOCTL_READI_FRAMES); // A 0x51
|
||||
print_line(")\n");
|
||||
|
||||
print_struct_header(snd_pcm_info);
|
||||
print_struct_member(snd_pcm_info, device, "unsigned_int");
|
||||
print_struct_member(snd_pcm_info, subdevice, "unsigned_int");
|
||||
print_struct_member(snd_pcm_info, stream, "signed_int");
|
||||
print_struct_member(snd_pcm_info, card, "signed_int");
|
||||
print_struct_member(snd_pcm_info, id, "[64]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, name, "[80]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, subname, "[32]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, dev_class, "signed_int");
|
||||
print_struct_member(snd_pcm_info, dev_subclass, "signed_int");
|
||||
print_struct_member(snd_pcm_info, subdevices_count, "unsigned_int");
|
||||
print_struct_member(snd_pcm_info, subdevices_avail, "unsigned_int");
|
||||
print_line("\tpad1 [16]unsigned_char");
|
||||
print_struct_member(snd_pcm_info, reserved, "[64]unsigned_char");
|
||||
print_line("}\n");
|
||||
|
||||
print_line("type snd_pcm_uframes_t = unsigned_long");
|
||||
print_line("type snd_pcm_sframes_t = signed_long\n");
|
||||
|
||||
print_struct_header(snd_xferi);
|
||||
print_struct_member(snd_xferi, result, "snd_pcm_sframes_t");
|
||||
print_struct_member(snd_xferi, buf, "void__user");
|
||||
print_struct_member(snd_xferi, frames, "snd_pcm_uframes_t");
|
||||
print_line("}\n");
|
||||
|
||||
print_line("const (");
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_ACCESS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FORMAT);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_SUBFORMAT);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FIRST_MASK);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_LAST_MASK);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_SAMPLE_BITS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FRAME_BITS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_CHANNELS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_RATE);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_TIME);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIOD_BYTES);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_PERIODS);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_TIME);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_SIZE);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_BUFFER_BYTES);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_TICK_TIME);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_FIRST_INTERVAL);
|
||||
print_int_const(SNDRV_PCM_HW_PARAM_LAST_INTERVAL);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_MASK_MAX);
|
||||
print_line("");
|
||||
print_int_const(SNDRV_PCM_TSTAMP_NONE);
|
||||
print_int_const(SNDRV_PCM_TSTAMP_ENABLE);
|
||||
print_line(")\n");
|
||||
|
||||
print_struct_header(snd_mask);
|
||||
print_struct_member(snd_mask, bits, "[(SNDRV_MASK_MAX+31)/32]__u32");
|
||||
print_line("}\n");
|
||||
|
||||
print_struct_header(snd_interval);
|
||||
print_struct_member(snd_interval, min, "unsigned_int");
|
||||
print_struct_member(snd_interval, max, "unsigned_int");
|
||||
print_line("\tbit unsigned_int");
|
||||
print_line("}\n");
|
||||
|
||||
print_struct_header(snd_pcm_hw_params);
|
||||
print_struct_member(snd_pcm_hw_params, flags, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, masks, "[SNDRV_PCM_HW_PARAM_LAST_MASK-SNDRV_PCM_HW_PARAM_FIRST_MASK+1]snd_mask");
|
||||
print_struct_member(snd_pcm_hw_params, mres, "[5]snd_mask");
|
||||
print_struct_member(snd_pcm_hw_params, intervals, "[SNDRV_PCM_HW_PARAM_LAST_INTERVAL-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL+1]snd_interval");
|
||||
print_struct_member(snd_pcm_hw_params, ires, "[9]snd_interval");
|
||||
print_struct_member(snd_pcm_hw_params, rmask, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, cmask, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, info, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, msbits, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, rate_num, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, rate_den, "unsigned_int");
|
||||
print_struct_member(snd_pcm_hw_params, fifo_size, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_hw_params, reserved, "[64]unsigned_char");
|
||||
print_line("}\n");
|
||||
|
||||
print_struct_header(snd_pcm_sw_params);
|
||||
print_struct_member(snd_pcm_sw_params, tstamp_mode, "signed_int");
|
||||
print_struct_member(snd_pcm_sw_params, period_step, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, sleep_min, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, avail_min, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, xfer_align, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, start_threshold, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, stop_threshold, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, silence_threshold, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, silence_size, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, boundary, "snd_pcm_uframes_t");
|
||||
print_struct_member(snd_pcm_sw_params, proto, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, tstamp_type, "unsigned_int");
|
||||
print_struct_member(snd_pcm_sw_params, reserved, "[56]unsigned_char");
|
||||
print_line("}\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package device
|
||||
|
||||
type unsigned_char = byte
|
||||
type signed_int = int32
|
||||
type unsigned_int = uint32
|
||||
type signed_long = int64
|
||||
type unsigned_long = uint64
|
||||
type __u32 = uint32
|
||||
type void__user = uintptr
|
||||
|
||||
const (
|
||||
SNDRV_PCM_STREAM_PLAYBACK = 0
|
||||
SNDRV_PCM_STREAM_CAPTURE = 1
|
||||
|
||||
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED = 0
|
||||
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
|
||||
SNDRV_PCM_ACCESS_MMAP_COMPLEX = 2
|
||||
SNDRV_PCM_ACCESS_RW_INTERLEAVED = 3
|
||||
SNDRV_PCM_ACCESS_RW_NONINTERLEAVED = 4
|
||||
|
||||
SNDRV_PCM_FORMAT_S8 = 0
|
||||
SNDRV_PCM_FORMAT_U8 = 1
|
||||
SNDRV_PCM_FORMAT_S16_LE = 2
|
||||
SNDRV_PCM_FORMAT_S16_BE = 3
|
||||
SNDRV_PCM_FORMAT_U16_LE = 4
|
||||
SNDRV_PCM_FORMAT_U16_BE = 5
|
||||
SNDRV_PCM_FORMAT_S24_LE = 6
|
||||
SNDRV_PCM_FORMAT_S24_BE = 7
|
||||
SNDRV_PCM_FORMAT_U24_LE = 8
|
||||
SNDRV_PCM_FORMAT_U24_BE = 9
|
||||
SNDRV_PCM_FORMAT_S32_LE = 10
|
||||
SNDRV_PCM_FORMAT_S32_BE = 11
|
||||
SNDRV_PCM_FORMAT_U32_LE = 12
|
||||
SNDRV_PCM_FORMAT_U32_BE = 13
|
||||
SNDRV_PCM_FORMAT_FLOAT_LE = 14
|
||||
SNDRV_PCM_FORMAT_FLOAT_BE = 15
|
||||
SNDRV_PCM_FORMAT_FLOAT64_LE = 16
|
||||
SNDRV_PCM_FORMAT_FLOAT64_BE = 17
|
||||
SNDRV_PCM_FORMAT_MU_LAW = 20
|
||||
SNDRV_PCM_FORMAT_A_LAW = 21
|
||||
SNDRV_PCM_FORMAT_MPEG = 23
|
||||
|
||||
SNDRV_PCM_IOCTL_PVERSION = 0x40044100
|
||||
SNDRV_PCM_IOCTL_INFO = 0x41204101
|
||||
SNDRV_PCM_IOCTL_HW_REFINE = 0xc25c4110
|
||||
SNDRV_PCM_IOCTL_HW_PARAMS = 0xc25c4111
|
||||
SNDRV_PCM_IOCTL_SW_PARAMS = 0xc0684113
|
||||
SNDRV_PCM_IOCTL_PREPARE = 0x20004140
|
||||
SNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x800c4150
|
||||
SNDRV_PCM_IOCTL_READI_FRAMES = 0x400c4151
|
||||
)
|
||||
|
||||
type snd_pcm_info struct { // size 288
|
||||
device unsigned_int // offset 0, size 4
|
||||
subdevice unsigned_int // offset 4, size 4
|
||||
stream signed_int // offset 8, size 4
|
||||
card signed_int // offset 12, size 4
|
||||
id [64]unsigned_char // offset 16, size 64
|
||||
name [80]unsigned_char // offset 80, size 80
|
||||
subname [32]unsigned_char // offset 160, size 32
|
||||
dev_class signed_int // offset 192, size 4
|
||||
dev_subclass signed_int // offset 196, size 4
|
||||
subdevices_count unsigned_int // offset 200, size 4
|
||||
subdevices_avail unsigned_int // offset 204, size 4
|
||||
pad1 [16]unsigned_char
|
||||
reserved [64]unsigned_char // offset 224, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_uframes_t = unsigned_long
|
||||
type snd_pcm_sframes_t = signed_long
|
||||
|
||||
type snd_xferi struct { // size 12
|
||||
result snd_pcm_sframes_t // offset 0, size 4
|
||||
buf void__user // offset 4, size 4
|
||||
frames snd_pcm_uframes_t // offset 8, size 4
|
||||
}
|
||||
|
||||
const (
|
||||
SNDRV_PCM_HW_PARAM_ACCESS = 0
|
||||
SNDRV_PCM_HW_PARAM_FORMAT = 1
|
||||
SNDRV_PCM_HW_PARAM_SUBFORMAT = 2
|
||||
SNDRV_PCM_HW_PARAM_FIRST_MASK = 0
|
||||
SNDRV_PCM_HW_PARAM_LAST_MASK = 2
|
||||
|
||||
SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 8
|
||||
SNDRV_PCM_HW_PARAM_FRAME_BITS = 9
|
||||
SNDRV_PCM_HW_PARAM_CHANNELS = 10
|
||||
SNDRV_PCM_HW_PARAM_RATE = 11
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_TIME = 12
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_SIZE = 13
|
||||
SNDRV_PCM_HW_PARAM_PERIOD_BYTES = 14
|
||||
SNDRV_PCM_HW_PARAM_PERIODS = 15
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_TIME = 16
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_SIZE = 17
|
||||
SNDRV_PCM_HW_PARAM_BUFFER_BYTES = 18
|
||||
SNDRV_PCM_HW_PARAM_TICK_TIME = 19
|
||||
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8
|
||||
SNDRV_PCM_HW_PARAM_LAST_INTERVAL = 19
|
||||
|
||||
SNDRV_MASK_MAX = 256
|
||||
|
||||
SNDRV_PCM_TSTAMP_NONE = 0
|
||||
SNDRV_PCM_TSTAMP_ENABLE = 1
|
||||
)
|
||||
|
||||
type snd_mask struct { // size 32
|
||||
bits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32
|
||||
}
|
||||
|
||||
type snd_interval struct { // size 12
|
||||
min unsigned_int // offset 0, size 4
|
||||
max unsigned_int // offset 4, size 4
|
||||
bit unsigned_int
|
||||
}
|
||||
|
||||
type snd_pcm_hw_params struct { // size 604
|
||||
flags unsigned_int // offset 0, size 4
|
||||
masks [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask // offset 4, size 96
|
||||
mres [5]snd_mask // offset 100, size 160
|
||||
intervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144
|
||||
ires [9]snd_interval // offset 404, size 108
|
||||
rmask unsigned_int // offset 512, size 4
|
||||
cmask unsigned_int // offset 516, size 4
|
||||
info unsigned_int // offset 520, size 4
|
||||
msbits unsigned_int // offset 524, size 4
|
||||
rate_num unsigned_int // offset 528, size 4
|
||||
rate_den unsigned_int // offset 532, size 4
|
||||
fifo_size snd_pcm_uframes_t // offset 536, size 4
|
||||
reserved [64]unsigned_char // offset 540, size 64
|
||||
}
|
||||
|
||||
type snd_pcm_sw_params struct { // size 104
|
||||
tstamp_mode signed_int // offset 0, size 4
|
||||
period_step unsigned_int // offset 4, size 4
|
||||
sleep_min unsigned_int // offset 8, size 4
|
||||
avail_min snd_pcm_uframes_t // offset 12, size 4
|
||||
xfer_align snd_pcm_uframes_t // offset 16, size 4
|
||||
start_threshold snd_pcm_uframes_t // offset 20, size 4
|
||||
stop_threshold snd_pcm_uframes_t // offset 24, size 4
|
||||
silence_threshold snd_pcm_uframes_t // offset 28, size 4
|
||||
silence_size snd_pcm_uframes_t // offset 32, size 4
|
||||
boundary snd_pcm_uframes_t // offset 36, size 4
|
||||
proto unsigned_int // offset 40, size 4
|
||||
tstamp_type unsigned_int // offset 44, size 4
|
||||
reserved [56]unsigned_char // offset 48, size 56
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
fd uintptr
|
||||
path string
|
||||
|
||||
hwparams snd_pcm_hw_params
|
||||
frameBytes int // sample size * channels
|
||||
}
|
||||
|
||||
func Open(path string) (*Device, error) {
|
||||
// important to use nonblock because can get lock
|
||||
fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// important to remove nonblock because better to handle reads and writes
|
||||
if err = syscall.SetNonblock(fd, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := &Device{fd: uintptr(fd), path: path}
|
||||
d.init()
|
||||
|
||||
// load all supported formats, channels, rates, etc.
|
||||
if err = ioctl(d.fd, SNDRV_PCM_IOCTL_HW_REFINE, &d.hwparams); err != nil {
|
||||
_ = d.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.setMask(SNDRV_PCM_HW_PARAM_ACCESS, SNDRV_PCM_ACCESS_RW_INTERLEAVED)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *Device) Close() error {
|
||||
return syscall.Close(int(d.fd))
|
||||
}
|
||||
|
||||
func (d *Device) IsCapture() bool {
|
||||
// path: /dev/snd/pcmC0D0c, where p - playback, c - capture
|
||||
return d.path[len(d.path)-1] == 'c'
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Card int
|
||||
Device int
|
||||
SubDevice int
|
||||
Stream int
|
||||
ID string
|
||||
Name string
|
||||
SubName string
|
||||
}
|
||||
|
||||
func (d *Device) Info() (*Info, error) {
|
||||
var info snd_pcm_info
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_INFO, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Info{
|
||||
Card: int(info.card),
|
||||
Device: int(info.device),
|
||||
SubDevice: int(info.subdevice),
|
||||
Stream: int(info.stream),
|
||||
ID: str(info.id[:]),
|
||||
Name: str(info.name[:]),
|
||||
SubName: str(info.subname[:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Device) CheckFormat(format byte) bool {
|
||||
return d.checkMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))
|
||||
}
|
||||
|
||||
func (d *Device) ListFormats() (formats []byte) {
|
||||
for i := byte(0); i <= 28; i++ {
|
||||
if d.CheckFormat(i) {
|
||||
formats = append(formats, i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) RangeRates() (uint32, uint32) {
|
||||
return d.getInterval(SNDRV_PCM_HW_PARAM_RATE)
|
||||
}
|
||||
|
||||
func (d *Device) RangeChannels() (byte, byte) {
|
||||
minCh, maxCh := d.getInterval(SNDRV_PCM_HW_PARAM_CHANNELS)
|
||||
return byte(minCh), byte(maxCh)
|
||||
}
|
||||
|
||||
func (d *Device) GetRateNear(rate uint32) uint32 {
|
||||
r1, r2 := d.RangeRates()
|
||||
if rate < r1 {
|
||||
return r1
|
||||
}
|
||||
if rate > r2 {
|
||||
return r2
|
||||
}
|
||||
return rate
|
||||
}
|
||||
|
||||
func (d *Device) GetChannelsNear(channels byte) byte {
|
||||
c1, c2 := d.RangeChannels()
|
||||
if channels < c1 {
|
||||
return c1
|
||||
}
|
||||
if channels > c2 {
|
||||
return c2
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
const bufferSize = 4096
|
||||
|
||||
func (d *Device) SetHWParams(format byte, rate uint32, channels byte) error {
|
||||
d.setInterval(SNDRV_PCM_HW_PARAM_CHANNELS, uint32(channels))
|
||||
d.setInterval(SNDRV_PCM_HW_PARAM_RATE, rate)
|
||||
d.setMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))
|
||||
//d.setMask(SNDRV_PCM_HW_PARAM_SUBFORMAT, 0)
|
||||
|
||||
// important for smooth playback
|
||||
d.setInterval(SNDRV_PCM_HW_PARAM_BUFFER_SIZE, bufferSize)
|
||||
//d.setInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE, 2000)
|
||||
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_HW_PARAMS, &d.hwparams); err != nil {
|
||||
return fmt.Errorf("[alsa] set hw_params: %w", err)
|
||||
}
|
||||
|
||||
_, i := d.getInterval(SNDRV_PCM_HW_PARAM_FRAME_BITS)
|
||||
d.frameBytes = int(i / 8)
|
||||
|
||||
_, periods := d.getInterval(SNDRV_PCM_HW_PARAM_PERIODS)
|
||||
_, periodSize := d.getInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE)
|
||||
threshold := snd_pcm_uframes_t(periods * periodSize) // same as bufferSize
|
||||
|
||||
swparams := snd_pcm_sw_params{
|
||||
//tstamp_mode: SNDRV_PCM_TSTAMP_ENABLE,
|
||||
period_step: 1,
|
||||
avail_min: 1, // start as soon as possible
|
||||
stop_threshold: threshold,
|
||||
}
|
||||
|
||||
if d.IsCapture() {
|
||||
swparams.start_threshold = 1
|
||||
} else {
|
||||
swparams.start_threshold = threshold
|
||||
}
|
||||
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_SW_PARAMS, &swparams); err != nil {
|
||||
return fmt.Errorf("[alsa] set sw_params: %w", err)
|
||||
}
|
||||
|
||||
if err := ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil); err != nil {
|
||||
return fmt.Errorf("[alsa] prepare: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) Write(b []byte) (n int, err error) {
|
||||
xfer := &snd_xferi{
|
||||
buf: uintptr(unsafe.Pointer(&b[0])),
|
||||
frames: snd_pcm_uframes_t(len(b) / d.frameBytes),
|
||||
}
|
||||
err = ioctl(d.fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, xfer)
|
||||
if err == syscall.EPIPE {
|
||||
// auto handle underrun state
|
||||
// https://stackoverflow.com/questions/59396728/how-to-properly-handle-xrun-in-alsa-programming-when-playing-audio-with-snd-pcm
|
||||
err = ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil)
|
||||
}
|
||||
n = int(xfer.result) * d.frameBytes
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) Read(b []byte) (n int, err error) {
|
||||
xfer := &snd_xferi{
|
||||
buf: uintptr(unsafe.Pointer(&b[0])),
|
||||
frames: snd_pcm_uframes_t(len(b) / d.frameBytes),
|
||||
}
|
||||
err = ioctl(d.fd, SNDRV_PCM_IOCTL_READI_FRAMES, xfer)
|
||||
n = int(xfer.result) * d.frameBytes
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) init() {
|
||||
for i := range d.hwparams.masks {
|
||||
d.hwparams.masks[i].bits[0] = 0xFFFFFFFF
|
||||
d.hwparams.masks[i].bits[1] = 0xFFFFFFFF
|
||||
}
|
||||
for i := range d.hwparams.intervals {
|
||||
d.hwparams.intervals[i].max = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
d.hwparams.rmask = 0xFFFFFFFF
|
||||
d.hwparams.cmask = 0
|
||||
d.hwparams.info = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
func (d *Device) setInterval(param, val uint32) {
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max = val
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].bit = 0b0100 // integer
|
||||
}
|
||||
|
||||
func (d *Device) setIntervalMin(param, val uint32) {
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val
|
||||
}
|
||||
|
||||
func (d *Device) getInterval(param uint32) (uint32, uint32) {
|
||||
return d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
|
||||
d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max
|
||||
}
|
||||
|
||||
func (d *Device) setMask(mask, val uint32) {
|
||||
d.hwparams.masks[mask].bits[0] = 0
|
||||
d.hwparams.masks[mask].bits[1] = 0
|
||||
d.hwparams.masks[mask].bits[val>>5] = 1 << (val & 0x1F)
|
||||
}
|
||||
|
||||
func (d *Device) checkMask(mask, val uint32) bool {
|
||||
return d.hwparams.masks[mask].bits[val>>5]&(1<<(val&0x1F)) > 0
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func ioctl(fd, req uintptr, arg any) error {
|
||||
var ptr uintptr
|
||||
if arg != nil {
|
||||
ptr = reflect.ValueOf(arg).Pointer()
|
||||
}
|
||||
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, req, ptr)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func str(b []byte) string {
|
||||
if i := bytes.IndexByte(b, 0); i >= 0 {
|
||||
return string(b[:i])
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
func Open(rawURL string) (core.Producer, error) {
|
||||
// Example (ffmpeg source compatible):
|
||||
// alsa:device?audio=/dev/snd/pcmC0D0p
|
||||
// TODO: ?audio=default
|
||||
// TODO: ?audio=hw:0,0
|
||||
// TODO: &sample_rate=48000&channels=2
|
||||
// TODO: &backchannel=1
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := u.Query().Get("audio")
|
||||
dev, err := device.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dev.CheckFormat(device.SNDRV_PCM_FORMAT_S16_LE) {
|
||||
_ = dev.Close()
|
||||
return nil, errors.New("alsa: format S16LE not supported")
|
||||
}
|
||||
|
||||
switch path[len(path)-1] {
|
||||
case 'p': // playback
|
||||
return newPlayback(dev)
|
||||
case 'c': // capture
|
||||
return newCapture(dev)
|
||||
}
|
||||
|
||||
_ = dev.Close()
|
||||
return nil, fmt.Errorf("alsa: unknown path: %s", path)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Playback struct {
|
||||
core.Connection
|
||||
dev *device.Device
|
||||
closed core.Waiter
|
||||
}
|
||||
|
||||
func newPlayback(dev *device.Device) (*Playback, error) {
|
||||
medias := []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{
|
||||
{Name: core.CodecPCML}, // support ffmpeg producer (auto transcode)
|
||||
{Name: core.CodecPCMA, ClockRate: 8000}, // support webrtc producer
|
||||
},
|
||||
},
|
||||
}
|
||||
return &Playback{
|
||||
Connection: core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "alsa",
|
||||
Medias: medias,
|
||||
Transport: dev,
|
||||
},
|
||||
dev: dev,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Playback) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (p *Playback) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
|
||||
src := track.Codec
|
||||
dst := &core.Codec{
|
||||
Name: core.CodecPCML,
|
||||
ClockRate: p.dev.GetRateNear(src.ClockRate),
|
||||
Channels: p.dev.GetChannelsNear(src.Channels),
|
||||
}
|
||||
sender := core.NewSender(media, dst)
|
||||
|
||||
sender.Handler = func(pkt *rtp.Packet) {
|
||||
if n, err := p.dev.Write(pkt.Payload); err == nil {
|
||||
p.Send += n
|
||||
}
|
||||
}
|
||||
|
||||
if sender.Handler = pcm.TranscodeHandler(dst, src, sender.Handler); sender.Handler == nil {
|
||||
return fmt.Errorf("alsa: can't convert %s to %s", src, dst)
|
||||
}
|
||||
|
||||
// typical card support:
|
||||
// - Formats: S16_LE, S32_LE
|
||||
// - ClockRates: 8000 - 192000
|
||||
// - Channels: 2 - 10
|
||||
err := p.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, dst.ClockRate, byte(dst.Channels))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sender.HandleRTP(track)
|
||||
p.Senders = append(p.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Playback) Start() (err error) {
|
||||
return p.closed.Wait()
|
||||
}
|
||||
|
||||
func (p *Playback) Stop() error {
|
||||
p.closed.Done(nil)
|
||||
return p.Connection.Stop()
|
||||
}
|
||||
+8
-2
@@ -89,6 +89,12 @@ func (r *Reader) ReadBits64(n byte) (res uint64) {
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Reader) ReadFloat32() float64 {
|
||||
i := r.ReadUint16()
|
||||
f := r.ReadUint16()
|
||||
return float64(i) + float64(f)/65536
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBytes(n int) (b []byte) {
|
||||
if r.bits == 0 {
|
||||
if r.pos+n > len(r.buf) {
|
||||
@@ -122,9 +128,9 @@ func (r *Reader) ReadUEGolomb() uint32 {
|
||||
// ReadSEGolomb - ReadSignedExponentialGolomb
|
||||
func (r *Reader) ReadSEGolomb() int32 {
|
||||
if b := r.ReadUEGolomb(); b%2 == 0 {
|
||||
return -int32(b >> 1)
|
||||
return -int32(b / 2)
|
||||
} else {
|
||||
return int32(b >> 1)
|
||||
return int32((b + 1) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+34
-1
@@ -13,7 +13,7 @@ import (
|
||||
type Codec struct {
|
||||
Name string // H264, PCMU, PCMA, opus...
|
||||
ClockRate uint32 // 90000, 8000, 16000...
|
||||
Channels uint16 // 0, 1, 2
|
||||
Channels uint8 // 0, 1, 2
|
||||
FmtpLine string
|
||||
PayloadType uint8
|
||||
}
|
||||
@@ -249,3 +249,36 @@ func DecodeH264(fmtp string) (profile string, level byte) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseCodecString(s string) *Codec {
|
||||
var codec Codec
|
||||
|
||||
ss := strings.Split(s, "/")
|
||||
switch strings.ToLower(ss[0]) {
|
||||
case "pcm_s16be", "s16be", "pcm":
|
||||
codec.Name = CodecPCM
|
||||
case "pcm_s16le", "s16le", "pcml":
|
||||
codec.Name = CodecPCML
|
||||
case "pcm_alaw", "alaw", "pcma", "g711a":
|
||||
codec.Name = CodecPCMA
|
||||
case "pcm_mulaw", "mulaw", "pcmu", "g711u":
|
||||
codec.Name = CodecPCMU
|
||||
case "aac", "mpeg4-generic":
|
||||
codec.Name = CodecAAC
|
||||
case "opus":
|
||||
codec.Name = CodecOpus
|
||||
case "flac":
|
||||
codec.Name = CodecFLAC
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(ss) >= 2 {
|
||||
codec.ClockRate = uint32(Atoi(ss[1]))
|
||||
}
|
||||
if len(ss) >= 3 {
|
||||
codec.Channels = uint8(Atoi(ss[1]))
|
||||
}
|
||||
|
||||
return &codec
|
||||
}
|
||||
|
||||
@@ -118,3 +118,17 @@ func TestName(t *testing.T) {
|
||||
// stage3
|
||||
_ = prod2.Stop()
|
||||
}
|
||||
|
||||
func TestStripUserinfo(t *testing.T) {
|
||||
s := `streams:
|
||||
test:
|
||||
- ffmpeg:rtsp://username:password@10.1.2.3:554/stream1
|
||||
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
|
||||
`
|
||||
s = StripUserinfo(s)
|
||||
require.Equal(t, `streams:
|
||||
test:
|
||||
- ffmpeg:rtsp://***@10.1.2.3:554/stream1
|
||||
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
|
||||
`, s)
|
||||
}
|
||||
|
||||
+30
-3
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -10,9 +11,9 @@ import (
|
||||
|
||||
const (
|
||||
BufferSize = 64 * 1024 // 64K
|
||||
ConnDialTimeout = time.Second * 3
|
||||
ConnDeadline = time.Second * 5
|
||||
ProbeTimeout = time.Second * 3
|
||||
ConnDialTimeout = 5 * time.Second
|
||||
ConnDeadline = 5 * time.Second
|
||||
ProbeTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
|
||||
@@ -66,6 +67,21 @@ func Atoi(s string) (i int) {
|
||||
return
|
||||
}
|
||||
|
||||
// ParseByte - fast parsing string to byte function
|
||||
func ParseByte(s string) (b byte) {
|
||||
for i, ch := range []byte(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return 0
|
||||
}
|
||||
if i > 0 {
|
||||
b *= 10
|
||||
}
|
||||
b += ch
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Assert(ok bool) {
|
||||
if !ok {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
@@ -77,3 +93,14 @@ func Caller() string {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
return file + ":" + strconv.Itoa(line)
|
||||
}
|
||||
|
||||
const (
|
||||
unreserved = `A-Za-z0-9-._~`
|
||||
subdelims = `!$&'()*+,;=`
|
||||
userinfo = unreserved + subdelims + `%:`
|
||||
)
|
||||
|
||||
func StripUserinfo(s string) string {
|
||||
sanitizer := regexp.MustCompile(`://[` + userinfo + `]+@`)
|
||||
return sanitizer.ReplaceAllString(s, `://***@`)
|
||||
}
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, uint16(codec.Channels), codec.FmtpLine)
|
||||
|
||||
if media.Direction != "" {
|
||||
md.WithPropertyAttribute(media.Direction)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user