Compare commits

...

153 Commits

Author SHA1 Message Date
Alexey Khit
2243110e08 Fix H265 support for different sources 2022-09-21 23:43:52 +03:00
Alexey Khit
04a6e64650 Adds support WebRTC + H265 to Safari 2022-09-21 22:28:59 +03:00
Alexey Khit
62c13f016b Remove broken Safari codec from WebRTC API 2022-09-21 22:27:46 +03:00
Alexey Khit
9596c6139f Adds support H265 for MP4 2022-09-18 20:24:21 +03:00
Alexey Khit
34f5b99126 Update codecs.html 2022-09-18 17:37:44 +03:00
Alexey Khit
b562392d45 Remove unnecessary imports 2022-09-18 17:14:59 +03:00
Alexey Khit
eb8a4919a2 Adds fix on RemoveConsumer panic 2022-09-18 17:14:18 +03:00
Alexey Khit
237fbf23a1 FIx backward support for RTSPtoWebRTC API 2022-09-18 02:01:43 +03:00
Alexey Khit
12a73b00cb Fix readme 2022-09-17 20:32:17 +03:00
Alexey Khit
ce0fac959f Adds module MJPEG 2022-09-17 19:13:25 +03:00
Alexey Khit
1b14be7033 Update readme about Hass module 2022-09-17 06:41:56 +03:00
Alexey Khit
bbbade4097 Adds rtsp link to index.html 2022-09-16 17:59:54 +03:00
Alexey Khit
8f43ad2a35 Adds pretty print to info 2022-09-16 17:39:29 +03:00
Alexey Khit
105331d50f Fix track async access 2022-09-16 17:22:48 +03:00
Alexey Khit
a45d0b507b Code refactoring 2022-09-16 17:04:00 +03:00
Alexey Khit
407ccc45bc Adds URL templates to integration with Hass 2022-09-16 17:03:03 +03:00
Alexey Khit
428628fcce Code refactoring 2022-09-16 17:00:56 +03:00
Alexey Khit
fa23bb6899 Handle FFmpeg RTMP as HTTP source 2022-09-16 17:00:24 +03:00
Alexey Khit
71e1c840a7 Fix base_path for integration with Hass 2022-09-16 14:19:23 +03:00
Alexey Khit
63b9639e86 Adds trace logs for API 2022-09-16 12:11:40 +03:00
Alexey Khit
ae3e1372c8 Adds support RTSPtoWeb API (entity_id for zero config from Hass) 2022-09-16 12:10:58 +03:00
Alexey Khit
800ebb39be Adds canditates from domain resolver 2022-09-15 09:07:53 +03:00
Alexey Khit
3a10cb25bb Fix green video from RTSP H264 2022-09-15 06:55:05 +03:00
Alexey Khit
7784b0e64c Adds about ivideon to readme 2022-09-14 17:59:12 +03:00
Alexey Khit
945b486fe0 Update readme about new source Echo 2022-09-14 15:53:46 +03:00
Alexey Khit
d72d7b089c Reduce docker container size and add python inside 2022-09-14 14:12:43 +03:00
Alexey Khit
d339fbe712 Fix backchannel option for Dahua VTO2111D 2022-09-13 21:57:07 +03:00
Alexey Khit
3aeb278c47 Option to disable backchannel for RTSP 2022-09-13 21:54:49 +03:00
Alexey Khit
c92c1fc3e9 Adds echo source 2022-09-13 15:42:39 +03:00
Alexey Khit
def57119f4 Move shell QuoteSplit to separate pkg 2022-09-13 15:42:23 +03:00
Alexey Khit
b20275d2b5 Adds support ivideon source 2022-09-13 14:41:28 +03:00
Alexey Khit
a11ca1da6e Adds error output to producer start function 2022-09-13 14:40:58 +03:00
Alexey Khit
0fb7132947 Move SplitAVC to public function 2022-09-13 14:39:55 +03:00
Alexey Khit
0f9e3c97c5 Update mp4 entry duration 2022-09-13 14:39:19 +03:00
Alexey Khit
e049a17216 Adds error handler for mp4 init 2022-09-13 14:38:54 +03:00
Alexey Khit
217c8c2bf6 Update readme about MP4 module 2022-09-10 06:26:15 +03:00
Alexey Khit
9f0153e2a8 Adds skip SEI frame 2022-09-09 19:32:56 +03:00
Alexey Khit
b2eaf03914 Adds match any clockrate or channels 2022-09-09 19:32:36 +03:00
Alexey Khit
8b54444c89 Adds mp4 module 2022-09-09 19:31:52 +03:00
Alexey Khit
76b352d67f Add exec launch time to debug 2022-09-07 12:21:36 +03:00
Alexey Khit
e8edb65a31 Adds ignoring unnecessary ffmpeg rtsp input tracks 2022-09-07 11:31:15 +03:00
Alexey Khit
88a6208912 Update ffmpeg output param name 2022-09-07 11:29:59 +03:00
Alexey Khit
14b6df68ce Adds support nginx with wrong port 2022-09-06 18:09:44 +03:00
Alexey Khit
77080663ee Add the feature for update to any version 2022-09-06 14:10:08 +03:00
Alexey Khit
d25d27a0ee Fix SDP parsing for noname camera 2022-09-06 12:43:10 +03:00
Alexey Khit
5460e194e8 Update loggers 2022-09-06 07:08:35 +03:00
Alexey Khit
e4f565f343 Fix H264 in RTSP processing 2022-09-06 07:00:22 +03:00
Alexey Khit
6b274f2a37 Adds Hass stream info to log 2022-09-06 06:36:13 +03:00
Alexey Khit
f442aab176 Fix RTSP from RTMP stream 2022-09-05 20:04:30 +03:00
Alexey Khit
0e71bd4dcb Adds low delay for any ffmpeg source 2022-09-04 23:02:54 +03:00
Alexey Khit
e3618d70c3 Adds support MPA codec 2022-09-04 22:34:39 +03:00
Alexey Khit
99c4a3e34a Update RTSP Setup link logic 2022-09-04 21:43:32 +03:00
Alexey Khit
b78de349ab Hide streams from Hass by default 2022-09-02 20:52:39 +03:00
Alexey Khit
b4990b1e90 Fix support RTSPtoWebRTC API 2022-09-02 20:52:22 +03:00
Alexey Khit
687bdadba6 Update docs about HomeKit 2022-09-01 16:11:47 +03:00
Alexey Khit
c0c96cfcdb Adds support HomeKit cameras! 2022-09-01 16:11:34 +03:00
Alexey Khit
4f92608f33 Fix webrtc close when it starts from ws 2022-08-31 23:09:01 +03:00
Alexey Khit
ac49bbef4d Update readme 2022-08-28 06:44:18 +03:00
Alexey Khit
f2aedbaf04 Adds build script for linux amd 2022-08-28 06:29:37 +03:00
Alexey Khit
1654ac8c82 Move ffmpeg device to separate module 2022-08-28 06:29:16 +03:00
Alexey Khit
38a18cab62 Clear webrtc resources on failed connection 2022-08-27 15:57:47 +03:00
Alexey Khit
a006394e5f Fix empty webrtc remote 2022-08-27 15:57:16 +03:00
Alexey Khit
1b3024b055 Adds license 2022-08-27 06:35:30 +03:00
Alexey Khit
9101cd4458 Adds warn about reading config 2022-08-26 20:14:26 +03:00
Alexey Khit
62c0fcd1ed Adds about video rotation to readme 2022-08-26 17:14:54 +03:00
Alexey Khit
ff810d3394 Return support skip RTSPS verification 2022-08-26 17:14:29 +03:00
Alexey Khit
14dae12ce2 Adds webcam support to readme 2022-08-26 11:54:26 +03:00
Alexey Khit
64247fc90f Adds support local USB cameras 2022-08-26 10:43:35 +03:00
Alexey Khit
c019dc58b1 Update panic message for RTMP 2022-08-26 10:10:45 +03:00
Alexey Khit
d3adaf05b1 Update build scripts 2022-08-26 10:09:52 +03:00
Alexey Khit
e6cfd1818b Adds on the fly add producer feature 2022-08-26 10:07:35 +03:00
Alexey Khit
fae4398d21 Adds FAQ to readme 2022-08-26 07:22:12 +03:00
Alexey Khit
5daf043937 Update readme 2022-08-25 14:21:34 +03:00
Alexey Khit
18d7b9075b Autoadd cameras from Hass config 2022-08-25 06:41:39 +03:00
Alexey Khit
7c4497f856 Fix incoming RTSP without OPTIONS 2022-08-25 06:38:08 +03:00
Alexey Khit
befa4ca1e6 Remove wrong RTSP channel panic 2022-08-25 06:37:47 +03:00
Alexey Khit
dd3b326f7a Update readme 2022-08-24 14:41:00 +03:00
Alexey Khit
e36123bb19 Update build docker 2022-08-24 14:30:02 +03:00
Alexey Khit
9310343ad3 Update docker cwd to /config 2022-08-24 12:49:59 +03:00
Alexey Khit
e2d4fa3393 Advanced debug on app start 2022-08-24 12:49:48 +03:00
Alexey Khit
5fea2932c1 Error on wrong config 2022-08-24 12:49:40 +03:00
Alexey Khit
1fd110b70d Update readme 2022-08-24 09:55:34 +03:00
Alexey Khit
8377cf2655 Change url param to src in Web API 2022-08-24 09:55:16 +03:00
Alexey Khit
8f01b08d42 Code refactoring 2022-08-24 09:54:28 +03:00
Alexey Khit
97ce4c3114 Adds Security section to readme 2022-08-23 09:34:06 +03:00
Alexey Khit
4813a64d9d Adds build script for mips 2022-08-23 05:43:15 +03:00
Alexey Khit
7923ec74a8 Adds network filter for webrtc 2022-08-23 05:43:01 +03:00
Alexey Khit
1f0a5fb880 Stop WebRTC conn on AddConsumer error 2022-08-22 22:46:08 +03:00
Alexey Khit
c6a3ee65b8 Remove UPX from Windows builds because antiviruses 2022-08-22 22:32:23 +03:00
Alexey Khit
12b712426d Fix busy RTSP backchannel 2022-08-22 15:41:25 +03:00
Alexey Khit
a9af245ef8 Fix async requests to Producer 2022-08-22 15:40:28 +03:00
Alexey Khit
f251129a2f Fix RTSP Transport header parsing 2022-08-22 14:46:39 +03:00
Alexey Khit
d28debabe9 Update fix for parsing RTSP SDP 2022-08-22 14:44:33 +03:00
Alexey Khit
07bf00f9f6 Update readme 2022-08-22 13:40:58 +03:00
Alexey Khit
be6ec7dbb9 Fix RTSP requests for some cameras 2022-08-22 13:38:26 +03:00
Alexey Khit
4e575d1356 Adds build file for win64 2022-08-22 11:43:42 +03:00
Alexey Khit
4cbacfec0c Adds empty response on RTSP error 2022-08-22 11:43:26 +03:00
Alexey Khit
31e24c6e03 Adds stop with empty producer warning 2022-08-22 11:33:38 +03:00
Alexey Khit
401bf85a10 Update RTSP error output 2022-08-22 09:09:18 +03:00
Alexey Khit
f36851f83a Fix response with empty producer 2022-08-22 09:06:40 +03:00
Alexey Khit
67522dbb19 Update readme 2022-08-22 08:44:27 +03:00
Alexey Khit
26b5745f0a Adds keep-alive to RTSP connection 2022-08-22 06:54:58 +03:00
Alexey Khit
46f6a5d8e1 Return unmodified errors from RTSP 2022-08-22 06:54:42 +03:00
Alexey Khit
48f58d0669 Fix wrong stream name request 2022-08-22 06:54:08 +03:00
Alexey Khit
fd0b8f3c39 Fix RTMP with audio 2022-08-22 05:46:22 +03:00
Alexey Khit
863bf503e2 Fix empty remote for webrtc 2022-08-21 18:00:02 +03:00
Alexey Khit
7a3a1a5336 Fix empty producer track 2022-08-21 17:51:36 +03:00
Alexey Khit
b851041caa Fix concurrent map iteration for Track 2022-08-21 17:51:19 +03:00
Alexey Khit
a4acde6d95 Fix two connections to Dahua camera simultaniosly 2022-08-21 17:26:27 +03:00
Alexey Khit
1139d4fcad Fix wrong RTSP Transport responses 2022-08-21 16:58:35 +03:00
Alexey Khit
159ad52277 Fix RTSP Content-Base requests 2022-08-21 16:45:43 +03:00
Alexey Khit
87bc07e404 Update readme 2022-08-21 13:38:42 +03:00
Alexey Khit
d1b29275d7 Adds API for create and delete stream 2022-08-21 09:29:44 +03:00
Alexey Khit
7560bcbc83 Adds log info about serve static dir 2022-08-21 09:29:20 +03:00
Alexey Khit
090c360747 Adds fast script for building linux/amd64 2022-08-21 09:28:47 +03:00
Alexey Khit
a81bf0daa8 Update web interface 2022-08-21 09:28:26 +03:00
Alexey Khit
c7128897b8 Fix webrtc ontrack panic 2022-08-21 09:27:33 +03:00
Alexey Khit
07def5ba04 Adds restarts support to docker container 2022-08-21 09:27:02 +03:00
Alexey Khit
b7f4c63517 Update exec timeout to 15 2022-08-21 06:56:43 +03:00
Alexey Khit
92c67df7b4 Rewrite ffmpeg query format 2022-08-21 06:56:24 +03:00
Alexey Khit
64c0f287ed Adds ffmpeg and ngrok to docker 2022-08-21 00:38:04 +03:00
Alexey Khit
d96af31f86 Adds builder action 2022-08-20 23:27:02 +03:00
Alexey Khit
cc55281f12 Fix known goroutines 2022-08-20 15:52:15 +03:00
Alexey Khit
c10d619df8 Fix unclosed GetPublicIP request 2022-08-20 15:51:47 +03:00
Alexey Khit
65f451e0c5 Fix incoming rtsp url from Hass 2022-08-20 15:50:58 +03:00
Alexey Khit
ecd46700db Support candidates for Hass integration 2022-08-19 18:06:16 +03:00
Alexey Khit
2f588c77c4 Show all streams in list 2022-08-19 18:05:32 +03:00
Alexey Khit
f879663f55 Adds static www content to binary 2022-08-19 16:56:44 +03:00
Alexey Khit
3e1f4a0110 Update defaul ports 2022-08-19 15:41:48 +03:00
Alexey Khit
5b7e1a89d7 Adds UPX versions to build file 2022-08-19 14:58:50 +03:00
Alexey Khit
2c8f0a90f0 Adds inspired by section to readme 2022-08-19 14:57:05 +03:00
Alexey Khit
38438bbfae Update readme for scripts 2022-08-19 14:56:32 +03:00
Alexey Khit
c3b18097b9 Remove cmd.go and fix interrupts catch 2022-08-19 14:55:13 +03:00
Alexey Khit
2ecf3c4a70 Fix log wrong module warning 2022-08-19 09:12:26 +03:00
Alexey Khit
43a69531b3 Code cleanup 2022-08-19 09:09:50 +03:00
Alexey Khit
49182737c8 Adds readme to scripts 2022-08-19 09:09:13 +03:00
Alexey Khit
6d264d6336 Fix Hass integration 2022-08-19 06:55:43 +03:00
Alexey Khit
cc00633161 Adds stats to RTMP connection 2022-08-18 23:58:22 +03:00
Alexey Khit
7c23625a24 Support adding streams on the fly 2022-08-18 23:53:24 +03:00
Alexey Khit
6dceed64ed Fix RTMP to WebRTC 2022-08-18 23:25:09 +03:00
Alexey Khit
e0a3e5ae96 Fix RTMP to RTSP 2022-08-18 23:09:25 +03:00
Alexey Khit
7d064a8d33 Update readme about hass 2022-08-18 23:01:26 +03:00
Alexey Khit
95f6592571 Adds integration with default hass webrtc card 2022-08-18 22:47:48 +03:00
Alexey Khit
7cdf97c91a Adds ExchangeSDP function to webrtc module 2022-08-18 22:46:43 +03:00
Alexey Khit
e70a3629d7 Fix unknown codec type for SDP parsing 2022-08-18 22:45:59 +03:00
Alexey Khit
d8baea7741 Rewrite producer init to GetProducer func 2022-08-18 22:45:20 +03:00
Alexey Khit
69d45c3216 Make new stream function public 2022-08-18 22:44:38 +03:00
Alexey Khit
77c4590170 Adds keyframe API 2022-08-18 17:27:29 +03:00
Alexey Khit
90b37d809b Update readme about webrtc and ngrok 2022-08-18 17:26:53 +03:00
Alexey Khit
3b2d1c2728 Change ngrok candate log output 2022-08-18 17:26:11 +03:00
Alexey Khit
88a02938a5 Fix ngrok logger 2022-08-18 17:25:42 +03:00
Alexey Khit
a2ad01caad Check GetLogger module name 2022-08-18 17:24:40 +03:00
Alexey Khit
9862978bd9 Update readme 2022-08-18 13:30:24 +03:00
107 changed files with 7136 additions and 757 deletions

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

@@ -0,0 +1,59 @@
# https://github.com/home-assistant/builder
name: 'Builder'
on:
push:
tags: [ 'v*' ]
workflow_dispatch:
jobs:
hassio:
name: Hassio Addon
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Branch name
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "REPO=alexxit/go2rtc" >> $GITHUB_ENV
echo "TAG=${VERSION}" >> $GITHUB_ENV
echo "IMAGE=alexxit/go2rtc:${VERSION}" >> $GITHUB_ENV
- name: Build amd64
uses: home-assistant/builder@master
with:
args: --amd64 --target build/hassio --version $TAG-amd64 --no-latest --docker-hub-check
- name: Build i386
uses: home-assistant/builder@master
with:
args: --i386 --target build/hassio --version $TAG-i386 --no-latest --docker-hub-check
- name: Build aarch64
uses: home-assistant/builder@master
with:
args: --aarch64 --target build/hassio --version $TAG-aarch64 --no-latest --docker-hub-check
- name: Build armv7
uses: home-assistant/builder@master
with:
args: --armv7 --target build/hassio --version $TAG-armv7 --no-latest --docker-hub-check
- name: Docker manifest
run: |
# thanks to https://github.com/aler9/rtsp-simple-server/blob/main/Makefile
docker manifest create "${IMAGE}" \
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
docker manifest push "${IMAGE}"
docker manifest create "${REPO}:latest" \
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
docker manifest push "${REPO}:latest"

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@
.tmp/
go2rtc.yaml
go2rtc.json

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Alexey Khit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

509
README.md
View File

