Compare commits

..

48 Commits

Author SHA1 Message Date
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
Alexey Khit
f879663f55 Adds static www content to binary 2022-08-19 16:56:44 +03:00
Alexey Khit
3e1f4a0110 Update defaul ports 2022-08-19 15:41:48 +03:00
Alexey Khit
5b7e1a89d7 Adds UPX versions to build file 2022-08-19 14:58:50 +03:00
Alexey Khit
2c8f0a90f0 Adds inspired by section to readme 2022-08-19 14:57:05 +03:00
Alexey Khit
38438bbfae Update readme for scripts 2022-08-19 14:56:32 +03:00
Alexey Khit
c3b18097b9 Remove cmd.go and fix interrupts catch 2022-08-19 14:55:13 +03:00
Alexey Khit
2ecf3c4a70 Fix log wrong module warning 2022-08-19 09:12:26 +03:00
Alexey Khit
43a69531b3 Code cleanup 2022-08-19 09:09:50 +03:00
Alexey Khit
49182737c8 Adds readme to scripts 2022-08-19 09:09:13 +03:00
Alexey Khit
6d264d6336 Fix Hass integration 2022-08-19 06:55:43 +03:00
Alexey Khit
cc00633161 Adds stats to RTMP connection 2022-08-18 23:58:22 +03:00
Alexey Khit
7c23625a24 Support adding streams on the fly 2022-08-18 23:53:24 +03:00
Alexey Khit
6dceed64ed Fix RTMP to WebRTC 2022-08-18 23:25:09 +03:00
Alexey Khit
e0a3e5ae96 Fix RTMP to RTSP 2022-08-18 23:09:25 +03:00
Alexey Khit
7d064a8d33 Update readme about hass 2022-08-18 23:01:26 +03:00
Alexey Khit
95f6592571 Adds integration with default hass webrtc card 2022-08-18 22:47:48 +03:00
Alexey Khit
7cdf97c91a Adds ExchangeSDP function to webrtc module 2022-08-18 22:46:43 +03:00
Alexey Khit
e70a3629d7 Fix unknown codec type for SDP parsing 2022-08-18 22:45:59 +03:00
Alexey Khit
d8baea7741 Rewrite producer init to GetProducer func 2022-08-18 22:45:20 +03:00
Alexey Khit
69d45c3216 Make new stream function public 2022-08-18 22:44:38 +03:00
Alexey Khit
77c4590170 Adds keyframe API 2022-08-18 17:27:29 +03:00
Alexey Khit
90b37d809b Update readme about webrtc and ngrok 2022-08-18 17:26:53 +03:00
Alexey Khit
3b2d1c2728 Change ngrok candate log output 2022-08-18 17:26:11 +03:00
Alexey Khit
88a02938a5 Fix ngrok logger 2022-08-18 17:25:42 +03:00
Alexey Khit
a2ad01caad Check GetLogger module name 2022-08-18 17:24:40 +03:00
Alexey Khit
9862978bd9 Update readme 2022-08-18 13:30:24 +03:00
45 changed files with 1228 additions and 413 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"

304
README.md
View File

