mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-27 04:36:12 +08:00
Compare commits
217 Commits
v0.1-beta.
...
v0.1-rc.7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
89eb653d67 | ||
![]() |
0e49ffdfff | ||
![]() |
bd2fc1252d | ||
![]() |
78ac88448c | ||
![]() |
4cd9757e53 | ||
![]() |
f9cb6fd670 | ||
![]() |
57fa6a5530 | ||
![]() |
6906b56524 | ||
![]() |
c9b0806c84 | ||
![]() |
a9d1e64f88 | ||
![]() |
9e9f07f3f7 | ||
![]() |
b51aabd3d9 | ||
![]() |
368562c540 | ||
![]() |
6d6e7010b4 | ||
![]() |
4157a53dd8 | ||
![]() |
bdf5654c01 | ||
![]() |
66f729aa0e | ||
![]() |
96d1ef2d2c | ||
![]() |
9739f7f416 | ||
![]() |
654fa32b3a | ||
![]() |
db2263c7fe | ||
![]() |
e6c36f1cf7 | ||
![]() |
110f90cb34 | ||
![]() |
aca3bab238 | ||
![]() |
4df44645d7 | ||
![]() |
097fdfbbb8 | ||
![]() |
dc21a04da7 | ||
![]() |
db255b476a | ||
![]() |
464ea417ef | ||
![]() |
c1fac66329 | ||
![]() |
a6057a2eca | ||
![]() |
7c69ba13b0 | ||
![]() |
2b8bfe8bd9 | ||
![]() |
0bd54da456 | ||
![]() |
9f6af1c9e4 | ||
![]() |
c9dd0e37e4 | ||
![]() |
562872beb8 | ||
![]() |
46a278c067 | ||
![]() |
270fc7c1b6 | ||
![]() |
6feb635522 | ||
![]() |
6f48131e4d | ||
![]() |
f120db71a3 | ||
![]() |
72823af9d0 | ||
![]() |
15d9d4ebf4 | ||
![]() |
b09bbd79c4 | ||
![]() |
1830273f02 | ||
![]() |
07f3972794 | ||
![]() |
4c2ebd20bc | ||
![]() |
440c7bd6e1 | ||
![]() |
74c3510a10 | ||
![]() |
c2748fc77b | ||
![]() |
d334551591 | ||
![]() |
cfe20925ac | ||
![]() |
5b39f78ace | ||
![]() |
b965c191b7 | ||
![]() |
7057b4846f | ||
![]() |
a746b96adc | ||
![]() |
b7718b33b8 | ||
![]() |
69b17230f3 | ||
![]() |
e2ecd909ab | ||
![]() |
ea79da0d53 | ||
![]() |
e64919838c | ||
![]() |
162b11213d | ||
![]() |
d27acbd7e3 | ||
![]() |
a692ecd7c1 | ||
![]() |
98c5366ba9 | ||
![]() |
3eaaa3fcfa | ||
![]() |
7409b32836 | ||
![]() |
e2cfdf8419 | ||
![]() |
4c0929d854 | ||
![]() |
258a0ffb91 | ||
![]() |
999e81c2dd | ||
![]() |
8c6729027b | ||
![]() |
d3bd5eeab5 | ||
![]() |
dbbf2ea310 | ||
![]() |
b8234e0c76 | ||
![]() |
96cd753e27 | ||
![]() |
c522e5bb08 | ||
![]() |
a16d8acc30 | ||
![]() |
684878b4b1 | ||
![]() |
140a742cee | ||
![]() |
1b518b94fd | ||
![]() |
5678121c50 | ||
![]() |
4915f12bde | ||
![]() |
bb91240b95 | ||
![]() |
b1d5d53832 | ||
![]() |
31fbbf91bb | ||
![]() |
a3f72fbab9 | ||
![]() |
fae59c7992 | ||
![]() |
aff34f1d21 | ||
![]() |
65e7efa775 | ||
![]() |
3c3e9d282b | ||
![]() |
bd51069086 | ||
![]() |
1ddf7f1a6c | ||
![]() |
0e281e36d3 | ||
![]() |
3d6472cfb1 | ||
![]() |
7c31fa2ffd | ||
![]() |
0ed9d2410a | ||
![]() |
1c89e7945e | ||
![]() |
48635ae341 | ||
![]() |
fdb316910f | ||
![]() |
e29f2594fa | ||
![]() |
c3da7584b0 | ||
![]() |
1e247cba92 | ||
![]() |
01631d9eb0 | ||
![]() |
4b27d119f0 | ||
![]() |
dd55c03dc2 | ||
![]() |
a4eab1944a | ||
![]() |
eea413a36c | ||
![]() |
cdd42a8ed2 | ||
![]() |
4815ce1baf | ||
![]() |
e6d3939c78 | ||
![]() |
220b9ca318 | ||
![]() |
d625620dfd | ||
![]() |
dd503f3410 | ||
![]() |
3e8e87bfcc | ||
![]() |
64d218886e | ||
![]() |
e91ccc211e | ||
![]() |
9f8a219483 | ||
![]() |
b617796941 | ||
![]() |
77888fe086 | ||
![]() |
7bc3534bcb | ||
![]() |
77bc0630d6 | ||
![]() |
2f68711405 | ||
![]() |
b8cab5db60 | ||
![]() |
eae01be71f | ||
![]() |
0127115180 | ||
![]() |
aef84cef6b | ||
![]() |
d478436758 | ||
![]() |
f77db44529 | ||
![]() |
149d1bf235 | ||
![]() |
b650475b10 | ||
![]() |
16e5406156 | ||
![]() |
49f6233bde | ||
![]() |
78c5c70c73 | ||
![]() |
32651c74ab | ||
![]() |
5c64d1f847 | ||
![]() |
717af29630 | ||
![]() |
ea18475d31 | ||
![]() |
701a9c69ec | ||
![]() |
c06253c8b2 | ||
![]() |
3a07e9fa03 | ||
![]() |
e1bc30fab3 | ||
![]() |
d16ae0972f | ||
![]() |
8b93c97e69 | ||
![]() |
d8158bc1e3 | ||
![]() |
f4f588d2c6 | ||
![]() |
e287b52808 | ||
![]() |
ff96257252 | ||
![]() |
909f21b7e4 | ||
![]() |
7d6a5b44f8 | ||
![]() |
278f7696b6 | ||
![]() |
3cbf2465ae | ||
![]() |
e9ea7a0b1f | ||
![]() |
0231fc3a90 | ||
![]() |
9ef2633840 | ||
![]() |
5a8df3e90a | ||
![]() |
a31cbec3eb | ||
![]() |
54f547977e | ||
![]() |
65d91e02bd | ||
![]() |
7fc3f0f641 | ||
![]() |
7725d5ed31 | ||
![]() |
6c1b9daa8b | ||
![]() |
6d432574bf | ||
![]() |
616f69c88b | ||
![]() |
f72440712b | ||
![]() |
ceed146fb8 | ||
![]() |
f17dadbbbf | ||
![]() |
3d4514eab9 | ||
![]() |
2629dccb81 | ||
![]() |
04f1aa2900 | ||
![]() |
0dacdea1c3 | ||
![]() |
24082b1616 | ||
![]() |
7964b1743b | ||
![]() |
49773a1ece | ||
![]() |
c97a48a73f | ||
![]() |
e03231ebb4 | ||
![]() |
649525a842 | ||
![]() |
d411c1a25c | ||
![]() |
2f0bcf4ae0 | ||
![]() |
831c504cab | ||
![]() |
12925a6bc5 | ||
![]() |
e50e929150 | ||
![]() |
d0c87e0379 | ||
![]() |
247b61790e | ||
![]() |
2ec618334a | ||
![]() |
6f9976c806 | ||
![]() |
17b3a4cf3a | ||
![]() |
ba30f46c02 | ||
![]() |
4134f2a89c | ||
![]() |
a81160bea1 | ||
![]() |
80392acb78 | ||
![]() |
5afac513b4 | ||
![]() |
2243110e08 | ||
![]() |
04a6e64650 | ||
![]() |
62c13f016b | ||
![]() |
9596c6139f | ||
![]() |
34f5b99126 | ||
![]() |
b562392d45 | ||
![]() |
eb8a4919a2 | ||
![]() |
237fbf23a1 | ||
![]() |
12a73b00cb | ||
![]() |
ce0fac959f | ||
![]() |
1b14be7033 | ||
![]() |
bbbade4097 | ||
![]() |
8f43ad2a35 | ||
![]() |
105331d50f | ||
![]() |
a45d0b507b | ||
![]() |
407ccc45bc | ||
![]() |
428628fcce | ||
![]() |
fa23bb6899 | ||
![]() |
71e1c840a7 | ||
![]() |
63b9639e86 | ||
![]() |
ae3e1372c8 | ||
![]() |
800ebb39be | ||
![]() |
3a10cb25bb | ||
![]() |
7784b0e64c |
59
.github/workflows/builder.yml
vendored
59
.github/workflows/builder.yml
vendored
@@ -1,59 +0,0 @@
|
||||
# https://github.com/home-assistant/builder
|
||||
name: 'Builder'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ 'v*' ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
hassio:
|
||||
name: Hassio Addon
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Branch name
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
echo "REPO=alexxit/go2rtc" >> $GITHUB_ENV
|
||||
echo "TAG=${VERSION}" >> $GITHUB_ENV
|
||||
echo "IMAGE=alexxit/go2rtc:${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build amd64
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --amd64 --target build/hassio --version $TAG-amd64 --no-latest --docker-hub-check
|
||||
|
||||
- name: Build i386
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --i386 --target build/hassio --version $TAG-i386 --no-latest --docker-hub-check
|
||||
|
||||
- name: Build aarch64
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --aarch64 --target build/hassio --version $TAG-aarch64 --no-latest --docker-hub-check
|
||||
|
||||
- name: Build armv7
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --armv7 --target build/hassio --version $TAG-armv7 --no-latest --docker-hub-check
|
||||
|
||||
- name: Docker manifest
|
||||
run: |
|
||||
# thanks to https://github.com/aler9/rtsp-simple-server/blob/main/Makefile
|
||||
docker manifest create "${IMAGE}" \
|
||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
||||
docker manifest push "${IMAGE}"
|
||||
|
||||
docker manifest create "${REPO}:latest" \
|
||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
||||
docker manifest push "${REPO}:latest"
|
75
.github/workflows/ci.yml
vendored
Normal file
75
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: ci
|
||||
|
||||
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: alexxit/go2rtc
|
||||
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: alexxit/go2rtc
|
||||
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@v3
|
||||
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 }}
|
||||
|
||||
- name: Build and push Hardware
|
||||
uses: docker/build-push-action@v3
|
||||
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 }}
|
62
Dockerfile
Normal file
62
Dockerfile
Normal file
@@ -0,0 +1,62 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG GO_VERSION="1.19"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GOOS=${TARGETOS}
|
||||
ENV GOARCH=${TARGETARCH}
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Collect all files
|
||||
FROM scratch AS rootfs
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
COPY ./build/docker/run.sh /
|
||||
|
||||
|
||||
# 3. Final image
|
||||
FROM base
|
||||
|
||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
RUN apk add --no-cache tini ffmpeg bash curl jq
|
||||
|
||||
# Hardware Acceleration for Intel CPU (+50MB)
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver intel-media-driver; fi
|
||||
|
||||
# Hardware: AMD and NVidia VAAPI (not sure about this)
|
||||
# RUN libva-glx mesa-va-gallium
|
||||
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||
|
||||
COPY --from=rootfs / /
|
||||
|
||||
RUN chmod a+x /run.sh && mkdir -p /config
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
|
||||
CMD ["/run.sh"]
|
318
README.md
318
README.md
@@ -6,9 +6,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
|
||||
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
|
||||
- zero-delay for many supported protocols (lowest possible streaming latency)
|
||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device), [files](#source-ffmpeg) and [other sources](#module-streams)
|
||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc) or [MSE](#module-mp4)
|
||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [HTTP](#source-http) (FLV/MJPEG/JPEG), [FFmpeg](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
|
||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) 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 ([read more](https://github.com/AlexxIT/Blog/issues/5))
|
||||
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
||||
- mixing tracks from different sources to single stream
|
||||
@@ -26,35 +27,6 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
- [MediaSoup](https://mediasoup.org/) framework routing idea
|
||||
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
|
||||
|
||||
## Codecs negotiation
|
||||
|
||||
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
|
||||
|
||||
- this camera support 2-way audio standard **ONVIF Profile T**
|
||||
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
|
||||
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
|
||||
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
|
||||
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
|
||||
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
|
||||
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
|
||||
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
|
||||
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
|
||||
|
||||
Now you have stream with two sources - **RTSP and FFmpeg**:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
|
||||
```
|
||||
|
||||
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
|
||||
|
||||

|
||||
|
||||
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
|
||||
|
||||
## Fast start
|
||||
|
||||
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
|
||||
@@ -78,13 +50,14 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
|
||||
|
||||
- `go2rtc_win64.zip` - Windows 64-bit
|
||||
- `go2rtc_win32.zip` - Windows 32-bit
|
||||
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
|
||||
- `go2rtc_linux_amd64` - Linux 64-bit
|
||||
- `go2rtc_linux_i386` - Linux 32-bit
|
||||
- `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_mipsel` - Linux on MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
|
||||
- `go2rtc_mac_amd64` - Mac with Intel
|
||||
- `go2rtc_mac_arm64` - Mac with M1
|
||||
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
|
||||
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
|
||||
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
|
||||
|
||||
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
@@ -97,10 +70,6 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
- go2rtc > Install > Start
|
||||
2. Setup [Integration](#module-hass)
|
||||
|
||||
**Optionally:**
|
||||
|
||||
- create `go2rtc.yaml` in your Home Assistant [config](https://www.home-assistant.io/docs/configuration) folder
|
||||
|
||||
### go2rtc: Docker
|
||||
|
||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
|
||||
@@ -131,8 +100,10 @@ Available modules:
|
||||
- [api](#module-api) - HTTP API (important for WebRTC support)
|
||||
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
||||
- [webrtc](#module-webrtc) - WebRTC Server
|
||||
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
|
||||
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
||||
- [hass](#module-hass) - Home Assistant integration
|
||||
- [log](#module-log) - logs config
|
||||
|
||||
@@ -144,15 +115,15 @@ Available source types:
|
||||
|
||||
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
|
||||
- [rtmp](#source-rtmp) - `RTMP` streams
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types)
|
||||
- [http](#source-http) - `HTTP-FLV`, `JPEG` (snapshots), `MJPEG` streams
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others)
|
||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||
- [echo](#source-echo) - get stream link via bash or python
|
||||
- [echo](#source-echo) - get stream link from bash or python
|
||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
|
||||
- [hass](#source-hass) - Home Assistant integration
|
||||
|
||||
**PS.** You can use sources like `MJPEG`, `HLS` and others via FFmpeg integration.
|
||||
|
||||
#### Source: RTSP
|
||||
|
||||
- Support **RTSP and RTSPS** links with multiple video and audio tracks
|
||||
@@ -176,15 +147,39 @@ streams:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
|
||||
```
|
||||
|
||||
**PS.** For disable bachannel just add `#backchannel=0` to end of RTSP link.
|
||||
|
||||
#### Source: RTMP
|
||||
|
||||
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
|
||||
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp).
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
rtmp_stream: rtmp://192.168.1.123/live/camera1
|
||||
```
|
||||
|
||||
#### Source: HTTP
|
||||
|
||||
Support Content-Type:
|
||||
|
||||
- **HTTP-FLV** (`video/x-flv`) - same as RTMP, but over HTTP
|
||||
- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream
|
||||
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# [HTTP-FLV] stream in video/x-flv format
|
||||
http_flv: http://192.168.1.123:20880/api/camera/stream/780900131155/657617
|
||||
|
||||
# [JPEG] snapshots from Dahua camera, will be converted to MJPEG stream
|
||||
dahua_snap: http://admin:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1
|
||||
|
||||
# [MJPEG] stream will be proxied without modification
|
||||
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
|
||||
```
|
||||
|
||||
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
|
||||
|
||||
#### Source: FFmpeg
|
||||
|
||||
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
|
||||
@@ -209,24 +204,27 @@ streams:
|
||||
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
|
||||
|
||||
# [MJPEG] video will be transcoded to H264
|
||||
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264
|
||||
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
|
||||
|
||||
# [RTSP] video with rotation, should be transcoded, so select H264
|
||||
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#raw=-vf transpose=1#video=h264
|
||||
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||
```
|
||||
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac/16000`.
|
||||
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
|
||||
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
bin: ffmpeg # path to ffmpeg binary
|
||||
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
|
||||
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
|
||||
mycodec: "-any args that support ffmpeg..."
|
||||
```
|
||||
|
||||
Also you can use `raw` param for any additional FFmpeg arguments. As example for video rotation (`#raw=-vf transpose=1`). Remember that rotation is not possible without transcoding, so add supported codec as second param (`#video=h264`).
|
||||
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
|
||||
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
|
||||
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
|
||||
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
|
||||
|
||||
#### Source: FFmpeg Device
|
||||
|
||||
@@ -281,8 +279,35 @@ You can pair device with go2rtc on the HomeKit page. If you can't see your devic
|
||||
|
||||
If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it.
|
||||
|
||||
**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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
|
||||
|
||||
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
|
||||
|
||||
#### Source: Ivideon
|
||||
|
||||
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0
|
||||
```
|
||||
|
||||
#### Source: Hass
|
||||
|
||||
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
|
||||
@@ -299,11 +324,40 @@ streams:
|
||||
aqara_g3: hass:Camera-Hub-G3-AB12
|
||||
```
|
||||
|
||||
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
|
||||
|
||||
### Module: API
|
||||
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
|
||||
|
||||
- you can use WebRTC only when HTTP API enabled
|
||||
go2rtc has its own JS video player (`video-rtc.js`) with:
|
||||
|
||||
- support technologies:
|
||||
- WebRTC over UDP or TCP
|
||||
- MSE or MP4 or MJPEG over WebSocket
|
||||
- automatic selection best technology according on:
|
||||
- codecs inside your stream
|
||||
- current browser capabilities
|
||||
- current network configuration
|
||||
- automatic stop stream while browser or page not active
|
||||
- automatic stop stream while player not inside page viewport
|
||||
- automatic reconnection
|
||||
|
||||
Technology selection based on priorities:
|
||||
|
||||
1. Video and Audio better than just Video
|
||||
2. H265 better than H264
|
||||
3. WebRTC better than MSE, than MP4, than MJPEG
|
||||
|
||||
go2rtc has simple HTML page (`stream.html`) with support params in URL:
|
||||
|
||||
- multiple streams on page `src=camera1&src=camera2...`
|
||||
- stream technology autoselection `mode=webrtc,mse,mp4,mjpeg`
|
||||
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
|
||||
- player width setting in pixels `width=320px` or percents `width=50%`
|
||||
|
||||
**Module config**
|
||||
|
||||
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
||||
@@ -311,30 +365,36 @@ The HTTP API is the main part for interacting with the application. Default addr
|
||||
|
||||
```yaml
|
||||
api:
|
||||
listen: ":1984" # HTTP API port ("" - disabled)
|
||||
base_path: "" # API prefix for serve on suburl
|
||||
static_dir: "" # folder for static files (custom web interface)
|
||||
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
|
||||
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
|
||||
static_dir: "www" # default "", folder for static files (custom web interface)
|
||||
origin: "*" # default "", allow CORS requests (only * supported)
|
||||
```
|
||||
|
||||
**PS. go2rtc** don't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks.
|
||||
**PS. go2rtc** doesn't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks.
|
||||
|
||||
**PS2.** You can access microphone (for 2-way audio) only with HTTPS
|
||||
**PS2.** You can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
|
||||
|
||||
**PS3.** MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446).
|
||||
|
||||
**PS4.** MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4.
|
||||
|
||||
### Module: RTSP
|
||||
|
||||
You can get any stream as RTSP-stream with codecs filter:
|
||||
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
|
||||
|
||||
```
|
||||
rtsp://192.168.1.123/{stream_name}?video={codec}&audio={codec1}&audio={codec2}
|
||||
```
|
||||
|
||||
- you can omit the codecs, so one first video and one first audio will be selected
|
||||
- you can omit the codec filters, so one first video and one first audio will be selected
|
||||
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
|
||||
- you can set multiple video or audio, so all of them will be selected
|
||||
- you can enable external password protection for your RTSP streams
|
||||
|
||||
Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server)
|
||||
|
||||
```yaml
|
||||
rtsp:
|
||||
listen: ":8554"
|
||||
listen: ":8554" # RTSP Server TCP port, default - 8554
|
||||
username: admin # optional, default - disabled
|
||||
password: pass # optional, default - disabled
|
||||
```
|
||||
|
||||
### Module: WebRTC
|
||||
@@ -384,13 +444,13 @@ ngrok:
|
||||
command: ...
|
||||
```
|
||||
|
||||
**Own TCP-tunnel**
|
||||
**Hard tech way 1. Own TCP-tunnel**
|
||||
|
||||
If you have personal VPS, you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
|
||||
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
|
||||
|
||||
**Using TURN-server**
|
||||
**Hard tech way 2. Using TURN-server**
|
||||
|
||||
TODO...
|
||||
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can install TURN server (e.g. [coturn](https://github.com/coturn/coturn), config [example](https://github.com/AlexxIT/WebRTC/wiki/Coturn-Example)).
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
@@ -457,22 +517,31 @@ tunnels:
|
||||
|
||||
### Module: Hass
|
||||
|
||||
**go2rtc** compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration.
|
||||
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
|
||||
|
||||
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example:
|
||||
#### From go2rtc to Hass
|
||||
|
||||
- `http://127.0.0.1:1984/` to web interface
|
||||
- `rtsp://127.0.0.1:8554/camera1` to RTSP streams
|
||||
Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency.
|
||||
|
||||
In other cases you need to use IP-address of server with **go2rtc** application.
|
||||
1. Add your stream to [go2rtc config](#configuration)
|
||||
2. Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1`
|
||||
|
||||
1. Add integration with link to go2rtc HTTP API:
|
||||
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
|
||||
2. Add generic camera with RTSP link:
|
||||
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://...` or `rtmp://...`
|
||||
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc**
|
||||
#### From Hass to go2rtc
|
||||
|
||||
View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
|
||||
|
||||
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
|
||||
|
||||
1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
|
||||
2. RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
|
||||
3. Use Picture Entity or Picture Glance lovelace card
|
||||
4. Open full screen card - this is should be WebRTC stream
|
||||
|
||||
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
|
||||
```
|
||||
|
||||
PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
|
||||
|
||||
@@ -482,7 +551,32 @@ Provides several features:
|
||||
|
||||
1. MSE stream (fMP4 over WebSocket)
|
||||
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/)
|
||||
3. Progressive MP4 stream - bad format for streaming because of high latency, doesn't work in Safari
|
||||
3. MP4 "file stream" - bad format for streaming because of high latency, doesn't work in Safari
|
||||
|
||||
### Module: MJPEG
|
||||
|
||||
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API.
|
||||
|
||||
You can receive an MJPEG stream in several ways:
|
||||
|
||||
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
|
||||
- some cameras has HTTP link with [MJPEG stream](#source-http)
|
||||
- some cameras has HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
|
||||
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
|
||||
|
||||
With this example, your stream will have both H264 and MJPEG codecs:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1:
|
||||
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||
- ffmpeg:camera1#video=mjpeg
|
||||
```
|
||||
|
||||
API examples:
|
||||
|
||||
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
|
||||
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
|
||||
|
||||
### Module: Log
|
||||
|
||||
@@ -525,6 +619,74 @@ If you need Web interface protection without Home Assistant Add-on - you need to
|
||||
|
||||
PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port.
|
||||
|
||||
## Codecs madness
|
||||
|
||||
`AVC/H.264` codec can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
||||
|
||||
| Device | WebRTC | MSE | MP4 |
|
||||
|---------------------|-------------|-------------|-------------|
|
||||
| *latency* | best | medium | bad |
|
||||
| Desktop Chrome 107+ | H264 | H264, H265* | H264, H265* |
|
||||
| Desktop Edge | H264 | H264, H265* | H264, H265* |
|
||||
| Desktop Safari | H264, H265* | H264, H265 | **no!** |
|
||||
| Desktop Firefox | H264 | H264 | H264 |
|
||||
| Android Chrome 107+ | H264 | H264, H265* | H264 |
|
||||
| iPad Safari 13+ | H264, H265* | H264, H265 | **no!** |
|
||||
| iPhone Safari 13+ | H264, H265* | **no!** | **no!** |
|
||||
| masOS Hass App | no | no | no |
|
||||
|
||||
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
|
||||
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
|
||||
- Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265
|
||||
- iOS Safari H265: Settings > Safari > Advanced > Experimental > WebRTC H265
|
||||
|
||||
**Audio**
|
||||
|
||||
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
|
||||
- MSE/MP4 audio codecs: `AAC`
|
||||
|
||||
**Apple devices**
|
||||
|
||||
- all Apple devices don't support MP4 stream (they only support progressive loading of static files)
|
||||
- iPhones don't support MSE technology because it competes with the HLS technology, invented by Apple
|
||||
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
|
||||
|
||||
## Codecs negotiation
|
||||
|
||||
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
|
||||
|
||||
- this camera support 2-way audio standard **ONVIF Profile T**
|
||||
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
|
||||
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
|
||||
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
|
||||
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
|
||||
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
|
||||
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
|
||||
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
|
||||
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
|
||||
|
||||
Now you have stream with two sources - **RTSP and FFmpeg**:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
|
||||
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
|
||||
```
|
||||
|
||||
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
|
||||
|
||||

|
||||
|
||||
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
|
||||
|
||||
## TIPS
|
||||
|
||||
**Using apps for low RTSP delay**
|
||||
|
||||
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
|
||||
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
|
||||
|
19
build/docker/run.sh
Normal file
19
build/docker/run.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Starting go2rtc..." >&2
|
||||
|
||||
readonly config_path="/config"
|
||||
|
||||
if [[ -x "${config_path}/go2rtc" ]]; then
|
||||
readonly binary_path="${config_path}/go2rtc"
|
||||
echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2
|
||||
else
|
||||
readonly binary_path="/usr/local/bin/go2rtc"
|
||||
fi
|
||||
|
||||
# set cwd for go2rtc (for config file, Hass integration, etc)
|
||||
cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2
|
||||
|
||||
exec "${binary_path}"
|
@@ -1,41 +0,0 @@
|
||||
ARG BUILD_FROM
|
||||
|
||||
FROM $BUILD_FROM as build
|
||||
|
||||
# 1. Build go2rtc
|
||||
RUN apk add --no-cache git go
|
||||
|
||||
RUN git clone https://github.com/AlexxIT/go2rtc \
|
||||
&& cd go2rtc \
|
||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
# 2. Download ngrok
|
||||
ARG BUILD_ARCH
|
||||
|
||||
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
||||
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
||||
&& cd go2rtc \
|
||||
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
||||
&& unzip ngrok
|
||||
|
||||
|
||||
|
||||
# https://devopscube.com/reduce-docker-image-size/
|
||||
FROM $BUILD_FROM
|
||||
|
||||
# 3. Copy go2rtc and ngrok to release
|
||||
COPY --from=build /go2rtc/go2rtc /usr/local/bin
|
||||
COPY --from=build /go2rtc/ngrok /usr/local/bin
|
||||
|
||||
# 4. Install ffmpeg
|
||||
# apk base OK: 22 MiB in 40 packages
|
||||
# ffmpeg OK: 113 MiB in 110 packages
|
||||
# python3 OK: 161 MiB in 114 packages
|
||||
RUN apk add --no-cache ffmpeg python3
|
||||
|
||||
# 5. Copy run to release
|
||||
COPY run.sh /
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
CMD [ "/run.sh" ]
|
@@ -1,6 +0,0 @@
|
||||
# https://github.com/home-assistant/builder/blob/master/builder.sh
|
||||
name: go2rtc
|
||||
description: Ultimate camera streaming application
|
||||
url: https://github.com/AlexxIT/go2rtc
|
||||
image: alexxit/go2rtc
|
||||
arch: [ amd64, aarch64, i386, armv7 ]
|
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
|
||||
set +e
|
||||
|
||||
# set cwd for go2rtc (for config file, Hass integration, etc)
|
||||
cd /config
|
||||
|
||||
# add the feature to override go2rtc binary from Hass config folder
|
||||
export PATH="/config:$PATH"
|
||||
|
||||
while true; do
|
||||
go2rtc
|
||||
sleep 5
|
||||
done
|
@@ -4,8 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -17,6 +15,7 @@ func Init() {
|
||||
Listen string `yaml:"listen"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
StaticDir string `yaml:"static_dir"`
|
||||
Origin string `yaml:"origin"`
|
||||
} `yaml:"api"`
|
||||
}
|
||||
|
||||
@@ -34,10 +33,10 @@ func Init() {
|
||||
log = app.GetLogger("api")
|
||||
|
||||
initStatic(cfg.Mod.StaticDir)
|
||||
initWS()
|
||||
initWS(cfg.Mod.Origin)
|
||||
|
||||
HandleFunc("/api/streams", streamsHandler)
|
||||
HandleFunc("/api/ws", apiWS)
|
||||
HandleFunc("api/streams", streamsHandler)
|
||||
HandleFunc("api/ws", apiWS)
|
||||
|
||||
// ensure we can listen without errors
|
||||
listener, err := net.Listen("tcp", cfg.Mod.Listen)
|
||||
@@ -48,32 +47,66 @@ func Init() {
|
||||
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
||||
|
||||
s := http.Server{}
|
||||
s.Handler = http.DefaultServeMux
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
s.Handler = middlewareLog(s.Handler)
|
||||
}
|
||||
|
||||
if cfg.Mod.Origin == "*" {
|
||||
s.Handler = middlewareCORS(s.Handler)
|
||||
}
|
||||
|
||||
go func() {
|
||||
s := http.Server{}
|
||||
if err = s.Serve(listener); err != nil {
|
||||
log.Fatal().Err(err).Msg("[api] serve")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HandleFunc handle pattern with relative path:
|
||||
// - "api/streams" => "{basepath}/api/streams"
|
||||
// - "/streams" => "/streams"
|
||||
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||
http.HandleFunc(basePath+pattern, handler)
|
||||
if len(pattern) == 0 || pattern[0] != '/' {
|
||||
pattern = basePath + "/" + pattern
|
||||
}
|
||||
log.Trace().Str("path", pattern).Msg("[api] register path")
|
||||
http.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
func HandleWS(msgType string, handler WSHandler) {
|
||||
wsHandlers[msgType] = handler
|
||||
}
|
||||
const StreamNotFound = "stream not found"
|
||||
|
||||
var basePath string
|
||||
var log zerolog.Logger
|
||||
var wsHandlers = make(map[string]WSHandler)
|
||||
|
||||
func middlewareLog(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace().Msgf("[api] %s %s", r.Method, r.URL)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func middlewareCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
name := r.URL.Query().Get("name")
|
||||
|
||||
if name == "" {
|
||||
name = src
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "PUT":
|
||||
streams.Get(src)
|
||||
streams.New(name, src)
|
||||
return
|
||||
case "DELETE":
|
||||
streams.Delete(src)
|
||||
@@ -86,37 +119,8 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
v = streams.All()
|
||||
}
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.streams] marshal")
|
||||
}
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.streams] write")
|
||||
}
|
||||
}
|
||||
|
||||
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := new(Context)
|
||||
if err := ctx.Upgrade(w, r); err != nil {
|
||||
log.Error().Err(err).Msg("[api.ws] upgrade")
|
||||
return
|
||||
}
|
||||
defer ctx.Close()
|
||||
|
||||
for {
|
||||
msg := new(streamer.Message)
|
||||
if err := ctx.Conn.ReadJSON(msg); err != nil {
|
||||
if websocket.IsUnexpectedCloseError(
|
||||
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure,
|
||||
) {
|
||||
log.Error().Err(err).Msg("[api.ws] readJSON")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
handler := wsHandlers[msg.Type]
|
||||
if handler != nil {
|
||||
handler(ctx, msg)
|
||||
}
|
||||
}
|
||||
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", " ")
|
||||
_ = e.Encode(v)
|
||||
}
|
||||
|
@@ -14,13 +14,13 @@ func initStatic(staticDir string) {
|
||||
root = http.FS(www.Static)
|
||||
}
|
||||
|
||||
base := len(basePath)
|
||||
fileServer := http.FileServer(root)
|
||||
|
||||
HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if basePath != "" {
|
||||
r.URL.Path = r.URL.Path[len(basePath):]
|
||||
HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
|
||||
if base > 0 {
|
||||
r.URL.Path = r.URL.Path[base:]
|
||||
}
|
||||
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
159
cmd/api/ws.go
159
cmd/api/ws.go
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -9,85 +8,131 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func initWS() {
|
||||
// Message - struct for data exchange in Web API
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type WSHandler func(tr *Transport, msg *Message) error
|
||||
|
||||
func HandleWS(msgType string, handler WSHandler) {
|
||||
wsHandlers[msgType] = handler
|
||||
}
|
||||
|
||||
var wsHandlers = make(map[string]WSHandler)
|
||||
|
||||
func initWS(origin string) {
|
||||
wsUp = &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 512000,
|
||||
WriteBufferSize: 2028,
|
||||
}
|
||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
return true
|
||||
}
|
||||
o, err := url.Parse(origin[0])
|
||||
if err != nil {
|
||||
|
||||
switch origin {
|
||||
case "":
|
||||
// same origin + ignore port
|
||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
return true
|
||||
}
|
||||
o, err := url.Parse(origin[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if o.Host == r.Host {
|
||||
return true
|
||||
}
|
||||
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
|
||||
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||
return o.Host[:i] == r.Host
|
||||
}
|
||||
return false
|
||||
}
|
||||
if o.Host == r.Host {
|
||||
case "*":
|
||||
// any origin
|
||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
log.Trace().Msgf("[api.ws] origin: %s, host: %s", o.Host, r.Host)
|
||||
// some users change Nginx external port using Docker port
|
||||
// so origin will be with a port and host without
|
||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||
return o.Host[:i] == r.Host
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := wsUp.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
origin := r.Header.Get("Origin")
|
||||
log.Error().Err(err).Caller().Msgf("host=%s origin=%s", r.Host, origin)
|
||||
return
|
||||
}
|
||||
|
||||
tr := &Transport{Request: r}
|
||||
tr.OnWrite(func(msg interface{}) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
_ = ws.WriteMessage(websocket.BinaryMessage, data)
|
||||
} else {
|
||||
_ = ws.WriteJSON(msg)
|
||||
}
|
||||
})
|
||||
|
||||
for {
|
||||
msg := new(Message)
|
||||
if err = ws.ReadJSON(msg); err != nil {
|
||||
log.Trace().Err(err).Caller().Send()
|
||||
_ = ws.Close()
|
||||
break
|
||||
}
|
||||
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
go func() {
|
||||
if err = handler(tr, msg); err != nil {
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
tr.Close()
|
||||
}
|
||||
|
||||
var wsUp *websocket.Upgrader
|
||||
|
||||
type WSHandler func(ctx *Context, msg *streamer.Message)
|
||||
|
||||
type Context struct {
|
||||
Conn *websocket.Conn
|
||||
type Transport struct {
|
||||
Request *http.Request
|
||||
Consumer interface{} // TODO: rewrite
|
||||
|
||||
onClose []func()
|
||||
mu sync.Mutex
|
||||
mx sync.Mutex
|
||||
|
||||
onChange func()
|
||||
onWrite func(msg interface{})
|
||||
onClose []func()
|
||||
}
|
||||
|
||||
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
|
||||
ctx.Conn, err = wsUp.Upgrade(w, r, nil)
|
||||
ctx.Request = r
|
||||
return
|
||||
func (t *Transport) OnWrite(f func(msg interface{})) {
|
||||
t.mx.Lock()
|
||||
if t.onChange != nil {
|
||||
t.onChange()
|
||||
}
|
||||
t.onWrite = f
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
func (ctx *Context) Close() {
|
||||
for _, f := range ctx.onClose {
|
||||
func (t *Transport) Write(msg interface{}) {
|
||||
t.mx.Lock()
|
||||
t.onWrite(msg)
|
||||
t.mx.Unlock()
|
||||
}
|
||||
|
||||
func (t *Transport) Close() {
|
||||
for _, f := range t.onClose {
|
||||
f()
|
||||
}
|
||||
_ = ctx.Conn.Close()
|
||||
}
|
||||
|
||||
func (ctx *Context) Write(msg interface{}) {
|
||||
ctx.mu.Lock()
|
||||
defer ctx.mu.Unlock()
|
||||
|
||||
var err error
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *streamer.Message:
|
||||
err = ctx.Conn.WriteJSON(msg)
|
||||
case []byte:
|
||||
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
//panic(err) // TODO: fix panic
|
||||
}
|
||||
func (t *Transport) OnChange(f func()) {
|
||||
t.onChange = f
|
||||
}
|
||||
|
||||
func (ctx *Context) Error(err error) {
|
||||
ctx.Write(&streamer.Message{
|
||||
Type: "error", Value: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func (ctx *Context) OnClose(f func()) {
|
||||
ctx.onClose = append(ctx.onClose, f)
|
||||
func (t *Transport) OnClose(f func()) {
|
||||
t.onClose = append(t.onClose, f)
|
||||
}
|
||||
|
@@ -3,12 +3,16 @@ package app
|
||||
import (
|
||||
"flag"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var Version = "0.1-rc.7"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
func Init() {
|
||||
config := flag.String(
|
||||
"config",
|
||||
@@ -30,10 +34,19 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
||||
|
||||
modules = cfg.Mod
|
||||
|
||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
path, _ := os.Getwd()
|
||||
log.Debug().Str("cwd", path).Send()
|
||||
}
|
||||
|
||||
func NewLogger(format string, level string) zerolog.Logger {
|
||||
var writer io.Writer = os.Stdout
|
||||
|
||||
// styles
|
||||
format := cfg.Mod["format"]
|
||||
if format != "json" {
|
||||
writer = zerolog.ConsoleWriter{
|
||||
Out: writer, TimeFormat: "15:04:05.000",
|
||||
@@ -43,18 +56,12 @@ func Init() {
|
||||
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||
|
||||
lvl, err := zerolog.ParseLevel(cfg.Mod["level"])
|
||||
lvl, err := zerolog.ParseLevel(level)
|
||||
if err != nil || lvl == zerolog.NoLevel {
|
||||
lvl = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
||||
|
||||
modules = cfg.Mod
|
||||
|
||||
path, _ := os.Getwd()
|
||||
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
|
||||
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
|
||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
||||
}
|
||||
|
||||
func LoadConfig(v interface{}) {
|
||||
@@ -68,15 +75,13 @@ func LoadConfig(v interface{}) {
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
if s, ok := modules[module]; ok {
|
||||
lvl, err := zerolog.ParseLevel(s)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[log]")
|
||||
return log
|
||||
if err == nil {
|
||||
return log.Level(lvl)
|
||||
}
|
||||
|
||||
return log.Level(lvl)
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
return log
|
||||
return log.Logger
|
||||
}
|
||||
|
||||
// internal
|
||||
@@ -84,8 +89,5 @@ func GetLogger(module string) zerolog.Logger {
|
||||
// data - config content
|
||||
var data []byte
|
||||
|
||||
// log - main logger
|
||||
var log zerolog.Logger
|
||||
|
||||
// modules log levels
|
||||
var modules map[string]string
|
||||
|
@@ -10,8 +10,8 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
api.HandleFunc("/api/stack", stackHandler)
|
||||
api.HandleFunc("/api/exit", exitHandler)
|
||||
api.HandleFunc("api/stack", stackHandler)
|
||||
api.HandleFunc("api/exit", exitHandler)
|
||||
|
||||
streams.HandleFunc("null", nullHandler)
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ var stackSkip = [][]byte{
|
||||
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
|
||||
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
|
||||
|
||||
// webrtc/api.go
|
||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -23,22 +25,22 @@ func Init() {
|
||||
return
|
||||
}
|
||||
|
||||
rtsp.OnProducer = func(prod streamer.Producer) bool {
|
||||
if conn := prod.(*pkg.Conn); conn != nil {
|
||||
if waiter := waiters[conn.URL.Path]; waiter != nil {
|
||||
waiter <- prod
|
||||
return true
|
||||
}
|
||||
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
|
||||
waitersMu.Lock()
|
||||
waiter := waiters[conn.URL.Path]
|
||||
waitersMu.Unlock()
|
||||
|
||||
if waiter == nil {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
waiter <- conn
|
||||
return true
|
||||
})
|
||||
|
||||
streams.HandleFunc("exec", Handle)
|
||||
|
||||
log = app.GetLogger("exec")
|
||||
|
||||
// TODO: add sync.Mutex
|
||||
waiters = map[string]chan streamer.Producer{}
|
||||
}
|
||||
|
||||
func Handle(url string) (streamer.Producer, error) {
|
||||
@@ -55,13 +57,22 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
if log.Debug().Enabled() {
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
ch := make(chan streamer.Producer)
|
||||
|
||||
waitersMu.Lock()
|
||||
waiters[path] = ch
|
||||
defer delete(waiters, path)
|
||||
waitersMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
waitersMu.Lock()
|
||||
delete(waiters, path)
|
||||
waitersMu.Unlock()
|
||||
}()
|
||||
|
||||
log.Debug().Str("url", url).Msg("[exec] run")
|
||||
|
||||
@@ -72,11 +83,19 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chErr := make(chan error)
|
||||
|
||||
go func() {
|
||||
chErr <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 15):
|
||||
case <-time.After(time.Second * 60):
|
||||
_ = cmd.Process.Kill()
|
||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
||||
return nil, errors.New("timeout")
|
||||
case err := <-chErr:
|
||||
return nil, fmt.Errorf("exec: %s", err)
|
||||
case prod := <-ch:
|
||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
||||
return prod, nil
|
||||
@@ -86,4 +105,5 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
// internal
|
||||
|
||||
var log zerolog.Logger
|
||||
var waiters map[string]chan streamer.Producer
|
||||
var waiters = map[string]chan streamer.Producer{}
|
||||
var waitersMu sync.Mutex
|
||||
|
@@ -1,3 +1,17 @@
|
||||
## FFplay output
|
||||
|
||||
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
|
||||
|
||||
- `7.11` - master clock, is the time from start of the stream/video
|
||||
- `A-V` - av_diff, difference between audio and video timestamps
|
||||
- `fd` - frames dropped
|
||||
- `aq` - audio queue (0 - no delay)
|
||||
- `vq` - video queue (0 - no delay)
|
||||
- `sq` - subtitle queue
|
||||
- `f` - timestamp error correction rate (Not 100% sure)
|
||||
|
||||
`M-V`, `M-A` means video stream only, audio stream only respectively.
|
||||
|
||||
## Devices Windows
|
||||
|
||||
```
|
||||
@@ -39,3 +53,9 @@
|
||||
- https://html5test.com/
|
||||
- https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
- https://trac.ffmpeg.org/wiki/DirectShow
|
||||
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
|
||||
- https://github.com/tuupola/esp_video/blob/master/README.md
|
||||
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
|
||||
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
|
||||
- https://slhck.info/video/2017/02/24/vbr-settings.html
|
||||
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)
|
||||
|
@@ -15,7 +15,7 @@ import (
|
||||
func Init() {
|
||||
log = app.GetLogger("exec")
|
||||
|
||||
api.HandleFunc("/api/devices", handle)
|
||||
api.HandleFunc("api/devices", handle)
|
||||
}
|
||||
|
||||
func GetInput(src string) (string, error) {
|
||||
|
@@ -1,12 +1,16 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -15,136 +19,226 @@ func Init() {
|
||||
Mod map[string]string `yaml:"ffmpeg"`
|
||||
}
|
||||
|
||||
// defaults
|
||||
|
||||
cfg.Mod = map[string]string{
|
||||
"bin": "ffmpeg",
|
||||
|
||||
// inputs
|
||||
"file": "-re -stream_loop -1 -i {input}",
|
||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
|
||||
|
||||
// output
|
||||
"output": "-rtsp_transport tcp -f rtsp {output}",
|
||||
|
||||
// `-g 30` - group of picture, GOP, keyframe interval
|
||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||
// `-tune zerolatency` - for minimal latency
|
||||
// `-profile main -level 4.1` - most used streaming profile
|
||||
// `-pix_fmt yuv420p` - if input pix format 4:2:2
|
||||
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1 -pix_fmt yuv420p",
|
||||
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
|
||||
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
|
||||
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
|
||||
"opus": "-codec:a libopus -ar 48000 -ac 2",
|
||||
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
|
||||
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
|
||||
"pcmu/48000": "-codec:a pcm_mulaw -ar 48000 -ac 1",
|
||||
"pcma": "-codec:a pcm_alaw -ar 8000 -ac 1",
|
||||
"pcma/16000": "-codec:a pcm_alaw -ar 16000 -ac 1",
|
||||
"pcma/48000": "-codec:a pcm_alaw -ar 48000 -ac 1",
|
||||
"aac/16000": "-codec:a aac -ar 16000 -ac 1",
|
||||
}
|
||||
cfg.Mod = defaults // will be overriden from yaml
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
tpl := cfg.Mod
|
||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
||||
defaults["global"] += " -v error"
|
||||
}
|
||||
|
||||
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
|
||||
s = s[7:] // remove `ffmpeg:`
|
||||
|
||||
var query url.Values
|
||||
var queryVideo, queryAudio bool
|
||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
||||
query = parseQuery(s[i+1:])
|
||||
queryVideo = query["video"] != nil
|
||||
queryAudio = query["audio"] != nil
|
||||
s = s[:i]
|
||||
} else {
|
||||
// by default query both video and audio
|
||||
queryVideo = true
|
||||
queryAudio = true
|
||||
streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) {
|
||||
args := parseArgs(url[7:]) // remove `ffmpeg:`
|
||||
if args == nil {
|
||||
return nil, errors.New("can't generate ffmpeg command")
|
||||
}
|
||||
|
||||
var input string
|
||||
if i := strings.IndexByte(s, ':'); i > 0 {
|
||||
switch s[:i] {
|
||||
case "http", "https":
|
||||
input = strings.Replace(tpl["http"], "{input}", s, 1)
|
||||
case "rtsp", "rtsps":
|
||||
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
||||
// skip unnecessary input tracks
|
||||
switch {
|
||||
case queryVideo && queryAudio:
|
||||
input = "-allowed_media_types video+audio "
|
||||
case queryVideo:
|
||||
input = "-allowed_media_types video "
|
||||
case queryAudio:
|
||||
input = "-allowed_media_types audio "
|
||||
}
|
||||
|
||||
input += strings.Replace(tpl["rtsp"], "{input}", s, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if input == "" {
|
||||
if strings.HasPrefix(s, "device?") {
|
||||
var err error
|
||||
input, err = device.GetInput(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
input = strings.Replace(tpl["file"], "{input}", s, 1)
|
||||
}
|
||||
}
|
||||
|
||||
s = "exec:" + tpl["bin"] + " -hide_banner " + input
|
||||
|
||||
if query != nil {
|
||||
for _, raw := range query["raw"] {
|
||||
s += " " + raw
|
||||
}
|
||||
|
||||
// TODO: multiple codecs via -map
|
||||
// s += fmt.Sprintf(" -map 0:v:0 -c:v:%d copy", i)
|
||||
|
||||
for _, video := range query["video"] {
|
||||
if video == "copy" {
|
||||
s += " -codec:v copy"
|
||||
} else {
|
||||
s += " " + tpl[video]
|
||||
}
|
||||
}
|
||||
|
||||
for _, audio := range query["audio"] {
|
||||
if audio == "copy" {
|
||||
s += " -codec:v copy"
|
||||
} else {
|
||||
s += " " + tpl[audio]
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case queryVideo && !queryAudio:
|
||||
s += " -an"
|
||||
case queryAudio && !queryVideo:
|
||||
s += " -vn"
|
||||
}
|
||||
} else {
|
||||
s += " -c copy"
|
||||
}
|
||||
|
||||
s += " " + tpl["output"]
|
||||
|
||||
return exec.Handle(s)
|
||||
return exec.Handle("exec:" + args.String())
|
||||
})
|
||||
|
||||
device.Bin = cfg.Mod["bin"]
|
||||
device.Bin = defaults["bin"]
|
||||
device.Init()
|
||||
}
|
||||
|
||||
var defaults = map[string]string{
|
||||
"bin": "ffmpeg",
|
||||
"global": "-hide_banner",
|
||||
|
||||
// inputs
|
||||
"file": "-re -stream_loop -1 -i {input}",
|
||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
|
||||
|
||||
// output
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||
|
||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||
// `-tune zerolatency` - for minimal latency
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
|
||||
"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",
|
||||
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
||||
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
||||
"aac": "-c:a aac", // keep sample rate and channels
|
||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||
|
||||
// hardware Intel and AMD on Linux
|
||||
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
||||
// `-bf 0` - disable B-frames is very important
|
||||
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
||||
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
|
||||
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
||||
|
||||
// hardware Raspberry
|
||||
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
||||
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
||||
|
||||
// hardware NVidia on Linux and Windows
|
||||
// preset=p2 - faster, tune=ll - low latency
|
||||
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
||||
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
|
||||
|
||||
// hardware Intel on Windows
|
||||
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
||||
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
|
||||
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
|
||||
|
||||
// hardware macOS
|
||||
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
|
||||
}
|
||||
|
||||
func parseArgs(s string) *Args {
|
||||
// init FFmpeg arguments
|
||||
args := &Args{
|
||||
bin: defaults["bin"],
|
||||
global: defaults["global"],
|
||||
output: defaults["output"],
|
||||
}
|
||||
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
||||
query = parseQuery(s[i+1:])
|
||||
args.video = len(query["video"])
|
||||
args.audio = len(query["audio"])
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
// Parse input:
|
||||
// 1. Input as xxxx:// link (http or rtsp or any other)
|
||||
// 2. Input as stream name
|
||||
// 3. Input as FFmpeg device (local USB camera)
|
||||
if i := strings.Index(s, "://"); i > 0 {
|
||||
switch s[:i] {
|
||||
case "http", "https", "rtmp":
|
||||
args.input = strings.Replace(defaults["http"], "{input}", s, 1)
|
||||
case "rtsp", "rtsps":
|
||||
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
||||
// skip unnecessary input tracks
|
||||
switch {
|
||||
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
|
||||
args.input = "-allowed_media_types video+audio "
|
||||
case args.video > 0:
|
||||
args.input = "-allowed_media_types video "
|
||||
case args.audio > 0:
|
||||
args.input = "-allowed_media_types audio "
|
||||
}
|
||||
|
||||
args.input += strings.Replace(defaults["rtsp"], "{input}", s, 1)
|
||||
default:
|
||||
args.input = "-i " + s
|
||||
}
|
||||
} else if streams.Get(s) != nil {
|
||||
s = "rtsp://localhost:" + rtsp.Port + "/" + s
|
||||
switch {
|
||||
case args.video > 0 && args.audio == 0:
|
||||
s += "?video"
|
||||
case args.audio > 0 && args.video == 0:
|
||||
s += "?audio"
|
||||
}
|
||||
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
|
||||
} else if strings.HasPrefix(s, "device?") {
|
||||
var err error
|
||||
args.input, err = device.GetInput(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
args.input = strings.Replace(defaults["file"], "{input}", s, 1)
|
||||
}
|
||||
|
||||
if query["async"] != nil {
|
||||
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
|
||||
}
|
||||
|
||||
// Parse query params:
|
||||
// 1. `width`/`height` params
|
||||
// 2. `rotate` param
|
||||
// 3. `video` params (support multiple)
|
||||
// 4. `audio` params (support multiple)
|
||||
// 5. `hardware` param
|
||||
if query != nil {
|
||||
// 1. Process raw params for FFmpeg
|
||||
for _, raw := range query["raw"] {
|
||||
args.AddCodec(raw)
|
||||
}
|
||||
|
||||
// 2. Process video filters (resize and rotation)
|
||||
if query["width"] != nil || query["height"] != nil {
|
||||
filter := "scale="
|
||||
if query["width"] != nil {
|
||||
filter += query["width"][0]
|
||||
} else {
|
||||
filter += "-1"
|
||||
}
|
||||
filter += ":"
|
||||
if query["height"] != nil {
|
||||
filter += query["height"][0]
|
||||
} else {
|
||||
filter += "-1"
|
||||
}
|
||||
args.AddFilter(filter)
|
||||
}
|
||||
|
||||
if query["rotate"] != nil {
|
||||
var filter string
|
||||
switch query["rotate"][0] {
|
||||
case "90":
|
||||
filter = "transpose=1" // 90 degrees clockwise
|
||||
case "180":
|
||||
filter = "transpose=1,transpose=1"
|
||||
case "-90", "270":
|
||||
filter = "transpose=2" // 90 degrees counterclockwise
|
||||
}
|
||||
if filter != "" {
|
||||
args.AddFilter(filter)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Process video codecs
|
||||
if args.video > 0 {
|
||||
for _, video := range query["video"] {
|
||||
if video != "copy" {
|
||||
args.AddCodec(defaults[video])
|
||||
} else {
|
||||
args.AddCodec("-c:v copy")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args.AddCodec("-vn")
|
||||
}
|
||||
|
||||
// 4. Process audio codecs
|
||||
if args.audio > 0 {
|
||||
for _, audio := range query["audio"] {
|
||||
if audio != "copy" {
|
||||
args.AddCodec(defaults[audio])
|
||||
} else {
|
||||
args.AddCodec("-c:a copy")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args.AddCodec("-an")
|
||||
}
|
||||
|
||||
if query["hardware"] != nil {
|
||||
MakeHardware(args, query["hardware"][0])
|
||||
}
|
||||
}
|
||||
|
||||
if args.codecs == nil {
|
||||
args.AddCodec("-c copy")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func parseQuery(s string) map[string][]string {
|
||||
query := map[string][]string{}
|
||||
for _, key := range strings.Split(s, "#") {
|
||||
@@ -157,3 +251,76 @@ func parseQuery(s string) map[string][]string {
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
bin string // ffmpeg
|
||||
global string // -hide_banner -v error
|
||||
input string // -re -stream_loop -1 -i /media/bunny.mp4
|
||||
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
||||
filters []string // scale=1920:1080
|
||||
output string // -f rtsp {output}
|
||||
|
||||
video, audio int // count of video and audio params
|
||||
}
|
||||
|
||||
func (a *Args) AddCodec(codec string) {
|
||||
a.codecs = append(a.codecs, codec)
|
||||
}
|
||||
|
||||
func (a *Args) AddFilter(filter string) {
|
||||
a.filters = append(a.filters, filter)
|
||||
}
|
||||
|
||||
func (a *Args) InsertFilter(filter string) {
|
||||
a.filters = append([]string{filter}, a.filters...)
|
||||
}
|
||||
|
||||
func (a *Args) String() string {
|
||||
b := bytes.NewBuffer(make([]byte, 0, 512))
|
||||
|
||||
b.WriteString(a.bin)
|
||||
|
||||
if a.global != "" {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.global)
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.input)
|
||||
|
||||
multimode := a.video > 1 || a.audio > 1
|
||||
var iv, ia int
|
||||
|
||||
for _, codec := range a.codecs {
|
||||
// support multiple video and/or audio codecs
|
||||
if multimode && len(codec) >= 5 {
|
||||
switch codec[:5] {
|
||||
case "-c:v ":
|
||||
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
|
||||
iv++
|
||||
case "-c:a ":
|
||||
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
|
||||
ia++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(codec)
|
||||
}
|
||||
|
||||
if a.filters != nil {
|
||||
for i, filter := range a.filters {
|
||||
if i == 0 {
|
||||
b.WriteString(" -vf ")
|
||||
} else {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(filter)
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(a.output)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
112
cmd/ffmpeg/hardware.go
Normal file
112
cmd/ffmpeg/hardware.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
EngineSoftware = "software"
|
||||
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
|
||||
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
|
||||
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
||||
EngineDXVA2 = "dxva2" // Intel on Windows
|
||||
EngineVideoToolbox = "videotoolbox" // macOS
|
||||
)
|
||||
|
||||
var cache = map[string]string{}
|
||||
|
||||
// MakeHardware converts software FFmpeg args to hardware args
|
||||
// empty engine for autoselect
|
||||
func MakeHardware(args *Args, engine string) {
|
||||
for i, codec := range args.codecs {
|
||||
if len(codec) < 12 {
|
||||
continue // skip short line (-c:v libx264...)
|
||||
}
|
||||
|
||||
// get current codec name
|
||||
name := cut(codec, ' ', 1)
|
||||
switch name {
|
||||
case "libx264":
|
||||
name = "h264"
|
||||
case "libx265":
|
||||
name = "h265"
|
||||
case "mjpeg":
|
||||
default:
|
||||
continue // skip unsupported codec
|
||||
}
|
||||
|
||||
// temporary disable probe for H265 and MJPEG
|
||||
if engine == "" && name == "h264" {
|
||||
if engine = cache[name]; engine == "" {
|
||||
engine = ProbeHardware(name)
|
||||
cache[name] = engine
|
||||
}
|
||||
}
|
||||
|
||||
switch engine {
|
||||
case EngineVAAPI:
|
||||
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_vaapi=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
// fix if input doesn't support hwaccel, do nothing when support
|
||||
args.InsertFilter("format=vaapi|nv12,hwupload")
|
||||
|
||||
case EngineCUDA:
|
||||
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_cuda=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
case EngineDXVA2:
|
||||
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for i, filter := range args.filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.filters[i] = "scale_qsv=" + filter[6:]
|
||||
}
|
||||
}
|
||||
|
||||
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
|
||||
|
||||
case EngineVideoToolbox:
|
||||
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
case EngineV4L2M2M:
|
||||
args.codecs[i] = defaults[name+"/"+engine]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(arg ...string) bool {
|
||||
err := exec.Command(defaults["bin"], arg...).Run()
|
||||
log.Printf("%v %v", arg, err)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func cut(s string, sep byte, pos int) string {
|
||||
for n := 0; n < pos; n++ {
|
||||
if i := strings.IndexByte(s, sep); i > 0 {
|
||||
s = s[i+1:]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
if i := strings.IndexByte(s, sep); i > 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
21
cmd/ffmpeg/hardware_darwin.go
Normal file
21
cmd/ffmpeg/hardware_darwin.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package ffmpeg
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_videotoolbox", "-f", "null", "-") {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_videotoolbox", "-f", "null", "-") {
|
||||
return EngineVideoToolbox
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
67
cmd/ffmpeg/hardware_linux.go
Normal file
67
cmd/ffmpeg/hardware_linux.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_v4l2m2m", "-f", "null", "-") {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run(
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
|
||||
return EngineV4L2M2M
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "h264":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "h264_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "hevc_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run("-init_hw_device", "vaapi",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-vf", "format=nv12,hwupload",
|
||||
"-c", "mjpeg_vaapi", "-f", "null", "-") {
|
||||
return EngineVAAPI
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
40
cmd/ffmpeg/hardware_windows.go
Normal file
40
cmd/ffmpeg/hardware_windows.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package ffmpeg
|
||||
|
||||
func ProbeHardware(name string) string {
|
||||
switch name {
|
||||
case "h264":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "h264_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "h265":
|
||||
if run("-init_hw_device", "cuda",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||
return EngineCUDA
|
||||
}
|
||||
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "hevc_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
|
||||
case "mjpeg":
|
||||
if run("-init_hw_device", "dxva2",
|
||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||
"-c", "mjpeg_qsv", "-f", "null", "-") {
|
||||
return EngineDXVA2
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
}
|
153
cmd/hass/api.go
Normal file
153
cmd/hass/api.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package hass
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func initAPI() {
|
||||
ok := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
|
||||
}
|
||||
|
||||
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
|
||||
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
api.HandleFunc("/streams", ok)
|
||||
|
||||
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
// /stream/{id}/add
|
||||
case strings.HasSuffix(r.RequestURI, "/add"):
|
||||
var v addJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we can get three types of links:
|
||||
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
||||
// 2. static link to Hass camera
|
||||
// 3. dynamic link to Hass camera
|
||||
stream := streams.Get(v.Name)
|
||||
if stream == nil {
|
||||
// check if it is rtsp link to go2rtc
|
||||
stream = rtspStream(v.Channels.First.Url)
|
||||
if stream != nil {
|
||||
streams.New(v.Name, stream)
|
||||
} else {
|
||||
stream = streams.New(v.Name, "{input}")
|
||||
}
|
||||
}
|
||||
|
||||
stream.SetSource(v.Channels.First.Url)
|
||||
|
||||
ok(w, r)
|
||||
|
||||
// /stream/{id}/channel/0/webrtc
|
||||
default:
|
||||
i := strings.IndexByte(r.RequestURI[8:], '/')
|
||||
if i <= 0 {
|
||||
log.Warn().Msgf("wrong request: %s", r.RequestURI)
|
||||
return
|
||||
}
|
||||
name := r.RequestURI[8 : 8+i]
|
||||
|
||||
stream := streams.Get(name)
|
||||
if stream == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] parse form")
|
||||
return
|
||||
}
|
||||
|
||||
s := r.FormValue("data")
|
||||
offer, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
|
||||
return
|
||||
}
|
||||
|
||||
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] exchange SDP")
|
||||
return
|
||||
}
|
||||
|
||||
s = base64.StdEncoding.EncodeToString([]byte(s))
|
||||
_, _ = w.Write([]byte(s))
|
||||
}
|
||||
})
|
||||
|
||||
// api from RTSPtoWebRTC
|
||||
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
str := r.FormValue("sdp64")
|
||||
offer, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
src := r.FormValue("url")
|
||||
src, err = url.QueryUnescape(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
if stream = rtspStream(src); stream != nil {
|
||||
streams.New(src, stream)
|
||||
} else {
|
||||
stream = streams.New(src, src)
|
||||
}
|
||||
}
|
||||
|
||||
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v := struct {
|
||||
Answer string `json:"sdp64"`
|
||||
}{
|
||||
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
})
|
||||
}
|
||||
|
||||
func rtspStream(url string) *streams.Stream {
|
||||
if strings.HasPrefix(url, "rtsp://") {
|
||||
if i := strings.IndexByte(url[7:], '/'); i > 0 {
|
||||
return streams.Get(url[8+i:])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type addJSON struct {
|
||||
Name string `json:"name"`
|
||||
Channels struct {
|
||||
First struct {
|
||||
//Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
} `json:"0"`
|
||||
} `json:"channels"`
|
||||
}
|
@@ -1,20 +1,14 @@
|
||||
package hass
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -28,11 +22,7 @@ func Init() {
|
||||
|
||||
log = app.GetLogger("hass")
|
||||
|
||||
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
|
||||
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
api.HandleFunc("/stream", handler)
|
||||
initAPI()
|
||||
|
||||
// support load cameras from Hass config file
|
||||
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
|
||||
@@ -78,73 +68,13 @@ func Init() {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info().Str("url", "hass:" + entrie.Title).Msg("[hass] load stream")
|
||||
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
|
||||
//streams.Get("hass:" + entrie.Title)
|
||||
}
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] parse form")
|
||||
return
|
||||
}
|
||||
|
||||
src := r.FormValue("url")
|
||||
src, err := url.QueryUnescape(src)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] query unescape")
|
||||
return
|
||||
}
|
||||
|
||||
str := r.FormValue("sdp64")
|
||||
|
||||
offer, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
|
||||
return
|
||||
}
|
||||
|
||||
// check if stream links to our rtsp server
|
||||
if strings.HasPrefix(src, "rtsp://") {
|
||||
i := strings.IndexByte(src[7:], '/')
|
||||
if i > 0 && streams.Has(src[8+i:]) {
|
||||
src = src[8+i:]
|
||||
}
|
||||
}
|
||||
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
log.Error().Str("url", src).Msg("[api.hass] unsupported source")
|
||||
return
|
||||
}
|
||||
|
||||
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] exchange SDP")
|
||||
return
|
||||
}
|
||||
|
||||
resp := struct {
|
||||
Answer string `json:"sdp64"`
|
||||
}{
|
||||
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] marshal JSON")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.hass] write")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type entries struct {
|
||||
Data struct {
|
||||
Entries []struct {
|
||||
|
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -54,99 +54,82 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
items = append(items, device)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(items)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.homekit]")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.homekit]")
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(items)
|
||||
|
||||
case "POST":
|
||||
// TODO: post params...
|
||||
|
||||
id := r.URL.Query().Get("id")
|
||||
pin := r.URL.Query().Get("pin")
|
||||
|
||||
client, err := homekit.Pair(id, pin)
|
||||
if err != nil {
|
||||
// log error
|
||||
log.Error().Err(err).Msg("[api.homekit] pair")
|
||||
// response error
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
dict := store.GetDict("streams")
|
||||
dict[name] = client.URL()
|
||||
if err = store.Set("streams", dict); err != nil {
|
||||
// log error
|
||||
log.Error().Err(err).Msg("[api.homekit] save to store")
|
||||
// response error
|
||||
if err := hkPair(id, pin, name); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
streams.New(name, client.URL())
|
||||
|
||||
case "DELETE":
|
||||
src := r.URL.Query().Get("src")
|
||||
dict := store.GetDict("streams")
|
||||
for name, rawURL := range dict {
|
||||
if name != src {
|
||||
continue
|
||||
}
|
||||
|
||||
client, err := homekit.NewClient(rawURL.(string))
|
||||
if err != nil {
|
||||
// log error
|
||||
log.Error().Err(err).Msg("[api.homekit] new client")
|
||||
// response error
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err = client.Dial(); err != nil {
|
||||
// log error
|
||||
log.Error().Err(err).Msg("[api.homekit] client dial")
|
||||
// response error
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
go client.Handle()
|
||||
|
||||
if err = client.ListPairings(); err != nil {
|
||||
// log error
|
||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
||||
// response error
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err = client.DeletePairing(client.ClientID); err != nil {
|
||||
// log error
|
||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
||||
// response error
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
delete(dict, name)
|
||||
|
||||
if err = store.Set("streams", dict); err != nil {
|
||||
// log error
|
||||
log.Error().Err(err).Msg("[api.homekit] store set")
|
||||
// response error
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
return
|
||||
if err := hkDelete(src); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hkPair(deviceID, pin, name string) (err error) {
|
||||
var conn *hap.Conn
|
||||
|
||||
if conn, err = hap.Pair(deviceID, pin); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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 err = conn.ListPairings(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.DeletePairing(conn.ClientID); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
delete(dict, name)
|
||||
|
||||
return store.Set("streams", dict)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
@@ -3,6 +3,7 @@ package homekit
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
@@ -14,26 +15,18 @@ func Init() {
|
||||
|
||||
streams.HandleFunc("homekit", streamHandler)
|
||||
|
||||
api.HandleFunc("/api/homekit", apiHandler)
|
||||
api.HandleFunc("api/homekit", apiHandler)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func streamHandler(url string) (streamer.Producer, error) {
|
||||
client, err := homekit.NewClient(url)
|
||||
conn, err := homekit.NewClient(url, srtp.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.Dial(); err != nil {
|
||||
if err = conn.Dial();err!=nil{
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// start gorutine for reading responses from camera
|
||||
go func() {
|
||||
if err = client.Handle(); err != nil {
|
||||
log.Warn().Err(err).Msg("[homekit] client")
|
||||
}
|
||||
}()
|
||||
|
||||
return &Producer{client: client}, nil
|
||||
return conn, nil
|
||||
}
|
||||
|
@@ -1,189 +0,0 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit/camera"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/brutella/hap/characteristic"
|
||||
"github.com/brutella/hap/rtp"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
streamer.Element
|
||||
|
||||
client *homekit.Client
|
||||
medias []*streamer.Media
|
||||
tracks []*streamer.Track
|
||||
|
||||
sessions []*pkg.Session
|
||||
}
|
||||
|
||||
func (c *Producer) GetMedias() []*streamer.Media {
|
||||
if c.medias == nil {
|
||||
c.medias = c.getMedias()
|
||||
}
|
||||
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
for _, track := range c.tracks {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
track := &streamer.Track{Codec: codec, Direction: media.Direction}
|
||||
c.tracks = append(c.tracks, track)
|
||||
return track
|
||||
}
|
||||
|
||||
func (c *Producer) Start() error {
|
||||
if c.tracks == nil {
|
||||
return errors.New("producer without tracks")
|
||||
}
|
||||
|
||||
// get our server local IP-address
|
||||
host, _, err := net.SplitHostPort(c.client.LocalAddr())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get our server SRTP port
|
||||
port, err := strconv.Atoi(srtp.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// setup HomeKit stream session
|
||||
hkSession := camera.NewSession()
|
||||
hkSession.SetLocalEndpoint(host, uint16(port))
|
||||
|
||||
// create client for processing camera accessory
|
||||
cam := camera.NewClient(c.client)
|
||||
// try to start HomeKit stream
|
||||
if err = cam.StartStream2(hkSession); err != nil {
|
||||
panic(err) // TODO: fixme
|
||||
}
|
||||
|
||||
// SRTP Video Session
|
||||
vs := &pkg.Session{
|
||||
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
|
||||
RemoteSSRC: hkSession.Answer.SsrcVideo,
|
||||
Track: c.tracks[0],
|
||||
}
|
||||
if err = vs.SetKeys(
|
||||
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
|
||||
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// SRTP Audio Session
|
||||
as := &pkg.Session{
|
||||
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
|
||||
RemoteSSRC: hkSession.Answer.SsrcAudio,
|
||||
Track: &streamer.Track{},
|
||||
}
|
||||
if err = as.SetKeys(
|
||||
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
|
||||
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srtp.AddSession(vs)
|
||||
srtp.AddSession(as)
|
||||
|
||||
c.sessions = []*pkg.Session{vs, as}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Producer) Stop() error {
|
||||
err := c.client.Close()
|
||||
|
||||
for _, session := range c.sessions {
|
||||
srtp.RemoveSession(session)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Producer) getMedias() []*streamer.Media {
|
||||
var medias []*streamer.Media
|
||||
|
||||
accs, err := c.client.GetAccessories()
|
||||
acc := accs[0]
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// get supported video config (not really necessary)
|
||||
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
|
||||
v1 := &rtp.VideoStreamConfiguration{}
|
||||
if err = char.ReadTLV8(v1); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, hkCodec := range v1.Codecs {
|
||||
codec := &streamer.Codec{ClockRate: 90000}
|
||||
|
||||
switch hkCodec.Type {
|
||||
case rtp.VideoCodecType_H264:
|
||||
codec.Name = streamer.CodecH264
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
|
||||
v2 := &rtp.AudioStreamConfiguration{}
|
||||
if err = char.ReadTLV8(v2); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, hkCodec := range v2.Codecs {
|
||||
codec := &streamer.Codec{
|
||||
Channels: uint16(hkCodec.Parameters.Channels),
|
||||
}
|
||||
|
||||
switch hkCodec.Type {
|
||||
case rtp.AudioCodecType_AAC_ELD:
|
||||
codec.Name = streamer.CodecAAC
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
|
||||
}
|
||||
|
||||
switch hkCodec.Parameters.Samplerate {
|
||||
case rtp.AudioCodecSampleRate8Khz:
|
||||
codec.ClockRate = 8000
|
||||
case rtp.AudioCodecSampleRate16Khz:
|
||||
codec.ClockRate = 16000
|
||||
case rtp.AudioCodecSampleRate24Khz:
|
||||
codec.ClockRate = 24000
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
return medias
|
||||
}
|
56
cmd/http/http.go
Normal file
56
cmd/http/http.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("http", handle)
|
||||
streams.HandleFunc("https", handle)
|
||||
}
|
||||
|
||||
func handle(url string) (streamer.Producer, error) {
|
||||
// first we get the Content-Type to define supported producer
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
|
||||
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":
|
||||
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
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
|
||||
}
|
131
cmd/mjpeg/mjpeg.go
Normal file
131
cmd/mjpeg/mjpeg.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
||||
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
||||
|
||||
api.HandleWS("mjpeg", handlerWS)
|
||||
}
|
||||
|
||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
exit := make(chan []byte)
|
||||
|
||||
cons := &mjpeg.Consumer{}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
exit <- msg
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
data := <-exit
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "image/jpeg")
|
||||
h.Set("Content-Length", strconv.Itoa(len(data)))
|
||||
h.Set("Cache-Control", "no-cache")
|
||||
h.Set("Connection", "close")
|
||||
h.Set("Pragma", "no-cache")
|
||||
|
||||
if _, err := w.Write(data); 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) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
flusher := w.(http.Flusher)
|
||||
|
||||
cons := &mjpeg.Consumer{}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
data := []byte(header + strconv.Itoa(len(msg)))
|
||||
data = append(data, '\r', '\n', '\r', '\n')
|
||||
data = append(data, msg...)
|
||||
data = append(data, '\r', '\n')
|
||||
|
||||
// Chrome bug: mjpeg image always shows the second to last image
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
||||
_, _ = w.Write(data)
|
||||
flusher.Flush()
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
||||
return
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||
h.Set("Cache-Control", "no-cache")
|
||||
h.Set("Connection", "close")
|
||||
h.Set("Pragma", "no-cache")
|
||||
|
||||
<-r.Context().Done()
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
|
||||
//log.Trace().Msg("[api.mjpeg] close")
|
||||
}
|
||||
|
||||
func handlerWS(tr *api.Transport, _ *api.Message) error {
|
||||
src := tr.Request.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
cons := &mjpeg.Consumer{}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
tr.Write(data)
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
tr.Write(&api.Message{Type: "mjpeg"})
|
||||
|
||||
tr.OnClose(func() {
|
||||
stream.RemoveConsumer(cons)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
@@ -9,15 +9,17 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
log = app.GetLogger("mp4")
|
||||
|
||||
api.HandleWS(MsgTypeMSE, handlerWS)
|
||||
api.HandleWS("mse", handlerWSMSE)
|
||||
api.HandleWS("mp4", handlerWSMP4)
|
||||
|
||||
api.HandleFunc("/api/frame.mp4", handlerKeyframe)
|
||||
api.HandleFunc("/api/stream.mp4", handlerMP4)
|
||||
api.HandleFunc("api/frame.mp4", handlerKeyframe)
|
||||
api.HandleFunc("api/stream.mp4", handlerMP4)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
@@ -28,72 +30,68 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
exit := make(chan []byte)
|
||||
|
||||
cons := &mp4.Consumer{}
|
||||
cons := &mp4.Segment{OnlyKeyframe: true}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
exit <- msg
|
||||
if data, ok := msg.([]byte); ok && exit != nil {
|
||||
exit <- data
|
||||
exit = nil
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Msg("[api.keyframe] add consumer")
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
defer stream.RemoveConsumer(cons)
|
||||
data := <-exit
|
||||
|
||||
w.Header().Set("Content-Type", cons.MimeType())
|
||||
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.keyframe] init")
|
||||
return
|
||||
}
|
||||
data = append(data, <-exit...)
|
||||
stream.RemoveConsumer(cons)
|
||||
|
||||
// Apple Safari won't show frame without length
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
w.Header().Set("Content-Type", cons.MimeType)
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.keyframe] add consumer")
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
|
||||
func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
|
||||
|
||||
if isChromeFirst(w, r) || isSafari(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[api.mp4] %+v", r)
|
||||
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
exit := make(chan struct{})
|
||||
exit := make(chan error)
|
||||
|
||||
cons := &mp4.Consumer{}
|
||||
cons.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
case []byte:
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
exit <- struct{}{}
|
||||
if data, ok := msg.([]byte); ok {
|
||||
if _, err := w.Write(data); err != nil && exit != nil {
|
||||
exit <- err
|
||||
exit = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Msg("[api.mp4] add consumer")
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,18 +101,36 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.mp4] init")
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.mp4] write")
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
<-exit
|
||||
cons.Start()
|
||||
|
||||
log.Trace().Msg("[api.mp4] close")
|
||||
var duration *time.Timer
|
||||
if s := r.URL.Query().Get("duration"); s != "" {
|
||||
if i, _ := strconv.Atoi(s); i > 0 {
|
||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
||||
if exit != nil {
|
||||
exit <- nil
|
||||
exit = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
err = <-exit
|
||||
|
||||
log.Trace().Err(err).Caller().Send()
|
||||
|
||||
if duration != nil {
|
||||
duration.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
|
||||
|
@@ -1,52 +0,0 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
)
|
||||
|
||||
const MsgTypeMSE = "mse" // fMP4
|
||||
|
||||
func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
||||
src := ctx.Request.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cons := &mp4.Consumer{}
|
||||
cons.UserAgent = ctx.Request.UserAgent()
|
||||
cons.RemoteAddr = ctx.Request.RemoteAddr
|
||||
|
||||
cons.Listen(func(msg interface{}) {
|
||||
switch msg.(type) {
|
||||
case *streamer.Message, []byte:
|
||||
ctx.Write(msg)
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.mse] add consumer")
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.OnClose(func() {
|
||||
stream.RemoveConsumer(cons)
|
||||
})
|
||||
|
||||
ctx.Write(&streamer.Message{
|
||||
Type: MsgTypeMSE, Value: cons.MimeType(),
|
||||
})
|
||||
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[api.mse] init")
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Write(data)
|
||||
}
|
135
cmd/mp4/ws.go
Normal file
135
cmd/mp4/ws.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const packetSize = 1400
|
||||
|
||||
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
||||
src := tr.Request.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
cons := &mp4.Consumer{}
|
||||
cons.UserAgent = tr.Request.UserAgent()
|
||||
cons.RemoteAddr = tr.Request.RemoteAddr
|
||||
|
||||
if codecs, ok := msg.Value.(string); ok {
|
||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
||||
cons.Medias = parseMedias(codecs, true)
|
||||
}
|
||||
|
||||
cons.Listen(func(msg interface{}) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
for len(data) > packetSize {
|
||||
tr.Write(data[:packetSize])
|
||||
data = data[packetSize:]
|
||||
}
|
||||
tr.Write(data)
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
tr.OnClose(func() {
|
||||
stream.RemoveConsumer(cons)
|
||||
})
|
||||
|
||||
tr.Write(&api.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
|
||||
}
|
||||
|
||||
func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
||||
src := tr.Request.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
cons := &mp4.Segment{}
|
||||
|
||||
if codecs, ok := msg.Value.(string); ok {
|
||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
||||
cons.Medias = parseMedias(codecs, false)
|
||||
}
|
||||
|
||||
cons.Listen(func(msg interface{}) {
|
||||
if data, ok := msg.([]byte); ok {
|
||||
tr.Write(data)
|
||||
}
|
||||
})
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
tr.Write(&api.Message{Type: "mp4", Value: cons.MimeType})
|
||||
|
||||
tr.OnClose(func() {
|
||||
stream.RemoveConsumer(cons)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
|
||||
var videos []*streamer.Codec
|
||||
var audios []*streamer.Codec
|
||||
|
||||
for _, name := range strings.Split(codecs, ",") {
|
||||
switch name {
|
||||
case "avc1.640029":
|
||||
codec := &streamer.Codec{Name: streamer.CodecH264}
|
||||
videos = append(videos, codec)
|
||||
case "hvc1.1.6.L153.B0":
|
||||
codec := &streamer.Codec{Name: streamer.CodecH265}
|
||||
videos = append(videos, codec)
|
||||
case "mp4a.40.2":
|
||||
codec := &streamer.Codec{Name: streamer.CodecAAC}
|
||||
audios = append(audios, codec)
|
||||
}
|
||||
}
|
||||
|
||||
if videos != nil {
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: videos,
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
if audios != nil && parseAudio {
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: audios,
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -15,5 +15,8 @@ func handle(url string) (streamer.Producer, error) {
|
||||
if err := conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := conn.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
233
cmd/rtsp/rtsp.go
233
cmd/rtsp/rtsp.go
@@ -14,7 +14,9 @@ import (
|
||||
func Init() {
|
||||
var conf struct {
|
||||
Mod struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Listen string `yaml:"listen"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
} `yaml:"rtsp"`
|
||||
}
|
||||
|
||||
@@ -32,20 +34,49 @@ func Init() {
|
||||
|
||||
// RTSP server support
|
||||
address := conf.Mod.Listen
|
||||
if address != "" {
|
||||
_, Port, _ = net.SplitHostPort(address)
|
||||
|
||||
go worker(address)
|
||||
if address == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[rtsp] listen")
|
||||
return
|
||||
}
|
||||
|
||||
_, Port, _ = net.SplitHostPort(address)
|
||||
|
||||
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := rtsp.NewServer(conn)
|
||||
// skip check auth for localhost
|
||||
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
|
||||
c.Auth(conf.Mod.Username, conf.Mod.Password)
|
||||
}
|
||||
go tcpHandler(c)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type Handler func(conn *rtsp.Conn) bool
|
||||
|
||||
func HandleFunc(handler Handler) {
|
||||
handlers = append(handlers, handler)
|
||||
}
|
||||
|
||||
var Port string
|
||||
|
||||
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
|
||||
|
||||
// internal
|
||||
|
||||
var log zerolog.Logger
|
||||
var handlers []Handler
|
||||
|
||||
func rtspHandler(url string) (streamer.Producer, error) {
|
||||
backchannel := true
|
||||
@@ -62,6 +93,8 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn.UserAgent = app.UserAgent
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
conn.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
@@ -84,10 +117,10 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
}
|
||||
|
||||
// second try without backchannel, we need to reconnect
|
||||
conn.Backchannel = false
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn.Backchannel = false
|
||||
if err = conn.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -96,101 +129,98 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func worker(address string) {
|
||||
srv, err := tcp.NewServer(address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[rtsp] listen")
|
||||
return
|
||||
}
|
||||
func tcpHandler(conn *rtsp.Conn) {
|
||||
var name string
|
||||
var closer func()
|
||||
|
||||
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
||||
trace := log.Trace().Enabled()
|
||||
|
||||
srv.Listen(func(msg interface{}) {
|
||||
switch msg.(type) {
|
||||
case net.Conn:
|
||||
var name string
|
||||
var onDisconnect func()
|
||||
conn.Listen(func(msg interface{}) {
|
||||
if trace {
|
||||
switch msg := msg.(type) {
|
||||
case *tcp.Request:
|
||||
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
|
||||
case *tcp.Response:
|
||||
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
trace := log.Trace().Enabled()
|
||||
|
||||
conn := rtsp.NewServer(msg.(net.Conn))
|
||||
conn.Listen(func(msg interface{}) {
|
||||
if trace {
|
||||
switch msg := msg.(type) {
|
||||
case *tcp.Request:
|
||||
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
|
||||
case *tcp.Response:
|
||||
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
switch msg {
|
||||
case rtsp.MethodDescribe:
|
||||
name = conn.URL.Path[1:]
|
||||
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
||||
|
||||
stream := streams.Get(name) // TODO: rewrite
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
initMedias(conn)
|
||||
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
return
|
||||
}
|
||||
|
||||
onDisconnect = func() {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
|
||||
case rtsp.MethodAnnounce:
|
||||
if OnProducer != nil {
|
||||
if OnProducer(conn) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
name = conn.URL.Path[1:]
|
||||
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
||||
|
||||
str := streams.Get(conn.URL.Path[1:])
|
||||
if str == nil {
|
||||
return
|
||||
}
|
||||
|
||||
str.AddProducer(conn)
|
||||
|
||||
onDisconnect = func() {
|
||||
str.RemoveProducer(conn)
|
||||
}
|
||||
|
||||
case streamer.StatePlaying:
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] start")
|
||||
}
|
||||
})
|
||||
|
||||
if err = conn.Accept(); err != nil {
|
||||
log.Warn().Err(err).Msg("[rtsp] accept")
|
||||
switch msg {
|
||||
case rtsp.MethodDescribe:
|
||||
if len(conn.URL.Path) == 0 {
|
||||
log.Warn().Msg("[rtsp] server empty URL on DESCRIBE")
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.Handle(); err != nil {
|
||||
//log.Warn().Err(err).Msg("[rtsp] handle server")
|
||||
name = conn.URL.Path[1:]
|
||||
|
||||
stream := streams.Get(name)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if onDisconnect != nil {
|
||||
onDisconnect()
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
||||
|
||||
initMedias(conn)
|
||||
|
||||
if err := stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
|
||||
closer = func() {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
|
||||
case rtsp.MethodAnnounce:
|
||||
if len(conn.URL.Path) == 0 {
|
||||
log.Warn().Msg("[rtsp] server empty URL on ANNOUNCE")
|
||||
return
|
||||
}
|
||||
|
||||
name = conn.URL.Path[1:]
|
||||
|
||||
stream := streams.Get(name)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
||||
|
||||
stream.AddProducer(conn)
|
||||
|
||||
closer = func() {
|
||||
stream.RemoveProducer(conn)
|
||||
}
|
||||
|
||||
case streamer.StatePlaying:
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] start")
|
||||
}
|
||||
})
|
||||
|
||||
srv.Serve()
|
||||
if err := conn.Accept(); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
for _, handler := range handlers {
|
||||
if handler(conn) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if closer != nil {
|
||||
if err := conn.Handle(); err != nil {
|
||||
log.Debug().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
closer()
|
||||
|
||||
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
func initMedias(conn *rtsp.Conn) {
|
||||
@@ -198,16 +228,27 @@ func initMedias(conn *rtsp.Conn) {
|
||||
for key, value := range conn.URL.Query() {
|
||||
switch key {
|
||||
case streamer.KindVideo, streamer.KindAudio:
|
||||
for _, value := range value {
|
||||
for _, name := range value {
|
||||
name = strings.ToUpper(name)
|
||||
|
||||
// check aliases
|
||||
switch name {
|
||||
case "COPY":
|
||||
name = "" // pass empty codecs list
|
||||
case "MJPEG":
|
||||
name = streamer.CodecJPEG
|
||||
case "AAC":
|
||||
name = streamer.CodecAAC
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: key, Direction: streamer.DirectionRecvonly,
|
||||
}
|
||||
|
||||
switch value {
|
||||
case "", "copy": // pass empty codecs list
|
||||
default:
|
||||
codec := streamer.NewCodec(value)
|
||||
media.Codecs = append(media.Codecs, codec)
|
||||
// empty codecs match all codecs
|
||||
if name != "" {
|
||||
// empty clock rate and channels match any values
|
||||
media.Codecs = []*streamer.Codec{{Name: name}}
|
||||
}
|
||||
|
||||
conn.Medias = append(conn.Medias, media)
|
||||
|
@@ -3,7 +3,6 @@ package srtp
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
)
|
||||
|
||||
@@ -24,36 +23,23 @@ func Init() {
|
||||
return
|
||||
}
|
||||
|
||||
log = app.GetLogger("srtp")
|
||||
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).Msg("[srtp] listen")
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
|
||||
|
||||
_, Port, _ = net.SplitHostPort(cfg.Mod.Listen)
|
||||
|
||||
// run server
|
||||
go func() {
|
||||
server = &srtp.Server{}
|
||||
if err = server.Serve(conn); err != nil {
|
||||
log.Warn().Err(err).Msg("[srtp] serve")
|
||||
Server = &srtp.Server{}
|
||||
if err = Server.Serve(conn); err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
var server *srtp.Server
|
||||
|
||||
var Port string
|
||||
|
||||
func AddSession(session *srtp.Session) {
|
||||
server.AddSession(session)
|
||||
}
|
||||
|
||||
func RemoveSession(session *srtp.Session) {
|
||||
server.RemoveSession(session)
|
||||
}
|
||||
var Server *srtp.Server
|
||||
|
@@ -4,30 +4,36 @@ import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Handler func(url string) (streamer.Producer, error)
|
||||
|
||||
var handlers map[string]Handler
|
||||
var handlers = map[string]Handler{}
|
||||
var handlersMu sync.Mutex
|
||||
|
||||
func HandleFunc(scheme string, handler Handler) {
|
||||
if handlers == nil {
|
||||
handlers = make(map[string]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 {
|
||||
i := strings.IndexByte(url, ':')
|
||||
if i <= 0 { // TODO: i < 4 ?
|
||||
return false
|
||||
}
|
||||
return handlers[url[:i]] != nil
|
||||
return getHandler(url) != nil
|
||||
}
|
||||
|
||||
func GetProducer(url string) (streamer.Producer, error) {
|
||||
i := strings.IndexByte(url, ':')
|
||||
handler := handlers[url[:i]]
|
||||
handler := getHandler(url)
|
||||
if handler == nil {
|
||||
return nil, fmt.Errorf("unsupported scheme: %s", url)
|
||||
}
|
||||
|
@@ -2,7 +2,9 @@ package streams
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type state byte
|
||||
@@ -12,93 +14,176 @@ const (
|
||||
stateMedias
|
||||
stateTracks
|
||||
stateStart
|
||||
stateExternal
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
streamer.Element
|
||||
|
||||
url string
|
||||
url string
|
||||
template string
|
||||
|
||||
element streamer.Producer
|
||||
lastErr error
|
||||
tracks []*streamer.Track
|
||||
|
||||
state state
|
||||
mx sync.Mutex
|
||||
state state
|
||||
mu sync.Mutex
|
||||
restart *time.Timer
|
||||
}
|
||||
|
||||
func (p *Producer) SetSource(s string) {
|
||||
if p.template == "" {
|
||||
p.template = p.url
|
||||
}
|
||||
p.url = strings.Replace(p.template, "{input}", s, 1)
|
||||
}
|
||||
|
||||
func (p *Producer) GetMedias() []*streamer.Media {
|
||||
p.mx.Lock()
|
||||
defer p.mx.Unlock()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateNone {
|
||||
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
|
||||
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
|
||||
|
||||
var err error
|
||||
p.element, err = GetProducer(p.url)
|
||||
if err != nil || p.element == nil {
|
||||
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
|
||||
p.element, p.lastErr = GetProducer(p.url)
|
||||
if p.lastErr != nil || p.element == nil {
|
||||
log.Error().Err(p.lastErr).Str("url", p.url).Caller().Send()
|
||||
return nil
|
||||
}
|
||||
|
||||
p.state = stateMedias
|
||||
}
|
||||
|
||||
// if element in reconnect state
|
||||
if p.element == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.element.GetMedias()
|
||||
}
|
||||
|
||||
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
p.mx.Lock()
|
||||
defer p.mx.Unlock()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
if p.state == stateNone {
|
||||
return nil
|
||||
}
|
||||
|
||||
track := p.element.GetTrack(media, codec)
|
||||
|
||||
for _, t := range p.tracks {
|
||||
if track == t {
|
||||
for _, track := range p.tracks {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
track := p.element.GetTrack(media, codec)
|
||||
if track == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.tracks = append(p.tracks, track)
|
||||
|
||||
if p.state == stateMedias {
|
||||
p.state = stateTracks
|
||||
}
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
// internals
|
||||
|
||||
func (p *Producer) start() {
|
||||
p.mx.Lock()
|
||||
defer p.mx.Unlock()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state != stateTracks {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("url", p.url).Msg("[streams] start producer")
|
||||
log.Debug().Msgf("[streams] start producer url=%s", p.url)
|
||||
|
||||
p.state = stateStart
|
||||
go func() {
|
||||
// safe read element while mu locked
|
||||
if err := p.element.Start(); err != nil {
|
||||
log.Warn().Err(err).Str("url", p.url).Msg("[streams] start")
|
||||
log.Warn().Err(err).Str("url", p.url).Caller().Send()
|
||||
}
|
||||
p.reconnect()
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Producer) reconnect() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.state != stateStart {
|
||||
log.Trace().Msgf("[streams] stop reconnect url=%s", p.url)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
|
||||
|
||||
p.element, p.lastErr = GetProducer(p.url)
|
||||
if p.lastErr != nil || p.element == nil {
|
||||
log.Debug().Err(p.lastErr).Caller().Send()
|
||||
// TODO: dynamic timeout
|
||||
p.restart = time.AfterFunc(30*time.Second, p.reconnect)
|
||||
return
|
||||
}
|
||||
|
||||
medias := p.element.GetMedias()
|
||||
|
||||
// convert all old producer tracks to new tracks
|
||||
for i, oldTrack := range p.tracks {
|
||||
// match new element medias with old track codec
|
||||
for _, media := range medias {
|
||||
codec := media.MatchCodec(oldTrack.Codec)
|
||||
if codec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// move sink from old track to new track
|
||||
newTrack := p.element.GetTrack(media, codec)
|
||||
newTrack.GetSink(oldTrack)
|
||||
p.tracks[i] = newTrack
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := p.element.Start(); err != nil {
|
||||
log.Debug().Err(err).Caller().Send()
|
||||
}
|
||||
p.reconnect()
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Producer) stop() {
|
||||
p.mx.Lock()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
|
||||
switch p.state {
|
||||
case stateExternal:
|
||||
log.Debug().Msgf("[streams] can't stop external producer")
|
||||
return
|
||||
case stateNone:
|
||||
log.Debug().Msgf("[streams] can't stop none producer")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
|
||||
|
||||
if p.element != nil {
|
||||
_ = p.element.Stop()
|
||||
p.element = nil
|
||||
} else {
|
||||
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
|
||||
}
|
||||
p.tracks = nil
|
||||
p.state = stateNone
|
||||
if p.restart != nil {
|
||||
p.restart.Stop()
|
||||
p.restart = nil
|
||||
}
|
||||
|
||||
p.mx.Unlock()
|
||||
p.state = stateNone
|
||||
p.tracks = nil
|
||||
}
|
||||
|
@@ -3,7 +3,10 @@ package streams
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
@@ -14,46 +17,65 @@ type Consumer struct {
|
||||
type Stream struct {
|
||||
producers []*Producer
|
||||
consumers []*Consumer
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewStream(source interface{}) *Stream {
|
||||
s := new(Stream)
|
||||
|
||||
switch source := source.(type) {
|
||||
case string:
|
||||
s := new(Stream)
|
||||
prod := &Producer{url: source}
|
||||
s.producers = append(s.producers, prod)
|
||||
return s
|
||||
case []interface{}:
|
||||
s := new(Stream)
|
||||
for _, source := range source {
|
||||
prod := &Producer{url: source.(string)}
|
||||
s.producers = append(s.producers, prod)
|
||||
}
|
||||
return s
|
||||
case *Stream:
|
||||
return source
|
||||
case map[string]interface{}:
|
||||
return NewStream(source["url"])
|
||||
case nil:
|
||||
return new(Stream)
|
||||
default:
|
||||
panic("wrong source type")
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
func (s *Stream) SetSource(source string) {
|
||||
for _, prod := range s.producers {
|
||||
prod.SetSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
ic := len(s.consumers)
|
||||
|
||||
consumer := &Consumer{element: cons}
|
||||
var producers []*Producer // matched producers for consumer
|
||||
|
||||
var codecs string
|
||||
|
||||
// support for multiple simultaneous requests from different consumers
|
||||
s.wg.Add(1)
|
||||
|
||||
// Step 1. Get consumer medias
|
||||
for icc, consMedia := range cons.GetMedias() {
|
||||
log.Trace().Stringer("media", consMedia).
|
||||
Msgf("[streams] consumer:%d:%d candidate", ic, icc)
|
||||
Msgf("[streams] consumer=%d candidate=%d", ic, icc)
|
||||
|
||||
producers:
|
||||
for ip, prod := range s.producers {
|
||||
// Step 2. Get producer medias (not tracks yet)
|
||||
for ipc, prodMedia := range prod.GetMedias() {
|
||||
log.Trace().Stringer("media", prodMedia).
|
||||
Msgf("[streams] producer:%d:%d candidate", ip, ipc)
|
||||
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
|
||||
|
||||
collectCodecs(prodMedia, &codecs)
|
||||
|
||||
// Step 3. Match consumer/producer codecs list
|
||||
prodCodec := prodMedia.MatchMedia(consMedia)
|
||||
@@ -64,7 +86,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
// Step 4. Get producer track
|
||||
prodTrack := prod.GetTrack(prodMedia, prodCodec)
|
||||
if prodTrack == nil {
|
||||
log.Warn().Msg("[stream] can't get track")
|
||||
log.Warn().Str("url", prod.url).Msg("[stream] can't get track")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -72,20 +94,38 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
||||
|
||||
consumer.tracks = append(consumer.tracks, consTrack)
|
||||
producers = append(producers, prod)
|
||||
break producers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// can't match tracks for consumer
|
||||
if len(consumer.tracks) == 0 {
|
||||
return errors.New("couldn't find the matching tracks")
|
||||
s.wg.Done()
|
||||
s.wg.Wait()
|
||||
|
||||
if len(producers) == 0 {
|
||||
s.stopProducers()
|
||||
|
||||
if len(codecs) > 0 {
|
||||
return errors.New("codecs not match: " + codecs)
|
||||
}
|
||||
|
||||
for i, producer := range s.producers {
|
||||
if producer.lastErr != nil {
|
||||
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("sources unavailable: %d", len(s.producers))
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.consumers = append(s.consumers, consumer)
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, prod := range s.producers {
|
||||
// there may be duplicates, but that's not a problem
|
||||
for _, prod := range producers {
|
||||
prod.start()
|
||||
}
|
||||
|
||||
@@ -93,6 +133,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
||||
s.mu.Lock()
|
||||
for i, consumer := range s.consumers {
|
||||
if consumer.element == cons {
|
||||
// remove consumer pads from all producers
|
||||
@@ -104,50 +145,60 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, producer := range s.producers {
|
||||
var sink bool
|
||||
for _, track := range producer.tracks {
|
||||
if len(track.Sink) > 0 {
|
||||
sink = true
|
||||
}
|
||||
}
|
||||
if !sink {
|
||||
producer.stop()
|
||||
}
|
||||
}
|
||||
s.stopProducers()
|
||||
}
|
||||
|
||||
func (s *Stream) AddProducer(prod streamer.Producer) {
|
||||
producer := &Producer{element: prod, state: stateTracks}
|
||||
producer := &Producer{element: prod, state: stateExternal}
|
||||
s.mu.Lock()
|
||||
s.producers = append(s.producers, producer)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
||||
s.mu.Lock()
|
||||
for i, producer := range s.producers {
|
||||
if producer.element == prod {
|
||||
s.removeProducer(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Stream) Active() bool {
|
||||
if len(s.consumers) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, prod := range s.producers {
|
||||
if prod.element != nil {
|
||||
return true
|
||||
func (s *Stream) stopProducers() {
|
||||
s.mu.Lock()
|
||||
producers:
|
||||
for _, producer := range s.producers {
|
||||
for _, track := range producer.tracks {
|
||||
if track.HasSink() {
|
||||
continue producers
|
||||
}
|
||||
}
|
||||
producer.stop()
|
||||
}
|
||||
|
||||
return false
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
//func (s *Stream) Active() bool {
|
||||
// if len(s.consumers) > 0 {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// for _, prod := range s.producers {
|
||||
// if prod.element != nil {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
//}
|
||||
|
||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
var v []interface{}
|
||||
s.mu.Lock()
|
||||
for _, prod := range s.producers {
|
||||
if prod.element != nil {
|
||||
v = append(v, prod.element)
|
||||
@@ -157,6 +208,7 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||
// cons.element always not nil
|
||||
v = append(v, cons.element)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if len(v) == 0 {
|
||||
v = nil
|
||||
}
|
||||
@@ -188,3 +240,19 @@ func (s *Stream) removeProducer(i int) {
|
||||
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func collectCodecs(media *streamer.Media, codecs *string) {
|
||||
for _, codec := range media.Codecs {
|
||||
name := codec.Name
|
||||
if name == streamer.CodecAAC {
|
||||
name = "AAC"
|
||||
}
|
||||
if strings.Contains(*codecs, name) {
|
||||
continue
|
||||
}
|
||||
if len(*codecs) > 0 {
|
||||
*codecs += ","
|
||||
}
|
||||
*codecs += name
|
||||
}
|
||||
}
|
||||
|
@@ -24,10 +24,19 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
func Get(src string) *Stream {
|
||||
func Get(name string) *Stream {
|
||||
return streams[name]
|
||||
}
|
||||
|
||||
func New(name string, source interface{}) *Stream {
|
||||
stream := NewStream(source)
|
||||
streams[name] = stream
|
||||
return stream
|
||||
}
|
||||
|
||||
func GetOrNew(src string) *Stream {
|
||||
if stream, ok := streams[src]; ok {
|
||||
return stream
|
||||
|
||||
}
|
||||
|
||||
if !HasProducer(src) {
|
||||
@@ -35,17 +44,8 @@ func Get(src string) *Stream {
|
||||
}
|
||||
|
||||
log.Info().Str("url", src).Msg("[streams] create new stream")
|
||||
stream := NewStream(src)
|
||||
streams[src] = stream
|
||||
return stream
|
||||
}
|
||||
|
||||
func Has(src string) bool {
|
||||
return streams[src] != nil
|
||||
}
|
||||
|
||||
func New(name string, source interface{}) {
|
||||
streams[name] = NewStream(source)
|
||||
return New(src, src)
|
||||
}
|
||||
|
||||
func Delete(name string) {
|
||||
|
@@ -2,10 +2,8 @@ package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var candidates []string
|
||||
@@ -14,7 +12,27 @@ func AddCandidate(address string) {
|
||||
candidates = append(candidates, address)
|
||||
}
|
||||
|
||||
func addCanditates(answer string) (string, error) {
|
||||
func asyncCandidates(tr *api.Transport) {
|
||||
for _, address := range candidates {
|
||||
address, err := webrtc.LookupIP(address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
continue
|
||||
}
|
||||
|
||||
cand, err := webrtc.NewCandidate(address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
continue
|
||||
}
|
||||
|
||||
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
|
||||
|
||||
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
|
||||
}
|
||||
}
|
||||
|
||||
func syncCanditates(answer string) (string, error) {
|
||||
if len(candidates) == 0 {
|
||||
return answer, nil
|
||||
}
|
||||
@@ -32,15 +50,11 @@ func addCanditates(answer string) (string, error) {
|
||||
}
|
||||
|
||||
for _, address := range candidates {
|
||||
if strings.HasPrefix(address, "stun:") {
|
||||
ip, err := webrtc.GetPublicIP()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] public IP")
|
||||
continue
|
||||
}
|
||||
address = ip.String() + address[4:]
|
||||
|
||||
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
|
||||
var err error
|
||||
address, err = webrtc.LookupIP(address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] candidate")
|
||||
continue
|
||||
}
|
||||
|
||||
cand, err := webrtc.NewCandidate(address)
|
||||
@@ -64,12 +78,14 @@ func addCanditates(answer string) (string, error) {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
if ctx.Consumer == nil {
|
||||
return
|
||||
func candidateHandler(tr *api.Transport, msg *api.Message) error {
|
||||
if tr.Consumer == nil {
|
||||
return nil
|
||||
}
|
||||
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
|
||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
|
||||
conn.Push(msg)
|
||||
if conn := tr.Consumer.(*webrtc.Conn); conn != nil {
|
||||
s := msg.Value.(string)
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] remote")
|
||||
conn.AddCandidate(s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,14 +1,16 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -31,7 +33,7 @@ func Init() {
|
||||
address := cfg.Mod.Listen
|
||||
pionAPI, err := webrtc.NewAPI(address)
|
||||
if pionAPI == nil {
|
||||
log.Error().Err(err).Msg("[webrtc] init API")
|
||||
log.Error().Err(err).Caller().Msg("webrtc.NewAPI")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,8 +55,10 @@ func Init() {
|
||||
|
||||
candidates = cfg.Mod.Candidates
|
||||
|
||||
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
|
||||
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
|
||||
api.HandleWS("webrtc/offer", asyncHandler)
|
||||
api.HandleWS("webrtc/candidate", candidateHandler)
|
||||
|
||||
api.HandleFunc("api/webrtc", syncHandler)
|
||||
}
|
||||
|
||||
var Port string
|
||||
@@ -62,11 +66,11 @@ var log zerolog.Logger
|
||||
|
||||
var NewPConn func() (*pion.PeerConnection, error)
|
||||
|
||||
func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
src := ctx.Request.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
func asyncHandler(tr *api.Transport, msg *api.Message) error {
|
||||
src := tr.Request.URL.Query().Get("src")
|
||||
stream := streams.GetOrNew(src)
|
||||
if stream == nil {
|
||||
return
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
|
||||
@@ -77,21 +81,23 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
conn := new(webrtc.Conn)
|
||||
conn.Conn, err = NewPConn()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] new conn")
|
||||
return
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
conn.UserAgent = ctx.Request.UserAgent()
|
||||
conn.UserAgent = tr.Request.UserAgent()
|
||||
conn.Listen(func(msg interface{}) {
|
||||
switch msg := msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateClosed {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
case *streamer.Message:
|
||||
// subscribe on webrtc server candidates
|
||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] local")
|
||||
ctx.Write(msg)
|
||||
case *pion.ICECandidate:
|
||||
if msg != nil {
|
||||
s := msg.ToJSON().Candidate
|
||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
||||
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -100,41 +106,61 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.webrtc] set offer")
|
||||
ctx.Error(err)
|
||||
return
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
||||
log.Warn().Err(err).Caller().Send()
|
||||
_ = conn.Conn.Close()
|
||||
ctx.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Init()
|
||||
|
||||
// exchange sdp without waiting all candidates
|
||||
//answer, err := conn.ExchangeSDP(offer, false)
|
||||
//answer, err := conn.GetAnswer()
|
||||
answer, err := conn.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = addCanditates(answer)
|
||||
}
|
||||
// 3. Exchange SDP without waiting all candidates
|
||||
answer, err := conn.GetAnswer()
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] get answer")
|
||||
ctx.Error(err)
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
|
||||
tr.Consumer = conn
|
||||
|
||||
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
|
||||
|
||||
asyncCandidates(tr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("src")
|
||||
stream := streams.Get(url)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Write(&streamer.Message{
|
||||
Type: webrtc.MsgTypeAnswer, Value: answer,
|
||||
})
|
||||
// get offer
|
||||
offer, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Consumer = conn
|
||||
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Msg("ExchangeSDP")
|
||||
return
|
||||
}
|
||||
|
||||
// send SDP to client
|
||||
if _, err = w.Write([]byte(answer)); err != nil {
|
||||
log.Error().Err(err).Caller().Msg("w.Write")
|
||||
}
|
||||
}
|
||||
|
||||
func ExchangeSDP(
|
||||
@@ -144,7 +170,7 @@ func ExchangeSDP(
|
||||
conn := new(webrtc.Conn)
|
||||
conn.Conn, err = NewPConn()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] new conn")
|
||||
log.Error().Err(err).Caller().Msg("NewPConn")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,13 +188,13 @@ func ExchangeSDP(
|
||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||
|
||||
if err = conn.SetOffer(offer); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.webrtc] set offer")
|
||||
log.Warn().Err(err).Caller().Msg("conn.SetOffer")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. AddConsumer, so we get new tracks
|
||||
if err = stream.AddConsumer(conn); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
||||
log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
|
||||
_ = conn.Conn.Close()
|
||||
return
|
||||
}
|
||||
@@ -179,12 +205,12 @@ func ExchangeSDP(
|
||||
//answer, err := conn.ExchangeSDP(offer, false)
|
||||
answer, err = conn.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = addCanditates(answer)
|
||||
answer, err = syncCanditates(answer)
|
||||
}
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] get answer")
|
||||
log.Error().Err(err).Caller().Msg("conn.GetCompleteAnswer")
|
||||
}
|
||||
|
||||
return
|
||||
|
@@ -1,58 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/pion/rtp"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, err := rtsp.NewClient(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = client.Dial(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = client.Describe(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, media := range client.GetMedias() {
|
||||
fmt.Printf("Media: %v\n", media)
|
||||
|
||||
if media.AV() {
|
||||
track := client.GetTrack(media, media.Codecs[0])
|
||||
fmt.Printf("Track: %v, %v\n", track, track.Codec)
|
||||
|
||||
track.Bind(func(packet *rtp.Packet) error {
|
||||
nalUnitType := packet.Payload[0] & 0x1F
|
||||
fmt.Printf(
|
||||
"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
|
||||
track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
|
||||
packet.PayloadType, packet.SSRC,
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.Play(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
time.AfterFunc(time.Second*5, func() {
|
||||
if err = client.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
if err = client.Handle(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("The End")
|
||||
}
|
18
go.mod
18
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.17
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/brutella/hap v0.0.17
|
||||
@@ -26,6 +26,7 @@ require (
|
||||
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/kr/pretty v0.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
@@ -41,21 +42,18 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.org/x/tools v0.1.11 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
// windows support: https://github.com/brutella/dnssd/pull/35
|
||||
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
|
||||
// RTP tlv8 fix
|
||||
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657
|
||||
// MSE update
|
||||
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e
|
||||
// AES_256_CM_HMAC_SHA1_80 support
|
||||
github.com/pion/srtp/v2 v2.0.10 => github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10
|
||||
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
|
||||
)
|
||||
|
68
go.sum
68
go.sum
@@ -1,12 +1,9 @@
|
||||
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78=
|
||||
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0=
|
||||
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg=
|
||||
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
||||
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
|
||||
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
|
||||
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 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
|
||||
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/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=
|
||||
@@ -17,7 +14,6 @@ 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/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@@ -31,7 +27,6 @@ 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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
@@ -39,13 +34,12 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
||||
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
||||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
@@ -56,72 +50,49 @@ github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7Xn
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
|
||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
|
||||
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
|
||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
||||
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
|
||||
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
|
||||
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
||||
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
||||
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
|
||||
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
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.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
|
||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
|
||||
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/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
|
||||
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
||||
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
|
||||
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
|
||||
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
||||
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
||||
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
|
||||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
||||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
|
||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
|
||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
|
||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
|
||||
github.com/pion/webrtc/v3 v3.1.41/go.mod h1:sUcW9SFPEWerDqGOBmdYEMfRvbdd7rgwo4bNzfsXww4=
|
||||
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
|
||||
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -132,7 +103,6 @@ github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -145,28 +115,21 @@ github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -190,13 +153,11 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cO
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -210,8 +171,9 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -222,12 +184,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
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 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@@ -242,9 +204,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
52
hardware.Dockerfile
Normal file
52
hardware.Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
||||
# 0. Prepare images
|
||||
# only debian 12 (bookworm) has latest ffmpeg
|
||||
ARG DEBIAN_VERSION="bookworm-slim"
|
||||
ARG GO_VERSION="1.19-buster"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM debian:${DEBIAN_VERSION} AS base
|
||||
FROM golang:${GO_VERSION} AS go
|
||||
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
FROM go AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Collect all files
|
||||
FROM scratch AS rootfs
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
COPY ./build/docker/run.sh /
|
||||
|
||||
|
||||
# 3. Final image
|
||||
FROM base
|
||||
|
||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
|
||||
|
||||
COPY --from=rootfs / /
|
||||
|
||||
RUN chmod a+x /run.sh && mkdir -p /config
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_VISIBLE_DEVICES all
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
|
||||
|
||||
CMD ["/run.sh"]
|
8
main.go
8
main.go
@@ -9,7 +9,9 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/cmd/hass"
|
||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||
"github.com/AlexxIT/go2rtc/cmd/http"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtmp"
|
||||
@@ -26,6 +28,8 @@ func main() {
|
||||
app.Init() // init config and logs
|
||||
streams.Init() // load streams list
|
||||
|
||||
api.Init() // init HTTP API server
|
||||
|
||||
echo.Init()
|
||||
|
||||
rtsp.Init() // add support RTSP client and RTSP server
|
||||
@@ -34,10 +38,10 @@ func main() {
|
||||
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
|
||||
hass.Init() // add support hass scheme
|
||||
|
||||
api.Init() // init HTTP API server
|
||||
|
||||
webrtc.Init()
|
||||
mp4.Init()
|
||||
mjpeg.Init()
|
||||
http.Init()
|
||||
|
||||
srtp.Init()
|
||||
homekit.Init()
|
||||
|
@@ -2,4 +2,7 @@
|
||||
|
||||
- https://www.wowza.com/blog/streaming-protocols
|
||||
- https://vimeo.com/blog/post/rtmp-stream/
|
||||
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a
|
||||
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a
|
||||
- [Android Supported media formats](https://developer.android.com/guide/topics/media/media-formats)
|
||||
- [THEOplayer](https://www.theoplayer.com/test-your-stream-hls-dash-hesp)
|
||||
- [How Generate DTS/PTS](https://www.ramugedia.com/how-generate-dts-pts-from-elementary-stream)
|
||||
|
20
pkg/aac/README.md
Normal file
20
pkg/aac/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## 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
|
||||
|
||||
## Useful links
|
||||
|
||||
- [4.6.20 Enhanced Low Delay Codec](https://csclub.uwaterloo.ca/~ehashman/ISO14496-3-2009.pdf)
|
||||
- https://stackoverflow.com/questions/40014508/aac-adts-for-aacobject-eld-packets
|
||||
- https://code.videolan.org/videolan/vlc/-/blob/master/modules/packetizer/mpeg4audio.c
|
57
pkg/aac/rtp.go
Normal file
57
pkg/aac/rtp.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package aac
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
const RTPPacketVersionAAC = 0
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
// support ONLY 2 bytes header size!
|
||||
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
|
||||
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)
|
||||
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAAC
|
||||
clone.Payload = packet.Payload[2+headersSize:]
|
||||
return push(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
if packet.Version != RTPPacketVersionAAC {
|
||||
return push(packet)
|
||||
}
|
||||
|
||||
// support ONLY one unit in payload
|
||||
size := uint16(len(packet.Payload))
|
||||
// 2 bytes header size + 2 bytes first payload size
|
||||
payload := make([]byte, 2+2+size)
|
||||
payload[1] = 16 // header size in bits
|
||||
binary.BigEndian.PutUint16(payload[2:], size<<3)
|
||||
copy(payload[4:], packet.Payload)
|
||||
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: true,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
return push(&clone)
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,7 +24,7 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
|
||||
panic("you shall not pass!")
|
||||
}
|
||||
|
||||
track := &streamer.Track{Codec: codec, Direction: media.Direction}
|
||||
track := streamer.NewTrack(codec, media.Direction)
|
||||
|
||||
switch media.Direction {
|
||||
case streamer.DirectionSendonly:
|
||||
|
@@ -1,3 +1,22 @@
|
||||
# H264
|
||||
|
||||
Access Unit (AU) can contain one or multiple NAL Unit:
|
||||
|
||||
1. [SEI,] SPS, PPS, IFrame, [IFrame...]
|
||||
2. BFrame, [BFrame...]
|
||||
3. IFrame, [IFrame...]
|
||||
|
||||
## RTP H264
|
||||
|
||||
Camera | NALu
|
||||
-------|-----
|
||||
EZVIZ C3S | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 1t, 1t, 1t
|
||||
Sonoff GK-200MP2-B | 28:28:28 -> 5t, 1t, 1t, 1t
|
||||
Dahua IPC-K42 | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 28:28:28 -> 1t
|
||||
FFmpeg copy | 5t, 1t, 1t, 28:28:28 -> 1t, 28:28:28 -> 1t
|
||||
FFmpeg h264 | 24 -> 6:5:5:5:5t, 24 -> 1:1:1:1t, 28:28:28 -> 5f, 28:28:28 -> 5f, 28:28:28 -> 5t
|
||||
FFmpeg resize | 6f, 28:28:28 -> 5f, 28... -> 5t, 24 -> 1:1f, 24 -> 1:1t
|
||||
|
||||
## WebRTC
|
||||
|
||||
Video codec | Media string | Device
|
||||
@@ -25,3 +44,4 @@ H.264/high | avc1.6400xx | FFmpeg superfast
|
||||
- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
|
||||
- [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/)
|
||||
|
@@ -6,55 +6,38 @@ import (
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
const PayloadTypeAVC = 255
|
||||
func EncodeAVC(nals ...[]byte) (avc []byte) {
|
||||
var i, n int
|
||||
|
||||
func IsAVC(codec *streamer.Codec) bool {
|
||||
return codec.PayloadType == PayloadTypeAVC
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeAVC(raw []byte) (avc []byte) {
|
||||
avc = make([]byte, len(raw)+4)
|
||||
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
|
||||
copy(avc[4:], raw)
|
||||
return
|
||||
}
|
||||
|
||||
func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
|
||||
sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||
sps = EncodeAVC(sps)
|
||||
pps = EncodeAVC(pps)
|
||||
ps := EncodeAVC(sps, pps)
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) (err error) {
|
||||
naluType := NALUType(packet.Payload)
|
||||
switch naluType {
|
||||
case NALUTypeSPS:
|
||||
sps = packet.Payload
|
||||
return
|
||||
case NALUTypePPS:
|
||||
pps = packet.Payload
|
||||
return
|
||||
if NALUType(packet.Payload) == NALUTypeIFrame {
|
||||
packet.Payload = Join(ps, packet.Payload)
|
||||
}
|
||||
|
||||
var clone rtp.Packet
|
||||
|
||||
if naluType == NALUTypeIFrame {
|
||||
clone = *packet
|
||||
clone.Payload = sps
|
||||
if err = push(&clone); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
clone = *packet
|
||||
clone.Payload = pps
|
||||
if err = push(&clone); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
clone = *packet
|
||||
clone.Payload = packet.Payload
|
||||
return push(&clone)
|
||||
return push(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,12 +46,12 @@ func SplitAVC(data []byte) [][]byte {
|
||||
var nals [][]byte
|
||||
for {
|
||||
// get AVC length
|
||||
size := int(binary.BigEndian.Uint32(data))
|
||||
size := int(binary.BigEndian.Uint32(data)) + 4
|
||||
|
||||
// check if multiple items in one packet
|
||||
if size+4 < len(data) {
|
||||
nals = append(nals, data[:size+4])
|
||||
data = data[size+4:]
|
||||
if size < len(data) {
|
||||
nals = append(nals, data[:size])
|
||||
data = data[size:]
|
||||
} else {
|
||||
nals = append(nals, data)
|
||||
break
|
||||
@@ -76,3 +59,18 @@ func SplitAVC(data []byte) [][]byte {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@@ -2,30 +2,66 @@ package h264
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
NALUTypePFrame = 1
|
||||
NALUTypeIFrame = 5
|
||||
NALUTypeSEI = 6
|
||||
NALUTypeSPS = 7
|
||||
NALUTypePPS = 8
|
||||
NALUTypePFrame = 1 // Coded slice of a non-IDR picture
|
||||
NALUTypeIFrame = 5 // Coded slice of an IDR picture
|
||||
NALUTypeSEI = 6 // Supplemental enhancement information (SEI)
|
||||
NALUTypeSPS = 7 // Sequence parameter set
|
||||
NALUTypePPS = 8 // Picture parameter set
|
||||
NALUTypeAUD = 9 // Access unit delimiter
|
||||
)
|
||||
|
||||
func NALUType(b []byte) byte {
|
||||
return b[4] & 0x1F
|
||||
}
|
||||
|
||||
// IsKeyframe - check if any NALU in one AU is Keyframe
|
||||
func IsKeyframe(b []byte) bool {
|
||||
return NALUType(b) == NALUTypeIFrame
|
||||
for {
|
||||
switch NALUType(b) {
|
||||
case NALUTypePFrame:
|
||||
return false
|
||||
case NALUTypeIFrame:
|
||||
return true
|
||||
}
|
||||
|
||||
size := int(binary.BigEndian.Uint32(b)) + 4
|
||||
if size < len(b) {
|
||||
b = b[size:]
|
||||
continue
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Join(ps, iframe []byte) []byte {
|
||||
b := make([]byte, len(ps)+len(iframe))
|
||||
i := copy(b, ps)
|
||||
copy(b[i:], iframe)
|
||||
return b
|
||||
}
|
||||
|
||||
func GetProfileLevelID(fmtp string) string {
|
||||
if fmtp == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// some cameras has wrong profile-level-id
|
||||
// https://github.com/AlexxIT/go2rtc/issues/155
|
||||
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
|
||||
sps, _ := base64.StdEncoding.DecodeString(s)
|
||||
if len(sps) >= 4 {
|
||||
return fmt.Sprintf("%06X", sps[1:4])
|
||||
}
|
||||
}
|
||||
|
||||
return streamer.Between(fmtp, "profile-level-id=", ";")
|
||||
}
|
||||
|
||||
|
166
pkg/h264/rtp.go
166
pkg/h264/rtp.go
@@ -13,91 +13,69 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
depack := &codecs.H264Packet{IsAVC: true}
|
||||
|
||||
sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||
sps = EncodeAVC(sps)
|
||||
pps = EncodeAVC(pps)
|
||||
ps := EncodeAVC(sps, pps)
|
||||
|
||||
var buffer []byte
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
//nalUnitType := packet.Payload[0] & 0x1F
|
||||
//fmt.Printf(
|
||||
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
|
||||
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
|
||||
// packet.PayloadType, packet.SSRC,
|
||||
//)
|
||||
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
// NALu packets can be split in different ways:
|
||||
// - single type 7 and type 8 packets
|
||||
// - join type 7 and type 8 packet (type 24)
|
||||
// - split type 5 on multiple 28 packets
|
||||
// - split type 5 on multiple separate 28 packets
|
||||
units, err := depack.Unmarshal(packet.Payload)
|
||||
if len(units) == 0 || err != nil {
|
||||
payload, err := depack.Unmarshal(packet.Payload)
|
||||
if len(payload) == 0 || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for len(units) > 0 {
|
||||
i := int(binary.BigEndian.Uint32(units)) + 4
|
||||
unit := units[:i] // NAL Unit with AVC header
|
||||
units = units[i:]
|
||||
|
||||
unitType := NALUType(unit)
|
||||
//fmt.Printf("[H264] type: %2d, size: %6d\n", unitType, i)
|
||||
switch unitType {
|
||||
case NALUTypeSPS:
|
||||
//println("new SPS")
|
||||
sps = unit
|
||||
continue
|
||||
case NALUTypePPS:
|
||||
//println("new PPS")
|
||||
pps = unit
|
||||
continue
|
||||
case NALUTypeSEI:
|
||||
// some unnecessary text information
|
||||
continue
|
||||
}
|
||||
|
||||
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
|
||||
// and every NALU will be sliced to multiple NALUs
|
||||
if !packet.Marker {
|
||||
buffer = append(buffer, unit...)
|
||||
continue
|
||||
}
|
||||
|
||||
if buffer != nil {
|
||||
buffer = append(buffer, unit...)
|
||||
unit = buffer
|
||||
buffer = nil
|
||||
}
|
||||
|
||||
var clone rtp.Packet
|
||||
|
||||
if unitType == NALUTypeIFrame {
|
||||
clone = *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = sps
|
||||
if err = push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clone = *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = pps
|
||||
if err = push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
clone = *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = unit
|
||||
if err = push(&clone); err != nil {
|
||||
return err
|
||||
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||
if packet.Marker {
|
||||
switch NALUType(payload) {
|
||||
case NALUTypeSPS, NALUTypePPS:
|
||||
buf = append(buf, payload...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
if len(buf) == 0 {
|
||||
for {
|
||||
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
|
||||
// Amcrest IP4M-1051: 9, 6, 1
|
||||
switch NALUType(payload) {
|
||||
case NALUTypeIFrame:
|
||||
// fix IFrame without SPS,PPS
|
||||
buf = append(buf, ps...)
|
||||
case NALUTypeSEI, NALUTypeAUD:
|
||||
// fix ffmpeg with transcoding first frame
|
||||
i := int(4 + binary.BigEndian.Uint32(payload))
|
||||
|
||||
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
|
||||
if i == len(payload) {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload = payload[i:]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// collect all NALs for Access Unit
|
||||
if !packet.Marker {
|
||||
buf = append(buf, payload...)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(buf) > 0 {
|
||||
payload = append(buf, payload...)
|
||||
buf = buf[:0]
|
||||
}
|
||||
|
||||
//log.Printf("[AVC] %v, len: %d", Types(payload), len(payload))
|
||||
|
||||
clone := *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = payload
|
||||
return push(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,28 +87,28 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
if packet.Version == RTPPacketVersionAVC {
|
||||
payloads := payloader.Payload(mtu, packet.Payload)
|
||||
for i, payload := range payloads {
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: i == len(payloads)-1,
|
||||
//PayloadType: packet.PayloadType,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
//SSRC: packet.SSRC,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if err := push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if packet.Version != RTPPacketVersionAVC {
|
||||
return push(packet)
|
||||
}
|
||||
|
||||
return push(packet)
|
||||
payloads := payloader.Payload(mtu, packet.Payload)
|
||||
last := len(payloads) - 1
|
||||
for i, payload := range payloads {
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: i == last,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if err := push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
pkg/h265/README.md
Normal file
4
pkg/h265/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## Useful links
|
||||
|
||||
- https://datatracker.ietf.org/doc/html/rfc7798
|
||||
- [Add initial support for WebRTC HEVC](https://trac.webkit.org/changeset/259452/webkit)
|
73
pkg/h265/helper.go
Normal file
73
pkg/h265/helper.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package h265
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
)
|
||||
|
||||
const (
|
||||
NALUTypePFrame = 1
|
||||
NALUTypeIFrame = 19
|
||||
NALUTypeIFrame2 = 20
|
||||
NALUTypeIFrame3 = 21
|
||||
NALUTypeVPS = 32
|
||||
NALUTypeSPS = 33
|
||||
NALUTypePPS = 34
|
||||
NALUTypeFU = 49
|
||||
)
|
||||
|
||||
func NALUType(b []byte) byte {
|
||||
return (b[4] >> 1) & 0x3F
|
||||
}
|
||||
|
||||
func IsKeyframe(b []byte) bool {
|
||||
for {
|
||||
switch NALUType(b) {
|
||||
case NALUTypePFrame:
|
||||
return false
|
||||
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||
return true
|
||||
}
|
||||
|
||||
size := int(binary.BigEndian.Uint32(b)) + 4
|
||||
if size < len(b) {
|
||||
b = b[size:]
|
||||
continue
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
|
||||
if fmtp == "" {
|
||||
return
|
||||
}
|
||||
|
||||
s := streamer.Between(fmtp, "sprop-vps=", ";")
|
||||
vps, _ = base64.StdEncoding.DecodeString(s)
|
||||
|
||||
s = streamer.Between(fmtp, "sprop-sps=", ";")
|
||||
sps, _ = base64.StdEncoding.DecodeString(s)
|
||||
|
||||
s = streamer.Between(fmtp, "sprop-pps=", ";")
|
||||
pps, _ = base64.StdEncoding.DecodeString(s)
|
||||
|
||||
return
|
||||
}
|
147
pkg/h265/rtp.go
Normal file
147
pkg/h265/rtp.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package h265
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
//vps, sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||
//ps := h264.EncodeAVC(vps, sps, pps)
|
||||
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
var nuStart int
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
data := packet.Payload
|
||||
nuType := (data[0] >> 1) & 0x3F
|
||||
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
if nuType == NALUTypeFU {
|
||||
switch data[2] >> 6 {
|
||||
case 2: // begin
|
||||
nuType = data[2] & 0x3F
|
||||
|
||||
// push PS data before keyframe
|
||||
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
|
||||
// buf = append(buf, ps...)
|
||||
//}
|
||||
|
||||
nuStart = len(buf)
|
||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
||||
buf = append(buf, data[3:]...)
|
||||
return nil
|
||||
case 0: // continue
|
||||
buf = append(buf, data[3:]...)
|
||||
return nil
|
||||
case 1: // end
|
||||
buf = append(buf, data[3:]...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
||||
}
|
||||
} else {
|
||||
nuStart = len(buf)
|
||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||
buf = append(buf, data...)
|
||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
|
||||
}
|
||||
|
||||
// collect all NAL Units for Access Unit
|
||||
if !packet.Marker {
|
||||
return nil
|
||||
}
|
||||
|
||||
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
|
||||
|
||||
clone := *packet
|
||||
clone.Version = h264.RTPPacketVersionAVC
|
||||
clone.Payload = buf
|
||||
|
||||
buf = buf[:0]
|
||||
|
||||
return push(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SafariPay - generate Safari friendly payload for H265
|
||||
// https://github.com/AlexxIT/Blog/issues/5
|
||||
func SafariPay(mtu uint16) streamer.WrapperFunc {
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
size := int(mtu - 12) // rtp.Header size
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return push(packet)
|
||||
}
|
||||
|
||||
// protect original packets from modification
|
||||
au := make([]byte, len(packet.Payload))
|
||||
copy(au, packet.Payload)
|
||||
|
||||
var start byte
|
||||
|
||||
for i := 0; i < len(au); {
|
||||
size := int(binary.BigEndian.Uint32(au[i:])) + 4
|
||||
|
||||
// convert AVC to Annex-B
|
||||
au[i] = 0
|
||||
au[i+1] = 0
|
||||
au[i+2] = 0
|
||||
au[i+3] = 1
|
||||
|
||||
switch NALUType(au[i:]) {
|
||||
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||
start = 3
|
||||
default:
|
||||
if start == 0 {
|
||||
start = 2
|
||||
}
|
||||
}
|
||||
|
||||
i += size
|
||||
}
|
||||
|
||||
// rtp.Packet payload
|
||||
b := make([]byte, 1, size)
|
||||
size-- // minus header byte
|
||||
|
||||
for au != nil {
|
||||
b[0] = start
|
||||
|
||||
if start > 1 {
|
||||
start -= 2
|
||||
}
|
||||
|
||||
if len(au) > size {
|
||||
b = append(b, au[:size]...)
|
||||
au = au[size:]
|
||||
} else {
|
||||
b = append(b, au...)
|
||||
au = nil
|
||||
}
|
||||
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: au == nil,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
},
|
||||
Payload: b,
|
||||
}
|
||||
if err := push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b = b[:1] // clear buffer
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
# Homekit
|
||||
# Home Accessory Protocol
|
||||
|
||||
> PS. Character = Characteristic
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package homekit
|
||||
package hap
|
||||
|
||||
type Accessory struct {
|
||||
AID int `json:"aid"`
|
@@ -2,22 +2,22 @@ package camera
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/brutella/hap/characteristic"
|
||||
"github.com/brutella/hap/rtp"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *homekit.Client
|
||||
client *hap.Conn
|
||||
}
|
||||
|
||||
func NewClient(client *homekit.Client) *Client {
|
||||
func NewClient(client *hap.Conn) *Client {
|
||||
return &Client{client: client}
|
||||
}
|
||||
|
||||
func (c *Client) StartStream2(ses *Session) (err error) {
|
||||
func (c *Client) StartStream(ses *Session) (err error) {
|
||||
// Step 1. Check if camera ready (free) to stream
|
||||
var srv *homekit.Service
|
||||
var srv *hap.Service
|
||||
if srv, err = c.GetFreeStream(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -35,8 +35,8 @@ func (c *Client) StartStream2(ses *Session) (err error) {
|
||||
// GetFreeStream search free streaming service.
|
||||
// Usual every HomeKit camera can stream only to two clients simultaniosly.
|
||||
// So it has two similar services for streaming.
|
||||
func (c *Client) GetFreeStream() (srv *homekit.Service, err error) {
|
||||
var accs []*homekit.Accessory
|
||||
func (c *Client) GetFreeStream() (srv *hap.Service, err error) {
|
||||
var accs []*hap.Accessory
|
||||
if accs, err = c.client.GetAccessories(); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func (c *Client) GetFreeStream() (srv *homekit.Service, err error) {
|
||||
}
|
||||
|
||||
func (c *Client) SetupEndpoins(
|
||||
srv *homekit.Service, req *rtp.SetupEndpoints,
|
||||
srv *hap.Service, req *rtp.SetupEndpoints,
|
||||
) (res *rtp.SetupEndpointsResponse, err error) {
|
||||
// get setup endpoint character ID
|
||||
char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
|
||||
@@ -87,7 +87,7 @@ func (c *Client) SetupEndpoins(
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) SetConfig(srv *homekit.Service, config *rtp.StreamConfiguration) (err error) {
|
||||
func (c *Client) SetConfig(srv *hap.Service, config *rtp.StreamConfiguration) (err error) {
|
||||
// get setup endpoint character ID
|
||||
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
|
||||
char.Event = nil
|
75
pkg/hap/camera/session.go
Normal file
75
pkg/hap/camera/session.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"github.com/brutella/hap/rtp"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Offer *rtp.SetupEndpoints
|
||||
Answer *rtp.SetupEndpointsResponse
|
||||
Config *rtp.StreamConfiguration
|
||||
}
|
||||
|
||||
func NewSession(vp *rtp.VideoParameters, ap *rtp.AudioParameters) *Session {
|
||||
vp.RTP = rtp.RTPParams{
|
||||
PayloadType: 99,
|
||||
Ssrc: RandomUint32(),
|
||||
Bitrate: 2048,
|
||||
Interval: 10,
|
||||
MTU: 1200, // like WebRTC
|
||||
}
|
||||
ap.RTP = rtp.RTPParams{
|
||||
PayloadType: 110,
|
||||
Ssrc: RandomUint32(),
|
||||
Bitrate: 32,
|
||||
Interval: 10,
|
||||
ComfortNoisePayloadType: 98,
|
||||
MTU: 0,
|
||||
}
|
||||
|
||||
sessionID := RandomBytes(16)
|
||||
s := &Session{
|
||||
Offer: &rtp.SetupEndpoints{
|
||||
SessionId: sessionID,
|
||||
Video: rtp.CryptoSuite{
|
||||
MasterKey: RandomBytes(16),
|
||||
MasterSalt: RandomBytes(14),
|
||||
},
|
||||
Audio: rtp.CryptoSuite{
|
||||
MasterKey: RandomBytes(16),
|
||||
MasterSalt: RandomBytes(14),
|
||||
},
|
||||
},
|
||||
Config: &rtp.StreamConfiguration{
|
||||
Command: rtp.SessionControlCommand{
|
||||
Identifier: sessionID,
|
||||
Type: rtp.SessionControlCommandTypeStart,
|
||||
},
|
||||
Video: *vp,
|
||||
Audio: *ap,
|
||||
},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Session) SetLocalEndpoint(host string, port uint16) {
|
||||
s.Offer.ControllerAddr = rtp.Addr{
|
||||
IPAddr: host,
|
||||
VideoRtpPort: port,
|
||||
AudioRtpPort: port,
|
||||
}
|
||||
}
|
||||
|
||||
func RandomBytes(size int) []byte {
|
||||
data := make([]byte, size)
|
||||
_, _ = cryptorand.Read(data)
|
||||
return data
|
||||
}
|
||||
|
||||
func RandomUint32() uint32 {
|
||||
data := make([]byte, 4)
|
||||
_, _ = cryptorand.Read(data)
|
||||
return binary.BigEndian.Uint32(data)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package homekit
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bytes"
|
733
pkg/hap/conn.go
Normal file
733
pkg/hap/conn.go
Normal file
@@ -0,0 +1,733 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/brutella/hap"
|
||||
"github.com/brutella/hap/chacha20poly1305"
|
||||
"github.com/brutella/hap/curve25519"
|
||||
"github.com/brutella/hap/ed25519"
|
||||
"github.com/brutella/hap/hkdf"
|
||||
"github.com/brutella/hap/tlv8"
|
||||
"github.com/tadglines/go-pkgs/crypto/srp"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Conn for HomeKit. DevicePublic can be null.
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
|
||||
DeviceAddress string // including port
|
||||
DeviceID string
|
||||
DevicePublic []byte
|
||||
ClientID string
|
||||
ClientPrivate []byte
|
||||
|
||||
OnEvent func(res *http.Response)
|
||||
Output func(msg interface{})
|
||||
|
||||
conn net.Conn
|
||||
secure *Secure
|
||||
httpResponse chan *bufio.Reader
|
||||
}
|
||||
|
||||
func NewConn(rawURL string) (*Conn, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
c := &Conn{
|
||||
DeviceAddress: u.Host,
|
||||
DeviceID: query.Get("device_id"),
|
||||
DevicePublic: DecodeKey(query.Get("device_public")),
|
||||
ClientID: query.Get("client_id"),
|
||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Pair(deviceID, pin string) (*Conn, error) {
|
||||
entry := mdns.GetEntry(deviceID)
|
||||
if entry == nil {
|
||||
return nil, errors.New("can't find device via mDNS")
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
|
||||
DeviceID: deviceID,
|
||||
ClientID: GenerateUUID(),
|
||||
ClientPrivate: GenerateKey(),
|
||||
}
|
||||
|
||||
var mfi bool
|
||||
for _, field := range entry.InfoFields {
|
||||
if field[:2] == "ff" {
|
||||
if field[3] == '1' {
|
||||
mfi = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return c, c.Pair(mfi, pin)
|
||||
}
|
||||
|
||||
func (c *Conn) ClientPublic() []byte {
|
||||
return c.ClientPrivate[32:]
|
||||
}
|
||||
|
||||
func (c *Conn) URL() string {
|
||||
return fmt.Sprintf(
|
||||
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
|
||||
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Conn) DialAndServe() error {
|
||||
if err := c.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Handle()
|
||||
}
|
||||
|
||||
func (c *Conn) Dial() error {
|
||||
// update device host before dial
|
||||
if host := mdns.GetAddress(c.DeviceID); host != "" {
|
||||
c.DeviceAddress = host
|
||||
}
|
||||
|
||||
var err error
|
||||
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, time.Second*5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M1: send our session public to device
|
||||
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
||||
|
||||
// 1. generate payload
|
||||
// important not include other fields
|
||||
requestM1 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
}{
|
||||
State: hap.M1,
|
||||
PublicKey: sessionPublic[:],
|
||||
}
|
||||
// 2. pack payload to TLV8
|
||||
buf, err := tlv8.Marshal(requestM1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. send request
|
||||
resp, err := c.Post(UriPairVerify, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M2: unpack deviceID from response
|
||||
responseM2 := PairVerifyPayload{}
|
||||
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. generate session shared key
|
||||
var deviceSessionPublic [32]byte
|
||||
copy(deviceSessionPublic[:], responseM2.PublicKey)
|
||||
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
|
||||
sessionKey, err := hkdf.Sha512(
|
||||
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
|
||||
[]byte("Pair-Verify-Encrypt-Info"),
|
||||
)
|
||||
|
||||
// 2. decrypt M2 response with session key
|
||||
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
|
||||
var mac [16]byte
|
||||
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
|
||||
|
||||
buf, err = chacha20poly1305.DecryptAndVerify(
|
||||
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
|
||||
)
|
||||
|
||||
// 3. unpack payload from TLV8
|
||||
payloadM2 := PairVerifyPayload{}
|
||||
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. verify signature for M2 response with device public
|
||||
// device session + device id + our session
|
||||
if c.DevicePublic != nil {
|
||||
buf = nil
|
||||
buf = append(buf, responseM2.PublicKey[:]...)
|
||||
buf = append(buf, []byte(payloadM2.Identifier)...)
|
||||
buf = append(buf, sessionPublic[:]...)
|
||||
if !ed25519.ValidateSignature(
|
||||
c.DevicePublic[:], buf, payloadM2.Signature,
|
||||
) {
|
||||
return errors.New("device public signature invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// STEP M3: send our clientID to device
|
||||
// 1. generate signature with our private key
|
||||
// (our session + our ID + device session)
|
||||
buf = nil
|
||||
buf = append(buf, sessionPublic[:]...)
|
||||
buf = append(buf, []byte(c.ClientID)...)
|
||||
buf = append(buf, responseM2.PublicKey[:]...)
|
||||
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. generate payload
|
||||
payloadM3 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
Signature: signature,
|
||||
}
|
||||
// 3. pack payload to TLV8
|
||||
buf, err = tlv8.Marshal(payloadM3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. encrypt payload with session key
|
||||
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
|
||||
sessionKey[:], []byte("PV-Msg03"), buf, nil,
|
||||
)
|
||||
|
||||
// 4. generate request
|
||||
requestM3 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: hap.M3,
|
||||
EncryptedData: append(msg, mac[:]...),
|
||||
}
|
||||
// 5. pack payload to TLV8
|
||||
buf, err = tlv8.Marshal(requestM3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err = c.Post(UriPairVerify, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M4. Read response
|
||||
responseM4 := PairVerifyPayload{}
|
||||
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. check response state
|
||||
if responseM4.State != 4 || responseM4.Status != 0 {
|
||||
return fmt.Errorf("wrong M4 response: %+v", responseM4)
|
||||
}
|
||||
|
||||
c.secure, err = NewSecure(sessionShared, false)
|
||||
//c.secure.Buffer = bytes.NewBuffer(nil)
|
||||
c.secure.Conn = c.conn
|
||||
|
||||
c.httpResponse = make(chan *bufio.Reader, 10)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
|
||||
func (c *Conn) Pair(mfi bool, pin string) (err error) {
|
||||
pin = strings.ReplaceAll(pin, "-", "")
|
||||
if len(pin) != 8 {
|
||||
return fmt.Errorf("wrong PIN format: %s", pin)
|
||||
}
|
||||
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:]
|
||||
|
||||
c.conn, err = net.Dial("tcp", c.DeviceAddress)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M1. Generate request
|
||||
reqM1 := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: hap.M1,
|
||||
}
|
||||
if mfi {
|
||||
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
|
||||
}
|
||||
buf, err := tlv8.Marshal(reqM1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M1. Send request
|
||||
res, err := c.Post(UriPairSetup, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M2. Read response
|
||||
resM2 := struct {
|
||||
Salt []byte `tlv8:"2"`
|
||||
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
|
||||
return
|
||||
}
|
||||
if resM2.State != 2 || resM2.Error > 0 {
|
||||
return fmt.Errorf("wrong M2: %+v", resM2)
|
||||
}
|
||||
|
||||
// STEP M3. Generate session using pin
|
||||
username := []byte("Pair-Setup")
|
||||
|
||||
SRP, err := srp.NewSRP(
|
||||
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
SRP.SaltLength = 16
|
||||
|
||||
// username: "Pair-Setup"
|
||||
// password: PIN (with dashes)
|
||||
session := SRP.NewClientSession(username, []byte(pin))
|
||||
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M3. Generate request
|
||||
reqM3 := struct {
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Proof []byte `tlv8:"4"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
PublicKey: session.GetA(), // client public key, aka session.A
|
||||
Proof: session.ComputeAuthenticator(),
|
||||
State: hap.M3,
|
||||
}
|
||||
buf, err = tlv8.Marshal(reqM3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M3. Send request
|
||||
res, err = c.Post(UriPairSetup, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M4. Read response
|
||||
resM4 := struct {
|
||||
Proof []byte `tlv8:"4"` // server proof
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
|
||||
return
|
||||
}
|
||||
if resM4.Error == 2 {
|
||||
return fmt.Errorf("wrong PIN: %s", pin)
|
||||
}
|
||||
if resM4.State != 4 || resM4.Error > 0 {
|
||||
return fmt.Errorf("wrong M4: %+v", resM4)
|
||||
}
|
||||
|
||||
// STEP M4. Verify response
|
||||
if !session.VerifyServerAuthenticator(resM4.Proof) {
|
||||
return errors.New("verify server auth fail")
|
||||
}
|
||||
|
||||
// STEP M5. Generate signature
|
||||
saltKey, err := hkdf.Sha512(
|
||||
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
|
||||
[]byte("Pair-Setup-Controller-Sign-Info"),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf = nil
|
||||
buf = append(buf, saltKey[:]...)
|
||||
buf = append(buf, []byte(c.ClientID)...)
|
||||
buf = append(buf, c.ClientPublic()...)
|
||||
|
||||
signature, err := ed25519.Signature(c.ClientPrivate, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M5. Generate payload
|
||||
msgM5 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
PublicKey: c.ClientPublic(),
|
||||
Signature: signature,
|
||||
}
|
||||
buf, err = tlv8.Marshal(msgM5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M5. Encrypt payload
|
||||
sessionKey, err := hkdf.Sha512(
|
||||
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
|
||||
[]byte("Pair-Setup-Encrypt-Info"),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
|
||||
sessionKey[:], []byte("PS-Msg05"), buf, nil,
|
||||
)
|
||||
|
||||
// STEP M5. Generate request
|
||||
reqM5 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
EncryptedData: append(buf, mac[:]...),
|
||||
State: hap.M5,
|
||||
}
|
||||
buf, err = tlv8.Marshal(reqM5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M5. Send request
|
||||
res, err = c.Post(UriPairSetup, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M6. Read response
|
||||
resM6 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
|
||||
return
|
||||
}
|
||||
if resM6.State != 6 || resM6.Error > 0 {
|
||||
return fmt.Errorf("wrong M6: %+v", resM2)
|
||||
}
|
||||
|
||||
// STEP M6. Decrypt payload
|
||||
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
|
||||
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
|
||||
|
||||
buf, err = chacha20poly1305.DecryptAndVerify(
|
||||
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msgM6 := struct {
|
||||
Identifier []byte `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
}{}
|
||||
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M6. Verify payload
|
||||
if saltKey, err = hkdf.Sha512(
|
||||
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
|
||||
[]byte("Pair-Setup-Accessory-Sign-Info"),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf = nil
|
||||
buf = append(buf, saltKey[:]...)
|
||||
buf = append(buf, msgM6.Identifier...)
|
||||
buf = append(buf, msgM6.PublicKey...)
|
||||
|
||||
if !ed25519.ValidateSignature(
|
||||
msgM6.PublicKey[:], buf, msgM6.Signature,
|
||||
) {
|
||||
return errors.New("wrong server signature")
|
||||
}
|
||||
|
||||
if c.DeviceID != string(msgM6.Identifier) {
|
||||
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
|
||||
}
|
||||
|
||||
c.DevicePublic = msgM6.PublicKey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
conn := c.conn
|
||||
c.conn = nil
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
func (c *Conn) GetAccessories() ([]*Accessory, error) {
|
||||
res, err := c.Get("/accessories")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := Accessories{}
|
||||
if err = json.Unmarshal(data, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, accs := range p.Accessories {
|
||||
for _, serv := range accs.Services {
|
||||
for _, char := range serv.Characters {
|
||||
char.AID = accs.AID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p.Accessories, nil
|
||||
}
|
||||
|
||||
func (c *Conn) GetCharacters(query string) ([]*Character, error) {
|
||||
res, err := c.Get("/characteristics?id=" + query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := Characters{}
|
||||
if err = json.Unmarshal(data, &ch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ch.Characters, nil
|
||||
}
|
||||
|
||||
func (c *Conn) GetCharacter(char *Character) error {
|
||||
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
|
||||
chars, err := c.GetCharacters(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
char.Value = chars[0].Value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PutCharacters(characters ...*Character) (err error) {
|
||||
for i, char := range characters {
|
||||
if char.Event != nil {
|
||||
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
|
||||
} else {
|
||||
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
|
||||
}
|
||||
characters[i] = char
|
||||
}
|
||||
var data []byte
|
||||
if data, err = json.Marshal(Characters{characters}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res *http.Response
|
||||
if res, err = c.Put("/characteristics", data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
return errors.New("wrong response status")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) GetImage(width, height int) ([]byte, error) {
|
||||
res, err := c.Post(
|
||||
"/resource", []byte(fmt.Sprintf(
|
||||
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
|
||||
width, height,
|
||||
)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
//func (c *Client) onEventData(r io.Reader) error {
|
||||
// if c.OnEvent == nil {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// data, err := io.ReadAll(r)
|
||||
//
|
||||
// ch := Characters{}
|
||||
// if err = json.Unmarshal(data, &ch); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// c.OnEvent(ch.Characters)
|
||||
//
|
||||
// return nil
|
||||
//}
|
||||
|
||||
func (c *Conn) ListPairings() error {
|
||||
pReq := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
Method: hap.MethodListPairings,
|
||||
State: hap.M1,
|
||||
}
|
||||
data, err := tlv8.Marshal(pReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Post("/pairings", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = io.ReadAll(res.Body)
|
||||
// TODO: don't know how to fix array of items
|
||||
var pRes struct {
|
||||
State byte `tlv8:"6"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
|
||||
pReq := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}{
|
||||
Method: hap.MethodAddPairing,
|
||||
Identifier: clientID,
|
||||
PublicKey: clientPublic,
|
||||
State: hap.M1,
|
||||
Permission: hap.PermissionUser,
|
||||
}
|
||||
if admin {
|
||||
pReq.Permission = hap.PermissionAdmin
|
||||
}
|
||||
|
||||
data, err := tlv8.Marshal(pReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Post("/pairings", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = io.ReadAll(res.Body)
|
||||
var pRes struct {
|
||||
State byte `tlv8:"6"`
|
||||
Unknown byte `tlv8:"7"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) DeletePairing(id string) error {
|
||||
reqM1 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
}{
|
||||
State: hap.M1,
|
||||
Method: hap.MethodDeletePairing,
|
||||
Identifier: id,
|
||||
}
|
||||
data, err := tlv8.Marshal(reqM1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Post("/pairings", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = io.ReadAll(res.Body)
|
||||
var resM2 struct {
|
||||
State byte `tlv8:"6"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(data, &resM2); err != nil {
|
||||
return err
|
||||
}
|
||||
if resM2.State != hap.M2 {
|
||||
return errors.New("wrong state")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() string {
|
||||
return c.conn.LocalAddr().String()
|
||||
}
|
||||
|
||||
func DecodeKey(s string) []byte {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
data, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package homekit
|
||||
package hap
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -29,13 +29,13 @@ func GenerateUUID() string {
|
||||
}
|
||||
|
||||
type PairVerifyPayload struct {
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
Status byte `tlv8:"7"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
Method byte `tlv8:"0,optional"`
|
||||
Identifier string `tlv8:"1,optional"`
|
||||
PublicKey []byte `tlv8:"3,optional"`
|
||||
EncryptedData []byte `tlv8:"5,optional"`
|
||||
State byte `tlv8:"6,optional"`
|
||||
Status byte `tlv8:"7,optional"`
|
||||
Signature []byte `tlv8:"10,optional"`
|
||||
}
|
||||
|
||||
//func (c *Character) Unmarshal(value interface{}) error {
|
@@ -1,4 +1,4 @@
|
||||
package homekit
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
UriResource = "/resource"
|
||||
)
|
||||
|
||||
func (c *Client) Write(p []byte) (r io.Reader, err error) {
|
||||
func (c *Conn) Write(p []byte) (r io.Reader, err error) {
|
||||
if c.secure == nil {
|
||||
if _, err = c.conn.Write(p); err == nil {
|
||||
r = bufio.NewReader(c.conn)
|
||||
@@ -36,7 +36,7 @@ func (c *Client) Write(p []byte) (r io.Reader, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
func (c *Conn) Do(req *http.Request) (*http.Response, error) {
|
||||
if c.secure == nil {
|
||||
// insecure requests
|
||||
if err := req.Write(c.conn); err != nil {
|
||||
@@ -56,7 +56,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
return http.ReadResponse(buf, req)
|
||||
}
|
||||
|
||||
func (c *Client) Get(uri string) (*http.Response, error) {
|
||||
func (c *Conn) Get(uri string) (*http.Response, error) {
|
||||
req, err := http.NewRequest(
|
||||
"GET", "http://"+c.DeviceAddress+uri, nil,
|
||||
)
|
||||
@@ -66,7 +66,7 @@ func (c *Client) Get(uri string) (*http.Response, error) {
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
|
||||
func (c *Conn) Post(uri string, data []byte) (*http.Response, error) {
|
||||
req, err := http.NewRequest(
|
||||
"POST", "http://"+c.DeviceAddress+uri,
|
||||
bytes.NewReader(data),
|
||||
@@ -85,7 +85,7 @@ func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
|
||||
func (c *Conn) Put(uri string, data []byte) (*http.Response, error) {
|
||||
req, err := http.NewRequest(
|
||||
"PUT", "http://"+c.DeviceAddress+uri,
|
||||
bytes.NewReader(data),
|
||||
@@ -102,7 +102,7 @@ func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) Handle() (err error) {
|
||||
func (c *Conn) Handle() (err error) {
|
||||
defer func() {
|
||||
if c.conn == nil {
|
||||
err = nil
|
@@ -1,4 +1,4 @@
|
||||
package homekit
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
@@ -1,4 +1,4 @@
|
||||
package homekit
|
||||
package hap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
@@ -1,4 +1,4 @@
|
||||
package homekit
|
||||
package hap
|
||||
|
||||
import (
|
||||
"bufio"
|
@@ -1,103 +0,0 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"github.com/brutella/hap/rtp"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Offer *rtp.SetupEndpoints
|
||||
Answer *rtp.SetupEndpointsResponse
|
||||
Config *rtp.StreamConfiguration
|
||||
}
|
||||
|
||||
func NewSession() *Session {
|
||||
sessionID := RandomBytes(16)
|
||||
s := &Session{
|
||||
Offer: &rtp.SetupEndpoints{
|
||||
SessionId: sessionID,
|
||||
Video: rtp.CryptoSuite{
|
||||
MasterKey: RandomBytes(16),
|
||||
MasterSalt: RandomBytes(14),
|
||||
},
|
||||
Audio: rtp.CryptoSuite{
|
||||
MasterKey: RandomBytes(16),
|
||||
MasterSalt: RandomBytes(14),
|
||||
},
|
||||
},
|
||||
Config: &rtp.StreamConfiguration{
|
||||
Command: rtp.SessionControlCommand{
|
||||
Identifier: sessionID,
|
||||
Type: rtp.SessionControlCommandTypeStart,
|
||||
},
|
||||
Video: rtp.VideoParameters{
|
||||
CodecType: rtp.VideoCodecType_H264,
|
||||
CodecParams: rtp.VideoCodecParameters{
|
||||
Profiles: []rtp.VideoCodecProfile{
|
||||
{Id: rtp.VideoCodecProfileMain},
|
||||
},
|
||||
Levels: []rtp.VideoCodecLevel{
|
||||
{Level: rtp.VideoCodecLevel4},
|
||||
},
|
||||
Packetizations: []rtp.VideoCodecPacketization{
|
||||
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
|
||||
},
|
||||
},
|
||||
Attributes: rtp.VideoCodecAttributes{
|
||||
Width: 1920, Height: 1080, Framerate: 30,
|
||||
},
|
||||
RTP: rtp.RTPParams{
|
||||
PayloadType: 99,
|
||||
Ssrc: RandomUint32(),
|
||||
Bitrate: 299,
|
||||
Interval: 0.5,
|
||||
ComfortNoisePayloadType: 98,
|
||||
MTU: 0,
|
||||
},
|
||||
},
|
||||
Audio: rtp.AudioParameters{
|
||||
CodecType: rtp.AudioCodecType_AAC_ELD,
|
||||
CodecParams: rtp.AudioCodecParameters{
|
||||
Channels: 1,
|
||||
Bitrate: rtp.AudioCodecBitrateVariable,
|
||||
Samplerate: rtp.AudioCodecSampleRate16Khz,
|
||||
PacketTime: 30,
|
||||
},
|
||||
RTP: rtp.RTPParams{
|
||||
PayloadType: 110,
|
||||
Ssrc: RandomUint32(),
|
||||
Bitrate: 24,
|
||||
Interval: 5,
|
||||
MTU: 13,
|
||||
},
|
||||
ComfortNoise: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Session) SetLocalEndpoint(host string, port uint16) {
|
||||
s.Offer.ControllerAddr = rtp.Addr{
|
||||
IPAddr: host,
|
||||
VideoRtpPort: port,
|
||||
AudioRtpPort: port,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) SetVideo() {
|
||||
|
||||
}
|
||||
|
||||
func RandomBytes(size int) []byte {
|
||||
data := make([]byte, size)
|
||||
_, _ = cryptorand.Read(data)
|
||||
return data
|
||||
}
|
||||
|
||||
func RandomUint32() uint32 {
|
||||
data := make([]byte, 4)
|
||||
_, _ = cryptorand.Read(data)
|
||||
return binary.BigEndian.Uint32(data)
|
||||
}
|
@@ -1,732 +1,265 @@
|
||||
package homekit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/brutella/hap"
|
||||
"github.com/brutella/hap/chacha20poly1305"
|
||||
"github.com/brutella/hap/curve25519"
|
||||
"github.com/brutella/hap/ed25519"
|
||||
"github.com/brutella/hap/hkdf"
|
||||
"github.com/brutella/hap/tlv8"
|
||||
"github.com/tadglines/go-pkgs/crypto/srp"
|
||||
"io"
|
||||
"github.com/brutella/hap/characteristic"
|
||||
"github.com/brutella/hap/rtp"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Client for HomeKit. DevicePublic can be null.
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
|
||||
DeviceAddress string // including port
|
||||
DeviceID string
|
||||
DevicePublic []byte
|
||||
ClientID string
|
||||
ClientPrivate []byte
|
||||
conn *hap.Conn
|
||||
exit chan error
|
||||
server *srtp.Server
|
||||
url string
|
||||
|
||||
OnEvent func(res *http.Response)
|
||||
Output func(msg interface{})
|
||||
medias []*streamer.Media
|
||||
tracks []*streamer.Track
|
||||
|
||||
conn net.Conn
|
||||
secure *Secure
|
||||
httpResponse chan *bufio.Reader
|
||||
sessions []*srtp.Session
|
||||
}
|
||||
|
||||
func NewClient(rawURL string) (*Client, error) {
|
||||
func NewClient(rawURL string, server *srtp.Server) (*Client, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
c := &Client{
|
||||
c := &hap.Conn{
|
||||
DeviceAddress: u.Host,
|
||||
DeviceID: query.Get("device_id"),
|
||||
DevicePublic: DecodeKey(query.Get("device_public")),
|
||||
DevicePublic: hap.DecodeKey(query.Get("device_public")),
|
||||
ClientID: query.Get("client_id"),
|
||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||
ClientPrivate: hap.DecodeKey(query.Get("client_private")),
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Pair(deviceID, pin string) (*Client, error) {
|
||||
entry := mdns.GetEntry(deviceID)
|
||||
if entry == nil {
|
||||
return nil, errors.New("can't find device via mDNS")
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
|
||||
DeviceID: deviceID,
|
||||
ClientID: GenerateUUID(),
|
||||
ClientPrivate: GenerateKey(),
|
||||
}
|
||||
|
||||
var mfi bool
|
||||
for _, field := range entry.InfoFields {
|
||||
if field[:2] == "ff" {
|
||||
if field[3] == '1' {
|
||||
mfi = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return c, c.Pair(mfi, pin)
|
||||
}
|
||||
|
||||
func (c *Client) ClientPublic() []byte {
|
||||
return c.ClientPrivate[32:]
|
||||
}
|
||||
|
||||
func (c *Client) URL() string {
|
||||
return fmt.Sprintf(
|
||||
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
|
||||
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) DialAndServe() error {
|
||||
if err := c.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Handle()
|
||||
return &Client{conn: c, server: server}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Dial() error {
|
||||
// update device host before dial
|
||||
if host := mdns.GetAddress(c.DeviceID); host != "" {
|
||||
c.DeviceAddress = host
|
||||
}
|
||||
|
||||
var err error
|
||||
c.conn, err = net.Dial("tcp", c.DeviceAddress)
|
||||
if err != nil {
|
||||
if err := c.conn.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M1: send our session public to device
|
||||
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
||||
c.exit = make(chan error)
|
||||
|
||||
// 1. generate payload
|
||||
// important not include other fields
|
||||
requestM1 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
}{
|
||||
State: hap.M1,
|
||||
PublicKey: sessionPublic[:],
|
||||
}
|
||||
// 2. pack payload to TLV8
|
||||
buf, err := tlv8.Marshal(requestM1)
|
||||
if err != nil {
|
||||
return err
|
||||
go func() {
|
||||
//start goroutine for reading responses from camera
|
||||
c.exit <- c.conn.Handle()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
if c.medias == nil {
|
||||
c.medias = c.getMedias()
|
||||
}
|
||||
|
||||
// 3. send request
|
||||
resp, err := c.Post(UriPairVerify, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.medias
|
||||
}
|
||||
|
||||
// STEP M2: unpack deviceID from response
|
||||
responseM2 := PairVerifyPayload{}
|
||||
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. generate session shared key
|
||||
var deviceSessionPublic [32]byte
|
||||
copy(deviceSessionPublic[:], responseM2.PublicKey)
|
||||
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
|
||||
sessionKey, err := hkdf.Sha512(
|
||||
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
|
||||
[]byte("Pair-Verify-Encrypt-Info"),
|
||||
)
|
||||
|
||||
// 2. decrypt M2 response with session key
|
||||
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
|
||||
var mac [16]byte
|
||||
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
|
||||
|
||||
buf, err = chacha20poly1305.DecryptAndVerify(
|
||||
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
|
||||
)
|
||||
|
||||
// 3. unpack payload from TLV8
|
||||
payloadM2 := PairVerifyPayload{}
|
||||
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. verify signature for M2 response with device public
|
||||
// device session + device id + our session
|
||||
if c.DevicePublic != nil {
|
||||
buf = nil
|
||||
buf = append(buf, responseM2.PublicKey[:]...)
|
||||
buf = append(buf, []byte(payloadM2.Identifier)...)
|
||||
buf = append(buf, sessionPublic[:]...)
|
||||
if !ed25519.ValidateSignature(
|
||||
c.DevicePublic[:], buf, payloadM2.Signature,
|
||||
) {
|
||||
return errors.New("device public signature invalid")
|
||||
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
for _, track := range c.tracks {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
// STEP M3: send our clientID to device
|
||||
// 1. generate signature with our private key
|
||||
// (our session + our ID + device session)
|
||||
buf = nil
|
||||
buf = append(buf, sessionPublic[:]...)
|
||||
buf = append(buf, []byte(c.ClientID)...)
|
||||
buf = append(buf, responseM2.PublicKey[:]...)
|
||||
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
|
||||
track := streamer.NewTrack(codec, media.Direction)
|
||||
c.tracks = append(c.tracks, track)
|
||||
return track
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
if c.tracks == nil {
|
||||
return errors.New("producer without tracks")
|
||||
}
|
||||
|
||||
// get our server local IP-address
|
||||
host, _, err := net.SplitHostPort(c.conn.LocalAddr())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. generate payload
|
||||
payloadM3 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
Signature: signature,
|
||||
// TODO: set right config
|
||||
vp := &rtp.VideoParameters{
|
||||
CodecType: rtp.VideoCodecType_H264,
|
||||
CodecParams: rtp.VideoCodecParameters{
|
||||
Profiles: []rtp.VideoCodecProfile{
|
||||
{Id: rtp.VideoCodecProfileMain},
|
||||
},
|
||||
Levels: []rtp.VideoCodecLevel{
|
||||
{Level: rtp.VideoCodecLevel4},
|
||||
},
|
||||
Packetizations: []rtp.VideoCodecPacketization{
|
||||
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
|
||||
},
|
||||
},
|
||||
Attributes: rtp.VideoCodecAttributes{
|
||||
Width: 1920, Height: 1080, Framerate: 30,
|
||||
},
|
||||
}
|
||||
// 3. pack payload to TLV8
|
||||
buf, err = tlv8.Marshal(payloadM3)
|
||||
if err != nil {
|
||||
|
||||
ap := &rtp.AudioParameters{
|
||||
CodecType: rtp.AudioCodecType_AAC_ELD,
|
||||
CodecParams: rtp.AudioCodecParameters{
|
||||
Channels: 1,
|
||||
Bitrate: rtp.AudioCodecBitrateVariable,
|
||||
Samplerate: rtp.AudioCodecSampleRate16Khz,
|
||||
// packet time=20 => AAC-ELD packet size=480
|
||||
// packet time=30 => AAC-ELD packet size=480
|
||||
// packet time=40 => AAC-ELD packet size=480
|
||||
// packet time=60 => AAC-LD packet size=960
|
||||
PacketTime: 40,
|
||||
},
|
||||
}
|
||||
|
||||
// setup HomeKit stream session
|
||||
hkSession := camera.NewSession(vp, ap)
|
||||
hkSession.SetLocalEndpoint(host, c.server.Port())
|
||||
|
||||
// create client for processing camera accessory
|
||||
cam := camera.NewClient(c.conn)
|
||||
// try to start HomeKit stream
|
||||
if err = cam.StartStream(hkSession); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. encrypt payload with session key
|
||||
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
|
||||
sessionKey[:], []byte("PV-Msg03"), buf, nil,
|
||||
)
|
||||
|
||||
// 4. generate request
|
||||
requestM3 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: hap.M3,
|
||||
EncryptedData: append(msg, mac[:]...),
|
||||
// SRTP Video Session
|
||||
vs := &srtp.Session{
|
||||
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
|
||||
RemoteSSRC: hkSession.Answer.SsrcVideo,
|
||||
}
|
||||
// 5. pack payload to TLV8
|
||||
buf, err = tlv8.Marshal(requestM3)
|
||||
if err != nil {
|
||||
if err = vs.SetKeys(
|
||||
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
|
||||
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err = c.Post(UriPairVerify, buf)
|
||||
if err != nil {
|
||||
// SRTP Audio Session
|
||||
as := &srtp.Session{
|
||||
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
|
||||
RemoteSSRC: hkSession.Answer.SsrcAudio,
|
||||
}
|
||||
if err = as.SetKeys(
|
||||
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
|
||||
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M4. Read response
|
||||
responseM4 := PairVerifyPayload{}
|
||||
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
|
||||
return err
|
||||
for _, track := range c.tracks {
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
vs.Track = track
|
||||
case streamer.CodecELD:
|
||||
as.Track = track
|
||||
}
|
||||
}
|
||||
|
||||
// 1. check response state
|
||||
if responseM4.State != 4 || responseM4.Status != 0 {
|
||||
return fmt.Errorf("wrong M4 response: %+v", responseM4)
|
||||
c.server.AddSession(vs)
|
||||
c.server.AddSession(as)
|
||||
|
||||
c.sessions = []*srtp.Session{vs, as}
|
||||
|
||||
return <-c.exit
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
err := c.conn.Close()
|
||||
|
||||
for _, session := range c.sessions {
|
||||
c.server.RemoveSession(session)
|
||||
}
|
||||
|
||||
c.secure, err = NewSecure(sessionShared, false)
|
||||
//c.secure.Buffer = bytes.NewBuffer(nil)
|
||||
c.secure.Conn = c.conn
|
||||
|
||||
c.httpResponse = make(chan *bufio.Reader, 10)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
|
||||
func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
pin = strings.ReplaceAll(pin, "-", "")
|
||||
if len(pin) != 8 {
|
||||
return fmt.Errorf("wrong PIN format: %s", pin)
|
||||
}
|
||||
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:]
|
||||
func (c *Client) getMedias() []*streamer.Media {
|
||||
var medias []*streamer.Media
|
||||
|
||||
c.conn, err = net.Dial("tcp", c.DeviceAddress)
|
||||
accs, err := c.conn.GetAccessories()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M1. Generate request
|
||||
reqM1 := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: hap.M1,
|
||||
}
|
||||
if mfi {
|
||||
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
|
||||
}
|
||||
buf, err := tlv8.Marshal(reqM1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M1. Send request
|
||||
res, err := c.Post(UriPairSetup, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M2. Read response
|
||||
resM2 := struct {
|
||||
Salt []byte `tlv8:"2"`
|
||||
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
|
||||
return
|
||||
}
|
||||
if resM2.State != 2 || resM2.Error > 0 {
|
||||
return fmt.Errorf("wrong M2: %+v", resM2)
|
||||
}
|
||||
|
||||
// STEP M3. Generate session using pin
|
||||
username := []byte("Pair-Setup")
|
||||
|
||||
SRP, err := srp.NewSRP(
|
||||
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
SRP.SaltLength = 16
|
||||
|
||||
// username: "Pair-Setup"
|
||||
// password: PIN (with dashes)
|
||||
session := SRP.NewClientSession(username, []byte(pin))
|
||||
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M3. Generate request
|
||||
reqM3 := struct {
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Proof []byte `tlv8:"4"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
PublicKey: session.GetA(), // client public key, aka session.A
|
||||
Proof: session.ComputeAuthenticator(),
|
||||
State: hap.M3,
|
||||
}
|
||||
buf, err = tlv8.Marshal(reqM3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M3. Send request
|
||||
res, err = c.Post(UriPairSetup, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M4. Read response
|
||||
resM4 := struct {
|
||||
Proof []byte `tlv8:"4"` // server proof
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
|
||||
return
|
||||
}
|
||||
if resM4.Error == 2 {
|
||||
return fmt.Errorf("wrong PIN: %s", pin)
|
||||
}
|
||||
if resM4.State != 4 || resM4.Error > 0 {
|
||||
return fmt.Errorf("wrong M4: %+v", resM4)
|
||||
}
|
||||
|
||||
// STEP M4. Verify response
|
||||
if !session.VerifyServerAuthenticator(resM4.Proof) {
|
||||
return errors.New("verify server auth fail")
|
||||
}
|
||||
|
||||
// STEP M5. Generate signature
|
||||
saltKey, err := hkdf.Sha512(
|
||||
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
|
||||
[]byte("Pair-Setup-Controller-Sign-Info"),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf = nil
|
||||
buf = append(buf, saltKey[:]...)
|
||||
buf = append(buf, []byte(c.ClientID)...)
|
||||
buf = append(buf, c.ClientPublic()...)
|
||||
|
||||
signature, err := ed25519.Signature(c.ClientPrivate, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M5. Generate payload
|
||||
msgM5 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
PublicKey: c.ClientPublic(),
|
||||
Signature: signature,
|
||||
}
|
||||
buf, err = tlv8.Marshal(msgM5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M5. Encrypt payload
|
||||
sessionKey, err := hkdf.Sha512(
|
||||
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
|
||||
[]byte("Pair-Setup-Encrypt-Info"),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
|
||||
sessionKey[:], []byte("PS-Msg05"), buf, nil,
|
||||
)
|
||||
|
||||
// STEP M5. Generate request
|
||||
reqM5 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
EncryptedData: append(buf, mac[:]...),
|
||||
State: hap.M5,
|
||||
}
|
||||
buf, err = tlv8.Marshal(reqM5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// STEP M5. Send request
|
||||
res, err = c.Post(UriPairSetup, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M6. Read response
|
||||
resM6 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
|
||||
return
|
||||
}
|
||||
if resM6.State != 6 || resM6.Error > 0 {
|
||||
return fmt.Errorf("wrong M6: %+v", resM2)
|
||||
}
|
||||
|
||||
// STEP M6. Decrypt payload
|
||||
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
|
||||
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
|
||||
|
||||
buf, err = chacha20poly1305.DecryptAndVerify(
|
||||
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msgM6 := struct {
|
||||
Identifier []byte `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
}{}
|
||||
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M6. Verify payload
|
||||
if saltKey, err = hkdf.Sha512(
|
||||
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
|
||||
[]byte("Pair-Setup-Accessory-Sign-Info"),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf = nil
|
||||
buf = append(buf, saltKey[:]...)
|
||||
buf = append(buf, msgM6.Identifier...)
|
||||
buf = append(buf, msgM6.PublicKey...)
|
||||
|
||||
if !ed25519.ValidateSignature(
|
||||
msgM6.PublicKey[:], buf, msgM6.Signature,
|
||||
) {
|
||||
return errors.New("wrong server signature")
|
||||
}
|
||||
|
||||
if c.DeviceID != string(msgM6.Identifier) {
|
||||
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
|
||||
}
|
||||
|
||||
c.DevicePublic = msgM6.PublicKey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
conn := c.conn
|
||||
c.conn = nil
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessories() ([]*Accessory, error) {
|
||||
res, err := c.Get("/accessories")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
acc := accs[0]
|
||||
|
||||
// get supported video config (not really necessary)
|
||||
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
|
||||
v1 := &rtp.VideoStreamConfiguration{}
|
||||
if err = char.ReadTLV8(v1); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, hkCodec := range v1.Codecs {
|
||||
codec := &streamer.Codec{ClockRate: 90000}
|
||||
|
||||
p := Accessories{}
|
||||
if err = json.Unmarshal(data, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, accs := range p.Accessories {
|
||||
for _, serv := range accs.Services {
|
||||
for _, char := range serv.Characters {
|
||||
char.AID = accs.AID
|
||||
}
|
||||
switch hkCodec.Type {
|
||||
case rtp.VideoCodecType_H264:
|
||||
codec.Name = streamer.CodecH264
|
||||
codec.FmtpLine = "profile-level-id=420029"
|
||||
default:
|
||||
fmt.Printf("unknown codec: %d", hkCodec.Type)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return p.Accessories, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCharacters(query string) ([]*Character, error) {
|
||||
res, err := c.Get("/characteristics?id=" + query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := Characters{}
|
||||
if err = json.Unmarshal(data, &ch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ch.Characters, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCharacter(char *Character) error {
|
||||
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
|
||||
chars, err := c.GetCharacters(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
char.Value = chars[0].Value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PutCharacters(characters ...*Character) (err error) {
|
||||
for i, char := range characters {
|
||||
if char.Event != nil {
|
||||
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
|
||||
} else {
|
||||
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
}
|
||||
characters[i] = char
|
||||
}
|
||||
var data []byte
|
||||
if data, err = json.Marshal(Characters{characters}); err != nil {
|
||||
return
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
var res *http.Response
|
||||
if res, err = c.Put("/characteristics", data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
return errors.New("wrong response status")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetImage(width, height int) ([]byte, error) {
|
||||
res, err := c.Post(
|
||||
"/resource", []byte(fmt.Sprintf(
|
||||
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
|
||||
width, height,
|
||||
)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
//func (c *Client) onEventData(r io.Reader) error {
|
||||
// if c.OnEvent == nil {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// data, err := io.ReadAll(r)
|
||||
//
|
||||
// ch := Characters{}
|
||||
// if err = json.Unmarshal(data, &ch); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// c.OnEvent(ch.Characters)
|
||||
//
|
||||
// return nil
|
||||
//}
|
||||
|
||||
func (c *Client) ListPairings() error {
|
||||
pReq := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
Method: hap.MethodListPairings,
|
||||
State: hap.M1,
|
||||
}
|
||||
data, err := tlv8.Marshal(pReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Post("/pairings", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = io.ReadAll(res.Body)
|
||||
// TODO: don't know how to fix array of items
|
||||
var pRes struct {
|
||||
State byte `tlv8:"6"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
|
||||
pReq := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}{
|
||||
Method: hap.MethodAddPairing,
|
||||
Identifier: clientID,
|
||||
PublicKey: clientPublic,
|
||||
State: hap.M1,
|
||||
Permission: hap.PermissionUser,
|
||||
}
|
||||
if admin {
|
||||
pReq.Permission = hap.PermissionAdmin
|
||||
}
|
||||
|
||||
data, err := tlv8.Marshal(pReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Post("/pairings", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = io.ReadAll(res.Body)
|
||||
var pRes struct {
|
||||
State byte `tlv8:"6"`
|
||||
Unknown byte `tlv8:"7"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) DeletePairing(id string) error {
|
||||
reqM1 := struct {
|
||||
State byte `tlv8:"6"`
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
}{
|
||||
State: hap.M1,
|
||||
Method: hap.MethodDeletePairing,
|
||||
Identifier: id,
|
||||
}
|
||||
data, err := tlv8.Marshal(reqM1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.Post("/pairings", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err = io.ReadAll(res.Body)
|
||||
var resM2 struct {
|
||||
State byte `tlv8:"6"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(data, &resM2); err != nil {
|
||||
return err
|
||||
}
|
||||
if resM2.State != hap.M2 {
|
||||
return errors.New("wrong state")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) LocalAddr() string {
|
||||
return c.conn.LocalAddr().String()
|
||||
}
|
||||
|
||||
func DecodeKey(s string) []byte {
|
||||
if s == "" {
|
||||
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
|
||||
v2 := &rtp.AudioStreamConfiguration{}
|
||||
if err = char.ReadTLV8(v2); err != nil {
|
||||
return nil
|
||||
}
|
||||
data, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
|
||||
for _, hkCodec := range v2.Codecs {
|
||||
codec := &streamer.Codec{
|
||||
Channels: uint16(hkCodec.Parameters.Channels),
|
||||
}
|
||||
|
||||
switch hkCodec.Parameters.Samplerate {
|
||||
case rtp.AudioCodecSampleRate8Khz:
|
||||
codec.ClockRate = 8000
|
||||
case rtp.AudioCodecSampleRate16Khz:
|
||||
codec.ClockRate = 16000
|
||||
case rtp.AudioCodecSampleRate24Khz:
|
||||
codec.ClockRate = 24000
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
|
||||
}
|
||||
|
||||
switch hkCodec.Type {
|
||||
case rtp.AudioCodecType_AAC_ELD:
|
||||
codec.Name = streamer.CodecELD
|
||||
// only this value supported by FFmpeg
|
||||
codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
|
||||
default:
|
||||
fmt.Printf("unknown codec: %d", hkCodec.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
return data
|
||||
|
||||
return medias
|
||||
}
|
||||
|
3
pkg/httpflv/README.md
Normal file
3
pkg/httpflv/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## Useful links
|
||||
|
||||
- https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
|
104
pkg/httpflv/httpflv.go
Normal file
104
pkg/httpflv/httpflv.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package httpflv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/flv/flvio"
|
||||
"github.com/deepch/vdk/utils/bits/pio"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Dial(uri string) (*Conn, error) {
|
||||
req, err := http.NewRequest("GET", uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Accept(res)
|
||||
}
|
||||
|
||||
func Accept(res *http.Response) (*Conn, error) {
|
||||
c := Conn{
|
||||
conn: res.Body,
|
||||
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
||||
buf: make([]byte, 256),
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flags, n, err := flvio.ParseFileHeader(c.buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if flags&flvio.FILE_HAS_VIDEO == 0 {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
|
||||
if _, err = c.reader.Discard(n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
conn io.ReadCloser
|
||||
reader *bufio.Reader
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (c *Conn) Streams() ([]av.CodecData, error) {
|
||||
for {
|
||||
tag, _, err := flvio.ReadTag(c.reader, c.buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
|
||||
continue
|
||||
}
|
||||
|
||||
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []av.CodecData{stream}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) ReadPacket() (av.Packet, error) {
|
||||
for {
|
||||
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
|
||||
if err != nil {
|
||||
return av.Packet{}, err
|
||||
}
|
||||
|
||||
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
|
||||
continue
|
||||
}
|
||||
|
||||
return av.Packet{
|
||||
Idx: 0,
|
||||
Data: tag.Data,
|
||||
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
||||
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
||||
Time: flvio.TsToTime(ts),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Close() (err error) {
|
||||
return c.conn.Close()
|
||||
}
|
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||
@@ -128,7 +127,9 @@ func (c *Client) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
close(c.buffer)
|
||||
if c.buffer != nil {
|
||||
close(c.buffer)
|
||||
}
|
||||
c.closed = true
|
||||
return c.conn.Close()
|
||||
}
|
||||
@@ -162,9 +163,12 @@ func (c *Client) getTracks() error {
|
||||
continue
|
||||
}
|
||||
|
||||
codec := streamer.NewCodec(streamer.CodecH264)
|
||||
codec.FmtpLine = "profile-level-id=" + msg.CodecString[i+1:]
|
||||
codec.PayloadType = h264.PayloadTypeAVC
|
||||
codec := &streamer.Codec{
|
||||
Name: streamer.CodecH264,
|
||||
ClockRate: 90000,
|
||||
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
|
||||
PayloadType: streamer.PayloadTypeRAW,
|
||||
}
|
||||
|
||||
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
||||
if i < 0 {
|
||||
@@ -190,10 +194,7 @@ func (c *Client) getTracks() error {
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
track := &streamer.Track{
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codec: codec,
|
||||
}
|
||||
track := streamer.NewTrack(codec, streamer.DirectionSendonly)
|
||||
c.tracks[msg.TrackID] = track
|
||||
|
||||
case "mp4a": // mp4a.40.2
|
||||
@@ -245,14 +246,12 @@ func (c *Client) worker() {
|
||||
time.Sleep(d)
|
||||
|
||||
// can be SPS, PPS and IFrame in one packet
|
||||
for _, payload := range h264.SplitAVC(data[:entry.Size]) {
|
||||
packet := &rtp.Packet{
|
||||
// ivideon clockrate=1000, RTP clockrate=90000
|
||||
Header: rtp.Header{Timestamp: ts * 90},
|
||||
Payload: payload,
|
||||
}
|
||||
_ = track.WriteRTP(packet)
|
||||
packet := &rtp.Packet{
|
||||
// ivideon clockrate=1000, RTP clockrate=90000
|
||||
Header: rtp.Header{Timestamp: ts * 90},
|
||||
Payload: data[:entry.Size],
|
||||
}
|
||||
_ = track.WriteRTP(packet)
|
||||
|
||||
data = data[entry.Size:]
|
||||
ts += entry.Duration
|
||||
|
5
pkg/mjpeg/README.md
Normal file
5
pkg/mjpeg/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Useful links
|
||||
|
||||
- https://www.rfc-editor.org/rfc/rfc2435
|
||||
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
|
||||
- https://mjpeg.sanford.io/
|
152
pkg/mjpeg/client.go
Normal file
152
pkg/mjpeg/client.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
closed bool
|
||||
res *http.Response
|
||||
|
||||
track *streamer.Track
|
||||
}
|
||||
|
||||
func NewClient(res *http.Response) *Client {
|
||||
codec := &streamer.Codec{
|
||||
Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW,
|
||||
}
|
||||
return &Client{
|
||||
res: res,
|
||||
track: streamer.NewTrack(codec, streamer.DirectionSendonly),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
return []*streamer.Media{{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{c.track.Codec},
|
||||
}}
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
return c.track
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
ct := c.res.Header.Get("Content-Type")
|
||||
|
||||
if ct == "image/jpeg" {
|
||||
return c.startJPEG()
|
||||
}
|
||||
|
||||
// added in go1.18
|
||||
if _, s, ok := strings.Cut(ct, "boundary="); ok {
|
||||
return c.startMJPEG(s)
|
||||
}
|
||||
|
||||
return errors.New("wrong Content-Type: " + ct)
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
c.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) startJPEG() error {
|
||||
buf, err := io.ReadAll(c.res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
|
||||
req := c.res.Request
|
||||
|
||||
for !c.closed {
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New("wrong status: " + res.Status)
|
||||
}
|
||||
|
||||
buf, err = io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) startMJPEG(boundary string) error {
|
||||
boundary = "--" + boundary
|
||||
|
||||
r := bufio.NewReader(c.res.Body)
|
||||
tp := textproto.NewReader(r)
|
||||
|
||||
for !c.closed {
|
||||
s, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s != boundary {
|
||||
return errors.New("wrong boundary: " + s)
|
||||
}
|
||||
|
||||
header, err := tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s = header.Get("Content-Length")
|
||||
if s == "" {
|
||||
return errors.New("no content length")
|
||||
}
|
||||
|
||||
size, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(r, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
|
||||
if _, err = r.Discard(2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func now() uint32 {
|
||||
return uint32(time.Now().UnixMilli() * 90)
|
||||
}
|
40
pkg/mjpeg/consumer.go
Normal file
40
pkg/mjpeg/consumer.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
codecs []*streamer.Codec
|
||||
start bool
|
||||
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
return []*streamer.Media{{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{{Name: streamer.CodecJPEG}},
|
||||
}}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
push := func(packet *rtp.Packet) error {
|
||||
c.Fire(packet.Payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper := RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
182
pkg/mjpeg/rfc2435.go
Normal file
182
pkg/mjpeg/rfc2435.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package mjpeg
|
||||
|
||||
// RFC 2435. Appendix A
|
||||
|
||||
var jpeg_luma_quantizer = []byte{
|
||||
16, 11, 10, 16, 24, 40, 51, 61,
|
||||
12, 12, 14, 19, 26, 58, 60, 55,
|
||||
14, 13, 16, 24, 40, 57, 69, 56,
|
||||
14, 17, 22, 29, 51, 87, 80, 62,
|
||||
18, 22, 37, 56, 68, 109, 103, 77,
|
||||
24, 35, 55, 64, 81, 104, 113, 92,
|
||||
49, 64, 78, 87, 103, 121, 120, 101,
|
||||
72, 92, 95, 98, 112, 100, 103, 99,
|
||||
}
|
||||
var jpeg_chroma_quantizer = []byte{
|
||||
17, 18, 24, 47, 99, 99, 99, 99,
|
||||
18, 21, 26, 66, 99, 99, 99, 99,
|
||||
24, 26, 56, 99, 99, 99, 99, 99,
|
||||
47, 66, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
}
|
||||
|
||||
func MakeTables(q byte) (lqt, cqt []byte) {
|
||||
var factor int
|
||||
|
||||
switch {
|
||||
case q < 1:
|
||||
factor = 1
|
||||
case q > 99:
|
||||
factor = 99
|
||||
default:
|
||||
factor = int(q)
|
||||
}
|
||||
|
||||
if q < 50 {
|
||||
factor = 5000 / factor
|
||||
} else if q > 99 {
|
||||
factor = 200 - factor*2
|
||||
}
|
||||
|
||||
lqt = make([]byte, 64)
|
||||
cqt = make([]byte, 64)
|
||||
|
||||
for i := 0; i < 64; i++ {
|
||||
lq := (int(jpeg_luma_quantizer[i])*factor + 50) / 100
|
||||
cq := (int(jpeg_chroma_quantizer[i])*factor + 50) / 100
|
||||
|
||||
/* Limit the quantizers to 1 <= q <= 255 */
|
||||
switch {
|
||||
case lq < 1:
|
||||
lqt[i] = 1
|
||||
case lq > 255:
|
||||
lqt[i] = 255
|
||||
default:
|
||||
lqt[i] = byte(lq)
|
||||
}
|
||||
|
||||
switch {
|
||||
case cq < 1:
|
||||
cqt[i] = 1
|
||||
case cq > 255:
|
||||
cqt[i] = 255
|
||||
default:
|
||||
cqt[i] = byte(cq)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RFC 2435. Appendix B
|
||||
|
||||
var lum_dc_codelens = []byte{
|
||||
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
|
||||
}
|
||||
var lum_dc_symbols = []byte{
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||
}
|
||||
var lum_ac_codelens = []byte{
|
||||
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
|
||||
}
|
||||
var lum_ac_symbols = []byte{
|
||||
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
|
||||
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
|
||||
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
|
||||
0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
|
||||
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
|
||||
0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
|
||||
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
|
||||
0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
|
||||
0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
|
||||
0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
|
||||
0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
|
||||
0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
||||
0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
|
||||
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
|
||||
0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
|
||||
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
|
||||
0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
|
||||
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
|
||||
0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
|
||||
0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
||||
0xf9, 0xfa,
|
||||
}
|
||||
var chm_dc_codelens = []byte{
|
||||
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
|
||||
}
|
||||
var chm_dc_symbols = []byte{
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||
}
|
||||
var chm_ac_codelens = []byte{
|
||||
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
|
||||
}
|
||||
var chm_ac_symbols = []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
|
||||
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
|
||||
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
|
||||
0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
|
||||
0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
|
||||
0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
|
||||
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
|
||||
0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
|
||||
0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
|
||||
0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
|
||||
0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
|
||||
0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
|
||||
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
|
||||
0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
|
||||
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
|
||||
0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
|
||||
0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
|
||||
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
|
||||
0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
|
||||
0xf9, 0xfa,
|
||||
}
|
||||
|
||||
func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
|
||||
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
|
||||
p = append(p, 0xFF, 0xD8)
|
||||
|
||||
p = MakeQuantHeader(p, lqt, 0)
|
||||
p = MakeQuantHeader(p, cqt, 1)
|
||||
|
||||
if t == 0 {
|
||||
t = 0x21
|
||||
} else {
|
||||
t = 0x22
|
||||
}
|
||||
|
||||
p = append(p,
|
||||
0xFF, 0xC0, 0, 17, 8,
|
||||
byte(h>>8), byte(h&0xFF),
|
||||
byte(w>>8), byte(w&0xFF),
|
||||
3, 0, t, 0, 1, 0x11, 1, 2, 0x11, 1,
|
||||
)
|
||||
|
||||
p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
|
||||
p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
|
||||
p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
|
||||
p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
|
||||
|
||||
return append(p, 0xFF, 0xDA, 0, 12, 3, 0, 0, 1, 0x11, 2, 0x11, 0, 63, 0)
|
||||
}
|
||||
|
||||
func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {
|
||||
p = append(p, 0xFF, 0xDB, 0, 67, tableNo)
|
||||
return append(p, qt...)
|
||||
}
|
||||
|
||||
func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {
|
||||
p = append(p,
|
||||
0xFF, 0xC4, 0,
|
||||
byte(3+len(codelens)+len(symbols)),
|
||||
(tableClass<<4)|tableNo,
|
||||
)
|
||||
p = append(p, codelens...)
|
||||
return append(p, symbols...)
|
||||
}
|
212
pkg/mjpeg/rtp.go
Normal file
212
pkg/mjpeg/rtp.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
buf := make([]byte, 0, 512*1024) // 512K
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
|
||||
b := packet.Payload
|
||||
|
||||
// 3.1. JPEG header
|
||||
t := b[4]
|
||||
|
||||
// 3.1.7. Restart Marker header
|
||||
if 64 <= t && t <= 127 {
|
||||
b = b[12:] // skip it
|
||||
} else {
|
||||
b = b[8:]
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
var lqt, cqt []byte
|
||||
|
||||
// 3.1.8. Quantization Table header
|
||||
q := packet.Payload[5]
|
||||
if q >= 128 {
|
||||
lqt = b[4:68]
|
||||
cqt = b[68:132]
|
||||
b = b[132:]
|
||||
} else {
|
||||
lqt, cqt = MakeTables(q)
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
|
||||
// The maximum width is 2040 pixels.
|
||||
w := uint16(packet.Payload[6]) << 3
|
||||
h := uint16(packet.Payload[7]) << 3
|
||||
|
||||
// fix 2560x1920 and 2560x1440
|
||||
if w == 512 && (h == 1920 || h == 1440) {
|
||||
w = 2560
|
||||
}
|
||||
|
||||
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
|
||||
}
|
||||
|
||||
// 3.1.9. JPEG Payload
|
||||
buf = append(buf, b...)
|
||||
|
||||
if !packet.Marker {
|
||||
return nil
|
||||
}
|
||||
|
||||
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||
buf = append(buf, 0xFF, 0xD9)
|
||||
}
|
||||
|
||||
clone := *packet
|
||||
clone.Payload = buf
|
||||
|
||||
buf = buf[:0] // clear buffer
|
||||
|
||||
return push(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RTPPay() streamer.WrapperFunc {
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//func RTPPay() streamer.WrapperFunc {
|
||||
// const packetSize = 1436
|
||||
//
|
||||
// sequencer := rtp.NewRandomSequencer()
|
||||
//
|
||||
// return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
// return func(packet *rtp.Packet) error {
|
||||
// // reincode image to more common form
|
||||
// img, err := jpeg.Decode(bytes.NewReader(packet.Payload))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// wh := img.Bounds().Size()
|
||||
// w := wh.X
|
||||
// h := wh.Y
|
||||
//
|
||||
// if w > 2040 {
|
||||
// w = 2040
|
||||
// } else if w&3 > 0 {
|
||||
// w &= 3
|
||||
// }
|
||||
// if h > 2040 {
|
||||
// h = 2040
|
||||
// } else if h&3 > 0 {
|
||||
// h &= 3
|
||||
// }
|
||||
//
|
||||
// if w != wh.X || h != wh.Y {
|
||||
// x0 := (wh.X - w) / 2
|
||||
// y0 := (wh.Y - h) / 2
|
||||
// rect := image.Rect(x0, y0, x0+w, y0+h)
|
||||
// img = img.(*image.YCbCr).SubImage(rect)
|
||||
// }
|
||||
//
|
||||
// buf := bytes.NewBuffer(nil)
|
||||
// if err = jpeg.Encode(buf, img, nil); err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// h1 := make([]byte, 8)
|
||||
// h1[4] = 1 // Type
|
||||
// h1[5] = 255 // Q
|
||||
//
|
||||
// // MBZ=0, Precision=0, Length=128
|
||||
// h2 := make([]byte, 4, 132)
|
||||
// h2[3] = 128
|
||||
//
|
||||
// var jpgData []byte
|
||||
//
|
||||
// p := buf.Bytes()
|
||||
//
|
||||
// for jpgData == nil {
|
||||
// // 2 bytes h1
|
||||
// if p[0] != 0xFF {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// size := binary.BigEndian.Uint16(p[2:]) + 2
|
||||
//
|
||||
// // 2 bytes payload size (include 2 bytes)
|
||||
// switch p[1] {
|
||||
// case 0xD8: // 0. Start Of Image (size=0)
|
||||
// p = p[2:]
|
||||
// continue
|
||||
// case 0xDB: // 1. Define Quantization Table (size=130)
|
||||
// for i := uint16(4 + 1); i < size; i += 1 + 64 {
|
||||
// h2 = append(h2, p[i:i+64]...)
|
||||
// }
|
||||
// case 0xC0: // 2. Start Of Frame (size=15)
|
||||
// if p[4] != 8 {
|
||||
// return nil
|
||||
// }
|
||||
// h := binary.BigEndian.Uint16(p[5:])
|
||||
// w := binary.BigEndian.Uint16(p[7:])
|
||||
// h1[6] = uint8(w >> 3)
|
||||
// h1[7] = uint8(h >> 3)
|
||||
// case 0xC4: // 3. Define Huffman Table (size=416)
|
||||
// case 0xDA: // 4. Start Of Scan (size=10)
|
||||
// jpgData = p[size:]
|
||||
// }
|
||||
//
|
||||
// p = p[size:]
|
||||
// }
|
||||
//
|
||||
// offset := 0
|
||||
// p = make([]byte, 0)
|
||||
//
|
||||
// for jpgData != nil {
|
||||
// p = p[:0]
|
||||
//
|
||||
// if offset > 0 {
|
||||
// h1[1] = byte(offset >> 16)
|
||||
// h1[2] = byte(offset >> 8)
|
||||
// h1[3] = byte(offset)
|
||||
// p = append(p, h1...)
|
||||
// } else {
|
||||
// p = append(p, h1...)
|
||||
// p = append(p, h2...)
|
||||
// }
|
||||
//
|
||||
// dataLen := packetSize - len(p)
|
||||
// if dataLen < len(jpgData) {
|
||||
// p = append(p, jpgData[:dataLen]...)
|
||||
// jpgData = jpgData[dataLen:]
|
||||
// offset += dataLen
|
||||
// } else {
|
||||
// p = append(p, jpgData...)
|
||||
// jpgData = nil
|
||||
// }
|
||||
//
|
||||
// clone := rtp.Packet{
|
||||
// Header: rtp.Header{
|
||||
// Version: 2,
|
||||
// Marker: jpgData == nil,
|
||||
// SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
// Timestamp: packet.Timestamp,
|
||||
// },
|
||||
// Payload: p,
|
||||
// }
|
||||
// if err := push(&clone); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//}
|
24
pkg/mp4/README.md
Normal file
24
pkg/mp4/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## HEVC
|
||||
|
||||
Browser | avc1 | hvc1 | hev1
|
||||
------------|------|------|---
|
||||
Mac Chrome | + | - | +
|
||||
Mac Safari | + | + | -
|
||||
iOS 15? | + | + | -
|
||||
Mac Firefox | + | - | -
|
||||
iOS 12 | + | - | -
|
||||
Android 13 | + | - | -
|
||||
|
||||
```
|
||||
ffmpeg -i input-hev1.mp4 -c:v copy -tag:v hvc1 -c:a copy output-hvc1.mp4
|
||||
Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
|
||||
Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1
|
||||
- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
|
||||
- https://jellyfin.org/docs/general/clients/codec-support.html
|
||||
- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding
|
||||
- https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter
|
@@ -3,6 +3,8 @@ package mp4
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/deepch/vdk/format/mp4/mp4io"
|
||||
"github.com/deepch/vdk/format/mp4f"
|
||||
"github.com/deepch/vdk/format/mp4f/mp4fio"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -37,25 +39,17 @@ func MOOV() *mp4io.Movie {
|
||||
SelectionDuration: time0,
|
||||
CurrentTime: time0,
|
||||
},
|
||||
MovieExtend: &mp4io.MovieExtend{
|
||||
Tracks: []*mp4io.TrackExtend{
|
||||
{
|
||||
TrackId: 1,
|
||||
DefaultSampleDescIdx: 1,
|
||||
DefaultSampleDuration: 40,
|
||||
},
|
||||
},
|
||||
},
|
||||
MovieExtend: &mp4io.MovieExtend{},
|
||||
}
|
||||
}
|
||||
|
||||
func TRAK() *mp4io.Track {
|
||||
func TRAK(id int) *mp4io.Track {
|
||||
return &mp4io.Track{
|
||||
// trak > tkhd
|
||||
Header: &mp4io.TrackHeader{
|
||||
TrackId: int32(1), // change me
|
||||
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
|
||||
Duration: 0, // OK
|
||||
TrackId: int32(id),
|
||||
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
|
||||
Duration: 0, // OK
|
||||
Matrix: matrix,
|
||||
CreateTime: time0,
|
||||
ModifyTime: time0,
|
||||
@@ -92,3 +86,15 @@ func TRAK() *mp4io.Track {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ESDS(conf []byte) *mp4f.FDummy {
|
||||
esds := &mp4fio.ElemStreamDesc{DecConfig: conf}
|
||||
|
||||
b := make([]byte, esds.Len())
|
||||
esds.Marshal(b)
|
||||
|
||||
return &mp4f.FDummy{
|
||||
Data: b,
|
||||
Tag_: mp4io.Tag(uint32(mp4io.ESDS)),
|
||||
}
|
||||
}
|
||||
|
@@ -2,8 +2,9 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
@@ -11,75 +12,137 @@ import (
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
|
||||
Medias []*streamer.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
muxer *Muxer
|
||||
codecs []*streamer.Codec
|
||||
start bool
|
||||
wait byte
|
||||
|
||||
send int
|
||||
}
|
||||
|
||||
const (
|
||||
waitNone byte = iota
|
||||
waitKeyframe
|
||||
waitInit
|
||||
)
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264, ClockRate: 90000},
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC},
|
||||
},
|
||||
},
|
||||
//{
|
||||
// Kind: streamer.KindAudio,
|
||||
// Direction: streamer.DirectionRecvonly,
|
||||
// Codecs: []*streamer.Codec{
|
||||
// {Name: streamer.CodecAAC, ClockRate: 16000},
|
||||
// },
|
||||
//},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
trackID := byte(len(c.codecs))
|
||||
c.codecs = append(c.codecs, track.Codec)
|
||||
|
||||
codec := track.Codec
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
c.codecs = append(c.codecs, track.Codec)
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch h264.NALUType(packet.Payload) {
|
||||
case h264.NALUTypeIFrame:
|
||||
c.start = true
|
||||
case h264.NALUTypePFrame:
|
||||
if !c.start {
|
||||
if c.wait != waitNone {
|
||||
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(packet)
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h264.IsAVC(codec) {
|
||||
wrapper := h264.RTPDepay(track)
|
||||
var wrapper streamer.WrapperFunc
|
||||
if codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
} else {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
}
|
||||
push = wrapper(push)
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.wait != waitNone {
|
||||
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := h265.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if c.wait != waitNone {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := aac.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
fmt.Printf("[rtmp] unsupported codec: %+v\n", track.Codec)
|
||||
|
||||
return nil
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
func (c *Consumer) MimeType() string {
|
||||
@@ -87,12 +150,16 @@ func (c *Consumer) MimeType() string {
|
||||
}
|
||||
|
||||
func (c *Consumer) Init() ([]byte, error) {
|
||||
if c.muxer == nil {
|
||||
c.muxer = &Muxer{}
|
||||
}
|
||||
c.muxer = &Muxer{}
|
||||
return c.muxer.GetInit(c.codecs)
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() {
|
||||
if c.wait == waitInit {
|
||||
c.wait = waitKeyframe
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
|
151
pkg/mp4/muxer.go
151
pkg/mp4/muxer.go
@@ -2,10 +2,13 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"encoding/hex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/codec/h265parser"
|
||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||
"github.com/deepch/vdk/format/mp4/mp4io"
|
||||
"github.com/deepch/vdk/format/mp4f/mp4fio"
|
||||
@@ -14,19 +17,27 @@ import (
|
||||
|
||||
type Muxer struct {
|
||||
fragIndex uint32
|
||||
dts uint64
|
||||
pts uint32
|
||||
data []byte
|
||||
total int
|
||||
dts []uint64
|
||||
pts []uint32
|
||||
}
|
||||
|
||||
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
s := `video/mp4; codecs="`
|
||||
|
||||
for _, codec := range codecs {
|
||||
for i, codec := range codecs {
|
||||
if i > 0 {
|
||||
s += ","
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
case streamer.CodecH265:
|
||||
// H.265 profile=main level=5.1
|
||||
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
|
||||
s += "hvc1.1.6.L153.B0"
|
||||
case streamer.CodecAAC:
|
||||
s += "mp4a.40.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,15 +47,16 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
moov := MOOV()
|
||||
|
||||
for _, codec := range codecs {
|
||||
for i, codec := range codecs {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
return nil, fmt.Errorf("empty SPS: %#v", codec)
|
||||
// some dummy SPS and PPS not a problem
|
||||
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -53,11 +65,14 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
width := codecData.Width()
|
||||
height := codecData.Height()
|
||||
|
||||
trak := TRAK()
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.TrackWidth = float64(width)
|
||||
trak.Header.TrackHeight = float64(height)
|
||||
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||
}
|
||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||
Flags: 0x000001,
|
||||
}
|
||||
@@ -75,13 +90,91 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
|
||||
case streamer.CodecH265:
|
||||
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}
|
||||
sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}
|
||||
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
|
||||
}
|
||||
|
||||
codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := codecData.Width()
|
||||
height := codecData.Height()
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.TrackWidth = float64(width)
|
||||
trak.Header.TrackHeight = float64(height)
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||
}
|
||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||
Flags: 0x000001,
|
||||
}
|
||||
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
|
||||
DataRefIdx: 1,
|
||||
HorizontalResolution: 72,
|
||||
VorizontalResolution: 72,
|
||||
Width: int16(width),
|
||||
Height: int16(height),
|
||||
FrameCount: 1,
|
||||
Depth: 24,
|
||||
ColorTableId: -1,
|
||||
Conf: &mp4io.HV1Conf{
|
||||
Data: codecData.AVCDecoderConfRecordBytes(),
|
||||
},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.AlternateGroup = 1
|
||||
trak.Header.Duration = 0
|
||||
trak.Header.Volume = 1
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
|
||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'s', 'o', 'u', 'n'},
|
||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||
}
|
||||
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
|
||||
|
||||
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
||||
DataRefIdx: 1,
|
||||
NumberOfChannels: int16(codec.Channels),
|
||||
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
||||
SampleRate: float64(codec.ClockRate),
|
||||
Unknowns: []mp4io.Atom{ESDS(b)},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
}
|
||||
|
||||
trex := &mp4io.TrackExtend{
|
||||
TrackId: uint32(i + 1),
|
||||
DefaultSampleDescIdx: 1,
|
||||
DefaultSampleDuration: 0,
|
||||
}
|
||||
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
|
||||
|
||||
m.pts = append(m.pts, 0)
|
||||
m.dts = append(m.dts, 0)
|
||||
}
|
||||
|
||||
data := make([]byte, moov.Len())
|
||||
@@ -90,14 +183,15 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
return append(FTYP(), data...), nil
|
||||
}
|
||||
|
||||
func (m *Muxer) Rewind() {
|
||||
m.dts = 0
|
||||
m.pts = 0
|
||||
func (m *Muxer) Reset() {
|
||||
m.fragIndex = 0
|
||||
for i := range m.dts {
|
||||
m.dts[i] = 0
|
||||
m.pts[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
||||
trackID := uint8(1)
|
||||
|
||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||
run := &mp4fio.TrackFragRun{
|
||||
Flags: 0x000b05,
|
||||
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
|
||||
@@ -112,12 +206,12 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
||||
Tracks: []*mp4fio.TrackFrag{
|
||||
{
|
||||
Header: &mp4fio.TrackFragHeader{
|
||||
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID, 0x01, 0x01, 0x00, 0x00},
|
||||
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
|
||||
},
|
||||
DecodeTime: &mp4fio.TrackFragDecodeTime{
|
||||
Version: 1,
|
||||
Flags: 0,
|
||||
Time: m.dts,
|
||||
Time: m.dts[trackID],
|
||||
},
|
||||
Run: run,
|
||||
},
|
||||
@@ -125,17 +219,18 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
||||
}
|
||||
|
||||
entry := mp4io.TrackFragRunEntry{
|
||||
//Duration: 90000,
|
||||
Size: uint32(len(packet.Payload)),
|
||||
Size: uint32(len(packet.Payload)),
|
||||
}
|
||||
|
||||
newTime := packet.Timestamp
|
||||
if m.pts > 0 {
|
||||
//m.dts += uint64(newTime - m.pts)
|
||||
entry.Duration = newTime - m.pts
|
||||
m.dts += uint64(entry.Duration)
|
||||
if m.pts[trackID] > 0 {
|
||||
entry.Duration = newTime - m.pts[trackID]
|
||||
m.dts[trackID] += uint64(entry.Duration)
|
||||
} else {
|
||||
// important, or Safari will fail with first frame
|
||||
entry.Duration = 1
|
||||
}
|
||||
m.pts = newTime
|
||||
m.pts[trackID] = newTime
|
||||
|
||||
// important before moof.Len()
|
||||
run.Entries = append(run.Entries, entry)
|
||||
@@ -155,7 +250,7 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
||||
|
||||
m.fragIndex++
|
||||
|
||||
m.total += moofLen + mdatLen
|
||||
//m.total += moofLen + mdatLen
|
||||
|
||||
return buf
|
||||
}
|
||||
|
123
pkg/mp4/segment.go
Normal file
123
pkg/mp4/segment.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Segment struct {
|
||||
streamer.Element
|
||||
|
||||
Medias []*streamer.Media
|
||||
MimeType string
|
||||
OnlyKeyframe bool
|
||||
}
|
||||
|
||||
func (c *Segment) GetMedias() []*streamer.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
muxer := &Muxer{}
|
||||
|
||||
codecs := []*streamer.Codec{track.Codec}
|
||||
|
||||
init, err := muxer.GetInit(codecs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.MimeType = muxer.MimeType(codecs)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
var push streamer.WriterFunc
|
||||
|
||||
if c.OnlyKeyframe {
|
||||
push = func(packet *rtp.Packet) error {
|
||||
if !h264.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := muxer.Marshal(0, packet)
|
||||
c.Fire(append(init, buf...))
|
||||
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
var buf []byte
|
||||
|
||||
push = func(packet *rtp.Packet) error {
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
// fist frame - send only IFrame
|
||||
// other frames - send IFrame and all PFrames
|
||||
if buf == nil {
|
||||
buf = append(buf, init...)
|
||||
b := muxer.Marshal(0, packet)
|
||||
buf = append(buf, b...)
|
||||
}
|
||||
|
||||
c.Fire(buf)
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, init...)
|
||||
muxer.Reset()
|
||||
}
|
||||
|
||||
if buf != nil {
|
||||
b := muxer.Marshal(0, packet)
|
||||
buf = append(buf, b...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var wrapper streamer.WrapperFunc
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
} else {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
}
|
||||
push = wrapper(push)
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if !h265.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := muxer.Marshal(0, packet)
|
||||
c.Fire(append(init, buf...))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper := h265.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
}
|
164
pkg/mp4f/consumer.go
Normal file
164
pkg/mp4f/consumer.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package mp4f
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/mp4f"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
muxer *mp4f.Muxer
|
||||
streams []av.CodecData
|
||||
mimeType string
|
||||
start bool
|
||||
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264, ClockRate: 90000},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC, ClockRate: 16000},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
codec := track.Codec
|
||||
trackID := int8(len(c.streams))
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
c.streams = append(c.streams, stream)
|
||||
|
||||
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||
|
||||
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !c.start {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkt.Data = packet.Payload
|
||||
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||
if pkt.Time > 0 {
|
||||
pkt.Duration = newTime - pkt.Time
|
||||
}
|
||||
pkt.Time = newTime
|
||||
|
||||
ready, buf, _ := c.muxer.WritePacket(pkt, false)
|
||||
if ready {
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !codec.IsRAW() {
|
||||
wrapper := h264.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
|
||||
|
||||
c.mimeType += ",mp4a.40.2"
|
||||
c.streams = append(c.streams, stream)
|
||||
|
||||
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||
|
||||
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if !c.start {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkt.Data = packet.Payload
|
||||
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||
if pkt.Time > 0 {
|
||||
pkt.Duration = newTime - pkt.Time
|
||||
}
|
||||
pkt.Time = newTime
|
||||
|
||||
ready, buf, _ := c.muxer.WritePacket(pkt, false)
|
||||
if ready {
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
func (c *Consumer) MimeType() string {
|
||||
return `video/mp4; codecs="` + c.mimeType + `"`
|
||||
}
|
||||
|
||||
func (c *Consumer) Init() ([]byte, error) {
|
||||
c.muxer = mp4f.NewMuxer(nil)
|
||||
if err := c.muxer.WriteHeader(c.streams); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, data := c.muxer.GetInit(c.streams)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() {
|
||||
c.start = true
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
v := map[string]interface{}{
|
||||
"type": "MSE server consumer",
|
||||
"send": c.send,
|
||||
"remote_addr": c.RemoteAddr,
|
||||
"user_agent": c.UserAgent,
|
||||
}
|
||||
|
||||
return json.Marshal(v)
|
||||
}
|
@@ -4,16 +4,24 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/httpflv"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/rtmp"
|
||||
"github.com/pion/rtp"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Conn for RTMP and RTMPT (flv over HTTP)
|
||||
type Conn interface {
|
||||
Streams() (streams []av.CodecData, err error)
|
||||
ReadPacket() (pkt av.Packet, err error)
|
||||
Close() (err error)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
|
||||
@@ -22,7 +30,7 @@ type Client struct {
|
||||
medias []*streamer.Media
|
||||
tracks []*streamer.Track
|
||||
|
||||
conn *rtmp.Conn
|
||||
conn Conn
|
||||
closed bool
|
||||
|
||||
receive int
|
||||
@@ -34,10 +42,19 @@ func NewClient(uri string) *Client {
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
c.conn, err = rtmp.Dial(c.URI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Accept - convert http.Response to Client
|
||||
func Accept(res *http.Response) (*Client, error) {
|
||||
conn, err := httpflv.Accept(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{URI: res.Request.URL.String(), conn: conn}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Describe() (err error) {
|
||||
// important to get SPS/PPS
|
||||
streams, err := c.conn.Streams()
|
||||
if err != nil {
|
||||
@@ -47,16 +64,20 @@ func (c *Client) Dial() (err error) {
|
||||
for _, stream := range streams {
|
||||
switch stream.Type() {
|
||||
case av.H264:
|
||||
cd := stream.(h264parser.CodecData)
|
||||
fmtp := "sprop-parameter-sets=" +
|
||||
base64.StdEncoding.EncodeToString(cd.RecordInfo.SPS[0]) + "," +
|
||||
base64.StdEncoding.EncodeToString(cd.RecordInfo.PPS[0])
|
||||
info := stream.(h264parser.CodecData).RecordInfo
|
||||
|
||||
fmtp := fmt.Sprintf(
|
||||
"profile-level-id=%02X%02X%02X;sprop-parameter-sets=%s,%s",
|
||||
info.AVCProfileIndication, info.ProfileCompatibility, info.AVCLevelIndication,
|
||||
base64.StdEncoding.EncodeToString(info.SPS[0]),
|
||||
base64.StdEncoding.EncodeToString(info.PPS[0]),
|
||||
)
|
||||
|
||||
codec := &streamer.Codec{
|
||||
Name: streamer.CodecH264,
|
||||
ClockRate: 90000,
|
||||
FmtpLine: fmtp,
|
||||
PayloadType: h264.PayloadTypeAVC,
|
||||
PayloadType: streamer.PayloadTypeRAW,
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
@@ -66,26 +87,20 @@ func (c *Client) Dial() (err error) {
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
track := &streamer.Track{
|
||||
Codec: codec, Direction: media.Direction,
|
||||
}
|
||||
track := streamer.NewTrack(codec, media.Direction)
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
case av.AAC:
|
||||
// TODO: fix support
|
||||
cd := stream.(aacparser.CodecData)
|
||||
|
||||
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
||||
fmtp := fmt.Sprintf(
|
||||
"config=%s",
|
||||
hex.EncodeToString(cd.ConfigBytes),
|
||||
)
|
||||
|
||||
codec := &streamer.Codec{
|
||||
Name: streamer.CodecAAC,
|
||||
ClockRate: uint32(cd.Config.SampleRate),
|
||||
Channels: uint16(cd.Config.ChannelConfig),
|
||||
FmtpLine: fmtp,
|
||||
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
||||
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
|
||||
PayloadType: streamer.PayloadTypeRAW,
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
@@ -95,9 +110,7 @@ func (c *Client) Dial() (err error) {
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
track := &streamer.Track{
|
||||
Codec: codec, Direction: media.Direction,
|
||||
}
|
||||
track := streamer.NewTrack(codec, media.Direction)
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
default:
|
||||
@@ -129,22 +142,14 @@ func (c *Client) Handle() (err error) {
|
||||
|
||||
track := c.tracks[int(pkt.Idx)]
|
||||
|
||||
timestamp := uint32(pkt.Time / time.Duration(track.Codec.ClockRate))
|
||||
// convert seconds to RTP timestamp
|
||||
timestamp := uint32(pkt.Time * time.Duration(track.Codec.ClockRate) / time.Second)
|
||||
|
||||
var payloads [][]byte
|
||||
if track.Codec.Name == streamer.CodecH264 {
|
||||
payloads = h264.SplitAVC(pkt.Data)
|
||||
} else {
|
||||
payloads = [][]byte{pkt.Data}
|
||||
}
|
||||
|
||||
for _, payload := range payloads {
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: timestamp},
|
||||
Payload: payload,
|
||||
}
|
||||
_ = track.WriteRTP(packet)
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: timestamp},
|
||||
Payload: pkt.Data,
|
||||
}
|
||||
_ = track.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -32,7 +32,7 @@ func (c *Client) MarshalJSON() ([]byte, error) {
|
||||
v := map[string]interface{}{
|
||||
streamer.JSONReceive: c.receive,
|
||||
streamer.JSONType: "RTMP client producer",
|
||||
streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
|
||||
//streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
|
||||
"url": c.URI,
|
||||
}
|
||||
for i, media := range c.medias {
|
||||
|
251
pkg/rtsp/conn.go
251
pkg/rtsp/conn.go
@@ -7,7 +7,9 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtcp"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -43,7 +46,15 @@ const (
|
||||
ModeServerConsumer
|
||||
)
|
||||
|
||||
const KeepAlive = time.Second * 25
|
||||
type State byte
|
||||
|
||||
const (
|
||||
StateNone State = iota
|
||||
StateConn
|
||||
StateSetup
|
||||
StatePlay
|
||||
StateHandle
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
@@ -62,6 +73,8 @@ type Conn struct {
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
mode Mode
|
||||
state State
|
||||
stateMu sync.Mutex
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
uri string
|
||||
@@ -90,6 +103,11 @@ func NewServer(conn net.Conn) *Conn {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Conn) Auth(username, password string) {
|
||||
info := url.UserPassword(username, password)
|
||||
c.auth = tcp.NewAuth(info)
|
||||
}
|
||||
|
||||
func (c *Conn) parseURI() (err error) {
|
||||
c.URL, err = url.Parse(c.uri)
|
||||
if err != nil {
|
||||
@@ -108,16 +126,11 @@ func (c *Conn) parseURI() (err error) {
|
||||
}
|
||||
|
||||
func (c *Conn) Dial() (err error) {
|
||||
//if c.state != StateClientInit {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
if c.conn != nil {
|
||||
_ = c.parseURI()
|
||||
}
|
||||
|
||||
c.conn, err = net.DialTimeout(
|
||||
"tcp", c.URL.Host, 10*time.Second,
|
||||
)
|
||||
c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -139,6 +152,7 @@ func (c *Conn) Dial() (err error) {
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
c.state = StateConn
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -329,6 +343,9 @@ func (c *Conn) Setup() error {
|
||||
func (c *Conn) SetupMedia(
|
||||
media *streamer.Media, codec *streamer.Codec,
|
||||
) (*streamer.Track, error) {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
ch := c.GetChannel(media)
|
||||
if ch < 0 {
|
||||
return nil, fmt.Errorf("wrong media: %v", media)
|
||||
@@ -362,21 +379,25 @@ func (c *Conn) SetupMedia(
|
||||
var res *tcp.Response
|
||||
res, err = c.Do(req)
|
||||
if err != nil {
|
||||
// Dahua VTO2111D fail on this step because of backchannel
|
||||
// some Dahua/Amcrest cameras fail here because two simultaneous
|
||||
// backchannel connections
|
||||
if c.Backchannel {
|
||||
if err = c.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Backchannel = false
|
||||
if err = c.Describe(); err != nil {
|
||||
if err := c.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = c.Do(req)
|
||||
if err := c.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, newMedia := range c.Medias {
|
||||
if newMedia.Control == media.Control {
|
||||
return c.SetupMedia(newMedia, newMedia.Codecs[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.Session == "" {
|
||||
@@ -389,15 +410,25 @@ func (c *Conn) SetupMedia(
|
||||
}
|
||||
}
|
||||
|
||||
// in case the track has already been setup before
|
||||
if codec == nil {
|
||||
c.state = StateSetup
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// we send our `interleaved`, but camera can answer with another
|
||||
|
||||
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
|
||||
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.123;source=192.168.10.12;interleaved=0
|
||||
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0
|
||||
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
|
||||
s := res.Header.Get("Transport")
|
||||
// TODO: rewrite
|
||||
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
|
||||
return nil, fmt.Errorf("wrong transport: %s", s)
|
||||
// Escam Q6 has a bug:
|
||||
// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1
|
||||
if !strings.Contains(s, ";interleaved=") {
|
||||
return nil, fmt.Errorf("wrong transport: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
i := strings.Index(s, "interleaved=")
|
||||
@@ -416,9 +447,7 @@ func (c *Conn) SetupMedia(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
track := &streamer.Track{
|
||||
Codec: codec, Direction: media.Direction,
|
||||
}
|
||||
track := streamer.NewTrack(codec, media.Direction)
|
||||
|
||||
switch track.Direction {
|
||||
case streamer.DirectionSendonly:
|
||||
@@ -431,44 +460,50 @@ func (c *Conn) SetupMedia(
|
||||
track = c.bindTrack(track, byte(ch), codec.PayloadType)
|
||||
}
|
||||
|
||||
c.state = StateSetup
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Play() (err error) {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state != StateSetup {
|
||||
return fmt.Errorf("RTSP PLAY from wrong state: %s", c.state)
|
||||
}
|
||||
|
||||
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
|
||||
return c.Request(req)
|
||||
if err = c.Request(req); err == nil {
|
||||
c.state = StatePlay
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) Teardown() (err error) {
|
||||
//if c.state != StateClientPlay {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
|
||||
// allow TEARDOWN from any state (ex. ANNOUNCE > SETUP)
|
||||
req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
|
||||
return c.Request(req)
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if c.conn == nil {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state == StateNone {
|
||||
return nil
|
||||
}
|
||||
if err := c.Teardown(); err != nil {
|
||||
return err
|
||||
}
|
||||
conn := c.conn
|
||||
c.conn = nil
|
||||
return conn.Close()
|
||||
|
||||
_ = c.Teardown()
|
||||
c.state = StateNone
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
||||
|
||||
func (c *Conn) Accept() error {
|
||||
//if c.state != StateServerInit {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
|
||||
for {
|
||||
req, err := tcp.ReadRequest(c.reader)
|
||||
if err != nil {
|
||||
@@ -482,6 +517,17 @@ func (c *Conn) Accept() error {
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
if !c.auth.Validate(req) {
|
||||
res := &tcp.Response{
|
||||
Status: "401 Unauthorized",
|
||||
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
||||
}
|
||||
if err = c.Response(res); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
||||
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
||||
switch req.Method {
|
||||
@@ -509,9 +555,7 @@ func (c *Conn) Accept() error {
|
||||
// TODO: fix someday...
|
||||
c.channels = map[byte]*streamer.Track{}
|
||||
for i, media := range c.Medias {
|
||||
track := &streamer.Track{
|
||||
Codec: media.Codecs[0], Direction: media.Direction,
|
||||
}
|
||||
track := streamer.NewTrack(media.Codecs[0], media.Direction)
|
||||
c.tracks = append(c.tracks, track)
|
||||
c.channels[byte(i<<1)] = track
|
||||
}
|
||||
@@ -571,8 +615,9 @@ func (c *Conn) Accept() error {
|
||||
Request: req,
|
||||
}
|
||||
|
||||
if tr[:len(transport)] == transport {
|
||||
if strings.HasPrefix(tr, transport) {
|
||||
c.Session = "1" // TODO: fixme
|
||||
c.state = StateSetup
|
||||
res.Header.Set("Transport", tr[:len(transport)+3])
|
||||
} else {
|
||||
res.Status = "461 Unsupported transport"
|
||||
@@ -584,7 +629,10 @@ func (c *Conn) Accept() error {
|
||||
|
||||
case MethodRecord, MethodPlay:
|
||||
res := &tcp.Response{Request: req}
|
||||
return c.Response(res)
|
||||
if err = c.Response(res); err == nil {
|
||||
c.state = StatePlay
|
||||
}
|
||||
return err
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported method: %s", req.Method)
|
||||
@@ -593,17 +641,69 @@ func (c *Conn) Accept() error {
|
||||
}
|
||||
|
||||
func (c *Conn) Handle() (err error) {
|
||||
c.stateMu.Lock()
|
||||
|
||||
switch c.state {
|
||||
case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY)
|
||||
case StatePlay:
|
||||
c.state = StateHandle
|
||||
default:
|
||||
err = fmt.Errorf("RTSP Handle from wrong state: %d", c.state)
|
||||
|
||||
c.state = StateNone
|
||||
_ = c.conn.Close()
|
||||
}
|
||||
|
||||
c.stateMu.Unlock()
|
||||
|
||||
if c.state != StateHandle {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if c.conn == nil {
|
||||
c.stateMu.Lock()
|
||||
defer c.stateMu.Unlock()
|
||||
|
||||
if c.state == StateNone {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
//c.Fire(streamer.StateNull)
|
||||
|
||||
// may have gotten here because of the deadline
|
||||
// so close the connection to stop keepalive
|
||||
c.state = StateNone
|
||||
_ = c.conn.Close()
|
||||
}()
|
||||
|
||||
//c.Fire(streamer.StatePlaying)
|
||||
ts := time.Now().Add(KeepAlive)
|
||||
var timeout time.Duration
|
||||
|
||||
switch c.mode {
|
||||
case ModeClientProducer:
|
||||
// polling frames from remote RTSP Server (ex Camera)
|
||||
timeout = time.Second * 5
|
||||
go c.keepalive()
|
||||
|
||||
case ModeServerProducer:
|
||||
// polling frames from remote RTSP Client (ex FFmpeg)
|
||||
timeout = time.Second * 15
|
||||
|
||||
case ModeServerConsumer:
|
||||
// pushing frames to remote RTSP Client (ex VLC)
|
||||
timeout = time.Second * 60
|
||||
|
||||
default:
|
||||
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
|
||||
}
|
||||
|
||||
for {
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we can read:
|
||||
// 1. RTP interleaved: `$` + 1B channel number + 2B size
|
||||
// 2. RTSP response: RTSP/1.0 200 OK
|
||||
@@ -615,22 +715,21 @@ func (c *Conn) Handle() (err error) {
|
||||
}
|
||||
|
||||
if buf4[0] != '$' {
|
||||
if string(buf4) == "RTSP" {
|
||||
switch string(buf4) {
|
||||
case "RTSP":
|
||||
var res *tcp.Response
|
||||
res, err = tcp.ReadResponse(c.reader)
|
||||
if err != nil {
|
||||
if res, err = tcp.ReadResponse(c.reader); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
} else {
|
||||
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
||||
var req *tcp.Request
|
||||
req, err = tcp.ReadRequest(c.reader)
|
||||
if err != nil {
|
||||
if req, err = tcp.ReadRequest(c.reader); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
default:
|
||||
return fmt.Errorf("RTSP wrong input")
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -681,16 +780,19 @@ func (c *Conn) Handle() (err error) {
|
||||
|
||||
c.Fire(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep-alive
|
||||
now := time.Now()
|
||||
if now.After(ts) {
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
// don't need to wait respose on this request
|
||||
if err = c.Request(req); err != nil {
|
||||
return err
|
||||
}
|
||||
ts = now.Add(KeepAlive)
|
||||
func (c *Conn) keepalive() {
|
||||
// TODO: rewrite to RTCP
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
for {
|
||||
time.Sleep(time.Second * 25)
|
||||
if c.state == StateNone {
|
||||
return
|
||||
}
|
||||
if err := c.Request(req); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,20 +810,18 @@ func (c *Conn) bindTrack(
|
||||
track *streamer.Track, channel uint8, payloadType uint8,
|
||||
) *streamer.Track {
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if c.conn == nil {
|
||||
if c.state == StateNone {
|
||||
return nil
|
||||
}
|
||||
packet.Header.PayloadType = payloadType
|
||||
//packet.Header.PayloadType = 100
|
||||
//packet.Header.PayloadType = 8
|
||||
//packet.Header.PayloadType = 106
|
||||
|
||||
size := packet.MarshalSize()
|
||||
|
||||
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
data := make([]byte, 4+size)
|
||||
data[0] = '$'
|
||||
data[1] = channel
|
||||
//data[1] = 10
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(size))
|
||||
|
||||
if _, err := packet.MarshalTo(data[4:]); err != nil {
|
||||
@@ -737,9 +837,18 @@ func (c *Conn) bindTrack(
|
||||
return nil
|
||||
}
|
||||
|
||||
if h264.IsAVC(track.Codec) {
|
||||
wrapper := h264.RTPPay(1500)
|
||||
push = wrapper(push)
|
||||
if !track.Codec.IsRTP() {
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
wrapper := h264.RTPPay(1500)
|
||||
push = wrapper(push)
|
||||
case streamer.CodecAAC:
|
||||
wrapper := aac.RTPPay(1500)
|
||||
push = wrapper(push)
|
||||
case streamer.CodecJPEG:
|
||||
wrapper := mjpeg.RTPPay()
|
||||
push = wrapper(push)
|
||||
}
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
@@ -2,6 +2,7 @@ package rtsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strconv"
|
||||
)
|
||||
@@ -19,6 +20,13 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
|
||||
}
|
||||
}
|
||||
|
||||
// can't setup new tracks from play state - forcing a reconnection feature
|
||||
switch c.state {
|
||||
case StatePlay, StateHandle:
|
||||
go c.Close()
|
||||
return streamer.NewTrack(codec, media.Direction)
|
||||
}
|
||||
|
||||
track, err := c.SetupMedia(media, codec)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -27,13 +35,16 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
|
||||
}
|
||||
|
||||
func (c *Conn) Start() error {
|
||||
if c.mode == ModeServerProducer {
|
||||
return nil
|
||||
switch c.mode {
|
||||
case ModeClientProducer:
|
||||
if err := c.Play(); err != nil {
|
||||
return err
|
||||
}
|
||||
case ModeServerProducer:
|
||||
default:
|
||||
return fmt.Errorf("start wrong mode: %d", c.mode)
|
||||
}
|
||||
|
||||
if err := c.Play(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Handle()
|
||||
}
|
||||
|
||||
|
@@ -8,9 +8,19 @@ import (
|
||||
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
|
||||
// this is not really necessary but anyway
|
||||
type Server struct {
|
||||
conn net.PacketConn
|
||||
sessions map[uint32]*Session
|
||||
}
|
||||
|
||||
func (s *Server) Port() uint16 {
|
||||
addr := s.conn.LocalAddr().(*net.UDPAddr)
|
||||
return uint16(addr.Port)
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
return s.conn.Close()
|
||||
}
|
||||
|
||||
func (s *Server) AddSession(session *Session) {
|
||||
if s.sessions == nil {
|
||||
s.sessions = map[uint32]*Session{}
|
||||
@@ -23,6 +33,8 @@ func (s *Server) RemoveSession(session *Session) {
|
||||
}
|
||||
|
||||
func (s *Server) Serve(conn net.PacketConn) error {
|
||||
s.conn = conn
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
for {
|
||||
n, addr, err := conn.ReadFrom(buf)
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/srtp/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
@@ -16,6 +17,14 @@ type Session struct {
|
||||
|
||||
Write func(b []byte) (int, error)
|
||||
Track *streamer.Track
|
||||
|
||||
lastSequence uint32
|
||||
lastTimestamp uint32
|
||||
//lastPacket *rtp.Packet
|
||||
lastTime time.Time
|
||||
jitter float64
|
||||
//sequenceCycle uint16
|
||||
totalLost uint32
|
||||
}
|
||||
|
||||
func (s *Session) SetKeys(
|
||||
@@ -37,13 +46,42 @@ func (s *Session) HandleRTP(data []byte) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if s.Track == nil {
|
||||
return
|
||||
}
|
||||
|
||||
packet := &rtp.Packet{}
|
||||
if err = packet.Unmarshal(data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// https://www.ietf.org/rfc/rfc3550.txt
|
||||
if s.lastTimestamp != 0 {
|
||||
delta := packet.SequenceNumber - uint16(s.lastSequence)
|
||||
|
||||
// lost packet
|
||||
if delta > 1 {
|
||||
s.totalLost += uint32(delta - 1)
|
||||
}
|
||||
|
||||
// D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si)
|
||||
dTime := now.Sub(s.lastTime).Seconds()*float64(s.Track.Codec.ClockRate) -
|
||||
float64(packet.Timestamp-s.lastTimestamp)
|
||||
if dTime < 0 {
|
||||
dTime = -dTime
|
||||
}
|
||||
// J(i) = J(i-1) + (|D(i-1,i)| - J(i-1))/16
|
||||
s.jitter += (dTime - s.jitter) / 16
|
||||
}
|
||||
|
||||
// keeping cycles (overflow)
|
||||
s.lastSequence = s.lastSequence&0xFFFF0000 | uint32(packet.SequenceNumber)
|
||||
s.lastTimestamp = packet.Timestamp
|
||||
s.lastTime = now
|
||||
|
||||
_ = s.Track.WriteRTP(packet)
|
||||
//s.Output(core.RTP{Channel: s.Channel, Packet: packet})
|
||||
|
||||
return
|
||||
}
|
||||
@@ -60,7 +98,6 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
|
||||
}
|
||||
|
||||
_ = packets
|
||||
//s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets})
|
||||
|
||||
if header.Type == rtcp.TypeSenderReport {
|
||||
err = s.KeepAlive()
|
||||
@@ -70,9 +107,25 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
|
||||
}
|
||||
|
||||
func (s *Session) KeepAlive() (err error) {
|
||||
var data []byte
|
||||
// we can send empty receiver response, but should send it to hold the connection
|
||||
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
|
||||
|
||||
if s.lastTimestamp > 0 {
|
||||
//log.Printf("[RTCP] ssrc=%d seq=%d lost=%d jit=%.2f", s.RemoteSSRC, s.lastSequence, s.totalLost, s.jitter)
|
||||
|
||||
rep.Reports = []rtcp.ReceptionReport{{
|
||||
SSRC: s.RemoteSSRC,
|
||||
LastSequenceNumber: s.lastSequence,
|
||||
LastSenderReport: s.lastTimestamp,
|
||||
FractionLost: 0, // TODO
|
||||
TotalLost: s.totalLost,
|
||||
Delay: 0, // send just after receive
|
||||
Jitter: uint32(s.jitter),
|
||||
}}
|
||||
}
|
||||
|
||||
// we can send empty receiver response, but should send it to hold the connection
|
||||
|
||||
var data []byte
|
||||
if data, err = rep.Marshal(); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -90,8 +143,8 @@ func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
|
||||
switch len(masterKey) {
|
||||
case 16:
|
||||
return srtp.ProtectionProfileAes128CmHmacSha1_80
|
||||
case 32:
|
||||
return srtp.ProtectionProfileAes256CmHmacSha1_80
|
||||
//case 32:
|
||||
// return srtp.ProtectionProfileAes256CmHmacSha1_80
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
@@ -12,14 +12,6 @@ const (
|
||||
JSONSend = "send"
|
||||
)
|
||||
|
||||
// Message - struct for data exchange in Web API
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// other
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
|
@@ -25,6 +25,7 @@ const (
|
||||
CodecVP8 = "VP8"
|
||||
CodecVP9 = "VP9"
|
||||
CodecAV1 = "AV1"
|
||||
CodecJPEG = "JPEG" // payloadType: 26
|
||||
|
||||
CodecPCMU = "PCMU" // payloadType: 0
|
||||
CodecPCMA = "PCMA" // payloadType: 8
|
||||
@@ -32,13 +33,17 @@ const (
|
||||
CodecOpus = "OPUS" // payloadType: 111
|
||||
CodecG722 = "G722"
|
||||
CodecMPA = "MPA" // payload: 14
|
||||
|
||||
CodecELD = "ELD" // AAC-ELD
|
||||
)
|
||||
|
||||
const PayloadTypeRAW byte = 255
|
||||
|
||||
func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA, CodecELD:
|
||||
return KindAudio
|
||||
}
|
||||
return ""
|
||||
@@ -74,13 +79,13 @@ func (m *Media) AV() bool {
|
||||
return m.Kind == KindVideo || m.Kind == KindAudio
|
||||
}
|
||||
|
||||
func (m *Media) MatchCodec(codec *Codec) bool {
|
||||
func (m *Media) MatchCodec(codec *Codec) *Codec {
|
||||
for _, c := range m.Codecs {
|
||||
if c.Match(codec) {
|
||||
return true
|
||||
return c
|
||||
}
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Media) MatchMedia(media *Media) *Codec {
|
||||
@@ -126,20 +131,6 @@ type Codec struct {
|
||||
PayloadType uint8
|
||||
}
|
||||
|
||||
func NewCodec(name string) *Codec {
|
||||
name = strings.ToUpper(name)
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
||||
return &Codec{Name: name, ClockRate: 90000}
|
||||
case CodecPCMU, CodecPCMA:
|
||||
return &Codec{Name: name, ClockRate: 8000}
|
||||
case CodecOpus:
|
||||
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported codec: %s", name))
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
|
||||
if c.Channels > 0 {
|
||||
@@ -148,6 +139,10 @@ func (c *Codec) String() string {
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Codec) IsRTP() bool {
|
||||
return c.PayloadType != PayloadTypeRAW
|
||||
}
|
||||
|
||||
func (c *Codec) Clone() *Codec {
|
||||
clone := *c
|
||||
return &clone
|
||||
@@ -194,13 +189,19 @@ func MarshalSDP(medias []*Media) ([]byte, error) {
|
||||
}
|
||||
|
||||
codec := media.Codecs[0]
|
||||
|
||||
name := codec.Name
|
||||
if name == CodecELD {
|
||||
name = CodecAAC
|
||||
}
|
||||
|
||||
md := &sdp.MediaDescription{
|
||||
MediaName: sdp.MediaName{
|
||||
Media: media.Kind,
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(payloadType, codec.Name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
md.WithCodec(payloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
||||
|
||||
@@ -257,6 +258,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
}
|
||||
|
||||
if c.Name == "" {
|
||||
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||
switch payloadType {
|
||||
case "0":
|
||||
c.Name = CodecPCMU
|
||||
@@ -267,6 +269,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
case "14":
|
||||
c.Name = CodecMPA
|
||||
c.ClockRate = 44100
|
||||
case "26":
|
||||
c.Name = CodecJPEG
|
||||
c.ClockRate = 90000
|
||||
default:
|
||||
c.Name = payloadType
|
||||
}
|
||||
|
@@ -12,41 +12,60 @@ type WrapperFunc func(push WriterFunc) WriterFunc
|
||||
type Track struct {
|
||||
Codec *Codec
|
||||
Direction string
|
||||
Sink map[*Track]WriterFunc
|
||||
mx sync.Mutex
|
||||
sink map[*Track]WriterFunc
|
||||
sinkMu *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTrack(codec *Codec, direction string) *Track {
|
||||
return &Track{Codec: codec, Direction: direction, sinkMu: new(sync.RWMutex)}
|
||||
}
|
||||
|
||||
func (t *Track) String() string {
|
||||
s := t.Codec.String()
|
||||
s += fmt.Sprintf(", sinks=%d", len(t.Sink))
|
||||
t.sinkMu.RLock()
|
||||
s += fmt.Sprintf(", sinks=%d", len(t.sink))
|
||||
t.sinkMu.RUnlock()
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
||||
t.mx.Lock()
|
||||
for _, f := range t.Sink {
|
||||
t.sinkMu.RLock()
|
||||
for _, f := range t.sink {
|
||||
_ = f(p)
|
||||
}
|
||||
t.mx.Unlock()
|
||||
t.sinkMu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Track) Bind(w WriterFunc) *Track {
|
||||
if t.Sink == nil {
|
||||
t.Sink = map[*Track]WriterFunc{}
|
||||
t.sinkMu.Lock()
|
||||
|
||||
if t.sink == nil {
|
||||
t.sink = map[*Track]WriterFunc{}
|
||||
}
|
||||
|
||||
clone := &Track{
|
||||
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
|
||||
}
|
||||
t.mx.Lock()
|
||||
t.Sink[clone] = w
|
||||
t.mx.Unlock()
|
||||
return clone
|
||||
clone := *t
|
||||
t.sink[&clone] = w
|
||||
|
||||
t.sinkMu.Unlock()
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (t *Track) Unbind() {
|
||||
t.mx.Lock()
|
||||
delete(t.Sink, t)
|
||||
t.mx.Unlock()
|
||||
t.sinkMu.Lock()
|
||||
delete(t.sink, t)
|
||||
t.sinkMu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Track) GetSink(from *Track) {
|
||||
t.sinkMu.Lock()
|
||||
t.sink = from.sink
|
||||
t.sinkMu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Track) HasSink() bool {
|
||||
t.sinkMu.RLock()
|
||||
defer t.sinkMu.RUnlock()
|
||||
return len(t.sink) > 0
|
||||
}
|
||||
|
@@ -80,6 +80,24 @@ func (a *Auth) Write(req *Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) Validate(req *Request) bool {
|
||||
if a == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
header := req.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Method == AuthUnknown {
|
||||
a.Method = AuthBasic
|
||||
a.header = "Basic " + B64(a.user, a.pass)
|
||||
}
|
||||
|
||||
return header == a.header
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
|
68
pkg/tcp/request.go
Normal file
68
pkg/tcp/request.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Do - http.Client with support Digest Authorization
|
||||
func Do(req *http.Request) (*http.Response, error) {
|
||||
// need to create new client each time to reset timeout
|
||||
client := http.Client{Timeout: time.Second * 5000}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusUnauthorized && req.URL.User != nil {
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
if !strings.HasPrefix(auth, "Digest") {
|
||||
return nil, errors.New("unsupported auth: " + auth)
|
||||
}
|
||||
|
||||
realm := Between(auth, `realm="`, `"`)
|
||||
nonce := Between(auth, `nonce="`, `"`)
|
||||
qop := Between(auth, `qop="`, `"`)
|
||||
|
||||
user := req.URL.User
|
||||
username := user.Username()
|
||||
password, _ := user.Password()
|
||||
ha1 := HexMD5(username, realm, password)
|
||||
|
||||
uri := req.URL.RequestURI()
|
||||
ha2 := HexMD5(req.Method, uri)
|
||||
|
||||
var header string
|
||||
|
||||
switch qop {
|
||||
case "":
|
||||
response := HexMD5(ha1, nonce, ha2)
|
||||
header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
||||
user, realm, nonce, uri, response,
|
||||
)
|
||||
case "auth":
|
||||
nc := "00000001"
|
||||
cnonce := "00000001" // TODO: random...
|
||||
response := HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
|
||||
header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
|
||||
username, realm, nonce, uri, qop, nc, cnonce, response,
|
||||
)
|
||||
default:
|
||||
return nil, errors.New("unsupported qop: " + auth)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", header)
|
||||
|
||||
res, err = client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@@ -86,10 +86,6 @@ func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
|
||||
PayloadType: 98, //123,
|
||||
},
|
||||
// macOS Safari 15.1
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f", videoRTCPFeedback},
|
||||
PayloadType: 99,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback},
|
||||
PayloadType: 100,
|
||||
|
@@ -1,18 +1,10 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
MsgTypeOffer = "webrtc/offer"
|
||||
MsgTypeOfferComplete = "webrtc/offer-complete"
|
||||
MsgTypeAnswer = "webrtc/answer"
|
||||
MsgTypeCandidate = "webrtc/candidate"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
|
||||
@@ -29,11 +21,7 @@ type Conn struct {
|
||||
|
||||
func (c *Conn) Init() {
|
||||
c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate != nil {
|
||||
c.Fire(&streamer.Message{
|
||||
Type: MsgTypeCandidate, Value: candidate.ToJSON().Candidate,
|
||||
})
|
||||
}
|
||||
c.Fire(candidate)
|
||||
})
|
||||
|
||||
c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
@@ -58,8 +46,11 @@ func (c *Conn) Init() {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
|
||||
fmt.Printf("TODO: webrtc ontrack %#v\n", remote)
|
||||
//fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
|
||||
})
|
||||
|
||||
c.Conn.OnDataChannel(func(channel *webrtc.DataChannel) {
|
||||
c.Fire(channel)
|
||||
})
|
||||
|
||||
// OK connection:
|
||||
@@ -100,7 +91,24 @@ func (c *Conn) SetOffer(offer string) (err error) {
|
||||
return
|
||||
}
|
||||
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
||||
c.medias, err = streamer.UnmarshalSDP(rawSDP)
|
||||
medias, err := streamer.UnmarshalSDP(rawSDP)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// sort medias, so video will always be before audio
|
||||
// and ignore application media from Hass default lovelace card
|
||||
for _, media := range medias {
|
||||
if media.Kind == streamer.KindVideo {
|
||||
c.medias = append(c.medias, media)
|
||||
}
|
||||
}
|
||||
for _, media := range medias {
|
||||
if media.Kind == streamer.KindAudio {
|
||||
c.medias = append(c.medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ package webrtc
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v3"
|
||||
@@ -51,16 +52,26 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
|
||||
return trackLocal.WriteRTP(packet)
|
||||
}
|
||||
|
||||
if codec.Name == streamer.CodecH264 {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
wrapper := h264.RTPPay(1200)
|
||||
push = wrapper(push)
|
||||
|
||||
if h264.IsAVC(codec) {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
} else {
|
||||
if codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
} else {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
}
|
||||
push = wrapper(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
// SafariPay because it is the only browser in the world
|
||||
// that supports WebRTC + H265
|
||||
wrapper := h265.SafariPay(1200)
|
||||
push = wrapper(push)
|
||||
|
||||
wrapper = h265.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
track = track.Bind(push)
|
||||
@@ -97,14 +108,8 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
|
||||
|
||||
//
|
||||
|
||||
func (c *Conn) Push(msg interface{}) {
|
||||
if msg := msg.(*streamer.Message); msg != nil {
|
||||
if msg.Type == MsgTypeCandidate {
|
||||
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{
|
||||
Candidate: msg.Value.(string),
|
||||
})
|
||||
}
|
||||
}
|
||||
func (c *Conn) AddCandidate(candidate string) {
|
||||
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{Candidate: candidate})
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user