Compare commits

..

54 Commits

Author SHA1 Message Date
Alexey Khit
18d7b9075b Autoadd cameras from Hass config 2022-08-25 06:41:39 +03:00
Alexey Khit
7c4497f856 Fix incoming RTSP without OPTIONS 2022-08-25 06:38:08 +03:00
Alexey Khit
befa4ca1e6 Remove wrong RTSP channel panic 2022-08-25 06:37:47 +03:00
Alexey Khit
dd3b326f7a Update readme 2022-08-24 14:41:00 +03:00
Alexey Khit
e36123bb19 Update build docker 2022-08-24 14:30:02 +03:00
Alexey Khit
9310343ad3 Update docker cwd to /config 2022-08-24 12:49:59 +03:00
Alexey Khit
e2d4fa3393 Advanced debug on app start 2022-08-24 12:49:48 +03:00
Alexey Khit
5fea2932c1 Error on wrong config 2022-08-24 12:49:40 +03:00
Alexey Khit
1fd110b70d Update readme 2022-08-24 09:55:34 +03:00
Alexey Khit
8377cf2655 Change url param to src in Web API 2022-08-24 09:55:16 +03:00
Alexey Khit
8f01b08d42 Code refactoring 2022-08-24 09:54:28 +03:00
Alexey Khit
97ce4c3114 Adds Security section to readme 2022-08-23 09:34:06 +03:00
Alexey Khit
4813a64d9d Adds build script for mips 2022-08-23 05:43:15 +03:00
Alexey Khit
7923ec74a8 Adds network filter for webrtc 2022-08-23 05:43:01 +03:00
Alexey Khit
1f0a5fb880 Stop WebRTC conn on AddConsumer error 2022-08-22 22:46:08 +03:00
Alexey Khit
c6a3ee65b8 Remove UPX from Windows builds because antiviruses 2022-08-22 22:32:23 +03:00
Alexey Khit
12b712426d Fix busy RTSP backchannel 2022-08-22 15:41:25 +03:00
Alexey Khit
a9af245ef8 Fix async requests to Producer 2022-08-22 15:40:28 +03:00
Alexey Khit
f251129a2f Fix RTSP Transport header parsing 2022-08-22 14:46:39 +03:00
Alexey Khit
d28debabe9 Update fix for parsing RTSP SDP 2022-08-22 14:44:33 +03:00
Alexey Khit
07bf00f9f6 Update readme 2022-08-22 13:40:58 +03:00
Alexey Khit
be6ec7dbb9 Fix RTSP requests for some cameras 2022-08-22 13:38:26 +03:00
Alexey Khit
4e575d1356 Adds build file for win64 2022-08-22 11:43:42 +03:00
Alexey Khit
4cbacfec0c Adds empty response on RTSP error 2022-08-22 11:43:26 +03:00
Alexey Khit
31e24c6e03 Adds stop with empty producer warning 2022-08-22 11:33:38 +03:00
Alexey Khit
401bf85a10 Update RTSP error output 2022-08-22 09:09:18 +03:00
Alexey Khit
f36851f83a Fix response with empty producer 2022-08-22 09:06:40 +03:00
Alexey Khit
67522dbb19 Update readme 2022-08-22 08:44:27 +03:00
Alexey Khit
26b5745f0a Adds keep-alive to RTSP connection 2022-08-22 06:54:58 +03:00
Alexey Khit
46f6a5d8e1 Return unmodified errors from RTSP 2022-08-22 06:54:42 +03:00
Alexey Khit
48f58d0669 Fix wrong stream name request 2022-08-22 06:54:08 +03:00
Alexey Khit
fd0b8f3c39 Fix RTMP with audio 2022-08-22 05:46:22 +03:00
Alexey Khit
863bf503e2 Fix empty remote for webrtc 2022-08-21 18:00:02 +03:00
Alexey Khit
7a3a1a5336 Fix empty producer track 2022-08-21 17:51:36 +03:00
Alexey Khit
b851041caa Fix concurrent map iteration for Track 2022-08-21 17:51:19 +03:00
Alexey Khit
a4acde6d95 Fix two connections to Dahua camera simultaniosly 2022-08-21 17:26:27 +03:00
Alexey Khit
1139d4fcad Fix wrong RTSP Transport responses 2022-08-21 16:58:35 +03:00
Alexey Khit
159ad52277 Fix RTSP Content-Base requests 2022-08-21 16:45:43 +03:00
Alexey Khit
87bc07e404 Update readme 2022-08-21 13:38:42 +03:00
Alexey Khit
d1b29275d7 Adds API for create and delete stream 2022-08-21 09:29:44 +03:00
Alexey Khit
7560bcbc83 Adds log info about serve static dir 2022-08-21 09:29:20 +03:00
Alexey Khit
090c360747 Adds fast script for building linux/amd64 2022-08-21 09:28:47 +03:00
Alexey Khit
a81bf0daa8 Update web interface 2022-08-21 09:28:26 +03:00
Alexey Khit
c7128897b8 Fix webrtc ontrack panic 2022-08-21 09:27:33 +03:00
Alexey Khit
07def5ba04 Adds restarts support to docker container 2022-08-21 09:27:02 +03:00
Alexey Khit
b7f4c63517 Update exec timeout to 15 2022-08-21 06:56:43 +03:00
Alexey Khit
92c67df7b4 Rewrite ffmpeg query format 2022-08-21 06:56:24 +03:00
Alexey Khit
64c0f287ed Adds ffmpeg and ngrok to docker 2022-08-21 00:38:04 +03:00
Alexey Khit
d96af31f86 Adds builder action 2022-08-20 23:27:02 +03:00
Alexey Khit
cc55281f12 Fix known goroutines 2022-08-20 15:52:15 +03:00
Alexey Khit
c10d619df8 Fix unclosed GetPublicIP request 2022-08-20 15:51:47 +03:00
Alexey Khit
65f451e0c5 Fix incoming rtsp url from Hass 2022-08-20 15:50:58 +03:00
Alexey Khit
ecd46700db Support candidates for Hass integration 2022-08-19 18:06:16 +03:00
Alexey Khit
2f588c77c4 Show all streams in list 2022-08-19 18:05:32 +03:00
41 changed files with 787 additions and 406 deletions