@@ -2,30 +2,39 @@
**go2rtc** - ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
- zero-dependency and zero-config small app for all OS (Windows, macOS, Linux, ARM, etc.)
- zero-dependency and zero-config small [app for all OS](#installation) (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
- multi-source two-way [codecs negotiation](#codecs-negotiation)
- streaming from private networks via Ngrok or SSH-tunnels
- 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](#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
- [MediaSoup](https://mediasoup.org/) framework routing idea
## Codecs negotiation
For example, you want to watch 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 browser.
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 their audio codecs doesn't match with your browser codecs
- 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
- 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**
`go2rtc` automatically match codecs for you browser and all your stream sources. This called **multi-source two-way codecs negotiation**. And this is one of the main features of this app.
**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.
**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.
```yaml
streams:
@@ -36,45 +45,113 @@ streams:
![](codecs.svg)
## 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/):
- `go2rtc_win64.exe` - Windows 64-bit
- `go2rtc_win32.exe` - Windows 32-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
Don't forget to fix the rights `chmod +x go2rtc_linux_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.
## Configuration
Create file `go2rtc.yaml` next to the app. Modules:
Create file `go2rtc.yaml` next to the app.
- [Streams](#streams)
- by default, you need to config only your `streams` links
- `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/))
### Streams
Available modules:
**go2rtc** support different stream source types. You can setup only one link as stream source or multiple.
- [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
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
- [RTSP/RTSPS](#rtsp-source) - most cameras on market
- [RTMP](#rtmp-source)
- [FFmpeg/Exec](#ffmpeg-source) - FFmpeg integration
- [Hass](#hass-source) - Home Assistant integration
### Module: Streams
#### RTSP source
**go2rtc** support different stream source types. You can config only one link as stream source or multiple.
Available source types:
- [rtsp](#source-rtsp) - most cameras on market
- [rtmp](#source-rtmp)
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [hass](#source-hass) - Home Assistant 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)
- 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:** proprietary 2-way audio standards are not supported!
```yaml
streams:
rtsp_camera: rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
```
If your camera support two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream:
If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream.
**Attention:** Dahua cameras has different capabilities for different RTSP links. For example, it has support multiple codecs for two way audio with `&proto=Onvif` in link and only one coded without it.
**Attention:** Dahua cameras has different capabilities for different RTSP links. For example, it has support multiple codecs for 2-way audio with `&proto=Onvif` in link and only one codec without it.
```yaml
streams:
onvif_camera:
dahua_camera:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
```
#### RTMP source
#### 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.
@@ -83,11 +160,11 @@ streams:
rtmp_stream: rtmp://192.168.1.123/live/camera1
```
#### FFmpeg source
#### Source: FFmpeg
You can get any stream or file or device via FFmpeg and push it to go2rtc via RTSP protocol.
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:
@@ -98,7 +175,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
@@ -107,10 +184,10 @@ 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:554/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:
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.
```yaml
ffmpeg:
@@ -134,7 +211,7 @@ ffmpeg:
aac/16000: "-codec:a aac -ar 16000 -ac 1"
```
#### Exec source
#### Source: Exec
FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol:
@@ -143,9 +220,9 @@ streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i ~/media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
```
#### Hass source
#### Source: Hass
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files.
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
- support ONLY [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
@@ -157,31 +234,101 @@ streams:
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
```
### API server
### 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
- 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
- all files from `static_dir` hosted on root path: `/`
```yaml
api:
listen: ":3000" # HTTP API port
base_path: "" # API prefix for serve on suburl
static_dir: "www" # folder for static files
listen: ":1984" # HTTP API port ("" - disabled)
base_path: "" # API prefix for serve on suburl
static_dir: "" # folder for static files (custom web interface)
```
### RTSP server
**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:
```
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 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
```yaml
rtsp:
listen: ":554"
listen: ":8554"
```
### WebRTC server
### 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.
- by default, WebRTC use two random UDP ports for each connection (for video and audio)
- you can enable one additional TCP port for all connections and use it for external access
**Static public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- add your external IP-address and external port to YAML config
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
- 192.168.1.123:8555 # ip you have problems with UDP in LAN
- stun # if you have dynamic public IP-address (auto discovery via STUN)
```
**Dynamic public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- add `stun` word and external port to YAML config
- go2rtc automatically detects your external address with STUN-server
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- stun:8555 # if you have dynamic public IP-address
```
**Private IP**
- add some TCP port to YAML config (ex. 8555)
- setup integration with [Ngrok service](#module-ngrok)
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
ngrok:
command: ...
```
**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.
**Using TURN-server**
TODO...
```yaml
webrtc:
ice_servers:
- urls: [stun:stun.l.google.com:19302]
- urls: [turn:123.123.123.123:3478]
@@ -189,21 +336,84 @@ webrtc:
credential: your_pass
```
### Ngrok
### Module: Ngrok
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
- you may need external access for two different things:
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
- Ngrok support authorization for your web interface
- Ngrok automatically adds HTTPS to your web interface
Ngrok free subscription limitations:
- you will always get random external address (not a problem for webrtc stream)
- you can forward multiple ports but use only one Ngrok app
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
**Tunnel for only WebRTC Stream**
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
```
or
**Tunnel for WebRTC and Web interface**
You need to create `ngrok.yaml` config file and add it to go2rtc config:
```yaml
ngrok:
command: ngrok start --all --config ngrok.yml
command: ngrok start --all --config ngrok.yaml
```
### Log
Ngrok config example:
```yaml
version: "2"
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
tunnels:
api:
addr: 1984 # use the same port as in go2rtc config
proto: http
basic_auth:
- admin:password # you can set login/pass for your web interface
webrtc:
addr: 8555 # use the same port as in go2rtc config
proto: tcp
```
### 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, 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
4. Open full screen card - this is should be WebRTC stream
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc**
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
You can set different log levels for different modules.
```yaml
log:

22
build/hassio/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
ARG BUILD_FROM
FROM $BUILD_FROM
RUN apk add --no-cache git go ffmpeg
ARG BUILD_ARCH
WORKDIR app
RUN git clone https://github.com/AlexxIT/go2rtc .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 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 \
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
&& unzip ngrok
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 ]

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

@@ -0,0 +1,13 @@
#!/usr/bin/with-contenv bashio
set +e
while true; do
if [ -x /config/go2rtc ]; then
/config/go2rtc -config /config/go2rtc.yaml
else
/app/go2rtc -config /config/go2rtc.yaml
fi
sleep 5
done

View File

@@ -9,6 +9,8 @@ import (
"github.com/rs/zerolog"
"net"
"net/http"
"os"
"strconv"
)
func Init() {
@@ -21,8 +23,7 @@ func Init() {
}
// default config
cfg.Mod.Listen = ":3000"
cfg.Mod.StaticDir = "www"
cfg.Mod.Listen = ":1984"
// load config from YAML
app.LoadConfig(&cfg)
@@ -34,13 +35,13 @@ func Init() {
basePath = cfg.Mod.BasePath
log = app.GetLogger("api")
if cfg.Mod.StaticDir != "" {
fileServer = http.FileServer(http.Dir(cfg.Mod.StaticDir))
HandleFunc("/", fileServerHandlder)
}
initStatic(cfg.Mod.StaticDir)
HandleFunc("/api/frame.mp4", frameHandler)
HandleFunc("/api/frame.raw", frameHandler)
HandleFunc("/api/stack", stackHandler)
HandleFunc("/api/stats", statsHandler)
HandleFunc("/api/streams", streamsHandler)
HandleFunc("/api/exit", exitHandler)
HandleFunc("/api/ws", apiWS)
// ensure we can listen without errors
@@ -68,30 +69,42 @@ func HandleWS(msgType string, handler WSHandler) {
}
var basePath string
var fileServer http.Handler
var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler)
func fileServerHandlder(w http.ResponseWriter, r *http.Request) {
if basePath != "" {
r.URL.Path = r.URL.Path[len(basePath):]
}
fileServer.ServeHTTP(w, r)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
func statsHandler(w http.ResponseWriter, _ *http.Request) {
v := map[string]interface{}{
"streams": streams.Streams,
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")
}
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
func apiWS(w http.ResponseWriter, r *http.Request) {
ctx := new(Context)
if err := ctx.Upgrade(w, r); err != nil {

40
cmd/api/keyframe.go Normal file
View File

@@ -0,0 +1,40 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/keyframe"
"net/http"
"strings"
)
func frameHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
var ch = make(chan []byte)
cons := new(keyframe.Consumer)
cons.IsMP4 = strings.HasSuffix(r.URL.Path, ".mp4")
cons.Listen(func(msg interface{}) {
switch msg.(type) {
case []byte:
ch <- msg.([]byte)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.frame] add consumer")
return
}
data := <-ch
stream.RemoveConsumer(cons)
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.frame] write")
}
}

View File

@@ -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) {

26
cmd/api/static.go Normal file
View File

@@ -0,0 +1,26 @@
package api
import (
"github.com/AlexxIT/go2rtc/www"
"net/http"
)
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)
}
fileServer := http.FileServer(root)
HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if basePath != "" {
r.URL.Path = r.URL.Path[len(basePath):]
}
fileServer.ServeHTTP(w, r)
})
}

