Compare commits

..

126 Commits

Author SHA1 Message Date
Alexey Khit
fc1b6af436 Update version to 1.7.0 2023-09-02 16:05:09 +03:00
Alexey Khit
88fb589d2e Update readme about new features 2023-09-02 16:04:53 +03:00
Alexey Khit
5c5357cd79 Fix default video codec for HomeKit source 2023-09-02 15:22:03 +03:00
Alexey Khit
5ffd60c429 Update HomeKit default PIN 2023-09-02 14:44:13 +03:00
Alexey Khit
5645c73613 Update FFmpeg device discovery for Linux 2023-09-02 14:40:27 +03:00
Alexey Khit
13a7957cf3 Update HomeKit pairing status 2023-09-02 11:05:02 +03:00
Alexey Khit
c0d5a7c01a Rename PCM codecs print name 2023-09-02 09:17:38 +03:00
Alexey Khit
d87cc9ddb6 Fix homekit pairing table 2023-09-02 09:14:09 +03:00
Alexey Khit
b1c4bcc508 Fix patching YAML in some cases 2023-09-02 08:43:46 +03:00
Alexey Khit
6288c2a57f Add MJPEG support to HomeKit client 2023-09-02 07:39:16 +03:00
Alexey Khit
1c569e690d Fix HomeKit client stat info 2023-09-02 07:38:49 +03:00
Alexey Khit
7fdc6b9472 Update SRTP server constructor 2023-09-02 07:37:49 +03:00
Alexey Khit
60d7d525f2 Update add consumer error message 2023-09-02 07:37:15 +03:00
Alexey Khit
f6f2998e85 Fix JPEG screen length 2023-09-02 07:36:26 +03:00
Alexey Khit
82d1f2cf0b Add new goroutine to debug stack 2023-09-02 07:35:58 +03:00
Alexey Khit
f00e646612 Add support HomeKit server 2023-09-02 06:35:04 +03:00
Alexey Khit
a101387b26 Add debug logger 2023-09-02 06:31:34 +03:00
Alexey Khit
af31ab604d Update FFmpeg preset for OPUS 2023-09-02 06:31:10 +03:00
Alexey Khit
ccdd6ed490 Update SPS parser 2023-09-02 06:30:30 +03:00
Alexey Khit
9f404d965f Rewrite HomeKit pairing API 2023-09-01 22:48:06 +03:00
Alexey Khit
0621b82aff Change response sources API 2023-09-01 22:32:49 +03:00
Alexey Khit
22787b979d Rewrite HomeKit client 2023-09-01 10:38:38 +03:00
Alexey Khit
7d65c60711 Add stream redirect handler 2023-09-01 10:18:50 +03:00
Alexey Khit
69da64a49c Rename streams to sources in the discovery API 2023-09-01 10:17:58 +03:00
Alexey Khit
66c858e00e Rewrite JPEG snapshot consumer 2023-08-30 05:57:00 +03:00
Alexey Khit
ef63cec7a8 Rewrite once buffer for keyframes 2023-08-29 18:04:02 +03:00
Alexey Khit
0ac505ba09 Simplify MJPEG consumer 2023-08-29 17:16:51 +03:00
Alexey Khit
d4444c6257 Code refactoring 2023-08-28 22:43:07 +03:00
Alexey Khit
c6d5bb4eeb Add kasa client and simplify multipart client 2023-08-28 22:31:52 +03:00
Alexey Khit
7f232c5cf2 Add insecure HTTPS requests to IP addresses 2023-08-28 22:29:12 +03:00
Alexey Khit
dc2ab5fcc0 Add support TP-Link Kasa Spot KC401 #545 2023-08-28 19:30:34 +03:00
Alex X
137b23da10 Fix config file validating 2023-08-26 07:13:59 +03:00
Alexey Khit
54e361e3b8 Update go.mod 2023-08-26 07:03:03 +03:00
Alexey Khit
c78da1a7a9 Add about Wyze cameras project to readme 2023-08-25 11:05:19 +03:00
Alex X
27673cb0c1 Merge pull request #592 from skrashevich/go1.21
Update Go version to 1.21 in workflows and Dockerfiles
2023-08-23 18:19:40 +03:00
Alex X
c040a02fa8 Merge pull request #593 from skrashevich/ace-1.24.1
Update ace version to 1.24.1
2023-08-23 18:17:11 +03:00
Alexey Khit
a664e3b838 Code refactoring 2023-08-23 18:14:49 +03:00
Alexey Khit
317b3b5eeb Add support OpenIPC WebRTC format 2023-08-23 18:11:01 +03:00
Sergey Krashevich
9f14b30aae Refactor CSS in editor.html for better readability and remove duplicate body rule 2023-08-23 17:24:17 +03:00
Sergey Krashevich
065a6f4f46 Update Debian and Go versions to bookworm-slim and 1.21-bookworm respectively in hardware.Dockerfile 2023-08-23 17:06:21 +03:00
Alexey Khit
9f9dc7e844 Add support custom timeout for RTSP source 2023-08-23 14:08:15 +03:00
Alexey Khit
b1c0a28366 Update readme about artifacts 2023-08-23 13:27:49 +03:00
Alexey Khit
fc963dfe5c Fix H264 profile parsing for OpenIPC project 2023-08-23 13:26:57 +03:00
Sergey Krashevich
6f5ba2ade6 Update ace library version to 1.24.1 and fix code syntax in editor.html 2023-08-23 12:59:05 +03:00
Alexey Khit
ea708bb606 Add responses on RTSP OPTIONS pings 2023-08-23 10:14:58 +03:00
Sergey Krashevich
0822326900 Update Go version to 1.21 in build and test workflows and Dockerfiles 2023-08-23 10:09:53 +03:00
Alexey Khit
79fc0cd395 Update headers handling for http source 2023-08-23 07:46:34 +03:00
Alex X
357e7c1b18 Merge pull request #557 from h0nIg/patch-1
fix known problem of wrong profile declaration capabilities
2023-08-23 07:01:24 +03:00
Alexey Khit
71f1e445e1 Fix 400 response on PLAY for Reolink Doorbell #562 2023-08-23 06:52:33 +03:00
Alexey Khit
20efe22e60 Update readme about wyze-bridge #588 2023-08-23 06:08:11 +03:00
Alexey Khit
75a3dad745 Fix redirect for rtspx source #565 2023-08-23 06:07:23 +03:00
Alexey Khit
f5cca50830 Add check for empty H265 packet #589 2023-08-22 16:08:02 +03:00
Alexey Khit
8cd977f7ad Add support B-frames for MP4 consumer 2023-08-22 15:55:20 +03:00
Alexey Khit
90f2a9e106 Fix some audio in RTSP server 2023-08-21 20:54:45 +03:00
Alexey Khit
e0ad358aa9 Update timestamp processing for MPEG-TS 2023-08-21 20:34:18 +03:00
Alexey Khit
3db4002420 Support hass source without hass config #541 2023-08-21 16:56:58 +03:00
Alexey Khit
bf248c49c3 Add support two channel PCM family audio #580 2023-08-21 15:35:23 +03:00
Alexey Khit
69a3a30a0e Add media filter for RTSP source #198 2023-08-21 14:07:07 +03:00
Alexey Khit
f80f179e4c Fix MP4 consumer with only audio 2023-08-21 07:31:21 +03:00
Alexey Khit
c1c1d84cef Add AAC consumer 2023-08-21 07:12:30 +03:00
Alexey Khit
c431d888f0 Add AAC raw codec to MPEG-TS consumer 2023-08-21 07:03:40 +03:00
Alexey Khit
2ebb791eb7 Remove old code 2023-08-21 07:00:46 +03:00
Alex X
00b818b4d7 Add support custom headers for HTTP source 2023-08-21 06:30:05 +03:00
Alex X
ce1b0d442c Remove old unnecessary file 2023-08-21 06:29:19 +03:00
Alexey Khit
5283c9781c Update readme about dev version 2023-08-21 00:04:08 +03:00
Alexey Khit
279d8bf799 Rewrite GitHub actions 2023-08-20 23:41:39 +03:00
Alexey Khit
7114d63ba6 Update readme about Reolink cameras 2023-08-20 21:46:12 +03:00
Alexey Khit
120ae89578 Update dependencies 2023-08-20 21:45:08 +03:00
Alexey Khit
d1eb623fd6 Add buffer for RTSP output 2023-08-20 21:25:45 +03:00
Alexey Khit
873cf65317 Increase buffer for RTSP input 2023-08-20 21:24:55 +03:00
Alexey Khit
2091dead3f Refactoring for MP4 file handler 2023-08-20 18:43:42 +03:00
Alexey Khit
2ffd859f0e Update MPEG-TS consumer compatibility 2023-08-20 18:43:12 +03:00
Alexey Khit
da02a97a00 Fix close for HLS source 2023-08-20 18:40:19 +03:00
Alexey Khit
fb51dc781d Improve HLS reader 2023-08-20 16:35:09 +03:00
Alexey Khit
32bf64028d Fix ADTStoRTP parser 2023-08-20 16:33:57 +03:00
Alexey Khit
2e4e75e386 Rewrite MP4, HLS, MPEG-TS consumers 2023-08-20 09:57:46 +03:00
Alexey Khit
f67f6e5b9f Rewrite mpegts producer and consumer 2023-08-19 16:37:52 +03:00
Alexey Khit
24039218a1 Add multipart source to magic source 2023-08-19 16:14:46 +03:00
Alexey Khit
1f447ef73c Rewrite multipart source 2023-08-19 16:14:36 +03:00
Alexey Khit
4509198eef Rewrite FLV source 2023-08-19 16:13:43 +03:00
Alexey Khit
bc60cbefb8 Rewrite magic source 2023-08-19 15:19:09 +03:00
Alexey Khit
a9118562a9 Rewrite MJPEG consumer 2023-08-19 06:09:05 +03:00
Alexey Khit
24637be7c2 Fix panic for multipart client 2023-08-19 06:05:34 +03:00
Alexey Khit
d74be47696 Improve bits reader and writer 2023-08-19 06:04:31 +03:00
Alexey Khit
76a00031cd Code refactoring 2023-08-18 15:53:46 +03:00
Alexey Khit
063a192699 Add support RTMPS source 2023-08-17 08:00:02 +03:00
Alexey Khit
b016b7dc2a Refactoring for RTMP source 2023-08-17 07:59:21 +03:00
Alexey Khit
42f6441512 Fix mpegts reader for tapo client 2023-08-17 07:13:41 +03:00
Alexey Khit
dd066ba040 Add HLS client 2023-08-17 06:55:59 +03:00
Alexey Khit
b3def6cfa2 Rewrite support MPEG-TS client 2023-08-17 05:45:45 +03:00
Alexey Khit
4a82eb3503 Rewrite magic client 2023-08-16 20:13:42 +03:00
Alexey Khit
c3ba8db660 Rewrite FLV/RTMP clients 2023-08-16 19:35:13 +03:00
Alexey Khit
4e1a0e1ab9 Rewrite magic client 2023-08-16 17:15:27 +03:00
Alexey Khit
1dd3dbbcd8 Rewrite AnnexB/AVCC parsers 2023-08-16 16:50:55 +03:00
Alexey Khit
e1be2d9e48 Add buffer to pipe reader 2023-08-16 16:32:09 +03:00
Alexey Khit
8fbfccd024 Add MP4 atoms reader 2023-08-14 14:49:16 +03:00
Alexey Khit
de6bb33f01 Add SPS parser and AVC/HVC conf encoders 2023-08-14 11:55:08 +03:00
Alexey Khit
3a40515a90 Remove old source files 2023-08-14 11:35:34 +03:00
Alexey Khit
5d533338d0 Fix incoming FLV source 2023-08-14 06:49:37 +03:00
Alexey Khit
f412852d50 Remove old HTTP-FLV client 2023-08-14 06:49:37 +03:00
Alexey Khit
5fbec487e2 Add ffplay to links page 2023-08-14 06:49:37 +03:00
Alexey Khit
19c61e20c0 Total rework RTMP client 2023-08-14 06:49:37 +03:00
Alexey Khit
0b6fda2af5 Total rework FLV client 2023-08-14 06:49:37 +03:00
Alexey Khit
e9795e7521 Add goreportcard to readme 2023-08-13 15:44:29 +03:00
Alex X
3b8413a9dd Merge pull request #567 from awatuna/awatuna-patch-1
Update helpers.go
2023-08-07 06:49:32 +04:00
awatuna
b2f9ad7efb Update helpers.go
more tplink ipcams
2023-08-07 06:25:16 +08:00
Alexey Khit
4baa3f5588 Fix rare error with ws.close() 2023-08-04 16:31:13 +04:00
Alex X
9c5ae3260c Merge pull request #561 from dbuezas/fix/another-h265-mediaCode
Add 83 (0x53) to h265 mediaCode
2023-08-04 13:21:00 +04:00
David Buezas
b7baef0a48 Add 83 (0x53) to h265 mediaCode 2023-08-04 11:07:20 +02:00
Alexey Khit
8778d7c9ab Add support http/mixed video/audio #545 2023-08-02 17:57:33 +04:00
Hans-Joachim Kliemeck
d275997e54 fix known problem of wrong profile declaration capabilities 2023-08-01 22:18:45 +02:00
Alexey Khit
2faea1bb69 Fix bug with esp32-cam-webserver #545 2023-07-31 20:55:46 +03:00
Alexey Khit
ba6c96412b Add YAML pkg with Patch function 2023-07-25 18:05:50 +03:00
Alexey Khit
ed38122752 Code refactoring 2023-07-25 18:05:29 +03:00
Alexey Khit
922587ed2e Fix WebUI background color for dark mode browser 2023-07-24 21:57:10 +03:00
Alexey Khit
8e7c9d19e4 Fix H265 codec for bubble source 2023-07-24 14:11:28 +03:00
Alexey Khit
0f33ef0fc5 Add support MJPEG codec for HomeKit cameras 2023-07-23 22:35:53 +03:00
Alexey Khit
a14c87ad60 Code refactoring for MJPEG source 2023-07-23 22:35:31 +03:00
Alexey Khit
6d82b1ce89 Total rework HAP pkg and HomeKit source 2023-07-23 22:22:36 +03:00
Alexey Khit
d73e9f6bcf Fix custom OPUS params inside MP4 2023-07-23 22:19:35 +03:00
Alexey Khit
e6a87fbd69 Add RTSP SDP to stream info JSON 2023-07-23 22:18:54 +03:00
Alexey Khit
3defbd60db Add deadline handler for SRTP server 2023-07-23 17:20:58 +03:00
Alexey Khit
6e9574a1bd Fix receive SRTP with empty sessions 2023-07-23 17:08:14 +03:00
Alexey Khit
7005cd08f2 Improve mDNS handler 2023-07-23 17:07:12 +03:00
Alexey Khit
e94f338b77 Add error msg for producer empty medias 2023-07-23 17:04:19 +03:00
Alexey Khit
d6172587b3 Fix readme about first project in the World 2023-07-21 09:45:19 +03:00
214 changed files with 12150 additions and 8117 deletions