59
.github/workflows/builder.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
# 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"

165
README.md
View File

@@ -1,20 +1,20 @@
# go2rtc
**go2rtc** - ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
Ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
- zero-dependency and zero-config small [app for all OS](#installation) (Windows, macOS, Linux, ARM)
- zero-dependency and zero-config small [app for all OS](#go2rtc-binary) (Windows, macOS, Linux, ARM)
- zero-delay for all supported protocols (lowest possible streaming latency)
- zero-load on CPU for supported codecs
- on the fly transcoding for unsupported codecs [via FFmpeg](#source-ffmpeg)
- low CPU load for supported codecs
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
- streaming from private networks via [Ngrok or SSH-tunnels](#module-webrtc)
- streaming from private networks via [Ngrok](#module-webrtc)
**Inspired by:**
- [webrtc](https://github.com/pion/webrtc) go library and whole [@pion](https://github.com/pion) team
- series of streaming projects from [@deepch](https://github.com/deepch)
- [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server) idea from [@aler9](https://github.com/aler9)
- [GStreamer](https://gstreamer.freedesktop.org/) multimedia framework pipeline idea
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
- [MediaSoup](https://mediasoup.org/) framework routing idea
## Codecs negotiation
@@ -45,7 +45,24 @@ streams:
![](codecs.svg)
## Installation
## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
2. Open web interface [http://localhost:1984/](http://localhost:1984/)
**Optionally:**
- add your [streams](#module-streams) to [config](#configuration) file
- setup [external access](#module-webrtc) to webrtc
- setup [external access](#module-ngrok) to web interface
- install [ffmpeg](#source-ffmpeg) for transcoding
**Developers:**
- write your own [web interface](#module-api)
- integrate [web api](#module-api) into your smart home platform
### go2rtc: Binary
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
@@ -59,7 +76,34 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_mac_amd64` - Mac with Intel
- `go2rtc_mac_arm64` - Mac with M1
Don't forget to fix the rights `chmod +x go2rtc_linux_xxx` on Linux and Mac.
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Home Assistant Add-on
[![](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons)
1. Install Add-On:
- Settings > Add-ons > Plus > Repositories > Add `https://github.com/AlexxIT/hassio-addons`
- 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) and [Ngrok](#module-ngrok) applications.
```yaml
services:
go2rtc:
image: alexxit/go2rtc
network_mode: host
restart: always
volumes:
- "~/go2rtc.yaml:/config/go2rtc.yaml"
```
## Configuration
@@ -69,14 +113,14 @@ Create file `go2rtc.yaml` next to the app.
- `api` server will start on default **1984 port**
- `rtsp` server will start on default **8554 port**
- `webrtc` will use random UDP port for each connection
- `ffmpeg` will use default transcoding options (you need to install it [manually](https://ffmpeg.org/))
- `ffmpeg` will use default transcoding options (you may install it [manually](https://ffmpeg.org/))
Available modules:
- [streams](#module-streams)
- [api](#module-api) - HTTP API (important for WebRTC support)
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server (important for external access)
- [webrtc](#module-webrtc) - WebRTC Server
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [hass](#module-hass) - Home Assistant integration
@@ -84,7 +128,7 @@ Available modules:
### Module: Streams
**go2rtc** support different stream source types. You can config only one link as stream source or multiple.
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
Available source types:
@@ -94,12 +138,14 @@ Available source types:
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [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
- Support **2-way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
**Attention:** proprietary 2-way audio standards are not supported!
**Attention:** other 2-way audio standards are not supported! ONVIF without Profile T is not supported!
```yaml
streams:
@@ -130,7 +176,7 @@ streams:
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.
Format: `ffmpeg:{input}#{params}`. Examples:
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
```yaml
streams:
@@ -141,7 +187,7 @@ streams:
file2: ffmpeg:~/media/BigBuckBunny.mp4#video=h264
# [FILE] video will be copied, audio will be transcoded to pcmu
file3: ffmpeg:~/media/BigBuckBunny.mp4#video=copy&audio=pcmu
file3: ffmpeg:~/media/BigBuckBunny.mp4#video=copy#audio=pcmu
# [HLS] video will be copied, audio will be skipped
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
@@ -150,31 +196,18 @@ streams:
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264
# [RTSP] video and audio will be copied
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=copy&audio=copy
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=copy#audio=copy
```
All trascoding formats has built-in templates. But you can override them via YAML config. You can also add your own formats to config and use them with source params.
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`.
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
link: -hide_banner -i {input} # if input is link
file: -hide_banner -re -stream_loop -1 -i {input} # if input not link
rtsp: -hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input} # if input is RTSP link
output: -rtsp_transport tcp -f rtsp {output} # output
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
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"
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
mycodec: "-any args that support ffmpeg..."
```
#### Source: Exec
@@ -202,7 +235,7 @@ streams:
### Module: API
The HTTP API is the main part for interacting with the application.
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
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
@@ -212,11 +245,15 @@ The HTTP API is the main part for interacting with the application.
```yaml
api:
listen: ":1984" # HTTP API port ("" - disabled)
base_path: "" # API prefix for serve on suburl
static_dir: "www" # folder for static files ("" - disabled)
listen: ":1984" # HTTP API port ("" - disabled)
base_path: "" # API prefix for serve on suburl
static_dir: "" # folder for static files (custom web interface)
```
**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.
**PS2.** You can access microphone (for 2-way audio) only with HTTPS
### Module: RTSP
You can get any stream as RTSP-stream with codecs filter:
@@ -236,9 +273,9 @@ rtsp:
### Module: WebRTC
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of internet do you have.
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
- by default, WebRTC use two random UDP ports for each connection (for video and audio)
- by default, WebRTC use two random UDP ports for each connection (video and audio)
- you can enable one additional TCP port for all connections and use it for external access
**Static public IP**
@@ -353,14 +390,24 @@ tunnels:
### Module: Hass
go2rtc compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration API.
**go2rtc** compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration.
- add integration with link to go2rtc HTTP API:
- Hass > Settings > Integrations > Add Integration > RTSPtoWebRTC > `http://192.168.1.123:1984/`
- add generic camera with RTSP link:
- Hass > Settings > Integrations > Add Integration > Generic Camera > `rtsp://...`
- use Picture Entity or Picture Glance lovelace card
- open full screen card - this is should be WebRTC stream
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example:
- `http://127.0.0.1:1984/` to web interface
- `rtsp://127.0.0.1:8554/camera1` to RTSP streams
In other cases you need to use IP-address of server with **go2rtc** application.
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://...`
3. Use Picture Entity or Picture Glance lovelace card
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc**
4. Open full screen card - this is should be WebRTC stream
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.
### Module: Log
@@ -376,3 +423,29 @@ log:
streams: error
webrtc: fatal
```
## Security
By default `go2rtc` start Web interface on port `1984` and RTSP on port `8554`. Both ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
```yaml
api:
listen: "127.0.0.1:1984" # localhost
rtsp:
listen: "127.0.0.1:8554" # localhost
webrtc:
listen: ":8555" # external TCP port
```
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
- local access to API is not a problem for [Home Assistant Add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on same server and Add-on Web UI protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC 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.

24
build/hassio/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
ARG BUILD_FROM
FROM $BUILD_FROM
RUN apk add --no-cache git go ffmpeg
ARG BUILD_ARCH
RUN git clone https://github.com/AlexxIT/go2rtc \
&& cd go2rtc \
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin
# 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 -d /usr/local/bin
RUN rm -r /go2rtc
COPY run.sh /
RUN chmod a+x /run.sh
CMD [ "/run.sh" ]

6
build/hassio/config.yaml Normal file
View File

@@ -0,0 +1,6 @@
# 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 ]

14
build/hassio/run.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/with-contenv bashio
set +e
# set cwd for go2rtc (for config file, Hass itegration, 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

View File

@@ -37,14 +37,14 @@ func Init() {
HandleFunc("/api/frame.mp4", frameHandler)
HandleFunc("/api/frame.raw", frameHandler)
HandleFunc("/api/stack", stackHandler)
HandleFunc("/api/stats", statsHandler)
HandleFunc("/api/streams", streamsHandler)
HandleFunc("/api/ws", apiWS)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
@@ -52,7 +52,7 @@ func Init() {
go func() {
s := http.Server{}
if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] Serve")
log.Fatal().Err(err).Msg("[api] serve")
}
}()
}
@@ -69,16 +69,30 @@ var basePath string
var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler)
func statsHandler(w http.ResponseWriter, _ *http.Request) {
v := map[string]interface{}{
"streams": streams.All(),
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
switch r.Method {
case "PUT":
streams.Get(src)
return
case "DELETE":
streams.Delete(src)
return
}
var v interface{}
if src != "" {
v = streams.Get(src)
} else {
v = streams.All()
}
data, err := json.Marshal(v)
if err != nil {
log.Error().Err(err).Msg("[api.stats] marshal")
log.Error().Err(err).Msg("[api.streams] marshal")
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.stats] write")
log.Error().Err(err).Msg("[api.streams] write")
}
}

View File

@@ -8,8 +8,8 @@ import (
)
func frameHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return
}

View File

@@ -8,6 +8,7 @@ import (
func initStatic(staticDir string) {
var root http.FileSystem
if staticDir != "" {
log.Info().Str("dir", staticDir).Msg("[api] serve static")
root = http.Dir(staticDir)
} else {
root = http.FS(www.Static)

View File

@@ -1,6 +1,7 @@
package app
import (
"flag"
"github.com/rs/zerolog"
"gopkg.in/yaml.v3"
"io"
@@ -9,13 +10,25 @@ import (
)
func Init() {
data, _ = os.ReadFile("go2rtc.yaml")
config := flag.String(
"config",
"go2rtc.yaml",
"Path to go2rtc configuration file",
)
flag.Parse()
data, _ = os.ReadFile(*config)
var cfg struct {
Mod map[string]string `yaml:"log"`
}
LoadConfig(&cfg)
if data != nil {
if err := yaml.Unmarshal(data, &cfg); err != nil {
println("ERROR: " + err.Error())
}
}
var writer io.Writer = os.Stdout
@@ -39,7 +52,9 @@ func Init() {
modules = cfg.Mod
log.Info().Msgf("go2rtc %s/%s", runtime.GOOS, runtime.GOARCH)
path, _ := os.Getwd()
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
}
func LoadConfig(v interface{}) {

27
cmd/debug/debug.go Normal file
View File

@@ -0,0 +1,27 @@
package debug
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/http"
"os"
"strconv"
)
func Init() {
api.HandleFunc("/api/stack", stackHandler)
api.HandleFunc("/api/exit", exitHandler)
streams.HandleFunc("null", nullHandler)
}
func exitHandler(_ http.ResponseWriter, r *http.Request) {
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
func nullHandler(string) (streamer.Producer, error) {
return nil, nil
}

View File

@@ -1,4 +1,4 @@
package api
package debug
import (
"bytes"
@@ -8,19 +8,22 @@ import (
)
var stackSkip = [][]byte{
// debug.go
[]byte("github.com/AlexxIT/go2rtc/cmd/debug.handler"),
// cmd.go
[]byte("github.com/AlexxIT/go2rtc/cmd.Run"),
// main.go
[]byte("main.main()"),
[]byte("created by os/signal.Notify"),
// api.go
// api/stack.go
[]byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"),
// api/api.go
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
[]byte("created by net/http.(*connReader).startBackgroundRead"),
[]byte("created by net/http.(*Server).Serve"),
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
// webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
}
func stackHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -70,7 +70,7 @@ func Handle(url string) (streamer.Producer, error) {
}
select {
case <-time.After(time.Second * 10):
case <-time.After(time.Second * 15):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")

View File

@@ -54,7 +54,7 @@ func Init() {
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query, _ = url.ParseQuery(s[i+1:])
query = parseQuery(s[i+1:])
s = s[:i]
}
@@ -110,3 +110,16 @@ func Init() {
return exec.Handle(s)
})
}
func parseQuery(s string) map[string][]string {
query := map[string][]string{}
for _, key := range strings.Split(s, "#") {
var value string
i := strings.IndexByte(key, '=')
if i > 0 {
key, value = key[:i], key[i+1:]
}
query[key] = append(query[key], value)
}
return query
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/streamer"
@@ -13,6 +14,7 @@ import (
"net/http"
"os"
"path"
"strings"
)
func Init() {
@@ -39,28 +41,45 @@ func Init() {
return
}
ent := new(entries)
if err = json.Unmarshal(data, ent); err != nil {
storage := new(entries)
if err = json.Unmarshal(data, storage); err != nil {
return
}
urls := map[string]string{}
for _, entrie := range ent.Data.Entries {
switch entrie.Domain {
case "generic":
if entrie.Options.StreamSource != "" {
urls[entrie.Title] = entrie.Options.StreamSource
}
}
}
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
if hurl := urls[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
}
return nil, fmt.Errorf("can't get url: %s", url)
})
for _, entrie := range storage.Data.Entries {
switch entrie.Domain {
case "generic":
if entrie.Options.StreamSource == "" {
continue
}
urls[entrie.Title] = entrie.Options.StreamSource
//case "homekit_controller":
// if entrie.Data.ClientID == "" {
// continue
// }
// urls[entrie.Title] = fmt.Sprintf(
// "homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
// entrie.Data.DeviceHost, entrie.Data.DevicePort,
// entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
// entrie.Data.DeviceID, entrie.Data.DevicePublic,
// )
default:
continue
}
streams.Get("hass:" + entrie.Title)
}
}
var log zerolog.Logger
@@ -80,6 +99,15 @@ func handler(w http.ResponseWriter, r *http.Request) {
return
}
// TODO: fixme
if strings.HasPrefix(url, "rtsp://") {
port := ":" + rtsp.Port + "/"
i := strings.Index(url, port)
if i > 0 {
url = url[i+len(port):]
}
}
stream := streams.Get(url)
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {

View File

@@ -13,8 +13,8 @@ func Init() {
}
func handler(ctx *api.Context, msg *streamer.Message) {
url := ctx.Request.URL.Query().Get("url")
stream := streams.Get(url)
src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return
}

View File

@@ -65,8 +65,17 @@ func rtspHandler(url string) (streamer.Producer, error) {
if err = conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = true
if err = conn.Describe(); err != nil {
return nil, err
// second try without backchannel, we need to reconnect
if err = conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = false
if err = conn.Describe(); err != nil {
return nil, err
}
}
return conn, nil

View File

@@ -19,6 +19,9 @@ func HandleFunc(scheme string, handler Handler) {
func HasProducer(url string) bool {
i := strings.IndexByte(url, ':')
if i <= 0 { // TODO: i < 4 ?
return false
}
return handlers[url[:i]] != nil
}

View File

@@ -2,6 +2,7 @@ package streams
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"sync"
)
type state byte
@@ -21,15 +22,19 @@ type Producer struct {
tracks []*streamer.Track
state state
mx sync.Mutex
}
func (p *Producer) GetMedias() []*streamer.Media {
p.mx.Lock()
defer p.mx.Unlock()
if p.state == stateNone {
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
var err error
p.element, err = GetProducer(p.url)
if err != nil {
if err != nil || p.element == nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
return nil
}
@@ -41,6 +46,9 @@ func (p *Producer) GetMedias() []*streamer.Media {
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
p.mx.Lock()
defer p.mx.Unlock()
if p.state == stateMedias {
p.state = stateTracks
}
@@ -61,6 +69,9 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
// internals
func (p *Producer) start() {
p.mx.Lock()
defer p.mx.Unlock()
if p.state != stateTracks {
return
}
@@ -72,10 +83,18 @@ func (p *Producer) start() {
}
func (p *Producer) stop() {
p.mx.Lock()
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
_ = p.element.Stop()
p.element = nil
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
p.mx.Unlock()
}

View File

@@ -2,6 +2,7 @@ package streams
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
@@ -61,6 +62,10 @@ 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")
continue
}
// Step 5. Add track to consumer and get new track
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
@@ -74,7 +79,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// can't match tracks for consumer
if len(consumer.tracks) == 0 {
return nil
return errors.New("couldn't find the matching tracks")
}
s.consumers = append(s.consumers, consumer)
@@ -121,7 +126,7 @@ func (s *Stream) RemoveProducer(prod streamer.Producer) {
}
func (s *Stream) Active() bool {
if len(s.consumers) > 0{
if len(s.consumers) > 0 {
return true
}

View File

@@ -2,6 +2,7 @@ package streams
import (
"github.com/AlexxIT/go2rtc/pkg/fake"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/stretchr/testify/assert"
"testing"
@@ -103,7 +104,7 @@ a=control:streamid=0
func TestRouting(t *testing.T) {
prod := &fake.Producer{}
prod.Medias, _ = streamer.UnmarshalRTSPSDP([]byte(dahuaSimple))
prod.Medias, _ = rtsp.UnmarshalSDP([]byte(dahuaSimple))
assert.Len(t, prod.Medias, 3)
HandleFunc("fake", func(url string) (streamer.Producer, error) {

View File

@@ -34,14 +34,19 @@ func Get(name string) *Stream {
return nil
}
func Delete(name string) {
delete(streams, name)
}
func All() map[string]interface{} {
active := map[string]interface{}{}
all := map[string]interface{}{}
for name, stream := range streams {
if stream.Active() {
active[name] = stream
}
all[name] = stream
//if stream.Active() {
// all[name] = stream
//}
}
return active
return all
}
var log zerolog.Logger

75
cmd/webrtc/candidates.go Normal file
View File

@@ -0,0 +1,75 @@
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
func AddCandidate(address string) {
candidates = append(candidates, address)
}
func addCanditates(answer string) (string, error) {
if len(candidates) == 0 {
return answer, nil
}
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal([]byte(answer)); err != nil {
return "", err
}
md := sd.MediaDescriptions[0]
_, end := md.Attribute("end-of-candidates")
if end {
md.Attributes = md.Attributes[:len(md.Attributes)-1]
}
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")
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
md.WithPropertyAttribute(cand)
}
if end {
md.WithPropertyAttribute("end-of-candidates")
}
data, err := sd.Marshal()
if err != nil {
return "", err
}
return string(data), nil
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
return
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
conn.Push(msg)
}
}

View File

@@ -8,10 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"io/ioutil"
"net"
"net/http"
"strings"
)
func Init() {
@@ -56,120 +53,23 @@ func Init() {
candidates = cfg.Mod.Candidates
api.HandleFunc("/api/webrtc", apiHandler)
api.HandleFunc("/api/webrtc/camera", cameraHandler)
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
}
func AddCandidate(address string) {
candidates = append(candidates, address)
}
var Port string
var log zerolog.Logger
var candidates []string
var NewPConn func() (*pion.PeerConnection, error)
func apiHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("[webrtc] read offer")
return
}
// create new webrtc instance
cons := new(webrtc.Conn)
cons.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
cons.UserAgent = r.UserAgent()
cons.Listen(func(msg interface{}) {
if msg == streamer.StateNull {
stream.RemoveConsumer(cons)
}
})
if err = stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
return
}
cons.Init()
// exchange sdp with waiting all candidates
answer, err := cons.ExchangeSDP(string(offer), true)
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Msg("[api.webrtc] send answer")
}
}
func cameraHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("[webrtc] read offer")
return
}
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
conn.UserAgent = r.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateDisconnected {
stream.RemoveConsumer(conn)
}
case streamer.Track:
//stream.AddProducer(conn)
}
})
conn.Init()
// exchange sdp with waiting all candidates
answer, err := conn.ExchangeSDP(string(offer), true)
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Msg("[api.webrtc] send answer")
}
}
func offerHandler(ctx *api.Context, msg *streamer.Message) {
name := ctx.Request.URL.Query().Get("url")
stream := streams.Get(name)
src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[webrtc] new consumer")
log.Debug().Str("src", src).Msg("[webrtc] new consumer")
var err error
@@ -208,6 +108,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
_ = conn.Conn.Close()
ctx.Error(err)
return
}
@@ -216,7 +117,11 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
answer, err := conn.GetAnswer()
//answer, err := conn.GetAnswer()
answer, err := conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
@@ -229,29 +134,6 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
Type: webrtc.MsgTypeAnswer, Value: answer,
})
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")
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
conn.Fire(&streamer.Message{
Type: webrtc.MsgTypeCandidate, Value: cand,
})
}
ctx.Consumer = conn
}
@@ -287,6 +169,7 @@ func ExchangeSDP(
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
_ = conn.Conn.Close()
return
}
@@ -295,6 +178,9 @@ func ExchangeSDP(
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
answer, err = conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
@@ -303,13 +189,3 @@ func ExchangeSDP(
return
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
return
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
conn.Push(msg)
}
}

View File

@@ -3,6 +3,7 @@ package main
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/debug"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
@@ -33,6 +34,7 @@ func main() {
mse.Init()
ngrok.Init()
debug.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

View File

@@ -3,9 +3,12 @@ package rtmp
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"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/rtmp"
"github.com/pion/rtp"
@@ -70,9 +73,36 @@ func (c *Client) Dial() (err error) {
c.tracks = append(c.tracks, track)
case av.AAC:
panic("not implemented")
// 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,
}
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
c.medias = append(c.medias, media)
track := &streamer.Track{
Codec: codec, Direction: media.Direction,
}
c.tracks = append(c.tracks, track)
default:
panic("unsupported codec")
fmt.Printf("[rtmp] unsupported codec %+v\n", stream)
}
}

View File

@@ -43,11 +43,15 @@ const (
ModeServerConsumer
)
const KeepAlive = time.Second * 25
type Conn struct {
streamer.Element
// public
Backchannel bool
Medias []*streamer.Media
Session string
UserAgent string
@@ -104,6 +108,9 @@ func (c *Conn) Dial() (err error) {
//if c.state != StateClientInit {
// panic("wrong state")
//}
if c.conn != nil && c.auth != nil {
c.auth.Reset()
}
c.conn, err = net.DialTimeout(
"tcp", c.URL.Host, 10*time.Second,
@@ -144,7 +151,9 @@ func (c *Conn) Request(req *tcp.Request) error {
}
c.sequence++
req.Header.Set("CSeq", strconv.Itoa(c.sequence))
// important to send case sensitive CSeq
// https://github.com/AlexxIT/go2rtc/issues/7
req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)}
c.auth.Write(req)
@@ -189,7 +198,7 @@ func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("wrong response on %s", req.Method)
return res, fmt.Errorf("wrong response on %s", req.Method)
}
return res, nil
@@ -254,23 +263,27 @@ func (c *Conn) Describe() error {
Method: MethodDescribe,
URL: c.URL,
Header: map[string][]string{
"Accept": {"application/sdp"},
"Require": {"www.onvif.org/ver20/backchannel"},
"Accept": {"application/sdp"},
},
}
if c.Backchannel {
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
}
res, err := c.Do(req)
if err != nil {
return err
}
// fix bug in Sonoff camera SDP "o=- 1 1 IN IP4 rom t_rtsplin"
// TODO: make some universal fix
if i := bytes.Index(res.Body, []byte("rom t_rtsplin")); i > 0 {
res.Body[i+3] = '_'
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
if err != nil {
return err
}
}
c.Medias, err = streamer.UnmarshalRTSPSDP(res.Body)
c.Medias, err = UnmarshalSDP(res.Body)
if err != nil {
return err
}
@@ -355,10 +368,23 @@ func (c *Conn) SetupMedia(
// 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;ssrc=22345682;interleaved=0-1
s := res.Header.Get("Transport")
s, ok1, ok2 := between(s, "RTP/AVP/TCP;unicast;interleaved=", "-")
if !ok1 || !ok2 {
panic("wrong response")
// TODO: rewrite
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
return nil, fmt.Errorf("wrong transport: %s", s)
}
i := strings.Index(s, "interleaved=")
if i < 0 {
return nil, fmt.Errorf("wrong transport: %s", s)
}
s = s[i+len("interleaved="):]
i = strings.IndexAny(s, "-;")
if i > 0 {
s = s[:i]
}
ch, err = strconv.Atoi(s)
@@ -425,15 +451,17 @@ func (c *Conn) Accept() error {
return err
}
if c.URL == nil {
c.URL = req.URL
c.UserAgent = req.Header.Get("User-Agent")
}
c.Fire(req)
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
switch req.Method {
case MethodOptions:
c.URL = req.URL
c.UserAgent = req.Header.Get("User-Agent")
res := &tcp.Response{
Header: map[string][]string{
"Public": {"OPTIONS, SETUP, TEARDOWN, DESCRIBE, PLAY, PAUSE, ANNOUNCE, RECORD"},
@@ -449,7 +477,7 @@ func (c *Conn) Accept() error {
return errors.New("wrong content type")
}
c.Medias, err = streamer.UnmarshalRTSPSDP(req.Body)
c.Medias, err = UnmarshalSDP(req.Body)
if err != nil {
return err
}
@@ -549,6 +577,7 @@ func (c *Conn) Handle() (err error) {
}()
//c.Fire(streamer.StatePlaying)
ts := time.Now().Add(KeepAlive)
for {
// we can read:
@@ -603,7 +632,7 @@ func (c *Conn) Handle() (err error) {
if channelID&1 == 0 {
packet := &rtp.Packet{}
if err = packet.Unmarshal(buf); err != nil {
return errors.New("wrong RTP data")
return
}
track := c.channels[channelID]
@@ -611,22 +640,34 @@ func (c *Conn) Handle() (err error) {
_ = track.WriteRTP(packet)
//return fmt.Errorf("wrong channelID: %d", channelID)
} else {
panic("wrong channelID")
continue // TODO: maybe fix this
//panic("wrong channelID")
}
} else {
msg := &RTCP{Channel: channelID}
if err = msg.Header.Unmarshal(buf); err != nil {
return errors.New("wrong RTCP data")
return
}
msg.Packets, err = rtcp.Unmarshal(buf)
if err != nil {
return errors.New("wrong RTCP data")
return
}
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)
}
}
}
@@ -686,17 +727,35 @@ type RTCP struct {
Packets []rtcp.Packet
}
func between(s, sub1, sub2 string) (res string, ok1 bool, ok2 bool) {
i := strings.Index(s, sub1)
if i >= 0 {
ok1 = true
s = s[i+len(sub1):]
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
// fix SDP header for some cameras
i := bytes.Index(rawSDP, []byte("\nm="))
if i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
medias, err = streamer.UnmarshalSDP(rawSDP)
}
if err != nil {
return nil, err
}
}
i = strings.Index(s, sub2)
if i >= 0 {
return s[:i], ok1, true
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return s, ok1, false
return medias, nil
}

View File

@@ -180,26 +180,6 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
return medias, nil
}
func UnmarshalRTSPSDP(rawSDP []byte) ([]*Media, error) {
medias, err := UnmarshalSDP(rawSDP)
if err != nil {
return nil, err
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case DirectionRecvonly, "":
media.Direction = DirectionSendonly
case DirectionSendonly:
media.Direction = DirectionRecvonly
}
}
return medias, nil
}
func MarshalSDP(medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{}

View File

@@ -3,6 +3,7 @@ package streamer
import (
"fmt"
"github.com/pion/rtp"
"sync"
)
type WriterFunc func(packet *rtp.Packet) error
@@ -12,6 +13,7 @@ type Track struct {
Codec *Codec
Direction string
Sink map[*Track]WriterFunc
mx sync.Mutex
}
func (t *Track) String() string {
@@ -21,9 +23,11 @@ func (t *Track) String() string {
}
func (t *Track) WriteRTP(p *rtp.Packet) error {
t.mx.Lock()
for _, f := range t.Sink {
_ = f(p)
}
t.mx.Unlock()
return nil
}
@@ -35,10 +39,14 @@ func (t *Track) Bind(w WriterFunc) *Track {
clone := &Track{
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
}
t.mx.Lock()
t.Sink[clone] = w
t.mx.Unlock()
return clone
}
func (t *Track) Unbind() {
t.mx.Lock()
delete(t.Sink, t)
t.mx.Unlock()
}

View File

@@ -80,6 +80,12 @@ func (a *Auth) Write(req *Request) {
}
}
func (a *Auth) Reset() {
if a.Method == AuthDigest {
a.Method = AuthUnknown
}
}
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)
if i < 0 {

View File

@@ -47,10 +47,13 @@ func ReadResponse(r *bufio.Reader) (*Response, error) {
if err != nil {
return nil, err
}
if line == "" {
return nil, errors.New("empty response on RTSP request")
}
ss := strings.SplitN(line, " ", 3)
if len(ss) != 3 {
return nil, errors.New("malformed response")
return nil, fmt.Errorf("malformed response: %s", line)
}
res := &Response{

View File

@@ -1,6 +1,7 @@
package webrtc
import (
"github.com/pion/ice/v2"
"github.com/pion/interceptor"
"github.com/pion/webrtc/v3"
"net"
@@ -21,31 +22,30 @@ func NewAPI(address string) (*webrtc.API, error) {
return nil, err
}
if address == "" {
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
webrtc.WithInterceptorRegistry(i),
), nil
}
ln, err := net.Listen("tcp", address)
if err != nil {
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
webrtc.WithInterceptorRegistry(i),
), err
}
s := webrtc.SettingEngine{
//LoggerFactory: customLoggerFactory{},
}
s.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
// disable listen on Hassio docker interfaces
s.SetInterfaceFilter(func(name string) bool {
return name != "hassio" && name != "docker0"
})
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
s.SetICETCPMux(tcpMux)
// disable mDNS listener
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
if address != "" {
ln, err := net.Listen("tcp", address)
if err == nil {
s.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
})
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
s.SetICETCPMux(tcpMux)
}
}
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),

View File

@@ -1,6 +1,7 @@
package webrtc
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/webrtc/v3"
)
@@ -57,7 +58,8 @@ func (c *Conn) Init() {
}
}
panic("something wrong")
fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
fmt.Printf("TODO: webrtc ontrack %#v\n", remote)
})
c.Conn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
@@ -75,76 +77,6 @@ func (c *Conn) Init() {
})
}
func (c *Conn) ExchangeSDP(offer string, complete bool) (answer string, err error) {
sdOffer := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer, SDP: offer,
}
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
return
}
//for _, tr := range c.Conn.GetTransceivers() {
// switch tr.Direction() {
// case webrtc.RTPTransceiverDirectionSendonly:
// // disable transceivers if we don't have track
// // make direction=inactive
// // don't really necessary, but anyway
// if tr.Sender() == nil {
// if err = tr.Stop(); err != nil {
// return
// }
// }
// case webrtc.RTPTransceiverDirectionRecvonly:
// // TODO: change codecs list
// caps := webrtc.RTPCodecCapability{
// MimeType: webrtc.MimeTypePCMU,
// ClockRate: 8000,
// }
// codecs := []webrtc.RTPCodecParameters{
// {RTPCodecCapability: caps},
// }
// if err = tr.SetCodecPreferences(codecs); err != nil {
// return
// }
// }
//}
var sdAnswer webrtc.SessionDescription
sdAnswer, err = c.Conn.CreateAnswer(nil)
if err != nil {
return
}
//var sd *sdp.SessionDescription
//sd, err = sdAnswer.Unmarshal()
//for _, media := range sd.MediaDescriptions {
// if media.MediaName.Media != "audio" {
// continue
// }
// for i, attr := range media.Attributes {
// if attr.Key == "sendonly" {
// attr.Key = "inactive"
// media.Attributes[i] = attr
// break
// }
// }
//}
//var b []byte
//b, err = sd.Marshal()
//sdAnswer.SDP = string(b)
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
return
}
if complete {
<-webrtc.GatheringCompletePromise(c.Conn)
return c.Conn.LocalDescription().SDP, nil
}
return sdAnswer.SDP, nil
}
func (c *Conn) SetOffer(offer string) (err error) {
sdOffer := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer, SDP: offer,
@@ -198,7 +130,9 @@ func (c *Conn) GetCompleteAnswer() (answer string, err error) {
func (c *Conn) remote() string {
for _, trans := range c.Conn.GetTransceivers() {
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
return pair.Remote.String()
if pair.Remote != nil {
return pair.Remote.String()
}
}
return ""
}

View File

@@ -47,6 +47,9 @@ func GetPublicIP() (net.IP, error) {
if err = c.Do(message, func(e stun.Event) { res = e }); err != nil {
return nil, err
}
if err = c.Close(); err != nil {
return nil, err
}
if res.Error != nil {
return nil, res.Error

View File

@@ -2,11 +2,18 @@
- UPX-3.96 pack broken bin for `linux_mipsel`
- UPX-3.95 pack broken bin for `mac_amd64`
- UPX windows pack is recognised by anti-viruses as malicious
- `aarch64` = `arm64`
- `armv7` = `arm`
## Virus
- https://go.dev/doc/faq#virus
- https://groups.google.com/g/golang-nuts/c/lPwiWYaApSU
## Useful links
- https://github.com/golang/go/wiki/GoArm
- https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
- https://en.wikipedia.org/wiki/AArch64
- https://stackoverflow.com/questions/22267189/what-does-the-w-flag-mean-when-passed-in-via-the-ldflags-option-to-the-go-comman

View File

@@ -2,13 +2,13 @@
@SET GOOS=windows
@SET GOARCH=amd64
@SET FILENAME=go2rtc_win64.exe
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET FILENAME=go2rtc_win64.zip
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
@SET GOOS=windows
@SET GOARCH=386
@SET FILENAME=go2rtc_win32.exe
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET FILENAME=go2rtc_win32.zip
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
@SET GOOS=linux
@SET GOARCH=amd64

View File

@@ -0,0 +1,4 @@
@SET GOOS=linux
@SET GOARCH=amd64
cd ..
go build -ldflags "-s -w" -trimpath && upx-3.96 go2rtc

View File

@@ -0,0 +1,5 @@
@ECHO OFF
@SET GOOS=linux
@SET GOARCH=mipsle
cd ..
go build -ldflags "-s -w" -trimpath && upx-3.95 go2rtc

4
scripts/build_win64.cmd Normal file
View File

@@ -0,0 +1,4 @@
@SET GOOS=windows
@SET GOARCH=amd64
cd ..
go build -ldflags "-w -s" -trimpath

View File

@@ -46,4 +46,8 @@ pc.ontrack = ev => {
video.srcObject = ev.streams[0];
}
```
```
## Useful links
- https://divtable.com/table-styler/

View File

@@ -1,44 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>go2rtc</title>
<style>
table {
background-color: white;
text-align: left;
border-collapse: collapse;
}
table td, table th {
border: 1px solid black;
padding: 5px 5px;
}
table tbody td {
font-size: 13px;
}
table thead {
background: #CFCFCF;
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
border-bottom: 3px solid black;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: black;
text-align: center;
}
.header {
padding: 5px 5px;
}
</style>
</head>
<body>
<div id="header"></div>
<table id="items"></table>
<div class="header">
<input id="src" type="text" placeholder="url">
<a id="add" href="#">add</a>
</div>
<table id="streams">
<thead>
<tr>
<th>Name</th>
<th>Online</th>
<th>Commands</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const header = document.getElementById('header');
header.innerHTML = `<a href="api/stats">stats</a>`;
const links = [
'<a href="webrtc-async.html?url={name}">webrtc-async</a>',
// '<a href="webrtc-sync.html?url={name}">webrtc-sync</a>',
'<a href="api/frame.mp4?url={name}">frame.mp4</a>',
'<a href="api/frame.raw?url={name}">frame.raw</a>',
'<a href="mse.html?url={name}">mse</a>',
'<a href="webrtc.html?src={name}">webrtc</a>',
'<a href="mse.html?src={name}">mse</a>',
'<a href="api/frame.mp4?src={name}">frame.mp4</a>',
'<a href="api/streams?src={name}">info</a>',
];
fetch(`${baseUrl}/api/stats`).then(r => {
r.json().then(data => {
const content = document.getElementById('items');
function reload() {
fetch(`${baseUrl}/api/streams`).then(r => {
r.json().then(data => {
let html = '';
for (let name in data.streams) {
let html = `<tr><td>${name || 'default'}</td>`;
links.forEach(link => {
html += `<td>${link.replace('{name}', name)}</td>`
})
html += `</tr>`;
content.innerHTML += html
}
});
})
for (const [name, value] of Object.entries(data)) {
const online = value !== null ? value.length : 0
html += `<tr><td>${name || 'default'}</td><td>${online}</td><td>`;
links.forEach(link => {
html += link.replace('{name}', encodeURIComponent(name)) + ' ';
})
html += `<a href="#" onclick="deleteStream('${name}')">delete</a>`;
html += `</td></tr>`;
}
let content = document.getElementById('streams').getElementsByTagName('tbody')[0];
content.innerHTML = html
});
})
}
function deleteStream(src) {
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src)}`, {method: 'DELETE'}).then(reload);
}
const addButton = document.querySelector('a#add');
addButton.onclick = () => {
let src = document.querySelector('input#src');
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src.value)}`, {method: 'PUT'}).then(reload);
}
reload();
</script>
</body>
</html>