View File

@@ -1,6 +1,7 @@
package app
import (
"flag"
"github.com/rs/zerolog"
"gopkg.in/yaml.v3"
"io"
@@ -9,7 +10,15 @@ 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"`
@@ -49,11 +58,17 @@ func LoadConfig(v interface{}) {
}
func GetLogger(module string) zerolog.Logger {
lvl, err := zerolog.ParseLevel(modules[module])
if err != nil {
return log
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err != nil {
log.Warn().Err(err).Msg("[log]")
return log
}
return log.Level(lvl)
}
return log.Level(lvl)
return log
}
// internal

View File

@@ -1,41 +0,0 @@
package cmd
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/mse"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"os"
"os/signal"
)
func Run() {
app.Init() // init config and logs
streams.Init() // load streams list
rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme
api.Init() // init HTTP API server
webrtc.Init()
mse.Init()
ngrok.Init()
c := make(chan os.Signal)
signal.Notify(c)
<-c
println("exit OK")
}

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

@@ -1,13 +1,20 @@
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/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"net/http"
"os"
"path"
"strings"
)
func Init() {
@@ -19,6 +26,15 @@ func Init() {
app.LoadConfig(&conf)
log = app.GetLogger("api")
// 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)
// support load cameras from Hass config file
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
data, err := os.ReadFile(filename)
if err != nil {
@@ -49,6 +65,58 @@ func Init() {
})
}
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
}
url := r.FormValue("url")
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
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 {
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 {

View File

@@ -12,9 +12,6 @@ import (
func Init() {
var cfg struct {
Log struct {
Level string `yaml:"ngrok"`
} `yaml:"log"`
Mod struct {
Cmd string `yaml:"command"`
} `yaml:"ngrok"`
@@ -26,7 +23,7 @@ func Init() {
return
}
log = app.GetLogger(cfg.Log.Level)
log = app.GetLogger("ngrok")
ngr, err := ngrok.NewNgrok(cfg.Mod.Cmd)
if err != nil {
@@ -49,6 +46,9 @@ func Init() {
log.Warn().Err(err).Msg("[ngrok] add candidate")
return
}
log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC")
webrtc.AddCandidate(address)
}
}

View File

@@ -18,7 +18,7 @@ func Init() {
}
// default config
conf.Mod.Listen = ":554"
conf.Mod.Listen = ":8554"
app.LoadConfig(&conf)

View File

@@ -17,6 +17,11 @@ func HandleFunc(scheme string, handler Handler) {
handlers[scheme] = handler
}
func HasProducer(url string) bool {
i := strings.IndexByte(url, ':')
return handlers[url[:i]] != nil
}
func GetProducer(url string) (streamer.Producer, error) {
i := strings.IndexByte(url, ':')
handler := handlers[url[:i]]
@@ -24,4 +29,4 @@ func GetProducer(url string) (streamer.Producer, error) {
return nil, fmt.Errorf("unsupported scheme: %s", url)
}
return handler(url)
}
}

View File

@@ -2,7 +2,6 @@ package streams
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
type state byte
@@ -26,17 +25,10 @@ type Producer struct {
func (p *Producer) GetMedias() []*streamer.Media {
if p.state == stateNone {
i := strings.IndexByte(p.url, ':')
handler := handlers[p.url[:i]]
if handler == nil {
log.Warn().Str("url", p.url).Msg("[streams] unsupported scheme")
return nil
}
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
var err error
p.element, err = handler(p.url)
p.element, err = GetProducer(p.url)
if err != nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
return nil

View File

@@ -15,7 +15,7 @@ type Stream struct {
consumers []*Consumer
}
func newStream(source interface{}) *Stream {
func NewStream(source interface{}) *Stream {
s := new(Stream)
switch source := source.(type) {
@@ -28,7 +28,7 @@ func newStream(source interface{}) *Stream {
s.producers = append(s.producers, prod)
}
case map[string]interface{}:
return newStream(source["url"])
return NewStream(source["url"])
default:
panic("wrong source type")
}
@@ -61,6 +61,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)
@@ -120,6 +124,20 @@ func (s *Stream) RemoveProducer(prod streamer.Producer) {
panic("not implemented")
}
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{}
for _, prod := range s.producers {

View File

@@ -115,7 +115,7 @@ func TestRouting(t *testing.T) {
assert.Len(t, cons.Medias, 3)
// setup stream with one producer
stream := newStream("fake:")
stream := NewStream("fake:")
// main check:
err := stream.AddConsumer(cons)

View File

@@ -5,8 +5,6 @@ import (
"github.com/rs/zerolog"
)
var Streams = map[string]*Stream{}
func Init() {
var cfg struct {
Mod map[string]interface{} `yaml:"streams"`
@@ -17,12 +15,39 @@ func Init() {
log = app.GetLogger("streams")
for name, item := range cfg.Mod {
Streams[name] = newStream(item)
streams[name] = NewStream(item)
}
}
func Get(name string) *Stream {
return Streams[name]
if stream, ok := streams[name]; ok {
return stream
}
if HasProducer(name) {
log.Info().Str("url", name).Msg("[streams] create new stream")
stream := NewStream(name)
streams[name] = stream
return stream
}
return nil
}
func Delete(name string) {
delete(streams, name)
}
func All() map[string]interface{} {
all := map[string]interface{}{}
for name, stream := range streams {
all[name] = stream
//if stream.Active() {
// all[name] = stream
//}
}
return all
}
var log zerolog.Logger
var streams = map[string]*Stream{}

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() {
@@ -34,14 +31,14 @@ 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).Msg("[webrtc] init API")
return
}
if err != nil {
log.Warn().Err(err).Msg("[webrtc] Listen")
log.Warn().Err(err).Msg("[webrtc] listen")
} else if address != "" {
log.Info().Str("addr", address).Msg("[webrtc] Listen")
log.Info().Str("addr", address).Msg("[webrtc] listen")
_, Port, _ = net.SplitHostPort(address)
}
@@ -56,113 +53,15 @@ 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) {
log.Info().Str("addr", address).Msg("[webrtc] new candidate")
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)
@@ -217,7 +116,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 {
@@ -230,36 +133,57 @@ 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:]
}
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
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
func ExchangeSDP(
stream *streams.Stream, offer string, userAgent string,
) (answer string, err error) {
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] Remote")
conn.Push(msg)
conn.UserAgent = userAgent
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case streamer.EventType:
if msg == streamer.StateNull {
stream.RemoveConsumer(conn)
}
}
})
// 1. SetOffer, so we can get remote client codecs
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer")
return
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
return
}
conn.Init()
// 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 {
log.Error().Err(err).Msg("[webrtc] get answer")
}
return
}

2
go.mod
View File

@@ -7,7 +7,6 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/pion/ice/v2 v2.2.6
github.com/pion/interceptor v0.1.11
github.com/pion/logging v0.2.2
github.com/pion/rtcp v1.2.9
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.5
@@ -25,6 +24,7 @@ require (
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.2 // indirect

37
main.go
View File

@@ -1,9 +1,42 @@
package main
import (
"github.com/AlexxIT/go2rtc/cmd"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/mse"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"os"
"os/signal"
"syscall"
)
func main() {
cmd.Run()
app.Init() // init config and logs
streams.Init() // load streams list
rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme
api.Init() // init HTTP API server
webrtc.Init()
mse.Init()
ngrok.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
println("exit OK")
}

60
pkg/h264/avc.go Normal file
View File

@@ -0,0 +1,60 @@
package h264
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
const PayloadTypeAVC = 255
func IsAVC(codec *streamer.Codec) bool {
return codec.PayloadType == PayloadTypeAVC
}
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)
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
}
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)
}
}
}

