Compare commits

..

94 Commits

Author SHA1 Message Date
Alexey Khit b5d40caffc Update version to 1.6.0 2023-07-11 07:48:51 +03:00
Alexey Khit 1e0952be86 Fix duplicates in mDNS from Hass docker 2023-07-11 07:37:06 +03:00
Alexey Khit d5fa933772 Update external libraries 2023-07-11 00:49:49 +03:00
Alexey Khit 73bf96e123 Rewrite mDNS processing 2023-07-11 00:44:27 +03:00
Alexey Khit 4ea5a22eda Code refactoring after #510 2023-07-10 11:37:52 +03:00
Alexey Khit a79fe6041d Merge pull request #510 from horttorrell32/master 2023-07-10 10:58:11 +03:00
Alexey Khit 07440f359e Update MP4 codecs detection 2023-07-08 09:34:00 +03:00
Alexey Khit 01ef67153e Update HLS stream processing 2023-07-08 09:33:31 +03:00
Alexey Khit fded87aa33 Update stream info for MP4/MSE/HLS 2023-07-08 09:32:54 +03:00
Alexey Khit 52a4fc329c Clear html video resources on disconnect 2023-07-08 09:31:05 +03:00
Alexey Khit ce61d5759c Fix html video autoplay in some cases 2023-07-08 09:30:41 +03:00
Alexey Khit 39cc4610e3 Add ESLinter and fix JS lint problems 2023-07-08 09:30:02 +03:00
agalindo 67b25015df Update de ffmpeg test after sync 2023-07-07 17:18:01 +02:00
Galindo, Alex f0d627fa55 Revert "Add framerate parameter option to HTTPs"
This reverts commit f94cd16cb7.
2023-07-07 14:03:53 +02:00
agalindo 9809f41117 Merge branch 'master' of https://github.com/horttorrell32/go2rtc 2023-07-07 07:39:51 +02:00
Galindo, Alex 2ce72dbcca Add more ffmpeg Test 2023-07-06 16:27:23 +02:00
Alexey Khit ddfeb6fae6 Fix default bin for ffmpeg transcoding to jpeg 2023-07-06 15:22:07 +03:00
Alex X 19130a4858 Merge pull request #414 from skrashevich/230504-patch-dockerfile.hardware
Update hardware.Dockerfile for multi-platform support
2023-07-06 15:18:46 +03:00
Alexey Khit 51b494b193 Add support webrtc/tcp mode to video player 2023-07-06 15:02:39 +03:00
Alexey Khit fd3b3c9bf1 Replace MP4 stream mode to HLS mode 2023-07-06 15:02:39 +03:00
Alexey Khit fa763399c2 Improve HLS processing 2023-07-06 15:02:39 +03:00
Alexey Khit af2398c072 Move mp4 parse codecs func to pkg 2023-07-06 15:02:39 +03:00
Alexey Khit 19b0bc5f44 Update scripts readme 2023-07-06 15:02:39 +03:00
Galindo, Alex f94cd16cb7 Add framerate parameter option to HTTPs
Some HTTP (a.g. JPEG or MJPEG) needs set the input framerate explicitly.
2023-07-06 12:08:49 +02:00
Alexey Khit 3246e7284c Update API description about WebRTC 2023-07-06 11:36:42 +03:00
Alexey Khit 9339957c13 Add Ezviz to cameras experience 2023-07-06 11:29:46 +03:00
Alexey Khit 4ca397da3d Update OpenAPI link 2023-07-06 11:28:02 +03:00
Galindo, Alex f6936f7cee Allow add Input param without codecs changes.
Correct the Input Template to delete the 'input' parameter to allow set the copy codecs option, otherwise the '-vn -an' codecs option is selected.
2023-07-06 10:25:09 +02:00
Alexey Khit bdafaef7dc Add api.html webpage 2023-07-06 11:21:52 +03:00
Alexey Khit 209d7b47d9 Rewrite FFmpeg devices and add support ALSA for Linux 2023-07-04 16:47:07 +03:00
Alexey Khit 4283ae1022 Add RepackG711 func for WebRTC (fix PCMA/PCMU audio) 2023-07-04 16:47:00 +03:00
Alexey Khit c2a398211c Rewrite repack G711 func (for RTSP backchannel) 2023-07-04 16:46:54 +03:00
Alexey Khit 6c2f883f9e Fix OPUS transcoding quality 2023-07-04 10:37:40 +03:00
Alexey Khit c34f9ae2b7 Support FFmpeg drawtext param #487 2023-07-03 00:46:35 +03:00
Alexey Khit c29dd8c4e3 Support templates for FFmpeg raw param #487 2023-07-03 00:44:25 +03:00
Alexey Khit 9e65f18e08 Add interactive OpenAPI to readme 2023-07-02 20:51:40 +03:00
Alexey Khit db3fb72ac8 Add OpenAPI 2023-07-02 20:47:31 +03:00
Alexey Khit 90cdfafcf5 Add Content-Type to WebRTC API 2023-07-02 20:46:56 +03:00
Alexey Khit fa8d4e4807 Remove on the fly stream creation for security reason 2023-06-29 22:52:59 +03:00
Alexey Khit 37abe2ce0d Code refactoring after #274 2023-06-29 22:08:17 +03:00
Alexey Khit 1c3835f2a8 Merge pull request #274 from skrashevich/fix-exit-code-on-config-save 2023-06-29 22:04:08 +03:00
Alexey Khit bc6e4f40bf Code refactoring after #352 2023-06-29 21:39:31 +03:00
Alexey Khit ac5bcda492 Merge pull request #352 from skrashevich/patch-listen-tls 2023-06-29 21:35:19 +03:00
Alexey Khit 7bd42eb55f Fix onvif discovery close port 2023-06-29 20:29:35 +03:00
Alexey Khit e4c7ffd1b4 Code refactoring after #462 2023-06-29 17:17:45 +03:00
Alexey Khit d31cf5521b Merge pull request #462 from dbuezas/dvrip/discovery 2023-06-29 17:14:09 +03:00
Alexey Khit 9de980a63c Fix config tab showing byte string instead of text #479 2023-06-29 16:16:08 +03:00
Alexey Khit 74cef13479 Fix panic after RTSP reconnect feature #433 2023-06-29 16:09:17 +03:00
Alexey Khit 887a491077 Fix panic on processing RTCP from HomeKit cameras #287 2023-06-29 11:13:27 +03:00
Alexey Khit 253fc4c915 Code refactoring for SRTP 2023-06-29 11:13:00 +03:00
Alexey Khit 3a51fa2397 Fix panic with only audio for MP4/MSE #404 2023-06-29 10:55:43 +03:00
Alexey Khit 306451f94f Fix race on pcm pack backchannel #432 2023-06-28 20:25:40 +03:00
Alexey Khit 39811d121b Fix panic on empty path in RTSP link #474 2023-06-28 20:03:09 +03:00
Alexey Khit 99b962e7bb Fix panic on empty RTSP medias #481 2023-06-28 19:59:36 +03:00
Alex X 3dd14a826c Merge pull request #461 from dbuezas/master
For dvrip video codec, only compare least significant 4 bits
2023-06-16 22:48:04 +03:00
David Buezas a99d7097b9 Revert ignoring high 4 bits and add 0x43 as an h265 code 2023-06-16 21:45:24 +02:00
Alexey Khit 4f97e119ac Update selectall checkbox on index page 2023-06-16 15:18:22 +03:00
Alex X 44ee0066a5 Merge pull request #466 from Vipas-ana/patch-1
Update index.html
2023-06-16 15:14:28 +03:00
Alexey Khit e5e899450f Fix ws service not load origin from config #469 2023-06-16 15:07:38 +03:00
Alexey Khit 05a2f53b67 Update projects using section in readme 2023-06-16 15:01:42 +03:00
Alexey Khit 63bcaa836a Merge remote-tracking branch 'origin/master' 2023-06-16 15:00:10 +03:00
Alex X ba68bcb89e Merge pull request #413 from skrashevich/230504-patch-hwdetect-darwin
Refactor video toolbox probe commands to use SVGA resolution in hardw…
2023-06-15 16:52:00 +03:00
Alex X 4a162c9a55 Merge pull request #471 from makuser/patch-1
Fix camera brand typo in README.md
2023-06-12 13:10:41 +03:00
Marc Kolly c2f5f37f40 Fix camera brand typo in README.md 2023-06-12 10:56:53 +02:00
Vipas-ana 11201790d2 Update index.html
Don't add blank "src" because of the checked "selectall" box.
2023-06-07 10:04:00 -04:00
David Buezas 64804cbc87 Simplify code and improve error handling 2023-06-04 22:37:29 +02:00
David Buezas 75818d6967 Add dvrip discovery 2023-06-04 02:25:22 +02:00
David Buezas 14bb4b40f7 For dvrip video codec, only compare less significant byte 2023-06-03 12:00:35 +02:00
Alex X 0fdb0b128b Merge pull request #460 from dbuezas/master
Add mediaCode 0x12 to CodecH264 identifiers ini DVIRIP stream
2023-06-03 08:07:04 +03:00
David Buezas fe28c32400 Add mediaCode 0x12 to CodecH264 identifiers ini DVIRIP stream 2023-06-01 21:22:37 +02:00
Alexey Khit 888159d2b6 Update API response mime type 2023-05-31 14:41:57 +03:00
Alexey Khit 397eb0b6ee Fix tests 2023-05-31 14:41:17 +03:00
Alexey Khit ffeb473918 Remove broken tests 2023-05-31 14:36:00 +03:00
Alexey Khit 966bedd38c Fix MP4 with PCM on Android Telegram 2023-05-30 22:03:20 +03:00
Alexey Khit 0e270081fe Add content-type to API responses 2023-05-30 22:02:16 +03:00
Alexey Khit 1612f9c81e Fix AAC inside MP4 2023-05-25 06:49:04 +03:00
Alexey Khit bff9b06d5d Add support filename query param for mp4 files 2023-05-25 06:47:51 +03:00
Alexey Khit 59555cfe1d Move WS API to separate module 2023-05-23 14:21:39 +03:00
Sergey Krashevich c94d1e237d Merge remote-tracking branch 'remotes/upstream/master' into patch-listen-tls 2023-05-21 23:29:08 +03:00
Alexey Khit 82a8e07b66 Rewrite shell signal handling 2023-05-20 06:29:29 +03:00
Alexey Khit e29307125c Add Nest source for WebRTC cameras 2023-05-20 06:28:33 +03:00
Alexey Khit 1eaacdb217 Add Hass API source for WebRTC cameras 2023-05-20 06:26:05 +03:00
Alexey Khit c09438d3d0 Set prefer_tcp flag for ffmpeg 2023-05-16 18:39:39 +03:00
Alexey Khit 8b126c0d37 Add support RTSP over WebSocket 2023-05-06 14:31:46 +03:00
Alexey Khit 3139189975 Move ParseQuery from ffmpeg to streams module 2023-05-06 14:29:35 +03:00
Alexey Khit 4fe078c7c0 ONVIF code refactoring 2023-05-05 10:07:14 +03:00
Alexey Khit 083ec127fd Fix video timestamp accuracy 2023-05-05 09:45:55 +03:00
Alexey Khit bcb9756aca Update readme about ONVIF 2023-05-05 09:08:13 +03:00
Sergey Krashevich 981974eac9 Update hardware.Dockerfile 2023-05-04 22:45:39 +03:00
Sergey Krashevich 5b29306d4f Refactor video toolbox probe commands to use SVGA resolution in hardware_darwin.go 2023-05-04 22:22:00 +03:00
Alexey Khit e89c5cb429 Add bages to readme 2023-05-04 17:45:17 +03:00
Alexey Khit 04f263aa15 Add binary for old Raspberry 1 and Zero 2023-05-04 17:40:54 +03:00
Sergey Krashevich af717b2172 add tls support 2023-04-14 18:28:03 +03:00
Sergey Krashevich 91a7b5be27 Update editor.html 2023-02-27 05:37:17 +03:00
100 changed files with 3675 additions and 1370 deletions
+2 -1
View File
@@ -41,7 +41,8 @@ FROM base
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
# alsa-plugins-pulse for ALSA support (+0MB)
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse
# font-droid for FFmpeg drawtext filter (+2MB)
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid
# Hardware Acceleration for Intel CPU (+50MB)
ARG TARGETARCH
+26 -8
View File
@@ -1,5 +1,9 @@
# go2rtc
[![](https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/stargazers)
[![](https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls)](https://hub.docker.com/r/alexxit/go2rtc)
[![](https://img.shields.io/github/downloads/AlexxIT/go2rtc/total?color=blue&style=flat-square&logo=github)](https://github.com/AlexxIT/go2rtc/releases)
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
![](assets/go2rtc.png)
@@ -98,10 +102,12 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
@@ -199,7 +205,7 @@ streams:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
amcrest_doorbell:
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
unify_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK
glichy_camera: ffmpeg:rstp://username:password@192.168.1.123/live/ch00_1
```
@@ -207,7 +213,7 @@ streams:
- **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file
- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52)
- **Unify** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)
- **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html)
- If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream
- If the stream from your camera is glitchy, try using [ffmpeg source](#source-ffmpeg). It will not add CPU load if you won't use transcoding
@@ -447,8 +453,10 @@ streams:
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
- support [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- support [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
- [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
- [ONVIF](https://www.home-assistant.io/integrations/onvif/)
- [Roborock](https://github.com/humbertogontijo/homeassistant-roborock) vacuums with camera
```yaml
hass:
@@ -459,7 +467,7 @@ streams:
aqara_g3: hass:Camera-Hub-G3-AB12
```
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
#### Source: ISAPI
@@ -572,7 +580,9 @@ POST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
go2rtc has its own JS video player (`video-rtc.js`) with:
@@ -619,7 +629,6 @@ api:
**PS:**
- go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
@@ -805,6 +814,7 @@ You have several options on how to add a camera to Home Assistant:
2. Camera [any source](#module-streams) => [go2rtc config](#configuration) => [Generic Camera](https://www.home-assistant.io/integrations/generic/)
- Install any [go2rtc](#fast-start)
- Add your stream to [go2rtc config](#configuration)
- Hass > Settings > Integrations > Add Integration > [ONVIF](https://my.home-assistant.io/redirect/config_flow_start/?domain=onvif) > Host: `127.0.0.1`, Port: `1984`
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1` (change to your stream name)
You have several options on how to watch the stream from the cameras in Home Assistant:
@@ -826,7 +836,9 @@ streams:
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
```
PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
**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.
**PS.** There is also another nice card with go2rtc support - [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card).
### Module: MP4
@@ -1049,14 +1061,20 @@ streams:
## Projects using go2rtc
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
- [Frigate Lovelace Card](https://github.com/dermotduffy/frigate-hass-card) - custom card for Home Assistant
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for control Eufy security devices
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² Module
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)
## Cameras experience
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol realisation, many bugs in SDP
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best HTTP-FLV alternative (I recommend that you contact Reolink support for new firmware), few settings
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
+486
View File
@@ -0,0 +1,486 @@
openapi: 3.0.0
info:
title: go2rtc
license: { name: MIT,url: https://opensource.org/licenses/MIT }
version: 1.0.0
contact: { url: https://github.com/AlexxIT/go2rtc }
description: |
Ultimate camera streaming application with support RTSP, RTMP, HTTP-FLV, WebRTC, MSE, HLS, MP4, MJPEG, HomeKit, FFmpeg, etc.
servers:
- url: http://localhost:1984
components:
parameters:
stream_src_path:
name: src
in: path
description: Source stream name
required: true
schema: { type: string }
example: camera1
stream_dst_path:
name: dst
in: path
description: Destination stream name
required: true
schema: { type: string }
example: camera1
stream_src_query:
name: src
in: query
description: Source stream name
required: true
schema: { type: string }
example: camera1
mp4_filter:
name: mp4
in: query
description: MP4 codecs filter
required: false
schema:
type: string
enum: [ "", flac, all ]
example: flac
video_filter:
name: video
in: query
description: Video codecs filter
schema:
type: string
enum: [ "", all, h264, h265, mjpeg ]
example: h264,h265
audio_filter:
name: audio
in: query
description: Audio codecs filter
schema:
type: string
enum: [ "", all, aac, opus, pcm, pcmu, pcma ]
example: aac
responses:
discovery:
description: ""
content:
application/json:
example: { streams: [ { "name": "Camera 1","url": "..." } ] }
webtorrent:
description: ""
content:
application/json:
example: { share: AKDypPy4zz, pwd: H0Km1HLTTP }
tags:
- name: Application
description: "[Module: API](https://github.com/AlexxIT/go2rtc#module-api)"
- name: Config
description: "[Configuration](https://github.com/AlexxIT/go2rtc#configuration)"
- name: Streams list
description: "[Module: Streams](https://github.com/AlexxIT/go2rtc#module-streams)"
- name: Consume stream
- name: Snapshot
- name: Produce stream
- name: Discovery
- name: ONVIF
- name: RTSPtoWebRTC
- name: WebTorrent
description: "[Module: WebTorrent](https://github.com/AlexxIT/go2rtc#module-webtorrent)"
- name: Debug
paths:
/api:
get:
summary: Get application info
tags: [ Application ]
responses:
"200":
description: ""
content:
application/json:
example: { config_path: "/config/go2rtc.yaml",host: "192.168.1.123:1984",rtsp: { listen: ":8554",default_query: "video&audio" },version: "1.5.0" }
/api/exit:
post:
summary: Close application
tags: [ Application ]
parameters:
- name: code
in: query
description: Application exit code
required: false
schema: { type: integer }
example: 100
responses: { }
/api/config:
get:
summary: Get main config file content
tags: [ Config ]
responses:
"200":
description: ""
content:
application/yaml: { example: "streams:..." }
post:
summary: Rewrite main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses: { }
patch:
summary: Merge changes to main config file
tags: [ Config ]
requestBody:
content:
"*/*": { example: "streams:..." }
responses: { }
/api/streams:
get:
summary: Get all streams info
tags: [ Streams list ]
responses:
"200":
description: ""
content:
application/json: { example: { camera1: { producers: [ ],consumers: [ ] } } }
put:
summary: Create new stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
- name: name
in: query
description: Stream name
required: false
schema: { type: string }
example: camera1
responses: { }
patch:
summary: Update stream source
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
- name: name
in: query
description: Stream name
required: true
schema: { type: string }
example: camera1
responses: { }
delete:
summary: Delete stream
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream name
required: true
schema: { type: string }
example: camera1
responses: { }
post:
summary: Send stream from source to destination
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
tags: [ Streams list ]
parameters:
- name: src
in: query
description: Stream source (URI)
required: true
schema: { type: string }
example: "ffmpeg:http://example.com/song.mp3#audio=pcma#input=file"
- name: dst
in: query
description: Destination stream name
required: true
schema: { type: string }
example: camera1
responses: { }
/api/streams?src={src}:
get:
summary: Get stream info in JSON format
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
"200":
description: ""
content:
application/json:
example: { producers: [ { url: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0" } ], consumers: [ ] }
/api/webrtc?src={src}:
post:
summary: Get stream in WebRTC format (WHEP)
description: "[Module: WebRTC](https://github.com/AlexxIT/go2rtc#module-webrtc)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
requestBody:
description: |
Support:
- JSON format (`Content-Type: application/json`)
- WHEP standard (`Content-Type: application/sdp`)
- raw SDP (`Content-Type: anything`)
required: true
content:
application/json: { example: { type: offer, sdp: "v=0..." } }
"application/sdp": { example: "v=0..." }
"*/*": { example: "v=0..." }
responses:
"200":
description: "Response on JSON or raw SDP"
content:
application/json: { example: { type: answer, sdp: "v=0..." } }
application/sdp: { example: "v=0..." }
"201":
description: "Response on `Content-Type: application/sdp`"
content:
application/sdp: { example: "v=0..." }
/api/stream.mp4?src={src}:
get:
summary: Get stream in MP4 format (HTTP progressive)
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
- name: duration
in: query
description: Limit the length of the stream in seconds
required: false
schema: { type: string }
example: 15
- name: filename
in: query
description: Download as a file with this name
required: false
schema: { type: string }
example: camera1.mp4
- $ref: "#/components/parameters/mp4_filter"
- $ref: "#/components/parameters/video_filter"
- $ref: "#/components/parameters/audio_filter"
responses:
200:
description: ""
content: { video/mp4: { example: "" } }
/api/stream.m3u8?src={src}:
get:
summary: Get stream in HLS format
description: "[Module: HLS](https://github.com/AlexxIT/go2rtc#module-hls)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
- $ref: "#/components/parameters/mp4_filter"
- $ref: "#/components/parameters/video_filter"
- $ref: "#/components/parameters/audio_filter"
responses:
200:
description: ""
content: { application/vnd.apple.mpegurl: { example: "" } }
/api/stream.mjpeg?src={src}:
get:
summary: Get stream in MJPEG format
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
tags: [ Consume stream ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { multipart/x-mixed-replace: { example: "" } }
/api/frame.jpeg?src={src}:
get:
summary: Get snapshot in JPEG format
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
tags: [ Snapshot ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { image/jpeg: { example: "" } }
/api/frame.mp4?src={src}:
get:
summary: Get snapshot in MP4 format
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
tags: [ Snapshot ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200:
description: ""
content: { video/mp4: { example: "" } }
/api/webrtc?dst={dst}:
post:
summary: Post stream in WebRTC format
description: "[Incoming: WebRTC/WHIP](https://github.com/AlexxIT/go2rtc#incoming-webrtcwhip)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
/api/stream.flv?dst={dst}:
post:
summary: Post stream in FLV format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
/api/stream.ts?dst={dst}:
post:
summary: Post stream in MPEG-TS format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
/api/stream.mjpeg?dst={dst}:
post:
summary: Post stream in MJPEG format
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
tags: [ Produce stream ]
parameters:
- $ref: "#/components/parameters/stream_dst_path"
responses: { }
/api/dvrip:
get:
summary: DVRIP cameras discovery
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
tags: [ Discovery ]
responses: { }
/api/ffmpeg/devices:
get:
summary: FFmpeg USB devices discovery
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
tags: [ Discovery ]
responses: { }
/api/ffmpeg/hardware:
get:
summary: FFmpeg hardware transcoding discovery
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
tags: [ Discovery ]
responses: { }
/api/hass:
get:
summary: Home Assistant cameras discovery
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
tags: [ Discovery ]
responses: { }
/api/homekit:
get:
summary: HomeKit cameras discovery
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
tags: [ Discovery ]
responses: { }
/api/nest:
get:
summary: Nest cameras discovery
tags: [ Discovery ]
responses: { }
/api/onvif:
get:
summary: ONVIF cameras discovery
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
tags: [ Discovery ]
responses: { }
/api/roborock:
get:
summary: Roborock vacuums discovery
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
tags: [ Discovery ]
responses: { }
/onvif/:
get:
summary: ONVIF server implementation
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
tags: [ ONVIF ]
responses: { }
/stream/:
get:
summary: RTSPtoWebRTC server implementation
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
tags: [ RTSPtoWebRTC ]
responses: { }
/api/webtorrent?src={src}:
get:
summary: Get WebTorrent share info
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200: { $ref: "#/components/responses/webtorrent" }
post:
summary: Add WebTorrent share
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses:
200: { $ref: "#/components/responses/webtorrent" }
delete:
summary: Delete WebTorrent share
tags: [ WebTorrent ]
parameters:
- $ref: "#/components/parameters/stream_src_path"
responses: { }
/api/webtorrent:
get:
summary: Get all WebTorrent shares info
tags: [ WebTorrent ]
responses:
200: { $ref: "#/components/responses/discovery" }
/api/stack:
get:
summary: Show list unknown goroutines
tags: [ Debug ]
responses:
200:
description: ""
content: { text/plain: { example: "" } }
+20
View File
@@ -0,0 +1,20 @@
package main
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/hass"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Init()
streams.Init()
api.Init()
hass.Init()
shell.RunUntilSignal()
}
+2 -8
View File
@@ -4,9 +4,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"os"
"os/signal"
"syscall"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
@@ -15,9 +13,5 @@ func main() {
rtsp.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
println("exit OK")
shell.RunUntilSignal()
}
+20 -24
View File
@@ -6,54 +6,50 @@ require (
github.com/brutella/hap v0.0.17
github.com/deepch/vdk v0.0.19
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/mdns v1.0.5
github.com/pion/ice/v2 v2.3.1
github.com/pion/interceptor v0.1.12
github.com/miekg/dns v1.1.55
github.com/pion/ice/v2 v2.3.9
github.com/pion/interceptor v0.1.17
github.com/pion/rtcp v1.2.10
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.6
github.com/pion/srtp/v2 v2.0.12
github.com/pion/stun v0.4.0
github.com/pion/webrtc/v3 v3.1.58
github.com/rs/zerolog v1.29.0
github.com/pion/srtp/v2 v2.0.15
github.com/pion/stun v0.6.1
github.com/pion/webrtc/v3 v3.2.12
github.com/rs/zerolog v1.29.1
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
github.com/stretchr/testify v1.8.2
github.com/stretchr/testify v1.8.4
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.5 // indirect
github.com/brutella/dnssd v1.2.9 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/miekg/dns v1.1.52 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.6 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.6 // indirect
github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/turn/v2 v2.1.0 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/pion/sctp v1.8.7 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect
github.com/pion/turn/v2 v2.1.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.11.0 // indirect
)
replace (
// windows support: https://github.com/brutella/dnssd/pull/35
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
// RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
// fix reading AAC config bytes
+62 -48
View File
@@ -3,9 +3,9 @@ github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16z
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/brutella/dnssd v1.2.5 h1:b8syhho41/5ikw3X2X4baR9NWEBSlpZnfQgujsv7bk4=
github.com/brutella/dnssd v1.2.5/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/brutella/dnssd v1.2.9 h1:eUqO0qXZAMaFN4W4Ms1AAO/OtAbNoh9U87GAlN+1FCs=
github.com/brutella/dnssd v1.2.9/go.mod h1:yZ+GHHbGhtp5yJeKTnppdFGiy6OhiPoxs0WHW1KUcFA=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -32,8 +32,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -46,12 +44,11 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c=
github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -63,12 +60,12 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc=
github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/ice/v2 v2.3.9 h1:7yZpHf3PhPxJGT4JkMj1Y8Rl5cQ6fB709iz99aeMd/U=
github.com/pion/ice/v2 v2.3.9/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
github.com/pion/interceptor v0.1.17 h1:prJtgwFh/gB8zMqGZoOgJPHivOwVAp61i2aG61Du/1w=
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
@@ -80,31 +77,31 @@ github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
github.com/pion/srtp/v2 v2.0.15 h1:+tqRtXGsGwHC0G0IUIAzRmdkHvriF79IHVfZGfHrQoA=
github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
github.com/pion/webrtc/v3 v3.1.58 h1:husXqiKQuk6gbOqJlPHs185OskAyxUW6iAEgHghgCrc=
github.com/pion/webrtc/v3 v3.1.58/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk=
github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/turn/v2 v2.1.2 h1:wj0cAoGKltaZ790XEGW9HwoUewqjliwmhtxCuB2ApyM=
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
github.com/pion/webrtc/v3 v3.2.12 h1:pVqz5NdtTqyhKIhMcXR8bPp709kCf9blyAhDjoVRLvA=
github.com/pion/webrtc/v3 v3.2.12/go.mod h1:/Oz6K95CGWaN+3No+Z0NYvgOPOr3aY8UyTlMm/dec3A=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
@@ -118,8 +115,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
@@ -132,15 +131,16 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -148,7 +148,6 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -156,15 +155,19 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -175,7 +178,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -189,13 +191,21 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -203,15 +213,19 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+7 -1
View File
@@ -12,7 +12,13 @@ FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
# 1. Build go2rtc binary
FROM go AS build
FROM --platform=$BUILDPLATFORM go AS build
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS}
ENV GOARCH=${TARGETARCH}
WORKDIR /build
-11
View File
@@ -1,11 +0,0 @@
## Go
```
go mod why github.com/pion/rtcp
go list -deps .\cmd\go2rtc_rtsp\
```
## Useful links
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro
+4
View File
@@ -0,0 +1,4 @@
## Exit codes
- https://tldp.org/LDP/abs/html/exitcodes.html
- https://komodor.com/learn/exit-codes-in-containers-and-kubernetes-the-complete-guide/
+72 -10
View File
@@ -1,7 +1,9 @@
package api
import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/rs/zerolog"
"net"
@@ -21,6 +23,9 @@ func Init() {
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
TLSListen string `yaml:"tls_listen"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
} `yaml:"api"`
}
@@ -38,12 +43,10 @@ func Init() {
log = app.GetLogger("api")
initStatic(cfg.Mod.StaticDir)
initWS(cfg.Mod.Origin)
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/ws", apiWS)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
@@ -75,8 +78,43 @@ func Init() {
log.Fatal().Err(err).Msg("[api] serve")
}
}()
// Initialize the HTTPS server
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
cert, err := tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
if err != nil {
log.Error().Err(err).Caller().Send()
return
}
tlsListener, err := net.Listen("tcp4", cfg.Mod.TLSListen)
if err != nil {
log.Fatal().Err(err).Caller().Send()
return
}
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
tlsServer := &http.Server{
Handler: Handler,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
go func() {
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
log.Fatal().Err(err).Msg("[api] tls serve")
}
}()
}
}
const (
MimeJSON = "application/json"
MimeText = "text/plain"
)
var Handler http.Handler
// HandleFunc handle pattern with relative path:
@@ -90,6 +128,33 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(pattern, handler)
}
// ResponseJSON important always add Content-Type
// so go won't need to call http.DetectContentType
func ResponseJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", MimeJSON)
_ = json.NewEncoder(w).Encode(v)
}
func ResponsePrettyJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", MimeJSON)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}
func Response(w http.ResponseWriter, body any, contentType string) {
w.Header().Set("Content-Type", contentType)
switch v := body.(type) {
case []byte:
_, _ = w.Write(v)
case string:
_, _ = w.Write([]byte(v))
default:
_, _ = fmt.Fprint(w, body)
}
}
const StreamNotFound = "stream not found"
var basePath string
@@ -133,9 +198,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
app.Info["host"] = r.Host
mu.Unlock()
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
log.Warn().Err(err).Caller().Send()
}
ResponseJSON(w, app.Info)
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
@@ -160,11 +223,10 @@ func ResponseStreams(w http.ResponseWriter, streams []Stream) {
return
}
var response struct {
var response = struct {
Streams []Stream `json:"streams"`
}{
Streams: streams,
}
response.Streams = streams
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
ResponseJSON(w, response)
}
+2 -3
View File
@@ -21,9 +21,8 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusNotFound)
return
}
if _, err = w.Write(data); err != nil {
log.Warn().Err(err).Caller().Send()
}
// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
Response(w, data, "application/yaml")
case "POST", "PATCH":
data, err := io.ReadAll(r.Body)
+19 -2
View File
@@ -1,7 +1,10 @@
package api
package ws
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"net/http"
"net/url"
"strings"
@@ -9,6 +12,20 @@ import (
"time"
)
func Init() {
var cfg struct {
Mod struct {
Origin string `yaml:"origin"`
} `yaml:"api"`
}
app.LoadConfig(&cfg)
initWS(cfg.Mod.Origin)
api.HandleFunc("api/ws", apiWS)
}
// Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
@@ -33,7 +50,7 @@ func (m *Message) GetString(key string) string {
type WSHandler func(tr *Transport, msg *Message) error
func HandleWS(msgType string, handler WSHandler) {
func HandleFunc(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"gopkg.in/yaml.v3"
)
var Version = "1.5.0"
var Version = "1.6.0"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
+2 -3
View File
@@ -3,6 +3,7 @@ package debug
import (
"bytes"
"fmt"
"github.com/AlexxIT/go2rtc/internal/api"
"net/http"
"runtime"
)
@@ -51,7 +52,5 @@ func stackHandler(w http.ResponseWriter, r *http.Request) {
"Total: %d, Skipped: %d", runtime.NumGoroutine(), skipped),
)
if _, err := w.Write(buf[:i]); err != nil {
panic(err)
}
api.Response(w, buf[:i], api.MimeText)
}
+155
View File
@@ -1,13 +1,25 @@
package dvrip
import (
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/dvrip"
"github.com/rs/zerolog/log"
)
func Init() {
streams.HandleFunc("dvrip", handle)
// DVRIP client autodiscovery
api.HandleFunc("api/dvrip", apiDvrip)
}
func handle(url string) (core.Producer, error) {
@@ -23,3 +35,146 @@ func handle(url string) (core.Producer, error) {
}
return conn, nil
}
const Port = 34569 // UDP port number for dvrip discovery
func apiDvrip(w http.ResponseWriter, r *http.Request) {
items, err := discover()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
api.ResponseStreams(w, items)
}
func discover() ([]api.Stream, error) {
addr := &net.UDPAddr{
Port: Port,
IP: net.IP{239, 255, 255, 250},
}
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
return nil, err
}
defer conn.Close()
go sendBroadcasts(conn)
var items []api.Stream
for _, info := range getResponses(conn) {
if info.HostIP == "" || info.HostName == "" {
continue
}
host, err := hexToDecimalBytes(info.HostIP)
if err != nil {
continue
}
items = append(items, api.Stream{
Name: info.HostName,
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
})
}
return items, nil
}
func sendBroadcasts(conn *net.UDPConn) {
// broadcasting the same multiple times because the devies some times don't answer
data, err := hex.DecodeString("ff00000000000000000000000000fa0500000000")
if err != nil {
return
}
addr := &net.UDPAddr{
Port: Port,
IP: net.IP{255, 255, 255, 255},
}
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
if _, err = conn.WriteToUDP(data, addr); err != nil {
log.Err(err).Caller().Send()
}
}
}
type Message struct {
NetCommon NetCommon `json:"NetWork.NetCommon"`
Ret int `json:"Ret"`
SessionID string `json:"SessionID"`
}
type NetCommon struct {
BuildDate string `json:"BuildDate"`
ChannelNum int `json:"ChannelNum"`
DeviceType int `json:"DeviceType"`
GateWay string `json:"GateWay"`
HostIP string `json:"HostIP"`
HostName string `json:"HostName"`
HttpPort int `json:"HttpPort"`
MAC string `json:"MAC"`
MonMode string `json:"MonMode"`
NetConnectState int `json:"NetConnectState"`
OtherFunction string `json:"OtherFunction"`
SN string `json:"SN"`
SSLPort int `json:"SSLPort"`
Submask string `json:"Submask"`
TCPMaxConn int `json:"TCPMaxConn"`
TCPPort int `json:"TCPPort"`
UDPPort int `json:"UDPPort"`
UseHSDownLoad bool `json:"UseHSDownLoad"`
Version string `json:"Version"`
}
func getResponses(conn *net.UDPConn) (infos []*NetCommon) {
if err := conn.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil {
return
}
var ips []net.IP // processed IPs
b := make([]byte, 4096)
loop:
for {
n, addr, err := conn.ReadFromUDP(b)
if err != nil {
break
}
for _, ip := range ips {
if ip.Equal(addr.IP) {
continue loop
}
}
if n <= 20+1 {
continue
}
var msg Message
if err = json.Unmarshal(b[20:n-1], &msg); err != nil {
continue
}
infos = append(infos, &msg.NetCommon)
ips = append(ips, addr.IP)
}
return
}
func hexToDecimalBytes(hexIP string) (string, error) {
b, err := hex.DecodeString(hexIP[2:]) // remove the '0x' prefix
if err != nil {
return "", err
}
return fmt.Sprintf("%d.%d.%d.%d", b[3], b[2], b[1], b[0]), nil
}
+28 -11
View File
@@ -3,24 +3,41 @@ package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os/exec"
"regexp"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
func deviceInputSuffix(video, audio string) string {
switch {
case video != "" && audio != "":
return `"` + video + `:` + audio + `"`
case video != "":
return `"` + video + `"`
case audio != "":
return `":` + audio + `"`
if video == "" && audio == "" {
return ""
}
return ""
// https://ffmpeg.org/ffmpeg-devices.html#avfoundation
input := "-f avfoundation"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "pixel_format", "framerate", "video_size", "capture_cursor", "capture_mouse_clicks", "capture_raw_data":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
}
return input + ` -i "` + video + `:` + audio + `"`
}
func initDevices() {
+33 -5
View File
@@ -3,19 +3,36 @@ package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os"
"os/exec"
"regexp"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func queryToInput(query url.Values) string {
if video := query.Get("video"); video != "" {
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
input := "-f v4l2"
func deviceInputSuffix(video, audio string) string {
if video != "" {
return video
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
input += " -" + key + " " + value[0]
}
}
return input + " -i " + indexToItem(videos, video)
}
if audio := query.Get("audio"); audio != "" {
input := "-f alsa"
return input + " -i " + indexToItem(audios, audio)
}
return ""
}
@@ -57,4 +74,15 @@ func initDevices() {
streams = append(streams, stream)
}
}
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
if err == nil {
stream := api.Stream{
Name: "ALSA default",
URL: "ffmpeg:device?audio=default#audio=opus",
}
audios = append(audios, "default")
streams = append(streams, stream)
}
}
+48 -2
View File
@@ -3,12 +3,58 @@ package device
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"os/exec"
"regexp"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
func queryToInput(query url.Values) string {
video := query.Get("video")
audio := query.Get("audio")
if video == "" && audio == "" {
return ""
}
// https://ffmpeg.org/ffmpeg-devices.html#dshow
input := "-f dshow"
if video != "" {
video = indexToItem(videos, video)
for key, value := range query {
switch key {
case "resolution":
input += " -video_size " + value[0]
case "video_size", "framerate", "pixel_format":
input += " -" + key + " " + value[0]
}
}
}
if audio != "" {
audio = indexToItem(audios, audio)
for key, value := range query {
switch key {
case "sample_rate", "sample_size", "channels", "audio_buffer_size":
input += " -" + key + " " + value[0]
}
}
}
if video != "" {
input += ` -i video="` + video + `"`
if audio != "" {
input += `:audio="` + audio + `"`
}
} else {
input += ` -i audio="` + audio + `"`
}
return input
}
func deviceInputSuffix(video, audio string) string {
switch {
+21 -35
View File
@@ -1,6 +1,7 @@
package device
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"net/http"
"net/url"
@@ -16,45 +17,23 @@ func Init(bin string) {
}
func GetInput(src string) (string, error) {
i := strings.IndexByte(src, '?')
if i < 0 {
return "", errors.New("empty query: " + src)
}
query, err := url.ParseQuery(src[i+1:])
if err != nil {
return "", err
}
runonce.Do(initDevices)
input := deviceInputPrefix
var video, audio string
if i := strings.IndexByte(src, '?'); i > 0 {
query, err := url.ParseQuery(src[i+1:])
if err != nil {
return "", err
}
for key, value := range query {
switch key {
case "video":
video = value[0]
case "audio":
audio = value[0]
case "resolution":
input += " -video_size " + value[0]
default: // "input_format", "framerate", "video_size"
input += " -" + key + " " + value[0]
}
}
if input := queryToInput(query); input != "" {
return input, nil
}
if video != "" {
if i, err := strconv.Atoi(video); err == nil && i < len(videos) {
video = videos[i]
}
}
if audio != "" {
if i, err := strconv.Atoi(audio); err == nil && i < len(audios) {
audio = audios[i]
}
}
input += " -i " + deviceInputSuffix(video, audio)
return input, nil
return "", errors.New("wrong query: " + src)
}
var Bin string
@@ -68,3 +47,10 @@ func apiDevices(w http.ResponseWriter, r *http.Request) {
api.ResponseStreams(w, streams)
}
func indexToItem(items []string, index string) string {
if i, err := strconv.Atoi(index); err == nil && i < len(items) {
return items[i]
}
return index
}
+40 -29
View File
@@ -2,6 +2,9 @@ package ffmpeg
import (
"errors"
"net/url"
"strings"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
@@ -9,8 +12,6 @@ import (
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"net/url"
"strings"
)
func Init() {
@@ -45,7 +46,7 @@ var defaults = map[string]string{
// inputs
"file": "-re -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
@@ -62,7 +63,9 @@ var defaults = map[string]string{
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -compression_level:a 0",
// https://github.com/pion/webrtc/issues/1514
// `-af adelay=0|0` - force frame_size=960, important for WebRTC audio quality
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -af adelay=0|0",
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
@@ -89,8 +92,8 @@ var defaults = map[string]string{
// hardware NVidia on Linux and Windows
// preset=p2 - faster, tune=ll - low latency
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto",
// hardware Intel on Windows
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
@@ -102,15 +105,21 @@ var defaults = map[string]string{
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
}
// configTemplate - return template from config (defaults) if exist or return raw template
func configTemplate(template string) string {
if s := defaults[template]; s != "" {
return s
}
return template
}
// inputTemplate - select input template from YAML config by template name
// if query has input param - select another tempalte by this name
// if query has input param - select another template by this name
// if there is no another template - use input param as template
func inputTemplate(name, s string, query url.Values) string {
var template string
if input := query.Get("input"); input != "" {
if template = defaults[input]; template == "" {
template = input
}
template = configTemplate(input)
} else {
template = defaults[name]
}
@@ -127,7 +136,7 @@ func parseArgs(s string) *ffmpeg.Args {
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:])
query = streams.ParseQuery(s[i+1:])
args.Video = len(query["video"])
args.Audio = len(query["audio"])
s = s[:i]
@@ -191,6 +200,8 @@ func parseArgs(s string) *ffmpeg.Args {
if query != nil {
// 1. Process raw params for FFmpeg
for _, raw := range query["raw"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
raw = configTemplate(raw)
args.AddCodec(raw)
}
@@ -226,6 +237,18 @@ func parseArgs(s string) *ffmpeg.Args {
}
}
for _, drawtext := range query["drawtext"] {
// support templates https://github.com/AlexxIT/go2rtc/issues/487
drawtext = configTemplate(drawtext)
// support default timestamp format
if !strings.Contains(drawtext, "text=") {
drawtext += `:text='%{localtime\:%Y-%m-%d %X}'`
}
args.AddFilter("drawtext=" + drawtext)
}
// 3. Process video codecs
if args.Video > 0 {
for _, video := range query["video"] {
@@ -239,8 +262,6 @@ func parseArgs(s string) *ffmpeg.Args {
args.AddCodec("-c:v copy")
}
}
} else {
args.AddCodec("-vn")
}
// 4. Process audio codecs
@@ -256,8 +277,6 @@ func parseArgs(s string) *ffmpeg.Args {
args.AddCodec("-c:a copy")
}
}
} else {
args.AddCodec("-an")
}
if query["hardware"] != nil {
@@ -265,8 +284,13 @@ func parseArgs(s string) *ffmpeg.Args {
}
}
if args.Codecs == nil {
switch {
case args.Video == 0 && args.Audio == 0:
args.AddCodec("-c copy")
case args.Video == 0:
args.AddCodec("-vn")
case args.Audio == 0:
args.AddCodec("-an")
}
// transcoding to only mjpeg
@@ -278,16 +302,3 @@ func parseArgs(s string) *ffmpeg.Args {
return args
}
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
}
+198 -12
View File
@@ -1,23 +1,209 @@
package ffmpeg
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseArgs(t *testing.T) {
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
func TestParseArgsFile(t *testing.T) {
// [FILE] all tracks will be copied without transcoding codecs
args := parseArgs("/media/bbb.mp4")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
// [FILE] video will be transcoded to H264, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=h264")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be copied, audio will be transcoded to pcmu
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=h265#rotate=-90")
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg")
assert.Equal(t, "ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -", args.String())
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, args.String())
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf format=vaapi|nv12,hwupload -f mjpeg -", args.String())
args = parseArgs("device?video=0&input_format=mjpeg&video_size=1920x1080")
assert.Equal(t, `ffmpeg -hide_banner -f dshow -input_format mjpeg -video_size 1920x1080 -i video="0" -c copy -f mjpeg -`, args.String())
// https://github.com/AlexxIT/go2rtc/issues/509
args = parseArgs("ffmpeg:test.mp4#raw=-ss 00:00:20")
require.Equal(t, `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsDevice(t *testing.T) {
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
args := parseArgs("device?video=0&video_size=1920x1080")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1280x720 -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsIpCam(t *testing.T) {
// [HTTP] video will be copied
args := parseArgs("http://example.com")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HTTP-MJPEG] video will be transcoded to H264
args = parseArgs("http://example.com#video=h264")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [HLS] video will be copied, audio will be skipped
args = parseArgs("https://example.com#video=copy")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video will be copied without transcoding codecs
args = parseArgs("rtsp://example.com")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP
args = parseArgs("rtsp://example.com#input=rtsp/udp")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP
args = parseArgs("rtmp://example.com#input=rtsp/udp")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsAudio(t *testing.T) {
// [AUDIO] audio will be transcoded to AAC, video will be skipped
args := parseArgs("rtsp:///example.com#audio=aac")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to AAC/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=aac/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
args = parseArgs("rtsp:///example.com#audio=opus")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -af adelay=0|0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcmu/48000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma/16000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped
args = parseArgs("rtsp:///example.com#audio=pcma/48000")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwVaapi(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwV4l2m2m(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_v4l2m2m -g 50 -bf 0 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=v4l2m2m")
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwCuda(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format nv12 -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -vf "transpose=1,transpose=1,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -vf "scale_cuda=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=cuda")
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwDxva2(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv,scale_qsv=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -re -i /media/bbb.mp4 -c:v mjpeg_qsv -profile:v high -level:v 5.1 -an -vf "hwmap=derive_device=qsv,format=qsv" -f mjpeg -`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=dxva2")
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
func TestParseArgsHwVideotoolbox(t *testing.T) {
// [HTTP-MJPEG] video will be transcoded to H264
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with rotation, should be transcoded, so select H264
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
}
+35 -17
View File
@@ -55,33 +55,51 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
switch engine {
case EngineVAAPI:
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "transpose_vaapi=4" // reversal
} else {
args.Filters[i] = "transpose_vaapi=" + filter[10:]
if !args.HasFilters("drawtext=") {
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_vaapi=" + filter[6:]
}
if strings.HasPrefix(filter, "transpose=") {
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
args.Filters[i] = "transpose_vaapi=4" // reversal
} else {
args.Filters[i] = "transpose_vaapi=" + filter[10:]
}
}
}
// fix if input doesn't support hwaccel, do nothing when support
// insert as first filter before hardware scale and transpose
args.InsertFilter("format=vaapi|nv12,hwupload")
} else {
// enable software pixel for drawtext, scale and transpose
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 " + args.Input
args.AddFilter("hwupload")
}
// fix if input doesn't support hwaccel, do nothing when support
args.InsertFilter("format=vaapi|nv12,hwupload")
case EngineCUDA:
args.Input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.Input
args.Codecs[i] = defaults[name+"/"+engine]
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_cuda=" + filter[6:]
// CUDA doesn't support hardware transpose
// https://github.com/AlexxIT/go2rtc/issues/389
if !args.HasFilters("drawtext=", "transpose=") {
args.Input = "-hwaccel cuda -hwaccel_output_format cuda " + args.Input
for i, filter := range args.Filters {
if strings.HasPrefix(filter, "scale=") {
args.Filters[i] = "scale_cuda=" + filter[6:]
}
}
} else {
args.Input = "-hwaccel cuda -hwaccel_output_format nv12 " + args.Input
args.AddFilter("hwupload")
}
case EngineDXVA2:
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
)
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2 -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_videotoolbox -f null -"
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
func ProbeAll(bin string) []api.Stream {
return []api.Stream{
+1 -1
View File
@@ -6,7 +6,7 @@ import (
)
func TranscodeToJPEG(b []byte) ([]byte, error) {
cmd := exec.Command("ffmpeg", "-hide_banner", "-i", "-", "-f", "mjpeg", "-")
cmd := exec.Command(defaults["bin"], "-hide_banner", "-i", "-", "-f", "mjpeg", "-")
cmd.Stdin = bytes.NewBuffer(b)
return cmd.Output()
}
+60 -71
View File
@@ -11,79 +11,68 @@ import (
"strings"
)
func initAPI() {
ok := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
}
func apiOK(w http.ResponseWriter, r *http.Request) {
api.Response(w, `{"status":1,"payload":{}}`, api.MimeJSON)
}
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/streams", ok)
// api from RTSPtoWeb
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
return
}
// we can get three types of links:
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
}
stream.SetSource(v.Channels.First.Url)
ok(w, r)
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
log.Warn().Msgf("wrong request: %s", r.RequestURI)
return
}
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
func apiStream(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
return
}
})
// we can get three types of links:
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
}
stream.SetSource(v.Channels.First.Url)
apiOK(w, r)
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
log.Warn().Msgf("wrong request: %s", r.RequestURI)
return
}
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
}
}
func HassioAddr() string {
+66 -18
View File
@@ -9,10 +9,12 @@ import (
"github.com/AlexxIT/go2rtc/internal/roborock"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hass"
"github.com/rs/zerolog"
"net/http"
"os"
"path"
"sync"
)
func Init() {
@@ -29,10 +31,15 @@ func Init() {
log = app.GetLogger("hass")
initAPI()
// support API for https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", apiOK)
api.HandleFunc("/streams", apiOK)
api.HandleFunc("/stream/", apiStream)
// load static entries from Hass config
if err := importConfig(conf.Mod.Config); err != nil {
log.Debug().Msgf("[hass] can't import config: %s", err)
entries := importEntries(conf.Mod.Config)
if entries == nil {
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "no hass config", http.StatusNotFound)
})
@@ -40,18 +47,35 @@ func Init() {
}
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
once.Do(func() {
// load WebRTC entities from Hass API, works only for add-on version
if token := hass.SupervisorToken(); token != "" {
if err := importWebRTC(token); err != nil {
log.Warn().Err(err).Caller().Send()
}
}
})
var items []api.Stream
for name, url := range entries {
for name, url := range entities {
items = append(items, api.Stream{Name: name, URL: url})
}
api.ResponseStreams(w, items)
})
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
if hurl := entries[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
// check entity by name
if url2 := entities[url[5:]]; url2 != "" {
return streams.GetProducer(url2)
}
return nil, fmt.Errorf("can't get url: %s", url)
// support hass://supervisor?entity_id=camera.driveway_doorbell
client, err := hass.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
})
// for Addon listen on hassio interface, so WebUI feature will work
@@ -68,12 +92,12 @@ func Init() {
}
}
func importEntries(config string) map[string]string {
func importConfig(config string) error {
// support load cameras from Hass config file
filename := path.Join(config, ".storage/core.config_entries")
b, err := os.ReadFile(filename)
if err != nil {
return nil
return err
}
var storage struct {
@@ -88,11 +112,9 @@ func importEntries(config string) map[string]string {
}
if err = json.Unmarshal(b, &storage); err != nil {
return nil
return err
}
urls := map[string]string{}
for _, entrie := range storage.Data.Entries {
switch entrie.Domain {
case "generic":
@@ -102,7 +124,7 @@ func importEntries(config string) map[string]string {
if err = json.Unmarshal(entrie.Options, &options); err != nil {
continue
}
urls[entrie.Title] = options.StreamSource
entities[entrie.Title] = options.StreamSource
case "homekit_controller":
if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) {
@@ -121,7 +143,7 @@ func importEntries(config string) map[string]string {
if err = json.Unmarshal(entrie.Data, &data); err != nil {
continue
}
urls[entrie.Title] = fmt.Sprintf(
entities[entrie.Title] = fmt.Sprintf(
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
data.DeviceHost, data.DevicePort,
data.ClientID, data.ClientPrivate, data.ClientPublic,
@@ -143,22 +165,48 @@ func importEntries(config string) map[string]string {
}
if data.Username != "" && data.Password != "" {
urls[entrie.Title] = fmt.Sprintf(
entities[entrie.Title] = fmt.Sprintf(
"onvif://%s:%s@%s:%d", data.Username, data.Password, data.Host, data.Port,
)
} else {
urls[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
entities[entrie.Title] = fmt.Sprintf("onvif://%s:%d", data.Host, data.Port)
}
default:
continue
}
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config")
//streams.Get("hass:" + entrie.Title)
}
return urls
return nil
}
func importWebRTC(token string) error {
hassAPI, err := hass.NewAPI("ws://supervisor/core/websocket", token)
if err != nil {
return err
}
webrtcEntities, err := hassAPI.GetWebRTCEntities()
if err != nil {
return err
}
if len(webrtcEntities) == 0 {
log.Debug().Msg("[hass] webrtc cameras not found")
}
for name, entityID := range webrtcEntities {
entities[name] = "hass://supervisor?entity_id=" + entityID
log.Debug().Msgf("[hass] load webrtc name=%s entity_id=%s", name, entityID)
}
return nil
}
var entities = map[string]string{}
var log zerolog.Logger
var once sync.Once
+3
View File
@@ -0,0 +1,3 @@
## Useful links
- https://walterebert.com/playground/video/hls/
+60 -58
View File
@@ -1,14 +1,15 @@
package hls
import (
"fmt"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog"
"net/http"
"strings"
"sync"
@@ -16,6 +17,8 @@ import (
)
func Init() {
log = app.GetLogger("hls")
api.HandleFunc("api/stream.m3u8", handlerStream)
api.HandleFunc("api/hls/playlist.m3u8", handlerPlaylist)
@@ -25,6 +28,8 @@ func Init() {
// HLS (fMP4)
api.HandleFunc("api/hls/init.mp4", handlerInit)
api.HandleFunc("api/hls/segment.m4s", handlerSegmentMP4)
ws.HandleFunc("hls", handlerWSHLS)
}
type Consumer interface {
@@ -35,15 +40,7 @@ type Consumer interface {
Start()
}
type Session struct {
cons Consumer
playlist string
init []byte
segment []byte
seq int
alive *time.Timer
mu sync.Mutex
}
var log zerolog.Logger
const keepalive = 5 * time.Second
@@ -63,7 +60,7 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
@@ -75,6 +72,7 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
medias := mp4.ParseQuery(r.URL.Query())
if medias != nil {
cons = &mp4.Consumer{
Desc: "HLS/HTTP",
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: medias,
@@ -86,33 +84,37 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
}
}
session := &Session{cons: cons}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.segment = append(session.segment, data...)
session.mu.Unlock()
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
session := &Session{cons: cons}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.buffer = append(session.buffer, data...)
session.mu.Unlock()
}
})
sid := core.RandString(8, 62)
session.alive = time.AfterFunc(keepalive, func() {
sessionsMu.Lock()
delete(sessions, sid)
sessionsMu.Unlock()
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
sid := core.RandString(8, 62)
// two segments important for Chromecast
if medias != nil {
session.playlist = `#EXTM3U
session.template = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
@@ -122,7 +124,7 @@ segment.m4s?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d`
} else {
session.playlist = `#EXTM3U
session.template = `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
@@ -167,9 +169,7 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
return
}
s := fmt.Sprintf(session.playlist, session.seq, session.seq, session.seq+1)
if _, err := w.Write([]byte(s)); err != nil {
if _, err := w.Write([]byte(session.Playlist())); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -194,22 +194,13 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) {
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
data := session.Segment()
if data == nil {
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
session.mu.Lock()
data := session.segment
// important to start new segment with init
session.segment = session.init
session.seq++
session.mu.Unlock()
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
@@ -233,7 +224,17 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
return
}
if _, err := w.Write(session.init); err != nil {
data := session.init
session.init = nil
session.segment0 = session.Segment()
if session.segment0 == nil {
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
@@ -243,11 +244,13 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "video/iso.segment")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Methods", "GET")
return
}
sid := r.URL.Query().Get("id")
query := r.URL.Query()
sid := query.Get("id")
sessionsMu.RLock()
session := sessions[sid]
sessionsMu.RUnlock()
@@ -258,20 +261,19 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
session.alive.Reset(keepalive)
var i byte
for len(session.segment) == 0 {
if i++; i > 10 {
http.NotFound(w, r)
return
}
time.Sleep(time.Millisecond * 100)
var data []byte
if query.Get("n") != "0" {
data = session.Segment()
} else {
data = session.segment0
}
session.mu.Lock()
data := session.segment
session.segment = nil
session.seq++
session.mu.Unlock()
if data == nil {
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
http.NotFound(w, r)
return
}
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
+41
View File
@@ -0,0 +1,41 @@
package hls
import (
"fmt"
"sync"
"time"
)
type Session struct {
cons Consumer
template string
init []byte
segment0 []byte
buffer []byte
seq int
alive *time.Timer
mu sync.Mutex
}
func (s *Session) Playlist() string {
return fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1)
}
func (s *Session) Segment() (segment []byte) {
for i := 0; i < 60 && segment == nil; i++ {
if i > 0 {
time.Sleep(50 * time.Millisecond)
}
s.mu.Lock()
if len(s.buffer) > 0 {
segment = s.buffer
// for TS important to start new segment with init
s.buffer = s.init
s.seq++
}
s.mu.Unlock()
}
return
}
+83
View File
@@ -0,0 +1,83 @@
package hls
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"strings"
"time"
)
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
codecs := msg.String()
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
cons := &mp4.Consumer{
Desc: "HLS/WebSocket",
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
Medias: mp4.ParseCodecs(codecs, true),
}
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
session := &Session{cons: cons}
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
session.mu.Lock()
session.buffer = append(session.buffer, data...)
session.mu.Unlock()
}
})
session.alive = time.AfterFunc(keepalive, func() {
stream.RemoveConsumer(cons)
})
session.init, _ = cons.Init()
cons.Start()
sid := core.RandString(8, 62)
// two segments important for Chromecast
session.template = `#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:%d
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d
#EXTINF:0.500,
segment.m4s?id=` + sid + `&n=%d`
sessionsMu.Lock()
sessions[sid] = session
sessionsMu.Unlock()
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
codecs = strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
// bandwidth important for Safari, codecs useful for smooth playback
data := `#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
hls/playlist.m3u8?id=` + sid
tr.Write(&ws.Message{Type: "hls", Value: data})
return nil
}
+20 -24
View File
@@ -1,12 +1,11 @@
package homekit
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app/store"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"net/http"
"net/url"
"strings"
@@ -32,29 +31,26 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
}
}
for info := range mdns.GetAll() {
if !strings.HasSuffix(info.Name, mdns.Suffix) {
continue
}
name := info.Name[:len(info.Name)-len(mdns.Suffix)]
device := Device{
Name: strings.ReplaceAll(name, "\\", ""),
Addr: fmt.Sprintf("%s:%d", info.AddrV4, info.Port),
}
for _, field := range info.InfoFields {
switch field[:2] {
case "id":
device.ID = field[3:]
case "md":
device.Model = field[3:]
case "sf":
device.Paired = field[3] == '0'
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
if entry.Complete() {
device := Device{
Name: entry.Name,
Addr: entry.Addr(),
ID: entry.Info["id"],
Model: entry.Info["md"],
Paired: entry.Info["sf"] == "0",
}
items = append(items, device)
}
items = append(items, device)
return false
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(items)
api.ResponseJSON(w, items)
case "POST":
// TODO: post params...
@@ -64,14 +60,14 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if err := hkPair(id, pin, name); err != nil {
log.Error().Err(err).Caller().Send()
_, err = w.Write([]byte(err.Error()))
http.Error(w, err.Error(), http.StatusInternalServerError)
}
case "DELETE":
src := r.URL.Query().Get("src")
if err := hkDelete(src); err != nil {
log.Error().Err(err).Caller().Send()
_, err = w.Write([]byte(err.Error()))
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
+7 -6
View File
@@ -3,6 +3,7 @@ package mjpeg
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -20,12 +21,12 @@ func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleWS("mjpeg", handlerWS)
ws.HandleFunc("mjpeg", handlerWS)
}
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
@@ -90,7 +91,7 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
func outputMjpeg(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
@@ -156,9 +157,9 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
stream.RemoveProducer(client)
}
func handlerWS(tr *api.Transport, _ *api.Message) error {
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
@@ -178,7 +179,7 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
return err
}
tr.Write(&api.Message{Type: "mjpeg"})
tr.Write(&ws.Message{Type: "mjpeg"})
tr.OnClose(func() {
stream.RemoveConsumer(cons)
+22 -9
View File
@@ -1,6 +1,7 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/internal/api/ws"
"net/http"
"strconv"
"strings"
@@ -17,8 +18,8 @@ import (
func Init() {
log = app.GetLogger("mp4")
api.HandleWS("mse", handlerWSMSE)
api.HandleWS("mp4", handlerWSMP4)
ws.HandleFunc("mse", handlerWSMSE)
ws.HandleFunc("mp4", handlerWSMP4)
api.HandleFunc("api/frame.mp4", handlerKeyframe)
api.HandleFunc("api/stream.mp4", handlerMP4)
@@ -37,8 +38,9 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
}
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
query := r.URL.Query()
src := query.Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
@@ -68,8 +70,13 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
stream.RemoveConsumer(cons)
// Apple Safari won't show frame without length
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("Content-Type", cons.MimeType)
header := w.Header()
header.Set("Content-Length", strconv.Itoa(len(data)))
header.Set("Content-Type", cons.MimeType)
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
@@ -94,7 +101,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
}
src := query.Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
@@ -103,6 +110,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
exit := make(chan error, 1) // Add buffer to prevent blocking
cons := &mp4.Consumer{
Desc: "MP4/HTTP",
RemoteAddr: tcp.RemoteAddr(r),
UserAgent: r.UserAgent(),
Medias: mp4.ParseQuery(r.URL.Query()),
@@ -131,8 +139,6 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
defer stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", cons.MimeType())
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Caller().Send()
@@ -140,6 +146,13 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}
header := w.Header()
header.Set("Content-Type", cons.MimeType())
if filename := query.Get("filename"); filename != "" {
header.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
http.Error(w, err.Error(), http.StatusInternalServerError)
+10 -58
View File
@@ -3,28 +3,28 @@ package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"strings"
)
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Consumer{
Desc: "MSE/WebSocket",
RemoteAddr: tcp.RemoteAddr(tr.Request),
UserAgent: tr.Request.UserAgent(),
}
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
cons.Medias = parseMedias(codecs, true)
cons.Medias = mp4.ParseCodecs(codecs, true)
}
cons.Listen(func(msg any) {
@@ -42,7 +42,7 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
stream.RemoveConsumer(cons)
})
tr.Write(&api.Message{Type: "mse", Value: cons.MimeType()})
tr.Write(&ws.Message{Type: "mse", Value: cons.MimeType()})
data, err := cons.Init()
if err != nil {
@@ -57,9 +57,9 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
return nil
}
func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
stream := streams.Get(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
@@ -72,7 +72,7 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
if codecs := msg.String(); codecs != "" {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
cons.Medias = parseMedias(codecs, false)
cons.Medias = mp4.ParseCodecs(codecs, false)
}
cons.Listen(func(msg any) {
@@ -86,7 +86,7 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return err
}
tr.Write(&api.Message{Type: "mp4", Value: cons.MimeType})
tr.Write(&ws.Message{Type: "mp4", Value: cons.MimeType})
tr.OnClose(func() {
stream.RemoveConsumer(cons)
@@ -94,51 +94,3 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return nil
}
func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
var videos []*core.Codec
var audios []*core.Codec
for _, name := range strings.Split(codecs, ",") {
switch name {
case mp4.MimeH264:
codec := &core.Codec{Name: core.CodecH264}
videos = append(videos, codec)
case mp4.MimeH265:
codec := &core.Codec{Name: core.CodecH265}
videos = append(videos, codec)
case mp4.MimeAAC:
codec := &core.Codec{Name: core.CodecAAC}
audios = append(audios, codec)
case mp4.MimeFlac:
audios = append(audios,
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
)
case mp4.MimeOpus:
codec := &core.Codec{Name: core.CodecOpus}
audios = append(audios, codec)
}
}
if videos != nil {
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil && parseAudio {
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
}
+55
View File
@@ -0,0 +1,55 @@
package nest
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/nest"
"net/http"
)
func Init() {
streams.HandleFunc("nest", streamNest)
api.HandleFunc("api/nest", apiNest)
}
func streamNest(url string) (core.Producer, error) {
client, err := nest.NewClient(url)
if err != nil {
return nil, err
}
return client, nil
}
func apiNest(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
cliendID := query.Get("client_id")
cliendSecret := query.Get("client_secret")
refreshToken := query.Get("refresh_token")
projectID := query.Get("project_id")
nestAPI, err := nest.NewAPI(cliendID, cliendSecret, refreshToken)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
devices, err := nestAPI.GetDevices(projectID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var items []api.Stream
for name, deviceID := range devices {
query.Set("device_id", deviceID)
items = append(items, api.Stream{
Name: name, URL: "nest:?" + query.Encode(),
})
}
api.ResponseStreams(w, items)
}
+12 -13
View File
@@ -91,19 +91,19 @@ var log zerolog.Logger
var handlers []Handler
var defaultMedias []*core.Media
func rtspHandler(url string) (core.Producer, error) {
backchannel := true
func rtspHandler(rawURL string) (core.Producer, error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
if i := strings.IndexByte(url, '#'); i > 0 {
if url[i+1:] == "backchannel=0" {
backchannel = false
}
url = url[:i]
}
conn := rtsp.NewClient(url)
conn := rtsp.NewClient(rawURL)
conn.Backchannel = true
conn.UserAgent = app.UserAgent
if rawQuery != "" {
query := streams.ParseQuery(rawQuery)
conn.Backchannel = query.Get("backchannel") == "1"
conn.Transport = query.Get("transport")
}
if log.Trace().Enabled() {
conn.Listen(func(msg any) {
switch msg := msg.(type) {
@@ -121,12 +121,11 @@ func rtspHandler(url string) (core.Producer, error) {
return nil, err
}
conn.Backchannel = backchannel
if err := conn.Describe(); err != nil {
if !backchannel {
if !conn.Backchannel {
return nil, err
}
log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", backchannel, err)
log.Trace().Msgf("[rtsp] describe (backchannel=%t) err: %v", conn.Backchannel, err)
// second try without backchannel, we need to reconnect
conn.Backchannel = false
+19
View File
@@ -0,0 +1,19 @@
package streams
import (
"net/url"
"strings"
)
func ParseQuery(s string) url.Values {
params := url.Values{}
for _, key := range strings.Split(s, "#") {
var value string
i := strings.IndexByte(key, '=')
if i > 0 {
key, value = key[:i], key[i+1:]
}
params[key] = append(params[key], value)
}
return params
}
+4 -21
View File
@@ -1,7 +1,6 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/app/store"
@@ -43,7 +42,7 @@ func New(name string, source any) *Stream {
func NewTemplate(name string, source any) *Stream {
// check if source links to some stream name from go2rtc
if rawURL, ok := source.(string); ok {
if u, err := url.Parse(rawURL); err == nil && u.Scheme == "rtsp" {
if u, err := url.Parse(rawURL); err == nil && u.Scheme == "rtsp" && len(u.Path) > 1 {
if stream, ok := streams[u.Path[1:]]; ok {
streams[name] = stream
return stream
@@ -54,20 +53,6 @@ func NewTemplate(name string, source any) *Stream {
return New(name, "{input}")
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok {
return stream
}
if !HasProducer(src) {
return nil
}
log.Info().Str("url", src).Msg("[streams] create new stream")
return New(src, src)
}
func GetAll() (names []string) {
for name := range streams {
names = append(names, name)
@@ -81,16 +66,14 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
// without source - return all streams list
if src == "" && r.Method != "POST" {
_ = json.NewEncoder(w).Encode(streams)
api.ResponseJSON(w, streams)
return
}
// Not sure about all this API. Should be rewrited...
switch r.Method {
case "GET":
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(streams[src])
api.ResponsePrettyJSON(w, streams[src])
case "PUT":
name := query.Get("name")
@@ -121,7 +104,7 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
if err := stream.Play(src); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
_ = json.NewEncoder(w).Encode(stream)
api.ResponseJSON(w, stream)
}
} else {
http.Error(w, "", http.StatusNotFound)
+4 -4
View File
@@ -1,7 +1,7 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
"strconv"
@@ -56,7 +56,7 @@ func GetCandidates() (candidates []string) {
return
}
func asyncCandidates(tr *api.Transport, cons *webrtc.Conn) {
func asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {
tr.WithContext(func(ctx map[any]any) {
if candidates, ok := ctx["candidate"].([]string); ok {
// process candidates that receive before this moment
@@ -74,7 +74,7 @@ func asyncCandidates(tr *api.Transport, cons *webrtc.Conn) {
for _, candidate := range GetCandidates() {
log.Trace().Str("candidate", candidate).Msg("[webrtc] config")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: candidate})
tr.Write(&ws.Message{Type: "webrtc/candidate", Value: candidate})
}
}
@@ -102,7 +102,7 @@ func syncCanditates(answer string) (string, error) {
return string(data), nil
}
func candidateHandler(tr *api.Transport, msg *api.Message) error {
func candidateHandler(tr *ws.Transport, msg *ws.Message) error {
// process incoming candidate in sync function
tr.WithContext(func(ctx map[any]any) {
candidate := msg.String()
+12 -12
View File
@@ -2,7 +2,7 @@ package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
@@ -30,13 +30,13 @@ func streamsHandler(url string) (core.Producer, error) {
// ex: ws://localhost:1984/api/ws?src=camera1
func asyncClient(url string) (core.Producer, error) {
// 1. Connect to signalign server
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
_ = ws.Close()
_ = conn.Close()
}
}()
@@ -55,14 +55,14 @@ func asyncClient(url string) (core.Producer, error) {
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
_ = ws.Close()
_ = conn.Close()
case *pion.ICECandidate:
sendOffer.Wait()
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
_ = ws.WriteJSON(&api.Message{Type: "webrtc/candidate", Value: s})
_ = conn.WriteJSON(&ws.Message{Type: "webrtc/candidate", Value: s})
}
})
@@ -79,15 +79,15 @@ func asyncClient(url string) (core.Producer, error) {
}
// 4. Send offer
msg := &api.Message{Type: "webrtc/offer", Value: offer}
if err = ws.WriteJSON(msg); err != nil {
msg := &ws.Message{Type: "webrtc/offer", Value: offer}
if err = conn.WriteJSON(msg); err != nil {
return nil, err
}
sendOffer.Done()
// 5. Get answer
if err = ws.ReadJSON(msg); err != nil {
if err = conn.ReadJSON(msg); err != nil {
return nil, err
}
@@ -104,10 +104,10 @@ func asyncClient(url string) (core.Producer, error) {
go func() {
for {
// receive data from remote
msg := new(api.Message)
if err = ws.ReadJSON(msg); err != nil {
msg := new(ws.Message)
if err = conn.ReadJSON(msg); err != nil {
if cerr, ok := err.(*websocket.CloseError); ok {
log.Trace().Err(err).Caller().Msgf("[webrtc] ws code=%d", cerr)
log.Trace().Err(err).Caller().Msgf("[webrtc] ws code=%d", cerr.Code)
}
break
}
@@ -120,7 +120,7 @@ func asyncClient(url string) (core.Producer, error) {
}
}
_ = ws.Close()
_ = conn.Close()
}()
return prod, nil
+9 -8
View File
@@ -3,6 +3,7 @@ package webrtc
import (
"errors"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
@@ -68,9 +69,9 @@ func Init() {
}
// async WebRTC server (two API versions)
api.HandleWS("webrtc", asyncHandler)
api.HandleWS("webrtc/offer", asyncHandler)
api.HandleWS("webrtc/candidate", candidateHandler)
ws.HandleFunc("webrtc", asyncHandler)
ws.HandleFunc("webrtc/offer", asyncHandler)
ws.HandleFunc("webrtc/candidate", candidateHandler)
// sync WebRTC server (two API versions)
api.HandleFunc("api/webrtc", syncHandler)
@@ -84,13 +85,13 @@ var log zerolog.Logger
var PeerConnection func(active bool) (*pion.PeerConnection, error)
func asyncHandler(tr *api.Transport, msg *api.Message) error {
func asyncHandler(tr *ws.Transport, msg *ws.Message) error {
var stream *streams.Stream
var mode core.Mode
query := tr.Request.URL.Query()
if name := query.Get("src"); name != "" {
stream = streams.GetOrNew(name)
stream = streams.Get(name)
mode = core.ModePassiveConsumer
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
} else if name = query.Get("dst"); name != "" {
@@ -134,7 +135,7 @@ func asyncHandler(tr *api.Transport, msg *api.Message) error {
s := msg.ToJSON().Candidate
log.Trace().Str("candidate", s).Msg("[webrtc] local")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
tr.Write(&ws.Message{Type: "webrtc/candidate", Value: s})
}
})
@@ -179,9 +180,9 @@ func asyncHandler(tr *api.Transport, msg *api.Message) error {
if apiV2 {
desc := pion.SessionDescription{Type: pion.SDPTypeAnswer, SDP: answer}
tr.Write(&api.Message{Type: "webrtc", Value: desc})
tr.Write(&ws.Message{Type: "webrtc", Value: desc})
} else {
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
tr.Write(&ws.Message{Type: "webrtc/answer", Value: answer})
}
sendAnswer.Done()
+2
View File
@@ -125,6 +125,8 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) {
_, err = w.Write([]byte(answer))
default:
w.Header().Set("Content-Type", mediaType)
_, err = w.Write([]byte(answer))
}
+1 -1
View File
@@ -141,7 +141,7 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
data := fmt.Sprintf(`{"share":%q,"pwd":%q}`, share, pwd)
_, _ = w.Write([]byte(data))
api.Response(w, data, api.MimeJSON)
case "DELETE":
if ok {
+48 -33
View File
@@ -2,6 +2,7 @@ package main
import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/debug"
"github.com/AlexxIT/go2rtc/internal/dvrip"
@@ -17,6 +18,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/mjpeg"
"github.com/AlexxIT/go2rtc/internal/mp4"
"github.com/AlexxIT/go2rtc/internal/mpegts"
"github.com/AlexxIT/go2rtc/internal/nest"
"github.com/AlexxIT/go2rtc/internal/ngrok"
"github.com/AlexxIT/go2rtc/internal/onvif"
"github.com/AlexxIT/go2rtc/internal/roborock"
@@ -27,46 +29,59 @@ import (
"github.com/AlexxIT/go2rtc/internal/tapo"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/internal/webtorrent"
"os"
"os/signal"
"syscall"
"github.com/AlexxIT/go2rtc/pkg/shell"
)
func main() {
app.Init() // init config and logs
api.Init() // init HTTP API server
streams.Init() // load streams list
onvif.Init()
// 1. Core modules: app, api/ws, streams
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
echo.Init()
ivideon.Init()
http.Init()
dvrip.Init()
tapo.Init()
isapi.Init()
mpegts.Init()
roborock.Init()
app.Init() // init config and logs
srtp.Init()
homekit.Init()
api.Init() // init API before all others
ws.Init() // init WS API endpoint
webrtc.Init()
mp4.Init()
hls.Init()
mjpeg.Init()
streams.Init() // streams module
webtorrent.Init()
ngrok.Init()
debug.Init()
// 2. Main sources and servers
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
rtsp.Init() // rtsp source, RTSP server
webrtc.Init() // webrtc source, WebRTC server
println("exit OK")
// 3. Main API
mp4.Init() // MP4 API
hls.Init() // HLS API
mjpeg.Init() // MJPEG API
// 4. Other sources and servers
hass.Init() // hass source, Hass API server
onvif.Init() // onvif source, ONVIF API server
webtorrent.Init() // webtorrent source, WebTorrent module
// 5. Other sources
rtmp.Init() // rtmp source
exec.Init() // exec source
ffmpeg.Init() // ffmpeg source
echo.Init() // echo source
ivideon.Init() // ivideon source
http.Init() // http/tcp source
dvrip.Init() // dvrip source
tapo.Init() // tapo source
isapi.Init() // isapi source
mpegts.Init() // mpegts passive source
roborock.Init() // roborock source
homekit.Init() // homekit source
nest.Init() // nest source
// 6. Helper modules
ngrok.Init() // Ngrok module
srtp.Init() // SRTP server
debug.Init() // debug API
// 7. Go
shell.RunUntilSignal()
}
+40
View File
@@ -0,0 +1,40 @@
{
"devDependencies": {
"eslint": "^8.44.0",
"eslint-plugin-html": "^7.1.0"
},
"eslintConfig": {
"env": {
"browser": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module"
},
"rules": {
"no-var": "error",
"no-undef": "error",
"no-unused-vars": "warn",
"prefer-const": "error",
"quotes": [
"error",
"single"
],
"semi": "error"
},
"plugins": [
"html"
],
"overrides": [
{
"files": [
"*.html"
],
"parserOptions": {
"sourceType": "script"
}
}
]
}
}
+24 -8
View File
@@ -9,6 +9,8 @@ import (
const RTPPacketVersionAAC = 0
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
var timestamp uint32
return func(packet *rtp.Packet) {
// support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
@@ -16,15 +18,29 @@ func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
data := packet.Payload[2+headersSize:]
if IsADTS(data) {
data = data[7:]
}
headers := packet.Payload[2 : 2+headersSize]
units := packet.Payload[2+headersSize:]
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = data
handler(&clone)
for len(headers) > 0 {
unitSize := binary.BigEndian.Uint16(headers) >> 3
unit := units[:unitSize]
headers = headers[2:]
units = units[unitSize:]
timestamp += 1024
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Timestamp = timestamp
if IsADTS(unit) {
clone.Payload = unit[7:]
} else {
clone.Payload = unit
}
handler(&clone)
}
}
}
+1 -2
View File
@@ -10,9 +10,8 @@ import (
)
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
// same as: uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
func Now90000() uint32 {
return uint32(time.Duration(time.Now().UnixMilli()) * 90)
return uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
}
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
+2 -2
View File
@@ -337,7 +337,7 @@ func (c *Client) ResponseJSON() (res Response, err error) {
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
var codec *core.Codec
switch mediaCode {
case 2:
case 0x02, 0x12:
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
@@ -345,7 +345,7 @@ func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
FmtpLine: h264.GetFmtpLine(payload),
}
case 0x03, 0x13:
case 0x03, 0x13, 0x43:
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
+14 -1
View File
@@ -29,6 +29,18 @@ func (a *Args) InsertFilter(filter string) {
a.Filters = append([]string{filter}, a.Filters...)
}
func (a *Args) HasFilters(filters ...string) bool {
for _, f1 := range a.Filters {
for _, f2 := range filters {
if strings.HasPrefix(f1, f2) {
return true
}
}
}
return false
}
func (a *Args) String() string {
b := bytes.NewBuffer(make([]byte, 0, 512))
@@ -65,12 +77,13 @@ func (a *Args) String() string {
if a.Filters != nil {
for i, filter := range a.Filters {
if i == 0 {
b.WriteString(" -vf ")
b.WriteString(` -vf "`)
} else {
b.WriteByte(',')
}
b.WriteString(filter)
}
b.WriteByte('"')
}
b.WriteByte(' ')
-56
View File
@@ -1,56 +0,0 @@
package ps
import (
"bytes"
"testing"
)
func TestUnmarshalSPS(t *testing.T) {
raw := []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
s := SPS{}
if err := s.Unmarshal(raw); err != nil {
t.Fatal(err)
}
raw2 := s.Marshal()
if bytes.Compare(raw, raw2) != 0 {
t.Fatal()
}
}
func TestUnmarshalPPS(t *testing.T) {
raw := []byte{0x68, 0xce, 0x38, 0x80}
p := PPS{}
if err := p.Unmarshal(raw); err != nil {
t.Fatal(err)
}
raw2 := p.Marshal()
if bytes.Compare(raw, raw2) != 0 {
t.Fatal()
}
}
func TestUnmarshalPPS2(t *testing.T) {
raw := []byte{72, 238, 60, 128}
p := PPS{}
if err := p.Unmarshal(raw); err != nil {
t.Fatal(err)
}
raw2 := p.Marshal()
if bytes.Compare(raw, raw2) != 0 {
t.Fatal()
}
}
func TestSafari(t *testing.T) {
// CB66, L3.1: chrome, edge, safari, android chrome
s := EncodeProfile(0x42, 0xE0)
t.Logf("Profile: %s, Level: %d", s, 0x1F)
// B66, L3.1: chrome, edge
s = EncodeProfile(0x42, 0x00)
t.Logf("Profile: %s, Level: %d", s, 0x1F)
// M77, L3.1: chrome, edge
s = EncodeProfile(0x4D, 0x00)
t.Logf("Profile: %s, Level: %d", s, 0x1F)
}
+22 -17
View File
@@ -8,7 +8,7 @@ import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"github.com/AlexxIT/go2rtc/pkg/mdns"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
@@ -61,28 +61,29 @@ func NewConn(rawURL string) (*Conn, error) {
}
func Pair(deviceID, pin string) (*Conn, error) {
entry := mdns.GetEntry(deviceID)
if entry == nil {
var addr string
var mfi bool
_ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
if entry.Complete() && entry.Info["id"] == deviceID {
addr = entry.Addr()
mfi = entry.Info["ff"] == "1"
return true
}
return false
})
if addr == "" {
return nil, errors.New("can't find device via mDNS")
}
c := &Conn{
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
DeviceAddress: addr,
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
var mfi bool
for _, field := range entry.InfoFields {
if field[:2] == "ff" {
if field[3] == '1' {
mfi = true
}
break
}
}
return c, c.Pair(mfi, pin)
}
@@ -106,9 +107,13 @@ func (c *Conn) DialAndServe() error {
func (c *Conn) Dial() error {
// update device host before dial
if host := mdns.GetAddress(c.DeviceID); host != "" {
c.DeviceAddress = host
}
_ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
if entry.Complete() && entry.Info["id"] == c.DeviceID {
c.DeviceAddress = entry.Addr()
return true
}
return false
})
var err error
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, time.Second*5)
-42
View File
@@ -1,42 +0,0 @@
package mdns
import (
"fmt"
"github.com/hashicorp/mdns"
"strings"
)
const Suffix = "._hap._tcp.local."
func GetAll() chan *mdns.ServiceEntry {
entries := make(chan *mdns.ServiceEntry)
params := &mdns.QueryParam{
Service: "_hap._tcp", Entries: entries, DisableIPv6: true,
}
go func() {
_ = mdns.Query(params)
close(entries)
}()
return entries
}
func GetAddress(deviceID string) string {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port)
}
}
return ""
}
func GetEntry(deviceID string) *mdns.ServiceEntry {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return entry
}
}
return nil
}
-53
View File
@@ -1,53 +0,0 @@
package mdns
import (
"github.com/hashicorp/mdns"
"net"
)
const HostHeaderTail = "._hap._tcp.local"
func NewServer(name string, port int, ips []net.IP, txt []string) (*mdns.Server, error) {
if ips == nil || ips[0] == nil {
ips = LocalIPs()
}
// important to set hostName manually with any value and `.local.` tail
// important to set ips manually
service, _ := mdns.NewMDNSService(
name, "_hap._tcp", "", name+".local.", port, ips, txt,
)
return mdns.NewServer(&mdns.Config{Zone: service})
}
func LocalIPs() []net.IP {
ifaces, err := net.Interfaces()
if err != nil {
return nil
}
var ips []net.IP
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
var addrs []net.Addr
if addrs, err = iface.Addrs(); err != nil {
continue
}
for _, addr := range addrs {
switch addr := addr.(type) {
case *net.IPNet:
ips = append(ips, addr.IP)
case *net.IPAddr:
ips = append(ips, addr.IP)
}
}
}
return ips
}
+143
View File
@@ -0,0 +1,143 @@
package hass
import (
"errors"
"github.com/gorilla/websocket"
"os"
)
type API struct {
ws *websocket.Conn
}
func NewAPI(url, token string) (*API, error) {
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return nil, err
}
api := &API{ws: ws}
if err = api.Auth(token); err != nil {
_ = ws.Close()
return nil, err
}
return api, nil
}
func (a *API) Auth(token string) error {
var res ResponseAuth
if err := a.ws.ReadJSON(&res); err != nil {
return err
}
if res.Type != "auth_required" {
return errors.New("hass: wrong type: " + res.Type)
}
s := `{"type":"auth","access_token":"` + token + `"}`
if err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
return err
}
if err := a.ws.ReadJSON(&res); err != nil {
return err
}
if res.Type != "auth_ok" {
return errors.New("hass: wrong type: " + res.Type)
}
return nil
}
func (a *API) Close() error {
return a.ws.Close()
}
func (a *API) ExchangeSDP(entityID, offer string) (string, error) {
var msg = map[string]any{
"id": 1,
"type": "camera/web_rtc_offer",
"entity_id": entityID,
"offer": offer,
}
if err := a.ws.WriteJSON(msg); err != nil {
return "", err
}
var res ResponseOffer
if err := a.ws.ReadJSON(&res); err != nil {
return "", err
}
if res.Type != "result" || !res.Success {
return "", errors.New("hass: wrong response")
}
return res.Result.Answer, nil
}
func (a *API) GetWebRTCEntities() (map[string]string, error) {
s := `{"id":1,"type":"get_states"}`
if err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {
return nil, err
}
var res ResponseStates
if err := a.ws.ReadJSON(&res); err != nil {
return nil, err
}
if res.Type != "result" || !res.Success {
return nil, errors.New("hass: wrong response")
}
entities := map[string]string{}
for _, entity := range res.Result {
if entity.Attributes.FrontendStreamType == "web_rtc" {
entities[entity.Attributes.FriendlyName] = entity.EntityId
}
}
return entities, nil
}
type ResponseAuth struct {
Type string `json:"type"`
}
type ResponseStates struct {
//Id int `json:"id"`
Type string `json:"type"`
Success bool `json:"success"`
Result []struct {
EntityId string `json:"entity_id"`
//State string `json:"state"`
Attributes struct {
//ModelName string `json:"model_name"`
//Brand string `json:"brand"`
FrontendStreamType string `json:"frontend_stream_type"`
FriendlyName string `json:"friendly_name"`
//SupportedFeatures int `json:"supported_features"`
} `json:"attributes"`
//LastChanged time.Time `json:"last_changed"`
//LastUpdated time.Time `json:"last_updated"`
//Context struct {
// Id string `json:"id"`
// ParentId interface{} `json:"parent_id"`
// UserId interface{} `json:"user_id"`
//} `json:"context"`
} `json:"result"`
}
type ResponseOffer struct {
//Id int `json:"id"`
Type string `json:"type"`
Success bool `json:"success"`
Result struct {
Answer string `json:"answer"`
} `json:"result"`
}
func SupervisorToken() string {
return os.Getenv("SUPERVISOR_TOKEN")
}
+115
View File
@@ -0,0 +1,115 @@
package hass
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"net/url"
)
type Client struct {
conn *webrtc.Conn
}
func NewClient(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
entityID := query.Get("entity_id")
if entityID == "" {
return nil, errors.New("hass: no entity_id")
}
var uri, token string
if u.Host == "supervisor" {
uri = "ws://supervisor/core/websocket"
token = SupervisorToken()
} else {
uri = "ws://" + u.Host + "/api/websocket"
token = query.Get("token")
}
if token == "" {
return nil, errors.New("hass: no token")
}
// 1. Check connection to Hass
hassAPI, err := NewAPI(uri, token)
if err != nil {
return nil, err
}
defer hassAPI.Close()
// 2. Create WebRTC client
rtcAPI, err := webrtc.NewAPI("")
if err != nil {
return nil, err
}
conf := pion.Configuration{}
pc, err := rtcAPI.NewPeerConnection(conf)
if err != nil {
return nil, err
}
conn := webrtc.NewConn(pc)
conn.Desc = "Hass"
conn.Mode = core.ModeActiveProducer
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
medias := []*core.Media{
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: "app"}, // important for Nest
}
// 3. Create offer with candidates
offer, err := conn.CreateCompleteOffer(medias)
if err != nil {
return nil, err
}
// 4. Exchange SDP via Hass
answer, err := hassAPI.ExchangeSDP(entityID, offer)
if err != nil {
return nil, err
}
// 5. Set answer with remote medias
if err = conn.SetAnswer(answer); err != nil {
return nil, err
}
return &Client{conn: conn}, nil
}
func (c *Client) GetMedias() []*core.Media {
return c.conn.GetMedias()
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
return c.conn.GetTrack(media, codec)
}
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
return c.conn.AddTrack(media, codec, track)
}
func (c *Client) Start() error {
return c.conn.Start()
}
func (c *Client) Stop() error {
return c.conn.Stop()
}
func (c *Client) MarshalJSON() ([]byte, error) {
return c.conn.MarshalJSON()
}
+1 -1
View File
@@ -39,7 +39,7 @@ const (
SampleVideoIFrame = sampleDependsOn2
SampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync
SampleAudio = sampleIsNonSync
SampleAudio = sampleDependsOn2 //sampleIsNonSync
)
func (m *Movie) WriteFileType() {
+141
View File
@@ -0,0 +1,141 @@
package mdns
import (
"fmt"
"github.com/miekg/dns"
"net"
"strings"
"time"
)
const ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol
const requestTimeout = time.Millisecond * 505
const responseTimeout = time.Second * 2
type ServiceEntry struct {
Name string
IP net.IP
Port uint16
Info map[string]string
}
func (e *ServiceEntry) Complete() bool {
return e.IP != nil && e.Port > 0 && e.Info != nil
}
func (e *ServiceEntry) Addr() string {
return fmt.Sprintf("%s:%d", e.IP, e.Port)
}
func Discovery(service string, onentry func(*ServiceEntry) bool) error {
addr := &net.UDPAddr{
IP: net.IP{224, 0, 0, 251},
Port: 5353,
}
conn, err := net.ListenMulticastUDP("udp4", nil, addr)
if err != nil {
return err
}
defer conn.Close()
if err = conn.SetDeadline(time.Now().Add(responseTimeout)); err != nil {
return err
}
msg := &dns.Msg{
Question: []dns.Question{
{service, dns.TypePTR, dns.ClassINET},
},
}
b1, err := msg.Pack()
if err != nil {
return err
}
go func() {
for {
if _, err := conn.WriteToUDP(b1, addr); err != nil {
return
}
time.Sleep(requestTimeout)
}
}()
var skipPTR []string
b2 := make([]byte, 1500)
loop:
for {
// in the Hass docker network can receive same msg from different address
n, _, err := conn.ReadFromUDP(b2)
if err != nil {
break
}
if err = msg.Unpack(b2[:n]); err != nil {
continue
}
ptr := GetPTR(msg)
if !strings.HasSuffix(ptr, service) {
continue
}
for _, s := range skipPTR {
if s == ptr {
continue loop
}
}
if entry := NewServiceEntry(msg); onentry(entry) {
break
}
skipPTR = append(skipPTR, ptr)
}
return nil
}
func GetPTR(msg *dns.Msg) string {
for _, rr := range msg.Answer {
if rr, ok := rr.(*dns.PTR); ok {
return rr.Ptr
}
}
return ""
}
func NewServiceEntry(msg *dns.Msg) *ServiceEntry {
entry := &ServiceEntry{}
records := make([]dns.RR, 0, len(msg.Answer)+len(msg.Ns)+len(msg.Extra))
records = append(records, msg.Answer...)
records = append(records, msg.Ns...)
records = append(records, msg.Extra...)
for _, record := range records {
switch record := record.(type) {
case *dns.PTR:
if i := strings.IndexByte(record.Ptr, '.'); i > 0 {
entry.Name = record.Ptr[:i]
}
case *dns.A:
entry.IP = record.A
case *dns.SRV:
entry.Port = record.Port
case *dns.TXT:
entry.Info = make(map[string]string, len(record.Txt))
for _, txt := range record.Txt {
k, v, _ := strings.Cut(txt, "=")
entry.Info[k] = v
}
}
}
return entry
}
+16
View File
@@ -0,0 +1,16 @@
package mdns
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestDiscovery(t *testing.T) {
onentry := func(entry *ServiceEntry) bool {
return true
}
err := Discovery(ServiceHAP, onentry)
//err := Discovery("_ewelink._tcp.local.", time.Second, onentry)
// err := Discovery("_googlecast._tcp.local.", time.Second, onentry)
require.Nil(t, err)
}
+20 -16
View File
@@ -14,7 +14,9 @@ import (
type Consumer struct {
core.Listener
Medias []*core.Media
Medias []*core.Media
Desc string
UserAgent string
RemoteAddr string
@@ -22,7 +24,7 @@ type Consumer struct {
muxer *Muxer
mu sync.Mutex
wait byte
state byte
send int
}
@@ -60,18 +62,16 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
switch track.Codec.Name {
case core.CodecH264:
c.wait = waitInit
handler.Handler = func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC {
return
}
if c.wait != waitNone {
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
if c.state != stateStart {
if c.state != stateInit || !h264.IsKeyframe(packet.Payload) {
return
}
c.wait = waitNone
c.state = stateStart
}
// important to use Mutex because right fragment order
@@ -89,18 +89,16 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
}
case core.CodecH265:
c.wait = waitInit
handler.Handler = func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC {
return
}
if c.wait != waitNone {
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
if c.state != stateStart {
if c.state != stateInit || !h265.IsKeyframe(packet.Payload) {
return
}
c.wait = waitNone
c.state = stateStart
}
c.mu.Lock()
@@ -116,7 +114,7 @@ func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiv
default:
handler.Handler = func(packet *rtp.Packet) {
if c.wait != waitNone {
if c.state != stateStart {
return
}
@@ -182,14 +180,20 @@ func (c *Consumer) Init() ([]byte, error) {
}
func (c *Consumer) Start() {
if c.wait == waitInit {
c.wait = waitKeyframe
for _, sender := range c.senders {
switch sender.Codec.Name {
case core.CodecH264, core.CodecH265:
c.state = stateInit
return
}
}
c.state = stateStart
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "MP4 passive consumer",
Type: c.Desc + " passive consumer",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Medias: c.Medias,
+77
View File
@@ -0,0 +1,77 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestStartH264(t *testing.T) {
codec := &core.Codec{Name: core.CodecH264}
track := core.NewReceiver(nil, codec)
packetKey := &rtp.Packet{
Header: rtp.Header{Marker: true},
Payload: []byte{h264.NALUTypeIFrame, 0, 0},
}
packetNotKey := &rtp.Packet{
Header: rtp.Header{Marker: true},
Payload: []byte{h264.NALUTypePFrame, 0, 0},
}
cons := &Consumer{}
err := cons.AddTrack(nil, nil, track)
require.Nil(t, err)
track.WriteRTP(packetKey)
time.Sleep(time.Millisecond)
_, err = cons.Init()
require.Nil(t, err)
cons.Start()
track.WriteRTP(packetNotKey)
time.Sleep(time.Millisecond)
require.Zero(t, cons.send)
track.WriteRTP(packetKey)
time.Sleep(time.Millisecond)
require.NotZero(t, cons.send)
}
func TestStartOPUS(t *testing.T) {
// Test for fix this issue
// https://github.com/AlexxIT/go2rtc/issues/404
codec := &core.Codec{Name: core.CodecOpus}
track := core.NewReceiver(nil, codec)
cons := &Consumer{}
err := cons.AddTrack(nil, nil, track)
require.Nil(t, err)
track.WriteRTP(&rtp.Packet{
Payload: []byte{0},
})
time.Sleep(time.Millisecond)
require.Zero(t, cons.send)
_, err = cons.Init()
require.Nil(t, err)
cons.Start()
track.WriteRTP(&rtp.Packet{
Payload: []byte{0},
})
time.Sleep(time.Millisecond)
require.NotZero(t, cons.send)
}
+60 -4
View File
@@ -1,6 +1,9 @@
package mp4
import "github.com/AlexxIT/go2rtc/pkg/core"
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
)
// ParseQuery - like usual parse, but with mp4 param handler
func ParseQuery(query map[string][]string) []*core.Media {
@@ -48,8 +51,61 @@ func ParseQuery(query map[string][]string) []*core.Media {
return core.ParseQuery(query)
}
func ParseCodecs(codecs string, parseAudio bool) (medias []*core.Media) {
var videos []*core.Codec
var audios []*core.Codec
loop:
for _, name := range strings.Split(codecs, ",") {
switch name {
case MimeH264:
codec := &core.Codec{Name: core.CodecH264}
videos = append(videos, codec)
case MimeH265:
codec := &core.Codec{Name: core.CodecH265}
videos = append(videos, codec)
case MimeAAC:
codec := &core.Codec{Name: core.CodecAAC}
audios = append(audios, codec)
case "null":
// this means that the browser is lying about the codecs it can play
// and we are not supposed to believe that it can flac or opus
break loop
case MimeFlac:
audios = append(audios,
&core.Codec{Name: core.CodecPCMA},
&core.Codec{Name: core.CodecPCMU},
&core.Codec{Name: core.CodecPCM},
)
case MimeOpus:
codec := &core.Codec{Name: core.CodecOpus}
audios = append(audios, codec)
}
}
if videos != nil {
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil && parseAudio {
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
}
const (
waitNone byte = iota
waitKeyframe
waitInit
stateNone byte = iota
stateInit
stateStart
)
+14 -10
View File
@@ -151,19 +151,14 @@ func (m *Muxer) Reset() {
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
codec := m.codecs[trackID]
m.fragIndex++
duration := packet.Timestamp - m.pts[trackID]
m.pts[trackID] = packet.Timestamp
// minumum duration important for MSE in Apple Safari
if duration == 0 || duration > codec.ClockRate {
duration = codec.ClockRate/1000 + 1
m.pts[trackID] += duration
}
size := len(packet.Payload)
// flags important for Apple Finder video preview
var flags uint32
switch codec.Name {
case core.CodecH264:
if h264.IsKeyframe(packet.Payload) {
@@ -177,11 +172,20 @@ func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
} else {
flags = iso.SampleVideoNonIFrame
}
case core.CodecAAC:
duration = 1024 // important for Apple Finder and QuickTime
flags = iso.SampleAudio // not important?
default:
flags = iso.SampleAudio // not important
flags = iso.SampleAudio // important for FLAC on Android Telegram
}
m.fragIndex++
// minumum duration important for MSE in Apple Safari
if duration == 0 || duration > codec.ClockRate {
duration = codec.ClockRate/1000 + 1
m.pts[trackID] += duration
}
size := len(packet.Payload)
mv := iso.NewMovie(1024 + size)
mv.WriteMovieFragment(
-48
View File
@@ -1,48 +0,0 @@
package mpegts
import (
"encoding/hex"
"github.com/stretchr/testify/assert"
"strings"
"testing"
)
func TestTime(t *testing.T) {
w := NewWriter()
w.WriteTime(0xFFFFFFFF)
assert.Equal(t, []byte{0x27, 0xFF, 0xFF, 0xFF, 0xFF}, w.Bytes())
ts := ParseTime(w.Bytes())
assert.Equal(t, uint32(0xFFFFFFFF), ts)
}
func dec(s string) []byte {
s = strings.ReplaceAll(s, " ", "")
b, _ := hex.DecodeString(s)
return b
}
//func TestStream(t *testing.T) {
// // ffmpeg
// annexb := dec("00000001 09f0 00000001 6764001fac2484014016ec0440000003004000000c23c60c92 00000001 68ee32c8b0 000001 6588808003 00000001 09")
// avc, i := ParseAVC(annexb)
// assert.Equal(t, dec("00000019 6764001fac2484014016ec0440000003004000000c23c60c92 00000005 68ee32c8b0 00000005 6588808003"), avc)
// assert.Equal(t, dec("00000001 09"), annexb[i:])
//
// // http mpeg ts
// annexb = dec("00000001 0950 000001 6764001facd2014016e8400000fa400030e081 000001 68ea8f2c 000001 65b8400eff 00000001 09")
// avc, i = ParseAVC(annexb)
// assert.Equal(t, dec("00000013 6764001facd2014016e8400000fa400030e081 00000004 68ea8f2c 00000005 65b8400eff"), avc)
// assert.Equal(t, dec("00000001 09"), annexb[i:])
//
// // tapo TC60
// annexb = dec("00000001 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000001 68ee04c92240 00000001 45b80000d0 00000001 67")
// avc, i = ParseAVC(annexb)
// assert.Equal(t, dec("0000001C 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000006 68ee04c92240 00000005 45b80000d0"), avc)
// assert.Equal(t, dec("00000001 67"), annexb[i:])
//
// // Tapo ?
// annexb = dec("00000001 674d0032e90048014742000007d2000138d108 00000001 68ea8f20 00000001 65b8400cff 00000001 67")
// avc, i = ParseAVC(annexb)
// assert.Equal(t, dec("00000013 674d0032e90048014742000007d2000138d108 00000004 68ea8f20 00000005 65b8400cff"), avc)
// assert.Equal(t, dec("00000001 67"), annexb[i:])
//}
+205
View File
@@ -0,0 +1,205 @@
package nest
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
type API struct {
Token string
ExpiresAt time.Time
}
type Auth struct {
AccessToken string
}
var cache = map[string]*API{}
var cacheMu sync.Mutex
func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) {
cacheMu.Lock()
defer cacheMu.Unlock()
key := clientID + ":" + clientSecret + ":" + refreshToken
now := time.Now()
if api := cache[key]; api != nil && now.Before(api.ExpiresAt) {
return api, nil
}
data := url.Values{
"grant_type": []string{"refresh_token"},
"client_id": []string{clientID},
"client_secret": []string{clientSecret},
"refresh_token": []string{refreshToken},
}
client := &http.Client{Timeout: time.Second * 5000}
res, err := client.PostForm("https://www.googleapis.com/oauth2/v4/token", data)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, errors.New("nest: wrong status: " + res.Status)
}
var resv struct {
AccessToken string `json:"access_token"`
ExpiresIn time.Duration `json:"expires_in"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
return nil, err
}
api := &API{
Token: resv.AccessToken,
ExpiresAt: now.Add(resv.ExpiresIn * time.Second),
}
cache[key] = api
return api, nil
}
func (a *API) GetDevices(projectID string) (map[string]string, error) {
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices"
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+a.Token)
client := &http.Client{Timeout: time.Second * 5000}
res, err := client.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, errors.New("nest: wrong status: " + res.Status)
}
var resv struct {
Devices []Device
}
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
return nil, err
}
devices := map[string]string{}
for _, device := range resv.Devices {
if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 {
continue
}
if device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols[0] != "WEB_RTC" {
continue
}
i := strings.LastIndexByte(device.Name, '/')
if i <= 0 {
continue
}
name := device.Traits.SdmDevicesTraitsInfo.CustomName
devices[name] = device.Name[i+1:]
}
return devices, nil
}
func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) {
var reqv struct {
Command string `json:"command"`
Params struct {
Offer string `json:"offerSdp"`
} `json:"params"`
}
reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream"
reqv.Params.Offer = offer
b, err := json.Marshal(reqv)
if err != nil {
return "", err
}
uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" +
projectID + "/devices/" + deviceID + ":executeCommand"
req, err := http.NewRequest("POST", uri, bytes.NewReader(b))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+a.Token)
client := &http.Client{Timeout: time.Second * 5000}
res, err := client.Do(req)
if err != nil {
return "", err
}
if res.StatusCode != 200 {
return "", errors.New("nest: wrong status: " + res.Status)
}
var resv struct {
Results struct {
Answer string `json:"answerSdp"`
ExpiresAt time.Time `json:"expiresAt"`
MediaSessionId string `json:"mediaSessionId"`
} `json:"results"`
}
if err = json.NewDecoder(res.Body).Decode(&resv); err != nil {
return "", err
}
return resv.Results.Answer, nil
}
type Device struct {
Name string `json:"name"`
Type string `json:"type"`
//Assignee string `json:"assignee"`
Traits struct {
SdmDevicesTraitsInfo struct {
CustomName string `json:"customName"`
} `json:"sdm.devices.traits.Info"`
SdmDevicesTraitsCameraLiveStream struct {
VideoCodecs []string `json:"videoCodecs"`
AudioCodecs []string `json:"audioCodecs"`
SupportedProtocols []string `json:"supportedProtocols"`
} `json:"sdm.devices.traits.CameraLiveStream"`
//SdmDevicesTraitsCameraImage struct {
// MaxImageResolution struct {
// Width int `json:"width"`
// Height int `json:"height"`
// } `json:"maxImageResolution"`
//} `json:"sdm.devices.traits.CameraImage"`
//SdmDevicesTraitsCameraPerson struct {
//} `json:"sdm.devices.traits.CameraPerson"`
//SdmDevicesTraitsCameraMotion struct {
//} `json:"sdm.devices.traits.CameraMotion"`
//SdmDevicesTraitsDoorbellChime struct {
//} `json:"sdm.devices.traits.DoorbellChime"`
//SdmDevicesTraitsCameraClipPreview struct {
//} `json:"sdm.devices.traits.CameraClipPreview"`
} `json:"traits"`
//ParentRelations []struct {
// Parent string `json:"parent"`
// DisplayName string `json:"displayName"`
//} `json:"parentRelations"`
}
+101
View File
@@ -0,0 +1,101 @@
package nest
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"net/url"
)
type Client struct {
conn *webrtc.Conn
}
func NewClient(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
cliendID := query.Get("client_id")
cliendSecret := query.Get("client_secret")
refreshToken := query.Get("refresh_token")
projectID := query.Get("project_id")
deviceID := query.Get("device_id")
if cliendID == "" || cliendSecret == "" || refreshToken == "" || projectID == "" || deviceID == "" {
return nil, errors.New("nest: wrong query")
}
nestAPI, err := NewAPI(cliendID, cliendSecret, refreshToken)
if err != nil {
return nil, err
}
rtcAPI, err := webrtc.NewAPI("")
if err != nil {
return nil, err
}
conf := pion.Configuration{}
pc, err := rtcAPI.NewPeerConnection(conf)
if err != nil {
return nil, err
}
conn := webrtc.NewConn(pc)
conn.Desc = "Nest"
conn.Mode = core.ModeActiveProducer
// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields
medias := []*core.Media{
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: "app"}, // important for Nest
}
// 3. Create offer with candidates
offer, err := conn.CreateCompleteOffer(medias)
if err != nil {
return nil, err
}
// 4. Exchange SDP via Hass
answer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer)
if err != nil {
return nil, err
}
// 5. Set answer with remote medias
if err = conn.SetAnswer(answer); err != nil {
return nil, err
}
return &Client{conn: conn}, nil
}
func (c *Client) GetMedias() []*core.Media {
return c.conn.GetMedias()
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
return c.conn.GetTrack(media, codec)
}
func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
return c.conn.AddTrack(media, codec, track)
}
func (c *Client) Start() error {
return c.conn.Start()
}
func (c *Client) Stop() error {
return c.conn.Stop()
}
func (c *Client) MarshalJSON() ([]byte, error) {
return c.conn.MarshalJSON()
}
+2
View File
@@ -15,6 +15,8 @@ import (
"time"
)
const PathDevice = "/onvif/device_service"
type Client struct {
url *url.URL
+3 -5
View File
@@ -9,10 +9,6 @@ import (
"time"
)
const (
PathDevice = "/onvif/device_service"
)
func FindTagValue(b []byte, tag string) string {
re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`)
m := re.FindSubmatch(b)
@@ -34,6 +30,8 @@ func DiscoveryStreamingURLs() ([]string, error) {
return nil, err
}
defer conn.Close()
// https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_Feature_Discovery_Specification_16.07.pdf
// 5.3 Discovery Procedure:
msg := `<?xml version="1.0" ?>
@@ -45,7 +43,7 @@ func DiscoveryStreamingURLs() ([]string, error) {
</s:Header>
<s:Body>
<d:Probe xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery">
<d:Types />
<d:Types />
<d:Scopes />
</d:Probe>
</s:Body>
+9 -9
View File
@@ -45,12 +45,12 @@ func GetRequestAction(b []byte) string {
func GetCapabilitiesResponse(host string) string {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Capabilities xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:Device>
<tt:XAddr>http://` + host + `/onvif/device_service</tt:XAddr>
</tt:Device>
<s:Body>
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Capabilities xmlns:tt="http://www.onvif.org/ver10/schema">
<tt:Device>
<tt:XAddr>http://` + host + `/onvif/device_service</tt:XAddr>
</tt:Device>
<tt:Media>
<tt:XAddr>http://` + host + `/onvif/media_service</tt:XAddr>
<tt:StreamingCapabilities>
@@ -59,9 +59,9 @@ func GetCapabilitiesResponse(host string) string {
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
</tt:StreamingCapabilities>
</tt:Media>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>
</s:Body>
</tds:Capabilities>
</tds:GetCapabilitiesResponse>
</s:Body>
</s:Envelope>`
}
-35
View File
@@ -1,35 +0,0 @@
package pcm
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
func RepackBackchannel(handler core.HandlerFunc) core.HandlerFunc {
var buf []byte
var seq uint16
return func(packet *rtp.Packet) {
buf = append(buf, packet.Payload...)
if len(buf) < 1024 {
return
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true, // should be true
PayloadType: packet.PayloadType, // will be owerwriten
SequenceNumber: seq,
Timestamp: 0, // should be always zero
SSRC: packet.SSRC,
},
Payload: buf[:1024],
}
handler(pkt)
buf = buf[1024:]
seq++
}
}
+52
View File
@@ -3,6 +3,7 @@ package pcm
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"sync"
)
func Resample(codec *core.Codec, sampleRate uint32, handler core.HandlerFunc) core.HandlerFunc {
@@ -114,3 +115,54 @@ func DownsamplePCM(fromPCM func(int16) byte, n float32, handler core.HandlerFunc
handler(&clone)
}
}
// RepackG711 - Repack G.711 PCMA/PCMU into frames of size 1024
// 1. Fixes WebRTC audio quality issue (monotonic timestamp)
// 2. Fixes Reolink Doorbell backchannel issue (zero timestamp)
// https://github.com/AlexxIT/go2rtc/issues/331
func RepackG711(zeroTS bool, handler core.HandlerFunc) core.HandlerFunc {
const PacketSize = 1024
var buf []byte
var seq uint16
var ts uint32
// fix https://github.com/AlexxIT/go2rtc/issues/432
var mu sync.Mutex
return func(packet *rtp.Packet) {
mu.Lock()
buf = append(buf, packet.Payload...)
if len(buf) < PacketSize {
mu.Unlock()
return
}
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true, // should be true
PayloadType: packet.PayloadType, // will be owerwriten
SequenceNumber: seq,
SSRC: packet.SSRC,
},
Payload: buf[:PacketSize],
}
seq++
// don't know if zero TS important for Reolink Doorbell
// don't have this strange devices for tests
if !zeroTS {
pkt.Timestamp = ts
ts += PacketSize
}
buf = buf[PacketSize:]
mu.Unlock()
handler(pkt)
}
}
+12 -23
View File
@@ -2,9 +2,9 @@ package rtsp
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/tcp/websocket"
"net"
"net/http"
"net/url"
@@ -23,41 +23,30 @@ func NewClient(uri string) *Conn {
}
func (c *Conn) Dial() (err error) {
if c.URL, err = url.Parse(c.uri); err != nil {
return
var conn net.Conn
if c.Transport == "" {
conn, err = Dial(c.uri)
} else {
conn, err = websocket.Dial(c.Transport)
}
if strings.IndexByte(c.URL.Host, ':') < 0 {
c.URL.Host += ":554"
}
c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
if err != nil {
return
}
var tlsConf *tls.Config
switch c.URL.Scheme {
case "rtsps":
tlsConf = &tls.Config{ServerName: c.URL.Hostname()}
case "rtspx":
c.URL.Scheme = "rtsps"
tlsConf = &tls.Config{InsecureSkipVerify: true}
}
if tlsConf != nil {
tlsConn := tls.Client(c.conn, tlsConf)
if err = tlsConn.Handshake(); err != nil {
return err
}
c.conn = tlsConn
if c.URL, err = url.Parse(c.uri); err != nil {
return
}
// remove UserInfo from URL
c.auth = tcp.NewAuth(c.URL.User)
c.URL.User = nil
c.reader = bufio.NewReader(c.conn)
c.conn = conn
c.reader = bufio.NewReader(conn)
c.session = ""
c.sequence = 0
c.state = StateConn
return nil
+1 -1
View File
@@ -88,7 +88,7 @@ Session: 1
require.Nil(t, err)
require.Len(t, client.Medias, 3)
ch, err := client.SetupMedia(client.Medias[2], true)
ch, err := client.SetupMedia(client.Medias[2])
require.Nil(t, err)
require.Equal(t, ch, byte(4))
}
+1
View File
@@ -24,6 +24,7 @@ type Conn struct {
Backchannel bool
PacketSize uint16
SessionName string
Transport string // custom transport support, ex. RTSP over WebSocket
Medias []*core.Media
UserAgent string
+3 -3
View File
@@ -12,7 +12,7 @@ import (
)
func (c *Conn) GetMedias() []*core.Media {
core.Assert(c.Medias != nil)
//core.Assert(c.Medias != nil)
return c.Medias
}
@@ -62,9 +62,9 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
// important to send original codec for valid IsRTP check
sender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType)
// https://github.com/AlexxIT/go2rtc/issues/331
if c.mode == core.ModeActiveProducer && track.Codec.Name == core.CodecPCMA {
sender.Handler = pcm.RepackBackchannel(sender.Handler)
// Fix Reolink Doorbell https://github.com/AlexxIT/go2rtc/issues/331
sender.Handler = pcm.RepackG711(true, sender.Handler)
}
sender.HandleRTP(track)
+44
View File
@@ -0,0 +1,44 @@
package rtsp
import (
"crypto/tls"
"errors"
"net"
"net/url"
"strings"
"time"
)
func Dial(uri string) (net.Conn, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
switch u.Scheme {
case "rtsp":
return dialTCP(u.Host, nil)
case "rtsps":
tlsConf := &tls.Config{ServerName: u.Hostname()}
return dialTCP(u.Host, tlsConf)
case "rtspx":
tlsConf := &tls.Config{InsecureSkipVerify: true}
return dialTCP(u.Host, tlsConf)
}
return nil, errors.New("unsupported scheme: " + u.Scheme)
}
func dialTCP(address string, tlsConf *tls.Config) (net.Conn, error) {
if strings.IndexByte(address, ':') < 0 {
address += ":554"
}
conn, err := net.DialTimeout("tcp", address, time.Second*5)
if tlsConf == nil || err != nil {
return conn, err
}
tlsConn := tls.Client(conn, tlsConf)
return tlsConn, tlsConn.Handshake()
}
-32
View File
@@ -1,32 +0,0 @@
package shell
import (
"os"
"regexp"
"strings"
)
func ReplaceEnvVars(text string) string {
re := regexp.MustCompile(`\${([^}{]+)}`)
return re.ReplaceAllStringFunc(text, func(match string) string {
key := match[2 : len(match)-1]
var def string
var dok bool
if i := strings.IndexByte(key, ':'); i > 0 {
key, def = key[:i], key[i+1:]
dok = true
}
if value, vok := os.LookupEnv(key); vok {
return value
}
if dok {
return def
}
return match
})
}
+35
View File
@@ -1,7 +1,11 @@
package shell
import (
"os"
"os/signal"
"regexp"
"strings"
"syscall"
)
func QuoteSplit(s string) []string {
@@ -39,3 +43,34 @@ func QuoteSplit(s string) []string {
}
return a
}
func ReplaceEnvVars(text string) string {
re := regexp.MustCompile(`\${([^}{]+)}`)
return re.ReplaceAllStringFunc(text, func(match string) string {
key := match[2 : len(match)-1]
var def string
var dok bool
if i := strings.IndexByte(key, ':'); i > 0 {
key, def = key[:i], key[i+1:]
dok = true
}
if value, vok := os.LookupEnv(key); vok {
return value
}
if dok {
return def
}
return match
})
}
func RunUntilSignal() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
println("exit with signal:", (<-sigs).String())
}
+15 -13
View File
@@ -46,21 +46,13 @@ func (s *Server) Serve(conn net.PacketConn) error {
// Multiplexing RTP Data and Control Packets on a Single Port
// https://datatracker.ietf.org/doc/html/rfc5761
var handle func([]byte) error
// this is default position for SSRC in RTP packet
ssrc := binary.BigEndian.Uint32(buf[8:])
session, ok := s.sessions[ssrc]
if ok {
if session.Write == nil {
session.Write = func(b []byte) (int, error) {
return conn.WriteTo(b, addr)
}
}
atomic.AddUint32(&session.Recv, uint32(n))
if err = session.HandleRTP(buf[:n]); err != nil {
return err
}
handle = session.HandleRTP
} else {
// this is default position for SSRC in RTCP packet
ssrc = binary.BigEndian.Uint32(buf[4:])
@@ -68,9 +60,19 @@ func (s *Server) Serve(conn net.PacketConn) error {
continue // skip unknown ssrc
}
if err = session.HandleRTCP(buf[:n]); err != nil {
return err
handle = session.HandleRTCP
}
if session.Write == nil {
session.Write = func(b []byte) (int, error) {
return conn.WriteTo(b, addr)
}
}
atomic.AddUint32(&session.Recv, uint32(n))
if err = handle(buf[:n]); err != nil {
return err
}
}
}
+1 -4
View File
@@ -93,13 +93,10 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
return
}
var packets []rtcp.Packet
if packets, err = rtcp.Unmarshal(data); err != nil {
if _, err = rtcp.Unmarshal(data); err != nil {
return
}
_ = packets
if header.Type == rtcp.TypeSenderReport {
err = s.KeepAlive()
}
+130
View File
@@ -0,0 +1,130 @@
package websocket
import (
cryptorand "crypto/rand"
"encoding/binary"
"fmt"
"io"
"net"
"time"
)
const BinaryMessage = 2
type Client struct {
conn net.Conn
remain int
}
func NewClient(conn net.Conn) *Client {
return &Client{conn: conn}
}
const finalBit = 0x80
const maskBit = 0x80
func (w *Client) Read(b []byte) (n int, err error) {
if w.remain == 0 {
b2 := make([]byte, 2)
if _, err = io.ReadFull(w.conn, b2); err != nil {
return 0, err
}
frameType := b2[0] & 0xF
w.remain = int(b2[1] & 0x7F)
switch frameType {
case BinaryMessage:
default:
return 0, fmt.Errorf("unsupported frame type: %d", frameType)
}
switch w.remain {
case 126:
if _, err = io.ReadFull(w.conn, b2); err != nil {
return 0, err
}
w.remain = int(binary.BigEndian.Uint16(b2))
case 127:
b8 := make([]byte, 8)
if _, err = io.ReadFull(w.conn, b8); err != nil {
return 0, err
}
w.remain = int(binary.BigEndian.Uint64(b8))
}
}
if w.remain > len(b) {
n, err = io.ReadFull(w.conn, b)
w.remain -= n
return
}
n, err = io.ReadFull(w.conn, b[:w.remain])
w.remain = 0
return
}
func (w *Client) Write(b []byte) (n int, err error) {
var data []byte
var start byte
size := len(b)
switch {
case size > 65535:
start = 10
data = make([]byte, size+14)
data[1] = maskBit | 127
binary.BigEndian.PutUint64(data[2:], uint64(size))
case size > 125:
start = 4
data = make([]byte, size+8)
data[1] = maskBit | 126
binary.BigEndian.PutUint16(data[2:], uint16(size))
default:
start = 2
data = make([]byte, size+6)
data[1] = maskBit | byte(size)
}
data[0] = BinaryMessage | finalBit
mask := data[start : start+4]
msg := data[start+4:]
if _, err = cryptorand.Read(mask); err != nil {
return 0, err
}
for i := 0; i < len(b); i++ {
msg[i] = b[i] ^ mask[i%4]
}
return w.conn.Write(data)
}
func (w *Client) Close() error {
return w.conn.Close()
}
func (w *Client) LocalAddr() net.Addr {
return w.conn.LocalAddr()
}
func (w *Client) RemoteAddr() net.Addr {
return w.conn.RemoteAddr()
}
func (w *Client) SetDeadline(t time.Time) error {
return w.conn.SetDeadline(t)
}
func (w *Client) SetReadDeadline(t time.Time) error {
return w.conn.SetReadDeadline(t)
}
func (w *Client) SetWriteDeadline(t time.Time) error {
return w.conn.SetWriteDeadline(t)
}
+64
View File
@@ -0,0 +1,64 @@
package websocket
import (
cryptorand "crypto/rand"
"crypto/sha1"
"encoding/base64"
"errors"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net"
"net/http"
"strings"
)
func Dial(address string) (net.Conn, error) {
if strings.HasPrefix(address, "ws") {
address = "http" + address[2:] // support http and https
}
// using custom client for support Digest Auth
// https://github.com/AlexxIT/go2rtc/issues/415
ctx, pconn := tcp.WithConn()
req, err := http.NewRequestWithContext(ctx, "GET", address, nil)
if err != nil {
return nil, err
}
key, accept := GetKeyAccept()
// Version, Key, Protocol important for Axis cameras
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Sec-WebSocket-Version", "13")
req.Header.Set("Sec-WebSocket-Key", key)
req.Header.Set("Sec-WebSocket-Protocol", "binary")
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusSwitchingProtocols {
return nil, errors.New("wrong status: " + res.Status)
}
if res.Header.Get("Sec-Websocket-Accept") != accept {
return nil, errors.New("wrong websocket accept")
}
return NewClient(*pconn), nil
}
func GetKeyAccept() (key, accept string) {
b := make([]byte, 16)
_, _ = cryptorand.Read(b)
key = base64.StdEncoding.EncodeToString(b)
h := sha1.New()
h.Write([]byte(key))
h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
accept = base64.StdEncoding.EncodeToString(h.Sum(nil))
return
}
+3
View File
@@ -24,6 +24,9 @@ func (c *Conn) CreateOffer(medias []*core.Media) (string, error) {
case core.DirectionSendRecv:
// default transceiver is sendrecv
_, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind))
default:
// Nest cameras require data channel
_, err = c.pc.CreateDataChannel(media.Kind, nil)
}
if err != nil {
+11
View File
@@ -148,6 +148,17 @@ func (c *Conn) getTranseiver(mid string) *webrtc.RTPTransceiver {
return nil
}
func (c *Conn) getSenderTrack(mid string) *Track {
if tr := c.getTranseiver(mid); tr != nil {
if s := tr.Sender(); s != nil {
if t := s.Track().(*Track); t != nil {
return t
}
}
}
return nil
}
func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) {
for _, tr := range c.pc.GetTransceivers() {
// search Transeiver for this TrackRemote
+9 -1
View File
@@ -2,6 +2,7 @@ package webrtc
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
@@ -31,7 +32,11 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
panic(core.Caller())
}
localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track)
localTrack := c.getSenderTrack(media.ID)
if localTrack == nil {
return errors.New("webrtc: can't get track")
}
payloadType := codec.PayloadType
sender := core.NewSender(media, codec)
@@ -66,6 +71,9 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv
codec.ClockRate = 8000
sender.Handler = pcm.Resample(track.Codec, 8000, sender.Handler)
}
// Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500
sender.Handler = pcm.RepackG711(false, sender.Handler)
}
sender.HandleRTP(track)
-68
View File
@@ -1,68 +0,0 @@
package webrtc
import (
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestCandidates(t *testing.T) {
conf := &ice.CandidateHostConfig{
Network: "udp",
Address: "192.168.1.123",
Port: 8555,
Component: ice.ComponentRTP,
}
cand, err := ice.NewCandidateHost(conf)
require.Nil(t, err)
assert.Equal(t, "candidate:"+cand.Marshal(), CandidateManualHostUDP(conf.Address, conf.Port))
conf = &ice.CandidateHostConfig{
Network: "tcp",
Address: "192.168.1.123",
Port: 8555,
Component: ice.ComponentRTP,
TCPType: ice.TCPTypePassive,
}
cand, err = ice.NewCandidateHost(conf)
require.Nil(t, err)
assert.Equal(t, "candidate:"+cand.Marshal(), CandidateManualHostTCPPassive(conf.Address, conf.Port))
}
func TestPublicIP(t *testing.T) {
ip, err := GetPublicIP()
assert.Nil(t, err)
assert.NotNil(t, ip)
t.Logf("your public IP: %s", ip.String())
}
func TestMedia(t *testing.T) {
codec := webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
},
PayloadType: 96,
}
md := &sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: "video", Protos: []string{"RTP", "AVP"},
},
}
md.WithCodec(
uint8(codec.PayloadType), codec.MimeType[6:], codec.ClockRate,
codec.Channels, codec.SDPFmtpLine,
)
sd := &sdp.SessionDescription{
MediaDescriptions: []*sdp.MediaDescription{md},
}
data, err := sd.Marshal()
assert.Nil(t, err)
assert.NotNil(t, data)
}
+11
View File
@@ -7,6 +7,15 @@
- `aarch64` = `arm64`
- `armv7` = `arm`
## Go
```
go get -u
go mod tidy
go mod why github.com/pion/rtcp
go list -deps .\cmd\go2rtc_rtsp\
```
## Virus
- https://go.dev/doc/faq#virus
@@ -14,6 +23,8 @@
## Useful links
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro
- https://github.com/golang/go/wiki/GoArm
- https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
- https://en.wikipedia.org/wiki/AArch64
+6
View File
@@ -36,6 +36,12 @@ go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET FILENAME=go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=linux
@SET GOARCH=arm
@SET GOARM=6
@SET FILENAME=go2rtc_linux_armv6
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=linux
@SET GOARCH=mipsle
@SET FILENAME=go2rtc_linux_mipsel
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>go2rtc - API</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="https://raw.githubusercontent.com/AlexxIT/go2rtc/master/api/openapi.yaml"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</body>
</html>
+13 -1
View File
@@ -1,4 +1,14 @@
# HTML5
## Browser support
[ECMAScript 2019 (ES10)](https://caniuse.com/?search=es10) supported by [iOS 12](https://en.wikipedia.org/wiki/IOS_12) (iPhone 5S, iPad Air, iPad Mini 2, etc.).
But [ECMAScript 2017 (ES8)](https://caniuse.com/?search=es8) almost fine (`es6 + async`) and recommended for [React+TypeScript](https://github.com/typescript-cheatsheets/react).
## Known problems
- Autoplay doesn't work for WebRTC in Safari [read more](https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari/).
## HTML5
**1. Autoplay video tag**
@@ -11,6 +21,8 @@
<video id="video" autoplay controls playsinline muted></video>
```
- https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari/
**2. [Safari] pc.createOffer**
Don't work in Desktop Safari:
+104 -63
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Add Stream</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -59,20 +59,20 @@
<script src="main.js"></script>
<script>
async function getStreams(url, tableID) {
const table = document.getElementById(tableID)
table.innerText = 'loading...'
const table = document.getElementById(tableID);
table.innerText = 'loading...';
const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url
const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url;
if (!r.ok) {
table.innerText = await r.text()
return
table.innerText = await r.text();
return;
}
/** @type {{streams:Array<{name:string,url:string}>}} */
const data = await r.json()
const data = await r.json();
table.innerHTML = data.streams.reduce((html, item) => {
return html + `<tr><td>${item.name}</td><td>${item.url}</td></tr>`
}, '<thead><tr><th>Name</th><th>Source</th></tr></thead><tbody>') + '</tbody>'
return html + `<tr><td>${item.name}</td><td>${item.url}</td></tr>`;
}, '<thead><tr><th>Name</th><th>Source</th></tr></thead><tbody>') + '</tbody>';
}
</script>
@@ -87,19 +87,19 @@
</div>
<script>
document.getElementById('stream').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
})
ev.target.nextElementSibling.style.display = 'block';
});
document.getElementById('stream-form').addEventListener('submit', async ev => {
ev.preventDefault()
ev.preventDefault();
const url = new URL('api/streams', location.href)
url.searchParams.set('name', ev.target.elements['name'].value)
url.searchParams.set('src', ev.target.elements['src'].value)
const url = new URL('api/streams', location.href);
url.searchParams.set('name', ev.target.elements['name'].value);
url.searchParams.set('src', ev.target.elements['src'].value);
const r = await fetch(url, {method: 'PUT'})
alert(r.ok ? 'OK' : 'ERROR')
})
const r = await fetch(url, {method: 'PUT'});
alert(r.ok ? 'OK' : 'ERROR');
});
</script>
@@ -124,53 +124,65 @@
</div>
<script>
document.getElementById('homekit').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
const r = await fetch('api/homekit', {cache: 'no-cache'})
ev.target.nextElementSibling.style.display = 'block';
const r = await fetch('api/homekit', {cache: 'no-cache'});
/** @type {Array<{id:string,name:string,addr:string,model:string,paired:boolean}>} */
const data = await r.json()
const data = await r.json();
const tbody = document.getElementById('homekit-body')
const tbody = document.getElementById('homekit-body');
tbody.innerHTML =
data.reduce((res, item) => {
let commands = ''
if (item.id === "") {
commands = `<a href="#" onclick="unpair('${item.name}')">unpair</a>`
let commands = '';
if (item.id === '') {
commands = `<a href="#" onclick="unpair('${item.name}')">unpair</a>`;
} else if (item.paired === false) {
commands = `<a href="#" onclick="pair('${item.id}','${item.name}')">pair</a>`
commands = `<a href="#" onclick="pair('${item.id}','${item.name}')">pair</a>`;
}
return res + `<tr>
<td>${item.name}</td>
<td>${item.addr}</td>
<td>${item.model}</td>
<td>${commands}</td>
</tr>`
}, '')
})
</tr>`;
}, '');
});
function pair(id, name) {
const pin = document.querySelector('#pin').value
const pin = document.querySelector('#pin').value;
fetch(`api/homekit?id=${id}&name=${name}&pin=${pin}`, {method: 'POST'})
.then(r => r.text())
.then(data => {
if (data.length > 0) alert(data)
else window.location.reload()
if (data.length > 0) alert(data);
else window.location.reload();
})
.catch(console.error)
.catch(console.error);
}
function unpair(src) {
fetch(`api/homekit?src=${src}`, {method: 'DELETE'})
.then(r => r.text())
.then(data => {
if (data.length > 0) alert(data)
else window.location.reload()
if (data.length > 0) alert(data);
else window.location.reload();
})
.catch(console.error)
.catch(console.error);
}
</script>
<button id="dvrip">DVRIP</button>
<div class="module">
<table id="dvrip-table"></table>
</div>
<script>
document.getElementById('dvrip').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/dvrip', 'dvrip-table');
});
</script>
<button id="devices">FFmpeg Devices (USB)</button>
<div class="module">
<table id="devices-table">
@@ -178,9 +190,9 @@
</div>
<script>
document.getElementById('devices').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/ffmpeg/devices', 'devices-table')
})
ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/ffmpeg/devices', 'devices-table');
});
</script>
@@ -191,9 +203,38 @@
</div>
<script>
document.getElementById('hardware').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/ffmpeg/hardware', 'hardware-table')
})
ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/ffmpeg/hardware', 'hardware-table');
});
</script>
<button id="nest">Google Nest</button>
<div class="module">
<form id="nest-form" style="margin-bottom: 10px">
<input type="text" name="client_id" placeholder="client_id">
<input type="text" name="client_secret" placeholder="client_secret">
<input type="text" name="refresh_token" placeholder="refresh_token">
<input type="text" name="project_id" placeholder="project_id">
<input type="submit" value="Login">
</form>
<table id="nest-table">
</table>
</div>
<script>
document.getElementById('nest').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block';
});
document.getElementById('nest-form').addEventListener('submit', async ev => {
ev.preventDefault();
const query = new URLSearchParams(new FormData(ev.target));
const url = new URL('api/nest?' + query.toString(), location.href);
const r = await fetch(url, {cache: 'no-cache'});
await getStreams(r, 'nest-table');
});
</script>
@@ -203,9 +244,9 @@
</div>
<script>
document.getElementById('hass').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/hass', 'hass-table')
})
ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/hass', 'hass-table');
});
</script>
@@ -219,18 +260,18 @@
</div>
<script>
document.getElementById('onvif').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/onvif', 'onvif-table')
})
ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/onvif', 'onvif-table');
});
document.getElementById('onvif-form').addEventListener('submit', async ev => {
ev.preventDefault()
ev.preventDefault();
const url = new URL('api/onvif', location.href)
url.searchParams.set('src', ev.target.elements['src'].value)
const url = new URL('api/onvif', location.href);
url.searchParams.set('src', ev.target.elements['src'].value);
await getStreams(url.toString(), 'onvif-table')
})
await getStreams(url.toString(), 'onvif-table');
});
</script>
@@ -246,15 +287,15 @@
</div>
<script>
document.getElementById('roborock').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/roborock', 'roborock-table')
})
ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/roborock', 'roborock-table');
});
document.getElementById('roborock-form').addEventListener('submit', async ev => {
ev.preventDefault()
const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)})
await getStreams(r, 'roborock-table')
})
ev.preventDefault();
const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)});
await getStreams(r, 'roborock-table');
});
</script>
@@ -264,9 +305,9 @@
</div>
<script>
document.getElementById('webtorrent').addEventListener('click', async ev => {
ev.target.nextElementSibling.style.display = 'block'
await getStreams('api/webtorrent', 'webtorrent-table')
})
ev.target.nextElementSibling.style.display = 'block';
await getStreams('api/webtorrent', 'webtorrent-table');
});
</script>
+18 -14
View File
@@ -18,21 +18,21 @@
<body>
<div id="out"></div>
<script>
const out = document.getElementById("out");
const out = document.getElementById('out');
const print = (name, caps) => {
out.innerText += name + "\n";
out.innerText += name + '\n';
caps.codecs.forEach((codec) => {
out.innerText += [codec.mimeType, codec.channels, codec.clockRate, codec.sdpFmtpLine] + "\n";
out.innerText += [codec.mimeType, codec.channels, codec.clockRate, codec.sdpFmtpLine] + '\n';
});
out.innerText += "\n";
}
out.innerText += '\n';
};
if (RTCRtpReceiver.getCapabilities) {
print("receiver video", RTCRtpReceiver.getCapabilities("video"));
print("receiver audio", RTCRtpReceiver.getCapabilities("audio"));
print("sender video", RTCRtpSender.getCapabilities("video"));
print("sender audio", RTCRtpSender.getCapabilities("audio"));
print('receiver video', RTCRtpReceiver.getCapabilities('video'));
print('receiver audio', RTCRtpReceiver.getCapabilities('audio'));
print('sender video', RTCRtpSender.getCapabilities('video'));
print('sender audio', RTCRtpSender.getCapabilities('audio'));
}
const types = [
@@ -48,14 +48,18 @@
'video/mp4; codecs="hvc1.1.6.L93.B0"',
'video/mp4; codecs="hev1.1.6.L93.B0"',
'video/mp4; codecs="hev1.2.4.L120.B0"',
'video/mp4; codecs="flac"',
'video/mp4; codecs="opus"',
'video/mp4; codecs="mp3"',
'video/mp4; codecs="null"',
'application/vnd.apple.mpegurl',
];
const video = document.createElement("video");
out.innerText += "video.canPlayType\n";
const video = document.createElement('video');
out.innerText += 'video.canPlayType\n';
types.forEach(type => {
out.innerText += `${type} = ${MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`;
})
out.innerText += `${type} = ${'MediaSource' in window && MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`;
});
</script>
</body>
</html>
+5 -5
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>File Editor</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -32,8 +32,8 @@
<div id="config"></div>
<script>
ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.14.0/');
const editor = ace.edit("config", {
mode: "ace/mode/yaml",
const editor = ace.edit('config', {
mode: 'ace/mode/yaml',
});
document.getElementById('save').addEventListener('click', () => {
@@ -42,7 +42,7 @@
}).then(r => {
if (r.ok) {
alert('OK');
fetch('api/exit', {method: 'POST'});
fetch('api/exit?code=100', {method: 'POST'});
} else {
r.text().then(alert);
}
@@ -63,7 +63,7 @@
alert(`Unknown error: ${r.statusText} (${r.status})`);
}
});
})
});
</script>
</body>
</html>
+31 -35
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -46,10 +46,6 @@
align-items: center;
}
.header {
padding: 5px 5px;
}
.controls {
display: flex;
padding: 5px;
@@ -67,18 +63,18 @@
<button>stream</button>
<label><input type="checkbox" name="webrtc" checked>webrtc</label>
<label><input type="checkbox" name="mse" checked>mse</label>
<label><input type="checkbox" name="mp4" checked>mp4</label>
<label><input type="checkbox" name="hls" checked>hls</label>
<label><input type="checkbox" name="mjpeg" checked>mjpeg</label>
</div>
<table id="streams">
<table>
<thead>
<tr>
<th><input id="selectall" type="checkbox">Name</th>
<th><label><input id="selectall" type="checkbox">Name</label></th>
<th>Online</th>
<th>Commands</th>
</tr>
</thead>
<tbody>
<tbody id="streams">
</tbody>
</table>
<script>
@@ -88,55 +84,55 @@
'<a href="#" data-name="{name}">delete</a>',
];
document.querySelector(".controls > button")
.addEventListener("click", () => {
const url = new URL("stream.html", location.href);
document.querySelector('.controls > button')
.addEventListener('click', () => {
const url = new URL('stream.html', location.href);
const streams = document.querySelectorAll("#streams input");
const streams = document.querySelectorAll('#streams input');
streams.forEach(i => {
if (i.checked) url.searchParams.append("src", i.name);
if (i.checked) url.searchParams.append('src', i.name);
});
if (!url.searchParams.has("src")) return;
if (!url.searchParams.has('src')) return;
let mode = document.querySelectorAll(".controls input");
mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(",");
let mode = document.querySelectorAll('.controls input');
mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(',');
window.location.href = `${url}&mode=${mode}`;
});
const tbody = document.querySelector("#streams > tbody");
tbody.addEventListener("click", ev => {
if (ev.target.innerText !== "delete") return;
const tbody = document.getElementById('streams');
tbody.addEventListener('click', ev => {
if (ev.target.innerText !== 'delete') return;
ev.preventDefault();
const url = new URL("api/streams", location.href);
const url = new URL('api/streams', location.href);
const src = decodeURIComponent(ev.target.dataset.name);
url.searchParams.set("src", src);
fetch(url, {method: "DELETE"}).then(reload);
url.searchParams.set('src', src);
fetch(url, {method: 'DELETE'}).then(reload);
});
document.getElementById('selectall').addEventListener('change', ev => {
document.querySelectorAll('#streams input').forEach(el => {
el.checked = ev.target.checked
})
})
el.checked = ev.target.checked;
});
});
function reload() {
const url = new URL("api/streams", location.href);
const url = new URL('api/streams', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
tbody.innerHTML = "";
tbody.innerHTML = '';
for (const [name, value] of Object.entries(data)) {
const online = value && value.consumers ? value.consumers.length : 0;
const src = encodeURIComponent(name);
const links = templates.map(link => {
return link.replace("{name}", src);
}).join(" ");
return link.replace('{name}', src);
}).join(' ');
const tr = document.createElement("tr");
tr.dataset["id"] = name;
const tr = document.createElement('tr');
tr.dataset['id'] = name;
tr.innerHTML =
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
`<td><a href="api/streams?src=${src}">${online} / info</a></td>` +
@@ -146,13 +142,13 @@
});
}
const url = new URL("api", location.href);
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
const info = document.querySelector(".info");
const info = document.querySelector('.info');
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
});
reload();
</script>
</body>
</html>
</html>
+47 -47
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>go2rtc - links</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
@@ -42,21 +42,21 @@
<script src="main.js"></script>
<div id="links"></div>
<script>
const src = new URLSearchParams(location.search).get('src')
const src = new URLSearchParams(location.search).get('src');
document.getElementById('links').innerHTML = `
<h2>Any codec in source</h2>
<li><a href="stream.html?src=${src}">stream.html</a> with auto-select mode / browsers: all / codecs: H264, H265*, MJPEG, JPEG, AAC, PCMU, PCMA, OPUS</li>
<li><a href="api/streams?src=${src}">info.json</a> page with active connections</li>
`
`;
const url = new URL('api', location.href)
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
let rtsp = location.host + ':8554'
let rtsp = location.host + ':8554';
try {
const host = data.host.match(/^[^:]+/)[0]
const port = data.rtsp.listen.match(/[0-9]+$/)[0]
rtsp = `${host}:${port}`
const host = data.host.match(/^[^:]+/)[0];
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
rtsp = `${host}:${port}`;
} catch (e) {
}
@@ -80,8 +80,8 @@
<li><a href="stream.html?src=${src}&mode=mjpeg">stream.html</a> with MJPEG mode / browsers: all / codecs: MJPEG, JPEG</li>
<li><a href="api/stream.mjpeg?src=${src}">stream.mjpeg</a> MJPEG stream / browsers: all / codecs: MJPEG, JPEG</li>
<li><a href="api/frame.jpeg?src=${src}">frame.jpeg</a> snapshot in JPEG-format / browsers: all / codecs: MJPEG, JPEG</li>
`
})
`;
});
</script>
<div>
@@ -92,12 +92,12 @@
</div>
<script>
document.getElementById('send').addEventListener('click', ev => {
ev.preventDefault()
const url = new URL('api/streams', location.href)
url.searchParams.set('dst', src)
url.searchParams.set('src', document.getElementById('source').value)
fetch(url, {method: 'POST'})
})
ev.preventDefault();
const url = new URL('api/streams', location.href);
url.searchParams.set('dst', src);
url.searchParams.set('src', document.getElementById('source').value);
fetch(url, {method: 'POST'});
});
</script>
<div id="webrtc">
@@ -119,62 +119,62 @@
</div>
<script>
function webrtcLinksUpdate() {
const media = document.querySelector('input[name="webrtc"]:checked').value
const media = document.querySelector('input[name="webrtc"]:checked').value;
const direction = media.indexOf('video') >= 0 || media === 'audio' ? 'src' : 'dst'
document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}`
const direction = media.indexOf('video') >= 0 || media === 'audio' ? 'src' : 'dst';
document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}`;
const share = document.getElementById('shareget')
share.href = `https://alexxit.github.io/go2rtc/#${share.dataset.auth}&media=${media}`
const share = document.getElementById('shareget');
share.href = `https://alexxit.github.io/go2rtc/#${share.dataset.auth}&media=${media}`;
}
function share(method) {
const url = new URL('api/webtorrent', location.href)
url.searchParams.set('src', src)
return fetch(url, {method: method, cache: 'no-cache'})
const url = new URL('api/webtorrent', location.href);
url.searchParams.set('src', src);
return fetch(url, {method: method, cache: 'no-cache'});
}
function onshareadd(r) {
document.getElementById('shareget').dataset['auth'] = `share=${r.share}&pwd=${r.pwd}`
document.getElementById('shareget').dataset['auth'] = `share=${r.share}&pwd=${r.pwd}`;
document.getElementById('shareadd').style.display = 'none'
document.getElementById('shareget').style.display = ''
document.getElementById('sharedel').style.display = ''
document.getElementById('shareadd').style.display = 'none';
document.getElementById('shareget').style.display = '';
document.getElementById('sharedel').style.display = '';
webrtcLinksUpdate()
webrtcLinksUpdate();
}
function onsharedel() {
document.getElementById('shareadd').style.display = ''
document.getElementById('shareget').style.display = 'none'
document.getElementById('sharedel').style.display = 'none'
document.getElementById('shareadd').style.display = '';
document.getElementById('shareget').style.display = 'none';
document.getElementById('sharedel').style.display = 'none';
}
document.getElementById('shareadd').addEventListener('click', ev => {
ev.preventDefault()
share('POST').then(r => r.json()).then(r => onshareadd(r))
})
ev.preventDefault();
share('POST').then(r => r.json()).then(r => onshareadd(r));
});
document.getElementById('shareget').addEventListener('click', ev => {
ev.preventDefault()
navigator.clipboard.writeText(ev.target.href)
})
ev.preventDefault();
navigator.clipboard.writeText(ev.target.href);
});
document.getElementById('sharedel').addEventListener('click', ev => {
ev.preventDefault()
share('DELETE').then(r => onsharedel())
})
ev.preventDefault();
share('DELETE').then(() => onsharedel());
});
document.getElementById('webrtc').addEventListener('click', ev => {
if (ev.target.tagName === 'INPUT') webrtcLinksUpdate()
})
if (ev.target.tagName === 'INPUT') webrtcLinksUpdate();
});
share('GET').then(r => {
if (r.ok) r.json().then(r => onshareadd(r))
else onsharedel()
})
if (r.ok) r.json().then(r => onshareadd(r));
else onsharedel();
});
webrtcLinksUpdate()
webrtcLinksUpdate();
</script>
</body>
+8 -8
View File
@@ -30,9 +30,9 @@
const params = new URLSearchParams(location.search);
// support multiple streams and multiple modes
const streams = params.getAll("src");
const modes = params.getAll("mode");
if (modes.length === 0) modes.push("");
const streams = params.getAll('src');
const modes = params.getAll('mode');
if (modes.length === 0) modes.push('');
while (modes.length > streams.length) {
streams.push(streams[0]);
@@ -42,19 +42,19 @@
}
if (streams.length > 1) {
document.body.className = "flex";
document.body.className = 'flex';
}
const background = params.get("background") !== "false";
const width = "1 0 " + (params.get("width") || "320px");
const background = params.get('background') !== 'false';
const width = '1 0 ' + (params.get('width') || '320px');
for (let i = 0; i < streams.length; i++) {
/** @type {VideoStream} */
const video = document.createElement("video-stream");
const video = document.createElement('video-stream');
video.background = background;
video.mode = modes[i] || video.mode;
video.style.flex = width;
video.src = new URL("api/ws?src=" + encodeURIComponent(streams[i]), location.href);
video.src = new URL('api/ws?src=' + encodeURIComponent(streams[i]), location.href);
document.body.appendChild(video);
}
</script>
+121 -97
View File
@@ -1,17 +1,17 @@
/**
* Video player for go2rtc streaming application.
* VideoRTC v1.6.0 - Video player for go2rtc streaming application.
*
* All modern web technologies are supported in almost any browser except Apple Safari.
*
* Support:
* - ECMAScript 2017 (ES8) = ES6 + async
* - RTCPeerConnection for Safari iOS 11.0+
* - IntersectionObserver for Safari iOS 12.2+
*
* Doesn't support:
* - MediaSource for Safari iOS all
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
* - Public class fields because old Safari (before 14.0)
* - Autoplay for Safari
* - MediaSource for Safari iOS
* - Customized built-in elements (extends HTMLVideoElement) because Safari
* - Autoplay for WebRTC in Safari
*/
export class VideoRTC extends HTMLElement {
constructor() {
@@ -21,21 +21,22 @@ export class VideoRTC extends HTMLElement {
this.RECONNECT_TIMEOUT = 30000;
this.CODECS = [
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen)
"avc1.640033", // H.264 high 5.1 (Chromecast with Google TV)
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC
"mp4a.40.5", // AAC HE
"flac", // FLAC (PCM compatible)
"opus", // OPUS Chrome, Firefox
'avc1.640029', // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
'avc1.64002A', // H.264 high 4.2 (Chromecast 3rd Gen)
'avc1.640033', // H.264 high 5.1 (Chromecast with Google TV)
'hvc1.1.6.L153.B0', // H.265 main 5.1 (Chromecast Ultra)
'mp4a.40.2', // AAC LC
'mp4a.40.5', // AAC HE
'null', // for detecting liars (Safari iOS 12)
'flac', // FLAC (PCM compatible)
'opus', // OPUS Chrome, Firefox
];
/**
* [config] Supported modes (webrtc, mse, mp4, mjpeg).
* [config] Supported modes (webrtc, webrtc/tcp, mse, hls, mp4, mjpeg).
* @type {string}
*/
this.mode = "webrtc,mse,mp4,mjpeg";
this.mode = 'webrtc,mse,hls,mjpeg';
/**
* [config] Run stream when not displayed on the screen. Default `false`.
@@ -92,7 +93,7 @@ export class VideoRTC extends HTMLElement {
/**
* @type {string|URL}
*/
this.wsURL = "";
this.wsURL = '';
/**
* @type {RTCPeerConnection}
@@ -107,7 +108,7 @@ export class VideoRTC extends HTMLElement {
/**
* @type {string}
*/
this.mseCodecs = "";
this.mseCodecs = '';
/**
* [internal] Disconnect TimeoutID.
@@ -139,11 +140,11 @@ export class VideoRTC extends HTMLElement {
* @param {string|URL} value
*/
set src(value) {
if (typeof value !== "string") value = value.toString();
if (value.startsWith("http")) {
value = "ws" + value.substring(4);
} else if (value.startsWith("/")) {
value = "ws" + location.origin.substring(4) + value;
if (typeof value !== 'string') value = value.toString();
if (value.startsWith('http')) {
value = 'ws' + value.substring(4);
} else if (value.startsWith('/')) {
value = 'ws' + location.origin.substring(4) + value;
}
this.wsURL = value;
@@ -156,10 +157,12 @@ export class VideoRTC extends HTMLElement {
* https://developer.chrome.com/blog/autoplay/
*/
play() {
this.video.play().catch(er => {
if (er.name === "NotAllowedError" && !this.video.muted) {
this.video.play().catch(() => {
if (!this.video.muted) {
this.video.muted = true;
this.video.play().catch(() => console.debug);
this.video.play().catch(er => {
console.warn(er);
});
}
});
}
@@ -173,7 +176,7 @@ export class VideoRTC extends HTMLElement {
}
codecs(type) {
const test = type === "mse"
const test = type === 'mse'
? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`)
: codec => this.video.canPlayType(`video/mp4; codecs="${codec}"`);
return this.CODECS.filter(test).join();
@@ -227,30 +230,30 @@ export class VideoRTC extends HTMLElement {
* Creates child DOM elements. Called automatically once on `connectedCallback`.
*/
oninit() {
this.video = document.createElement("video");
this.video = document.createElement('video');
this.video.controls = true;
this.video.playsInline = true;
this.video.preload = "auto";
this.video.preload = 'auto';
this.video.style.display = "block"; // fix bottom margin 4px
this.video.style.width = "100%";
this.video.style.height = "100%"
this.video.style.display = 'block'; // fix bottom margin 4px
this.video.style.width = '100%';
this.video.style.height = '100%';
this.appendChild(this.video);
if (this.background) return;
if ("hidden" in document && this.visibilityCheck) {
document.addEventListener("visibilitychange", () => {
if ('hidden' in document && this.visibilityCheck) {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.disconnectedCallback();
} else if (this.isConnected) {
this.connectedCallback();
}
})
});
}
if ("IntersectionObserver" in window && this.visibilityThreshold) {
if ('IntersectionObserver' in window && this.visibilityThreshold) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) {
@@ -277,9 +280,9 @@ export class VideoRTC extends HTMLElement {
this.connectTS = Date.now();
this.ws = new WebSocket(this.wsURL);
this.ws.binaryType = "arraybuffer";
this.ws.addEventListener("open", ev => this.onopen(ev));
this.ws.addEventListener("close", ev => this.onclose(ev));
this.ws.binaryType = 'arraybuffer';
this.ws.addEventListener('open', () => this.onopen());
this.ws.addEventListener('close', () => this.onclose());
return true;
}
@@ -296,6 +299,9 @@ export class VideoRTC extends HTMLElement {
this.pc.close();
this.pc = null;
}
this.video.src = '';
this.video.srcObject = null;
}
/**
@@ -305,8 +311,8 @@ export class VideoRTC extends HTMLElement {
// CONNECTING => OPEN
this.wsState = WebSocket.OPEN;
this.ws.addEventListener("message", ev => {
if (typeof ev.data === "string") {
this.ws.addEventListener('message', ev => {
if (typeof ev.data === 'string') {
const msg = JSON.parse(ev.data);
for (const mode in this.onmessage) {
this.onmessage[mode](msg);
@@ -321,27 +327,30 @@ export class VideoRTC extends HTMLElement {
const modes = [];
if (this.mode.indexOf("mse") >= 0 && "MediaSource" in window) { // iPhone
modes.push("mse");
if (this.mode.indexOf('mse') >= 0 && 'MediaSource' in window) { // iPhone
modes.push('mse');
this.onmse();
} else if (this.mode.indexOf("mp4") >= 0) {
modes.push("mp4");
} else if (this.mode.indexOf('hls') >= 0 && this.video.canPlayType('application/vnd.apple.mpegurl')) {
modes.push('hls');
this.onhls();
} else if (this.mode.indexOf('mp4') >= 0) {
modes.push('mp4');
this.onmp4();
}
if (this.mode.indexOf("webrtc") >= 0 && "RTCPeerConnection" in window) { // macOS Desktop app
modes.push("webrtc");
if (this.mode.indexOf('webrtc') >= 0 && 'RTCPeerConnection' in window) { // macOS Desktop app
modes.push('webrtc');
this.onwebrtc();
}
if (this.mode.indexOf("mjpeg") >= 0) {
if (this.mode.indexOf('mjpeg') >= 0) {
if (modes.length) {
this.onmessage["mjpeg"] = msg => {
if (msg.type !== "error" || msg.value.indexOf(modes[0]) !== 0) return;
this.onmessage['mjpeg'] = msg => {
if (msg.type !== 'error' || msg.value.indexOf(modes[0]) !== 0) return;
this.onmjpeg();
}
};
} else {
modes.push("mjpeg");
modes.push('mjpeg');
this.onmjpeg();
}
}
@@ -372,25 +381,25 @@ export class VideoRTC extends HTMLElement {
onmse() {
const ms = new MediaSource();
ms.addEventListener("sourceopen", () => {
ms.addEventListener('sourceopen', () => {
URL.revokeObjectURL(this.video.src);
this.send({type: "mse", value: this.codecs("mse")});
this.send({type: 'mse', value: this.codecs('mse')});
}, {once: true});
this.video.src = URL.createObjectURL(ms);
this.video.srcObject = null;
this.play();
this.mseCodecs = "";
this.mseCodecs = '';
this.onmessage["mse"] = msg => {
if (msg.type !== "mse") return;
this.onmessage['mse'] = msg => {
if (msg.type !== 'mse') return;
this.mseCodecs = msg.value;
const sb = ms.addSourceBuffer(msg.value);
sb.mode = "segments"; // segments or sequence
sb.addEventListener("updateend", () => {
sb.mode = 'segments'; // segments or sequence
sb.addEventListener('updateend', () => {
if (sb.updating) return;
try {
@@ -428,23 +437,25 @@ export class VideoRTC extends HTMLElement {
// console.debug(e);
}
}
}
}
};
};
}
onwebrtc() {
const pc = new RTCPeerConnection(this.pcConfig);
/** @type {HTMLVideoElement} */
const video2 = document.createElement("video");
video2.addEventListener("loadeddata", ev => this.onpcvideo(ev), {once: true});
const video2 = document.createElement('video');
video2.addEventListener('loadeddata', ev => this.onpcvideo(ev), {once: true});
pc.addEventListener("icecandidate", ev => {
const candidate = ev.candidate ? ev.candidate.toJSON().candidate : "";
this.send({type: "webrtc/candidate", value: candidate});
pc.addEventListener('icecandidate', ev => {
if (ev.candidate && this.mode.indexOf('webrtc/tcp') >= 0 && ev.candidate.protocol === 'udp') return;
const candidate = ev.candidate ? ev.candidate.toJSON().candidate : '';
this.send({type: 'webrtc/candidate', value: candidate});
});
pc.addEventListener("track", ev => {
pc.addEventListener('track', ev => {
// when stream already init
if (video2.srcObject !== null) return;
@@ -457,8 +468,8 @@ export class VideoRTC extends HTMLElement {
video2.srcObject = ev.streams[0];
});
pc.addEventListener("connectionstatechange", () => {
if (pc.connectionState === "failed" || pc.connectionState === "disconnected") {
pc.addEventListener('connectionstatechange', () => {
if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
pc.close(); // stop next events
this.pcState = WebSocket.CLOSED;
@@ -468,33 +479,33 @@ export class VideoRTC extends HTMLElement {
}
});
this.onmessage["webrtc"] = msg => {
this.onmessage['webrtc'] = msg => {
switch (msg.type) {
case "webrtc/candidate":
pc.addIceCandidate({
candidate: msg.value,
sdpMid: "0"
}).catch(() => console.debug);
case 'webrtc/candidate':
if (this.mode.indexOf('webrtc/tcp') >= 0 && msg.value.indexOf(' udp ') > 0) return;
pc.addIceCandidate({candidate: msg.value, sdpMid: '0'}).catch(er => {
console.warn(er);
});
break;
case "webrtc/answer":
pc.setRemoteDescription({
type: "answer",
sdp: msg.value
}).catch(() => console.debug);
case 'webrtc/answer':
pc.setRemoteDescription({type: 'answer', sdp: msg.value}).catch(er => {
console.warn(er);
});
break;
case "error":
if (msg.value.indexOf("webrtc/offer") < 0) return;
case 'error':
if (msg.value.indexOf('webrtc/offer') < 0) return;
pc.close();
}
};
// Safari doesn't support "offerToReceiveVideo"
pc.addTransceiver("video", {direction: "recvonly"});
pc.addTransceiver("audio", {direction: "recvonly"});
pc.addTransceiver('video', {direction: 'recvonly'});
pc.addTransceiver('audio', {direction: 'recvonly'});
pc.createOffer().then(offer => {
pc.setLocalDescription(offer).then(() => {
this.send({type: "webrtc/offer", value: offer.sdp});
this.send({type: 'webrtc/offer', value: offer.sdp});
});
});
@@ -513,7 +524,7 @@ export class VideoRTC extends HTMLElement {
const state = this.pc.connectionState;
// Firefox doesn't support pc.connectionState
if (state === "connected" || state === "connecting" || !state) {
if (state === 'connected' || state === 'connecting' || !state) {
// Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE
let rtcPriority = 0, msePriority = 0;
@@ -522,9 +533,9 @@ export class VideoRTC extends HTMLElement {
if (ms.getVideoTracks().length > 0) rtcPriority += 0x220;
if (ms.getAudioTracks().length > 0) rtcPriority += 0x102;
if (this.mseCodecs.indexOf("hvc1.") >= 0) msePriority += 0x230;
if (this.mseCodecs.indexOf("avc1.") >= 0) msePriority += 0x210;
if (this.mseCodecs.indexOf("mp4a.") >= 0) msePriority += 0x101;
if (this.mseCodecs.indexOf('hvc1.') >= 0) msePriority += 0x230;
if (this.mseCodecs.indexOf('avc1.') >= 0) msePriority += 0x210;
if (this.mseCodecs.indexOf('mp4a.') >= 0) msePriority += 0x101;
if (rtcPriority >= msePriority) {
this.video.srcObject = ms;
@@ -548,25 +559,38 @@ export class VideoRTC extends HTMLElement {
onmjpeg() {
this.ondata = data => {
this.video.controls = false;
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data);
this.video.poster = 'data:image/jpeg;base64,' + VideoRTC.btoa(data);
};
this.send({type: "mjpeg"});
this.send({type: 'mjpeg'});
}
onhls() {
this.onmessage['hls'] = msg => {
if (msg.type !== 'hls') return;
const url = 'http' + this.wsURL.substring(2, this.wsURL.indexOf('/ws')) + '/hls/';
const playlist = msg.value.replace('hls/', url);
this.video.src = 'data:application/vnd.apple.mpegurl;base64,' + btoa(playlist);
this.play();
};
this.send({type: 'hls', value: this.codecs('hls')});
}
onmp4() {
/** @type {HTMLCanvasElement} **/
const canvas = document.createElement("canvas");
const canvas = document.createElement('canvas');
/** @type {CanvasRenderingContext2D} */
let context;
/** @type {HTMLVideoElement} */
const video2 = document.createElement("video");
const video2 = document.createElement('video');
video2.autoplay = true;
video2.playsInline = true;
video2.muted = true;
video2.addEventListener("loadeddata", ev => {
video2.addEventListener('loadeddata', () => {
if (!context) {
canvas.width = video2.videoWidth;
canvas.height = video2.videoHeight;
@@ -576,20 +600,20 @@ export class VideoRTC extends HTMLElement {
context.drawImage(video2, 0, 0, canvas.width, canvas.height);
this.video.controls = false;
this.video.poster = canvas.toDataURL("image/jpeg");
this.video.poster = canvas.toDataURL('image/jpeg');
});
this.ondata = data => {
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
video2.src = 'data:video/mp4;base64,' + VideoRTC.btoa(data);
};
this.send({type: "mp4", value: this.codecs("mp4")});
this.send({type: 'mp4', value: this.codecs('mp4')});
}
static btoa(buffer) {
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
let binary = "";
let binary = '';
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
+25 -24
View File
@@ -1,23 +1,23 @@
import {VideoRTC} from "./video-rtc.js";
import {VideoRTC} from './video-rtc.js';
class VideoStream extends VideoRTC {
set divMode(value) {
this.querySelector(".mode").innerText = value;
this.querySelector(".status").innerText = "";
this.querySelector('.mode').innerText = value;
this.querySelector('.status').innerText = '';
}
set divError(value) {
const state = this.querySelector(".mode").innerText;
if (state !== "loading") return;
this.querySelector(".mode").innerText = "error";
this.querySelector(".status").innerText = value;
const state = this.querySelector('.mode').innerText;
if (state !== 'loading') return;
this.querySelector('.mode').innerText = 'error';
this.querySelector('.status').innerText = value;
}
/**
* Custom GUI
*/
oninit() {
console.debug("stream.oninit");
console.debug('stream.oninit');
super.oninit();
this.innerHTML = `
@@ -43,56 +43,57 @@ class VideoStream extends VideoRTC {
</div>
`;
const info = this.querySelector(".info")
const info = this.querySelector('.info');
this.insertBefore(this.video, info);
}
onconnect() {
console.debug("stream.onconnect");
console.debug('stream.onconnect');
const result = super.onconnect();
if (result) this.divMode = "loading";
if (result) this.divMode = 'loading';
return result;
}
ondisconnect() {
console.debug("stream.ondisconnect");
console.debug('stream.ondisconnect');
super.ondisconnect();
}
onopen() {
console.debug("stream.onopen");
console.debug('stream.onopen');
const result = super.onopen();
this.onmessage["stream"] = msg => {
console.debug("stream.onmessge", msg);
this.onmessage['stream'] = msg => {
console.debug('stream.onmessge', msg);
switch (msg.type) {
case "error":
case 'error':
this.divError = msg.value;
break;
case "mse":
case "mp4":
case "mjpeg":
case 'mse':
case 'hls':
case 'mp4':
case 'mjpeg':
this.divMode = msg.type.toUpperCase();
break;
}
}
};
return result;
}
onclose() {
console.debug("stream.onclose");
console.debug('stream.onclose');
return super.onclose();
}
onpcvideo(ev) {
console.debug("stream.onpcvideo");
console.debug('stream.onpcvideo');
super.onpcvideo(ev);
if (this.pcState !== WebSocket.CLOSED) {
this.divMode = "RTC";
this.divMode = 'RTC';
}
}
}
customElements.define("video-stream", VideoStream);
customElements.define('video-stream', VideoStream);
+17 -17
View File
@@ -22,45 +22,45 @@
async function PeerConnection(media) {
const pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
})
});
document.getElementById('video').srcObject = new MediaStream([
pc.addTransceiver('audio', {direction: 'sendrecv'}).receiver.track,
pc.addTransceiver('video', {direction: 'sendrecv'}).receiver.track,
])
]);
const tracks = await navigator.mediaDevices.getUserMedia({
video: media.indexOf('camera') >= 0,
audio: media.indexOf('microphone') >= 0,
})
});
tracks.getTracks().forEach(track => {
pc.addTrack(track)
})
pc.addTrack(track);
});
return pc
return pc;
}
function getCompleteOffer(pc, timeout) {
return new Promise((resolve, reject) => {
pc.addEventListener('icegatheringstatechange', () => {
if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp)
})
if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp);
});
pc.createOffer().then(offer => pc.setLocalDescription(offer))
pc.createOffer().then(offer => pc.setLocalDescription(offer));
setTimeout(() => resolve(pc.localDescription.sdp), timeout || 3000)
})
setTimeout(() => resolve(pc.localDescription.sdp), timeout || 3000);
});
}
async function connect() {
const media = new URLSearchParams(location.search).get('media')
const pc = await PeerConnection(media)
const url = new URL('api/webrtc' + location.search, location.href)
const r = await fetch(url, {method: 'POST', body: await getCompleteOffer(pc)})
await pc.setRemoteDescription({type: 'answer', sdp: await r.text()})
const media = new URLSearchParams(location.search).get('media');
const pc = await PeerConnection(media);
const url = new URL('api/webrtc' + location.search, location.href);
const r = await fetch(url, {method: 'POST', body: await getCompleteOffer(pc)});
await pc.setRemoteDescription({type: 'answer', sdp: await r.text()});
}
connect()
connect();
</script>
</body>
</html>
+35 -35
View File
@@ -22,86 +22,86 @@
async function PeerConnection(media) {
const pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
})
});
const localTracks = []
const localTracks = [];
if (/camera|microphone/.test(media)) {
const tracks = await getMediaTracks('user', {
video: media.indexOf('camera') >= 0,
audio: media.indexOf('microphone') >= 0,
})
});
tracks.forEach(track => {
pc.addTransceiver(track, {direction: 'sendonly'})
if (track.kind === 'video') localTracks.push(track)
})
pc.addTransceiver(track, {direction: 'sendonly'});
if (track.kind === 'video') localTracks.push(track);
});
}
if (media.indexOf('display') >= 0) {
const tracks = await getMediaTracks('display', {
video: true,
audio: media.indexOf('speaker') >= 0,
})
});
tracks.forEach(track => {
pc.addTransceiver(track, {direction: 'sendonly'})
if (track.kind === 'video') localTracks.push(track)
})
pc.addTransceiver(track, {direction: 'sendonly'});
if (track.kind === 'video') localTracks.push(track);
});
}
if (/video|audio/.test(media)) {
const tracks = ['video', 'audio']
.filter(kind => media.indexOf(kind) >= 0)
.map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track)
localTracks.push(...tracks)
.map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track);
localTracks.push(...tracks);
}
document.getElementById('video').srcObject = new MediaStream(localTracks)
document.getElementById('video').srcObject = new MediaStream(localTracks);
return pc
return pc;
}
async function getMediaTracks(media, constraints) {
try {
const stream = media === 'user'
? await navigator.mediaDevices.getUserMedia(constraints)
: await navigator.mediaDevices.getDisplayMedia(constraints)
return stream.getTracks()
: await navigator.mediaDevices.getDisplayMedia(constraints);
return stream.getTracks();
} catch (e) {
console.warn(e)
return []
console.warn(e);
return [];
}
}
async function connect(media) {
const pc = await PeerConnection(media)
const url = new URL('api/ws' + location.search, location.href)
const ws = new WebSocket('ws' + url.toString().substring(4))
const pc = await PeerConnection(media);
const url = new URL('api/ws' + location.search, location.href);
const ws = new WebSocket('ws' + url.toString().substring(4));
ws.addEventListener('open', () => {
pc.addEventListener('icecandidate', ev => {
if (!ev.candidate) return
const msg = {type: 'webrtc/candidate', value: ev.candidate.candidate}
ws.send(JSON.stringify(msg))
})
if (!ev.candidate) return;
const msg = {type: 'webrtc/candidate', value: ev.candidate.candidate};
ws.send(JSON.stringify(msg));
});
pc.createOffer().then(offer => pc.setLocalDescription(offer)).then(() => {
const msg = {type: 'webrtc/offer', value: pc.localDescription.sdp}
ws.send(JSON.stringify(msg))
})
})
const msg = {type: 'webrtc/offer', value: pc.localDescription.sdp};
ws.send(JSON.stringify(msg));
});
});
ws.addEventListener('message', ev => {
const msg = JSON.parse(ev.data)
const msg = JSON.parse(ev.data);
if (msg.type === 'webrtc/candidate') {
pc.addIceCandidate({candidate: msg.value, sdpMid: '0'})
pc.addIceCandidate({candidate: msg.value, sdpMid: '0'});
} else if (msg.type === 'webrtc/answer') {
pc.setRemoteDescription({type: 'answer', sdp: msg.value})
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
}
})
});
}
const media = new URLSearchParams(location.search).get('media')
connect(media || 'video+audio')
const media = new URLSearchParams(location.search).get('media');
connect(media || 'video+audio');
</script>
</body>
</html>