mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-05 00:12:48 +08:00
Compare commits
126 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fc1b6af436 | ||
![]() |
88fb589d2e | ||
![]() |
5c5357cd79 | ||
![]() |
5ffd60c429 | ||
![]() |
5645c73613 | ||
![]() |
13a7957cf3 | ||
![]() |
c0d5a7c01a | ||
![]() |
d87cc9ddb6 | ||
![]() |
b1c4bcc508 | ||
![]() |
6288c2a57f | ||
![]() |
1c569e690d | ||
![]() |
7fdc6b9472 | ||
![]() |
60d7d525f2 | ||
![]() |
f6f2998e85 | ||
![]() |
82d1f2cf0b | ||
![]() |
f00e646612 | ||
![]() |
a101387b26 | ||
![]() |
af31ab604d | ||
![]() |
ccdd6ed490 | ||
![]() |
9f404d965f | ||
![]() |
0621b82aff | ||
![]() |
22787b979d | ||
![]() |
7d65c60711 | ||
![]() |
69da64a49c | ||
![]() |
66c858e00e | ||
![]() |
ef63cec7a8 | ||
![]() |
0ac505ba09 | ||
![]() |
d4444c6257 | ||
![]() |
c6d5bb4eeb | ||
![]() |
7f232c5cf2 | ||
![]() |
dc2ab5fcc0 | ||
![]() |
137b23da10 | ||
![]() |
54e361e3b8 | ||
![]() |
c78da1a7a9 | ||
![]() |
27673cb0c1 | ||
![]() |
c040a02fa8 | ||
![]() |
a664e3b838 | ||
![]() |
317b3b5eeb | ||
![]() |
9f14b30aae | ||
![]() |
065a6f4f46 | ||
![]() |
9f9dc7e844 | ||
![]() |
b1c0a28366 | ||
![]() |
fc963dfe5c | ||
![]() |
6f5ba2ade6 | ||
![]() |
ea708bb606 | ||
![]() |
0822326900 | ||
![]() |
79fc0cd395 | ||
![]() |
357e7c1b18 | ||
![]() |
71f1e445e1 | ||
![]() |
20efe22e60 | ||
![]() |
75a3dad745 | ||
![]() |
f5cca50830 | ||
![]() |
8cd977f7ad | ||
![]() |
90f2a9e106 | ||
![]() |
e0ad358aa9 | ||
![]() |
3db4002420 | ||
![]() |
bf248c49c3 | ||
![]() |
69a3a30a0e | ||
![]() |
f80f179e4c | ||
![]() |
c1c1d84cef | ||
![]() |
c431d888f0 | ||
![]() |
2ebb791eb7 | ||
![]() |
00b818b4d7 | ||
![]() |
ce1b0d442c | ||
![]() |
5283c9781c | ||
![]() |
279d8bf799 | ||
![]() |
7114d63ba6 | ||
![]() |
120ae89578 | ||
![]() |
d1eb623fd6 | ||
![]() |
873cf65317 | ||
![]() |
2091dead3f | ||
![]() |
2ffd859f0e | ||
![]() |
da02a97a00 | ||
![]() |
fb51dc781d | ||
![]() |
32bf64028d | ||
![]() |
2e4e75e386 | ||
![]() |
f67f6e5b9f | ||
![]() |
24039218a1 | ||
![]() |
1f447ef73c | ||
![]() |
4509198eef | ||
![]() |
bc60cbefb8 | ||
![]() |
a9118562a9 | ||
![]() |
24637be7c2 | ||
![]() |
d74be47696 | ||
![]() |
76a00031cd | ||
![]() |
063a192699 | ||
![]() |
b016b7dc2a | ||
![]() |
42f6441512 | ||
![]() |
dd066ba040 | ||
![]() |
b3def6cfa2 | ||
![]() |
4a82eb3503 | ||
![]() |
c3ba8db660 | ||
![]() |
4e1a0e1ab9 | ||
![]() |
1dd3dbbcd8 | ||
![]() |
e1be2d9e48 | ||
![]() |
8fbfccd024 | ||
![]() |
de6bb33f01 | ||
![]() |
3a40515a90 | ||
![]() |
5d533338d0 | ||
![]() |
f412852d50 | ||
![]() |
5fbec487e2 | ||
![]() |
19c61e20c0 | ||
![]() |
0b6fda2af5 | ||
![]() |
e9795e7521 | ||
![]() |
3b8413a9dd | ||
![]() |
b2f9ad7efb | ||
![]() |
4baa3f5588 | ||
![]() |
9c5ae3260c | ||
![]() |
b7baef0a48 | ||
![]() |
8778d7c9ab | ||
![]() |
d275997e54 | ||
![]() |
2faea1bb69 | ||
![]() |
ba6c96412b | ||
![]() |
ed38122752 | ||
![]() |
922587ed2e | ||
![]() |
8e7c9d19e4 | ||
![]() |
0f33ef0fc5 | ||
![]() |
a14c87ad60 | ||
![]() |
6d82b1ce89 | ||
![]() |
d73e9f6bcf | ||
![]() |
e6a87fbd69 | ||
![]() |
3defbd60db | ||
![]() |
6e9574a1bd | ||
![]() |
7005cd08f2 | ||
![]() |
e94f338b77 | ||
![]() |
d6172587b3 |
189
.github/workflows/build.yml
vendored
Normal file
189
.github/workflows/build.yml
vendored
Normal 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
|
78
.github/workflows/docker.yml
vendored
78
.github/workflows/docker.yml
vendored
@@ -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
|
99
.github/workflows/release.yml
vendored
99
.github/workflows/release.yml
vendored
@@ -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
|
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -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
5
.gitignore
vendored
@@ -1,8 +1,7 @@
|
||||
|
||||
.idea/
|
||||
|
||||
.tmp/
|
||||
|
||||
go2rtc.yaml
|
||||
|
||||
go2rtc.json
|
||||
|
||||
0_test.go
|
||||
|
@@ -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
107
README.md
@@ -3,6 +3,7 @@
|
||||
[](https://github.com/AlexxIT/go2rtc/stargazers)
|
||||
[](https://hub.docker.com/r/alexxit/go2rtc)
|
||||
[](https://github.com/AlexxIT/go2rtc/releases)
|
||||
[](https://goreportcard.com/report/github.com/AlexxIT/go2rtc)
|
||||
|
||||
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
39
go.mod
@@ -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
80
go.sum
@@ -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=
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
35
internal/app/migrate.go
Normal 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)
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/rs/zerolog/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const name = "go2rtc.json"
|
||||
|
||||
var store map[string]any
|
||||
|
||||
func load() {
|
||||
data, _ := os.ReadFile(name)
|
||||
if data != nil {
|
||||
if err := json.Unmarshal(data, &store); err != nil {
|
||||
// TODO: log
|
||||
log.Warn().Err(err).Msg("[app] read storage")
|
||||
}
|
||||
}
|
||||
|
||||
if store == nil {
|
||||
store = make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
func save() error {
|
||||
data, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(name, data, 0644)
|
||||
}
|
||||
|
||||
func GetRaw(key string) any {
|
||||
if store == nil {
|
||||
load()
|
||||
}
|
||||
|
||||
return store[key]
|
||||
}
|
||||
|
||||
func GetDict(key string) map[string]any {
|
||||
raw := GetRaw(key)
|
||||
if raw != nil {
|
||||
return raw.(map[string]any)
|
||||
}
|
||||
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
func Set(key string, v any) error {
|
||||
if store == nil {
|
||||
load()
|
||||
}
|
||||
|
||||
store[key] = v
|
||||
|
||||
return save()
|
||||
}
|
@@ -3,9 +3,10 @@ package debug
|
||||
import (
|
||||
"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"),
|
||||
|
@@ -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",
|
||||
})
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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())
|
||||
}
|
||||
|
@@ -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,
|
||||
})
|
||||
}
|
||||
|
@@ -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",
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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",
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
263
internal/homekit/server.go
Normal 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])
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
})
|
||||
|
@@ -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()
|
||||
|
@@ -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
36
internal/mpegts/aac.go
Normal 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)
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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")
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
149
internal/streams/add_consumer.go
Normal file
149
internal/streams/add_consumer.go
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
|
@@ -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
220
internal/webrtc/kinesis.go
Normal 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
168
internal/webrtc/openipc.go
Normal 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)
|
||||
}
|
@@ -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":
|
||||
|
@@ -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
124
pkg/aac/aac.go
Normal 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
43
pkg/aac/aac_test.go
Normal 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
131
pkg/aac/adts.go
Normal 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
58
pkg/aac/consumer.go
Normal 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()
|
||||
}
|
@@ -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
129
pkg/bits/reader.go
Normal 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
95
pkg/bits/writer.go
Normal 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
|
||||
}
|
@@ -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:],
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
112
pkg/core/readbuffer.go
Normal 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
|
||||
}
|
64
pkg/core/readbuffer_test.go
Normal file
64
pkg/core/readbuffer_test.go
Normal 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)
|
||||
}
|
@@ -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
111
pkg/core/writebuffer.go
Normal 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
58
pkg/debug/debug.go
Normal 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)
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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
|
||||
}
|
@@ -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
200
pkg/flv/amf/amf.go
Normal 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
234
pkg/flv/producer.go
Normal 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
|
||||
}
|
@@ -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
141
pkg/h264/annexb/annexb.go
Normal 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
|
||||
}
|
103
pkg/h264/avc.go
103
pkg/h264/avc.go
@@ -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
111
pkg/h264/avcc.go
Normal 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,
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
85
pkg/h264/h264_test.go
Normal 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
101
pkg/h264/mpeg4.go
Normal 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,
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
211
pkg/h264/sps.go
Normal 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
|
||||
}
|
@@ -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
43
pkg/h265/avcc.go
Normal 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
19
pkg/h265/h265_test.go
Normal 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
40
pkg/h265/mpeg4.go
Normal 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
Reference in New Issue
Block a user