View File

@@ -2,7 +2,6 @@ package h264
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
@@ -12,25 +11,12 @@ const (
NALUTypeIFrame = 5
NALUTypeSPS = 7
NALUTypePPS = 8
PayloadTypeAVC = 255
)
func NALUType(b []byte) byte {
return b[4] & 0x1F
}
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 IsAVC(codec *streamer.Codec) bool {
return codec.PayloadType == PayloadTypeAVC
}
func GetParameterSet(fmtp string) (sps, pps []byte) {
if fmtp == "" {
return

72
pkg/keyframe/consumer.go Normal file
View File

@@ -0,0 +1,72 @@
package keyframe
import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
var annexB = []byte{0, 0, 0, 1}
type Consumer struct {
streamer.Element
IsMP4 bool
}
func (k *Consumer) GetMedias() []*streamer.Media {
// support keyframe extraction only for one coded...
codec := streamer.NewCodec(streamer.CodecH264)
return []*streamer.Media{
{
Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{codec},
},
}
}
func (k *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
// sps and pps without AVC headers
sps, pps := h264.GetParameterSet(track.Codec.FmtpLine)
push := func(packet *rtp.Packet) error {
// TODO: remove it, unnecessary
if packet.Version != h264.RTPPacketVersionAVC {
panic("wrong packet type")
}
switch h264.NALUType(packet.Payload) {
case h264.NALUTypeSPS:
sps = packet.Payload[4:] // remove AVC header
case h264.NALUTypePPS:
pps = packet.Payload[4:] // remove AVC header
case h264.NALUTypeIFrame:
if sps == nil || pps == nil {
return nil
}
var data []byte
if k.IsMP4 {
data = mp4.MarshalMP4(sps, pps, packet.Payload)
} else {
data = append(data, annexB...)
data = append(data, sps...)
data = append(data, annexB...)
data = append(data, pps...)
data = append(data, annexB...)
data = append(data, packet.Payload[4:]...)
}
k.Fire(data)
}
return nil
}
if !h264.IsAVC(track.Codec) {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}

47
pkg/mp4/helpers.go Normal file
View File

@@ -0,0 +1,47 @@
package mp4
import (
"errors"
"io"
)
type MemoryWriter struct {
buf []byte
pos int
}
func (m *MemoryWriter) Write(p []byte) (n int, err error) {
minCap := m.pos + len(p)
if minCap > cap(m.buf) { // Make sure buf has enough capacity:
buf2 := make([]byte, len(m.buf), minCap+len(p)) // add some extra
copy(buf2, m.buf)
m.buf = buf2
}
if minCap > len(m.buf) {
m.buf = m.buf[:minCap]
}
copy(m.buf[m.pos:], p)
m.pos += len(p)
return len(p), nil
}
func (m *MemoryWriter) Seek(offset int64, whence int) (int64, error) {
newPos, offs := 0, int(offset)
switch whence {
case io.SeekStart:
newPos = offs
case io.SeekCurrent:
newPos = m.pos + offs
case io.SeekEnd:
newPos = len(m.buf) + offs
}
if newPos < 0 {
return 0, errors.New("negative result pos")
}
m.pos = newPos
return int64(newPos), nil
}
func (m *MemoryWriter) Bytes() []byte {
return m.buf
}

37
pkg/mp4/muxer.go Normal file
View File

@@ -0,0 +1,37 @@
package mp4
import (
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/mp4"
"time"
)
func MarshalMP4(sps, pps, frame []byte) []byte {
writer := &MemoryWriter{}
muxer := mp4.NewMuxer(writer)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
panic(err)
}
if err = muxer.WriteHeader([]av.CodecData{stream}); err != nil {
panic(err)
}
pkt := av.Packet{
CompositionTime: time.Millisecond,
IsKeyFrame: true,
Duration: time.Second,
Data: frame,
}
if err = muxer.WritePacket(pkt); err != nil {
panic(err)
}
if err = muxer.WriteTrailer(); err != nil {
panic(err)
}
return writer.buf
}

View File

@@ -22,6 +22,8 @@ type Client struct {
conn *rtmp.Conn
closed bool
receive int
}
func NewClient(uri string) *Client {
@@ -94,6 +96,8 @@ func (c *Client) Handle() (err error) {
return
}
c.receive += len(pkt.Data)
track := c.tracks[int(pkt.Idx)]
timestamp := uint32(pkt.Time / time.Duration(track.Codec.ClockRate))

View File

@@ -1,7 +1,9 @@
package rtmp
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv"
)
func (c *Client) GetMedias() []*streamer.Media {
@@ -24,3 +26,21 @@ func (c *Client) Start() error {
func (c *Client) Stop() error {
return c.Close()
}
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(),
"url": c.URI,
}
for i, media := range c.medias {
k := "media:" + strconv.Itoa(i)
v[k] = media.String()
}
for i, track := range c.tracks {
k := "track:" + strconv.Itoa(i)
v[k] = track.String()
}
return json.Marshal(v)
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtcp"
@@ -188,7 +189,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
@@ -260,7 +261,21 @@ func (c *Conn) Describe() error {
res, err := c.Do(req)
if err != nil {
return err
if res != nil {
// if we have answer - give second chanse without onvif header
req.Header.Del("Require")
res, err = c.Do(req)
}
if err != nil {
return err
}
}
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
if err != nil {
return err
}
}
// fix bug in Sonoff camera SDP "o=- 1 1 IN IP4 rom t_rtsplin"
@@ -354,10 +369,22 @@ 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
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;unicast") {
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)
@@ -671,6 +698,11 @@ func (c *Conn) bindTrack(
return nil
}
if h264.IsAVC(track.Codec) {
wrapper := h264.RTPPay(1500)
push = wrapper(push)
}
return track.Bind(push)
}

View File

@@ -281,7 +281,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c.Name = "PCMA"
c.ClockRate = 8000
default:
panic("unknown codec")
c.Name = payloadType
}
}

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