@@ -1,80 +1,178 @@
# go2rtc
**go2rtc** - ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
- zero-dependency and zero-config small app for all OS (Windows, macOS, Linux, ARM, etc.)
- zero-delay for all supported protocols (lowest possible streaming latency)
- zero-load on CPU for supported codecs
- on the fly transcoding for unsupported codecs via FFmpeg
- multi-source two-way [codecs negotiation](#codecs-negotiation)
- streaming from private networks via Ngrok or SSH-tunnels
![](assets/go2rtc.png)
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
- zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS/HTTP](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
- mixing tracks from different sources to single stream
- auto match client supported codecs
- 2-way audio for `ONVIF Profile T` Cameras
- streaming from private networks via [Ngrok](#module-ngrok)
- can be [integrated to](#module-api) any smart home platform or be used as [standalone app](#go2rtc-binary)
**Inspired by:**
- series of streaming projects from [@deepch](https://github.com/deepch)
- [webrtc](https://github.com/pion/webrtc) go library and whole [@pion](https://github.com/pion) team
- [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server) idea from [@aler9](https://github.com/aler9)
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
- [MediaSoup](https://mediasoup.org/) framework routing idea
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
## Codecs negotiation
For example, you want to watch stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your browser.
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because their audio codecs doesn't match with your browser codecs
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the PCMU/8000 or PCMA/8000
- now you have stream with two sources - **RTSP and FFmpeg**
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
`go2rtc` automatically match codecs for you browser and all your stream sources. This called **multi-source two-way codecs negotiation**. And this is one of the main features of this app.
**PS.** You can select PCMU or PCMA codec in camera setting and don't use transcoding at all. Or you can select AAC codec for main stream and PCMU codec for second stream and add both RTSP to YAML config, this also will work fine.
Now you have stream with two sources - **RTSP and FFmpeg**:
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif#audio=opus
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
```
![](codecs.svg)
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
![](assets/codecs.svg)
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
2. Open web interface: `http://localhost:1984/`
**Optionally:**
- add your [streams](#module-streams) to [config](#configuration) file
- setup [external access](#module-webrtc) to webrtc
- setup [external access](#module-ngrok) to web interface
- install [ffmpeg](#source-ffmpeg) for transcoding
**Developers:**
- write your own [web interface](#module-api)
- integrate [web api](#module-api) into your smart home platform
### go2rtc: Binary
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_mipsel` - Linux on MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
- `go2rtc_mac_amd64` - Mac with Intel
- `go2rtc_mac_arm64` - Mac with M1
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
### go2rtc: Home Assistant Add-on
[![](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons)
1. Install Add-On:
- Settings > Add-ons > Plus > Repositories > Add `https://github.com/AlexxIT/hassio-addons`
- go2rtc > Install > Start
2. Setup [Integration](#module-hass)
### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
```yaml
services:
go2rtc:
image: alexxit/go2rtc
network_mode: host
restart: always
volumes:
- "~/go2rtc.yaml:/config/go2rtc.yaml"
```
## Configuration
Create file `go2rtc.yaml` next to the app. Modules:
Create file `go2rtc.yaml` next to the app.
- [Streams](#streams)
- by default, you need to config only your `streams` links
- `api` server will start on default **1984 port**
- `rtsp` server will start on default **8554 port**
- `webrtc` will use random UDP port for each connection
- `ffmpeg` will use default transcoding options (you may install it [manually](https://ffmpeg.org/))
### Streams
Available modules:
**go2rtc** support different stream source types. You can setup only one link as stream source or multiple.
- [streams](#module-streams)
- [api](#module-api) - HTTP API (important for WebRTC support)
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
- [RTSP/RTSPS](#rtsp-source) - most cameras on market
- [RTMP](#rtmp-source)
- [FFmpeg/Exec](#ffmpeg-source) - FFmpeg integration
- [Hass](#hass-source) - Home Assistant integration
### Module: Streams
#### RTSP source
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
Available source types:
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
- [rtmp](#source-rtmp) - `RTMP` streams
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [echo](#source-echo) - get stream link from bash or python
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [ivideon](#source-ivideon) - public cameras from [Ivideon](https://tv.ivideon.com/) service
- [hass](#source-hass) - Home Assistant integration
#### Source: RTSP
- Support **RTSP and RTSPS** links with multiple video and audio tracks
- Support **2 way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
- Support **2-way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
**Attention:** proprietary 2 way audio standards are not supported!
**Attention:** other 2-way audio standards are not supported! ONVIF without Profile T is not supported!
```yaml
streams:
rtsp_camera: rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0
sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
```
If your camera support two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream:
If your camera has two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream.
**Attention:** Dahua cameras has different capabilities for different RTSP links. For example, it has support multiple codecs for two way audio with `&proto=Onvif` in link and only one coded without it.
**Attention:** Dahua cameras has different capabilities for different RTSP links. For example, it has support multiple codecs for 2-way audio with `&proto=Onvif` in link and only one codec without it.
```yaml
streams:
onvif_camera:
dahua_camera:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
```
#### RTMP source
#### Source: RTMP
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
@@ -83,22 +181,25 @@ streams:
rtmp_stream: rtmp://192.168.1.123/live/camera1
```
#### FFmpeg source
#### Source: FFmpeg
You can get any stream or file or device via FFmpeg and push it to go2rtc via RTSP protocol.
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
Format: `ffmpeg:{input}#{params}`. Examples:
- FFmpeg preistalled for **Docker** and **Hass Add-on** users
- **Hass Add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
```yaml
streams:
# [FILE] all tracks will be copied without transcoding codecs
file1: ffmpeg:~/media/BigBuckBunny.mp4
file1: ffmpeg:/media/BigBuckBunny.mp4
# [FILE] video will be transcoded to H264, audio will be skipped
file2: ffmpeg:~/media/BigBuckBunny.mp4#video=h264
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
# [FILE] video will be copied, audio will be transcoded to pcmu
file3: ffmpeg:~/media/BigBuckBunny.mp4#video=copy&audio=pcmu
file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu
# [HLS] video will be copied, audio will be skipped
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
@@ -106,82 +207,194 @@ streams:
# [MJPEG] video will be transcoded to H264
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264
# [RTSP] video and audio will be copied
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0#video=copy&audio=copy
# [RTSP] video with rotation, should be transcoded, so select H264
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#raw=-vf transpose=1#video=h264
```
All trascoding formats has built-in templates. But you can override them via YAML config:
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to config and use them with source params.
```yaml
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
link: -hide_banner -i {input} # if input is link
file: -hide_banner -re -stream_loop -1 -i {input} # if input not link
rtsp: -hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input} # if input is RTSP link
output: -rtsp_transport tcp -f rtsp {output} # output
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
h264/ultra: "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency"
h264/high: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency"
h265: "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency"
opus: "-codec:a libopus -ar 48000 -ac 2"
pcmu: "-codec:a pcm_mulaw -ar 8000 -ac 1"
pcmu/16000: "-codec:a pcm_mulaw -ar 16000 -ac 1"
pcmu/48000: "-codec:a pcm_mulaw -ar 48000 -ac 1"
pcma: "-codec:a pcm_alaw -ar 8000 -ac 1"
pcma/16000: "-codec:a pcm_alaw -ar 16000 -ac 1"
pcma/48000: "-codec:a pcm_alaw -ar 48000 -ac 1"
aac/16000: "-codec:a aac -ar 16000 -ac 1"
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
mycodec: "-any args that support ffmpeg..."
```
#### Exec source
Also you can use `raw` param for any additional FFmpeg arguments. As example for video rotation (`#raw=-vf transpose=1`). Remember that rotation is not possible without transcoding, so add supported codec as second param (`#video=h264`).
#### Source: FFmpeg Device
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
- check available devices in Web interface
- `resolution` and `framerate` must be supported by your camera!
- for Linux supported only video for now
- for macOS you can stream Facetime camera or whole Desktop!
- for macOS important to set right framerate
```yaml
streams:
linux_usbcam: ffmpeg:device?video=0&resolution=1280x720#video=h264
windows_webcam: ffmpeg:device?video=0#video=h264
macos_facetime: ffmpeg:device?video=0&audio=1&resolution=1280x720&framerate=30#video=h264#audio=pcma
```
#### Source: Exec
FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol:
```yaml
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i ~/media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
```
#### Hass source
#### Source: Echo
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files.
Some sources may have a dynamic link. And you will need to get it using a bash or python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
- support ONLY [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
**Docker** and **Hass Add-on** users has preinstalled `python3`, `curl`, `jq`.
Check examples in [wiki](https://github.com/AlexxIT/go2rtc/wiki/Source-Echo-examples).
```yaml
streams:
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
```
#### Source: HomeKit
**Important:**
- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home) - you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc - you can't pair it with iPhone
- HomeKit device should be in same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between device and go2rtc
go2rtc support import paired HomeKit devices from [Home Assistant](#source-hass). So you can use HomeKit camera with Hass and go2rtc simultaneously. If you using Hass, I recommend pairing devices with it, it will give you more options.
You can pair device with go2rtc on the HomeKit page. If you can't see your devices - reload the page. Also try reboot your HomeKit device (power off). If you still can't see it - you have a problems with mDNS.
If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it.
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
#### Source: Ivideon
Support public cameras from service [Ivideon](https://tv.ivideon.com/).
```yaml
streams:
quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0
```
#### Source: Hass
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
- support [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- support [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
```yaml
hass:
config: "~/.homeassistant"
config: "/config" # skip this setting if you Hass Add-on user
streams:
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
aqara_g3: hass:Camera-Hub-G3-AB12
```
### API server
### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
- you can use WebRTC only when HTTP API enabled
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
- you can change API `base_path` and host go2rtc on your main app webserver suburl
- all files from `static_dir` hosted on root path: `/`
```yaml
api:
listen: ":3000" # HTTP API port
base_path: "" # API prefix for serve on suburl
static_dir: "www" # folder for static files
listen: ":1984" # HTTP API port ("" - disabled)
base_path: "" # API prefix for serve on suburl
static_dir: "" # folder for static files (custom web interface)
```
### RTSP server
**PS. go2rtc** don't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks.
**PS2.** You can access microphone (for 2-way audio) only with HTTPS
### Module: RTSP
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
- you can omit the codec filters, so one first video and one first audio will be selected
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
- you can set multiple video or audio, so all of them will be selected
```yaml
rtsp:
listen: ":554"
listen: ":8554"
```
### WebRTC server
### Module: WebRTC
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
- by default, WebRTC use two random UDP ports for each connection (video and audio)
- you can enable one additional TCP port for all connections and use it for external access
**Static public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- add your external IP-address and external port to YAML config
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
- 192.168.1.123:8555 # ip you have problems with UDP in LAN
- stun # if you have dynamic public IP-address (auto discovery via STUN)
```
**Dynamic public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- add `stun` word and external port to YAML config
- go2rtc automatically detects your external address with STUN-server
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- stun:8555 # if you have dynamic public IP-address
```
**Private IP**
- add some TCP port to YAML config (ex. 8555)
- setup integration with [Ngrok service](#module-ngrok)
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
ngrok:
command: ...
```
**Own TCP-tunnel**
If you have personal VPS, you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
**Using TURN-server**
TODO...
```yaml
webrtc:
ice_servers:
- urls: [stun:stun.l.google.com:19302]
- urls: [turn:123.123.123.123:3478]
@@ -189,21 +402,113 @@ webrtc:
credential: your_pass
```
### Ngrok
### Module: Ngrok
With Ngrok integration you can get external access to your streams in situation when you have Internet with private IP-address.
- Ngrok preistalled for **Docker** and **Hass Add-on** users
- you may need external access for two different things:
- WebRTC stream, so you need tunnel WebRTC TCP port (ex. 8555)
- go2rtc web interface, so you need tunnel API HTTP port (ex. 1984)
- Ngrok support authorization for your web interface
- Ngrok automatically adds HTTPS to your web interface
Ngrok free subscription limitations:
- you will always get random external address (not a problem for webrtc stream)
- you can forward multiple ports but use only one Ngrok app
go2rtc will automatically get your external TCP address (if you enable it in ngrok config) and use it with WebRTC connection (if you enable it in webrtc config).
You need manually download [Ngrok agent app](https://ngrok.com/download) for your OS and register in [Ngrok service](https://ngrok.com/).
**Tunnel for only WebRTC Stream**
You need to add your [Ngrok token](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
```yaml
ngrok:
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
```
or
**Tunnel for WebRTC and Web interface**
You need to create `ngrok.yaml` config file and add it to go2rtc config:
```yaml
ngrok:
command: ngrok start --all --config ngrok.yml
command: ngrok start --all --config ngrok.yaml
```
### Log
Ngrok config example:
```yaml
version: "2"
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
tunnels:
api:
addr: 1984 # use the same port as in go2rtc config
proto: http
basic_auth:
- admin:password # you can set login/pass for your web interface
webrtc:
addr: 8555 # use the same port as in go2rtc config
proto: tcp
```
### Module: Hass
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
#### From go2rtc to Hass
Add any supported [stream source](#module-streams) as [Generic Camera](https://www.home-assistant.io/integrations/generic/) and view stream with built-in [Stream](https://www.home-assistant.io/integrations/stream/) integration. Technology `HLS`, supported codecs: `H264`, poor latency.
1. Add your stream to [go2rtc config](#configuration)
2. Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1`
#### From Hass to go2rtc
View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. Use Picture Entity or Picture Glance lovelace card
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
```yaml
streams:
"camera.hall": ffmpeg:{input}#video=copy#audio=opus
```
PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
### Module: MP4
Provides several features:
1. MSE stream (fMP4 over WebSocket)
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/)
3. Progressive MP4 stream - bad format for streaming because of high latency, doesn't work in Safari
### Module: MJPEG
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your camera outputs H264/H265 - you SHOULD use transcoding. With this example, your stream will have both H264 and MJPEG codecs:
```yaml
streams:
camera1:
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
- ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=mjpeg
```
Example link to MJPEG: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
### Module: Log
You can set different log levels for different modules.
```yaml
log:
@@ -215,3 +520,53 @@ log:
streams: error
webrtc: fatal
```
## Security
By default `go2rtc` start Web interface on port `1984` and RTSP on port `8554`. Both ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
```yaml
api:
listen: "127.0.0.1:1984" # localhost
rtsp:
listen: "127.0.0.1:8554" # localhost
webrtc:
listen: ":8555" # external TCP port
```
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
- local access to API is not a problem for [Home Assistant Add-on](#go2rtc-home-assistant-add-on), because Hass runs locally on same server and Add-on Web UI protected with Hass authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))
- external access to WebRTC TCP port is not a problem, because it used only for transmit encrypted media data
- anyway you need to open this port to your local network and to the Internet in order for WebRTC to work
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port.
## FAQ
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
**go2rtc** is a new version of the server-side [WebRTC Camera](https://github.com/AlexxIT/WebRTC) integration, completely rewritten from scratch, with a number of fixes and a huge number of new features. It is compatible with native Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. So you [can use](#module-hass) default lovelace Picture Entity or Picture Glance.
**Q. Why go2rtc is an addon and not an integration?**
Because **go2rtc** is more than just viewing your stream online with WebRTC. You can use it all the time for your various tasks. But every time the Hass is rebooted - all integrations are also rebooted. So your streams may be interrupted if you use them in additional tasks.
When **go2rtc** is released, the **WebRTC Camera** integration will be updated. And you can decide whether to use the integration or the addon.
**Q. Which RTSP link should I use inside Hass?**
You can use direct link to your cameras there (as you always do). **go2rtc** support zero-config feature. You may leave `streams` config section empty. And your streams will be created on the fly on first start from Hass. And your cameras will have multiple connections. Some from Hass directly and one from **go2rtc**.
Also you can specify your streams in **go2rtc** [config file](#configuration) and use RTSP links to this addon. With additional features: multi-source [codecs negotiation](#codecs-negotiation) or FFmpeg [transcoding](#source-ffmpeg) for unsupported codecs. Or use them as source for Frigate. And your cameras will have one connection from **go2rtc**. And **go2rtc** will have multiple connection - some from Hass via RTSP protocol, some from your browser via WebRTC protocol.
Use any config what you like.
**Q. What about lovelace card with support 2-way audio?**
At this moment I am focused on improving stability and adding new features to **go2rtc**. Maybe someone could write such a card themselves. It's not difficult, I have [some sketches](https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html).

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/go2rtc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

41
build/hassio/Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
ARG BUILD_FROM
FROM $BUILD_FROM as build
# 1. Build go2rtc
RUN apk add --no-cache git go
RUN git clone https://github.com/AlexxIT/go2rtc \
&& cd go2rtc \
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
# 2. Download ngrok
ARG BUILD_ARCH
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
&& cd go2rtc \
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
&& unzip ngrok
# https://devopscube.com/reduce-docker-image-size/
FROM $BUILD_FROM
# 3. Copy go2rtc and ngrok to release
COPY --from=build /go2rtc/go2rtc /usr/local/bin
COPY --from=build /go2rtc/ngrok /usr/local/bin
# 4. Install ffmpeg
# apk base OK: 22 MiB in 40 packages
# ffmpeg OK: 113 MiB in 110 packages
# python3 OK: 161 MiB in 114 packages
RUN apk add --no-cache ffmpeg python3
# 5. Copy run to release
COPY run.sh /
RUN chmod a+x /run.sh
CMD [ "/run.sh" ]

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

@@ -0,0 +1,6 @@
# https://github.com/home-assistant/builder/blob/master/builder.sh
name: go2rtc
description: Ultimate camera streaming application
url: https://github.com/AlexxIT/go2rtc
image: alexxit/go2rtc
arch: [ amd64, aarch64, i386, armv7 ]

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

@@ -0,0 +1,14 @@
#!/usr/bin/with-contenv bashio
set +e
# set cwd for go2rtc (for config file, Hass integration, etc)
cd /config
# add the feature to override go2rtc binary from Hass config folder
export PATH="/config:$PATH"
while true; do
go2rtc
sleep 5
done

View File

@@ -21,8 +21,7 @@ func Init() {
}
// default config
cfg.Mod.Listen = ":3000"
cfg.Mod.StaticDir = "www"
cfg.Mod.Listen = ":1984"
// load config from YAML
app.LoadConfig(&cfg)
@@ -34,33 +33,46 @@ func Init() {
basePath = cfg.Mod.BasePath
log = app.GetLogger("api")
if cfg.Mod.StaticDir != "" {
fileServer = http.FileServer(http.Dir(cfg.Mod.StaticDir))
HandleFunc("/", fileServerHandlder)
}
initStatic(cfg.Mod.StaticDir)
initWS()
HandleFunc("/api/stack", stackHandler)
HandleFunc("/api/stats", statsHandler)
HandleFunc("/api/ws", apiWS)
HandleFunc("api/streams", streamsHandler)
HandleFunc("api/ws", apiWS)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
return
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
go func() {
s := http.Server{}
if log.Trace().Enabled() {
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Stringer("url", r.URL).Msgf("[api] %s", r.Method)
http.DefaultServeMux.ServeHTTP(w, r)
})
}
if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] Serve")
log.Fatal().Err(err).Msg("[api] serve")
}
}()
}
// HandleFunc handle pattern with relative path:
// - "api/streams" => "{basepath}/api/streams"
// - "/streams" => "/streams"
func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(basePath+pattern, handler)
if len(pattern) == 0 || pattern[0] != '/' {
pattern = basePath + "/" + pattern
}
log.Trace().Str("path", pattern).Msg("[api] register path")
http.HandleFunc(pattern, handler)
}
func HandleWS(msgType string, handler WSHandler) {
@@ -68,28 +80,31 @@ func HandleWS(msgType string, handler WSHandler) {
}
var basePath string
var fileServer http.Handler
var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler)
func fileServerHandlder(w http.ResponseWriter, r *http.Request) {
if basePath != "" {
r.URL.Path = r.URL.Path[len(basePath):]
}
fileServer.ServeHTTP(w, r)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
func statsHandler(w http.ResponseWriter, _ *http.Request) {
v := map[string]interface{}{
"streams": streams.Streams,
switch r.Method {
case "PUT":
streams.New(src, src)
return
case "DELETE":
streams.Delete(src)
return
}
data, err := json.Marshal(v)
if err != nil {
log.Error().Err(err).Msg("[api.stats] marshal")
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.stats] write")
var v interface{}
if src != "" {
v = streams.Get(src)
} else {
v = streams.All()
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(v)
}
func apiWS(w http.ResponseWriter, r *http.Request) {

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

@@ -0,0 +1,26 @@
package api
import (
"github.com/AlexxIT/go2rtc/www"
"net/http"
)
func initStatic(staticDir string) {
var root http.FileSystem
if staticDir != "" {
log.Info().Str("dir", staticDir).Msg("[api] serve static")
root = http.Dir(staticDir)
} else {
root = http.FS(www.Static)
}
base := len(basePath)
fileServer := http.FileServer(root)
HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
if base > 0 {
r.URL.Path = r.URL.Path[base:]
}
fileServer.ServeHTTP(w, r)
})
}

View File

@@ -4,16 +4,42 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket"
"net/http"
"net/url"
"strings"
"sync"
)
type WSHandler func(ctx *Context, msg *streamer.Message)
var apiWsUp = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 512000,
func initWS() {
wsUp = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 512000,
}
wsUp.CheckOrigin = func(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true
}
o, err := url.Parse(origin[0])
if err != nil {
return false
}
if o.Host == r.Host {
return true
}
log.Trace().Msgf("[api.ws] origin: %s, host: %s", o.Host, r.Host)
// some users change Nginx external port using Docker port
// so origin will be with a port and host without
if i := strings.IndexByte(o.Host, ':'); i > 0 {
return o.Host[:i] == r.Host
}
return false
}
}
var wsUp *websocket.Upgrader
type WSHandler func(ctx *Context, msg *streamer.Message)
type Context struct {
Conn *websocket.Conn
Request *http.Request
@@ -24,7 +50,7 @@ type Context struct {
}
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
ctx.Conn, err = apiWsUp.Upgrade(w, r, nil)
ctx.Conn, err = wsUp.Upgrade(w, r, nil)
ctx.Request = r
return
}

View File

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

61
cmd/app/store/store.go Normal file
View File

@@ -0,0 +1,61 @@
package store
import (
"encoding/json"
"github.com/rs/zerolog/log"
"os"
)
const name = "go2rtc.json"
var store map[string]interface{}
func load() {
data, _ := os.ReadFile(name)
if data != nil {
if err := json.Unmarshal(data, &store); err != nil {
// TODO: log
log.Warn().Err(err).Msg("[app] read storage")
}
}
if store == nil {
store = make(map[string]interface{})
}
}
func save() error {
data, err := json.Marshal(store)
if err != nil {
return err
}
return os.WriteFile(name, data, 0644)
}
func GetRaw(key string) interface{} {
if store == nil {
load()
}
return store[key]
}
func GetDict(key string) map[string]interface{} {
raw := GetRaw(key)
if raw != nil {
return raw.(map[string]interface{})
}
return make(map[string]interface{})
}
func Set(key string, v interface{}) error {
if store == nil {
load()
}
store[key] = v
return save()
}

View File

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

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

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

View File

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

29
cmd/echo/echo.go Normal file
View File

@@ -0,0 +1,29 @@
package echo
import (
"bytes"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
)
func Init() {
log := app.GetLogger("echo")
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
args := shell.QuoteSplit(url[5:])
b, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return nil, err
}
b = bytes.TrimSpace(b)
log.Debug().Str("url", url).Msgf("[echo] %s", b)
return streams.GetProducer(string(b))
})
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
@@ -49,7 +50,7 @@ func Handle(url string) (streamer.Producer, error) {
)
// remove `exec:`
args := strings.Split(url[5:], " ")
args := shell.QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() {
@@ -64,17 +65,20 @@ func Handle(url string) (streamer.Producer, error) {
log.Debug().Str("url", url).Msg("[exec] run")
ts := time.Now()
if err := cmd.Start(); err != nil {
log.Error().Err(err).Str("url", url).Msg("[exec]")
return nil, err
}
select {
case <-time.After(time.Second * 10):
case <-time.After(time.Second * 15):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")
case prod := <-ch:
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
return prod, nil
}
}

View File

@@ -1,6 +1,43 @@
## Devices Windows
```
>ffmpeg -hide_banner -f dshow -list_options true -i video="VMware Virtual USB Video Device"
[dshow @ 0000025695e52900] DirectShow video device options (from video devices)
[dshow @ 0000025695e52900] Pin "Record" (alternative pin name "0")
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10 (tv, bt470bg/bt709/unknown, topleft)
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23 (tv, bt470bg/bt709/unknown, topleft)
```
## Devices Mac
```
% ./ffmpeg -hide_banner -f avfoundation -list_devices true -i ""
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation video devices:
[AVFoundation indev @ 0x7f8b1f504d80] [0] FaceTime HD Camera
[AVFoundation indev @ 0x7f8b1f504d80] [1] Capture screen 0
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation audio devices:
[AVFoundation indev @ 0x7f8b1f504d80] [0] Soundflower (2ch)
[AVFoundation indev @ 0x7f8b1f504d80] [1] Built-in Microphone
[AVFoundation indev @ 0x7f8b1f504d80] [2] Soundflower (64ch)
```
## Devices Linux
```
# ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Raw : yuyv422 : YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
```
## Useful links
- https://superuser.com/questions/564402/explanation-of-x264-tune
- https://stackoverflow.com/questions/33624016/why-sliced-thread-affect-so-much-on-realtime-encoding-using-ffmpeg-x264
- https://codec.fandom.com/ru/wiki/X264_-_%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B9_%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F
- https://codec.fandom.com/ru/wiki/X264_-_описание_ключей_кодирования
- https://html5test.com/
- https://trac.ffmpeg.org/wiki/Capture/Webcam
- https://trac.ffmpeg.org/wiki/DirectShow
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
- https://github.com/tuupola/esp_video/blob/master/README.md

View File

@@ -0,0 +1,63 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
audio := findMedia(streamer.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `"` + video.Title + `:` + audio.Title + `"`
case video != nil:
return `"` + video.Title + `"`
case audio != nil:
return `"` + audio.Title + `"`
}
return ""
}
func loadMedias() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy",
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
var kind string
lines := strings.Split(buf.String(), "\n")
process:
for _, line := range lines {
switch {
case strings.HasSuffix(line, "video devices:"):
kind = streamer.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = streamer.KindAudio
continue
case strings.HasPrefix(line, "dummy"):
break process
}
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera
name := line[42:]
media := loadMedia(kind, name)
medias = append(medias, media)
}
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{
Kind: kind, Title: name,
}
}

View File

@@ -0,0 +1,50 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"io/ioutil"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/Capture/Webcam
const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
return video.Title
}
func loadMedias() {
files, err := ioutil.ReadDir("/dev")
if err != nil {
return
}
for _, file := range files {
log.Trace().Msg("[ffmpeg] " + file.Name())
if strings.HasPrefix(file.Name(), streamer.KindVideo) {
media := loadMedia(streamer.KindVideo, "/dev/"+file.Name())
if media != nil {
medias = append(medias, media)
}
}
}
}
func loadMedia(kind, name string) *streamer.Media {
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
if !bytes.Contains(buf.Bytes(), []byte("Raw")) {
return nil
}
return &streamer.Media{
Kind: kind, Title: name,
}
}

View File

@@ -0,0 +1,59 @@
package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
"strings"
)
// https://trac.ffmpeg.org/wiki/DirectShow
const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
audio := findMedia(streamer.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `video="` + video.Title + `":audio=` + audio.Title + `"`
case video != nil:
return `video="` + video.Title + `"`
case audio != nil:
return `audio="` + audio.Title + `"`
}
return ""
}
func loadMedias() {
cmd := exec.Command(
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
)
var buf bytes.Buffer
cmd.Stderr = &buf
_ = cmd.Run()
lines := strings.Split(buf.String(), "\r\n")
for _, line := range lines {
var kind string
if strings.HasSuffix(line, "(video)") {
kind = streamer.KindVideo
} else if strings.HasSuffix(line, "(audio)") {
kind = streamer.KindAudio
} else {
continue
}
// hope we have constant prefix and suffix sizes
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video)
name := line[28 : len(line)-9]
media := loadMedia(kind, name)
medias = append(medias, media)
}
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{
Kind: kind, Title: name,
}
}

View File

@@ -0,0 +1,83 @@
package device
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"net/http"
"net/url"
"strconv"
"strings"
)
func Init() {
log = app.GetLogger("exec")
api.HandleFunc("api/devices", handle)
}
func GetInput(src string) (string, error) {
if medias == nil {
loadMedias()
}
input := deviceInputPrefix
var videoIdx, audioIdx int
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":
videoIdx, _ = strconv.Atoi(value[0])
case "audio":
audioIdx, _ = strconv.Atoi(value[0])
case "framerate":
input += " -framerate " + value[0]
case "resolution":
input += " -video_size " + value[0]
}
}
}
input += " -i " + deviceInputSuffix(videoIdx, audioIdx)
return input, nil
}
var Bin string
var log zerolog.Logger
var medias []*streamer.Media
func findMedia(kind string, index int) *streamer.Media {
for _, media := range medias {
if media.Kind != kind {
continue
}
if index == 0 {
return media
}
index--
}
return nil
}
func handle(w http.ResponseWriter, r *http.Request) {
if medias == nil {
loadMedias()
}
data, err := json.Marshal(medias)
if err != nil {
log.Error().Err(err).Msg("[api.ffmpeg]")
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.ffmpeg]")
}
}

View File

@@ -3,6 +3,7 @@ package ffmpeg
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/url"
@@ -20,21 +21,23 @@ func Init() {
"bin": "ffmpeg",
// inputs
"link": "-hide_banner -i {input}",
"rtsp": "-hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
"file": "-hide_banner -re -stream_loop -1 -i {input}",
"file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
// output
"out": "-rtsp_transport tcp -f rtsp {output}",
"output": "-rtsp_transport tcp -f rtsp {output}",
// `-g 30` - group of picture, GOP, keyframe interval
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile main -level 4.1` - most used streaming profile
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1",
// `-pix_fmt yuv420p` - if input pix format 4:2:2
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1 -pix_fmt yuv420p",
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
"mjpeg": "-codec:v mjpeg -force_duplicated_matrix 1 -huffman 0 -pix_fmt yuvj420p",
"opus": "-codec:a libopus -ar 48000 -ac 2",
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
@@ -53,23 +56,52 @@ func Init() {
s = s[7:] // remove `ffmpeg:`
var query url.Values
var queryVideo, queryAudio bool
if i := strings.IndexByte(s, '#'); i > 0 {
query, _ = url.ParseQuery(s[i+1:])
query = parseQuery(s[i+1:])
queryVideo = query["video"] != nil
queryAudio = query["audio"] != nil
s = s[:i]
} else {
// by default query both video and audio
queryVideo = true
queryAudio = true
}
var template string
switch {
case strings.HasPrefix(s, "rtsp"):
template = tpl["rtsp"]
case strings.Contains(s, "://"):
template = tpl["link"]
default:
template = tpl["file"]
var input string
if i := strings.IndexByte(s, ':'); i > 0 {
switch s[:i] {
case "http", "https", "rtmp":
input = strings.Replace(tpl["http"], "{input}", s, 1)
case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
// skip unnecessary input tracks
switch {
case queryVideo && queryAudio:
input = "-allowed_media_types video+audio "
case queryVideo:
input = "-allowed_media_types video "
case queryAudio:
input = "-allowed_media_types audio "
}
input += strings.Replace(tpl["rtsp"], "{input}", s, 1)
}
}
s = "exec:" + tpl["bin"] + " " +
strings.Replace(template, "{input}", s, 1)
if input == "" {
if strings.HasPrefix(s, "device?") {
var err error
input, err = device.GetInput(s)
if err != nil {
return nil, err
}
} else {
input = strings.Replace(tpl["file"], "{input}", s, 1)
}
}
s = "exec:" + tpl["bin"] + " -hide_banner " + input
if query != nil {
for _, raw := range query["raw"] {
@@ -95,18 +127,34 @@ func Init() {
}
}
if query["video"] == nil {
s += " -vn"
}
if query["audio"] == nil {
switch {
case queryVideo && !queryAudio:
s += " -an"
case queryAudio && !queryVideo:
s += " -vn"
}
} else {
s += " -c copy"
}
s += " " + tpl["out"]
s += " " + tpl["output"]
return exec.Handle(s)
})
device.Bin = cfg.Mod["bin"]
device.Init()
}
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
}

149
cmd/hass/api.go Normal file
View File

@@ -0,0 +1,149 @@
package hass
import (
"encoding/base64"
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"net/http"
"net/url"
"strings"
)
func initAPI() {
ok := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
}
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/streams", ok)
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
switch {
// /stream/{id}/add
case strings.HasSuffix(r.RequestURI, "/add"):
var v addJSON
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
return
}
// we can get three types of links:
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
// 2. static link to Hass camera
// 3. dynamic link to Hass camera
stream := streams.Get(v.Name)
if stream == nil {
// check if it is rtsp link to go2rtc
stream = rtspStream(v.Channels.First.Url)
if stream != nil {
streams.New(v.Name, stream)
} else {
stream = streams.New(v.Name, "{input}")
}
}
stream.SetSource(v.Channels.First.Url)
ok(w, r)
// /stream/{id}/channel/0/webrtc
default:
i := strings.IndexByte(r.RequestURI[8:], '/')
name := r.RequestURI[8 : 8+i]
stream := streams.Get(name)
if stream == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
s := r.FormValue("data")
offer, err := base64.StdEncoding.DecodeString(s)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
s = base64.StdEncoding.EncodeToString([]byte(s))
_, _ = w.Write([]byte(s))
}
})
// api from RTSPtoWebRTC
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return
}
src := r.FormValue("url")
src, err = url.QueryUnescape(src)
if err != nil {
return
}
stream := streams.Get(src)
if stream == nil {
if stream = rtspStream(src); stream != nil {
streams.New(src, stream)
} else {
stream = streams.New(src, src)
}
}
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
return
}
v := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
})
}
func rtspStream(url string) *streams.Stream {
if strings.HasPrefix(url, "rtsp://") {
if i := strings.IndexByte(url[7:], '/'); i > 0 {
return streams.Get(url[8+i:])
}
}
return nil
}
type addJSON struct {
Name string `json:"name"`
Channels struct {
First struct {
//Name string `json:"name"`
Url string `json:"url"`
} `json:"0"`
} `json:"channels"`
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
"path"
)
@@ -19,36 +20,61 @@ func Init() {
app.LoadConfig(&conf)
log = app.GetLogger("hass")
initAPI()
// support load cameras from Hass config file
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
data, err := os.ReadFile(filename)
if err != nil {
return
}
ent := new(entries)
if err = json.Unmarshal(data, ent); err != nil {
storage := new(entries)
if err = json.Unmarshal(data, storage); err != nil {
return
}
urls := map[string]string{}
for _, entrie := range ent.Data.Entries {
switch entrie.Domain {
case "generic":
if entrie.Options.StreamSource != "" {
urls[entrie.Title] = entrie.Options.StreamSource
}
}
}
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
if hurl := urls[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
}
return nil, fmt.Errorf("can't get url: %s", url)
})
for _, entrie := range storage.Data.Entries {
switch entrie.Domain {
case "generic":
if entrie.Options.StreamSource == "" {
continue
}
urls[entrie.Title] = entrie.Options.StreamSource
case "homekit_controller":
if entrie.Data.ClientID == "" {
continue
}
urls[entrie.Title] = fmt.Sprintf(
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
entrie.Data.DeviceHost, entrie.Data.DevicePort,
entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
entrie.Data.DeviceID, entrie.Data.DevicePublic,
)
default:
continue
}
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
//streams.Get("hass:" + entrie.Title)
}
}
var log zerolog.Logger
type entries struct {
Data struct {
Entries []struct {

149
cmd/homekit/api.go Normal file
View File

@@ -0,0 +1,149 @@
package homekit
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
"net/http"
"net/url"
"strings"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
items := make([]interface{}, 0)
for name, src := range store.GetDict("streams") {
if src := src.(string); strings.HasPrefix(src, "homekit") {
u, err := url.Parse(src)
if err != nil {
continue
}
device := Device{
Name: name,
Addr: u.Host,
Paired: true,
}
items = append(items, device)
}
}
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'
}
}
items = append(items, device)
}
_= json.NewEncoder(w).Encode(items)
case "POST":
// TODO: post params...
id := r.URL.Query().Get("id")
pin := r.URL.Query().Get("pin")
client, err := homekit.Pair(id, pin)
if err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] pair")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
name := r.URL.Query().Get("name")
dict := store.GetDict("streams")
dict[name] = client.URL()
if err = store.Set("streams", dict); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] save to store")
// response error
_, err = w.Write([]byte(err.Error()))
}
streams.New(name, client.URL())
case "DELETE":
src := r.URL.Query().Get("src")
dict := store.GetDict("streams")
for name, rawURL := range dict {
if name != src {
continue
}
client, err := homekit.NewClient(rawURL.(string))
if err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] new client")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
if err = client.Dial(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] client dial")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
go client.Handle()
if err = client.ListPairings(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
if err = client.DeletePairing(client.ClientID); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
}
delete(dict, name)
if err = store.Set("streams", dict); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] store set")
// response error
_, err = w.Write([]byte(err.Error()))
}
return
}
}
}
type Device struct {
ID string `json:"id"`
Name string `json:"name"`
Addr string `json:"addr"`
Model string `json:"model"`
Paired bool `json:"paired"`
//Type string `json:"type"`
}

39
cmd/homekit/homekit.go Normal file
View File

@@ -0,0 +1,39 @@
package homekit
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("api/homekit", apiHandler)
}
var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) {
client, err := homekit.NewClient(url)
if err != nil {
return nil, err
}
if err = client.Dial(); err != nil {
return nil, err
}
// start gorutine for reading responses from camera
go func() {
if err = client.Handle(); err != nil {
log.Warn().Err(err).Msg("[homekit] client")
}
}()
return &Producer{client: client}, nil
}

189
cmd/homekit/producer.go Normal file
View File

@@ -0,0 +1,189 @@
package homekit
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/homekit/camera"
pkg "github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
"net"
"strconv"
)
type Producer struct {
streamer.Element
client *homekit.Client
medias []*streamer.Media
tracks []*streamer.Track
sessions []*pkg.Session
}
func (c *Producer) GetMedias() []*streamer.Media {
if c.medias == nil {
c.medias = c.getMedias()
}
return c.medias
}
func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
track := &streamer.Track{Codec: codec, Direction: media.Direction}
c.tracks = append(c.tracks, track)
return track
}
func (c *Producer) Start() error {
if c.tracks == nil {
return errors.New("producer without tracks")
}
// get our server local IP-address
host, _, err := net.SplitHostPort(c.client.LocalAddr())
if err != nil {
return err
}
// get our server SRTP port
port, err := strconv.Atoi(srtp.Port)
if err != nil {
return err
}
// setup HomeKit stream session
hkSession := camera.NewSession()
hkSession.SetLocalEndpoint(host, uint16(port))
// create client for processing camera accessory
cam := camera.NewClient(c.client)
// try to start HomeKit stream
if err = cam.StartStream2(hkSession); err != nil {
panic(err) // TODO: fixme
}
// SRTP Video Session
vs := &pkg.Session{
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcVideo,
Track: c.tracks[0],
}
if err = vs.SetKeys(
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
); err != nil {
return err
}
// SRTP Audio Session
as := &pkg.Session{
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcAudio,
Track: &streamer.Track{},
}
if err = as.SetKeys(
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
); err != nil {
return err
}
srtp.AddSession(vs)
srtp.AddSession(as)
c.sessions = []*pkg.Session{vs, as}
return nil
}
func (c *Producer) Stop() error {
err := c.client.Close()
for _, session := range c.sessions {
srtp.RemoveSession(session)
}
return err
}
func (c *Producer) getMedias() []*streamer.Media {
var medias []*streamer.Media
accs, err := c.client.GetAccessories()
acc := accs[0]
if err != nil {
panic(err)
}
// get supported video config (not really necessary)
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
v1 := &rtp.VideoStreamConfiguration{}
if err = char.ReadTLV8(v1); err != nil {
panic(err)
}
for _, hkCodec := range v1.Codecs {
codec := &streamer.Codec{ClockRate: 90000}
switch hkCodec.Type {
case rtp.VideoCodecType_H264:
codec.Name = streamer.CodecH264
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
media := &streamer.Media{
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
v2 := &rtp.AudioStreamConfiguration{}
if err = char.ReadTLV8(v2); err != nil {
panic(err)
}
for _, hkCodec := range v2.Codecs {
codec := &streamer.Codec{
Channels: uint16(hkCodec.Parameters.Channels),
}
switch hkCodec.Type {
case rtp.AudioCodecType_AAC_ELD:
codec.Name = streamer.CodecAAC
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
switch hkCodec.Parameters.Samplerate {
case rtp.AudioCodecSampleRate8Khz:
codec.ClockRate = 8000
case rtp.AudioCodecSampleRate16Khz:
codec.ClockRate = 16000
case rtp.AudioCodecSampleRate24Khz:
codec.ClockRate = 24000
default:
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
}
media := &streamer.Media{
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
return medias
}

19
cmd/ivideon/ivideon.go Normal file
View File

@@ -0,0 +1,19 @@
package ivideon
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
func Init() {
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
id := strings.Replace(url[8:], "/", ":", 1)
prod := ivideon.NewClient(id)
if err := prod.Dial(); err != nil {
return nil, err
}
return prod, nil
})
}

54
cmd/mjpeg/mjpeg.go Normal file
View File

@@ -0,0 +1,54 @@
package mjpeg
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/rs/zerolog/log"
"net/http"
"strconv"
)
func Init() {
api.HandleFunc("api/stream.mjpeg", handler)
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan struct{})
cons := &mjpeg.Consumer{}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
data := []byte(header + strconv.Itoa(len(msg)))
data = append(data, 0x0D, 0x0A, 0x0D, 0x0A)
data = append(data, msg...)
data = append(data, 0x0D, 0x0A)
if _, err := w.Write(data); err != nil {
exit <- struct{}{}
}
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
return
}
w.Header().Set("Content-Type", `multipart/x-mixed-replace; boundary=frame`)
<-exit
stream.RemoveConsumer(cons)
//log.Trace().Msg("[api.mjpeg] close")
}

138
cmd/mp4/mp4.go Normal file
View File

@@ -0,0 +1,138 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/rs/zerolog"
"net/http"
"strconv"
"strings"
)
func Init() {
log = app.GetLogger("mp4")
api.HandleWS(MsgTypeMSE, handlerWS)
api.HandleFunc("api/frame.mp4", handlerKeyframe)
api.HandleFunc("api/stream.mp4", handlerMP4)
}
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
if isChromeFirst(w, r) {
return
}
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan []byte)
cons := &mp4.Consumer{}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
exit <- msg
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.keyframe] add consumer")
return
}
defer stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", cons.MimeType())
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Msg("[api.keyframe] init")
return
}
data = append(data, <-exit...)
// Apple Safari won't show frame without length
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.keyframe] add consumer")
}
}
func handlerMP4(w http.ResponseWriter, r *http.Request) {
if isChromeFirst(w, r) || isSafari(w, r) {
return
}
log.Trace().Msgf("[api.mp4] %+v", r)
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan struct{})
cons := &mp4.Consumer{}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
if _, err := w.Write(msg); err != nil {
exit <- struct{}{}
}
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mp4] add consumer")
return
}
defer stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", cons.MimeType())
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Msg("[api.mp4] init")
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.mp4] write")
return
}
<-exit
log.Trace().Msg("[api.mp4] close")
}
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
if strings.Contains(r.UserAgent(), " Chrome/") {
if r.Header.Values("Range") == nil {
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
return true
}
}
return false
}
func isSafari(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Range") == "bytes=0-1" {
handlerKeyframe(w, r)
return true
}
return false
}

View File

@@ -1,35 +1,34 @@
package mse
package mp4
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mse"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog/log"
)
func Init() {
api.HandleWS("mse", handler)
}
const MsgTypeMSE = "mse" // fMP4
func handler(ctx *api.Context, msg *streamer.Message) {
url := ctx.Request.URL.Query().Get("url")
stream := streams.Get(url)
func handlerWS(ctx *api.Context, msg *streamer.Message) {
src := ctx.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
cons := new(mse.Consumer)
cons := &mp4.Consumer{}
cons.UserAgent = ctx.Request.UserAgent()
cons.RemoteAddr = ctx.Request.RemoteAddr
cons.Listen(func(msg interface{}) {
switch msg.(type) {
case *streamer.Message, []byte:
ctx.Write(msg)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.mse] Add consumer")
log.Warn().Err(err).Msg("[api.mse] add consumer")
ctx.Error(err)
return
}
@@ -38,5 +37,16 @@ func handler(ctx *api.Context, msg *streamer.Message) {
stream.RemoveConsumer(cons)
})
cons.Init()
ctx.Write(&streamer.Message{
Type: MsgTypeMSE, Value: cons.MimeType(),
})
data, err := cons.Init()
if err != nil {
log.Warn().Err(err).Msg("[api.mse] init")
ctx.Error(err)
return
}
ctx.Write(data)
}

View File

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

View File

@@ -8,6 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
"strings"
)
func Init() {
@@ -18,7 +19,7 @@ func Init() {
}
// default config
conf.Mod.Listen = ":554"
conf.Mod.Listen = ":8554"
app.LoadConfig(&conf)
@@ -27,6 +28,7 @@ func Init() {
// RTSP client support
streams.HandleFunc("rtsp", rtspHandler)
streams.HandleFunc("rtsps", rtspHandler)
streams.HandleFunc("rtspx", rtspHandler)
// RTSP server support
address := conf.Mod.Listen
@@ -46,6 +48,15 @@ var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
var log zerolog.Logger
func rtspHandler(url string) (streamer.Producer, error) {
backchannel := true
if i := strings.IndexByte(url, '#'); i > 0 {
if url[i+1:] == "backchannel=0" {
backchannel = false
}
url = url[:i]
}
conn, err := rtsp.NewClient(url)
if err != nil {
return nil, err
@@ -65,8 +76,21 @@ func rtspHandler(url string) (streamer.Producer, error) {
if err = conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = backchannel
if err = conn.Describe(); err != nil {
return nil, err
if !backchannel {
return nil, err
}
// second try without backchannel, we need to reconnect
if err = conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = false
if err = conn.Describe(); err != nil {
return nil, err
}
}
return conn, nil
@@ -133,15 +157,15 @@ func worker(address string) {
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
str := streams.Get(conn.URL.Path[1:])
if str == nil {
stream := streams.Get(name)
if stream == nil {
return
}
str.AddProducer(conn)
stream.AddProducer(conn)
onDisconnect = func() {
str.RemoveProducer(conn)
stream.RemoveProducer(conn)
}
case streamer.StatePlaying:

59
cmd/srtp/srtp.go Normal file
View File

@@ -0,0 +1,59 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/rs/zerolog"
"net"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
} `yaml:"srtp"`
}
// default config
cfg.Mod.Listen = ":8443"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
return
}
log = app.GetLogger("srtp")
// create SRTP server (endpoint) for receiving video from HomeKit camera
conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
if err != nil {
log.Warn().Err(err).Msg("[srtp] listen")
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
_, Port, _ = net.SplitHostPort(cfg.Mod.Listen)
// run server
go func() {
server = &srtp.Server{}
if err = server.Serve(conn); err != nil {
log.Warn().Err(err).Msg("[srtp] serve")
}
}()
}
var log zerolog.Logger
var server *srtp.Server
var Port string
func AddSession(session *srtp.Session) {
server.AddSession(session)
}
func RemoveSession(session *srtp.Session) {
server.RemoveSession(session)
}

View File

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

View File

@@ -3,6 +3,7 @@ package streams
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
)
type state byte
@@ -17,27 +18,33 @@ const (
type Producer struct {
streamer.Element
url string
url string
template string
element streamer.Producer
tracks []*streamer.Track
state state
mx sync.Mutex
}
func (p *Producer) SetSource(s string) {
if p.template == "" {
p.template = p.url
}
p.url = strings.Replace(p.template, "{input}", s, 1)
}
func (p *Producer) GetMedias() []*streamer.Media {
if p.state == stateNone {
i := strings.IndexByte(p.url, ':')
handler := handlers[p.url[:i]]
if handler == nil {
log.Warn().Str("url", p.url).Msg("[streams] unsupported scheme")
return nil
}
p.mx.Lock()
defer p.mx.Unlock()
if p.state == stateNone {
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
var err error
p.element, err = handler(p.url)
if err != nil {
p.element, err = GetProducer(p.url)
if err != nil || p.element == nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
return nil
}
@@ -49,6 +56,9 @@ func (p *Producer) GetMedias() []*streamer.Media {
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
p.mx.Lock()
defer p.mx.Unlock()
if p.state == stateMedias {
p.state = stateTracks
}
@@ -69,6 +79,9 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
// internals
func (p *Producer) start() {
p.mx.Lock()
defer p.mx.Unlock()
if p.state != stateTracks {
return
}
@@ -76,14 +89,26 @@ func (p *Producer) start() {
log.Debug().Str("url", p.url).Msg("[streams] start producer")
p.state = stateStart
go p.element.Start()
go func() {
if err := p.element.Start(); err != nil {
log.Warn().Err(err).Str("url", p.url).Msg("[streams] start")
}
}()
}
func (p *Producer) stop() {
p.mx.Lock()
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
_ = p.element.Stop()
p.element = nil
if p.element != nil {
_ = p.element.Stop()
p.element = nil
} else {
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
}
p.tracks = nil
p.state = stateNone
p.mx.Unlock()
}

View File

@@ -2,6 +2,7 @@ package streams
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
@@ -15,25 +16,35 @@ type Stream struct {
consumers []*Consumer
}
func newStream(source interface{}) *Stream {
s := new(Stream)
func NewStream(source interface{}) *Stream {
switch source := source.(type) {
case string:
s := new(Stream)
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
return s
case []interface{}:
s := new(Stream)
for _, source := range source {
prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod)
}
return s
case *Stream:
return source
case map[string]interface{}:
return newStream(source["url"])
return NewStream(source["url"])
case nil:
return new(Stream)
default:
panic("wrong source type")
}
}
return s
func (s *Stream) SetSource(source string) {
for _, prod := range s.producers {
prod.SetSource(source)
}
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
@@ -61,6 +72,10 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// Step 4. Get producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
if prodTrack == nil {
log.Warn().Msg("[stream] can't get track")
continue
}
// Step 5. Add track to consumer and get new track
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
@@ -74,7 +89,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
// can't match tracks for consumer
if len(consumer.tracks) == 0 {
return nil
return errors.New("couldn't find the matching tracks")
}
s.consumers = append(s.consumers, consumer)
@@ -88,6 +103,11 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
for i, consumer := range s.consumers {
if consumer == nil {
log.Warn().Msgf("empty consumer: %+v\n", s)
continue
}
if consumer.element == cons {
// remove consumer pads from all producers
for _, track := range consumer.tracks {
@@ -100,6 +120,11 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
}
for _, producer := range s.producers {
if producer == nil {
log.Warn().Msgf("empty producer: %+v\n", s)
continue
}
var sink bool
for _, track := range producer.tracks {
if len(track.Sink) > 0 {
@@ -113,11 +138,31 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
}
func (s *Stream) AddProducer(prod streamer.Producer) {
panic("not implemented")
producer := &Producer{element: prod, state: stateTracks}
s.producers = append(s.producers, producer)
}
func (s *Stream) RemoveProducer(prod streamer.Producer) {
panic("not implemented")
for i, producer := range s.producers {
if producer.element == prod {
s.removeProducer(i)
break
}
}
}
func (s *Stream) Active() bool {
if len(s.consumers) > 0 {
return true
}
for _, prod := range s.producers {
if prod.element != nil {
return true
}
}
return false
}
func (s *Stream) MarshalJSON() ([]byte, error) {

View File

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

View File

@@ -2,11 +2,10 @@ package streams
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/rs/zerolog"
)
var Streams = map[string]*Stream{}
func Init() {
var cfg struct {
Mod map[string]interface{} `yaml:"streams"`
@@ -17,12 +16,52 @@ func Init() {
log = app.GetLogger("streams")
for name, item := range cfg.Mod {
Streams[name] = newStream(item)
streams[name] = NewStream(item)
}
for name, item := range store.GetDict("streams") {
streams[name] = NewStream(item)
}
}
func Get(name string) *Stream {
return Streams[name]
return streams[name]
}
func New(name string, source interface{}) *Stream {
stream := NewStream(source)
streams[name] = stream
return stream
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok {
return stream
}
if !HasProducer(src) {
return nil
}
log.Info().Str("url", src).Msg("[streams] create new stream")
return New(src, src)
}
func Delete(name string) {
delete(streams, name)
}
func All() map[string]interface{} {
all := map[string]interface{}{}
for name, stream := range streams {
all[name] = stream
//if stream.Active() {
// all[name] = stream
//}
}
return all
}
var log zerolog.Logger
var streams = map[string]*Stream{}

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

@@ -0,0 +1,70 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3"
)
var candidates []string
func AddCandidate(address string) {
candidates = append(candidates, address)
}
func addCanditates(answer string) (string, error) {
if len(candidates) == 0 {
return answer, nil
}
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal([]byte(answer)); err != nil {
return "", err
}
md := sd.MediaDescriptions[0]
_, end := md.Attribute("end-of-candidates")
if end {
md.Attributes = md.Attributes[:len(md.Attributes)-1]
}
for _, address := range candidates {
var err error
address, err = webrtc.LookupIP(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
md.WithPropertyAttribute(cand)
}
if end {
md.WithPropertyAttribute("end-of-candidates")
}
data, err := sd.Marshal()
if err != nil {
return "", err
}
return string(data), nil
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
return
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
conn.Push(msg)
}
}

View File

@@ -8,10 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"io/ioutil"
"net"
"net/http"
"strings"
)
func Init() {
@@ -34,14 +31,14 @@ func Init() {
address := cfg.Mod.Listen
pionAPI, err := webrtc.NewAPI(address)
if pionAPI == nil {
log.Error().Err(err).Msg("[webrtc] Init API")
log.Error().Err(err).Msg("[webrtc] init API")
return
}
if err != nil {
log.Warn().Err(err).Msg("[webrtc] Listen")
log.Warn().Err(err).Msg("[webrtc] listen")
} else if address != "" {
log.Info().Str("addr", address).Msg("[webrtc] Listen")
log.Info().Str("addr", address).Msg("[webrtc] listen")
_, Port, _ = net.SplitHostPort(address)
}
@@ -56,121 +53,23 @@ func Init() {
candidates = cfg.Mod.Candidates
api.HandleFunc("/api/webrtc", apiHandler)
api.HandleFunc("/api/webrtc/camera", cameraHandler)
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
}
func AddCandidate(address string) {
log.Info().Str("addr", address).Msg("[webrtc] new candidate")
candidates = append(candidates, address)
}
var Port string
var log zerolog.Logger
var candidates []string
var NewPConn func() (*pion.PeerConnection, error)
func apiHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("[webrtc] read offer")
return
}
// create new webrtc instance
cons := new(webrtc.Conn)
cons.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
cons.UserAgent = r.UserAgent()
cons.Listen(func(msg interface{}) {
if msg == streamer.StateNull {
stream.RemoveConsumer(cons)
}
})
if err = stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
return
}
cons.Init()
// exchange sdp with waiting all candidates
answer, err := cons.ExchangeSDP(string(offer), true)
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Msg("[api.webrtc] send answer")
}
}
func cameraHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("[webrtc] read offer")
return
}
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
conn.UserAgent = r.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateDisconnected {
stream.RemoveConsumer(conn)
}
case streamer.Track:
//stream.AddProducer(conn)
}
})
conn.Init()
// exchange sdp with waiting all candidates
answer, err := conn.ExchangeSDP(string(offer), true)
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Msg("[api.webrtc] send answer")
}
}
func offerHandler(ctx *api.Context, msg *streamer.Message) {
name := ctx.Request.URL.Query().Get("url")
stream := streams.Get(name)
src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[webrtc] new consumer")
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
var err error
@@ -185,8 +84,8 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
conn.UserAgent = ctx.Request.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case streamer.EventType:
if msg == streamer.StateNull {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
case *streamer.Message:
@@ -209,6 +108,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
_ = conn.Conn.Close()
ctx.Error(err)
return
}
@@ -217,7 +117,11 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
answer, err := conn.GetAnswer()
//answer, err := conn.GetAnswer()
answer, err := conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
@@ -230,36 +134,58 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
Type: webrtc.MsgTypeAnswer, Value: answer,
})
for _, address := range candidates {
if strings.HasPrefix(address, "stun:") {
ip, err := webrtc.GetPublicIP()
if err != nil {
log.Warn().Err(err).Msg("[webrtc] public IP")
continue
}
address = ip.String() + address[4:]
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
conn.Fire(&streamer.Message{
Type: webrtc.MsgTypeCandidate, Value: cand,
})
}
ctx.Consumer = conn
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
func ExchangeSDP(
stream *streams.Stream, offer string, userAgent string,
) (answer string, err error) {
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] Remote")
conn.Push(msg)
conn.UserAgent = userAgent
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
}
})
// 1. SetOffer, so we can get remote client codecs
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer")
return
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
_ = conn.Conn.Close()
return
}
conn.Init()
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
answer, err = conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer")
}
return
}

22
go.mod
View File

@@ -3,39 +3,55 @@ module github.com/AlexxIT/go2rtc
go 1.17
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.2.6
github.com/pion/interceptor v0.1.11
github.com/pion/logging v0.2.2
github.com/pion/rtcp v1.2.9
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.5
github.com/pion/srtp/v2 v2.0.10
github.com/pion/stun v0.3.5
github.com/pion/webrtc/v3 v3.1.43
github.com/rs/zerolog v1.27.0
github.com/stretchr/testify v1.7.1
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.3 // 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/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/srtp/v2 v2.0.10 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // 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.0.0-20220516162934-403b01795ae8 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
replace github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e
replace (
// windows support: https://github.com/brutella/dnssd/pull/35
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
// RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657
)

37
go.sum
View File

@@ -1,12 +1,18 @@
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78=
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0=
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepch/vdk v0.0.19 h1:r6xYyBTtXEIEh+csO0XHT00sI7xLF+hQFkJE9/go5II=
github.com/deepch/vdk v0.0.19/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -28,6 +34,8 @@ 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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -40,6 +48,9 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
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=
@@ -110,7 +121,7 @@ github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
github.com/pion/webrtc/v3 v3.1.41/go.mod h1:sUcW9SFPEWerDqGOBmdYEMfRvbdd7rgwo4bNzfsXww4=
github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -128,7 +139,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -140,6 +156,8 @@ golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/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=
@@ -152,7 +170,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -164,6 +186,8 @@ golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug
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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -176,7 +200,10 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/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=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -190,13 +217,17 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
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=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

52
main.go
View File

@@ -1,9 +1,57 @@
package main
import (
"github.com/AlexxIT/go2rtc/cmd"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/debug"
"github.com/AlexxIT/go2rtc/cmd/echo"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/homekit"
"github.com/AlexxIT/go2rtc/cmd/ivideon"
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
"github.com/AlexxIT/go2rtc/cmd/mp4"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"os"
"os/signal"
"syscall"
)
func main() {
cmd.Run()
app.Init() // init config and logs
streams.Init() // load streams list
api.Init() // init HTTP API server
echo.Init()
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
webrtc.Init()
mp4.Init()
mjpeg.Init()
srtp.Init()
homekit.Init()
ivideon.Init()
ngrok.Init()
debug.Init()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
println("exit OK")
}

5
pkg/README.md Normal file
View File

@@ -0,0 +1,5 @@
## Useful links
- https://www.wowza.com/blog/streaming-protocols
- https://vimeo.com/blog/post/rtmp-stream/
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a

View File

@@ -1,3 +1,14 @@
## RTP H264
Camera | NALu
-------|-----
EZVIZ C3S | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 1t, 1t, 1t
Sonoff GK-200MP2-B | 28:28:28 -> 5t, 1t, 1t, 1t
Dahua IPC-K42 | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg copy | 5t, 1t, 1t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg h264 | 24 -> 6:5:5:5:5t, 24 -> 1:1:1:1t, 28:28:28 -> 5f, 28:28:28 -> 5f, 28:28:28 -> 5t
FFmpeg resize | 6f, 28:28:28 -> 5f, 28... -> 5t, 24 -> 1:1f, 24 -> 1:1t
## WebRTC
Video codec | Media string | Device
@@ -25,3 +36,4 @@ H.264/high | avc1.6400xx | FFmpeg superfast
- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)
- [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)
- [Two stream formats, Annex-B, AVCC (H.264) and HVCC (H.265)](https://www.programmersought.com/article/3901815022/)

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

@@ -0,0 +1,78 @@
package h264
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
const PayloadTypeAVC = 255
func IsAVC(codec *streamer.Codec) bool {
return codec.PayloadType == PayloadTypeAVC
}
func EncodeAVC(raw []byte) (avc []byte) {
avc = make([]byte, len(raw)+4)
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
copy(avc[4:], raw)
return
}
func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps)
pps = EncodeAVC(pps)
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) (err error) {
naluType := NALUType(packet.Payload)
switch naluType {
case NALUTypeSPS:
sps = packet.Payload
return
case NALUTypePPS:
pps = packet.Payload
return
}
var clone rtp.Packet
if naluType == NALUTypeIFrame {
clone = *packet
clone.Payload = sps
if err = push(&clone); err != nil {
return
}
clone = *packet
clone.Payload = pps
if err = push(&clone); err != nil {
return
}
}
clone = *packet
clone.Payload = packet.Payload
return push(&clone)
}
}
}
func SplitAVC(data []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(data))
// check if multiple items in one packet
if size+4 < len(data) {
nals = append(nals, data[:size+4])
data = data[size+4:]
} else {
nals = append(nals, data)
break
}
}
return nals
}

View File

@@ -2,7 +2,6 @@ package h264
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
@@ -10,25 +9,24 @@ import (
const (
NALUTypePFrame = 1
NALUTypeIFrame = 5
NALUTypeSEI = 6
NALUTypeSPS = 7
NALUTypePPS = 8
PayloadTypeAVC = 255
)
func NALUType(b []byte) byte {
return b[4] & 0x1F
}
func EncodeAVC(raw []byte) (avc []byte) {
avc = make([]byte, len(raw)+4)
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
copy(avc[4:], raw)
return
func IsKeyframe(b []byte) bool {
return NALUType(b) == NALUTypeIFrame
}
func IsAVC(codec *streamer.Codec) bool {
return codec.PayloadType == PayloadTypeAVC
func GetProfileLevelID(fmtp string) string {
if fmtp == "" {
return ""
}
return streamer.Between(fmtp, "profile-level-id=", ";")
}
func GetParameterSet(fmtp string) (sps, pps []byte) {

View File

@@ -1,6 +1,7 @@
package h264
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
@@ -19,62 +20,90 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//println(packet.SequenceNumber, packet.Payload[0]&0x1F, packet.Payload[0], packet.Payload[1], packet.Marker, packet.Timestamp)
//nalUnitType := packet.Payload[0] & 0x1F
//fmt.Printf(
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n",
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC, packet.SequenceNumber,
//)
data, err := depack.Unmarshal(packet.Payload)
if len(data) == 0 || err != nil {
return nil
}
naluType := NALUType(data)
//println(naluType, len(data))
for {
unitType := NALUType(data)
//fmt.Printf("[H264] nalu: %2d, size: %6d\n", unitType, len(data))
switch naluType {
case NALUTypeSPS:
//println("new SPS")
sps = data
return nil
case NALUTypePPS:
//println("new PPS")
pps = data
return nil
}
// multiple 5 and 1 in one payload is OK
if unitType != NALUTypeIFrame && unitType != NALUTypePFrame {
i := int(binary.BigEndian.Uint32(data)) + 4
if i < len(data) {
data0 := data[:i] // NAL Unit with AVC header
data = data[i:]
switch unitType {
case NALUTypeSPS:
sps = data0
continue
case NALUTypePPS:
pps = data0
continue
case NALUTypeSEI:
// some unnecessary text information
continue
}
}
}
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
// and every NALU will be sliced to multiple NALUs
if !packet.Marker {
buffer = append(buffer, data...)
return nil
}
switch unitType {
case NALUTypeSPS:
sps = data
return nil
case NALUTypePPS:
pps = data
return nil
case NALUTypeSEI:
// some unnecessary text information
return nil
}
if buffer != nil {
buffer = append(buffer, data...)
data = buffer
buffer = nil
}
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
// and every NALU will be sliced to multiple NALUs
if !packet.Marker {
buffer = append(buffer, data...)
return nil
}
var clone rtp.Packet
if buffer != nil {
buffer = append(buffer, data...)
data = buffer
buffer = nil
}
if naluType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
var clone rtp.Packet
if unitType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
if err = push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
if err = push(&clone); err != nil {
return err
}
clone.Payload = data
return push(&clone)
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = data
return push(&clone)
}
}
}

3
pkg/h265/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Useful links
- https://datatracker.ietf.org/doc/html/rfc7798

35
pkg/h265/helper.go Normal file
View File

@@ -0,0 +1,35 @@
package h265
import (
"encoding/base64"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
const (
NALUnitTypeIFrame = 19
)
func NALUnitType(b []byte) byte {
return b[4] >> 1
}
func IsKeyframe(b []byte) bool {
return NALUnitType(b) == NALUnitTypeIFrame
}
func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
if fmtp == "" {
return
}
s := streamer.Between(fmtp, "sprop-vps=", ";")
vps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-sps=", ";")
sps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-pps=", ";")
pps, _ = base64.StdEncoding.DecodeString(s)
return
}

153
pkg/h265/rtp.go Normal file
View File

@@ -0,0 +1,153 @@
package h265
import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h265parser"
"github.com/pion/rtp"
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
vps, sps, pps := GetParameterSet(track.Codec.FmtpLine)
var buffer []byte
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
nut := (packet.Payload[0] >> 1) & 0x3f
//fmt.Printf(
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n",
// track.Codec.Name, nut, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC, packet.SequenceNumber,
//)
switch nut {
case h265parser.NAL_UNIT_UNSPECIFIED_49:
data := packet.Payload
switch data[2] >> 6 {
case 2: // begin
buffer = []byte{
(data[0] & 0x81) | (data[2] & 0x3f << 1), data[1],
}
buffer = append(buffer, data[3:]...)
return nil
case 0: // continue
buffer = append(buffer, data[3:]...)
return nil
case 1: // end
packet.Payload = append(buffer, data[3:]...)
}
case h265parser.NAL_UNIT_VPS:
vps = packet.Payload
return nil
case h265parser.NAL_UNIT_SPS:
sps = packet.Payload
return nil
case h265parser.NAL_UNIT_PPS:
pps = packet.Payload
return nil
default:
//panic("not implemented")
}
var clone rtp.Packet
nut = (packet.Payload[0] >> 1) & 0x3f
if nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA {
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(vps)
if err := push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(sps)
if err := push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(pps)
if err := push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(packet.Payload)
return push(&clone)
}
}
}
// SafariPay - generate Safari friendly payload for H265
func SafariPay(mtu uint16) streamer.WrapperFunc {
sequencer := rtp.NewRandomSequencer()
size := int(mtu - 12) // rtp.Header size
var buffer []byte
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return push(packet)
}
data := packet.Payload
data[0] = 0
data[1] = 0
data[2] = 0
data[3] = 1
var start byte
nut := (data[4] >> 1) & 0b111111
//fmt.Printf("[H265] nut: %2d, size: %6d, data: %16x\n", nut, len(data), data[4:20])
switch {
case nut >= h265parser.NAL_UNIT_VPS && nut <= h265parser.NAL_UNIT_PPS:
buffer = append(buffer, data...)
return nil
case nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA:
buffer = append([]byte{3}, buffer...)
data = append(buffer, data...)
start = 1
default:
data = append([]byte{2}, data...)
start = 0
}
for len(data) > size {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: false,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: data[:size],
}
if err := push(&clone); err != nil {
return err
}
data = append([]byte{start}, data[size:]...)
}
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: data,
}
return push(&clone)
}
}
}

33
pkg/homekit/README.md Normal file
View File

@@ -0,0 +1,33 @@
# Homekit
> PS. Character = Characteristic
**Device** - HomeKit end device (swith, camera, etc)
- mDNS name: `MyCamera._hap._tcp.local.`
- DeviceID - mac-like: `0E:AA:CE:2B:35:71`
- HomeKit device is described by:
- one or more `Accessories` - has `AID` and `Services`
- `Services` - has `IID`, `Type` and `Characters`
- `Characters` - has `IID`, `Type`, `Format` and `Value`
**Client** - HomeKit client (iPhone, iPad, MacBook or opensource library)
- ClientID - static random UUID
- ClientPublic/ClientPrivate - static random 32 byte keypair
- can pair with Device (exchange ClientID/ClientPublic, ServerID/ServerPublic using Pin)
- can auth to Device using ClientPrivate
- holding persistant Secure connection to device
- can read device Accessories
- can read and write device Characters
- can subscribe on device Characters change (Event)
**Server** - HomeKit server (soft on end device or opensource library)
- ServerID - same as DeviceID (using for Client auth)
- ServerPublic/ServerPrivate - static random 32 byte keypair
## Useful links
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)

62
pkg/homekit/accessory.go Normal file
View File

@@ -0,0 +1,62 @@
package homekit
type Accessory struct {
AID int `json:"aid"`
Services []*Service `json:"services"`
}
type Accessories struct {
Accessories []*Accessory `json:"accessories"`
}
type Characters struct {
Characters []*Character `json:"characteristics"`
}
func (a *Accessory) GetService(servType string) *Service {
for _, serv := range a.Services {
if serv.Type == servType {
return serv
}
}
return nil
}
func (a *Accessory) GetCharacter(charType string) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.Type == charType {
return char
}
}
}
return nil
}
func (a *Accessory) GetCharacterByID(iid int) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.IID == iid {
return char
}
}
}
return nil
}
type Service struct {
IID int `json:"iid"`
Type string `json:"type"`
Primary bool `json:"primary,omitempty"`
Hidden bool `json:"hidden,omitempty"`
Characters []*Character `json:"characteristics"`
}
func (s *Service) GetCharacter(charType string) *Character {
for _, char := range s.Characters {
if char.Type == charType {
return char
}
}
return nil
}

View File

@@ -0,0 +1,100 @@
package camera
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
)
type Client struct {
client *homekit.Client
}
func NewClient(client *homekit.Client) *Client {
return &Client{client: client}
}
func (c *Client) StartStream2(ses *Session) (err error) {
// Step 1. Check if camera ready (free) to stream
var srv *homekit.Service
if srv, err = c.GetFreeStream(); err != nil {
return err
}
if srv == nil {
return errors.New("no free streams")
}
if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil {
return
}
return c.SetConfig(srv, ses.Config)
}
// GetFreeStream search free streaming service.
// Usual every HomeKit camera can stream only to two clients simultaniosly.
// So it has two similar services for streaming.
func (c *Client) GetFreeStream() (srv *homekit.Service, err error) {
var accs []*homekit.Accessory
if accs, err = c.client.GetAccessories(); err != nil {
return
}
for _, srv = range accs[0].Services {
for _, char := range srv.Characters {
if char.Type == characteristic.TypeStreamingStatus {
status := rtp.StreamingStatus{}
if err = char.ReadTLV8(&status); err != nil {
return
}
if status.Status == rtp.SessionStatusSuccess {
return
}
}
}
}
return nil, nil
}
func (c *Client) SetupEndpoins(
srv *homekit.Service, req *rtp.SetupEndpoints,
) (res *rtp.SetupEndpointsResponse, err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
char.Event = nil
// encode new character value
if err = char.Write(req); err != nil {
return
}
// write (put) new endpoint value to device
if err = c.client.PutCharacters(char); err != nil {
return
}
// get new endpoint value from device (response)
if err = c.client.GetCharacter(char); err != nil {
return
}
// decode new endpoint value
res = &rtp.SetupEndpointsResponse{}
if err = char.ReadTLV8(res); err != nil {
return
}
return
}
func (c *Client) SetConfig(srv *homekit.Service, config *rtp.StreamConfiguration) (err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
char.Event = nil
// encode new character value
if err = char.Write(config); err != nil {
panic(err)
}
// write (put) new character value to device
return c.client.PutCharacters(char)
}

View File

@@ -0,0 +1,103 @@
package camera
import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/brutella/hap/rtp"
)
type Session struct {
Offer *rtp.SetupEndpoints
Answer *rtp.SetupEndpointsResponse
Config *rtp.StreamConfiguration
}
func NewSession() *Session {
sessionID := RandomBytes(16)
s := &Session{
Offer: &rtp.SetupEndpoints{
SessionId: sessionID,
Video: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
Audio: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &rtp.StreamConfiguration{
Command: rtp.SessionControlCommand{
Identifier: sessionID,
Type: rtp.SessionControlCommandTypeStart,
},
Video: rtp.VideoParameters{
CodecType: rtp.VideoCodecType_H264,
CodecParams: rtp.VideoCodecParameters{
Profiles: []rtp.VideoCodecProfile{
{Id: rtp.VideoCodecProfileMain},
},
Levels: []rtp.VideoCodecLevel{
{Level: rtp.VideoCodecLevel4},
},
Packetizations: []rtp.VideoCodecPacketization{
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
},
},
Attributes: rtp.VideoCodecAttributes{
Width: 1920, Height: 1080, Framerate: 30,
},
RTP: rtp.RTPParams{
PayloadType: 99,
Ssrc: RandomUint32(),
Bitrate: 299,
Interval: 0.5,
ComfortNoisePayloadType: 98,
MTU: 0,
},
},
Audio: rtp.AudioParameters{
CodecType: rtp.AudioCodecType_AAC_ELD,
CodecParams: rtp.AudioCodecParameters{
Channels: 1,
Bitrate: rtp.AudioCodecBitrateVariable,
Samplerate: rtp.AudioCodecSampleRate16Khz,
PacketTime: 30,
},
RTP: rtp.RTPParams{
PayloadType: 110,
Ssrc: RandomUint32(),
Bitrate: 24,
Interval: 5,
MTU: 13,
},
ComfortNoise: false,
},
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = rtp.Addr{
IPAddr: host,
VideoRtpPort: port,
AudioRtpPort: port,
}
}
func (s *Session) SetVideo() {
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = cryptorand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = cryptorand.Read(data)
return binary.BigEndian.Uint32(data)
}

133
pkg/homekit/character.go Normal file
View File

@@ -0,0 +1,133 @@
package homekit
import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/tlv8"
"io"
"net/http"
)
type Character struct {
AID int `json:"aid,omitempty"`
IID int `json:"iid"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Value interface{} `json:"value,omitempty"`
Event interface{} `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"`
//MaxDataLen int `json:"maxDataLen"`
listeners map[io.Writer]bool
}
func (c *Character) AddListener(w io.Writer) {
// TODO: sync.Mutex
if c.listeners == nil {
c.listeners = map[io.Writer]bool{}
}
c.listeners[w] = true
}
func (c *Character) RemoveListener(w io.Writer) {
delete(c.listeners, w)
if len(c.listeners) == 0 {
c.listeners = nil
}
}
func (c *Character) NotifyListeners(ignore io.Writer) error {
if c.listeners == nil {
return nil
}
data, err := c.GenerateEvent()
if err != nil {
return err
}
for w, _ := range c.listeners {
if w == ignore {
continue
}
if _, err = w.Write(data); err != nil {
// error not a problem - just remove listener
c.RemoveListener(w)
}
}
return nil
}
// GenerateEvent with raw HTTP headers
func (c *Character) GenerateEvent() (data []byte, err error) {
chars := Characters{
Characters: []*Character{{AID: DeviceAID, IID: c.IID, Value: c.Value}},
}
if data, err = json.Marshal(chars); err != nil {
return
}
res := http.Response{
StatusCode: http.StatusOK,
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header{"Content-Type": []string{MimeJSON}},
ContentLength: int64(len(data)),
Body: io.NopCloser(bytes.NewReader(data)),
}
buf := bytes.NewBuffer([]byte{0})
if err = res.Write(buf); err != nil {
return
}
copy(buf.Bytes(), "EVENT")
return buf.Bytes(), err
}
// Set new value and NotifyListeners
func (c *Character) Set(v interface{}) (err error) {
if err = c.Write(v); err != nil {
return
}
return c.NotifyListeners(nil)
}
// Write new value with right format
func (c *Character) Write(v interface{}) (err error) {
switch c.Format {
case characteristic.FormatTLV8:
var data []byte
if data, err = tlv8.Marshal(v); err != nil {
return
}
c.Value = base64.StdEncoding.EncodeToString(data)
case characteristic.FormatBool:
switch v.(type) {
case bool:
c.Value = v.(bool)
case float64:
c.Value = v.(float64) != 0
}
}
return
}
// ReadTLV8 value to right struct
func (c *Character) ReadTLV8(v interface{}) (err error) {
var data []byte
if data, err = base64.StdEncoding.DecodeString(c.Value.(string)); err != nil {
return
}
return tlv8.Unmarshal(data, v)
}
func (c *Character) ReadBool() bool {
return c.Value.(bool)
}

732
pkg/homekit/client.go Normal file
View File

@@ -0,0 +1,732 @@
package homekit
import (
"bufio"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"io"
"net"
"net/http"
"net/url"
"strings"
)
// Client for HomeKit. DevicePublic can be null.
type Client struct {
streamer.Element
DeviceAddress string // including port
DeviceID string
DevicePublic []byte
ClientID string
ClientPrivate []byte
OnEvent func(res *http.Response)
Output func(msg interface{})
conn net.Conn
secure *Secure
httpResponse chan *bufio.Reader
}
func NewClient(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
c := &Client{
DeviceAddress: u.Host,
DeviceID: query.Get("device_id"),
DevicePublic: DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"),
ClientPrivate: DecodeKey(query.Get("client_private")),
}
return c, nil
}
func Pair(deviceID, pin string) (*Client, error) {
entry := mdns.GetEntry(deviceID)
if entry == nil {
return nil, errors.New("can't find device via mDNS")
}
c := &Client{
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
var mfi bool
for _, field := range entry.InfoFields {
if field[:2] == "ff" {
if field[3] == '1' {
mfi = true
}
break
}
}
return c, c.Pair(mfi, pin)
}
func (c *Client) ClientPublic() []byte {
return c.ClientPrivate[32:]
}
func (c *Client) URL() string {
return fmt.Sprintf(
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
)
}
func (c *Client) DialAndServe() error {
if err := c.Dial(); err != nil {
return err
}
return c.Handle()
}
func (c *Client) Dial() error {
// update device host before dial
if host := mdns.GetAddress(c.DeviceID); host != "" {
c.DeviceAddress = host
}
var err error
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return err
}
// STEP M1: send our session public to device
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. generate payload
// important not include other fields
requestM1 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
}{
State: hap.M1,
PublicKey: sessionPublic[:],
}
// 2. pack payload to TLV8
buf, err := tlv8.Marshal(requestM1)
if err != nil {
return err
}
// 3. send request
resp, err := c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M2: unpack deviceID from response
responseM2 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
return err
}
// 1. generate session shared key
var deviceSessionPublic [32]byte
copy(deviceSessionPublic[:], responseM2.PublicKey)
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
sessionKey, err := hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
)
// 2. decrypt M2 response with session key
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
var mac [16]byte
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
)
// 3. unpack payload from TLV8
payloadM2 := PairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
return err
}
// 4. verify signature for M2 response with device public
// device session + device id + our session
if c.DevicePublic != nil {
buf = nil
buf = append(buf, responseM2.PublicKey[:]...)
buf = append(buf, []byte(payloadM2.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
c.DevicePublic[:], buf, payloadM2.Signature,
) {
return errors.New("device public signature invalid")
}
}
// STEP M3: send our clientID to device
// 1. generate signature with our private key
// (our session + our ID + device session)
buf = nil
buf = append(buf, sessionPublic[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, responseM2.PublicKey[:]...)
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
if err != nil {
return err
}
// 2. generate payload
payloadM3 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
Signature: signature,
}
// 3. pack payload to TLV8
buf, err = tlv8.Marshal(payloadM3)
if err != nil {
return err
}
// 4. encrypt payload with session key
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg03"), buf, nil,
)
// 4. generate request
requestM3 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M3,
EncryptedData: append(msg, mac[:]...),
}
// 5. pack payload to TLV8
buf, err = tlv8.Marshal(requestM3)
if err != nil {
return err
}
resp, err = c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M4. Read response
responseM4 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
return err
}
// 1. check response state
if responseM4.State != 4 || responseM4.Status != 0 {
return fmt.Errorf("wrong M4 response: %+v", responseM4)
}
c.secure, err = NewSecure(sessionShared, false)
//c.secure.Buffer = bytes.NewBuffer(nil)
c.secure.Conn = c.conn
c.httpResponse = make(chan *bufio.Reader, 10)
return err
}
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
func (c *Client) Pair(mfi bool, pin string) (err error) {
pin = strings.ReplaceAll(pin, "-", "")
if len(pin) != 8 {
return fmt.Errorf("wrong PIN format: %s", pin)
}
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:]
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return
}
// STEP M1. Generate request
reqM1 := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
State: hap.M1,
}
if mfi {
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
}
buf, err := tlv8.Marshal(reqM1)
if err != nil {
return
}
// STEP M1. Send request
res, err := c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M2. Read response
resM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
return
}
if resM2.State != 2 || resM2.Error > 0 {
return fmt.Errorf("wrong M2: %+v", resM2)
}
// STEP M3. Generate session using pin
username := []byte("Pair-Setup")
SRP, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
if err != nil {
return
}
SRP.SaltLength = 16
// username: "Pair-Setup"
// password: PIN (with dashes)
session := SRP.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
if err != nil {
return
}
// STEP M3. Generate request
reqM3 := struct {
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
PublicKey: session.GetA(), // client public key, aka session.A
Proof: session.ComputeAuthenticator(),
State: hap.M3,
}
buf, err = tlv8.Marshal(reqM3)
if err != nil {
return err
}
// STEP M3. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M4. Read response
resM4 := struct {
Proof []byte `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
return
}
if resM4.Error == 2 {
return fmt.Errorf("wrong PIN: %s", pin)
}
if resM4.State != 4 || resM4.Error > 0 {
return fmt.Errorf("wrong M4: %+v", resM4)
}
// STEP M4. Verify response
if !session.VerifyServerAuthenticator(resM4.Proof) {
return errors.New("verify server auth fail")
}
// STEP M5. Generate signature
saltKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
)
if err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, c.ClientPublic()...)
signature, err := ed25519.Signature(c.ClientPrivate, buf)
if err != nil {
return
}
// STEP M5. Generate payload
msgM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
PublicKey: c.ClientPublic(),
Signature: signature,
}
buf, err = tlv8.Marshal(msgM5)
if err != nil {
return
}
// STEP M5. Encrypt payload
sessionKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
)
if err != nil {
return
}
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg05"), buf, nil,
)
// STEP M5. Generate request
reqM5 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
EncryptedData: append(buf, mac[:]...),
State: hap.M5,
}
buf, err = tlv8.Marshal(reqM5)
if err != nil {
return err
}
// STEP M5. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M6. Read response
resM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
return
}
if resM6.State != 6 || resM6.Error > 0 {
return fmt.Errorf("wrong M6: %+v", resM2)
}
// STEP M6. Decrypt payload
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
)
if err != nil {
return
}
msgM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
return
}
// STEP M6. Verify payload
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, msgM6.Identifier...)
buf = append(buf, msgM6.PublicKey...)
if !ed25519.ValidateSignature(
msgM6.PublicKey[:], buf, msgM6.Signature,
) {
return errors.New("wrong server signature")
}
if c.DeviceID != string(msgM6.Identifier) {
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
}
c.DevicePublic = msgM6.PublicKey
return nil
}
func (c *Client) Close() error {
if c.conn == nil {
return nil
}
conn := c.conn
c.conn = nil
return conn.Close()
}
func (c *Client) GetAccessories() ([]*Accessory, error) {
res, err := c.Get("/accessories")
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
p := Accessories{}
if err = json.Unmarshal(data, &p); err != nil {
return nil, err
}
for _, accs := range p.Accessories {
for _, serv := range accs.Services {
for _, char := range serv.Characters {
char.AID = accs.AID
}
}
}
return p.Accessories, nil
}
func (c *Client) GetCharacters(query string) ([]*Character, error) {
res, err := c.Get("/characteristics?id=" + query)
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return nil, err
}
return ch.Characters, nil
}
func (c *Client) GetCharacter(char *Character) error {
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
chars, err := c.GetCharacters(query)
if err != nil {
return err
}
char.Value = chars[0].Value
return nil
}
func (c *Client) PutCharacters(characters ...*Character) (err error) {
for i, char := range characters {
if char.Event != nil {
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
} else {
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
}
characters[i] = char
}
var data []byte
if data, err = json.Marshal(Characters{characters}); err != nil {
return
}
var res *http.Response
if res, err = c.Put("/characteristics", data); err != nil {
return
}
if res.StatusCode >= 400 {
return errors.New("wrong response status")
}
return
}
func (c *Client) GetImage(width, height int) ([]byte, error) {
res, err := c.Post(
"/resource", []byte(fmt.Sprintf(
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
width, height,
)),
)
if err != nil {
return nil, err
}
return io.ReadAll(res.Body)
}
//func (c *Client) onEventData(r io.Reader) error {
// if c.OnEvent == nil {
// return nil
// }
//
// data, err := io.ReadAll(r)
//
// ch := Characters{}
// if err = json.Unmarshal(data, &ch); err != nil {
// return err
// }
//
// c.OnEvent(ch.Characters)
//
// return nil
//}
func (c *Client) ListPairings() error {
pReq := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
Method: hap.MethodListPairings,
State: hap.M1,
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
// TODO: don't know how to fix array of items
var pRes struct {
State byte `tlv8:"6"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
pReq := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}{
Method: hap.MethodAddPairing,
Identifier: clientID,
PublicKey: clientPublic,
State: hap.M1,
Permission: hap.PermissionUser,
}
if admin {
pReq.Permission = hap.PermissionAdmin
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var pRes struct {
State byte `tlv8:"6"`
Unknown byte `tlv8:"7"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Client) DeletePairing(id string) error {
reqM1 := struct {
State byte `tlv8:"6"`
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
}{
State: hap.M1,
Method: hap.MethodDeletePairing,
Identifier: id,
}
data, err := tlv8.Marshal(reqM1)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var resM2 struct {
State byte `tlv8:"6"`
}
if err = tlv8.Unmarshal(data, &resM2); err != nil {
return err
}
if resM2.State != hap.M2 {
return errors.New("wrong state")
}
return nil
}
func (c *Client) LocalAddr() string {
return c.conn.LocalAddr().String()
}
func DecodeKey(s string) []byte {
if s == "" {
return nil
}
data, err := hex.DecodeString(s)
if err != nil {
return nil
}
return data
}

90
pkg/homekit/helpers.go Normal file
View File

@@ -0,0 +1,90 @@
package homekit
import (
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
)
const DeviceAID = 1 // TODO: fix someday
func GenerateID(name string) string {
sum := sha512.Sum512([]byte(name))
return fmt.Sprintf(
"%02X:%02X:%02X:%02X:%02X:%02X",
sum[0], sum[1], sum[2], sum[3], sum[4], sum[5],
)
}
func GenerateUUID() string {
//12345678-9012-3456-7890-123456789012
data := make([]byte, 16)
_, _ = rand.Read(data)
s := hex.EncodeToString(data)
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
}
type PairVerifyPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Status byte `tlv8:"7"`
Signature []byte `tlv8:"10"`
}
//func (c *Character) Unmarshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := base64.StdEncoding.DecodeString(c.Value.(string))
// if err != nil {
// return err
// }
// return tlv8.Unmarshal(data, value)
// }
// return nil
//}
//func (c *Character) Marshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := tlv8.Marshal(value)
// if err != nil {
// return err
// }
// c.Value = base64.StdEncoding.EncodeToString(data)
// }
// return nil
//}
func (c *Character) String() string {
data, err := json.Marshal(c)
if err != nil {
return "ERROR"
}
return string(data)
}
func UnmarshalEvent(res *http.Response) (char *Character, err error) {
var data []byte
if data, err = io.ReadAll(res.Body); err != nil {
return
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return
}
if len(ch.Characters) > 1 {
panic("not implemented")
}
char = ch.Characters[0]
return
}

246
pkg/homekit/http.go Normal file
View File

@@ -0,0 +1,246 @@
package homekit
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/textproto"
"strconv"
)
const (
MimeTLV8 = "application/pairing+tlv8"
MimeJSON = "application/hap+json"
UriPairSetup = "/pair-setup"
UriPairVerify = "/pair-verify"
UriPairings = "/pairings"
UriAccessories = "/accessories"
UriCharacteristics = "/characteristics"
UriResource = "/resource"
)
func (c *Client) Write(p []byte) (r io.Reader, err error) {
if c.secure == nil {
if _, err = c.conn.Write(p); err == nil {
r = bufio.NewReader(c.conn)
}
} else {
if _, err = c.secure.Write(p); err == nil {
r = <-c.httpResponse
}
}
return
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
if c.secure == nil {
// insecure requests
if err := req.Write(c.conn); err != nil {
return nil, err
}
return http.ReadResponse(bufio.NewReader(c.conn), req)
}
// secure support write interface to connection
if err := req.Write(c.secure); err != nil {
return nil, err
}
// get decrypted buffer from connection
buf := <-c.httpResponse
return http.ReadResponse(buf, req)
}
func (c *Client) Get(uri string) (*http.Response, error) {
req, err := http.NewRequest(
"GET", "http://"+c.DeviceAddress+uri, nil,
)
if err != nil {
return nil, err
}
return c.Do(req)
}
func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"POST", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case "/pair-verify", "/pairings":
req.Header.Set("Content-Type", MimeTLV8)
case UriResource:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"PUT", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case UriCharacteristics:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Client) Handle() (err error) {
defer func() {
if c.conn == nil {
err = nil
}
}()
b := make([]byte, 512000)
for {
var total, content int
header := -1
for {
var n1 int
n1, err = c.secure.Read(b[total:])
if err != nil {
return err
}
if n1 == 0 {
return io.EOF
}
total += n1
// TODO: rewrite
if header == -1 {
// step 1. wait whole header
header = bytes.Index(b[:total], []byte("\r\n\r\n"))
if header < 0 {
continue
}
header += 4
// step 2. check content-length
i1 := bytes.Index(b[:total], []byte("Content-Length: "))
if i1 < 0 {
break
}
i1 += 16
i2 := bytes.IndexByte(b[i1:total], '\r')
content, err = strconv.Atoi(string(b[i1 : i1+i2]))
if err != nil {
break
}
}
if total >= header+content {
break
}
}
// copy slice to buffer
buf := bytes.NewBuffer(make([]byte, 0, total))
buf.Write(b[:total])
r := bufio.NewReader(buf)
// EVENT/1.0 200 OK
if b[0] == 'E' {
if c.OnEvent == nil {
continue
}
tp := textproto.NewReader(r)
var s string
if s, err = tp.ReadLine(); err != nil {
return err
}
if s != "EVENT/1.0 200 OK" {
return errors.New("wrong response")
}
var mimeHeader textproto.MIMEHeader
if mimeHeader, err = tp.ReadMIMEHeader(); err != nil {
return err
}
var cl int
if cl, err = strconv.Atoi(
mimeHeader.Get("Content-Length"),
); err != nil {
return err
}
res := http.Response{
StatusCode: 200,
Proto: "EVENT/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header(mimeHeader),
ContentLength: int64(cl),
Body: io.NopCloser(r),
}
c.OnEvent(&res)
continue
}
//if bytes.Index(b, []byte("image/jpeg")) > 0 {
// if total, err = c.secure.Read(b); err != nil {
// return
// }
// buf.Write(b[:total])
//}
c.httpResponse <- r
}
}
func WriteStatusCode(w io.Writer, statusCode int) (err error) {
body := []byte(fmt.Sprintf(
"HTTP/1.1 %d %s\n\n", statusCode, http.StatusText(statusCode),
))
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteResponse(
w io.Writer, statusCode int, contentType string, body []byte,
) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 %d %s\nContent-Type: %s\nContent-Length: %d\n\n",
statusCode, http.StatusText(statusCode), contentType, len(body),
)
body = append([]byte(header), body...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteChunked(w io.Writer, contentType string, body []byte) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 200 OK\nContent-Type: %s\nTransfer-Encoding: chunked\n\n%x\n",
contentType, len(body),
)
body = append([]byte(header), body...)
body = append(body, "\n0\n\n"...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}

View File

@@ -0,0 +1,42 @@
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
}

View File

@@ -0,0 +1,53 @@
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
}

410
pkg/homekit/pairing.go Normal file
View File

@@ -0,0 +1,410 @@
package homekit
import (
"bufio"
"crypto/sha512"
"errors"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"net"
"net/http"
)
type pairSetupPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
RetryDelay byte `tlv8:"8"`
Certificate []byte `tlv8:"9"`
Signature []byte `tlv8:"10"`
Permissions byte `tlv8:"11"`
FragmentData []byte `tlv8:"13"`
FragmentLast []byte `tlv8:"14"`
}
func (s *Server) PairSetupHandler(
conn net.Conn, req *http.Request,
) (clientID string, err error) {
// STEP 1. Request from iPhone
payloadM1 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
// generate our session public and salt using PIN
username := []byte("Pair-Setup")
var SRP *srp.SRP
if SRP, err = srp.NewSRP(
"rfc5054.3072", sha512.New,
keyDerivativeFuncRFC2945(username),
); err != nil {
return
}
SRP.SaltLength = 16
var salt, verifier []byte
if salt, verifier, err = SRP.ComputeVerifier([]byte(s.Pin)); err != nil {
return
}
session := SRP.NewServerSession(username, salt, verifier)
// STEP 2. Response to iPhone
payloadM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
}{
State: hap.M2,
PublicKey: session.GetB(),
Salt: salt,
}
var buf []byte
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
payloadM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM3); err != nil {
return
}
if payloadM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
// important to compute key before verify client
var sessionShared []byte
if sessionShared, err = session.ComputeKey(payloadM3.PublicKey); err != nil {
return
}
// support skip pin verify (any pin accepted)
if s.Pin != "" && !session.VerifyClientAuthenticator(payloadM3.Proof) {
err = errors.New("client proof is invalid")
return
}
serverProof := session.ComputeAuthenticator(payloadM3.Proof)
// STEP 4. Response to iPhone
payloadM4 := struct {
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
State: hap.M4, Proof: serverProof,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 5. Request from iPhone
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM5 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM5); err != nil {
return
}
if encryptedM5.State != hap.M5 {
err = errors.New("wrong state")
return
}
msg := encryptedM5.EncryptedData[:len(encryptedM5.EncryptedData)-16]
var mac [16]byte
copy(mac[:], encryptedM5.EncryptedData[len(msg):]) // 16 byte (MAC)
// decrypt message using session shared
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
); err != nil {
return
}
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg05"), msg, mac, nil,
); err != nil {
return
}
// unpack message from TLV8
payloadM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &payloadM5); err != nil {
return
}
// 3. verify client ID and Public
var saltKey [32]byte
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, payloadM5.Identifier...)
buf = append(buf, payloadM5.PublicKey[:]...)
if !ed25519.ValidateSignature(
payloadM5.PublicKey[:], buf, payloadM5.Signature,
) {
err = errors.New("wrong client signature")
return
}
// 4. generate signature to our ID adn Public
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(s.ServerID)...)
buf = append(buf, s.ServerPrivate[32:]...) // ServerPublic
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate, buf); err != nil {
return
}
// 5. pack our ID and Public
payloadM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: []byte(s.ServerID),
PublicKey: s.ServerPrivate[32:],
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM6); err != nil {
return
}
// 6. encrypt message
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg06"), buf, nil,
)
// STEP 6. Response to iPhone
encryptedM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M6,
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM6); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
if s.Pairings != nil {
s.Pairings[payloadM5.Identifier] = append(
payloadM5.PublicKey, 1, // adds admin (1) flag
)
}
clientID = payloadM5.Identifier
return
}
func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc {
return func(salt, pin []byte) []byte {
h := sha512.New()
h.Write(username)
h.Write([]byte(":"))
h.Write(pin)
t2 := h.Sum(nil)
h.Reset()
h.Write(salt)
h.Write(t2)
return h.Sum(nil)
}
}
type pairVerifyPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Signature []byte `tlv8:"10"`
}
func (s *Server) PairVerifyHandler(
conn net.Conn, req *http.Request,
) (secure *Secure, err error) {
// STEP M1. Request from iPhone
payloadM1 := pairVerifyPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
var clientPublic [32]byte
copy(clientPublic[:], payloadM1.PublicKey)
// Generate the key pair.
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
sessionShared := curve25519.SharedSecret(sessionPrivate, clientPublic)
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
); err != nil {
return
}
var buf []byte
buf = append(buf, sessionPublic[:]...)
buf = append(buf, s.ServerID...)
buf = append(buf, clientPublic[:]...)
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate[:], buf); err != nil {
return
}
// STEP M2. Response to iPhone
payloadM2 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: s.ServerID,
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
var mac [16]byte
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg02"), buf, nil,
)
encryptedM2 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
}{
State: hap.M2,
PublicKey: sessionPublic[:],
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP M3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM3); err != nil {
return
}
if encryptedM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
buf = encryptedM3.EncryptedData[:len(encryptedM3.EncryptedData)-16]
copy(mac[:], encryptedM3.EncryptedData[len(buf):]) // 16 byte (MAC)
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg03"), buf, mac, nil,
); err != nil {
return
}
payloadM3 := pairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM3); err != nil {
return
}
if s.Pairings != nil {
pairing := s.Pairings[payloadM3.Identifier]
if pairing == nil {
err = errors.New("not paired yet")
return
}
buf = nil
buf = append(buf, clientPublic[:]...)
buf = append(buf, []byte(payloadM3.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
pairing[:32], buf, payloadM3.Signature,
) {
err = errors.New("signature invalid")
return
}
}
// STEP M4. Response to iPhone
payloadM4 := struct {
State byte `tlv8:"6"`
}{
State: hap.M4,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf)
if secure, err = NewSecure(sessionShared, true); err != nil {
return
}
secure.Conn = conn
return
}

137
pkg/homekit/secure.go Normal file
View File

@@ -0,0 +1,137 @@
package homekit
import (
"encoding/binary"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/hkdf"
"net"
"sync"
)
type Secure struct {
Conn net.Conn
encryptKey [32]byte
decryptKey [32]byte
encryptCount uint64
decryptCount uint64
mx sync.Mutex
}
func NewSecure(sharedKey [32]byte, isServer bool) (*Secure, error) {
salt := []byte("Control-Salt")
key1, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Read-Encryption-Key"),
)
if err != nil {
return nil, err
}
key2, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Write-Encryption-Key"),
)
if err != nil {
return nil, err
}
if isServer {
return &Secure{encryptKey: key1, decryptKey: key2}, nil
} else {
return &Secure{encryptKey: key2, decryptKey: key1}, nil
}
}
func (s *Secure) Read(b []byte) (n int, err error) {
for {
var length uint16
if err = binary.Read(s.Conn, binary.LittleEndian, &length); err != nil {
return
}
var enc = make([]byte, length)
if err = binary.Read(s.Conn, binary.LittleEndian, &enc); err != nil {
return
}
var mac [16]byte
if err = binary.Read(s.Conn, binary.LittleEndian, &mac); err != nil {
return
}
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.decryptCount)
s.decryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, length)
var msg []byte
if msg, err = chacha20poly1305.DecryptAndVerify(
s.decryptKey[:], nonce[:], enc, mac, bLength,
); err != nil {
return
}
n += copy(b[n:], msg)
// Finish when all bytes fit in b
if length < packetLengthMax {
//fmt.Printf(">>>%s>>>\n", b[:n])
return
}
}
}
func (s *Secure) Write(b []byte) (n int, err error) {
s.mx.Lock()
defer s.mx.Unlock()
var packetLen = len(b)
for {
if packetLen > packetLengthMax {
packetLen = packetLengthMax
}
//fmt.Printf("<<<%s<<<\n", b[:packetLen])
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.encryptCount)
s.encryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, uint16(packetLen))
var enc []byte
var mac [16]byte
enc, mac, err = chacha20poly1305.EncryptAndSeal(
s.encryptKey[:], nonce[:], b[:packetLen], bLength[:],
)
if err != nil {
return
}
enc = append(bLength, enc...)
enc = append(enc, mac[:]...)
if _, err = s.Conn.Write(enc); err != nil {
return
}
n += packetLen
if packetLen == packetLengthMax {
b = b[packetLengthMax:]
packetLen = len(b)
} else {
break
}
}
return
}
const (
// packetLengthMax is the max length of encrypted packets
packetLengthMax = 0x400
)

155
pkg/homekit/server.go Normal file
View File

@@ -0,0 +1,155 @@
package homekit
import (
"bufio"
"crypto/ed25519"
"github.com/brutella/hap"
"github.com/brutella/hap/tlv8"
"io"
"net"
"net/http"
)
type Server struct {
// Pin can't be null because server proof will be wrong
Pin string `json:"-"`
ServerID string `json:"server_id"`
// 32 bytes private key + 32 bytes public key
ServerPrivate []byte `json:"server_private"`
// Pairings can be nil for disable pair verify check
// ClientID: 32 bytes client public + 1 byte (isAdmin)
Pairings map[string][]byte `json:"pairings"`
DefaultPlainHandler func(w io.Writer, r *http.Request) error
DefaultSecureHandler func(w io.Writer, r *http.Request) error
OnPairChange func(clientID string, clientPublic []byte) `json:"-"`
OnRequest func(w io.Writer, r *http.Request) `json:"-"`
}
func GenerateKey() []byte {
_, key, _ := ed25519.GenerateKey(nil)
return key
}
func NewServer(name string) *Server {
return &Server{
ServerID: GenerateID(name),
ServerPrivate: GenerateKey(),
Pairings: map[string][]byte{},
}
}
func (s *Server) Serve(address string) (err error) {
var ln net.Listener
if ln, err = net.Listen("tcp", address); err != nil {
return
}
for {
var conn net.Conn
if conn, err = ln.Accept(); err != nil {
continue
}
go func() {
//fmt.Printf("[%s] new connection\n", conn.RemoteAddr().String())
s.Accept(conn)
//fmt.Printf("[%s] close connection\n", conn.RemoteAddr().String())
}()
}
}
func (s *Server) Accept(conn net.Conn) (err error) {
defer conn.Close()
var req *http.Request
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
return s.HandleRequest(conn, req)
}
func (s *Server) HandleRequest(conn net.Conn, req *http.Request) (err error) {
if s.OnRequest != nil {
s.OnRequest(conn, req)
}
switch req.URL.Path {
case UriPairSetup:
if _, err = s.PairSetupHandler(conn, req); err != nil {
return
}
case UriPairVerify:
var secure *Secure
if secure, err = s.PairVerifyHandler(conn, req); err != nil {
return
}
err = s.HandleSecure(secure)
default:
if s.DefaultPlainHandler != nil {
err = s.DefaultPlainHandler(conn, req)
}
}
return
}
func (s *Server) HandleSecure(secure *Secure) (err error) {
r := bufio.NewReader(secure)
for {
var req *http.Request
if req, err = http.ReadRequest(r); err != nil {
return
}
if s.OnRequest != nil {
s.OnRequest(secure, req)
}
switch req.URL.Path {
case UriPairings:
s.HandlePairings(secure, req)
default:
if err = s.DefaultSecureHandler(secure, req); err != nil {
return
}
}
}
}
func (s *Server) HandlePairings(w io.Writer, r *http.Request) {
req := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
State byte `tlv8:"6"`
}{}
if err := tlv8.UnmarshalReader(r.Body, &req); err != nil {
panic(err)
}
switch req.Method {
case hap.MethodAddPairing, hap.MethodDeletePairing:
res := struct {
State byte `tlv8:"6"`
}{
State: hap.M2,
}
data, err := tlv8.Marshal(res)
if err != nil {
panic(err)
}
if err = WriteResponse(w, http.StatusOK, MimeJSON, data); err != nil {
panic(err)
}
}
}

286
pkg/ivideon/client.go Normal file
View File

@@ -0,0 +1,286 @@
package ivideon
import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/gorilla/websocket"
"github.com/pion/rtp"
"io"
"net/http"
"strings"
"time"
)
type Client struct {
streamer.Element
ID string
conn *websocket.Conn
medias []*streamer.Media
tracks map[byte]*streamer.Track
closed bool
msg *message
t0 time.Time
buffer chan []byte
}
func NewClient(id string) *Client {
return &Client{ID: id}
}
func (c *Client) Dial() (err error) {
resp, err := http.Get(
"https://openapi-alpha.ivideon.com/cameras/" + c.ID +
"/live_stream?op=GET&access_token=public&q=2&" +
"video_codecs=h264&format=ws-fmp4",
)
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var v liveResponse
if err = json.Unmarshal(data, &v); err != nil {
return err
}
if !v.Success {
return fmt.Errorf("wrong response: %s", data)
}
c.conn, _, err = websocket.DefaultDialer.Dial(v.Result.URL, nil)
if err != nil {
return err
}
if err = c.getTracks(); err != nil {
_ = c.conn.Close()
return err
}
return nil
}
func (c *Client) Handle() error {
c.buffer = make(chan []byte, 5)
// add delay to the stream for smooth playing (not a best solution)
c.t0 = time.Now().Add(time.Second)
// processing stream in separate thread for lower delay between packets
go c.worker()
_, data, err := c.conn.ReadMessage()
if err != nil {
return err
}
track := c.tracks[c.msg.Track]
if track != nil {
c.buffer <- data
}
// we have one unprocessed msg after getTracks
for {
_, data, err = c.conn.ReadMessage()
if err != nil {
return err
}
var msg message
if err = json.Unmarshal(data, &msg); err != nil {
return err
}
switch msg.Type {
case "stream-init":
continue
case "fragment":
_, data, err = c.conn.ReadMessage()
if err != nil {
return err
}
track = c.tracks[msg.Track]
if track != nil {
c.buffer <- data
}
default:
return fmt.Errorf("wrong message type: %s", data)
}
}
}
func (c *Client) Close() error {
if c.conn == nil {
return nil
}
close(c.buffer)
c.closed = true
return c.conn.Close()
}
func (c *Client) getTracks() error {
c.tracks = map[byte]*streamer.Track{}
for {
_, data, err := c.conn.ReadMessage()
if err != nil {
return err
}
var msg message
if err = json.Unmarshal(data, &msg); err != nil {
return err
}
switch msg.Type {
case "stream-init":
s := msg.CodecString
i := strings.IndexByte(s, '.')
if i > 0 {
s = s[:i]
}
switch s {
case "avc1": // avc1.4d0029
// skip multiple identical init
if c.tracks[msg.TrackID] != nil {
continue
}
codec := streamer.NewCodec(streamer.CodecH264)
codec.FmtpLine = "profile-level-id=" + msg.CodecString[i+1:]
codec.PayloadType = h264.PayloadTypeAVC
i = bytes.Index(msg.Data, []byte("avcC")) - 4
if i < 0 {
return fmt.Errorf("wrong AVC: %s", msg.Data)
}
avccLen := binary.BigEndian.Uint32(msg.Data[i:])
data = msg.Data[i+8 : i+int(avccLen)]
record := h264parser.AVCDecoderConfRecord{}
if _, err = record.Unmarshal(data); err != nil {
return err
}
codec.FmtpLine += ";sprop-parameter-sets=" +
base64.StdEncoding.EncodeToString(record.SPS[0]) + "," +
base64.StdEncoding.EncodeToString(record.PPS[0])
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
c.medias = append(c.medias, media)
track := &streamer.Track{
Direction: streamer.DirectionSendonly,
Codec: codec,
}
c.tracks[msg.TrackID] = track
case "mp4a": // mp4a.40.2
}
case "fragment":
c.msg = &msg
return nil
default:
return fmt.Errorf("wrong message type: %s", data)
}
}
}
func (c *Client) worker() {
var track *streamer.Track
for _, track = range c.tracks {
break
}
for data := range c.buffer {
moof := &fmp4io.MovieFrag{}
if _, err := moof.Unmarshal(data, 0); err != nil {
continue
}
moofLen := binary.BigEndian.Uint32(data)
_ = moofLen
mdat := moof.Unknowns[0]
if mdat.Tag() != fmp4io.MDAT {
continue
}
i, _ := mdat.Pos() // offset, size
data = data[i+8:]
traf := moof.Tracks[0]
ts := uint32(traf.DecodeTime.Time)
//println("!!!", (time.Duration(ts) * time.Millisecond).String(), time.Since(c.t0).String())
for _, entry := range traf.Run.Entries {
// synchronize framerate for WebRTC and MSE
d := time.Duration(ts)*time.Millisecond - time.Since(c.t0)
if d < 0 {
d = time.Duration(entry.Duration) * time.Millisecond / 2
}
time.Sleep(d)
// can be SPS, PPS and IFrame in one packet
for _, payload := range h264.SplitAVC(data[:entry.Size]) {
packet := &rtp.Packet{
// ivideon clockrate=1000, RTP clockrate=90000
Header: rtp.Header{Timestamp: ts * 90},
Payload: payload,
}
_ = track.WriteRTP(packet)
}
data = data[entry.Size:]
ts += entry.Duration
}
if len(data) != 0 {
continue
}
}
}
type liveResponse struct {
Result struct {
URL string `json:"url"`
} `json:"result"`
Success bool `json:"success"`
}
type message struct {
Type string `json:"type"`
CodecString string `json:"codec_string"`
Data []byte `json:"data"`
TrackID byte `json:"track_id"`
Track byte `json:"track"`
StartTime float32 `json:"start_time"`
Duration float32 `json:"duration"`
IsKey bool `json:"is_key"`
DataOffset uint32 `json:"data_offset"`
}

31
pkg/ivideon/streamer.go Normal file
View File

@@ -0,0 +1,31 @@
package ivideon
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
}
func (c *Client) Start() error {
err := c.Handle()
if c.closed {
return nil
}
return err
}
func (c *Client) Stop() error {
return c.Close()
}

4
pkg/mjpeg/README.md Normal file
View File

@@ -0,0 +1,4 @@
## Useful links
- https://www.rfc-editor.org/rfc/rfc2435
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c

87
pkg/mjpeg/consumer.go Normal file
View File

@@ -0,0 +1,87 @@
package mjpeg
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
codecs []*streamer.Codec
start bool
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{{Name: streamer.CodecJPEG}},
}}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
var header, payload []byte
push := func(packet *rtp.Packet) error {
//fmt.Printf(
// "[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\n",
// track.Codec.Name, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
//)
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
b := packet.Payload
// 3.1. JPEG header
t := b[4]
// 3.1.7. Restart Marker header
if 64 <= t && t <= 127 {
b = b[12:] // skip it
} else {
b = b[8:]
}
if header == nil {
var lqt, cqt []byte
// 3.1.8. Quantization Table header
q := packet.Payload[5]
if q >= 128 {
lqt = b[4:68]
cqt = b[68:132]
b = b[132:]
} else {
lqt, cqt = MakeTables(q)
}
w := uint16(packet.Payload[6]) << 3
h := uint16(packet.Payload[7]) << 3
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
header = MakeHeaders(t, w, h, lqt, cqt)
}
// 3.1.9. JPEG Payload
payload = append(payload, b...)
if packet.Marker {
b = append(header, payload...)
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
b = append(b, 0xFF, 0xD9)
}
c.Fire(b)
header = nil
payload = nil
}
return nil
}
return track.Bind(push)
}

182
pkg/mjpeg/rfc2435.go Normal file
View File

@@ -0,0 +1,182 @@
package mjpeg
// RFC 2435. Appendix A
var jpeg_luma_quantizer = []byte{
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99,
}
var jpeg_chroma_quantizer = []byte{
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
}
func MakeTables(q byte) (lqt, cqt []byte) {
var factor int
switch {
case q < 1:
factor = 1
case q > 99:
factor = 99
default:
factor = int(q)
}
if q < 50 {
factor = 5000 / factor
} else if q > 99 {
factor = 200 - factor*2
}
lqt = make([]byte, 64)
cqt = make([]byte, 64)
for i := 0; i < 64; i++ {
lq := (int(jpeg_luma_quantizer[i])*factor + 50) / 100
cq := (int(jpeg_chroma_quantizer[i])*factor + 50) / 100
/* Limit the quantizers to 1 <= q <= 255 */
switch {
case lq < 1:
lqt[i] = 1
case lq > 255:
lqt[i] = 255
default:
lqt[i] = byte(lq)
}
switch {
case cq < 1:
cqt[i] = 1
case cq > 255:
cqt[i] = 255
default:
cqt[i] = byte(cq)
}
}
return
}
// RFC 2435. Appendix B
var lum_dc_codelens = []byte{
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
}
var lum_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var lum_ac_codelens = []byte{
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
}
var lum_ac_symbols = []byte{
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
var chm_dc_codelens = []byte{
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
}
var chm_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var chm_ac_codelens = []byte{
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
}
var chm_ac_symbols = []byte{
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
func MakeHeaders(t byte, w, h uint16, lqt, cqt []byte) []byte {
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
p := []byte{0xFF, 0xD8}
p = MakeQuantHeader(p, lqt, 0)
p = MakeQuantHeader(p, cqt, 1)
if t == 0 {
t = 0x21
} else {
t = 0x22
}
p = append(p,
0xFF, 0xC0, 0, 17, 8,
byte(h>>8), byte(h&0xFF),
byte(w>>8), byte(w&0xFF),
3, 0, t, 0, 1, 0x11, 1, 2, 0x11, 1,
)
p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
return append(p, 0xFF, 0xDA, 0, 12, 3, 0, 0, 1, 0x11, 2, 0x11, 0, 63, 0)
}
func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {
p = append(p, 0xFF, 0xDB, 0, 67, tableNo)
return append(p, qt...)
}
func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {
p = append(p,
0xFF, 0xC4, 0,
byte(3+len(codelens)+len(symbols)),
(tableClass<<4)|tableNo,
)
p = append(p, codelens...)
return append(p, symbols...)
}

23
pkg/mp4/README.md Normal file
View File

@@ -0,0 +1,23 @@
## HEVC
Browser | avc1 | hvc1 | hev1
------------|------|------|---
Mac Chrome | + | - | +
Mac Safari | + | + | -
iOS 15? | + | + | -
Mac Firefox | + | - | -
iOS 12 | + | - | -
Android 13 | + | - | -
```
ffmpeg -i input-hev1.mp4 -c:v copy -tag:v hvc1 -c:a copy output-hvc1.mp4
Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps,
```
## Useful links
- https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1
- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
- https://jellyfin.org/docs/general/clients/codec-support.html
- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding

94
pkg/mp4/const.go Normal file
View File

@@ -0,0 +1,94 @@
package mp4
import (
"encoding/binary"
"github.com/deepch/vdk/format/mp4/mp4io"
"time"
)
var matrix = [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000}
var time0 = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
func FTYP() []byte {
b := make([]byte, 0x18)
binary.BigEndian.PutUint32(b, 0x18)
copy(b[0x04:], "ftyp")
copy(b[0x08:], "iso5")
copy(b[0x10:], "iso5")
copy(b[0x14:], "avc1")
return b
}
func MOOV() *mp4io.Movie {
return &mp4io.Movie{
Header: &mp4io.MovieHeader{
PreferredRate: 1,
PreferredVolume: 1,
Matrix: matrix,
NextTrackId: -1,
Duration: 0,
TimeScale: 1000,
CreateTime: time0,
ModifyTime: time0,
PreviewTime: time0,
PreviewDuration: time0,
PosterTime: time0,
SelectionTime: time0,
SelectionDuration: time0,
CurrentTime: time0,
},
MovieExtend: &mp4io.MovieExtend{
Tracks: []*mp4io.TrackExtend{
{
TrackId: 1,
DefaultSampleDescIdx: 1,
DefaultSampleDuration: 40,
},
},
},
}
}
func TRAK() *mp4io.Track {
return &mp4io.Track{
// trak > tkhd
Header: &mp4io.TrackHeader{
TrackId: int32(1), // change me
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
Duration: 0, // OK
Matrix: matrix,
CreateTime: time0,
ModifyTime: time0,
},
// trak > mdia
Media: &mp4io.Media{
// trak > mdia > mdhd
Header: &mp4io.MediaHeader{
TimeScale: 1000,
Duration: 0,
Language: 0x55C4,
CreateTime: time0,
ModifyTime: time0,
},
// trak > mdia > minf
Info: &mp4io.MediaInfo{
// trak > mdia > minf > dinf
Data: &mp4io.DataInfo{
Refer: &mp4io.DataRefer{
Url: &mp4io.DataReferUrl{
Flags: 0x000001, // self reference
},
},
},
// trak > mdia > minf > stbl
Sample: &mp4io.SampleTable{
SampleDesc: &mp4io.SampleDesc{},
TimeToSample: &mp4io.TimeToSample{},
SampleToChunk: &mp4io.SampleToChunk{},
SampleSize: &mp4io.SampleSize{},
ChunkOffset: &mp4io.ChunkOffset{},
},
},
},
}
}

View File

@@ -1,27 +1,23 @@
package mse
package mp4
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/mp4f"
"github.com/pion/rtp"
"time"
)
const MsgTypeMSE = "mse"
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
muxer *mp4f.Muxer
streams []av.CodecData
start bool
muxer *Muxer
codecs []*streamer.Codec
start bool
send int
}
@@ -33,14 +29,16 @@ func (c *Consumer) GetMedias() []*streamer.Media {
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264, ClockRate: 90000},
},
}, {
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC, ClockRate: 16000},
{Name: streamer.CodecH265, ClockRate: 90000},
},
},
//{
// Kind: streamer.KindAudio,
// Direction: streamer.DirectionRecvonly,
// Codecs: []*streamer.Codec{
// {Name: streamer.CodecAAC, ClockRate: 16000},
// },
//},
}
}
@@ -48,18 +46,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
codec := track.Codec
switch codec.Name {
case streamer.CodecH264:
idx := int8(len(c.streams))
sps, pps := h264.GetParameterSet(codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil
}
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: idx, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
c.codecs = append(c.codecs, track.Codec)
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
@@ -69,7 +56,6 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
switch h264.NALUType(packet.Payload) {
case h264.NALUTypeIFrame:
c.start = true
pkt.IsKeyFrame = true
case h264.NALUTypePFrame:
if !c.start {
return nil
@@ -78,17 +64,9 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
for _, buf := range c.muxer.WritePacketV5(pkt) {
c.send += len(buf)
c.Fire(buf)
}
buf := c.muxer.Marshal(packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
@@ -98,30 +76,60 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecH265:
c.codecs = append(c.codecs, track.Codec)
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if !c.start {
if h265.IsKeyframe(packet.Payload) {
c.start = true
} else {
return nil
}
}
buf := c.muxer.Marshal(packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
if !h264.IsAVC(codec) {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
fmt.Printf("[rtmp] unsupported codec: %+v\n", track.Codec)
return nil
}
func (c *Consumer) Init() {
c.muxer = mp4f.NewMuxer(nil)
if err := c.muxer.WriteHeader(c.streams); err != nil {
return
func (c *Consumer) MimeType() string {
return c.muxer.MimeType(c.codecs)
}
func (c *Consumer) Init() ([]byte, error) {
if c.muxer == nil {
c.muxer = &Muxer{}
}
codecs, buf := c.muxer.GetInit(c.streams)
c.Fire(&streamer.Message{Type: MsgTypeMSE, Value: codecs})
c.send += len(buf)
c.Fire(buf)
return c.muxer.GetInit(c.codecs)
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MSE server consumer",
"type": "MP4 server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,

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

@@ -0,0 +1,209 @@
package mp4
import (
"encoding/binary"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f/mp4fio"
"github.com/pion/rtp"
)
type Muxer struct {
fragIndex uint32
dts uint64
pts uint32
data []byte
total int
}
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
s := `video/mp4; codecs="`
for _, codec := range codecs {
switch codec.Name {
case streamer.CodecH264:
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
case streamer.CodecH265:
// +Safari +Chrome +Edge -iOS15 -Android13
s += "hvc1.1.6.L93.B0" // hev1.1.6.L93.B0
}
}
return s + `"`
}
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
moov := MOOV()
for _, codec := range codecs {
switch codec.Name {
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil {
return nil, fmt.Errorf("empty SPS: %#v", codec)
}
// TODO: remove
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK()
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.AVC1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil {
return nil, fmt.Errorf("empty SPS: %#v", codec)
}
codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
if err != nil {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK()
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.HV1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
moov.Tracks = append(moov.Tracks, trak)
}
}
data := make([]byte, moov.Len())
moov.Marshal(data)
return append(FTYP(), data...), nil
}
func (m *Muxer) Rewind() {
m.dts = 0
m.pts = 0
}
func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
trackID := uint8(1)
run := &mp4fio.TrackFragRun{
Flags: 0x000b05,
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
DataOffset: 0,
Entries: []mp4io.TrackFragRunEntry{},
}
moof := &mp4fio.MovieFrag{
Header: &mp4fio.MovieFragHeader{
Seqnum: m.fragIndex + 1,
},
Tracks: []*mp4fio.TrackFrag{
{
Header: &mp4fio.TrackFragHeader{
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID, 0x01, 0x01, 0x00, 0x00},
},
DecodeTime: &mp4fio.TrackFragDecodeTime{
Version: 1,
Flags: 0,
Time: m.dts,
},
Run: run,
},
},
}
entry := mp4io.TrackFragRunEntry{
//Duration: 90000,
Size: uint32(len(packet.Payload)),
}
newTime := packet.Timestamp
if m.pts > 0 {
//m.dts += uint64(newTime - m.pts)
entry.Duration = newTime - m.pts
m.dts += uint64(entry.Duration)
}
m.pts = newTime
// important before moof.Len()
run.Entries = append(run.Entries, entry)
moofLen := moof.Len()
mdatLen := 8 + len(packet.Payload)
// important after moof.Len()
run.DataOffset = uint32(moofLen + 8)
buf := make([]byte, moofLen+mdatLen)
moof.Marshal(buf)
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
copy(buf[moofLen+4:], "mdat")
copy(buf[moofLen+8:], packet.Payload)
m.fragIndex++
m.total += moofLen + mdatLen
return buf
}

View File

@@ -2,10 +2,12 @@ package rtmp
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp"
@@ -22,6 +24,8 @@ type Client struct {
conn *rtmp.Conn
closed bool
receive int
}
func NewClient(uri string) *Client {
@@ -68,9 +72,36 @@ func (c *Client) Dial() (err error) {
c.tracks = append(c.tracks, track)
case av.AAC:
panic("not implemented")
// TODO: fix support
cd := stream.(aacparser.CodecData)
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
fmtp := fmt.Sprintf(
"config=%s",
hex.EncodeToString(cd.ConfigBytes),
)
codec := &streamer.Codec{
Name: streamer.CodecAAC,
ClockRate: uint32(cd.Config.SampleRate),
Channels: uint16(cd.Config.ChannelConfig),
FmtpLine: fmtp,
}
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
c.medias = append(c.medias, media)
track := &streamer.Track{
Codec: codec, Direction: media.Direction,
}
c.tracks = append(c.tracks, track)
default:
panic("unsupported codec")
fmt.Printf("[rtmp] unsupported codec %+v\n", stream)
}
}
@@ -94,13 +125,15 @@ func (c *Client) Handle() (err error) {
return
}
c.receive += len(pkt.Data)
track := c.tracks[int(pkt.Idx)]
timestamp := uint32(pkt.Time / time.Duration(track.Codec.ClockRate))
var payloads [][]byte
if track.Codec.Name == streamer.CodecH264 {
payloads = splitAVC(pkt.Data)
payloads = h264.SplitAVC(pkt.Data)
} else {
payloads = [][]byte{pkt.Data}
}
@@ -122,21 +155,3 @@ func (c *Client) Close() error {
c.closed = true
return c.conn.Close()
}
func splitAVC(data []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(data))
// check if multiple items in one packet
if size+4 < len(data) {
nals = append(nals, data[:size+4])
data = data[size+4:]
} else {
nals = append(nals, data)
break
}
}
return nals
}

View File

@@ -1,7 +1,10 @@
package rtmp
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv"
)
func (c *Client) GetMedias() []*streamer.Media {
@@ -14,7 +17,7 @@ func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streame
return track
}
}
panic("wrong codec")
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
}
func (c *Client) Start() error {
@@ -24,3 +27,21 @@ func (c *Client) Start() error {
func (c *Client) Stop() error {
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONReceive: c.receive,
streamer.JSONType: "RTMP client producer",
streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
"url": c.URI,
}
for i, media := range c.medias {
k := "media:" + strconv.Itoa(i)
v[k] = media.String()
}
for i, track := range c.tracks {
k := "track:" + strconv.Itoa(i)
v[k] = track.String()
}
return json.Marshal(v)
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtcp"
@@ -42,11 +43,15 @@ const (
ModeServerConsumer
)
const KeepAlive = time.Second * 25
type Conn struct {
streamer.Element
// public
Backchannel bool
Medias []*streamer.Media
Session string
UserAgent string
@@ -56,10 +61,10 @@ type Conn struct {
auth *tcp.Auth
conn net.Conn
mode Mode
reader *bufio.Reader
sequence int
mode Mode
uri string
tracks []*streamer.Track
channels map[byte]*streamer.Track
@@ -71,24 +76,10 @@ type Conn struct {
}
func NewClient(uri string) (*Conn, error) {
var err error
c := new(Conn)
c.URL, err = url.Parse(uri)
if err != nil {
return nil, err
}
if strings.IndexByte(c.URL.Host, ':') < 0 {
c.URL.Host += ":554"
}
// remove UserInfo from URL
c.auth = tcp.NewAuth(c.URL.User)
c.mode = ModeClientProducer
c.URL.User = nil
return c, nil
c.uri = uri
return c, c.parseURI()
}
func NewServer(conn net.Conn) *Conn {
@@ -99,10 +90,30 @@ func NewServer(conn net.Conn) *Conn {
return c
}
func (c *Conn) parseURI() (err error) {
c.URL, err = url.Parse(c.uri)
if err != nil {
return err
}
if strings.IndexByte(c.URL.Host, ':') < 0 {
c.URL.Host += ":554"
}
// remove UserInfo from URL
c.auth = tcp.NewAuth(c.URL.User)
c.URL.User = nil
return nil
}
func (c *Conn) Dial() (err error) {
//if c.state != StateClientInit {
// panic("wrong state")
//}
if c.conn != nil {
_ = c.parseURI()
}
c.conn, err = net.DialTimeout(
"tcp", c.URL.Host, 10*time.Second,
@@ -143,7 +154,9 @@ func (c *Conn) Request(req *tcp.Request) error {
}
c.sequence++
req.Header.Set("CSeq", strconv.Itoa(c.sequence))
// important to send case sensitive CSeq
// https://github.com/AlexxIT/go2rtc/issues/7
req.Header["CSeq"] = []string{strconv.Itoa(c.sequence)}
c.auth.Write(req)
@@ -188,7 +201,7 @@ func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("wrong response on %s", req.Method)
return res, fmt.Errorf("wrong response on %s", req.Method)
}
return res, nil
@@ -253,23 +266,27 @@ func (c *Conn) Describe() error {
Method: MethodDescribe,
URL: c.URL,
Header: map[string][]string{
"Accept": {"application/sdp"},
"Require": {"www.onvif.org/ver20/backchannel"},
"Accept": {"application/sdp"},
},
}
if c.Backchannel {
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
}
res, err := c.Do(req)
if err != nil {
return err
}
// fix bug in Sonoff camera SDP "o=- 1 1 IN IP4 rom t_rtsplin"
// TODO: make some universal fix
if i := bytes.Index(res.Body, []byte("rom t_rtsplin")); i > 0 {
res.Body[i+3] = '_'
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
if err != nil {
return err
}
}
c.Medias, err = streamer.UnmarshalRTSPSDP(res.Body)
c.Medias, err = UnmarshalSDP(res.Body)
if err != nil {
return err
}
@@ -317,11 +334,18 @@ func (c *Conn) SetupMedia(
return nil, fmt.Errorf("wrong media: %v", media)
}
trackURL, err := url.Parse(media.Control)
rawURL := media.Control
if !strings.Contains(rawURL, "://") {
rawURL = c.URL.String()
if !strings.HasSuffix(rawURL, "/") {
rawURL += "/"
}
rawURL += media.Control
}
trackURL, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
trackURL = c.URL.ResolveReference(trackURL)
req := &tcp.Request{
Method: MethodSetup,
@@ -338,7 +362,21 @@ func (c *Conn) SetupMedia(
var res *tcp.Response
res, err = c.Do(req)
if err != nil {
return nil, err
// Dahua VTO2111D fail on this step because of backchannel
if c.Backchannel {
if err = c.Dial(); err != nil {
return nil, err
}
c.Backchannel = false
if err = c.Describe(); err != nil {
return nil, err
}
res, err = c.Do(req)
}
if err != nil {
return nil, err
}
}
if c.Session == "" {
@@ -354,10 +392,23 @@ func (c *Conn) SetupMedia(
// we send our `interleaved`, but camera can answer with another
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.123;source=192.168.10.12;interleaved=0
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
s := res.Header.Get("Transport")
s, ok1, ok2 := between(s, "RTP/AVP/TCP;unicast;interleaved=", "-")
if !ok1 || !ok2 {
panic("wrong response")
// TODO: rewrite
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
return nil, fmt.Errorf("wrong transport: %s", s)
}
i := strings.Index(s, "interleaved=")
if i < 0 {
return nil, fmt.Errorf("wrong transport: %s", s)
}
s = s[i+len("interleaved="):]
i = strings.IndexAny(s, "-;")
if i > 0 {
s = s[:i]
}
ch, err = strconv.Atoi(s)
@@ -424,15 +475,17 @@ func (c *Conn) Accept() error {
return err
}
if c.URL == nil {
c.URL = req.URL
c.UserAgent = req.Header.Get("User-Agent")
}
c.Fire(req)
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
switch req.Method {
case MethodOptions:
c.URL = req.URL
c.UserAgent = req.Header.Get("User-Agent")
res := &tcp.Response{
Header: map[string][]string{
"Public": {"OPTIONS, SETUP, TEARDOWN, DESCRIBE, PLAY, PAUSE, ANNOUNCE, RECORD"},
@@ -448,7 +501,7 @@ func (c *Conn) Accept() error {
return errors.New("wrong content type")
}
c.Medias, err = streamer.UnmarshalRTSPSDP(req.Body)
c.Medias, err = UnmarshalSDP(req.Body)
if err != nil {
return err
}
@@ -548,6 +601,7 @@ func (c *Conn) Handle() (err error) {
}()
//c.Fire(streamer.StatePlaying)
ts := time.Now().Add(KeepAlive)
for {
// we can read:
@@ -602,7 +656,7 @@ func (c *Conn) Handle() (err error) {
if channelID&1 == 0 {
packet := &rtp.Packet{}
if err = packet.Unmarshal(buf); err != nil {
return errors.New("wrong RTP data")
return
}
track := c.channels[channelID]
@@ -610,22 +664,34 @@ func (c *Conn) Handle() (err error) {
_ = track.WriteRTP(packet)
//return fmt.Errorf("wrong channelID: %d", channelID)
} else {
panic("wrong channelID")
continue // TODO: maybe fix this
//panic("wrong channelID")
}
} else {
msg := &RTCP{Channel: channelID}
if err = msg.Header.Unmarshal(buf); err != nil {
return errors.New("wrong RTCP data")
return
}
msg.Packets, err = rtcp.Unmarshal(buf)
if err != nil {
return errors.New("wrong RTCP data")
return
}
c.Fire(msg)
}
// keep-alive
now := time.Now()
if now.After(ts) {
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
// don't need to wait respose on this request
if err = c.Request(req); err != nil {
return err
}
ts = now.Add(KeepAlive)
}
}
}
@@ -671,6 +737,11 @@ func (c *Conn) bindTrack(
return nil
}
if h264.IsAVC(track.Codec) {
wrapper := h264.RTPPay(1500)
push = wrapper(push)
}
return track.Bind(push)
}
@@ -680,17 +751,35 @@ type RTCP struct {
Packets []rtcp.Packet
}
func between(s, sub1, sub2 string) (res string, ok1 bool, ok2 bool) {
i := strings.Index(s, sub1)
if i >= 0 {
ok1 = true
s = s[i+len(sub1):]
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
// fix SDP header for some cameras
i := bytes.Index(rawSDP, []byte("\nm="))
if i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
medias, err = streamer.UnmarshalSDP(rawSDP)
}
if err != nil {
return nil, err
}
}
i = strings.Index(s, sub2)
if i >= 0 {
return s[:i], ok1, true
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return s, ok1, false
return medias, nil
}

41
pkg/shell/shell.go Normal file
View File

@@ -0,0 +1,41 @@
package shell
import (
"strings"
)
func QuoteSplit(s string) []string {
var a []string
for len(s) > 0 {
is := strings.IndexByte(s, ' ')
if is >= 0 {
// skip prefix and double spaces
if is == 0 {
// goto next symbol
s = s[1:]
continue
}
// check if quote in word
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
// search quote end
if is = strings.Index(s, `" `); is > 0 {
is += 1
} else {
is = -1
}
}
}
if is >= 0 {
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
s = s[is+1:]
} else {
//add last word
a = append(a, s)
break
}
}
return a
}

61
pkg/srtp/server.go Normal file
View File

@@ -0,0 +1,61 @@
package srtp
import (
"encoding/binary"
"net"
)
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
// this is not really necessary but anyway
type Server struct {
sessions map[uint32]*Session
}
func (s *Server) AddSession(session *Session) {
if s.sessions == nil {
s.sessions = map[uint32]*Session{}
}
s.sessions[session.RemoteSSRC] = session
}
func (s *Server) RemoveSession(session *Session) {
delete(s.sessions, session.RemoteSSRC)
}
func (s *Server) Serve(conn net.PacketConn) error {
buf := make([]byte, 2048)
for {
n, addr, err := conn.ReadFrom(buf)
if err != nil {
return err
}
// Multiplexing RTP Data and Control Packets on a Single Port
// https://datatracker.ietf.org/doc/html/rfc5761
// 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)
}
}
if err = session.HandleRTP(buf[:n]); err != nil {
return err
}
} else {
// this is default position for SSRC in RTCP packet
ssrc = binary.BigEndian.Uint32(buf[4:])
if session, ok = s.sessions[ssrc]; !ok {
continue // skip unknown ssrc
}
if err = session.HandleRTCP(buf[:n]); err != nil {
return err
}
}
}
}

97
pkg/srtp/session.go Normal file
View File

@@ -0,0 +1,97 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/srtp/v2"
)
type Session struct {
LocalSSRC uint32 // outgoing SSRC
RemoteSSRC uint32 // incoming SSRC
localCtx *srtp.Context // write context
remoteCtx *srtp.Context // read context
Write func(b []byte) (int, error)
Track *streamer.Track
}
func (s *Session) SetKeys(
localKey, localSalt, remoteKey, remoteSalt []byte,
) (err error) {
if s.localCtx, err = srtp.CreateContext(
localKey, localSalt, GuessProfile(localKey),
); err != nil {
return
}
s.remoteCtx, err = srtp.CreateContext(
remoteKey, remoteSalt, GuessProfile(remoteKey),
)
return
}
func (s *Session) HandleRTP(data []byte) (err error) {
if data, err = s.remoteCtx.DecryptRTP(nil, data, nil); err != nil {
return
}
packet := &rtp.Packet{}
if err = packet.Unmarshal(data); err != nil {
return
}
_ = s.Track.WriteRTP(packet)
//s.Output(core.RTP{Channel: s.Channel, Packet: packet})
return
}
func (s *Session) HandleRTCP(data []byte) (err error) {
header := &rtcp.Header{}
if data, err = s.remoteCtx.DecryptRTCP(nil, data, header); err != nil {
return
}
var packets []rtcp.Packet
if packets, err = rtcp.Unmarshal(data); err != nil {
return
}
_ = packets
//s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets})
if header.Type == rtcp.TypeSenderReport {
err = s.KeepAlive()
}
return
}
func (s *Session) KeepAlive() (err error) {
var data []byte
// we can send empty receiver response, but should send it to hold the connection
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
if data, err = rep.Marshal(); err != nil {
return
}
if data, err = s.localCtx.EncryptRTCP(nil, data, nil); err != nil {
return
}
_, err = s.Write(data)
return
}
func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
switch len(masterKey) {
case 16:
return srtp.ProtectionProfileAes128CmHmacSha1_80
//case 32:
// return srtp.ProtectionProfileAes256CmHmacSha1_80
}
return 0
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/pion/sdp/v3"
"strconv"
"strings"
"unicode"
)
const (
@@ -24,19 +25,21 @@ const (
CodecVP8 = "VP8"
CodecVP9 = "VP9"
CodecAV1 = "AV1"
CodecJPEG = "JPEG" // payloadType: 26
CodecPCMU = "PCMU" // payloadType: 0
CodecPCMA = "PCMA" // payloadType: 8
CodecAAC = "MPEG4-GENERIC"
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
CodecMPA = "MPA" // payload: 14
)
func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722:
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
return KindAudio
}
return ""
@@ -46,12 +49,13 @@ func GetKind(name string) string {
// - deepch/vdk/format/rtsp/sdp.Media
// - pion/sdp.MediaDescription
type Media struct {
Kind string // video, audio
Direction string
Codecs []*Codec
Kind string `json:"kind,omitempty"` // video or audio
Direction string `json:"direction,omitempty"`
Codecs []*Codec `json:"codecs,omitempty"`
MID string // TODO: fixme?
Control string // TODO: fixme?
MID string `json:"mid,omitempty"` // TODO: fixme?
Control string `json:"control,omitempty"` // TODO: fixme?
Title string `json:"title,omitempty"` // TODO: fixme?
}
func (m *Media) String() string {
@@ -126,12 +130,14 @@ type Codec struct {
func NewCodec(name string) *Codec {
name = strings.ToUpper(name)
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return &Codec{Name: name, ClockRate: 90000}
case CodecPCMU, CodecPCMA:
return &Codec{Name: name, ClockRate: 8000}
case CodecOpus:
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
case "MJPEG":
return &Codec{Name: CodecJPEG, ClockRate: 90000}
}
panic(fmt.Sprintf("unsupported codec: %s", name))
@@ -152,8 +158,8 @@ func (c *Codec) Clone() *Codec {
func (c *Codec) Match(codec *Codec) bool {
return c.Name == codec.Name &&
c.ClockRate == codec.ClockRate &&
c.Channels == codec.Channels
(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
(c.Channels == codec.Channels || codec.Channels == 0)
}
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
@@ -180,26 +186,6 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
return medias, nil
}
func UnmarshalRTSPSDP(rawSDP []byte) ([]*Media, error) {
medias, err := UnmarshalSDP(rawSDP)
if err != nil {
return nil, err
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case DirectionRecvonly, "":
media.Direction = DirectionSendonly
case DirectionSendonly:
media.Direction = DirectionRecvonly
}
}
return medias, nil
}
func MarshalSDP(medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{}
@@ -260,7 +246,8 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
ss := strings.Split(attr.Value[i+1:], "/")
c.Name = strings.ToUpper(ss[0])
c.ClockRate = uint32(atoi(ss[1]))
// fix tailing space: `a=rtpmap:96 H264/90000 `
c.ClockRate = uint32(atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
if len(ss) == 3 && ss[2] == "2" {
c.Channels = 2
@@ -273,15 +260,22 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
}
if c.Name == "" {
// https://en.wikipedia.org/wiki/RTP_payload_formats
switch payloadType {
case "0":
c.Name = "PCMU"
c.Name = CodecPCMU
c.ClockRate = 8000
case "8":
c.Name = "PCMA"
c.Name = CodecPCMA
c.ClockRate = 8000
case "14":
c.Name = CodecMPA
c.ClockRate = 44100
case "26":
c.Name = CodecJPEG
c.ClockRate = 90000
default:
panic("unknown codec")
c.Name = payloadType
}
}

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package webrtc
import (
"github.com/pion/ice/v2"
"github.com/pion/interceptor"
"github.com/pion/webrtc/v3"
"net"
@@ -21,31 +22,30 @@ func NewAPI(address string) (*webrtc.API, error) {
return nil, err
}
if address == "" {
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
webrtc.WithInterceptorRegistry(i),
), nil
}
ln, err := net.Listen("tcp", address)
if err != nil {
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
webrtc.WithInterceptorRegistry(i),
), err
}
s := webrtc.SettingEngine{
//LoggerFactory: customLoggerFactory{},
}
s.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
// disable listen on Hassio docker interfaces
s.SetInterfaceFilter(func(name string) bool {
return name != "hassio" && name != "docker0"
})
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
s.SetICETCPMux(tcpMux)
// disable mDNS listener
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
if address != "" {
ln, err := net.Listen("tcp", address)
if err == nil {
s.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
})
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
s.SetICETCPMux(tcpMux)
}
}
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
@@ -86,10 +86,6 @@ func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
PayloadType: 98, //123,
},
// macOS Safari 15.1
{
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f", videoRTCPFeedback},
PayloadType: 99,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback},
PayloadType: 100,

View File

@@ -1,6 +1,7 @@
package webrtc
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/webrtc/v3"
)
@@ -57,94 +58,39 @@ func (c *Conn) Init() {
}
}
panic("something wrong")
fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
})
// OK connection:
// 15:01:46 ICE connection state changed: checking
// 15:01:46 peer connection state changed: connected
// 15:01:54 peer connection state changed: disconnected
// 15:02:20 peer connection state changed: failed
//
// Fail connection:
// 14:53:08 ICE connection state changed: checking
// 14:53:39 peer connection state changed: failed
c.Conn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
c.Fire(state)
// TODO: remove
switch state {
case webrtc.PeerConnectionStateConnected:
c.Fire(streamer.StatePlaying)
c.Fire(streamer.StatePlaying) // TODO: remove
case webrtc.PeerConnectionStateDisconnected:
c.Fire(streamer.StateNull)
case webrtc.PeerConnectionStateFailed:
c.Fire(streamer.StateNull) // TODO: remove
// disconnect event comes earlier, than failed
// but it comes only for success connections
_ = c.Conn.Close()
c.Conn = nil
case webrtc.PeerConnectionStateFailed:
if c.Conn != nil {
_ = c.Conn.Close()
}
}
})
}
func (c *Conn) ExchangeSDP(offer string, complete bool) (answer string, err error) {
sdOffer := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer, SDP: offer,
}
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
return
}
//for _, tr := range c.Conn.GetTransceivers() {
// switch tr.Direction() {
// case webrtc.RTPTransceiverDirectionSendonly:
// // disable transceivers if we don't have track
// // make direction=inactive
// // don't really necessary, but anyway
// if tr.Sender() == nil {
// if err = tr.Stop(); err != nil {
// return
// }
// }
// case webrtc.RTPTransceiverDirectionRecvonly:
// // TODO: change codecs list
// caps := webrtc.RTPCodecCapability{
// MimeType: webrtc.MimeTypePCMU,
// ClockRate: 8000,
// }
// codecs := []webrtc.RTPCodecParameters{
// {RTPCodecCapability: caps},
// }
// if err = tr.SetCodecPreferences(codecs); err != nil {
// return
// }
// }
//}
var sdAnswer webrtc.SessionDescription
sdAnswer, err = c.Conn.CreateAnswer(nil)
if err != nil {
return
}
//var sd *sdp.SessionDescription
//sd, err = sdAnswer.Unmarshal()
//for _, media := range sd.MediaDescriptions {
// if media.MediaName.Media != "audio" {
// continue
// }
// for i, attr := range media.Attributes {
// if attr.Key == "sendonly" {
// attr.Key = "inactive"
// media.Attributes[i] = attr
// break
// }
// }
//}
//var b []byte
//b, err = sd.Marshal()
//sdAnswer.SDP = string(b)
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
return
}
if complete {
<-webrtc.GatheringCompletePromise(c.Conn)
return c.Conn.LocalDescription().SDP, nil
}
return sdAnswer.SDP, nil
}
func (c *Conn) SetOffer(offer string) (err error) {
sdOffer := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer, SDP: offer,
@@ -186,10 +132,47 @@ func (c *Conn) GetAnswer() (answer string, err error) {
return sdAnswer.SDP, nil
}
func (c *Conn) GetCompleteAnswer() (answer string, err error) {
if _, err = c.GetAnswer(); err != nil {
return
}
<-webrtc.GatheringCompletePromise(c.Conn)
return c.Conn.LocalDescription().SDP, nil
}
func (c *Conn) remote() string {
if c.Conn == nil {
return ""
}
for _, trans := range c.Conn.GetTransceivers() {
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
if trans == nil {
continue
}
receiver := trans.Receiver()
if receiver == nil {
continue
}
transport := receiver.Transport()
if transport == nil {
continue
}
iceTransport := transport.ICETransport()
if iceTransport == nil {
continue
}
pair, _ := iceTransport.GetSelectedCandidatePair()
if pair == nil || pair.Remote == nil {
continue
}
return pair.Remote.String()
}
return ""
}

View File

@@ -3,6 +3,7 @@ package webrtc
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3"
@@ -51,14 +52,26 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
return trackLocal.WriteRTP(packet)
}
if codec.Name == streamer.CodecH264 {
switch codec.Name {
case streamer.CodecH264:
wrapper := h264.RTPPay(1200)
push = wrapper(push)
if codec.PayloadType != 255 {
if h264.IsAVC(codec) {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track)
push = wrapper(push)
}
push = wrapper(push)
case streamer.CodecH265:
// SafariPay because it is the only browser in the world
// that supports WebRTC + H265
wrapper := h265.SafariPay(1200)
push = wrapper(push)
wrapper = h265.RTPDepay(track)
push = wrapper(push)
}
track = track.Bind(push)

View File

@@ -1,12 +1,14 @@
package webrtc
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/ice/v2"
"github.com/pion/stun"
"github.com/pion/webrtc/v3"
"net"
"strconv"
"strings"
)
func NewCandidate(address string) (string, error) {
@@ -34,6 +36,31 @@ func NewCandidate(address string) (string, error) {
return "candidate:" + cand.Marshal(), nil
}
func LookupIP(address string) (string, error) {
if strings.HasPrefix(address, "stun:") {
ip, err := GetPublicIP()
if err != nil {
return "", err
}
return ip.String() + address[4:], nil
}
if IsIP(address) {
return address, nil
}
i := strings.IndexByte(address, ':')
ips, err := net.LookupIP(address[:i])
if err != nil {
return "", err
}
if len(ips) == 0 {
return "", fmt.Errorf("can't resolve: %s", address)
}
return ips[0].String() + address[i:], nil
}
// GetPublicIP example from https://github.com/pion/stun
func GetPublicIP() (net.IP, error) {
c, err := stun.Dial("udp", "stun.l.google.com:19302")
@@ -47,6 +74,9 @@ func GetPublicIP() (net.IP, error) {
if err = c.Do(message, func(e stun.Event) { res = e }); err != nil {
return nil, err
}
if err = c.Close(); err != nil {
return nil, err
}
if res.Error != nil {
return nil, res.Error
@@ -60,6 +90,15 @@ func GetPublicIP() (net.IP, error) {
return xorAddr.IP, nil
}
func IsIP(host string) bool {
for _, i := range host {
if i >= 'A' {
return false
}
}
return true
}
func MimeType(codec *streamer.Codec) string {
switch codec.Name {
case streamer.CodecH264:

19
scripts/README.md Normal file
View File

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

View File

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

View File

@@ -0,0 +1,5 @@
@SET GOOS=linux
@SET GOARCH=amd64
@cd ..
del go2rtc
go build -ldflags "-s -w" -trimpath

View File

@@ -0,0 +1,5 @@
@SET GOOS=linux
@SET GOARCH=arm
@cd ..
del go2rtc
go build -ldflags "-s -w" -trimpath

View File

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

View File

@@ -0,0 +1,5 @@
@SET GOOS=darwin
@SET GOARCH=amd64
@cd ..
del go2rtc
go build -ldflags "-s -w" -trimpath

4
scripts/build_win64.cmd Normal file
View File

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

View File

@@ -46,4 +46,10 @@ pc.ontrack = ev => {
video.srcObject = ev.streams[0];
}
```
```
## Useful links
- https://www.webrtc-experiment.com/DetectRTC/
- https://divtable.com/table-styler/
- https://www.chromium.org/audio-video/

View File

@@ -45,7 +45,9 @@
'video/mp4; codecs="avc1.640032"',
'video/mp4; codecs="avc1.640C32"',
'video/mp4; codecs="avc1.F4001F"',
'video/mp4; codecs="hvc1.016000"',
'video/mp4; codecs="hvc1.1.6.L93.B0"',
'video/mp4; codecs="hev1.1.6.L93.B0"',
'video/mp4; codecs="hev1.2.4.L120.B0"',
];
const video = document.createElement("video");

75
www/devices.html Normal file
View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>go2rtc</title>
<style>
table {
background-color: white;
text-align: left;
border-collapse: collapse;
}
table td, table th {
border: 1px solid black;
padding: 5px 5px;
}
table tbody td {
font-size: 13px;
}
table thead {
background: #CFCFCF;
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
border-bottom: 3px solid black;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: black;
text-align: center;
}
.header {
padding: 5px 5px;
}
</style>
</head>
<body>
<script src="main.js"></script>
<table>
<thead>
<tr>
<th>Kind</th>
<th>Name</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
fetch(`${baseUrl}/api/devices`)
.then(r => r.json())
.then(data => {
document.querySelector("body > table > tbody").innerHTML =
data.reduce((html, item) => {
return html + `<tr>
<td>${item.kind}</td>
<td>${item.title}</td>
</tr>`;
}, '');
})
.catch(console.error);
</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More