189
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,189 @@
name: Build and Push
on:
workflow_dispatch:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
build-binaries:
name: Build binaries
runs-on: ubuntu-latest
env: { CGO_ENABLED: 0 }
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with: { go-version: '1.21' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win64
uses: actions/upload-artifact@v3
with: { name: go2rtc_win64, path: go2rtc.exe }
- name: Build go2rtc_win32
env: { GOOS: windows, GOARCH: 386 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win32
uses: actions/upload-artifact@v3
with: { name: go2rtc_win32, path: go2rtc.exe }
- name: Build go2rtc_win_arm64
env: { GOOS: windows, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_win_arm64
uses: actions/upload-artifact@v3
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
- name: Build go2rtc_linux_amd64
env: { GOOS: linux, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_amd64
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_amd64, path: go2rtc }
- name: Build go2rtc_linux_i386
env: { GOOS: linux, GOARCH: 386 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_i386
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_i386, path: go2rtc }
- name: Build go2rtc_linux_arm64
env: { GOOS: linux, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_arm64
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_arm64, path: go2rtc }
- name: Build go2rtc_linux_arm
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_arm
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_arm, path: go2rtc }
- name: Build go2rtc_linux_armv6
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_armv6
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_armv6, path: go2rtc }
- name: Build go2rtc_linux_mipsel
env: { GOOS: linux, GOARCH: mipsle }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_linux_mipsel
uses: actions/upload-artifact@v3
with: { name: go2rtc_linux_mipsel, path: go2rtc }
- name: Build go2rtc_mac_amd64
env: { GOOS: darwin, GOARCH: amd64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_amd64
uses: actions/upload-artifact@v3
with: { name: go2rtc_mac_amd64, path: go2rtc }
- name: Build go2rtc_mac_arm64
env: { GOOS: darwin, GOARCH: arm64 }
run: go build -ldflags "-s -w" -trimpath
- name: Upload go2rtc_mac_arm64
uses: actions/upload-artifact@v3
with: { name: go2rtc_mac_arm64, path: go2rtc }
docker-master:
name: Build docker master
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: |
linux/amd64
linux/386
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
docker-hardware:
name: Build docker hardware
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta-hw
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
flavor: |
suffix=-hardware
latest=false
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,78 +0,0 @@
name: docker
on:
workflow_dispatch:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Docker meta Hardware
id: meta-hw
uses: docker/metadata-action@v4
with:
images: ${{ github.repository }}
flavor: |
suffix=-hardware
latest=false
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
type=match,pattern=v(.*),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: |
linux/amd64
linux/386
linux/arm/v7
linux/arm64/v8
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Hardware
uses: docker/build-push-action@v4
with:
context: .
file: hardware.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-hw.outputs.tags }}
labels: ${{ steps.meta-hw.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,99 +0,0 @@
name: release
on:
workflow_dispatch:
# push:
# tags:
# - 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate changelog
run: |
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
- name: install lipo
run: |
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
chmod +x /tmp/lipo
mv /tmp/lipo /usr/local/bin
- name: Build Go binaries
run: |
#!/bin/bash
export CGO_ENABLED=0
mkdir -p artifacts
export GOOS=windows
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_win64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=386
export FILENAME=artifacts/go2rtc_win32.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_win_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=linux
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_linux_amd64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=386
export FILENAME=artifacts/go2rtc_linux_i386
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_linux_arm64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm
export GOARM=7
export FILENAME=artifacts/go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=mipsle
export FILENAME=artifacts/go2rtc_linux_mipsel
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=darwin
export GOARCH=amd64
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
export GOOS=darwin
export GOARCH=arm64
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
export FILENAME=artifacts/go2rtc_mac_universal.zip
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 7z a -mx9 -sdel "$FILENAME" go2rtc
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ failure() }}
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Create GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: artifacts/*
generate_release_notes: true
name: Release ${{ env.RELEASE_VERSION }}
body_path: CHANGELOG.md
draft: false
prerelease: false

View File

@@ -1,11 +1,11 @@
name: Test Build and Run
on:
push:
branches:
- '*'
pull_request:
merge_group:
# push:
# branches:
# - '*'
# pull_request:
# merge_group:
workflow_dispatch:
jobs:
@@ -26,7 +26,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.20'
go-version: '1.21'
- name: Build Go binary
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc

5
.gitignore vendored
View File

@@ -1,8 +1,7 @@
.idea/
.tmp/
go2rtc.yaml
go2rtc.json
0_test.go

View File

@@ -2,7 +2,7 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.20"
ARG GO_VERSION="1.21"
ARG NGROK_VERSION="3"
FROM python:${PYTHON_VERSION}-alpine AS base

107
README.md
View File

@@ -3,6 +3,7 @@
[![](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers)
[![](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc)
[![](https://img.shields.io/github/downloads/AlexxIT/go2rtc/total?color=blue&style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/releases)
[![](https://goreportcard.com/badge/github.com/AlexxIT/go2rtc)](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
@@ -12,9 +13,9 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [DVRIP](#source-dvrip), [HTTP](#source-http) (FLV/MJPEG/JPEG/TS), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming from any sources, supported by [FFmpeg](#source-ffmpeg)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4), [HomeKit](#module-homekit) [HLS](#module-hls) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- first project in the World with support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- play audio files and live streams on some cameras with [speaker](#stream-to-camera)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
@@ -40,6 +41,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [go2rtc: Docker](#go2rtc-docker)
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
* [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)
* [go2rtc: Dev version](#go2rtc-dev-version)
* [Configuration](#configuration)
* [Module: Streams](#module-streams)
* [Two way audio](#two-way-audio)
@@ -55,6 +57,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Source: Bubble](#source-bubble)
* [Source: DVRIP](#source-dvrip)
* [Source: Tapo](#source-tapo)
* [Source: Kasa](#source-kasa)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Source: ISAPI](#source-isapi)
@@ -67,6 +70,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: WebRTC](#module-webrtc)
* [Module: HomeKit](#module-homekit)
* [Module: WebTorrent](#module-webtorrent)
* [Module: Ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
@@ -110,7 +114,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
@@ -133,6 +137,14 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any [Home Assistant installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.
### go2rtc: Dev version
Latest, but maybe unstable version:
- Binary: GitHub > [Actions](https://github.com/AlexxIT/go2rtc/actions) > [Build and Push](https://github.com/AlexxIT/go2rtc/actions/workflows/build.yml) > latest run > Artifacts section (you should be logged in to GitHub)
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
## Configuration
- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory
@@ -175,6 +187,7 @@ Available source types:
- [bubble](#source-bubble) - streaming from ESeeCloud/dvr163 NVR
- [dvrip](#source-dvrip) - streaming from DVR-IP NVR
- [tapo](#source-tapo) - TP-Link Tapo cameras with [two way audio](#two-way-audio) support
- [kasa](#source-tapo) - TP-Link Kasa cameras
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
- [isapi](#source-isapi) - two way audio for Hikvision (ISAPI) cameras
@@ -216,12 +229,22 @@ streams:
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](#source-ffmpeg)
**Other options**
Format: `rtsp...#{param1}#{param2}#{param3}`
- Add custom timeout `#timeout=30` (in seconds)
- Ignore audio - `#media=video` or ignore video - `#media=audio`
- Ignore two way audio API `#backchannel=0` - important for some glitchy cameras
- Use WebSocket transport `#transport=ws...`
**RTSP over WebSocket**
```yaml
@@ -265,6 +288,9 @@ streams:
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
tcp_magic: tcp://192.168.1.123:12345
# Add custom header
custom_header: "https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX"
```
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
@@ -413,9 +439,8 @@ If you see a device but it does not have a pair button - it is paired to some ec
**Important:**
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violation
- Audio can be transcoded by [ffmpeg](#source-ffmpeg) source with `#async` option
- Audio can be played by `ffplay` with `-use_wallclock_as_timestamps 1 -async 1` options
- Audio can't be played in `VLC` and probably any other player
- Audio should be transcoded for using with MSE, WebRTC, etc.
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
@@ -423,7 +448,7 @@ Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
streams:
aqara_g3:
- hass:Camera-Hub-G3-AB12
- ffmpeg:aqara_g3#audio=aac#audio=opus#async
- ffmpeg:aqara_g3#audio=aac#audio=opus
```
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
@@ -472,6 +497,15 @@ streams:
camera2: tapo://admin:MD5-PASSWORD-HASH@192.168.1.123
```
#### Source: Kasa
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
```yaml
streams:
kasa: kasa://user:pass@192.168.1.123:19443/https/stream/mixed
```
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
@@ -562,6 +596,10 @@ This source type support four connection formats.
This format is only supported in go2rtc. Unlike WHEP it supports asynchronous WebRTC connection and two way audio.
**openipc**
Support connection to [OpenIPC](https://openipc.org/) cameras.
**wyze**
Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC protocol. You can use [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge) project to get connection credentials.
@@ -574,11 +612,12 @@ Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-str
streams:
webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1
webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1
webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}]
webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze
webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]
```
**PS.** For `wyze` and `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
**PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language.
#### Source: WebTorrent
@@ -781,6 +820,58 @@ webrtc:
credential: your_pass
```
### Module: HomeKit
HomeKit module can work in two modes:
- export any H264 camera to Apple HomeKit
- transparent proxy any Apple HomeKit camera (Aqara, Eve, Eufy, etc.) back to Apple HomeKit, so you will have all camera features in Apple Home and also will have RTSP/WebRTC/MP4/etc. from your HomeKit camera
**Important**
- HomeKit cameras supports only H264 video and OPUS audio
**Minimal config**
```yaml
streams:
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
homekit:
dahua1: # same stream ID from streams list, default PIN - 195502224
```
**Full config**
```yaml
streams:
dahua1:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
- ffmpeg:dahua1#video=h264#hardware # if your camera doesn't support H264, important for HomeKit
- ffmpeg:dahua1#audio=opus # only OPUS audio supported by HomeKit
homekit:
dahua1: # same stream ID from streams list
pin: 12345678 # custom PIN, default: 195502224
name: Dahua camera # custom camera name, default: generated from stream ID
device_id: dahua1 # custom ID, default: generated from stream ID
device_private: dahua1 # custom key, default: generated from stream ID
```
**Proxy HomeKit camera**
- Video stream from HomeKit camera to Apple device (iPhone, AppleTV) will be transmitted directly
- Video stream from HomeKit camera to RTSP/WebRTC/MP4/etc. will be transmitted via go2rtc
```yaml
streams:
aqara1:
- homekit://...
- ffmpeg:aqara1#audio=aac#audio=opus # optional audio transcoding
homekit:
aqara1: # same stream ID from streams list
```
### Module: WebTorrent
This module support:
@@ -1151,7 +1242,7 @@ streams:
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol realisation, many bugs in SDP
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best HTTP-FLV alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 202 KiB

39
go.mod
View File

@@ -1,33 +1,30 @@
module github.com/AlexxIT/go2rtc
go 1.20
go 1.21
require (
github.com/brutella/hap v0.0.17
github.com/deepch/vdk v0.0.19
github.com/gorilla/websocket v1.5.0
github.com/miekg/dns v1.1.55
github.com/pion/ice/v2 v2.3.9
github.com/pion/ice/v2 v2.3.10
github.com/pion/interceptor v0.1.17
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.7.13
github.com/pion/rtp v1.8.1
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.15
github.com/pion/srtp/v2 v2.0.16
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.12
github.com/rs/zerolog v1.29.1
github.com/pion/webrtc/v3 v3.2.17
github.com/rs/zerolog v1.30.0
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.4
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
golang.org/x/crypto v0.12.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.9 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
@@ -36,22 +33,12 @@ require (
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.7 // indirect
github.com/pion/sctp v1.8.8 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect
github.com/pion/turn/v2 v2.1.2 // indirect
github.com/pion/turn/v2 v2.1.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.11.0 // indirect
)
replace (
// RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
// fix reading AAC config bytes
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/tools v0.12.0 // indirect
)

80
go.sum
View File

@@ -1,18 +1,9 @@
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/brutella/dnssd v1.2.9 h1:eUqO0qXZAMaFN4W4Ms1AAO/OtAbNoh9U87GAlN+1FCs=
github.com/brutella/dnssd v1.2.9/go.mod h1:yZ+GHHbGhtp5yJeKTnppdFGiy6OhiPoxs0WHW1KUcFA=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -28,8 +19,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -46,7 +38,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -62,8 +53,8 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/ice/v2 v2.3.9 h1:7yZpHf3PhPxJGT4JkMj1Y8Rl5cQ6fB709iz99aeMd/U=
github.com/pion/ice/v2 v2.3.9/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
github.com/pion/ice/v2 v2.3.10 h1:T3bUJKqh7pGEdMyTngUcTeQd6io9X8JjgsVWZDannnY=
github.com/pion/ice/v2 v2.3.10/go.mod h1:hHGCibDfmXGqukayQw979xEctASp2Pe5Oe0iDU8pRus=
github.com/pion/interceptor v0.1.17 h1:prJtgwFh/gB8zMqGZoOgJPHivOwVAp61i2aG61Du/1w=
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -74,34 +65,35 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.8.0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.1 h1:26OxTc6lKg/qLSGir5agLyj0QKaOv8OP5wps2SFnVNQ=
github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U=
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.15 h1:+tqRtXGsGwHC0G0IUIAzRmdkHvriF79IHVfZGfHrQoA=
github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw=
github.com/pion/srtp/v2 v2.0.16 h1:impT2XBrHKsDpXr1x5hHIRydwssrSWKpmw3KvSfXbso=
github.com/pion/srtp/v2 v2.0.16/go.mod h1:NCLCV+U+NpxQ+vXhfOETet4OgKioIgrFjZmIM3ldJYE=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/turn/v2 v2.1.2 h1:wj0cAoGKltaZ790XEGW9HwoUewqjliwmhtxCuB2ApyM=
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
github.com/pion/webrtc/v3 v3.2.12 h1:pVqz5NdtTqyhKIhMcXR8bPp709kCf9blyAhDjoVRLvA=
github.com/pion/webrtc/v3 v3.2.12/go.mod h1:/Oz6K95CGWaN+3No+Z0NYvgOPOr3aY8UyTlMm/dec3A=
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.17 h1:4ra4H3atxp02e891dz8ZOye2Rgfsv8E2VUksyS1EW28=
github.com/pion/webrtc/v3 v3.2.17/go.mod h1:stMj0DIIhmUF0yOSR02uPAoKapzYbDIthSwW/Uk+AGs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
@@ -110,7 +102,6 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDf
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -121,22 +112,17 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
@@ -147,10 +133,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
@@ -158,16 +141,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -178,9 +161,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -195,8 +176,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -205,7 +187,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -215,17 +197,14 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -245,7 +224,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -3,7 +3,7 @@
# 0. Prepare images
# only debian 12 (bookworm) has latest ffmpeg
ARG DEBIAN_VERSION="bookworm-slim"
ARG GO_VERSION="1.20-buster"
ARG GO_VERSION="1.21-bookworm"
ARG NGROK_VERSION="3"
FROM debian:${DEBIAN_VERSION} AS base

View File

@@ -50,7 +50,8 @@ func Init() {
HandleFunc("api/exit", exitHandler)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
var err error
ln, err = net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
@@ -75,7 +76,7 @@ func Init() {
go func() {
s := http.Server{}
s.Handler = Handler
if err = s.Serve(listener); err != nil {
if err = s.Serve(ln); err != nil {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
@@ -111,6 +112,13 @@ func Init() {
}
}
func Port() int {
if ln == nil {
return 0
}
return ln.Addr().(*net.TCPAddr).Port
}
const (
MimeJSON = "application/json"
MimeText = "text/plain"
@@ -192,6 +200,7 @@ func middlewareCORS(next http.Handler) http.Handler {
})
}
var ln net.Listener
var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
@@ -213,21 +222,30 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
os.Exit(code)
}
type Stream struct {
Name string `json:"name"`
URL string `json:"url"`
type Source struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Info string `json:"info,omitempty"`
URL string `json:"url,omitempty"`
Location string `json:"location,omitempty"`
}
func ResponseStreams(w http.ResponseWriter, streams []Stream) {
if len(streams) == 0 {
http.Error(w, "no streams", http.StatusNotFound)
func ResponseSources(w http.ResponseWriter, sources []*Source) {
if len(sources) == 0 {
http.Error(w, "no sources", http.StatusNotFound)
return
}
var response = struct {
Streams []Stream `json:"streams"`
Sources []*Source `json:"sources"`
}{
Streams: streams,
Sources: sources,
}
ResponseJSON(w, response)
}
func Error(w http.ResponseWriter, err error) {
log.Error().Err(err).Caller(1).Send()
http.Error(w, err.Error(), http.StatusInsufficientStorage)
}

View File

@@ -1,11 +1,12 @@
package api
import (
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
"io"
"net/http"
"os"
"github.com/AlexxIT/go2rtc/internal/app"
"gopkg.in/yaml.v3"
)
func configHandler(w http.ResponseWriter, r *http.Request) {
@@ -40,8 +41,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
}
} else {
// validate config
var tmp struct{}
if err = yaml.Unmarshal(data, &tmp); err != nil {
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@@ -1,15 +1,17 @@
package ws
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
)
func Init() {
@@ -101,13 +103,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
}
tr := &Transport{Request: r}
tr.OnWrite(func(msg any) {
tr.OnWrite(func(msg any) error {
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
if data, ok := msg.([]byte); ok {
_ = ws.WriteMessage(websocket.BinaryMessage, data)
return ws.WriteMessage(websocket.BinaryMessage, data)
} else {
_ = ws.WriteJSON(msg)
return ws.WriteJSON(msg)
}
})
@@ -147,11 +149,11 @@ type Transport struct {
wrmx sync.Mutex
onChange func()
onWrite func(msg any)
onWrite func(msg any) error
onClose []func()
}
func (t *Transport) OnWrite(f func(msg any)) {
func (t *Transport) OnWrite(f func(msg any) error) {
t.mx.Lock()
if t.onChange != nil {
t.onChange()
@@ -162,7 +164,7 @@ func (t *Transport) OnWrite(f func(msg any)) {
func (t *Transport) Write(msg any) {
t.wrmx.Lock()
t.onWrite(msg)
_ = t.onWrite(msg)
t.wrmx.Unlock()
}
@@ -200,3 +202,20 @@ func (t *Transport) WithContext(f func(ctx map[any]any)) {
f(t.ctx)
t.mx.Unlock()
}
func (t *Transport) Writer() io.Writer {
return &writer{t: t}
}
type writer struct {
t *Transport
}
func (w *writer) Write(p []byte) (n int, err error) {
w.t.wrmx.Lock()
if err = w.t.onWrite(p); err == nil {
n = len(p)
}
w.t.wrmx.Unlock()
return
}

View File

@@ -1,6 +1,7 @@
package app
import (
"errors"
"flag"
"fmt"
"io"
@@ -11,12 +12,12 @@ import (
"time"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
var Version = "1.6.2"
var Version = "1.7.0"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
@@ -81,6 +82,8 @@ func Init() {
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
migrateStore()
}
func NewLogger(format string, level string) zerolog.Logger {
@@ -123,6 +126,22 @@ func GetLogger(module string) zerolog.Logger {
return log.Logger
}
func PatchConfig(key string, value any, path ...string) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
// empty config is OK
b, _ := os.ReadFile(ConfigPath)
b, err := yaml.Patch(b, key, value, path...)
if err != nil {
return err
}
return os.WriteFile(ConfigPath, b, 0644)
}
// internal
type Config []string

35
internal/app/migrate.go Normal file
View File

@@ -0,0 +1,35 @@
package app
import (
"encoding/json"
"os"
"github.com/rs/zerolog/log"
)
func migrateStore() {
const name = "go2rtc.json"
data, _ := os.ReadFile(name)
if data == nil {
return
}
var store struct {
Streams map[string]string `json:"streams"`
}
if err := json.Unmarshal(data, &store); err != nil {
log.Warn().Err(err).Caller().Send()
return
}
for id, url := range store.Streams {
if err := PatchConfig(id, url, "streams"); err != nil {
log.Warn().Err(err).Caller().Send()
return
}
}
_ = os.Remove(name)
}

View File

@@ -1,61 +0,0 @@
package store
import (
"encoding/json"
"github.com/rs/zerolog/log"
"os"
)
const name = "go2rtc.json"
var store map[string]any
func load() {
data, _ := os.ReadFile(name)
if data != nil {
if err := json.Unmarshal(data, &store); err != nil {
// TODO: log
log.Warn().Err(err).Msg("[app] read storage")
}
}
if store == nil {
store = make(map[string]any)
}
}
func save() error {
data, err := json.Marshal(store)
if err != nil {
return err
}
return os.WriteFile(name, data, 0644)
}
func GetRaw(key string) any {
if store == nil {
load()
}
return store[key]
}
func GetDict(key string) map[string]any {
raw := GetRaw(key)
if raw != nil {
return raw.(map[string]any)
}
return make(map[string]any)
}
func Set(key string, v any) error {
if store == nil {
load()
}
store[key] = v
return save()
}

View File

@@ -3,9 +3,10 @@ package debug
import (
"bytes"
"fmt"
"github.com/AlexxIT/go2rtc/internal/api"
"net/http"
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
var stackSkip = [][]byte{
@@ -24,6 +25,9 @@ var stackSkip = [][]byte{
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
// homekit
[]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"),

View File

@@ -45,10 +45,10 @@ func apiDvrip(w http.ResponseWriter, r *http.Request) {
return
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}
func discover() ([]api.Stream, error) {
func discover() ([]*api.Source, error) {
addr := &net.UDPAddr{
Port: Port,
IP: net.IP{239, 255, 255, 250},
@@ -63,7 +63,7 @@ func discover() ([]api.Stream, error) {
go sendBroadcasts(conn)
var items []api.Stream
var items []*api.Source
for _, info := range getResponses(conn) {
if info.HostIP == "" || info.HostName == "" {
@@ -75,7 +75,7 @@ func discover() ([]api.Stream, error) {
continue
}
items = append(items, api.Stream{
items = append(items, &api.Source{
Name: info.HostName,
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
})

View File

@@ -2,28 +2,28 @@ package echo
import (
"bytes"
"os/exec"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/shell"
"os/exec"
)
func Init() {
log := app.GetLogger("echo")
streams.HandleFunc("echo", func(url string) (core.Producer, error) {
streams.RedirectFunc("echo", func(url string) (string, error) {
args := shell.QuoteSplit(url[5:])
b, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return nil, err
return "", err
}
b = bytes.TrimSpace(b)
log.Debug().Str("url", url).Msgf("[echo] %s", b)
return streams.GetProducer(string(b))
return string(b), nil
})
}

View File

@@ -5,6 +5,11 @@ import (
"encoding/hex"
"errors"
"fmt"
"os"
"os/exec"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -13,10 +18,6 @@ import (
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"os"
"os/exec"
"sync"
"time"
)
func Init() {
@@ -82,15 +83,7 @@ func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
return nil, err
}
client := magic.NewClient(r)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "exec active producer"
client.URL = url
return client, nil
return magic.Open(r)
}
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {

View File

@@ -1,9 +1,11 @@
package exec
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"bufio"
"io"
"os/exec"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// PipeCloser - return StdoutPipe that Kill cmd on Close call
@@ -13,14 +15,16 @@ func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) {
return nil, err
}
return pipeCloser{stdout, cmd}, nil
// add buffer for pipe reader to reduce syscall
return pipeCloser{bufio.NewReaderSize(stdout, core.BufferSize), stdout, cmd}, nil
}
type pipeCloser struct {
io.ReadCloser
io.Reader
io.Closer
cmd *exec.Cmd
}
func (p pipeCloser) Close() error {
return core.Any(p.ReadCloser.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
return core.Any(p.Closer.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
}

View File

@@ -1,12 +1,13 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os/exec"
"regexp"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
@@ -78,7 +79,7 @@ func initDevices() {
audios = append(audios, name)
}
streams = append(streams, api.Stream{
streams = append(streams, &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
})
}

View File

@@ -70,8 +70,9 @@ func initDevices() {
m := re.FindAllStringSubmatch(string(b), -1)
for _, i := range m {
size, _, _ := strings.Cut(i[4], " ")
stream := api.Stream{
Name: i[3] + " | " + i[4],
stream := &api.Source{
Name: i[3],
Info: i[4],
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
}
@@ -86,8 +87,9 @@ func initDevices() {
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
if err == nil {
stream := api.Stream{
stream := &api.Source{
Name: "ALSA default",
Info: " ",
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
}

View File

@@ -1,11 +1,12 @@
package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os/exec"
"regexp"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func queryToInput(query url.Values) string {
@@ -79,7 +80,7 @@ func initDevices() {
name := m[1]
kind := m[2]
stream := api.Stream{
stream := &api.Source{
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
}

View File

@@ -2,12 +2,13 @@ package device
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
)
func Init(bin string) {
@@ -39,13 +40,13 @@ func GetInput(src string) (string, error) {
var Bin string
var videos, audios []string
var streams []api.Stream
var streams []*api.Source
var runonce sync.Once
func apiDevices(w http.ResponseWriter, r *http.Request) {
runonce.Do(initDevices)
api.ResponseStreams(w, streams)
api.ResponseSources(w, streams)
}
func indexToItem(items []string, index string) string {

View File

@@ -1,7 +1,6 @@
package ffmpeg
import (
"errors"
"net/url"
"strings"
@@ -10,7 +9,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
)
@@ -27,12 +25,9 @@ func Init() {
defaults["global"] += " -v error"
}
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
args := parseArgs(url[7:]) // remove `ffmpeg:`
if args == nil {
return nil, errors.New("can't generate ffmpeg command")
}
return streams.GetProducer("exec:" + args.String())
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
args := parseArgs(url[7:])
return "exec:" + args.String(), nil
})
device.Init(defaults["bin"])
@@ -66,7 +61,7 @@ var defaults = map[string]string{
// https://github.com/pion/webrtc/issues/1514
// https://ffmpeg.org/ffmpeg-resampler.html
// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -min_comp 0",
"opus": "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",

View File

@@ -1,12 +1,13 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/http"
"os/exec"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog/log"
)
@@ -21,7 +22,7 @@ const (
func Init(bin string) {
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
api.ResponseStreams(w, ProbeAll(bin))
api.ResponseSources(w, ProbeAll(bin))
})
}
@@ -58,7 +59,7 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
args.Codecs[i] = defaults[name+"/"+engine]
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
@@ -78,7 +79,7 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
args.InsertFilter("format=vaapi|nv12,hwupload")
} else {
// enable software pixel for drawtext, scale and transpose
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 " + args.Input
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch " + args.Input
args.AddFilter("hwupload")
}

View File

@@ -7,8 +7,8 @@ import (
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeVideoToolboxH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,

View File

@@ -1,8 +1,9 @@
package hardware
import (
"github.com/AlexxIT/go2rtc/internal/api"
"runtime"
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
@@ -13,9 +14,9 @@ const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf form
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
func ProbeAll(bin string) []api.Stream {
func ProbeAll(bin string) []*api.Source {
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
return []api.Stream{
return []*api.Source{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
@@ -27,7 +28,7 @@ func ProbeAll(bin string) []api.Stream {
}
}
return []api.Stream{
return []*api.Source{
{
Name: runToString(bin, ProbeVAAPIH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,

View File

@@ -8,8 +8,8 @@ const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeDXVA2H264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,

View File

@@ -12,22 +12,36 @@ import (
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func TranscodeToJPEG(b []byte, query url.Values) ([]byte, error) {
ffmpegArgs := parseQuery(query)
cmdArgs := shell.QuoteSplit(ffmpegArgs.String())
func JPEGWithQuery(b []byte, query url.Values) ([]byte, error) {
args := parseQuery(query)
return transcode(b, args.String())
}
func JPEGWithScale(b []byte, width, height int) ([]byte, error) {
args := defaultArgs()
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
return transcode(b, args.String())
}
func transcode(b []byte, args string) ([]byte, error) {
cmdArgs := shell.QuoteSplit(args)
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}
func parseQuery(query url.Values) *ffmpeg.Args {
args := &ffmpeg.Args{
func defaultArgs() *ffmpeg.Args {
return &ffmpeg.Args{
Bin: defaults["bin"],
Global: defaults["global"],
Input: "-i -",
Codecs: []string{defaults["mjpeg"]},
Output: defaults["output/mjpeg"],
}
}
func parseQuery(query url.Values) *ffmpeg.Args {
args := defaultArgs()
var width = -1
var height = -1

View File

@@ -4,6 +4,11 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"path"
"sync"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/roborock"
@@ -11,10 +16,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hass"
"github.com/rs/zerolog"
"net/http"
"os"
"path"
"sync"
)
func Init() {
@@ -36,6 +37,24 @@ func Init() {
api.HandleFunc("/streams", apiOK)
api.HandleFunc("/stream/", apiStream)
streams.RedirectFunc("hass", func(url string) (string, error) {
if location := entities[url[5:]]; location != "" {
return location, nil
}
return "", nil
})
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
// support hass://supervisor?entity_id=camera.driveway_doorbell
client, err := hass.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
})
// load static entries from Hass config
if err := importConfig(conf.Mod.Config); err != nil {
log.Debug().Msgf("[hass] can't import config: %s", err)
@@ -56,26 +75,13 @@ func Init() {
}
})
var items []api.Stream
var items []*api.Source
for name, url := range entities {
items = append(items, api.Stream{Name: name, URL: url})
items = append(items, &api.Source{
Name: name, URL: "hass:" + name, Location: url,
})
}
api.ResponseStreams(w, items)
})
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
// check entity by name
if url2 := entities[url[5:]]; url2 != "" {
return streams.GetProducer(url2)
}
// support hass://supervisor?entity_id=camera.driveway_doorbell
client, err := hass.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
api.ResponseSources(w, items)
})
// for Addon listen on hassio interface, so WebUI feature will work

View File

@@ -2,7 +2,6 @@ package hls
import (
"net/http"
"strings"
"sync"
"time"
@@ -33,21 +32,12 @@ func Init() {
ws.HandleFunc("hls", handlerWSHLS)
}
type Consumer interface {
core.Consumer
Listen(f core.EventFunc)
Init() ([]byte, error)
MimeCodecs() string
Start()
}
var log zerolog.Logger
const keepalive = 5 * time.Second
var sessions = map[string]*Session{}
// once I saw 404 on MP4 segment, so better to use mutex
var sessions = map[string]*Session{}
var sessionsMu sync.RWMutex
func handlerStream(w http.ResponseWriter, r *http.Request) {
@@ -67,22 +57,22 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
return
}
var cons Consumer
var cons core.Consumer
// use fMP4 with codecs filter and TS without
medias := mp4.ParseQuery(r.URL.Query())
if medias != nil {
cons = &mp4.Consumer{
Desc: "HLS/HTTP",
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: medias,
}
c := mp4.NewConsumer(medias)
c.Type = "HLS/fMP4 consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
cons = c
} else {
cons = &mpegts.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
c := mpegts.NewConsumer()
c.Type = "HLS/TS consumer"
c.RemoteAddr = tcp.RemoteAddr(r)
c.UserAgent = r.UserAgent()
cons = c
}
if err := stream.AddConsumer(cons); err != nil {
@@ -90,63 +80,22 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
return
}
session := &Session{cons: cons}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.buffer = append(session.buffer, data...)
session.mu.Unlock()
}
})
sid := core.RandString(8, 62)
session := NewSession(cons)
session.alive = time.AfterFunc(keepalive, func() {
sessionsMu.Lock()
delete(sessions, sid)
delete(sessions, session.id)
sessionsMu.Unlock()
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
// two segments important for Chromecast
if medias != nil {
session.template = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d`
} else {
session.template = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.ts?id=` + sid + `&n=%d`
}
sessionsMu.Lock()
sessions[sid] = session
sessions[session.id] = session
sessionsMu.Unlock()
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, "fLaC", 1)
go session.Run()
// bandwidth important for Safari, codecs useful for smooth playback
data := []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid)
if _, err := w.Write(data); err != nil {
if _, err := w.Write(session.Main()); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -169,7 +118,7 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
return
}
if _, err := w.Write([]byte(session.Playlist())); err != nil {
if _, err := w.Write(session.Playlist()); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -224,11 +173,8 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
return
}
data := session.init
session.init = nil
session.segment0 = session.Segment()
if session.segment0 == nil {
data := session.Init()
if data == nil {
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
http.NotFound(w, r)
return
@@ -261,14 +207,7 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
session.alive.Reset(keepalive)
var data []byte
if query.Get("n") != "0" {
data = session.Segment()
} else {
data = session.segment0
}
data := session.Segment()
if data == nil {
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
http.NotFound(w, r)

View File

@@ -2,23 +2,105 @@ package hls
import (
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
)
type Session struct {
cons Consumer
cons core.Consumer
id string
template string
init []byte
segment0 []byte
buffer []byte
seq int
alive *time.Timer
mu sync.Mutex
}
func (s *Session) Playlist() string {
return fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1)
func NewSession(cons core.Consumer) *Session {
s := &Session{
id: core.RandString(8, 62),
cons: cons,
}
// two segments important for Chromecast
if _, ok := cons.(*mp4.Consumer); ok {
s.template = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + s.id + `"
#EXTINF:0.500,
segment.m4s?id=` + s.id + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + s.id + `&n=%d`
} else {
s.template = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXTINF:0.500,
segment.ts?id=` + s.id + `&n=%d
#EXTINF:0.500,
segment.ts?id=` + s.id + `&n=%d`
}
return s
}
func (s *Session) Write(p []byte) (n int, err error) {
s.mu.Lock()
if s.init == nil {
s.init = p
} else {
s.buffer = append(s.buffer, p...)
}
s.mu.Unlock()
return len(p), nil
}
func (s *Session) Run() {
_, _ = s.cons.(io.WriterTo).WriteTo(s)
}
func (s *Session) Main() []byte {
type withCodecs interface {
Codecs() []*core.Codec
}
codecs := mp4.MimeCodecs(s.cons.(withCodecs).Codecs())
codecs = strings.Replace(codecs, mp4.MimeFlac, "fLaC", 1)
// bandwidth important for Safari, codecs useful for smooth playback
return []byte(`#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + s.id)
}
func (s *Session) Playlist() []byte {
return []byte(fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1))
}
func (s *Session) Init() (init []byte) {
for i := 0; i < 60 && init == nil; i++ {
if i > 0 {
time.Sleep(50 * time.Millisecond)
}
s.mu.Lock()
// return init only when have some buffer
if len(s.buffer) > 0 {
init = s.init
}
s.mu.Unlock()
}
return
}
func (s *Session) Segment() (segment []byte) {
@@ -30,8 +112,12 @@ func (s *Session) Segment() (segment []byte) {
s.mu.Lock()
if len(s.buffer) > 0 {
segment = s.buffer
// for TS important to start new segment with init
s.buffer = s.init
if _, ok := s.cons.(*mp4.Consumer); ok {
s.buffer = nil
} else {
// for TS important to start new segment with init
s.buffer = s.init
}
s.seq++
}
s.mu.Unlock()

View File

@@ -2,13 +2,11 @@ package hls
import (
"errors"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
@@ -20,63 +18,37 @@ func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
}
codecs := msg.String()
medias := mp4.ParseCodecs(codecs, true)
cons := mp4.NewConsumer(medias)
cons.Type = "HLS/fMP4 consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
cons := &mp4.Consumer{
Desc: "HLS/WebSocket",
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
Medias: mp4.ParseCodecs(codecs, true),
}
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
session := &Session{cons: cons}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.buffer = append(session.buffer, data...)
session.mu.Unlock()
}
})
session := NewSession(cons)
session.alive = time.AfterFunc(keepalive, func() {
sessionsMu.Lock()
delete(sessions, session.id)
sessionsMu.Unlock()
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
sid := core.RandString(8, 62)
// two segments important for Chromecast
session.template = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d`
sessionsMu.Lock()
sessions[sid] = session
sessions[session.id] = session
sessionsMu.Unlock()
codecs = strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, "fLaC", 1)
go session.Run()
// bandwidth important for Safari, codecs useful for smooth playback
data := `#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid
tr.Write(&ws.Message{Type: "hls", Value: data})
main := session.Main()
tr.Write(&ws.Message{Type: "hls", Value: string(main)})
return nil
}

View File

@@ -1,136 +1,138 @@
package homekit
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/mdns"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
items := make([]any, 0)
for name, src := range store.GetDict("streams") {
if src := src.(string); strings.HasPrefix(src, "homekit") {
u, err := url.Parse(src)
if err != nil {
continue
}
device := Device{
Name: name,
Addr: u.Host,
Paired: true,
}
items = append(items, device)
}
}
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
if entry.Complete() {
device := Device{
Name: entry.Name,
Addr: entry.Addr(),
ID: entry.Info["id"],
Model: entry.Info["md"],
Paired: entry.Info["sf"] == "0",
}
items = append(items, device)
}
return false
})
sources, err := discovery()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
api.Error(w, err)
return
}
api.ResponseJSON(w, items)
urls := findHomeKitURLs()
for id, u := range urls {
deviceID := u.Query().Get("device_id")
for _, source := range sources {
if strings.Contains(source.URL, deviceID) {
source.Location = id
break
}
}
}
for _, source := range sources {
if source.Location == "" {
source.Location = " "
}
}
api.ResponseSources(w, sources)
case "POST":
// TODO: post params...
if err := r.ParseMultipartForm(1024); err != nil {
api.Error(w, err)
return
}
id := r.URL.Query().Get("id")
pin := r.URL.Query().Get("pin")
name := r.URL.Query().Get("name")
if err := hkPair(id, pin, name); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
api.Error(w, err)
}
case "DELETE":
src := r.URL.Query().Get("src")
if err := hkDelete(src); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
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)
}
}
}
func hkPair(deviceID, pin, name string) (err error) {
var conn *hap.Conn
func discovery() ([]*api.Source, error) {
var sources []*api.Source
if conn, err = hap.Pair(deviceID, pin); err != nil {
return
}
// 1. Get streams from Discovery
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
log.Trace().Msgf("[homekit] mdns=%s", entry)
streams.New(name, conn.URL())
dict := store.GetDict("streams")
dict[name] = conn.URL()
return store.Set("streams", dict)
}
func hkDelete(name string) (err error) {
dict := store.GetDict("streams")
for key, rawURL := range dict {
if key != name {
continue
}
var conn *hap.Conn
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
return
}
if err = conn.Dial(); err != nil {
return
}
go func() {
if err = conn.Handle(); err != nil {
log.Warn().Err(err).Caller().Send()
if entry.Complete() && entry.Info[hap.TXTCategory] == hap.CategoryCamera {
source := &api.Source{
Name: entry.Name,
Info: entry.Info[hap.TXTModel],
URL: fmt.Sprintf(
"homekit://%s:%d?device_id=%s&feature=%s&status=%s",
entry.IP, entry.Port, entry.Info[hap.TXTDeviceID],
entry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags],
),
}
}()
if err = conn.ListPairings(); err != nil {
return
sources = append(sources, source)
}
return false
})
if err = conn.DeletePairing(conn.ClientID); err != nil {
log.Error().Err(err).Caller().Send()
}
delete(dict, name)
return store.Set("streams", dict)
if err != nil {
return nil, err
}
return nil
return sources, nil
}
type Device struct {
ID string `json:"id"`
Name string `json:"name"`
Addr string `json:"addr"`
Model string `json:"model"`
Paired bool `json:"paired"`
//Type string `json:"type"`
func apiPair(id, url string) error {
conn, err := hap.Pair(url)
if err != nil {
return err
}
streams.New(id, conn.URL())
return app.PatchConfig(id, conn.URL(), "streams")
}
func apiUnpair(id string) error {
stream := streams.Get(id)
if stream == nil {
return errors.New(api.StreamNotFound)
}
rawURL := findHomeKitURL(stream)
if rawURL == "" {
return errors.New("not homekit source")
}
if err := hap.Unpair(rawURL); err != nil {
return err
}
streams.Delete(id)
return app.PatchConfig(id, nil, "streams")
}
func findHomeKitURLs() map[string]*url.URL {
urls := map[string]*url.URL{}
for id, stream := range streams.Streams() {
if rawURL := findHomeKitURL(stream); rawURL != "" {
if u, err := url.Parse(rawURL); err == nil {
urls[id] = u
}
}
}
return urls
}

View File

@@ -1,32 +1,197 @@
package homekit
import (
"io"
"net"
"net/http"
"strings"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod map[string]struct {
Pin string `json:"pin"`
Name string `json:"name"`
DeviceID string `json:"device_id"`
DevicePrivate string `json:"device_private"`
Pairings []string `json:"pairings"`
//Listen string `json:"listen"`
} `yaml:"homekit"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHandler)
if cfg.Mod == nil {
return
}
servers = map[string]*server{}
var entries []*mdns.ServiceEntry
for id, conf := range cfg.Mod {
stream := streams.Get(id)
if stream == nil {
log.Warn().Msgf("[homekit] missing stream: %s", id)
continue
}
if conf.Pin == "" {
conf.Pin = "19550224" // default PIN
}
pin, err := hap.SanitizePin(conf.Pin)
if err != nil {
log.Error().Err(err).Caller().Send()
continue
}
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
name := calcName(conf.Name, deviceID)
srv := &server{
stream: id,
srtp: srtp.Server,
pairings: conf.Pairings,
}
srv.hap = &hap.Server{
Pin: pin,
DeviceID: deviceID,
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
GetPair: srv.GetPair,
AddPair: srv.AddPair,
Handler: homekit.ServerHandler(srv),
}
if url := findHomeKitURL(stream); url != "" {
// 1. Act as transparent proxy for HomeKit camera
dial := func() (net.Conn, error) {
client, err := homekit.Dial(url, srtp.Server)
if err != nil {
return nil, err
}
return client.Conn(), nil
}
srv.hap.Handler = homekit.ProxyHandler(srv, dial)
} else {
// 2. Act as basic HomeKit camera
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
srv.hap.Handler = homekit.ServerHandler(srv)
}
srv.mdns = &mdns.ServiceEntry{
Name: name,
Port: uint16(api.Port()),
Info: map[string]string{
hap.TXTConfigNumber: "1",
hap.TXTFeatureFlags: "0",
hap.TXTDeviceID: deviceID,
hap.TXTModel: app.UserAgent,
hap.TXTProtoVersion: "1.1",
hap.TXTStateNumber: "1",
hap.TXTStatusFlags: hap.StatusNotPaired,
hap.TXTCategory: hap.CategoryCamera,
hap.TXTSetupHash: srv.hap.SetupHash(),
},
}
entries = append(entries, srv.mdns)
srv.UpdateStatus()
host := srv.mdns.Host(mdns.ServiceHAP)
servers[host] = srv
}
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
log.Trace().Msgf("[homekit] mnds: %s", entries)
go func() {
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
log.Error().Err(err).Caller().Send()
}
}()
}
var log zerolog.Logger
var servers map[string]*server
func streamHandler(url string) (core.Producer, error) {
conn, err := homekit.NewClient(url, srtp.Server)
if err != nil {
return nil, err
}
if err = conn.Dial(); err != nil {
return nil, err
}
return conn, nil
return homekit.Dial(url, srtp.Server)
}
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
srv, ok := servers[r.Host]
if !ok {
log.Error().Msg("[homekit] unknown host: " + r.Host)
return
}
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
if err = srv.hap.PairSetup(r, rw, conn); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func hapPairVerify(w http.ResponseWriter, r *http.Request) {
srv, ok := servers[r.Host]
if !ok {
log.Error().Msg("[homekit] unknown host: " + r.Host)
return
}
conn, rw, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer conn.Close()
if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF {
log.Error().Err(err).Caller().Send()
}
}
func findHomeKitURL(stream *streams.Stream) string {
sources := stream.Sources()
if len(sources) == 0 {
return ""
}
url := sources[0]
if strings.HasPrefix(url, "homekit") {
return url
}
if strings.HasPrefix(url, "hass") {
location, _ := streams.Location(url)
if strings.HasPrefix(location, "homekit") {
return url
}
}
return ""
}

263
internal/homekit/server.go Normal file
View File

@@ -0,0 +1,263 @@
package homekit
import (
"crypto/ed25519"
"crypto/sha512"
"encoding/hex"
"fmt"
"net"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
srtp2 "github.com/AlexxIT/go2rtc/internal/srtp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/AlexxIT/go2rtc/pkg/srtp"
)
type server struct {
stream string // stream name from YAML
hap *hap.Server // server for HAP connection and encryption
mdns *mdns.ServiceEntry
srtp *srtp.Server
accessory *hap.Accessory // HAP accessory
pairings []string // pairings list
streams map[string]*homekit.Consumer
consumer *homekit.Consumer
}
func (s *server) UpdateStatus() {
// true status is important, or device may be offline in Apple Home
if len(s.pairings) == 0 {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired
} else {
s.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired
}
}
func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {
return []*hap.Accessory{s.accessory}
}
func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] get unknown characteristic: %d", iid)
return nil
}
switch char.Type {
case camera.TypeSetupEndpoints:
if s.consumer == nil {
return nil
}
answer := s.consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
if err != nil {
return nil
}
return v
}
return char.Value
}
func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value)
char := s.accessory.GetCharacterByID(iid)
if char == nil {
log.Warn().Msgf("[homekit] set unknown characteristic: %d", iid)
return
}
switch char.Type {
case camera.TypeSetupEndpoints:
var offer camera.SetupEndpoints
if err := tlv8.UnmarshalBase64(value.(string), &offer); err != nil {
return
}
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
s.consumer.SetOffer(&offer)
case camera.TypeSelectedStreamConfiguration:
var conf camera.SelectedStreamConfig
if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil {
return
}
log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command)
switch conf.Control.Command {
case camera.SessionCommandEnd:
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
_ = consumer.Stop()
}
case camera.SessionCommandStart:
if s.consumer == nil {
return
}
if !s.consumer.SetConfig(&conf) {
log.Warn().Msgf("[homekit] wrong config")
return
}
if s.streams == nil {
s.streams = map[string]*homekit.Consumer{}
}
s.streams[conf.Control.SessionID] = s.consumer
stream := streams.Get(s.stream)
if err := stream.AddConsumer(s.consumer); err != nil {
return
}
go func() {
_, _ = s.consumer.WriteTo(nil)
stream.RemoveConsumer(s.consumer)
delete(s.streams, conf.Control.SessionID)
}()
}
}
}
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height)
stream := streams.Get(s.stream)
cons := magic.NewKeyframe()
if err := stream.AddConsumer(cons); err != nil {
return nil
}
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
var err error
if b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil {
return nil
}
}
return b
}
func (s *server) GetPair(conn net.Conn, id string) []byte {
log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id)
for _, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
query, err := url.ParseQuery(pairing)
if err != nil {
continue
}
if query.Get("client_id") != id {
continue
}
s := query.Get("client_public")
b, _ := hex.DecodeString(s)
return b
}
return nil
}
func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) {
log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions)
query := url.Values{
"client_id": []string{id},
"client_public": []string{hex.EncodeToString(public)},
"permissions": []string{string('0' + permissions)},
}
s.pairings = append(s.pairings, query.Encode())
s.UpdateStatus()
s.PatchConfig()
}
func (s *server) DelPair(conn net.Conn, id string) {
log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id)
id = "client_id=" + id
for i, pairing := range s.pairings {
if !strings.Contains(pairing, id) {
continue
}
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
s.UpdateStatus()
s.PatchConfig()
break
}
}
func (s *server) PatchConfig() {
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
log.Error().Err(err).Msgf(
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
)
}
}
func calcName(name, seed string) string {
if name != "" {
return name
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("go2rtc-%02X%02X", b[0], b[2])
}
func calcDeviceID(deviceID, seed string) string {
if deviceID != "" {
if len(deviceID) >= 17 {
// 1. Returd device_id as is (ex. AA:BB:CC:DD:EE:FF)
return deviceID
}
// 2. Use device_id as seed if not zero
seed = deviceID
}
b := sha512.Sum512([]byte(seed))
return fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", b[32], b[34], b[36], b[38], b[40], b[42])
}
func calcDevicePrivate(private, seed string) []byte {
if private != "" {
// 1. Decode private from HEX string
if b, _ := hex.DecodeString(private); len(b) == ed25519.PrivateKeySize {
// 2. Return if OK
return b
}
// 3. Use private as seed if not zero
seed = private
}
b := sha512.Sum512([]byte(seed))
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
}

View File

@@ -2,17 +2,18 @@ package http
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hls"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/multipart"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
func Init() {
@@ -23,13 +24,24 @@ func Init() {
streams.HandleFunc("tcp", handleTCP)
}
func handleHTTP(url string) (core.Producer, error) {
func handleHTTP(rawURL string) (core.Producer, error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequest("GET", rawURL, nil)
if err != nil {
return nil, err
}
if rawQuery != "" {
query := streams.ParseQuery(rawQuery)
for _, header := range query["header"] {
key, value, _ := strings.Cut(header, ":")
req.Header.Add(key, strings.TrimSpace(value))
}
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
@@ -39,37 +51,29 @@ func handleHTTP(url string) (core.Producer, error) {
return nil, errors.New(res.Status)
}
// 1. Guess format from content type
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
switch ct {
case "image/jpeg", "multipart/x-mixed-replace":
var ext string
if i := strings.LastIndexByte(req.URL.Path, '.'); i > 0 {
ext = req.URL.Path[i+1:]
}
switch {
case ct == "image/jpeg":
return mjpeg.NewClient(res), nil
case "video/x-flv":
var conn *rtmp.Client
if conn, err = rtmp.Accept(res); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
case ct == "multipart/x-mixed-replace":
return multipart.Open(res.Body)
default: // "video/mpeg":
case ct == "application/vnd.apple.mpegurl" || ext == "m3u8":
return hls.OpenURL(req.URL, res.Body)
}
client := magic.NewClient(res.Body)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "HTTP active producer"
client.URL = url
return client, nil
return magic.Open(res.Body)
}
func handleTCP(rawURL string) (core.Producer, error) {
@@ -78,18 +82,10 @@ func handleTCP(rawURL string) (core.Producer, error) {
return nil, err
}
conn, err := net.DialTimeout("tcp", u.Host, time.Second*3)
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
if err != nil {
return nil, err
}
client := magic.NewClient(conn)
if err = client.Probe(); err != nil {
return nil, err
}
client.Desc = "TCP active producer"
client.URL = rawURL
return client, nil
return magic.Open(conn)
}

View File

@@ -33,27 +33,18 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
return
}
exit := make(chan []byte)
cons := &magic.Keyframe{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
if b, ok := msg.([]byte); ok {
select {
case exit <- b:
default:
}
}
})
cons := magic.NewKeyframe()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b := once.Buffer()
stream.RemoveConsumer(cons)
@@ -61,7 +52,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if data, err = ffmpeg.TranscodeToJPEG(data, r.URL.Query()); err != nil {
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -70,18 +61,16 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(data)))
h.Set("Content-Length", strconv.Itoa(len(b)))
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if _, err := w.Write(data); err != nil {
if _, err := w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handlerStream(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
outputMjpeg(w, r)
@@ -98,26 +87,9 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
return
}
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg any) {
switch msg := msg.(type) {
case []byte:
data := []byte(header + strconv.Itoa(len(msg)))
data = append(data, '\r', '\n', '\r', '\n')
data = append(data, msg...)
data = append(data, '\r', '\n')
// Chrome bug: mjpeg image always shows the second to last image
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
_, _ = w.Write(data)
flusher.Flush()
}
})
cons := mjpeg.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
@@ -130,11 +102,33 @@ func outputMjpeg(w http.ResponseWriter, r *http.Request) {
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
<-r.Context().Done()
wr := &writer{wr: w, buf: []byte(header)}
_, _ = cons.WriteTo(wr)
stream.RemoveConsumer(cons)
}
//log.Trace().Msg("[api.mjpeg] close")
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
type writer struct {
wr io.Writer
buf []byte
}
func (w *writer) Write(p []byte) (n int, err error) {
w.buf = w.buf[:len(header)]
w.buf = append(w.buf, strconv.Itoa(len(p))...)
w.buf = append(w.buf, "\r\n\r\n"...)
w.buf = append(w.buf, p...)
w.buf = append(w.buf, "\r\n"...)
// Chrome bug: mjpeg image always shows the second to last image
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
if n, err = w.wr.Write(w.buf); err == nil {
w.wr.(http.Flusher).Flush()
}
return
}
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
@@ -164,15 +158,9 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
cons := mjpeg.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
@@ -181,6 +169,8 @@ func handlerWS(tr *ws.Transport, _ *ws.Message) error {
tr.Write(&ws.Message{Type: "mjpeg"})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})

View File

@@ -47,18 +47,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
return
}
exit := make(chan []byte, 1)
cons := &mp4.Segment{OnlyKeyframe: true}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok && exit != nil {
select {
case exit <- data:
default:
}
exit = nil
}
})
cons := mp4.NewKeyframe(nil)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
@@ -66,20 +55,21 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
return
}
data := <-exit
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
stream.RemoveConsumer(cons)
// Apple Safari won't show frame without length
header := w.Header()
header.Set("Content-Length", strconv.Itoa(len(data)))
header.Set("Content-Type", cons.MimeType)
header.Set("Content-Length", strconv.Itoa(once.Len()))
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
if _, err := w.Write(data); err != nil {
if _, err := once.WriteTo(w); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -108,29 +98,11 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
exit := make(chan error, 1) // Add buffer to prevent blocking
cons := &mp4.Consumer{
Desc: "MP4/HTTP",
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: mp4.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg any) {
if exit == nil {
return
}
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil {
select {
case exit <- err:
default:
}
exit = nil
}
}
})
medias := mp4.ParseQuery(r.URL.Query())
cons := mp4.NewConsumer(medias)
cons.Type = "MP4/HTTP active consumer"
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
@@ -138,59 +110,36 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
defer stream.RemoveConsumer(cons)
if rotate := query.Get("rotate"); rotate != "" {
cons.Rotate = core.Atoi(rotate)
}
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
if scale := query.Get("scale"); scale != "" {
if sx, sy, ok := strings.Cut(scale, ":"); ok {
cons.ScaleX = core.Atoi(sx)
cons.ScaleY = core.Atoi(sy)
}
}
header := w.Header()
header.Set("Content-Type", cons.MimeType())
header.Set("Content-Type", mp4.ContentType(cons.Codecs()))
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
if rotate := query.Get("rotate"); rotate != "" {
mp4.PatchVideoRotate(data, core.Atoi(rotate))
}
if scale := query.Get("scale"); scale != "" {
if sx, sy, ok := strings.Cut(scale, ":"); ok {
mp4.PatchVideoScale(data, core.Atoi(sx), core.Atoi(sy))
}
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cons.Start()
var duration *time.Timer
if s := query.Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
if exit != nil {
select {
case exit <- nil:
default:
}
exit = nil
}
_ = cons.Stop()
})
}
}
err = <-exit
exit = nil
_, _ = cons.WriteTo(w)
log.Trace().Err(err).Caller().Send()
stream.RemoveConsumer(cons)
if duration != nil {
duration.Stop()

View File

@@ -6,6 +6,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
)
@@ -16,44 +17,30 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Consumer{
Desc: "MSE/WebSocket",
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
var medias []*core.Media
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
cons.Medias = mp4.ParseCodecs(codecs, true)
medias = mp4.ParseCodecs(codecs, true)
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
cons := mp4.NewConsumer(medias)
cons.Type = "MSE/WebSocket active consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Debug().Err(err).Msg("[mp4] add consumer")
return err
}
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
tr.Write(&ws.Message{Type: "mse", Value: cons.MimeType()})
data, err := cons.Init()
if err != nil {
log.Warn().Err(err).Caller().Send()
return err
}
tr.Write(data)
cons.Start()
return nil
}
@@ -63,29 +50,25 @@ func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Segment{
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
OnlyKeyframe: true,
}
var medias []*core.Media
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
cons.Medias = mp4.ParseCodecs(codecs, false)
medias = mp4.ParseCodecs(codecs, false)
}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
cons := mp4.NewKeyframe(medias)
cons.Type = "MP4/WebSocket active consumer"
cons.RemoteAddr = tcp.RemoteAddr(tr.Request)
cons.UserAgent = tr.Request.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.Write(&ws.Message{Type: "mp4", Value: cons.MimeType})
tr.Write(&ws.Message{Type: "mse", Value: mp4.ContentType(cons.Codecs())})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)

36
internal/mpegts/aac.go Normal file
View File

@@ -0,0 +1,36 @@
package mpegts
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := aac.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "audio/aac")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}

View File

@@ -1,22 +1,54 @@
package mpegts
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"net/http"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
)
func Init() {
api.HandleFunc("api/stream.ts", apiHandle)
api.HandleFunc("api/stream.aac", apiStreamAAC)
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusMethodNotAllowed)
outputMpegTS(w, r)
} else {
inputMpegTS(w, r)
}
}
func outputMpegTS(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := mpegts.NewConsumer()
cons.RemoteAddr = tcp.RemoteAddr(r)
cons.UserAgent = r.UserAgent()
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "video/mp2t")
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}
func inputMpegTS(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
@@ -25,16 +57,15 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
}
res := &http.Response{Body: r.Body, Request: r}
client := mpegts.NewClient(res)
if err := client.Handle(); err != nil {
client, err := mpegts.Open(res.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
if err := client.Handle(); err != nil {
if err = client.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@@ -1,11 +1,12 @@
package nest
import (
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/nest"
"net/http"
)
func Init() {
@@ -41,15 +42,15 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
return
}
var items []api.Stream
var items []*api.Source
for name, deviceID := range devices {
query.Set("device_id", deviceID)
items = append(items, api.Stream{
items = append(items, &api.Source{
Name: name, URL: "nest:?" + query.Encode(),
})
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}

View File

@@ -1,13 +1,6 @@
package onvif
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/onvif"
"github.com/rs/zerolog"
"io"
"net"
"net/http"
@@ -15,6 +8,14 @@ import (
"os"
"strconv"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/onvif"
"github.com/rs/zerolog"
)
func Init() {
@@ -121,7 +122,7 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
func apiOnvif(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
var items []api.Stream
var items []*api.Source
if src == "" {
urls, err := onvif.DiscoveryStreamingURLs()
@@ -149,7 +150,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
u.Path = ""
}
items = append(items, api.Stream{Name: u.Host, URL: u.String()})
items = append(items, &api.Source{Name: u.Host, URL: u.String()})
}
} else {
client, err := onvif.NewClient(src)
@@ -176,19 +177,19 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
}
for i, token := range tokens {
items = append(items, api.Stream{
items = append(items, &api.Source{
Name: name + " stream" + strconv.Itoa(i),
URL: src + "?subtype=" + token,
})
}
if len(tokens) > 0 && client.HasSnapshots() {
items = append(items, api.Stream{
items = append(items, &api.Source{
Name: name + " snapshot",
URL: src + "?subtype=" + tokens[0] + "&snapshot",
})
}
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}

View File

@@ -2,11 +2,12 @@ package roborock
import (
"fmt"
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/roborock"
"net/http"
)
func Init() {
@@ -84,7 +85,7 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
return
}
var items []api.Stream
var items []*api.Source
for _, device := range devices {
source := fmt.Sprintf(
@@ -93,8 +94,8 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
Auth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,
device.DID, device.Key,
)
items = append(items, api.Stream{Name: device.Name, URL: source})
items = append(items, &api.Source{Name: device.Name, URL: source})
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}

View File

@@ -1,30 +1,31 @@
package rtmp
import (
"io"
"net/http"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/flv"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/rs/zerolog/log"
"io"
"net/http"
)
func Init() {
streams.HandleFunc("rtmp", streamsHandle)
streams.HandleFunc("rtmps", streamsHandle)
streams.HandleFunc("rtmpx", streamsHandle)
api.HandleFunc("api/stream.flv", apiHandle)
}
func streamsHandle(url string) (core.Producer, error) {
conn := rtmp.NewClient(url)
if err := conn.Dial(); err != nil {
client, err := rtmp.Dial(url)
if err != nil {
return nil, err
}
if err := conn.Describe(); err != nil {
return nil, err
}
return conn, nil
return client, nil
}
func apiHandle(w http.ResponseWriter, r *http.Request) {
@@ -40,18 +41,12 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
return
}
res := &http.Response{Body: r.Body, Request: r}
client, err := rtmp.Accept(res)
client, err := flv.Open(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = client.Describe(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
stream.AddProducer(client)
if err = client.Start(); err != nil && err != io.EOF {

View File

@@ -101,6 +101,8 @@ func rtspHandler(rawURL string) (core.Producer, error) {
if rawQuery != "" {
query := streams.ParseQuery(rawQuery)
conn.Backchannel = query.Get("backchannel") == "1"
conn.Media = query.Get("media")
conn.Timeout = core.Atoi(query.Get("timeout"))
conn.Transport = query.Get("transport")
}

View File

@@ -1,8 +1,6 @@
package srtp
import (
"net"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
)
@@ -24,23 +22,8 @@ func Init() {
return
}
log := app.GetLogger("srtp")
// create SRTP server (endpoint) for receiving video from HomeKit camera
conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
if err != nil {
log.Warn().Err(err).Caller().Send()
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
// run server
go func() {
Server = &srtp.Server{}
if err = Server.Serve(conn); err != nil {
log.Warn().Err(err).Caller().Send()
}
}()
// create SRTP server (endpoint) for receiving video from HomeKit cameras
Server = srtp.NewServer(cfg.Mod.Listen)
}
var Server *srtp.Server

View File

@@ -0,0 +1,149 @@
package streams
import (
"errors"
"strings"
"sync/atomic"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
consN := atomic.AddInt32(&s.requests, 1) - 1
var prodErrors []error
var prodMedias []*core.Media
var prods []*Producer // matched producers for consumer
// Step 1. Get consumer medias
consMedias := cons.GetMedias()
for _, consMedia := range consMedias {
log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia)
producers:
for prodN, prod := range s.producers {
if err = prod.Dial(); err != nil {
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
prodErrors = append(prodErrors, err)
continue
}
// Step 2. Get producer medias (not tracks yet)
for _, prodMedia := range prod.GetMedias() {
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
prodMedias = append(prodMedias, prodMedia)
// Step 3. Match consumer/producer codecs list
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
if prodCodec == nil {
continue
}
var track *core.Receiver
switch prodMedia.Direction {
case core.DirectionRecvonly:
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
// Step 4. Get recvonly track from producer
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to consumer
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
case core.DirectionSendonly:
log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN)
// Step 4. Get recvonly track from consumer (backchannel)
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to producer
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
}
prods = append(prods, prod)
if !consMedia.MatchAll() {
break producers
}
}
}
}
// stop producers if they don't have readers
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
if len(prods) == 0 {
return formatError(consMedias, prodMedias, prodErrors)
}
s.mu.Lock()
s.consumers = append(s.consumers, cons)
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range prods {
prod.start()
}
return nil
}
func formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error {
if prodMedias != nil {
var prod, cons string
for _, media := range prodMedias {
if media.Direction == core.DirectionRecvonly {
for _, codec := range media.Codecs {
prod = appendString(prod, codec.PrintName())
}
}
}
for _, media := range consMedias {
if media.Direction == core.DirectionSendonly {
for _, codec := range media.Codecs {
cons = appendString(cons, codec.PrintName())
}
}
}
return errors.New("streams: codecs not matched: " + prod + " => " + cons)
}
if prodErrors != nil {
var text string
for _, err := range prodErrors {
text = appendString(text, err.Error())
}
return errors.New("streams: " + text)
}
return errors.New("streams: unknown error")
}
func appendString(s, elem string) string {
if strings.Contains(s, elem) {
return s
}
if len(s) == 0 {
return elem
}
return s + ", " + elem
}

View File

@@ -1,41 +1,75 @@
package streams
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"errors"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/pkg/core"
)
type Handler func(url string) (core.Producer, error)
var handlers = map[string]Handler{}
var handlersMu sync.Mutex
func HandleFunc(scheme string, handler Handler) {
handlersMu.Lock()
handlers[scheme] = handler
handlersMu.Unlock()
}
func getHandler(url string) Handler {
i := strings.IndexByte(url, ':')
if i <= 0 { // TODO: i < 4 ?
return nil
}
handlersMu.Lock()
defer handlersMu.Unlock()
return handlers[url[:i]]
}
func HasProducer(url string) bool {
return getHandler(url) != nil
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if _, ok := handlers[scheme]; ok {
return true
}
if _, ok := redirects[scheme]; ok {
return true
}
}
return false
}
func GetProducer(url string) (core.Producer, error) {
handler := getHandler(url)
if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url)
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if redirect, ok := redirects[scheme]; ok {
location, err := redirect(url)
if err != nil {
return nil, err
}
if location != "" {
return GetProducer(location)
}
}
if handler, ok := handlers[scheme]; ok {
return handler(url)
}
}
return handler(url)
return nil, errors.New("streams: unsupported scheme: " + url)
}
// Redirect can return: location URL or error or empty URL and error
type Redirect func(url string) (string, error)
var redirects = map[string]Redirect{}
func RedirectFunc(scheme string, redirect Redirect) {
redirects[scheme] = redirect
}
func Location(url string) (string, error) {
if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i]
if redirect, ok := redirects[scheme]; ok {
return redirect(url)
}
}
return "", nil
}

View File

@@ -2,10 +2,7 @@ package streams
import (
"encoding/json"
"errors"
"strings"
"sync"
"sync/atomic"
"github.com/AlexxIT/go2rtc/pkg/core"
)
@@ -38,105 +35,19 @@ func NewStream(source any) *Stream {
}
}
func (s *Stream) Sources() (sources []string) {
for _, prod := range s.producers {
sources = append(sources, prod.url)
}
return
}
func (s *Stream) SetSource(source string) {
for _, prod := range s.producers {
prod.SetSource(source)
}
}
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
consN := atomic.AddInt32(&s.requests, 1) - 1
var statErrors []error
var statMedias []*core.Media
var statProds []*Producer // matched producers for consumer
// Step 1. Get consumer medias
for _, consMedia := range cons.GetMedias() {
log.Trace().Msgf("[streams] check cons=%d media=%s", consN, consMedia)
producers:
for prodN, prod := range s.producers {
if err = prod.Dial(); err != nil {
log.Trace().Err(err).Msgf("[streams] skip prod=%s", prod.url)
statErrors = append(statErrors, err)
continue
}
// Step 2. Get producer medias (not tracks yet)
for _, prodMedia := range prod.GetMedias() {
log.Trace().Msgf("[streams] check prod=%d media=%s", prodN, prodMedia)
statMedias = append(statMedias, prodMedia)
// Step 3. Match consumer/producer codecs list
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
if prodCodec == nil {
continue
}
var track *core.Receiver
switch prodMedia.Direction {
case core.DirectionRecvonly:
log.Trace().Msgf("[streams] match prod=%d => cons=%d", prodN, consN)
// Step 4. Get recvonly track from producer
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to consumer
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
case core.DirectionSendonly:
log.Trace().Msgf("[streams] match cons=%d => prod=%d", consN, prodN)
// Step 4. Get recvonly track from consumer (backchannel)
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to producer
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
}
statProds = append(statProds, prod)
if !consMedia.MatchAll() {
break producers
}
}
}
}
// stop producers if they don't have readers
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
if len(statProds) == 0 {
return formatError(statMedias, statErrors)
}
s.mu.Lock()
s.consumers = append(s.consumers, cons)
s.mu.Unlock()
// there may be duplicates, but that's not a problem
for _, prod := range statProds {
prod.start()
}
return nil
}
func (s *Stream) RemoveConsumer(cons core.Consumer) {
_ = cons.Stop()
@@ -206,48 +117,3 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
return json.Marshal(info)
}
func formatError(statMedias []*core.Media, statErrors []error) error {
var text string
for _, media := range statMedias {
if media.Direction == core.DirectionRecvonly {
continue
}
for _, codec := range media.Codecs {
name := codec.Name
if name == core.CodecAAC {
name = "AAC"
}
if strings.Contains(text, name) {
continue
}
if len(text) > 0 {
text += ","
}
text += name
}
}
if text != "" {
return errors.New(text)
}
for _, err := range statErrors {
s := err.Error()
if strings.Contains(text, s) {
continue
}
if len(text) > 0 {
text += ","
}
text += s
}
if text != "" {
return errors.New(text)
}
return errors.New("unknown error")
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/rs/zerolog"
)
@@ -25,10 +24,6 @@ func Init() {
streams[name] = NewStream(item)
}
for name, item := range store.GetDict("streams") {
streams[name] = NewStream(item)
}
api.HandleFunc("api/streams", streamsHandler)
}
@@ -118,6 +113,14 @@ func GetAll() (names []string) {
return
}
func Streams() map[string]*Stream {
return streams
}
func Delete(id string) {
delete(streams, id)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
src := query.Get("src")
@@ -141,6 +144,11 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
if New(name, src) == nil {
http.Error(w, "", http.StatusBadRequest)
return
}
if err := app.PatchConfig(name, src, "streams"); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
case "PATCH":
@@ -173,6 +181,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
case "DELETE":
delete(streams, src)
if err := app.PatchConfig(src, nil, "streams"); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
}

View File

@@ -3,17 +3,16 @@ package tapo
import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/kasa"
"github.com/AlexxIT/go2rtc/pkg/tapo"
)
func Init() {
streams.HandleFunc("tapo", handle)
}
streams.HandleFunc("kasa", func(url string) (core.Producer, error) {
return kasa.Dial(url)
})
func handle(url string) (core.Producer, error) {
conn := tapo.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
return conn, nil
streams.HandleFunc("tapo", func(url string) (core.Producer, error) {
return tapo.Dial(url)
})
}

View File

@@ -1,9 +1,7 @@
package webrtc
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
@@ -42,6 +40,8 @@ func streamsHandler(rawURL string) (core.Producer, error) {
// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc
return kinesisClient(rawURL, query, "WebRTC/Kinesis")
} else if format == "openipc" {
return openIPCClient(rawURL, query)
} else {
return go2rtcClient(rawURL)
}
@@ -212,207 +212,3 @@ func whepClient(url string) (core.Producer, error) {
return prod, nil
}
type KinesisRequest struct {
Action string `json:"action"`
ClientID string `json:"recipientClientId"`
Payload []byte `json:"messagePayload"`
}
func (k KinesisRequest) String() string {
return fmt.Sprintf("action=%s, payload=%s", k.Action, k.Payload)
}
type KinesisResponse struct {
Payload []byte `json:"messagePayload"`
Type string `json:"messageType"`
}
func (k KinesisResponse) String() string {
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
}
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
if err != nil {
return nil, err
}
// 2. Load ICEServers from query param (base64 json)
conf := pion.Configuration{}
if s := query.Get("ice_servers"); s != "" {
conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))
if err != nil {
log.Warn().Err(err).Caller().Send()
}
}
// close websocket when we ready return Producer or connection error
defer conn.Close()
// 3. Create Peer Connection
api, err := webrtc.NewAPI("")
if err != nil {
return nil, err
}
pc, err := api.NewPeerConnection(conf)
if err != nil {
return nil, err
}
// protect from sending ICE candidate before Offer
var sendOffer core.Waiter
// protect from blocking on errors
defer sendOffer.Done(nil)
// waiter will wait PC error or WS error or nil (connection OK)
var connState core.Waiter
req := KinesisRequest{
ClientID: query.Get("client_id"),
}
prod := webrtc.NewConn(pc)
prod.Desc = desc
prod.Mode = core.ModeActiveProducer
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
_ = sendOffer.Wait()
req.Action = "ICE_CANDIDATE"
req.Payload, _ = json.Marshal(msg.ToJSON())
if err = conn.WriteJSON(&req); err != nil {
connState.Done(err)
return
}
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
case pion.PeerConnectionState:
switch msg {
case pion.PeerConnectionStateConnecting:
case pion.PeerConnectionStateConnected:
connState.Done(nil)
default:
connState.Done(errors.New("webrtc: " + msg.String()))
}
}
})
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
}
// 4. Create offer
offer, err := prod.CreateOffer(medias)
if err != nil {
return nil, err
}
// 5. Send offer
req.Action = "SDP_OFFER"
req.Payload, _ = json.Marshal(pion.SessionDescription{
Type: pion.SDPTypeOffer,
SDP: offer,
})
if err = conn.WriteJSON(req); err != nil {
return nil, err
}
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
sendOffer.Done(nil)
go func() {
var err error
// will be closed when conn will be closed
for {
var res KinesisResponse
if err = conn.ReadJSON(&res); err != nil {
// some buggy messages from Amazon servers
if errors.Is(err, io.ErrUnexpectedEOF) {
continue
}
break
}
log.Trace().Msgf("[webrtc] kinesis recv: %s", res)
switch res.Type {
case "SDP_ANSWER":
// 6. Get answer
var sd pion.SessionDescription
if err = json.Unmarshal(res.Payload, &sd); err != nil {
break
}
if err = prod.SetAnswer(sd.SDP); err != nil {
break
}
case "ICE_CANDIDATE":
// 7. Continue to receiving candidates
var ci pion.ICECandidateInit
if err = json.Unmarshal(res.Payload, &ci); err != nil {
break
}
if err = prod.AddCandidate(ci.Candidate); err != nil {
break
}
}
}
connState.Done(err)
}()
if err = connState.Wait(); err != nil {
return nil, err
}
return prod, nil
}
type WyzeKVS struct {
ClientId string `json:"ClientId"`
Cam string `json:"cam"`
Result string `json:"result"`
Servers json.RawMessage `json:"servers"`
URL string `json:"signalingUrl"`
}
func wyzeClient(rawURL string) (core.Producer, error) {
client := http.Client{Timeout: 5 * time.Second}
res, err := client.Get(rawURL)
if err != nil {
return nil, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var kvs WyzeKVS
if err = json.Unmarshal(b, &kvs); err != nil {
return nil, err
}
if kvs.Result != "ok" {
return nil, errors.New("wyse: wrong result: " + kvs.Result)
}
query := url.Values{
"client_id": []string{kvs.ClientId},
"ice_servers": []string{string(kvs.Servers)},
}
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
}

220
internal/webrtc/kinesis.go Normal file
View File

@@ -0,0 +1,220 @@
package webrtc
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v3"
)
type kinesisRequest struct {
Action string `json:"action"`
ClientID string `json:"recipientClientId"`
Payload []byte `json:"messagePayload"`
}
func (k kinesisRequest) String() string {
return fmt.Sprintf("action=%s, payload=%s", k.Action, k.Payload)
}
type kinesisResponse struct {
Payload []byte `json:"messagePayload"`
Type string `json:"messageType"`
}
func (k kinesisResponse) String() string {
return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload)
}
func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
if err != nil {
return nil, err
}
// 2. Load ICEServers from query param (base64 json)
conf := pion.Configuration{}
if s := query.Get("ice_servers"); s != "" {
conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))
if err != nil {
log.Warn().Err(err).Caller().Send()
}
}
// close websocket when we ready return Producer or connection error
defer conn.Close()
// 3. Create Peer Connection
api, err := webrtc.NewAPI("")
if err != nil {
return nil, err
}
pc, err := api.NewPeerConnection(conf)
if err != nil {
return nil, err
}
// protect from sending ICE candidate before Offer
var sendOffer core.Waiter
// protect from blocking on errors
defer sendOffer.Done(nil)
// waiter will wait PC error or WS error or nil (connection OK)
var connState core.Waiter
req := kinesisRequest{
ClientID: query.Get("client_id"),
}
prod := webrtc.NewConn(pc)
prod.Desc = desc
prod.Mode = core.ModeActiveProducer
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
_ = sendOffer.Wait()
req.Action = "ICE_CANDIDATE"
req.Payload, _ = json.Marshal(msg.ToJSON())
if err = conn.WriteJSON(&req); err != nil {
connState.Done(err)
return
}
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
case pion.PeerConnectionState:
switch msg {
case pion.PeerConnectionStateConnecting:
case pion.PeerConnectionStateConnected:
connState.Done(nil)
default:
connState.Done(errors.New("webrtc: " + msg.String()))
}
}
})
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
}
// 4. Create offer
offer, err := prod.CreateOffer(medias)
if err != nil {
return nil, err
}
// 5. Send offer
req.Action = "SDP_OFFER"
req.Payload, _ = json.Marshal(pion.SessionDescription{
Type: pion.SDPTypeOffer,
SDP: offer,
})
if err = conn.WriteJSON(req); err != nil {
return nil, err
}
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
sendOffer.Done(nil)
go func() {
var err error
// will be closed when conn will be closed
for {
var res kinesisResponse
if err = conn.ReadJSON(&res); err != nil {
// some buggy messages from Amazon servers
if errors.Is(err, io.ErrUnexpectedEOF) {
continue
}
break
}
log.Trace().Msgf("[webrtc] kinesis recv: %s", res)
switch res.Type {
case "SDP_ANSWER":
// 6. Get answer
var sd pion.SessionDescription
if err = json.Unmarshal(res.Payload, &sd); err != nil {
break
}
if err = prod.SetAnswer(sd.SDP); err != nil {
break
}
case "ICE_CANDIDATE":
// 7. Continue to receiving candidates
var ci pion.ICECandidateInit
if err = json.Unmarshal(res.Payload, &ci); err != nil {
break
}
if err = prod.AddCandidate(ci.Candidate); err != nil {
break
}
}
}
connState.Done(err)
}()
if err = connState.Wait(); err != nil {
return nil, err
}
return prod, nil
}
type wyzeKVS struct {
ClientId string `json:"ClientId"`
Cam string `json:"cam"`
Result string `json:"result"`
Servers json.RawMessage `json:"servers"`
URL string `json:"signalingUrl"`
}
func wyzeClient(rawURL string) (core.Producer, error) {
client := http.Client{Timeout: 5 * time.Second}
res, err := client.Get(rawURL)
if err != nil {
return nil, err
}
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var kvs wyzeKVS
if err = json.Unmarshal(b, &kvs); err != nil {
return nil, err
}
if kvs.Result != "ok" {
return nil, errors.New("wyse: wrong result: " + kvs.Result)
}
query := url.Values{
"client_id": []string{kvs.ClientId},
"ice_servers": []string{string(kvs.Servers)},
}
return kinesisClient(kvs.URL, query, "WebRTC/Wyze")
}

168
internal/webrtc/openipc.go Normal file
View File

@@ -0,0 +1,168 @@
package webrtc
import (
"encoding/json"
"errors"
"io"
"net/url"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v3"
)
func openIPCClient(rawURL string, query url.Values) (core.Producer, error) {
// 1. Connect to signalign server
conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)
if err != nil {
return nil, err
}
// 2. Load ICEServers from query param (base64 json)
var conf pion.Configuration
if s := query.Get("ice_servers"); s != "" {
conf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))
if err != nil {
log.Warn().Err(err).Caller().Send()
}
}
// close websocket when we ready return Producer or connection error
defer conn.Close()
// 3. Create Peer Connection
api, err := webrtc.NewAPI("")
if err != nil {
return nil, err
}
pc, err := api.NewPeerConnection(conf)
if err != nil {
return nil, err
}
// protect from sending ICE candidate before Offer
var sendAnswer core.Waiter
// protect from blocking on errors
defer sendAnswer.Done(nil)
// waiter will wait PC error or WS error or nil (connection OK)
var connState core.Waiter
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/OpenIPC"
prod.Mode = core.ModeActiveProducer
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case *pion.ICECandidate:
_ = sendAnswer.Wait()
req := openIPCReq{
Data: msg.ToJSON().Candidate,
Req: "candidate",
}
if err = conn.WriteJSON(&req); err != nil {
connState.Done(err)
return
}
log.Trace().Msgf("[webrtc] openipc send: %s", req)
case pion.PeerConnectionState:
switch msg {
case pion.PeerConnectionStateConnecting:
case pion.PeerConnectionStateConnected:
connState.Done(nil)
default:
connState.Done(errors.New("webrtc: " + msg.String()))
}
}
})
go func() {
var err error
// will be closed when conn will be closed
for err == nil {
var rep openIPCReply
if err = conn.ReadJSON(&rep); err != nil {
// some buggy messages from Amazon servers
if errors.Is(err, io.ErrUnexpectedEOF) {
continue
}
break
}
log.Trace().Msgf("[webrtc] openipc recv: %s", rep)
switch rep.Reply {
case "webrtc_answer":
// 6. Get answer
var sd pion.SessionDescription
if err = json.Unmarshal(rep.Data, &sd); err != nil {
break
}
if err = prod.SetOffer(sd.SDP); err != nil {
break
}
var answer string
if answer, err = prod.GetAnswer(); err != nil {
break
}
req := openIPCReq{Data: answer, Req: "answer"}
if err = conn.WriteJSON(req); err != nil {
break
}
log.Trace().Msgf("[webrtc] kinesis send: %s", req)
sendAnswer.Done(nil)
case "webrtc_candidate":
// 7. Continue to receiving candidates
var ci pion.ICECandidateInit
if err = json.Unmarshal(rep.Data, &ci); err != nil {
break
}
if err = prod.AddCandidate(ci.Candidate); err != nil {
break
}
}
}
connState.Done(err)
}()
if err = connState.Wait(); err != nil {
return nil, err
}
return prod, nil
}
type openIPCReply struct {
Data json.RawMessage `json:"data"`
Reply string `json:"reply"`
}
func (r openIPCReply) String() string {
b, _ := json.Marshal(r)
return string(b)
}
type openIPCReq struct {
Data string `json:"data"`
Req string `json:"req"`
}
func (r openIPCReq) String() string {
b, _ := json.Marshal(r)
return string(b)
}

View File

@@ -3,6 +3,9 @@ package webtorrent
import (
"errors"
"fmt"
"net/http"
"net/url"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
@@ -10,8 +13,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/rs/zerolog"
"net/http"
"net/url"
)
func Init() {
@@ -110,13 +111,13 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
}
} else {
// response all shares
var items []api.Stream
var items []*api.Source
for src, share := range shares {
pwd := srv.GetSharePwd(share)
source := fmt.Sprintf("webtorrent:?share=%s&pwd=%s", share, pwd)
items = append(items, api.Stream{Name: src, URL: source})
items = append(items, &api.Source{ID: src, URL: source})
}
api.ResponseStreams(w, items)
api.ResponseSources(w, items)
}
case "POST":

View File

@@ -1,17 +1,17 @@
## AAC-LD and AAC-ELD
Codec | Rate | QuickTime | ffmpeg | VLC
------|------|-----------|--------|----
AAC-LD | 8000 | yes | no | no
AAC-LD | 16000 | yes | no | no
AAC-LD | 22050 | yes | yes | no
AAC-LD | 24000 | yes | yes | no
AAC-LD | 32000 | yes | yes | no
AAC-ELD | 8000 | yes | no | no
AAC-ELD | 16000 | yes | no | no
AAC-ELD | 22050 | yes | yes | yes
AAC-ELD | 24000 | yes | yes | yes
AAC-ELD | 32000 | yes | yes | yes
| Codec | Rate | QuickTime | ffmpeg | VLC |
|---------|-------|-----------|--------|-----|
| AAC-LD | 8000 | yes | no | no |
| AAC-LD | 16000 | yes | no | no |
| AAC-LD | 22050 | yes | yes | no |
| AAC-LD | 24000 | yes | yes | no |
| AAC-LD | 32000 | yes | yes | no |
| AAC-ELD | 8000 | yes | no | no |
| AAC-ELD | 16000 | yes | no | no |
| AAC-ELD | 22050 | yes | yes | yes |
| AAC-ELD | 24000 | yes | yes | yes |
| AAC-ELD | 32000 | yes | yes | yes |
## Useful links

124
pkg/aac/aac.go Normal file
View File

@@ -0,0 +1,124 @@
package aac
import (
"encoding/hex"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/bits"
"github.com/AlexxIT/go2rtc/pkg/core"
)
const (
TypeAACMain = 1
TypeAACLC = 2 // Low Complexity
TypeAACLD = 23 // Low Delay (48000, 44100, 32000, 24000, 22050)
TypeESCAPE = 31
TypeAACELD = 39 // Enhanced Low Delay
AUTime = 1024
// FMTP streamtype=5 - audio stream
FMTP = "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config="
)
var sampleRates = [16]uint32{
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,
0, 0, 0, // protection from request sampleRates[15]
}
func ConfigToCodec(conf []byte) *core.Codec {
// https://en.wikipedia.org/wiki/MPEG-4_Part_3#MPEG-4_Audio_Object_Types
rd := bits.NewReader(conf)
codec := &core.Codec{
FmtpLine: FMTP + hex.EncodeToString(conf),
PayloadType: core.PayloadTypeRAW,
}
objType := rd.ReadBits(5)
if objType == TypeESCAPE {
objType = 32 + rd.ReadBits(6)
}
switch objType {
case TypeAACLC, TypeAACLD, TypeAACELD:
codec.Name = core.CodecAAC
default:
codec.Name = fmt.Sprintf("AAC-%X", objType)
}
if sampleRateIdx := rd.ReadBits8(4); sampleRateIdx < 0x0F {
codec.ClockRate = sampleRates[sampleRateIdx]
} else {
codec.ClockRate = rd.ReadBits(24)
}
codec.Channels = rd.ReadBits16(4)
return codec
}
func DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate uint32) {
rd := bits.NewReader(b)
objType = rd.ReadBits8(5)
if objType == 0b11111 {
objType = 32 + rd.ReadBits8(6)
}
sampleFreqIdx = rd.ReadBits8(4)
if sampleFreqIdx == 0b1111 {
sampleRate = rd.ReadBits(24)
}
channels = rd.ReadBits8(4)
return
}
func EncodeConfig(objType byte, sampleRate uint32, channels byte, shortFrame bool) []byte {
wr := bits.NewWriter(nil)
if objType < TypeESCAPE {
wr.WriteBits8(objType, 5)
} else {
wr.WriteBits8(TypeESCAPE, 5)
wr.WriteBits8(objType-32, 6)
}
i := indexUint32(sampleRates[:], sampleRate)
if i >= 0 {
wr.WriteBits8(byte(i), 4)
} else {
wr.WriteBits8(0xF, 4)
wr.WriteBits(sampleRate, 24)
}
wr.WriteBits8(channels, 4)
switch objType {
case TypeAACLD:
// https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L841
wr.WriteBool(shortFrame)
wr.WriteBit(0) // dependsOnCoreCoder
wr.WriteBit(0) // extension_flag
wr.WriteBits8(0, 2) // ep_config
case TypeAACELD:
// https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L922
wr.WriteBool(shortFrame)
wr.WriteBits8(0, 3) // res_flags
wr.WriteBit(0) // ldSbrPresentFlag
wr.WriteBits8(0, 4) // ELDEXT_TERM
wr.WriteBits8(0, 2) // ep_config
}
return wr.Bytes()
}
func indexUint32(s []uint32, v uint32) int {
for i := range s {
if v == s[i] {
return i
}
}
return -1
}

43
pkg/aac/aac_test.go Normal file
View File

@@ -0,0 +1,43 @@
package aac
import (
"encoding/hex"
"testing"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/stretchr/testify/require"
)
func TestConfigToCodec(t *testing.T) {
s := "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
s = core.Between(s, "config=", ";")
src, err := hex.DecodeString(s)
require.Nil(t, err)
codec := ConfigToCodec(src)
require.Equal(t, core.CodecAAC, codec.Name)
require.Equal(t, uint32(24000), codec.ClockRate)
require.Equal(t, uint16(1), codec.Channels)
dst := EncodeConfig(TypeAACELD, 24000, 1, true)
require.Equal(t, src, dst)
}
func TestADTS(t *testing.T) {
// FFmpeg MPEG-TS AAC (one packet)
s := "fff15080021ffc210049900219002380fff15080021ffc212049900219002380" //...
src, err := hex.DecodeString(s)
require.Nil(t, err)
codec := ADTSToCodec(src)
require.Equal(t, uint32(44100), codec.ClockRate)
require.Equal(t, uint16(2), codec.Channels)
size := ReadADTSSize(src)
require.Equal(t, uint16(16), size)
dst := CodecToADTS(codec)
WriteADTSSize(dst, size)
require.Equal(t, src[:len(dst)], dst)
}

131
pkg/aac/adts.go Normal file
View File

@@ -0,0 +1,131 @@
package aac
import (
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/bits"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
func IsADTS(b []byte) bool {
_ = b[1]
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
}
func ADTSToCodec(b []byte) *core.Codec {
// 1. Check ADTS header
if !IsADTS(b) {
return nil
}
// 2. Decode ADTS params
// https://wiki.multimedia.cx/index.php/ADTS
rd := bits.NewReader(b)
_ = rd.ReadBits(12) // Syncword, all bits must be set to 1
_ = rd.ReadBit() // MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2
_ = rd.ReadBits(2) // Layer, always set to 0
_ = rd.ReadBit() // Protection absence, set to 1 if there is no CRC and 0 if there is CRC
objType := rd.ReadBits8(2) + 1 // Profile, the MPEG-4 Audio Object Type minus 1
sampleRateIdx := rd.ReadBits8(4) // MPEG-4 Sampling Frequency Index
_ = rd.ReadBit() // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
channels := rd.ReadBits16(3) // MPEG-4 Channel Configuration
//_ = rd.ReadBit() // Originality, set to 1 to signal originality of the audio and 0 otherwise
//_ = rd.ReadBit() // Home, set to 1 to signal home usage of the audio and 0 otherwise
//_ = rd.ReadBit() // Copyright ID bit
//_ = rd.ReadBit() // Copyright ID start
//_ = rd.ReadBits(13) // Frame length
//_ = rd.ReadBits(11) // Buffer fullness
//_ = rd.ReadBits(2) // Number of AAC frames (Raw Data Blocks) in ADTS frame minus 1
//_ = rd.ReadBits(16) // CRC check
// 3. Encode RTP config
wr := bits.NewWriter(nil)
wr.WriteBits8(objType, 5)
wr.WriteBits8(sampleRateIdx, 4)
wr.WriteBits16(channels, 4)
conf := wr.Bytes()
codec := &core.Codec{
Name: core.CodecAAC,
ClockRate: sampleRates[sampleRateIdx],
Channels: channels,
FmtpLine: FMTP + hex.EncodeToString(conf),
}
return codec
}
func ReadADTSSize(b []byte) uint16 {
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
_ = b[5] // bounds
return uint16(b[3]&0x03)<<(8+3) | uint16(b[4])<<3 | uint16(b[5]>>5)
}
func WriteADTSSize(b []byte, size uint16) {
// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
_ = b[5] // bounds
b[3] |= byte(size >> (8 + 3))
b[4] = byte(size >> 3)
b[5] |= byte(size << 5)
return
}
func ADTSTimeSize(b []byte) uint32 {
var units uint32
for len(b) > ADTSHeaderSize {
auSize := ReadADTSSize(b)
b = b[auSize:]
units++
}
return units * AUTime
}
func CodecToADTS(codec *core.Codec) []byte {
s := core.Between(codec.FmtpLine, "config=", ";")
conf, err := hex.DecodeString(s)
if err != nil {
return nil
}
objType, sampleFreqIdx, channels, _ := DecodeConfig(conf)
profile := objType - 1
wr := bits.NewWriter(nil)
wr.WriteAllBits(1, 12) // Syncword, all bits must be set to 1
wr.WriteBit(0) // MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2
wr.WriteBits8(0, 2) // Layer, always set to 0
wr.WriteBit(1) // Protection absence, set to 1 if there is no CRC and 0 if there is CRC
wr.WriteBits8(profile, 2) // Profile, the MPEG-4 Audio Object Type minus 1
wr.WriteBits8(sampleFreqIdx, 4) // MPEG-4 Sampling Frequency Index
wr.WriteBit(0) // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
wr.WriteBits8(channels, 3) // MPEG-4 Channel Configuration
wr.WriteBit(0) // Originality, set to 1 to signal originality of the audio and 0 otherwise
wr.WriteBit(0) // Home, set to 1 to signal home usage of the audio and 0 otherwise
wr.WriteBit(0) // Copyright ID bit
wr.WriteBit(0) // Copyright ID start
wr.WriteBits16(0, 13) // Frame length
wr.WriteAllBits(1, 11) // Buffer fullness (variable bitrate)
wr.WriteBits8(0, 2) // Number of AAC frames (Raw Data Blocks) in ADTS frame minus 1
return wr.Bytes()
}
func EncodeToADTS(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
adts := CodecToADTS(codec)
return func(packet *rtp.Packet) {
if !IsADTS(packet.Payload) {
b := make([]byte, ADTSHeaderSize+len(packet.Payload))
copy(b, adts)
copy(b[ADTSHeaderSize:], packet.Payload)
WriteADTSSize(b, uint16(len(b)))
clone := *packet
clone.Payload = b
handler(&clone)
} else {
handler(packet)
}
}
}

58
pkg/aac/consumer.go Normal file
View File

@@ -0,0 +1,58 @@
package aac
import (
"io"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
type Consumer struct {
core.SuperConsumer
wr *core.WriteBuffer
}
func NewConsumer() *Consumer {
cons := &Consumer{
wr: core.NewWriteBuffer(nil),
}
cons.Medias = []*core.Media{
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
return cons
}
func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
sender := core.NewSender(media, track.Codec)
sender.Handler = func(pkt *rtp.Packet) {
if n, err := c.wr.Write(pkt.Payload); err == nil {
c.Send += n
}
}
if track.Codec.IsRTP() {
sender.Handler = RTPToADTS(track.Codec, sender.Handler)
} else {
sender.Handler = EncodeToADTS(track.Codec, sender.Handler)
}
sender.HandleRTP(track)
c.Senders = append(c.Senders, sender)
return nil
}
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
return c.wr.WriteTo(wr)
}
func (c *Consumer) Stop() error {
_ = c.SuperConsumer.Close()
return c.wr.Close()
}

View File

@@ -2,11 +2,13 @@ package aac
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
const RTPPacketVersionAAC = 0
const ADTSHeaderSize = 7
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
var timestamp uint32
@@ -14,6 +16,7 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
return func(packet *rtp.Packet) {
// support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
// https://datatracker.ietf.org/doc/html/rfc3640
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
@@ -29,13 +32,13 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
headers = headers[2:]
units = units[unitSize:]
timestamp += 1024
timestamp += AUTime
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Timestamp = timestamp
if IsADTS(unit) {
clone.Payload = unit[7:]
clone.Payload = unit[ADTSHeaderSize:]
} else {
clone.Payload = unit
}
@@ -54,11 +57,11 @@ func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
}
// support ONLY one unit in payload
size := uint16(len(packet.Payload))
auSize := uint16(len(packet.Payload))
// 2 bytes header size + 2 bytes first payload size
payload := make([]byte, 2+2+size)
payload := make([]byte, 2+2+auSize)
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], size<<3)
binary.BigEndian.PutUint16(payload[2:], auSize<<3)
copy(payload[4:], packet.Payload)
clone := rtp.Packet{
@@ -74,6 +77,58 @@ func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
}
}
func IsADTS(b []byte) bool {
return len(b) > 7 && b[0] == 0xFF && b[1]&0xF0 == 0xF0
func ADTStoRTP(src []byte) (dst []byte) {
dst = make([]byte, 2) // header bytes
for i, n := 0, len(src)-ADTSHeaderSize; i < n; {
auSize := ReadADTSSize(src[i:])
dst = append(dst, byte(auSize>>5), byte(auSize<<3)) // size in bits
i += int(auSize)
}
hdrSize := uint16(len(dst) - 2)
binary.BigEndian.PutUint16(dst, hdrSize<<3) // size in bits
return append(dst, src...)
}
func RTPTimeSize(b []byte) uint32 {
// convert RTP header size to units count
units := binary.BigEndian.Uint16(b) >> 4
return uint32(units) * AUTime
}
func RTPToADTS(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
adts := CodecToADTS(codec)
return func(packet *rtp.Packet) {
src := packet.Payload
dst := make([]byte, 0, len(src))
headersSize := binary.BigEndian.Uint16(src) >> 3
headers := src[2 : 2+headersSize]
units := src[2+headersSize:]
for len(headers) > 0 {
unitSize := binary.BigEndian.Uint16(headers) >> 3
headers = headers[2:]
unit := units[:unitSize]
units = units[unitSize:]
if !IsADTS(unit) {
i := len(dst)
dst = append(dst, adts...)
WriteADTSSize(dst[i:], ADTSHeaderSize+uint16(len(unit)))
}
dst = append(dst, unit...)
}
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = dst
handler(&clone)
}
}
func RTPToCodec(b []byte) *core.Codec {
hdrSize := binary.BigEndian.Uint16(b) / 8
return ADTSToCodec(b[2+hdrSize:])
}

129
pkg/bits/reader.go Normal file
View File

@@ -0,0 +1,129 @@
package bits
type Reader struct {
EOF bool // if end of buffer raised during reading
buf []byte // total buf
byte byte // current byte
bits byte // bits left in byte
pos int // current pos in buf
}
func NewReader(b []byte) *Reader {
return &Reader{buf: b}
}
//goland:noinspection GoStandardMethods
func (r *Reader) ReadByte() byte {
if r.bits != 0 {
return r.ReadBits8(8)
}
if r.pos >= len(r.buf) {
r.EOF = true
return 0
}
b := r.buf[r.pos]
r.pos++
return b
}
func (r *Reader) ReadUint16() uint16 {
if r.bits != 0 {
return r.ReadBits16(16)
}
return uint16(r.ReadByte())<<8 | uint16(r.ReadByte())
}
func (r *Reader) ReadUint24() uint32 {
if r.bits != 0 {
return r.ReadBits(24)
}
return uint32(r.ReadByte())<<16 | uint32(r.ReadByte())<<8 | uint32(r.ReadByte())
}
func (r *Reader) ReadUint32() uint32 {
if r.bits != 0 {
return r.ReadBits(32)
}
return uint32(r.ReadByte())<<24 | uint32(r.ReadByte())<<16 | uint32(r.ReadByte())<<8 | uint32(r.ReadByte())
}
func (r *Reader) ReadBit() byte {
if r.bits == 0 {
r.byte = r.ReadByte()
r.bits = 7
} else {
r.bits--
}
return (r.byte >> r.bits) & 0b1
}
func (r *Reader) ReadBits(n byte) (res uint32) {
for i := n - 1; i != 255; i-- {
res |= uint32(r.ReadBit()) << i
}
return
}
func (r *Reader) ReadBits8(n byte) (res uint8) {
for i := n - 1; i != 255; i-- {
res |= r.ReadBit() << i
}
return
}
func (r *Reader) ReadBits16(n byte) (res uint16) {
for i := n - 1; i != 255; i-- {
res |= uint16(r.ReadBit()) << i
}
return
}
func (r *Reader) ReadBits64(n byte) (res uint64) {
for i := n - 1; i != 255; i-- {
res |= uint64(r.ReadBit()) << i
}
return
}
func (r *Reader) ReadBytes(n int) (b []byte) {
if r.bits == 0 {
if r.pos+n > len(r.buf) {
r.EOF = true
return nil
}
b = r.buf[r.pos : r.pos+n]
r.pos += n
} else {
b = make([]byte, n)
for i := 0; i < n; i++ {
b[i] = r.ReadByte()
}
}
return
}
// ReadUEGolomb - ReadExponentialGolomb (unsigned)
func (r *Reader) ReadUEGolomb() uint32 {
var size byte
for size = 0; size < 32; size++ {
if b := r.ReadBit(); b != 0 || r.EOF {
break
}
}
return r.ReadBits(size) + (1 << size) - 1
}
// ReadSEGolomb - ReadSignedExponentialGolomb
func (r *Reader) ReadSEGolomb() int32 {
if b := r.ReadUEGolomb(); b%2 == 0 {
return -int32(b >> 1)
} else {
return int32(b >> 1)
}
}

95
pkg/bits/writer.go Normal file
View File

@@ -0,0 +1,95 @@
package bits
type Writer struct {
buf []byte // total buf
byte *byte // pointer to current byte
bits byte // bits left in byte
}
func NewWriter(buf []byte) *Writer {
return &Writer{buf: buf}
}
//goland:noinspection GoStandardMethods
func (w *Writer) WriteByte(b byte) {
if w.bits != 0 {
w.WriteBits8(b, 8)
}
w.buf = append(w.buf, b)
}
func (w *Writer) WriteBit(b byte) {
if w.bits == 0 {
w.buf = append(w.buf, 0)
w.byte = &w.buf[len(w.buf)-1]
w.bits = 7
} else {
w.bits--
}
*w.byte |= (b & 1) << w.bits
}
func (w *Writer) WriteBits(v uint32, n byte) {
for i := n - 1; i != 255; i-- {
w.WriteBit(byte(v>>i) & 0b1)
}
}
func (w *Writer) WriteBits16(v uint16, n byte) {
for i := n - 1; i != 255; i-- {
w.WriteBit(byte(v>>i) & 0b1)
}
}
func (w *Writer) WriteBits8(v, n byte) {
for i := n - 1; i != 255; i-- {
w.WriteBit((v >> i) & 0b1)
}
}
func (w *Writer) WriteAllBits(bit, n byte) {
for i := byte(0); i < n; i++ {
w.WriteBit(bit)
}
}
func (w *Writer) WriteBool(b bool) {
if b {
w.WriteBit(1)
} else {
w.WriteBit(0)
}
}
func (w *Writer) WriteUint16(v uint16) {
if w.bits != 0 {
w.WriteBits16(v, 16)
}
w.buf = append(w.buf, byte(v>>8), byte(v))
}
func (w *Writer) WriteBytes(bytes ...byte) {
if w.bits != 0 {
for _, b := range bytes {
w.WriteByte(b)
}
}
w.buf = append(w.buf, bytes...)
}
func (w *Writer) Bytes() []byte {
return w.buf
}
func (w *Writer) Len() int {
return len(w.buf)
}
func (w *Writer) Reset() {
w.buf = w.buf[:0]
w.bits = 0
}

View File

@@ -17,7 +17,7 @@ import (
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtp"
)
@@ -132,7 +132,7 @@ func (c *Client) Dial() (err error) {
// <stream1 name="360p.265" size="640x360" x1="yes" x2="yes" x4="yes" />
// <vin0>
// </bubble>
re := regexp.MustCompile("<stream " + stream + `[^>]+`)
re := regexp.MustCompile("<stream" + stream + " [^>]+")
stream = re.FindString(string(xml))
if strings.Contains(stream, ".265") {
c.videoCodec = core.CodecH265
@@ -226,7 +226,7 @@ func (c *Client) Handle() error {
Header: rtp.Header{
Timestamp: core.Now90000(),
},
Payload: h264.AnnexB2AVC(b[6:]),
Payload: annexb.EncodeToAVCC(b[6:], false),
}
c.videoTrack.WriteRTP(pkt)
} else {
@@ -245,6 +245,7 @@ func (c *Client) Handle() error {
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
Timestamp: audioTS,
},
Payload: b[6+36:],

View File

@@ -52,6 +52,30 @@ func (c *Codec) IsRTP() bool {
return c.PayloadType != PayloadTypeRAW
}
func (c *Codec) IsVideo() bool {
return c.Kind() == KindVideo
}
func (c *Codec) IsAudio() bool {
return c.Kind() == KindAudio
}
func (c *Codec) Kind() string {
return GetKind(c.Name)
}
func (c *Codec) PrintName() string {
switch c.Name {
case CodecAAC:
return "AAC"
case CodecPCM:
return "S16B"
case CodecPCML:
return "S16L"
}
return c.Name
}
func (c *Codec) Clone() *Codec {
clone := *c
return &clone

View File

@@ -47,7 +47,10 @@ type Producer interface {
// GetTrack - return Receiver, that can only produce rtp.Packet(s)
GetTrack(media *Media, codec *Codec) (*Receiver, error)
// Deprecated: rename to Run()
Start() error
// Deprecated: rename to Close()
Stop() error
}
@@ -59,6 +62,7 @@ type Consumer interface {
AddTrack(media *Media, codec *Codec, track *Receiver) error
// Deprecated: rename to Close()
Stop() error
}
@@ -90,6 +94,7 @@ type Info struct {
URL string `json:"url,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
SDP string `json:"sdp,omitempty"`
Medias []*Media `json:"medias,omitempty"`
Receivers []*Receiver `json:"receivers,omitempty"`
Senders []*Sender `json:"senders,omitempty"`
@@ -101,3 +106,72 @@ const (
UnsupportedCodec = "unsupported codec"
WrongMediaDirection = "wrong media direction"
)
type SuperProducer struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
SDP string `json:"sdp,omitempty"`
Medias []*Media `json:"medias,omitempty"`
Receivers []*Receiver `json:"receivers,omitempty"`
Recv int `json:"recv,omitempty"`
}
func (s *SuperProducer) GetMedias() []*Media {
return s.Medias
}
func (s *SuperProducer) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
for _, receiver := range s.Receivers {
if receiver.Codec == codec {
return receiver, nil
}
}
receiver := NewReceiver(media, codec)
s.Receivers = append(s.Receivers, receiver)
return receiver, nil
}
func (s *SuperProducer) Close() error {
for _, receiver := range s.Receivers {
receiver.Close()
}
return nil
}
type SuperConsumer struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
SDP string `json:"sdp,omitempty"`
Medias []*Media `json:"medias,omitempty"`
Senders []*Sender `json:"senders,omitempty"`
Send int `json:"send,omitempty"`
}
func (s *SuperConsumer) GetMedias() []*Media {
return s.Medias
}
func (s *SuperConsumer) AddTrack(media *Media, codec *Codec, track *Receiver) error {
return nil
}
//func (b *SuperConsumer) WriteTo(w io.Writer) (n int64, err error) {
// return 0, nil
//}
func (s *SuperConsumer) Close() error {
for _, sender := range s.Senders {
sender.Close()
}
return nil
}
func (s *SuperConsumer) Codecs() []*Codec {
codecs := make([]*Codec, len(s.Senders))
for i, sender := range s.Senders {
codecs[i] = sender.Codec
}
return codecs
}

View File

@@ -1,14 +1,20 @@
package core
import (
cryptorand "crypto/rand"
"github.com/rs/zerolog/log"
"crypto/rand"
"runtime"
"strconv"
"strings"
"time"
)
const (
BufferSize = 64 * 1024 // 64K
ConnDialTimeout = time.Second * 3
ConnDeadline = time.Second * 3
ProbeTimeout = time.Second * 3
)
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
func Now90000() uint32 {
return uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
@@ -16,12 +22,16 @@ func Now90000() uint32 {
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
// RandString base10 - numbers, base16 - hex, base36 - digits+letters, base64 - URL safe symbols
// RandString base10 - numbers, base16 - hex, base36 - digits+letters
// base64 - URL safe symbols, base0 - crypto random
func RandString(size, base byte) string {
b := make([]byte, size)
if _, err := cryptorand.Read(b); err != nil {
if _, err := rand.Read(b); err != nil {
panic(err)
}
if base == 0 {
return string(b)
}
for i := byte(0); i < size; i++ {
b[i] = symbols[b[i]%base]
}
@@ -44,12 +54,7 @@ func Between(s, sub1, sub2 string) string {
}
s = s[i+len(sub1):]
if len(sub2) == 1 {
i = strings.IndexByte(s, sub2[0])
} else {
i = strings.Index(s, sub2)
}
if i >= 0 {
if i = strings.Index(s, sub2); i >= 0 {
return s[:i]
}
@@ -57,7 +62,9 @@ func Between(s, sub1, sub2 string) string {
}
func Atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
if s != "" {
i, _ = strconv.Atoi(s)
}
return
}
@@ -69,7 +76,6 @@ func Assert(ok bool) {
}
func Caller() string {
log.Error().Caller(0).Send()
_, file, line, _ := runtime.Caller(1)
return file + ":" + strconv.Itoa(line)
}

View File

@@ -1,31 +0,0 @@
package core
import "time"
type Probe struct {
deadline time.Time
items map[any]struct{}
}
func NewProbe(enable bool) *Probe {
if enable {
return &Probe{
deadline: time.Now().Add(time.Second * 3),
items: map[any]struct{}{},
}
} else {
return nil
}
}
// Active return true if probe enabled and not finish
func (p *Probe) Active() bool {
return len(p.items) < 2 && time.Now().Before(p.deadline)
}
// Append safe to run if Probe is nil
func (p *Probe) Append(v any) {
if p != nil {
p.items[v] = struct{}{}
}
}

112
pkg/core/readbuffer.go Normal file
View File

@@ -0,0 +1,112 @@
package core
import (
"errors"
"io"
)
const ProbeSize = 1024 * 1024 // 1MB
const (
BufferDisable = 0
BufferDrainAndClear = -1
)
// ReadBuffer support buffering and Seek over buffer
// positive BufferSize will enable buffering mode
// Seek to negative offset will clear buffer
// Seek with a positive BufferSize will continue buffering after the last read from the buffer
// Seek with a negative BufferSize will clear buffer after the last read from the buffer
// Read more than BufferSize will raise error
type ReadBuffer struct {
io.Reader
BufferSize int
buf []byte
pos int
}
func NewReadBuffer(rd io.Reader) *ReadBuffer {
if rs, ok := rd.(*ReadBuffer); ok {
return rs
}
return &ReadBuffer{Reader: rd}
}
func (r *ReadBuffer) Read(p []byte) (n int, err error) {
// with zero buffer - read as usual
if r.BufferSize == BufferDisable {
return r.Reader.Read(p)
}
// if buffer not empty - read from it
if r.pos < len(r.buf) {
n = copy(p, r.buf[r.pos:])
r.pos += n
return
}
// with negative buffer - empty it and read as usual
if r.BufferSize < 0 {
r.BufferSize = BufferDisable
r.buf = nil
r.pos = 0
return r.Reader.Read(p)
}
n, err = r.Reader.Read(p)
if len(r.buf)+n > r.BufferSize {
return 0, errors.New("probe reader overflow")
}
r.buf = append(r.buf, p[:n]...)
r.pos += n
return
}
func (r *ReadBuffer) Close() error {
if closer, ok := r.Reader.(io.Closer); ok {
return closer.Close()
}
return nil
}
func (r *ReadBuffer) Seek(offset int64, whence int) (int64, error) {
var pos int
switch whence {
case io.SeekStart:
pos = int(offset)
case io.SeekCurrent:
pos = r.pos + int(offset)
case io.SeekEnd:
pos = len(r.buf) + int(offset)
}
// negative offset - empty buffer
if pos < 0 {
r.buf = nil
r.pos = 0
} else if pos >= len(r.buf) {
r.pos = len(r.buf)
} else {
r.pos = pos
}
return int64(r.pos), nil
}
func (r *ReadBuffer) Peek(n int) ([]byte, error) {
r.BufferSize = n
b := make([]byte, n)
if _, err := io.ReadAtLeast(r, b, n); err != nil {
return nil, err
}
r.Reset()
return b, nil
}
func (r *ReadBuffer) Reset() {
r.BufferSize = BufferDrainAndClear
r.pos = 0
}

View File

@@ -0,0 +1,64 @@
package core
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/require"
)
func TestReadSeeker(t *testing.T) {
b := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
buf := bytes.NewReader(b)
rd := NewReadBuffer(buf)
rd.BufferSize = ProbeSize
// 1. Read to buffer
b = make([]byte, 3)
n, err := rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{0, 1, 2}, b[:n])
// 2. Seek to start
_, err = rd.Seek(0, io.SeekStart)
require.Nil(t, err)
// 3. Read from buffer
b = make([]byte, 2)
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{0, 1}, b[:n])
// 4. Read from buffer
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{2}, b[:n])
// 5. Read to buffer
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{3, 4}, b[:n])
// 6. Seek to start
_, err = rd.Seek(0, io.SeekStart)
require.Nil(t, err)
// 7. Disable buffer
rd.BufferSize = -1
// 8. Read from buffer
b = make([]byte, 10)
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{0, 1, 2, 3, 4}, b[:n])
// 9. Direct read
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{5, 6, 7, 8, 9}, b[:n])
// 10. Check buffer empty
require.Nil(t, rd.buf)
}

View File

@@ -4,11 +4,20 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/pion/rtp"
"strconv"
"sync"
"github.com/pion/rtp"
)
type Packet struct {
PayloadType uint8
Sequence uint16
Timestamp uint32 // PTS if DTS == 0 else DTS
Composition uint32 // CTS = PTS-DTS (for support B-frames)
Payload []byte
}
var ErrCantGetTrack = errors.New("can't get track")
type Receiver struct {
@@ -181,3 +190,16 @@ func (s *Sender) String() string {
func (s *Sender) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
// VA - helper, for extract video and audio receivers from list
func VA(receivers []*Receiver) (video, audio *Receiver) {
for _, receiver := range receivers {
switch GetKind(receiver.Codec.Name) {
case KindVideo:
video = receiver
case KindAudio:
audio = receiver
}
}
return
}

111
pkg/core/writebuffer.go Normal file
View File

@@ -0,0 +1,111 @@
package core
import (
"bytes"
"io"
"sync"
)
// WriteBuffer by defaul Write(s) to bytes.Buffer.
// But after WriteTo to new io.Writer - calls Reset.
// Reset will flush current buffer data to new writer and starts to Write to new io.Writer
// WriteTo will be locked until Write fails or Close will be called.
type WriteBuffer struct {
io.Writer
err error
mu sync.Mutex
wg sync.WaitGroup
state byte
}
func NewWriteBuffer(wr io.Writer) *WriteBuffer {
if wr == nil {
wr = bytes.NewBuffer(nil)
}
return &WriteBuffer{Writer: wr}
}
func (w *WriteBuffer) Write(p []byte) (n int, err error) {
w.mu.Lock()
if w.err != nil {
err = w.err
} else if n, err = w.Writer.Write(p); err != nil {
w.err = err
w.done()
}
w.mu.Unlock()
return
}
func (w *WriteBuffer) WriteTo(wr io.Writer) (n int64, err error) {
w.Reset(wr)
w.wg.Wait()
return 0, w.err // TODO: fix counter
}
func (w *WriteBuffer) Close() error {
if closer, ok := w.Writer.(io.Closer); ok {
return closer.Close()
}
w.mu.Lock()
w.done()
w.mu.Unlock()
return nil
}
func (w *WriteBuffer) Reset(wr io.Writer) {
w.mu.Lock()
w.add()
if buf, ok := w.Writer.(*bytes.Buffer); ok && buf.Len() != 0 {
if _, err := io.Copy(wr, buf); err != nil {
w.err = err
w.done()
}
}
w.Writer = wr
w.mu.Unlock()
}
const (
none = iota
start
end
)
func (w *WriteBuffer) add() {
if w.state == none {
w.state = start
w.wg.Add(1)
}
}
func (w *WriteBuffer) done() {
if w.state == start {
w.state = end
w.wg.Done()
}
}
// OnceBuffer will catch only first message
type OnceBuffer struct {
buf []byte
}
func (o *OnceBuffer) Write(p []byte) (n int, err error) {
if o.buf == nil {
o.buf = p
}
return 0, io.EOF
}
func (o *OnceBuffer) WriteTo(w io.Writer) (n int64, err error) {
return io.Copy(w, bytes.NewReader(o.buf))
}
func (o *OnceBuffer) Buffer() []byte {
return o.buf
}
func (o *OnceBuffer) Len() int {
return len(o.buf)
}

58
pkg/debug/debug.go Normal file
View File

@@ -0,0 +1,58 @@
package debug
import (
"fmt"
"time"
"github.com/pion/rtp"
)
func Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) {
var lastTime = time.Now()
var lastTS uint32
var secCnt int
var secSize int
var secTS uint32
var secTime time.Time
return func(packet *rtp.Packet) {
if include != nil && !include(packet) {
return
}
now := time.Now()
fmt.Printf(
"%s: size:%6d, ts:%10d, type:%2d, ssrc:%d, seq:%5d, mark:%t, dts:%4d, dtime:%3d\n",
now.Format("15:04:05.000"),
len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
packet.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(),
)
lastTS = packet.Timestamp
lastTime = now
if secTS == 0 {
secTS = lastTS
secTime = now
return
}
if dt := now.Sub(secTime); dt > time.Second {
fmt.Printf(
"%s: size:%6d, cnt:%d, dts: %d, dtime:%d\n",
now.Format("15:04:05.000"),
secSize, secCnt, lastTS-secTS, dt.Milliseconds(),
)
secCnt = 0
secSize = 0
secTS = lastTS
secTime = now
}
secCnt++
secSize += len(packet.Payload)
}
}

View File

@@ -8,14 +8,16 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
"io"
"net"
"net/url"
"time"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
)
type Client struct {
@@ -173,7 +175,7 @@ func (c *Client) Handle() error {
switch dataType {
case 0x1FC, 0x1FE: // video IFrame
payload := h264.AnnexB2AVC(b[16:])
payload := annexb.EncodeToAVCC(b[16:], false)
if c.videoTrack == nil {
fps := b[5]
@@ -208,7 +210,7 @@ func (c *Client) Handle() error {
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: c.videoTS},
Payload: h264.AnnexB2AVC(b[8:]),
Payload: annexb.EncodeToAVCC(b[8:], false),
}
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
@@ -345,7 +347,7 @@ func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
FmtpLine: h264.GetFmtpLine(payload),
}
case 0x03, 0x13, 0x43:
case 0x03, 0x13, 0x43, 0x53:
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,

View File

@@ -1,47 +0,0 @@
package fake
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"time"
)
type Consumer struct {
streamer.Element
Medias []*streamer.Media
Tracks []*streamer.Track
RecvPackets int
SendPackets int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return c.Medias
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
switch track.Direction {
case streamer.DirectionSendonly:
track = track.Bind(func(packet *rtp.Packet) error {
if track.Codec.PayloadType != packet.PayloadType {
panic("wrong payload type")
}
c.RecvPackets++
return nil
})
case streamer.DirectionRecvonly:
go func() {
for {
pkt := &rtp.Packet{}
pkt.PayloadType = track.Codec.PayloadType
if err := track.WriteRTP(pkt); err != nil {
return
}
c.SendPackets++
time.Sleep(time.Second)
}
}()
}
c.Tracks = append(c.Tracks, track)
return track
}

View File

@@ -1,62 +0,0 @@
package fake
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"time"
)
type Producer struct {
streamer.Element
Medias []*streamer.Media
Tracks []*streamer.Track
RecvPackets int
SendPackets int
}
func (p *Producer) GetMedias() []*streamer.Media {
return p.Medias
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
if !streamer.Contains(p.Medias, media, codec) {
panic("you shall not pass!")
}
track := streamer.NewTrack(codec, media.Direction)
switch media.Direction {
case streamer.DirectionSendonly:
track2 := track.Bind(func(packet *rtp.Packet) error {
p.RecvPackets++
return nil
})
p.Tracks = append(p.Tracks, track2)
case streamer.DirectionRecvonly:
p.Tracks = append(p.Tracks, track)
}
return track
}
func (p *Producer) Start() error {
for {
for _, track := range p.Tracks {
if track.Direction != streamer.DirectionSendonly {
continue
}
pkt := &rtp.Packet{}
pkt.PayloadType = track.Codec.PayloadType
if err := track.WriteRTP(pkt); err != nil {
return err
}
p.SendPackets++
}
time.Sleep(time.Second)
}
}
func (p *Producer) Stop() error {
panic("not implemented")
}

200
pkg/flv/amf/amf.go Normal file
View File

@@ -0,0 +1,200 @@
package amf
import (
"encoding/binary"
"errors"
"math"
)
const (
TypeNumber byte = iota
TypeBoolean
TypeString
TypeObject
TypeNull = 5
TypeEcmaArray = 8
TypeObjectEnd = 9
)
// AMF spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
type AMF struct {
buf []byte
pos int
}
var ErrRead = errors.New("amf: read error")
func NewReader(b []byte) *AMF {
return &AMF{buf: b}
}
func (a *AMF) ReadItems() ([]any, error) {
var items []any
for a.pos < len(a.buf) {
v, err := a.ReadItem()
if err != nil {
return nil, err
}
items = append(items, v)
}
return items, nil
}
func (a *AMF) ReadItem() (any, error) {
dataType, err := a.ReadByte()
if err != nil {
return nil, err
}
switch dataType {
case TypeNumber:
return a.ReadNumber()
case TypeBoolean:
b, err := a.ReadByte()
return b != 0, err
case TypeString:
return a.ReadString()
case TypeObject:
return a.ReadObject()
case TypeNull:
return nil, nil
case TypeObjectEnd:
return nil, nil
}
return nil, ErrRead
}
func (a *AMF) ReadByte() (byte, error) {
if a.pos >= len(a.buf) {
return 0, ErrRead
}
v := a.buf[a.pos]
a.pos++
return v, nil
}
func (a *AMF) ReadNumber() (float64, error) {
if a.pos+8 > len(a.buf) {
return 0, ErrRead
}
v := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])
a.pos += 8
return math.Float64frombits(v), nil
}
func (a *AMF) ReadString() (string, error) {
if a.pos+2 > len(a.buf) {
return "", ErrRead
}
size := int(binary.BigEndian.Uint16(a.buf[a.pos:]))
a.pos += 2
if a.pos+size > len(a.buf) {
return "", ErrRead
}
s := string(a.buf[a.pos : a.pos+size])
a.pos += size
return s, nil
}
func (a *AMF) ReadObject() (map[string]any, error) {
obj := make(map[string]any)
for {
k, err := a.ReadString()
if err != nil {
return nil, err
}
v, err := a.ReadItem()
if err != nil {
return nil, err
}
if k == "" {
break
}
obj[k] = v
}
return obj, nil
}
func (a *AMF) ReadEcmaArray() (map[string]any, error) {
if a.pos+4 > len(a.buf) {
return nil, ErrRead
}
a.pos += 4 // skip size
return a.ReadObject()
}
func NewWriter() *AMF {
return &AMF{}
}
func (a *AMF) Bytes() []byte {
return a.buf
}
func (a *AMF) WriteNumber(n float64) {
b := math.Float64bits(n)
a.buf = append(
a.buf, TypeNumber,
byte(b>>56), byte(b>>48), byte(b>>40), byte(b>>32),
byte(b>>24), byte(b>>16), byte(b>>8), byte(b),
)
}
func (a *AMF) WriteBool(b bool) {
if b {
a.buf = append(a.buf, TypeBoolean, 1)
} else {
a.buf = append(a.buf, TypeBoolean, 0)
}
}
func (a *AMF) WriteString(s string) {
n := len(s)
a.buf = append(a.buf, TypeString, byte(n>>8), byte(n))
a.buf = append(a.buf, s...)
}
func (a *AMF) WriteObject(obj map[string]any) {
a.buf = append(a.buf, TypeObject)
for k, v := range obj {
n := len(k)
a.buf = append(a.buf, byte(n>>8), byte(n))
a.buf = append(a.buf, k...)
switch v := v.(type) {
case string:
a.WriteString(v)
case int:
a.WriteNumber(float64(v))
case bool:
a.WriteBool(v)
default:
panic(v)
}
}
a.buf = append(a.buf, 0, 0, TypeObjectEnd)
}
func (a *AMF) WriteNull() {
a.buf = append(a.buf, TypeNull)
}

234
pkg/flv/producer.go Normal file
View File

@@ -0,0 +1,234 @@
package flv
import (
"bytes"
"encoding/binary"
"errors"
"io"
"time"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/pion/rtp"
)
type Producer struct {
core.SuperProducer
rd *core.ReadBuffer
video, audio *core.Receiver
}
func Open(rd io.Reader) (*Producer, error) {
prod := &Producer{rd: core.NewReadBuffer(rd)}
if err := prod.probe(); err != nil {
return nil, err
}
prod.Type = "FLV producer"
return prod, nil
}
const (
Signature = "FLV"
TagAudio = 8
TagVideo = 9
TagData = 18
CodecAAC = 10
CodecAVC = 7
)
func (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
receiver, _ := c.SuperProducer.GetTrack(media, codec)
if media.Kind == core.KindVideo {
c.video = receiver
} else {
c.audio = receiver
}
return receiver, nil
}
func (c *Producer) Start() error {
for {
pkt, err := c.readPacket()
if err != nil {
return err
}
c.Recv += len(pkt.Payload)
switch pkt.PayloadType {
case TagAudio:
if c.audio == nil || pkt.Payload[1] == 0 {
continue
}
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.audio.Codec.ClockRate)
pkt.Payload = pkt.Payload[2:]
c.audio.WriteRTP(pkt)
case TagVideo:
// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b
if c.video == nil || pkt.Payload[1] == 0 {
continue
}
pkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)
pkt.Payload = pkt.Payload[5:]
c.video.WriteRTP(pkt)
}
}
}
func (c *Producer) Stop() error {
_ = c.SuperProducer.Close()
return c.rd.Close()
}
func (c *Producer) probe() error {
if err := c.readHeader(); err != nil {
return err
}
c.rd.BufferSize = core.ProbeSize
defer c.rd.Reset()
// Normal software sends:
// 1. Video/audio flag in header
// 2. MetaData as first tag (with video/audio codec info)
// 3. Video/audio headers in 2nd and 3rd tag
// Reolink camera sends:
// 1. Empty video/audio flag
// 2. MedaData without stereo key for AAC
// 3. Audio header after Video keyframe tag
waitType := []byte{TagData}
timeout := time.Now().Add(core.ProbeTimeout)
for len(waitType) != 0 && time.Now().Before(timeout) {
pkt, err := c.readPacket()
if err != nil {
return err
}
if i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 {
continue
} else {
waitType = append(waitType[:i], waitType[i+1:]...)
}
switch pkt.PayloadType {
case TagAudio:
_ = pkt.Payload[1] // bounds
codecID := pkt.Payload[0] >> 4 // SoundFormat
_ = pkt.Payload[0] & 0b1100 // SoundRate
_ = pkt.Payload[0] & 0b0010 // SoundSize
_ = pkt.Payload[0] & 0b0001 // SoundType
if codecID != CodecAAC {
continue
}
if pkt.Payload[1] != 0 { // check if header
continue
}
codec := aac.ConfigToCodec(pkt.Payload[2:])
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
case TagVideo:
_ = pkt.Payload[1] // bounds
_ = pkt.Payload[0] >> 4 // FrameType
codecID := pkt.Payload[0] & 0b1111 // CodecID
if codecID != CodecAVC {
continue
}
if pkt.Payload[1] != 0 { // check if header
continue
}
codec := h264.ConfigToCodec(pkt.Payload[5:])
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.Medias = append(c.Medias, media)
case TagData:
if !bytes.Contains(pkt.Payload, []byte("onMetaData")) {
waitType = append(waitType, TagData)
}
if bytes.Contains(pkt.Payload, []byte("videocodecid")) {
waitType = append(waitType, TagVideo)
}
if bytes.Contains(pkt.Payload, []byte("audiocodecid")) {
waitType = append(waitType, TagAudio)
}
}
}
return nil
}
func (c *Producer) readHeader() error {
b := make([]byte, 9)
if _, err := io.ReadFull(c.rd, b); err != nil {
return err
}
if string(b[:3]) != Signature {
return errors.New("flv: wrong header")
}
_ = b[4] // flags (skip because unsupported by Reolink cameras)
if skip := binary.BigEndian.Uint32(b[5:]) - 9; skip > 0 {
if _, err := io.ReadFull(c.rd, make([]byte, skip)); err != nil {
return err
}
}
return nil
}
func (c *Producer) readPacket() (*rtp.Packet, error) {
// https://rtmp.veriskope.com/pdf/video_file_format_spec_v10.pdf
b := make([]byte, 4+11)
if _, err := io.ReadFull(c.rd, b); err != nil {
return nil, err
}
b = b[4 : 4+11] // skip previous tag size
size := uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
pkt := &rtp.Packet{
Header: rtp.Header{
PayloadType: b[0],
Timestamp: uint32(b[4])<<16 | uint32(b[5])<<8 | uint32(b[6]) | uint32(b[7])<<24,
},
Payload: make([]byte, size),
}
if _, err := io.ReadFull(c.rd, pkt.Payload); err != nil {
return nil, err
}
return pkt, nil
}
func TimeToRTP(timeMS uint32, clockRate uint32) uint32 {
return timeMS * clockRate / 1000
}

View File

@@ -13,3 +13,4 @@ Payloader code taken from [pion](https://github.com/pion/rtp) library. And chang
- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)
- [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)
- [Two stream formats, Annex-B, AVCC (H.264) and HVCC (H.265)](https://www.programmersought.com/article/3901815022/)
- https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/producer-reference-nal.html

141
pkg/h264/annexb/annexb.go Normal file
View File

@@ -0,0 +1,141 @@
// Package annexb - universal for H264 and H265
package annexb
import (
"bytes"
"encoding/binary"
)
const StartCode = "\x00\x00\x00\x01"
const startAUD = StartCode + "\x09\xF0"
const startAUDstart = startAUD + StartCode
// EncodeToAVCC
// will change original slice data!
// safeAppend should be used if original slice has useful data after end (part of other slice)
//
// FFmpeg MPEG-TS: 00000001 AUD 00000001 SPS 00000001 PPS 000001 IFrame
// FFmpeg H264: 00000001 SPS 00000001 PPS 000001 IFrame 00000001 PFrame
func EncodeToAVCC(b []byte, safeAppend bool) []byte {
const minSize = len(StartCode) + 1
// 1. Check frist "start code"
if len(b) < len(startAUDstart) || string(b[:len(StartCode)]) != StartCode {
return nil
}
// 2. Skip Access unit delimiter (AUD) from FFmpeg
if string(b[:len(startAUDstart)]) == startAUDstart {
b = b[6:]
}
var start int
for i, n := minSize, len(b)-minSize; i < n; {
// 3. Check "start code" (first 2 bytes)
if b[i] != 0 || b[i+1] != 0 {
i++
continue
}
// 4. Check "start code" (3 bytes size or 4 bytes size)
if b[i+2] == 1 {
if safeAppend {
// protect original slice from "damage"
b = bytes.Clone(b)
safeAppend = false
}
// convert start code from 3 bytes to 4 bytes
b = append(b, 0)
copy(b[i+1:], b[i:])
n++
} else if b[i+2] != 0 || b[i+3] != 1 {
i++
continue
}
// 5. Set size for previous AU
size := uint32(i - start - len(StartCode))
binary.BigEndian.PutUint32(b[start:], size)
start = i
i += minSize
}
// 6. Set size for last AU
size := uint32(len(b) - start - len(StartCode))
binary.BigEndian.PutUint32(b[start:], size)
return b
}
func DecodeAVCC(b []byte, safeClone bool) []byte {
if safeClone {
b = bytes.Clone(b)
}
for i := 0; i < len(b); {
size := int(binary.BigEndian.Uint32(b[i:]))
b[i] = 0
b[i+1] = 0
b[i+2] = 0
b[i+3] = 1
i += 4 + size
}
return b
}
// DecodeAVCCWithAUD - AUD doesn't important for FFmpeg, but important for Safari
func DecodeAVCCWithAUD(src []byte) []byte {
dst := make([]byte, len(startAUD)+len(src))
copy(dst, startAUD)
copy(dst[len(startAUD):], src)
DecodeAVCC(dst[len(startAUD):], false)
return dst
}
const (
h264PFrame = 1
h264IFrame = 5
h264SPS = 7
h264PPS = 8
h265VPS = 32
h265PFrame = 1
)
// IndexFrame - get new frame start position in the AnnexB stream
func IndexFrame(b []byte) int {
if len(b) < len(startAUDstart) {
return -1
}
for i := len(startAUDstart); ; {
if di := bytes.Index(b[i:], []byte(StartCode)); di < 0 {
break
} else {
i += di + 4 // move to NALU start
}
if i >= len(b) {
break
}
h264Type := b[i] & 0b1_1111
switch h264Type {
case h264PFrame, h264SPS:
return i - 4 // move to start code
case h264IFrame, h264PPS:
continue
}
h265Type := (b[i] >> 1) & 0b11_1111
switch h265Type {
case h265PFrame, h265VPS:
return i - 4 // move to start code
}
}
return -1
}

View File

@@ -3,46 +3,12 @@ package h264
import (
"bytes"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
func AnnexB2AVC(b []byte) []byte {
for i := 0; i < len(b); {
if i+4 >= len(b) {
break
}
size := bytes.Index(b[i+4:], []byte{0, 0, 0, 1})
if size < 0 {
size = len(b) - (i + 4)
}
binary.BigEndian.PutUint32(b[i:], uint32(size))
i += size + 4
}
return b
}
func AVCtoAnnexB(b []byte) []byte {
b = bytes.Clone(b)
for i := 0; i < len(b); {
size := int(binary.BigEndian.Uint32(b[i:]))
b[i] = 0
b[i+1] = 0
b[i+2] = 0
b[i+3] = 1
i += 4 + size
}
return b
}
const forbiddenZeroBit = 0x80
const nalUnitType = 0x1F
// DecodeStream - find and return first AU in AVC format
// Deprecated: DecodeStream - find and return first AU in AVC format
// useful for processing live streams with unknown separator size
func DecodeStream(annexb []byte) ([]byte, int) {
startPos := -1
@@ -154,70 +120,3 @@ func IndexFrom(b []byte, sep []byte, from int) int {
return bytes.Index(b, sep)
}
func EncodeAVC(nals ...[]byte) (avc []byte) {
var i, n int
for _, nal := range nals {
if i = len(nal); i > 0 {
n += 4 + i
}
}
avc = make([]byte, n)
n = 0
for _, nal := range nals {
if i = len(nal); i > 0 {
binary.BigEndian.PutUint32(avc[n:], uint32(i))
n += 4 + copy(avc[n+4:], nal)
}
}
return
}
func RepairAVC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
sps, pps := GetParameterSet(codec.FmtpLine)
ps := EncodeAVC(sps, pps)
return func(packet *rtp.Packet) {
if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload)
}
handler(packet)
}
}
func SplitAVC(data []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(data)) + 4
// check if multiple items in one packet
if size < len(data) {
nals = append(nals, data[:size])
data = data[size:]
} else {
nals = append(nals, data)
break
}
}
return nals
}
func Types(data []byte) []byte {
var types []byte
for {
types = append(types, NALUType(data))
size := 4 + int(binary.BigEndian.Uint32(data))
if size < len(data) {
data = data[size:]
} else {
break
}
}
return types
}

111
pkg/h264/avcc.go Normal file
View File

@@ -0,0 +1,111 @@
// Package h264 - AVCC format related functions
package h264
import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
sps, pps := GetParameterSet(codec.FmtpLine)
ps := JoinNALU(sps, pps)
return func(packet *rtp.Packet) {
if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload)
}
handler(packet)
}
}
func JoinNALU(nalus ...[]byte) (avcc []byte) {
var i, n int
for _, nalu := range nalus {
if i = len(nalu); i > 0 {
n += 4 + i
}
}
avcc = make([]byte, n)
n = 0
for _, nal := range nalus {
if i = len(nal); i > 0 {
binary.BigEndian.PutUint32(avcc[n:], uint32(i))
n += 4 + copy(avcc[n+4:], nal)
}
}
return
}
func SplitNALU(avcc []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(avcc)) + 4
// check if multiple items in one packet
if size < len(avcc) {
nals = append(nals, avcc[:size])
avcc = avcc[size:]
} else {
nals = append(nals, avcc)
break
}
}
return nals
}
func NALUTypes(avcc []byte) []byte {
var types []byte
for {
types = append(types, NALUType(avcc))
size := 4 + int(binary.BigEndian.Uint32(avcc))
if size < len(avcc) {
avcc = avcc[size:]
} else {
break
}
}
return types
}
func AVCCToCodec(avcc []byte) *core.Codec {
buf := bytes.NewBufferString("packetization-mode=1")
for {
size := 4 + int(binary.BigEndian.Uint32(avcc))
switch NALUType(avcc) {
case NALUTypeSPS:
buf.WriteString(";profile-level-id=")
buf.WriteString(hex.EncodeToString(avcc[5:8]))
buf.WriteString(";sprop-parameter-sets=")
buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
case NALUTypePPS:
buf.WriteString(",")
buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
}
if size < len(avcc) {
avcc = avcc[size:]
} else {
break
}
}
return &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
FmtpLine: buf.String(),
PayloadType: core.PayloadTypeRAW,
}
}

View File

@@ -1,87 +0,0 @@
package golomb
import "bytes"
type Reader struct {
r *bytes.Reader
b byte
shift byte
}
func NewReader(b []byte) *Reader {
return &Reader{
r: bytes.NewReader(b),
}
}
func (g *Reader) ReadBit() (b byte, err error) {
if g.shift == 0 {
if g.b, err = g.r.ReadByte(); err != nil {
return 0, err
}
g.shift = 7
} else {
g.shift--
}
b = (g.b >> g.shift) & 0b1
return
}
func (g *Reader) ReadBits(n byte) (res uint, err error) {
var b byte
for i := n - 1; i != 255; i-- {
if b, err = g.ReadBit(); err != nil {
return
}
res |= uint(b) << i
}
return
}
func (g *Reader) ReadUEGolomb() (res uint, err error) {
var b uint
var i byte
for i = 0; i < 32; i++ {
if b, err = g.ReadBits(1); err != nil {
return
}
if b != 0 {
break
}
}
if res, err = g.ReadBits(i); err != nil {
return
}
res += (1 << i) - 1
return
}
func (g *Reader) ReadSEGolomb() (res int, err error) {
var b uint
if b, err = g.ReadUEGolomb(); err != nil {
return
}
if b%2 == 0 {
res = -int(b >> 1)
} else {
res = int(b>>1)
}
return
}
func (g *Reader) ReadByte() (byte, error) {
return g.r.ReadByte()
}
func (g *Reader) End() bool {
// if only one bit in next byte left
if g.shift == 0 && g.r.Len() == 1 {
b, _ := g.r.ReadByte()
_ = g.r.UnreadByte()
return b == 0x80
}
if g.r.Len() == 0 {
//panic("not implemented")
}
return false
}

View File

@@ -1,56 +0,0 @@
package golomb
import "math/bits"
type Writer struct {
buf []byte
b byte // last byte
i int // last byte index
shift byte
}
func NewWriter() *Writer {
return &Writer{i: -1}
}
func (g *Writer) WriteBit(b byte) {
if g.shift == 0 {
g.buf = append(g.buf, 0)
g.b = 0
g.i++
g.shift = 7
} else {
g.shift--
}
g.b |= b << g.shift
g.buf[g.i] = g.b
}
func (g *Writer) WriteBits(b, n byte) {
for i := n - 1; i != 255; i-- {
g.WriteBit((b >> i) & 0b1)
}
}
func (g *Writer) WriteByte(b byte) {
g.buf = append(g.buf, b)
g.i++
}
func (g *Writer) WriteUEGolomb(b byte) {
b++
n := uint8(bits.Len8(b))*2 - 1
g.WriteBits(b, n)
}
func (g *Writer) WriteSEGolomb(b int8) {
if b > 0 {
g.WriteUEGolomb(byte(b)*2 - 1)
} else {
g.WriteUEGolomb(byte(-b) * 2)
}
}
func (g *Writer) Bytes() []byte {
return g.buf
}

View File

@@ -5,8 +5,9 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"github.com/AlexxIT/go2rtc/pkg/core"
)
const (
@@ -49,14 +50,23 @@ func Join(ps, iframe []byte) []byte {
return b
}
// https://developers.google.com/cast/docs/media
const (
ProfileBaseline = 0x42
ProfileMain = 0x4D
ProfileHigh = 0x64
CapabilityBaseline = 0xE0
CapabilityMain = 0x40
)
// GetProfileLevelID - get profile from fmtp line
// Some devices won't play video with high level, so limit max profile and max level.
// And return some profile even if fmtp line is empty.
func GetProfileLevelID(fmtp string) string {
// avc1.640029 - H.264 high 4.1 (Chromecast 1st and 2nd Gen)
profile := byte(0x64)
profile := byte(ProfileHigh)
capab := byte(0)
level := byte(0x29)
level := byte(41)
if fmtp != "" {
var conf []byte
@@ -70,12 +80,18 @@ func GetProfileLevelID(fmtp string) string {
conf, _ = hex.DecodeString(s)
}
if conf != nil {
if conf[0] < profile {
if len(conf) == 3 {
// sanitize profile, capab and level to supported values
switch conf[0] {
case ProfileBaseline, ProfileMain:
profile = conf[0]
}
switch conf[1] {
case CapabilityBaseline, CapabilityMain:
capab = conf[1]
}
if conf[2] < level {
switch conf[2] {
case 30, 31, 40:
level = conf[2]
}
}

85
pkg/h264/h264_test.go Normal file
View File

@@ -0,0 +1,85 @@
package h264
import (
"encoding/base64"
"encoding/hex"
"testing"
"github.com/stretchr/testify/require"
)
func TestDecodeConfig(t *testing.T) {
s := "01640033ffe1000c67640033ac1514a02800f19001000468ee3cb0"
src, err := hex.DecodeString(s)
require.Nil(t, err)
profile, sps, pps := DecodeConfig(src)
require.NotNil(t, profile)
require.NotNil(t, sps)
require.NotNil(t, pps)
dst := EncodeConfig(sps, pps)
require.Equal(t, src, dst)
}
func TestDecodeSPS(t *testing.T) {
s := "Z0IAMukAUAHjQgAAB9IAAOqcCAA=" // Amcrest AD410
b, err := base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps := DecodeSPS(b)
require.Equal(t, uint16(2560), sps.Width())
require.Equal(t, uint16(1920), sps.Height())
s = "R00AKZmgHgCJ+WEAAAMD6AAATiCE" // Sonoff
b, err = base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps = DecodeSPS(b)
require.Equal(t, uint16(1920), sps.Width())
require.Equal(t, uint16(1080), sps.Height())
s = "Z01AMqaAKAC1kAA=" // Dahua
b, err = base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps = DecodeSPS(b)
require.Equal(t, uint16(2560), sps.Width())
require.Equal(t, uint16(1440), sps.Height())
s = "Z2QAM6wVFKAoAPGQ" // Reolink
b, err = base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps = DecodeSPS(b)
require.Equal(t, uint16(2560), sps.Width())
require.Equal(t, uint16(1920), sps.Height())
s = "Z2QAKKwa0AoAt03AQEBQAAADABAAAAMB6PFCKg==" // TP-Link
b, err = base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps = DecodeSPS(b)
require.Equal(t, uint16(1280), sps.Width())
require.Equal(t, uint16(720), sps.Height())
s = "Z2QAFqwa0BQF/yzcBAQFAAADAAEAAAMAHo8UIqA=" // TP-Link sub
b, err = base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps = DecodeSPS(b)
require.Equal(t, uint16(640), sps.Width())
require.Equal(t, uint16(360), sps.Height())
}
func TestGetProfileLevelID(t *testing.T) {
// OpenIPC https://github.com/OpenIPC
s := "profile-level-id=0033e7; packetization-mode=1; "
profile := GetProfileLevelID(s)
require.Equal(t, "640029", profile)
// Eufy T8400 https://github.com/AlexxIT/go2rtc/issues/155
s = "packetization-mode=1;profile-level-id=276400"
profile = GetProfileLevelID(s)
require.Equal(t, "640029", profile)
}

101
pkg/h264/mpeg4.go Normal file
View File

@@ -0,0 +1,101 @@
// Package h264 - MPEG4 format related functions
package h264
import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/core"
)
// DecodeConfig - extract profile, SPS and PPS from MPEG4 config
func DecodeConfig(conf []byte) (profile []byte, sps []byte, pps []byte) {
if len(conf) < 6 || conf[0] != 1 {
return
}
profile = conf[1:4]
count := conf[5] & 0x1F
conf = conf[6:]
for i := byte(0); i < count; i++ {
if len(conf) < 2 {
return
}
size := 2 + int(binary.BigEndian.Uint16(conf))
if len(conf) < size {
return
}
if sps == nil {
sps = conf[2:size]
}
conf = conf[size:]
}
count = conf[0]
conf = conf[1:]
for i := byte(0); i < count; i++ {
if len(conf) < 2 {
return
}
size := 2 + int(binary.BigEndian.Uint16(conf))
if len(conf) < size {
return
}
if pps == nil {
pps = conf[2:size]
}
conf = conf[size:]
}
return
}
func EncodeConfig(sps, pps []byte) []byte {
spsSize := uint16(len(sps))
ppsSize := uint16(len(pps))
buf := make([]byte, 5+3+spsSize+3+ppsSize)
buf[0] = 1
copy(buf[1:], sps[1:4]) // profile
buf[4] = 3 | 0xFC // ? LengthSizeMinusOne
b := buf[5:]
_ = b[3]
b[0] = 1 | 0xE0 // ? sps count
binary.BigEndian.PutUint16(b[1:], spsSize)
copy(b[3:], sps)
b = buf[5+3+spsSize:]
_ = b[3]
b[0] = 1 // pps count
binary.BigEndian.PutUint16(b[1:], ppsSize)
copy(b[3:], pps)
return buf
}
func ConfigToCodec(conf []byte) *core.Codec {
buf := bytes.NewBufferString("packetization-mode=1")
profile, sps, pps := DecodeConfig(conf)
if profile != nil {
buf.WriteString(";profile-level-id=")
buf.WriteString(hex.EncodeToString(profile))
}
if sps != nil && pps != nil {
buf.WriteString(";sprop-parameter-sets=")
buf.WriteString(base64.StdEncoding.EncodeToString(sps))
buf.WriteString(",")
buf.WriteString(base64.StdEncoding.EncodeToString(pps))
}
return &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
FmtpLine: buf.String(),
PayloadType: core.PayloadTypeRAW,
}
}

View File

@@ -1,127 +0,0 @@
package ps
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/h264/golomb"
)
const PPSHeader = 0x68
// https://www.itu.int/rec/T-REC-H.264
// 7.3.2.2 Picture parameter set RBSP syntax
type PPS struct{}
func (p *PPS) Marshal() []byte {
w := golomb.NewWriter()
// this is typical PPS for most H264 cameras
w.WriteByte(PPSHeader)
w.WriteUEGolomb(0) // pic_parameter_set_id
w.WriteUEGolomb(0) // seq_parameter_set_id
w.WriteBit(1) // entropy_coding_mode_flag
w.WriteBit(0) // bottom_field_pic_order_in_frame_present_flag
w.WriteUEGolomb(0) // num_slice_groups_minus1
w.WriteUEGolomb(0) // num_ref_idx_l0_default_active_minus1
w.WriteUEGolomb(0) // num_ref_idx_l1_default_active_minus1
w.WriteBit(0) // weighted_pred_flag
w.WriteBits(0, 2) // weighted_bipred_idc
w.WriteSEGolomb(0) // pic_init_qp_minus26
w.WriteSEGolomb(0) // pic_init_qs_minus26
w.WriteSEGolomb(0) // chroma_qp_index_offset
w.WriteBit(1) // deblocking_filter_control_present_flag
w.WriteBit(0) // constrained_intra_pred_flag
w.WriteBit(0) // redundant_pic_cnt_present_flag
w.WriteBit(1) // rbsp_trailing_bits()
return w.Bytes()
}
func (p *PPS) Unmarshal(data []byte) (err error) {
r := golomb.NewReader(data)
var b byte
var u uint
if b, err = r.ReadByte(); err != nil {
return
}
if b&0x1F != 8 {
err = errors.New("not PPS data")
return
}
// pic_parameter_set_id
if u, err = r.ReadUEGolomb(); err != nil {
return
}
// seq_parameter_set_id
if u, err = r.ReadUEGolomb(); err != nil {
return
}
// entropy_coding_mode_flag
if b, err = r.ReadBit(); err != nil {
return
}
// bottom_field_pic_order_in_frame_present_flag
if b, err = r.ReadBit(); err != nil {
return
}
// num_slice_groups_minus1
if u, err = r.ReadUEGolomb(); err != nil {
return
}
if u > 0 {
//panic("not implemented")
return nil
}
// num_ref_idx_l0_default_active_minus1
if _, err = r.ReadUEGolomb(); err != nil {
return
}
// num_ref_idx_l1_default_active_minus1
if _, err = r.ReadUEGolomb(); err != nil {
return
}
// weighted_pred_flag
if _, err = r.ReadBit(); err != nil {
return
}
// weighted_bipred_idc
if _, err = r.ReadBits(2); err != nil {
return
}
// pic_init_qp_minus26
if _, err = r.ReadSEGolomb(); err != nil {
return
}
// pic_init_qs_minus26
if _, err = r.ReadSEGolomb(); err != nil {
return
}
// chroma_qp_index_offset
if _, err = r.ReadSEGolomb(); err != nil {
return
}
// deblocking_filter_control_present_flag
if _, err = r.ReadBit(); err != nil {
return
}
// constrained_intra_pred_flag
if _, err = r.ReadBit(); err != nil {
return
}
// redundant_pic_cnt_present_flag
if _, err = r.ReadBit(); err != nil {
return
}
if !r.End() {
//panic("not implemented")
}
return
}

View File

@@ -1,279 +0,0 @@
package ps
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/h264/golomb"
)
const firstByte = 0x67
// Google to "h264 specification pdf"
// https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-201602-S!!PDF-E&type=items
type SPS struct {
Profile string
ProfileIDC uint8
ProfileIOP uint8
LevelIDC uint8
Width uint16
Height uint16
}
func NewSPS(profile string, level uint8, width uint16, height uint16) *SPS {
s := &SPS{
Profile: profile, LevelIDC: level, Width: width, Height: height,
}
s.ProfileIDC, s.ProfileIOP = DecodeProfile(profile)
return s
}
// https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set
func (s *SPS) Marshal() []byte {
w := golomb.NewWriter()
// this is typical SPS for most H264 cameras
w.WriteByte(firstByte)
w.WriteByte(s.ProfileIDC)
w.WriteByte(s.ProfileIOP)
w.WriteByte(s.LevelIDC)
w.WriteUEGolomb(0) // seq_parameter_set_id (0)
w.WriteUEGolomb(0) // log2_max_frame_num_minus4 (depends)
w.WriteUEGolomb(0) // pic_order_cnt_type (0 or 2)
w.WriteUEGolomb(0) // log2_max_pic_order_cnt_lsb_minus4 (depends)
w.WriteUEGolomb(1) // num_ref_frames (1)
w.WriteBit(0) // gaps_in_frame_num_value_allowed_flag (0)
w.WriteUEGolomb(uint8(s.Width>>4) - 1) // pic_width_in_mbs_minus_1
w.WriteUEGolomb(uint8(s.Height>>4) - 1) // pic_height_in_map_units_minus_1
w.WriteBit(1) // frame_mbs_only_flag (1)
w.WriteBit(1) // direct_8x8_inference_flag (1)
w.WriteBit(0) // frame_cropping_flag (0 is OK)
w.WriteBit(0) // vui_prameters_present_flag (0 is OK)
w.WriteBit(1) // rbsp_stop_one_bit
return w.Bytes()
}
func (s *SPS) Unmarshal(data []byte) (err error) {
r := golomb.NewReader(data)
var b byte
var u uint
if b, err = r.ReadByte(); err != nil {
return
}
if b&0x1F != 7 {
err = errors.New("not SPS data")
return
}
if s.ProfileIDC, err = r.ReadByte(); err != nil {
return
}
if s.ProfileIOP, err = r.ReadByte(); err != nil {
return
}
if s.LevelIDC, err = r.ReadByte(); err != nil {
return
}
s.Profile = EncodeProfile(s.ProfileIDC, s.ProfileIOP)
u, err = r.ReadUEGolomb() // seq_parameter_set_id
if s.ProfileIDC == 100 || s.ProfileIDC == 110 || s.ProfileIDC == 122 ||
s.ProfileIDC == 244 || s.ProfileIDC == 44 || s.ProfileIDC == 83 ||
s.ProfileIDC == 86 || s.ProfileIDC == 118 || s.ProfileIDC == 128 ||
s.ProfileIDC == 138 || s.ProfileIDC == 139 || s.ProfileIDC == 134 ||
s.ProfileIDC == 135 {
var n byte
u, err = r.ReadUEGolomb() // chroma_format_idc
if u == 3 {
b, err = r.ReadBit() // separate_colour_plane_flag
n = 12
} else {
n = 8
}
u, err = r.ReadUEGolomb() // bit_depth_luma_minus8
u, err = r.ReadUEGolomb() // bit_depth_chroma_minus8
b, err = r.ReadBit() // qpprime_y_zero_transform_bypass_flag
b, err = r.ReadBit() // seq_scaling_matrix_present_flag
if b > 0 {
for i := byte(0); i < n; i++ {
b, err = r.ReadBit() // seq_scaling_list_present_flag[i]
if b > 0 {
panic("not implemented")
}
}
}
}
u, err = r.ReadUEGolomb() // log2_max_frame_num_minus4
u, err = r.ReadUEGolomb() // pic_order_cnt_type
switch u {
case 0:
u, err = r.ReadUEGolomb() // log2_max_pic_order_cnt_lsb_minus4
case 1:
b, err = r.ReadBit() // delta_pic_order_always_zero_flag
_, err = r.ReadSEGolomb() // offset_for_non_ref_pic
_, err = r.ReadSEGolomb() // offset_for_top_to_bottom_field
u, err = r.ReadUEGolomb() // num_ref_frames_in_pic_order_cnt_cycle
for i := byte(0); i < b; i++ {
_, err = r.ReadSEGolomb() // offset_for_ref_frame[i]
}
}
u, err = r.ReadUEGolomb() // num_ref_frames
b, err = r.ReadBit() // gaps_in_frame_num_value_allowed_flag
u, err = r.ReadUEGolomb() // pic_width_in_mbs_minus_1
s.Width = uint16(u+1) << 4
u, err = r.ReadUEGolomb() // pic_height_in_map_units_minus_1
s.Height = uint16(u+1) << 4
b, err = r.ReadBit() // frame_mbs_only_flag
if b == 0 {
_, err = r.ReadBit()
}
b, err = r.ReadBit() // direct_8x8_inference_flag
b, err = r.ReadBit() // frame_cropping_flag
if b > 0 {
u, err = r.ReadUEGolomb() // frame_crop_left_offset
s.Width -= uint16(u) << 1
u, err = r.ReadUEGolomb() // frame_crop_right_offset
s.Width -= uint16(u) << 1
u, err = r.ReadUEGolomb() // frame_crop_top_offset
s.Height -= uint16(u) << 1
u, err = r.ReadUEGolomb() // frame_crop_bottom_offset
s.Height -= uint16(u) << 1
}
b, err = r.ReadBit() // vui_prameters_present_flag
if b > 0 {
b, err = r.ReadBit() // vui_prameters_present_flag
if b > 0 {
u, err = r.ReadBits(8) // aspect_ratio_idc
if b == 255 {
u, err = r.ReadBits(16) // sar_width
u, err = r.ReadBits(16) // sar_height
}
}
b, err = r.ReadBit() // overscan_info_present_flag
if b > 0 {
b, err = r.ReadBit() // overscan_appropriate_flag
}
b, err = r.ReadBit() // video_signal_type_present_flag
if b > 0 {
u, err = r.ReadBits(3) // video_format
b, err = r.ReadBit() // video_full_range_flag
b, err = r.ReadBit() // colour_description_present_flag
if b > 0 {
u, err = r.ReadBits(8) // colour_primaries
u, err = r.ReadBits(8) // transfer_characteristics
u, err = r.ReadBits(8) // matrix_coefficients
}
}
b, err = r.ReadBit() // chroma_loc_info_present_flag
if b > 0 {
u, err = r.ReadUEGolomb() // chroma_sample_loc_type_top_field
u, err = r.ReadUEGolomb() // chroma_sample_loc_type_bottom_field
}
b, err = r.ReadBit() // timing_info_present_flag
if b > 0 {
u, err = r.ReadBits(32) // num_units_in_tick
u, err = r.ReadBits(32) // time_scale
b, err = r.ReadBit() // fixed_frame_rate_flag
}
b, err = r.ReadBit() // nal_hrd_parameters_present_flag
if b > 0 {
//panic("not implemented")
return nil
}
b, err = r.ReadBit() // vcl_hrd_parameters_present_flag
if b > 0 {
//panic("not implemented")
return nil
}
// if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag)
// b, err = r.ReadBit() // low_delay_hrd_flag
b, err = r.ReadBit() // pic_struct_present_flag
b, err = r.ReadBit() // bitstream_restriction_flag
if b > 0 {
b, err = r.ReadBit() // motion_vectors_over_pic_boundaries_flag
u, err = r.ReadUEGolomb() // max_bytes_per_pic_denom
u, err = r.ReadUEGolomb() // max_bits_per_mb_denom
u, err = r.ReadUEGolomb() // log2_max_mv_length_horizontal
u, err = r.ReadUEGolomb() // log2_max_mv_length_vertical
u, err = r.ReadUEGolomb() // max_num_reorder_frames
u, err = r.ReadUEGolomb() // max_dec_frame_buffering
}
}
b, err = r.ReadBit() // rbsp_stop_one_bit
return
}
func EncodeProfile(idc, iop byte) string {
// https://datatracker.ietf.org/doc/html/rfc6184#page-41
switch {
// 4240xx 42C0xx 42E0xx
case idc == 0x42 && iop&0b01001111 == 0b01000000:
return "CB"
case idc == 0x4D && iop&0b10001111 == 0b10000000:
return "CB"
case idc == 0x58 && iop&0b11001111 == 0b11000000:
return "CB"
// 4200xx
case idc == 0x42 && iop&0b01001111 == 0:
return "B"
case idc == 0x58 && iop&0b11001111 == 0b10000000:
return "B"
// 4d40xx
case idc == 0x4D && iop&0b10101111 == 0:
return "M"
case idc == 0x58 && iop&0b11001111 == 0:
return "E"
case idc == 0x64 && iop == 0:
return "H"
case idc == 0x6E && iop == 0:
return "H10"
}
return ""
}
func DecodeProfile(profile string) (idc, iop byte) {
switch profile {
case "CB":
return 0x42, 0b01000000
case "B":
return 0x42, 0 // 66
case "M":
return 0x4D, 0 // 77
case "E":
return 0x58, 0 // 88
case "H":
return 0x64, 0
}
return 0, 0
}

View File

@@ -2,7 +2,9 @@ package h264
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264/annexb"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
@@ -15,7 +17,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
depack := &codecs.H264Packet{IsAVC: true}
sps, pps := GetParameterSet(codec.FmtpLine)
ps := EncodeAVC(sps, pps)
ps := JoinNALU(sps, pps)
buf := make([]byte, 0, 512*1024) // 512K
@@ -81,7 +83,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
// https://github.com/AlexxIT/WebRTC/issues/391
// https://github.com/AlexxIT/WebRTC/issues/392
AnnexB2AVC(payload)
payload = annexb.EncodeToAVCC(payload, false)
}
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)

211
pkg/h264/sps.go Normal file
View File

@@ -0,0 +1,211 @@
package h264
import "github.com/AlexxIT/go2rtc/pkg/bits"
// http://www.itu.int/rec/T-REC-H.264
// https://webrtc.googlesource.com/src/+/refs/heads/main/common_video/h264/sps_parser.cc
//goland:noinspection GoSnakeCaseUsage
type SPS struct {
profile_idc uint8
profile_iop uint8
level_idc uint8
seq_parameter_set_id uint32
chroma_format_idc uint32
separate_colour_plane_flag byte
bit_depth_luma_minus8 uint32
bit_depth_chroma_minus8 uint32
qpprime_y_zero_transform_bypass_flag byte
seq_scaling_matrix_present_flag byte
log2_max_frame_num_minus4 uint32
pic_order_cnt_type uint32
log2_max_pic_order_cnt_lsb_minus4 uint32
delta_pic_order_always_zero_flag byte
offset_for_non_ref_pic int32
offset_for_top_to_bottom_field int32
num_ref_frames_in_pic_order_cnt_cycle uint32
num_ref_frames uint32
gaps_in_frame_num_value_allowed_flag byte
pic_width_in_mbs_minus_1 uint32
pic_height_in_map_units_minus_1 uint32
frame_mbs_only_flag byte
mb_adaptive_frame_field_flag byte
direct_8x8_inference_flag byte
frame_cropping_flag byte
frame_crop_left_offset uint32
frame_crop_right_offset uint32
frame_crop_top_offset uint32
frame_crop_bottom_offset uint32
vui_parameters_present_flag byte
aspect_ratio_info_present_flag byte
aspect_ratio_idc byte
sar_width uint16
sar_height uint16
overscan_info_present_flag byte
overscan_appropriate_flag byte
video_signal_type_present_flag byte
video_format uint8
video_full_range_flag byte
colour_description_present_flag byte
colour_description uint32
chroma_loc_info_present_flag byte
chroma_sample_loc_type_top_field uint32
chroma_sample_loc_type_bottom_field uint32
timing_info_present_flag byte
num_units_in_tick uint32
time_scale uint32
fixed_frame_rate_flag byte
}
func (s *SPS) Width() uint16 {
width := 16 * (s.pic_width_in_mbs_minus_1 + 1)
crop := 2 * (s.frame_crop_left_offset + s.frame_crop_right_offset)
return uint16(width - crop)
}
func (s *SPS) Height() uint16 {
height := 16 * (s.pic_height_in_map_units_minus_1 + 1)
crop := 2 * (s.frame_crop_top_offset + s.frame_crop_bottom_offset)
if s.frame_mbs_only_flag == 0 {
height *= 2
}
return uint16(height - crop)
}
func DecodeSPS(sps []byte) *SPS {
r := bits.NewReader(sps)
hdr := r.ReadByte()
if hdr&0x1F != NALUTypeSPS {
return nil
}
s := &SPS{
profile_idc: r.ReadByte(),
profile_iop: r.ReadByte(),
level_idc: r.ReadByte(),
seq_parameter_set_id: r.ReadUEGolomb(),
}
switch s.profile_idc {
case 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135:
n := byte(8)
s.chroma_format_idc = r.ReadUEGolomb()
if s.chroma_format_idc == 3 {
s.separate_colour_plane_flag = r.ReadBit()
n = 12
}
s.bit_depth_luma_minus8 = r.ReadUEGolomb()
s.bit_depth_chroma_minus8 = r.ReadUEGolomb()
s.qpprime_y_zero_transform_bypass_flag = r.ReadBit()
s.seq_scaling_matrix_present_flag = r.ReadBit()
if s.seq_scaling_matrix_present_flag != 0 {
for i := byte(0); i < n; i++ {
ssl := r.ReadBit() // seq_scaling_list_present_flag[i]
if ssl != 0 {
return nil // not implemented
}
}
}
}
s.log2_max_frame_num_minus4 = r.ReadUEGolomb()
s.pic_order_cnt_type = r.ReadUEGolomb()
switch s.pic_order_cnt_type {
case 0:
s.log2_max_pic_order_cnt_lsb_minus4 = r.ReadUEGolomb()
case 1:
s.delta_pic_order_always_zero_flag = r.ReadBit()
s.offset_for_non_ref_pic = r.ReadSEGolomb()
s.offset_for_top_to_bottom_field = r.ReadSEGolomb()
s.num_ref_frames_in_pic_order_cnt_cycle = r.ReadUEGolomb()
for i := uint32(0); i < s.num_ref_frames_in_pic_order_cnt_cycle; i++ {
_ = r.ReadSEGolomb() // offset_for_ref_frame[i]
}
}
s.num_ref_frames = r.ReadUEGolomb()
s.gaps_in_frame_num_value_allowed_flag = r.ReadBit()
s.pic_width_in_mbs_minus_1 = r.ReadUEGolomb()
s.pic_height_in_map_units_minus_1 = r.ReadUEGolomb()
s.frame_mbs_only_flag = r.ReadBit()
if s.frame_mbs_only_flag == 0 {
s.mb_adaptive_frame_field_flag = r.ReadBit()
}
s.direct_8x8_inference_flag = r.ReadBit()
s.frame_cropping_flag = r.ReadBit()
if s.frame_cropping_flag != 0 {
s.frame_crop_left_offset = r.ReadUEGolomb()
s.frame_crop_right_offset = r.ReadUEGolomb()
s.frame_crop_top_offset = r.ReadUEGolomb()
s.frame_crop_bottom_offset = r.ReadUEGolomb()
}
s.vui_parameters_present_flag = r.ReadBit()
if s.vui_parameters_present_flag != 0 {
s.aspect_ratio_info_present_flag = r.ReadBit()
if s.aspect_ratio_info_present_flag != 0 {
s.aspect_ratio_idc = r.ReadByte()
if s.aspect_ratio_idc == 255 {
s.sar_width = r.ReadUint16()
s.sar_height = r.ReadUint16()
}
}
s.overscan_info_present_flag = r.ReadBit()
if s.overscan_info_present_flag != 0 {
s.overscan_appropriate_flag = r.ReadBit()
}
s.video_signal_type_present_flag = r.ReadBit()
if s.video_signal_type_present_flag != 0 {
s.video_format = r.ReadBits8(3)
s.video_full_range_flag = r.ReadBit()
s.colour_description_present_flag = r.ReadBit()
if s.colour_description_present_flag != 0 {
s.colour_description = r.ReadUint24()
}
}
s.chroma_loc_info_present_flag = r.ReadBit()
if s.chroma_loc_info_present_flag != 0 {
s.chroma_sample_loc_type_top_field = r.ReadUEGolomb()
s.chroma_sample_loc_type_bottom_field = r.ReadUEGolomb()
}
s.timing_info_present_flag = r.ReadBit()
if s.timing_info_present_flag != 0 {
s.num_units_in_tick = r.ReadUint32()
s.time_scale = r.ReadUint32()
s.fixed_frame_rate_flag = r.ReadBit()
}
//...
}
if r.EOF {
return nil
}
return s
}

View File

@@ -5,7 +5,7 @@ import "github.com/AlexxIT/go2rtc/pkg/h264"
const forbiddenZeroBit = 0x80
const nalUnitType = 0x3F
// DecodeStream - find and return first AU in AVC format
// Deprecated: DecodeStream - find and return first AU in AVC format
// useful for processing live streams with unknown separator size
func DecodeStream(annexb []byte) ([]byte, int) {
startPos := -1

43
pkg/h265/avcc.go Normal file
View File

@@ -0,0 +1,43 @@
// Package h265 - AVCC format related functions
package h265
import (
"bytes"
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func AVCCToCodec(avcc []byte) *core.Codec {
buf := bytes.NewBufferString("profile-id=1")
for {
size := 4 + int(binary.BigEndian.Uint32(avcc))
switch NALUType(avcc) {
case NALUTypeVPS:
buf.WriteString(";sprop-vps=")
buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
case NALUTypeSPS:
buf.WriteString(";sprop-sps=")
buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
case NALUTypePPS:
buf.WriteString(";sprop-pps=")
buf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))
}
if size < len(avcc) {
avcc = avcc[size:]
} else {
break
}
}
return &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
FmtpLine: buf.String(),
PayloadType: core.PayloadTypeRAW,
}
}

19
pkg/h265/h265_test.go Normal file
View File

@@ -0,0 +1,19 @@
package h265
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/require"
)
func TestDecodeSPS(t *testing.T) {
s := "QgEBAWAAAAMAAAMAAAMAAAMAmaAAoAgBaH+KrTuiS7/8AAQABbAgApMuADN/mAE="
b, err := base64.StdEncoding.DecodeString(s)
require.Nil(t, err)
sps := DecodeSPS(b)
require.NotNil(t, sps)
require.Equal(t, uint16(5120), sps.Width())
require.Equal(t, uint16(1440), sps.Height())
}

40
pkg/h265/mpeg4.go Normal file
View File

@@ -0,0 +1,40 @@
// Package h265 - MPEG4 format related functions
package h265
import "encoding/binary"
func EncodeConfig(vps, sps, pps []byte) []byte {
vpsSize := uint16(len(vps))
spsSize := uint16(len(sps))
ppsSize := uint16(len(pps))
buf := make([]byte, 23+5+vpsSize+5+spsSize+5+ppsSize)
buf[0] = 1
copy(buf[1:], sps[3:6]) // profile
buf[21] = 3 // ?
buf[22] = 3 // ?
b := buf[23:]
_ = b[5]
b[0] = (vps[0] >> 1) & 0x3F
binary.BigEndian.PutUint16(b[1:], 1) // VPS count
binary.BigEndian.PutUint16(b[3:], vpsSize)
copy(b[5:], vps)
b = buf[23+5+vpsSize:]
_ = b[5]
b[0] = (sps[0] >> 1) & 0x3F
binary.BigEndian.PutUint16(b[1:], 1) // SPS count
binary.BigEndian.PutUint16(b[3:], spsSize)
copy(b[5:], sps)
b = buf[23+5+vpsSize+5+spsSize:]
_ = b[5]
b[0] = (pps[0] >> 1) & 0x3F
binary.BigEndian.PutUint16(b[1:], 1) // PPS count
binary.BigEndian.PutUint16(b[3:], ppsSize)
copy(b[5:], pps)
return buf
}

Some files were not shown because too many files have changed in this diff Show More