@@ -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,
@@ -186,10 +118,21 @@ func (c *Conn) GetAnswer() (answer string, err error) {
return sdAnswer.SDP, nil
}
func (c *Conn) GetCompleteAnswer() (answer string, err error) {
if _, err = c.GetAnswer(); err != nil {
return
}
<-webrtc.GatheringCompletePromise(c.Conn)
return c.Conn.LocalDescription().SDP, nil
}
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

@@ -55,10 +55,12 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
wrapper := h264.RTPPay(1200)
push = wrapper(push)
if codec.PayloadType != 255 {
if h264.IsAVC(codec) {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track)
push = wrapper(push)
}
push = wrapper(push)
}
track = track.Bind(push)

12
scripts/README.md Normal file
View File

@@ -0,0 +1,12 @@
## Build
- UPX-3.96 pack broken bin for `linux_mipsel`
- UPX-3.95 pack broken bin for `mac_amd64`
- `aarch64` = `arm64`
- `armv7` = `arm`
## Useful links
- https://github.com/golang/go/wiki/GoArm
- https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
- https://en.wikipedia.org/wiki/AArch64

View File

@@ -3,45 +3,45 @@
@SET GOOS=windows
@SET GOARCH=amd64
@SET FILENAME=go2rtc_win64.exe
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET GOOS=windows
@SET GOARCH=386
@SET FILENAME=go2rtc_win32.exe
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET GOOS=linux
@SET GOARCH=amd64
@SET FILENAME=go2rtc_linux_amd64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET GOOS=linux
@SET GOARCH=386
@SET FILENAME=go2rtc_linux_i386
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET GOOS=linux
@SET GOARCH=arm64
@SET FILENAME=go2rtc_linux_arm64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET GOOS=linux
@SET GOARCH=arm
@SET GOARM=7
@SET FILENAME=go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET GOOS=linux
@SET GOARCH=mipsle
@SET FILENAME=go2rtc_linux_mipsel
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.95 %FILENAME%
@SET GOOS=darwin
@SET GOARCH=amd64
@SET FILENAME=go2rtc_mac_amd64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
@SET GOOS=darwin
@SET GOARCH=arm64
@SET FILENAME=go2rtc_mac_arm64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%

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

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

View File

@@ -1,43 +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>` +
`<a href="webcam.html?url=webcam">webcam</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="webrtc.html?url={name}">webrtc</a>',
'<a href="mse.html?url={name}">mse</a>',
'<a href="api/frame.mp4?url={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>

6
www/static.go Normal file
View File

@@ -0,0 +1,6 @@
package www
import "embed"
//go:embed *.html
var Static embed.FS

View File

@@ -21,11 +21,8 @@
background: black;
}
</style>
<!-- Fix bugs for example with Safari... -->
<!-- <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>-->
</head>
<body>
<!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video>
<script>
function init(stream) {
@@ -38,11 +35,7 @@
ws.onopen = () => {
console.debug('ws.onopen');
pc.createOffer({
// this is adds two media to SDP with recvonly direction
// offerToReceiveAudio: true,
// offerToReceiveVideo: true,
}).then(offer => {
pc.createOffer().then(offer => {
pc.setLocalDescription(offer).then(() => {
console.log(offer.sdp);
const msg = {type: 'webrtc/offer', value: pc.localDescription.sdp};