Compare commits

...

141 Commits

Author SHA1 Message Date
Alexey Khit
fae59c7992 Update version to 0.1-rc.4 2022-11-24 02:00:31 +03:00
Alexey Khit
aff34f1d21 Totally rewritten MSE player 2022-11-24 01:59:48 +03:00
Alexey Khit
65e7efa775 Support codecs negotiation for MSE 2022-11-23 21:45:10 +03:00
Alexey Khit
3c3e9d282b Update about H265 support in readme 2022-11-23 20:35:11 +03:00
Alexey Khit
bd51069086 Add support CORS for API 2022-11-23 20:34:06 +03:00
Alexey Khit
1ddf7f1a6c Change trace log for stream.mp4 2022-11-23 12:57:11 +03:00
Alexey Khit
0e281e36d3 Fix race (concurency) for Track 2022-11-22 20:03:36 +03:00
Alexey Khit
3d6472cfb1 Update H265 preset for FFmpeg 2022-11-22 17:22:54 +03:00
Alexey Khit
7c31fa2ffd Fix empty SPS for MSE H265 2022-11-22 17:22:26 +03:00
Alexey Khit
0ed9d2410a Fix H265 support for MSE in Safari 2022-11-22 17:21:58 +03:00
Alexey Khit
1c89e7945e Remove printf for webrtc ontrack 2022-11-18 09:13:24 +03:00
Alexey Khit
48635ae341 Add two locks for Track 2022-11-18 09:12:48 +03:00
Alexey Khit
fdb316910f Fix WebRTC async connection 2022-11-16 11:26:56 +03:00
Alexey Khit
e29f2594fa Fix multiple transcoding when track not exists 2022-11-15 16:16:22 +03:00
Alexey Khit
c3da7584b0 Add check on RTSP server requers with empty url path 2022-11-14 19:06:43 +03:00
Alexey Khit
1e247cba92 Igroner app media for WebRTC from Hass 2022-11-14 17:26:59 +03:00
Alexey Khit
01631d9eb0 Update readme 2022-11-14 14:57:43 +03:00
Alexey Khit
4b27d119f0 Update version to 0.1-rc.3 2022-11-14 09:50:47 +03:00
Alexey Khit
dd55c03dc2 Add support multiple transcoding for ffmpeg 2022-11-14 02:26:14 +03:00
Alexey Khit
a4eab1944a Add ffmpeg async option for HomeKit audio 2022-11-14 01:29:45 +03:00
Alexey Khit
eea413a36c Support stream name as ffmpeg input 2022-11-14 01:22:07 +03:00
Alexey Khit
cdd42a8ed2 Change HomeKit codec from AAC to ELD 2022-11-14 00:58:34 +03:00
Alexey Khit
4815ce1baf Fix stop ffmpeg without matching tracks 2022-11-14 00:58:34 +03:00
Alexey Khit
e6d3939c78 Fix external producers 2022-11-13 21:33:09 +03:00
Alexey Khit
220b9ca318 Remove old example file 2022-11-13 20:54:19 +03:00
Alexey Khit
d625620dfd Fix ffmpeg profile warning 2022-11-13 20:09:18 +03:00
Alexey Khit
dd503f3410 Adds rotate template for ffmpeg 2022-11-13 20:05:54 +03:00
Alexey Khit
3e8e87bfcc Fix RTSP unknown response handler 2022-11-13 19:26:22 +03:00
Alexey Khit
64d218886e Add exec early exit handler 2022-11-13 19:24:26 +03:00
Alexey Khit
e91ccc211e Change ffmpeg verbose level to error 2022-11-13 19:18:53 +03:00
Alexey Khit
9f8a219483 Exec stderr will show with debug log 2022-11-13 19:15:12 +03:00
Alexey Khit
b617796941 Improve RTCP for HomeKit 2022-11-13 14:35:53 +03:00
Alexey Khit
77888fe086 Refactoring for HomeKit client 2022-11-11 22:47:14 +03:00
Alexey Khit
7bc3534bcb Add useful links to readmes 2022-11-11 22:44:54 +03:00
Alexey Khit
77bc0630d6 Add teaps to main readme 2022-11-11 22:44:49 +03:00
Alexey Khit
2f68711405 Fix MP4f test consumer 2022-11-11 22:44:34 +03:00
Alexey Khit
b8cab5db60 Remove aacparser from MP4 muxer 2022-11-11 22:44:05 +03:00
Alexey Khit
eae01be71f Add User-Agent to FFmpeg input and output 2022-11-11 18:04:31 +03:00
Alexey Khit
0127115180 Add User-Agent to go2rtc RTSP requests 2022-11-11 18:02:08 +03:00
Alexey Khit
aef84cef6b Add go2rtc version info 2022-11-11 18:01:38 +03:00
Alexey Khit
d478436758 Set video track for WebRTC always first 2022-11-11 16:33:08 +03:00
Alexey Khit
f77db44529 Refactoring for HomeKit client 2022-11-11 13:24:09 +03:00
Alex X
149d1bf235 Merge pull request #101 from felipecrs/patch-2
Mention alternative method to import hass cameras
2022-11-09 20:34:31 +03:00
Felipe Santos
b650475b10 Mention alternative method to import hass cameras 2022-11-09 13:00:30 -03:00
Alexey Khit
16e5406156 Update readme 2022-11-08 09:57:17 +03:00
Alexey Khit
49f6233bde Update RTSP server filters 2022-11-08 01:50:28 +03:00
Alexey Khit
78c5c70c73 Add duration API for MP4 file 2022-11-08 01:29:58 +03:00
Alexey Khit
32651c74ab Fix frame.mp4 support 2022-11-08 01:13:38 +03:00
Alexey Khit
5c64d1f847 Update MSE procession on JS side 2022-11-08 00:37:32 +03:00
Alexey Khit
717af29630 Refactoring 2022-11-08 00:37:13 +03:00
Alexey Khit
ea18475d31 Split MSE data on packets 2022-11-07 23:35:36 +03:00
Alexey Khit
701a9c69ec Update write websocket func 2022-11-07 23:35:08 +03:00
Alexey Khit
c06253c8b2 Fix producer request new track after start 2022-11-07 17:48:45 +03:00
Alexey Khit
3a07e9fa03 Fix lock on mp4 restarts 2022-11-07 13:32:27 +03:00
Alexey Khit
e1bc30fab3 Add support AAC for RTSP 2022-11-07 11:02:26 +03:00
Alexey Khit
d16ae0972f Code refactoring 2022-11-07 11:01:19 +03:00
Alexey Khit
8b93c97e69 Add support AAC for RTMP to MP4 2022-11-06 22:44:48 +03:00
Alexey Khit
d8158bc1e3 Update stream log message 2022-11-04 22:27:11 +03:00
Alexey Khit
f4f588d2c6 Add mutex to stream 2022-11-04 22:20:52 +03:00
Alexey Khit
e287b52808 Add check for RTSPtoWeb API 2022-11-04 22:12:00 +03:00
Alexey Khit
ff96257252 Add backchannel=0 option to readme 2022-11-04 21:49:36 +03:00
Alexey Khit
909f21b7e4 Update docs about TURN server 2022-11-04 21:44:12 +03:00
Alexey Khit
7d6a5b44f8 Add frame.jpeg api for MJPEG stream 2022-11-04 21:22:33 +03:00
Alexey Khit
278f7696b6 Make sink private for Track 2022-11-04 20:54:35 +03:00
Alexey Khit
3cbf2465ae Fix loopback producer 2022-11-04 17:52:26 +03:00
Alexey Khit
e9ea7a0b1f Add reconnection feature 2022-11-04 17:23:42 +03:00
Alexey Khit
0231fc3a90 Code refactoring 2022-11-04 17:16:42 +03:00
Alexey Khit
9ef2633840 Add 5 sec timeout to ffmpeg rtsp 2022-11-04 17:06:24 +03:00
Alexey Khit
5a8df3e90a Change RTSP dial timeout to 5 sec 2022-11-04 17:05:57 +03:00
Alexey Khit
a31cbec3eb Fix check RTSP transport prefix 2022-11-04 17:05:30 +03:00
Alexey Khit
54f547977e Add mutext to streams handlers 2022-11-04 17:04:47 +03:00
Alexey Khit
65d91e02bd Move NewLogger to function 2022-11-04 17:03:56 +03:00
Alexey Khit
7fc3f0f641 Ignore srtp init in stack list func 2022-11-04 10:07:50 +03:00
Alexey Khit
7725d5ed31 Rewrite get handlers code 2022-11-04 06:24:39 +03:00
Alexey Khit
6c1b9daa8b Update logs for RTSP packets (disabled) 2022-11-03 11:25:47 +03:00
Alexey Khit
6d432574bf Make main logger global 2022-11-03 10:26:26 +03:00
Alexey Khit
616f69c88b Cache public IP for 5 min 2022-11-02 12:48:36 +03:00
Alexey Khit
f72440712b Add timeout to GetPublicIP func 2022-11-02 12:47:26 +03:00
Alexey Khit
ceed146fb8 Add webrtc sync API 2022-11-02 12:46:39 +03:00
Alexey Khit
f17dadbbbf Rewrite keepalive and add timeouts for RTSP 2022-11-02 10:50:11 +03:00
Alexey Khit
3d4514eab9 Fix loopback for stream 2022-11-02 08:51:54 +03:00
Alexey Khit
2629dccb81 Rename HTTP-FLV 2022-10-31 20:05:28 +03:00
Alexey Khit
04f1aa2900 Fix trash in webrtc.html 2022-10-31 08:34:32 +03:00
Alexey Khit
0dacdea1c3 Add support RTMPT (flv over HTTP) 2022-10-30 17:17:42 +03:00
Alexey Khit
24082b1616 Fix backchannel reconnection issue 2022-10-29 11:33:01 +03:00
Alexey Khit
7964b1743b Fix RTSP processing for Amcrest IP4M-1051 2022-10-29 11:29:53 +03:00
Alexey Khit
49773a1ece Add mjpeg link to stream to main page 2022-10-21 12:01:00 +03:00
Alexey Khit
c97a48a73f Fix mjpeg for 2K cameras 2022-10-21 12:00:00 +03:00
Alexey Khit
e03231ebb4 fix ffmpeg transcoding for Reolink RLC-510A 2022-10-21 10:58:56 +03:00
Alexey Khit
649525a842 Merge remote-tracking branch 'origin/master' 2022-10-21 10:54:54 +03:00
Alex X
d411c1a25c Merge pull request #76 from NickM-27/name_api_stream
Add ability for API to set name of stream
2022-10-14 06:59:31 +03:00
Nicolas Mowen
2f0bcf4ae0 Add ability for API to set name of stream 2022-10-13 14:58:26 -06:00
Alexey Khit
831c504cab Fix memory usage for RTSP processing 2022-10-05 21:15:59 +03:00
Alexey Khit
12925a6bc5 Fix TP-Link Tapo TC70 support 2022-10-05 19:43:36 +03:00
Alexey Khit
e50e929150 Fix empty SPS for mp4 format 2022-10-05 15:35:30 +03:00
Alexey Khit
d0c87e0379 Support SEI NAL from ffmpeg transcoding 2022-10-05 15:35:04 +03:00
Alexey Khit
247b61790e Update EncodeAVC for empty NALs 2022-10-05 15:34:34 +03:00
Alexey Khit
2ec618334a Adds NALs types logger 2022-10-05 15:33:51 +03:00
Alexey Khit
6f9976c806 Rework RTSP and RTMP processing 2022-10-05 13:25:29 +03:00
Alexey Khit
17b3a4cf3a Code refactoring 2022-10-05 13:23:31 +03:00
Alexey Khit
ba30f46c02 Fix FmtpLine for RTMP 2022-10-05 10:50:00 +03:00
Alexey Khit
4134f2a89c Fix timestamp for RTMP 2022-10-05 10:48:37 +03:00
Alexey Khit
a81160bea1 Fix support Escam Q6 camera 2022-10-03 21:12:27 +03:00
Alexey Khit
80392acb78 Fix audio copy #46 2022-09-24 08:24:52 +03:00
Alexey Khit
5afac513b4 Adds codecs section to readme 2022-09-22 00:22:44 +03:00
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
95 changed files with 4919 additions and 2252 deletions

257
README.md
View File

@@ -5,11 +5,11 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
![](assets/go2rtc.png) ![](assets/go2rtc.png)
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM) - zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
- zero-delay for all supported protocols (lowest possible streaming latency) - zero-delay for many supported protocols (lowest possible streaming latency)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device), [files](#source-ffmpeg) and [other sources](#module-streams) - streaming 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) or [MSE](#module-api) - streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit) - first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- low CPU load for supported codecs - first project in the World with support H265 for WebRTC in browser (only Safari)
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg) - on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- multi-source 2-way [codecs negotiation](#codecs-negotiation) - multi-source 2-way [codecs negotiation](#codecs-negotiation)
- mixing tracks from different sources to single stream - mixing tracks from different sources to single stream
@@ -27,35 +27,6 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- [MediaSoup](https://mediasoup.org/) framework routing idea - [MediaSoup](https://mediasoup.org/) framework routing idea
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap) - HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
Now you have stream with two sources - **RTSP and FFmpeg**:
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
```
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
![](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 ## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) 1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
@@ -98,13 +69,9 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
- go2rtc > Install > Start - go2rtc > Install > Start
2. Setup [Integration](#module-hass) 2. Setup [Integration](#module-hass)
**Optionally:**
- create `go2rtc.yaml` in your Home Assistant [config](https://www.home-assistant.io/docs/configuration) folder
### go2rtc: Docker ### go2rtc: Docker
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg) and [Ngrok](#module-ngrok) applications. 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 ```yaml
services: services:
@@ -132,8 +99,9 @@ Available modules:
- [api](#module-api) - HTTP API (important for WebRTC support) - [api](#module-api) - HTTP API (important for WebRTC support)
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support) - [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
- [webrtc](#module-webrtc) - WebRTC Server - [webrtc](#module-webrtc) - WebRTC Server
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network) - [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot
- [ffmpeg](#source-ffmpeg) - FFmpeg integration - [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration - [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config - [log](#module-log) - logs config
@@ -144,15 +112,15 @@ Available modules:
Available source types: Available source types:
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras - [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
- [rtmp](#source-rtmp) - `RTMP` streams - [rtmp](#source-rtmp) - `RTMP` and `HTTP-FLV` streams
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types) - [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and others)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam - [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration - [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 - [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 - [hass](#source-hass) - Home Assistant integration
**PS.** You can use sources like `MJPEG`, `HLS` and others via FFmpeg integration.
#### Source: RTSP #### Source: RTSP
- Support **RTSP and RTSPS** links with multiple video and audio tracks - Support **RTSP and RTSPS** links with multiple video and audio tracks
@@ -176,6 +144,8 @@ streams:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1 - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
``` ```
**PS.** For disable bachannel just add `#backchannel=0` to end of RTSP link.
#### Source: RTMP #### Source: RTMP
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio. You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
@@ -209,24 +179,27 @@ streams:
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
# [MJPEG] video will be transcoded to H264 # [MJPEG] video will be transcoded to H264
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264 mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
# [RTSP] video with rotation, should be transcoded, so select H264 # [RTSP] video with rotation, should be transcoded, so select H264
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#raw=-vf transpose=1#video=h264 rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
``` ```
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac/16000`. All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to config and use them with source params. But you can override them via YAML config. You can also add your own formats to config and use them with source params.
```yaml ```yaml
ffmpeg: ffmpeg:
bin: ffmpeg # path to ffmpeg binary bin: ffmpeg # path to ffmpeg binary
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1" h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that support ffmpeg..." mycodec: "-any args that support ffmpeg..."
``` ```
Also you can use `raw` param for any additional FFmpeg arguments. As example for video rotation (`#raw=-vf transpose=1`). Remember that rotation is not possible without transcoding, so add supported codec as second param (`#video=h264`). - You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
#### Source: FFmpeg Device #### Source: FFmpeg Device
@@ -254,6 +227,19 @@ 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}
``` ```
#### Source: Echo
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).
**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 #### Source: HomeKit
**Important:** **Important:**
@@ -268,8 +254,35 @@ You can pair device with go2rtc on the HomeKit page. If you can't see your devic
If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it. If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it.
**Important:**
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violation
- Audio can be transcoded by [ffmpeg](#source-ffmpeg) source with `#async` option
- Audio can be played by `ffplay` with `-use_wallclock_as_timestamps 1 -async 1` options
- Audio can't be played in `VLC` and probably any other player
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
```
streams:
aqara_g3:
- hass:Camera-Hub-G3-AB12
- ffmpeg:aqara_g3#audio=aac#audio=opus#async
```
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions). **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 #### Source: Hass
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files: Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
@@ -286,6 +299,8 @@ streams:
aqara_g3: hass:Camera-Hub-G3-AB12 aqara_g3: hass:Camera-Hub-G3-AB12
``` ```
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
### Module: API ### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`. The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
@@ -299,23 +314,20 @@ The HTTP API is the main part for interacting with the application. Default addr
```yaml ```yaml
api: api:
listen: ":1984" # HTTP API port ("" - disabled) listen: ":1984" # HTTP API port ("" - disabled)
base_path: "" # API prefix for serve on suburl base_path: "/rtc" # API prefix for serve on suburl (/api => /rtc/api)
static_dir: "" # folder for static files (custom web interface) static_dir: "www" # folder for static files (custom web interface)
origin: "*" # allow CORS requests (only * supported)
``` ```
**PS. go2rtc** don't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks. **PS. go2rtc** doesn't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks.
**PS2.** You can access microphone (for 2-way audio) only with HTTPS **PS2.** You can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
### Module: RTSP ### Module: RTSP
You can get any stream as RTSP-stream with codecs filter: You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
``` - you can omit the codec filters, so one first video and one first audio will be selected
rtsp://192.168.1.123/{stream_name}?video={codec}&audio={codec1}&audio={codec2}
```
- you can omit the codecs, so one first video and one first audio will be selected
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected - you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
- you can set multiple video or audio, so all of them will be selected - you can set multiple video or audio, so all of them will be selected
@@ -371,13 +383,13 @@ ngrok:
command: ... command: ...
``` ```
**Own TCP-tunnel** **Hard tech way 1. Own TCP-tunnel**
If you have personal VPS, you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config. If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
**Using TURN-server** **Hard tech way 2. Using TURN-server**
TODO... If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can install TURN server (e.g. [coturn](https://github.com/coturn/coturn), config [example](https://github.com/AlexxIT/WebRTC/wiki/Coturn-Example)).
```yaml ```yaml
webrtc: webrtc:
@@ -444,25 +456,54 @@ tunnels:
### Module: Hass ### Module: Hass
**go2rtc** compatible with Home Assistant [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration. If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address. In other cases you need to use IP-address of server with **go2rtc** application.
If you install **go2rtc** as [Hass Add-on](#go2rtc-home-assistant-add-on) - you need to use localhost IP-address, example: #### From go2rtc to Hass
- `http://127.0.0.1:1984/` to web interface 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.
- `rtsp://127.0.0.1:8554/camera1` to RTSP streams
In other cases you need to use IP-address of server with **go2rtc** application. 1. Add your stream to [go2rtc config](#configuration)
2. Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://127.0.0.1:8554/camera1`
1. Add integration with link to go2rtc HTTP API: #### From Hass to go2rtc
- Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
2. Add generic camera with RTSP link: View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`PCMU`/`PCMA`/`OPUS`, best latency.
- Hass > Settings > Integrations > Add Integration > [Generic Camera](https://my.home-assistant.io/redirect/config_flow_start/?domain=generic) > `rtsp://...` or `rtmp://...`
- you can use either direct RTSP links to cameras or take RTSP streams from **go2rtc** 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.
3. Use Picture Entity or Picture Glance lovelace card
4. Open full screen card - this is should be WebRTC stream 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. 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 ### Module: Log
You can set different log levels for different modules. You can set different log levels for different modules.
@@ -504,6 +545,74 @@ If you need Web interface protection without Home Assistant Add-on - you need to
PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port. PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port.
## Codecs madness
`AVC/H.264` codec can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
Device | WebRTC | MSE | MP4
-------|--------|-----|----
*latency* | best | medium | bad
Desktop Chrome 107+ | H264 | H264, H265* | H264, H265*
Desktop Safari | H264, H265* | H264, H265 | **no!**
Desktop Edge | H264 | H264, H265* | H264, H265*
Desktop Firefox | H264 | H264 | H264
iPad Safari 13+ | H264, H265* | H264, H265 | **no!**
iPhone Safari 13+ | H264, H265* | **no!** | **no!**
Android Chrome 107+ | H264 | H264, H265* | H264
masOS Hass App | no | no | no
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
- Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265
- iOS Safari H265: Settings > Safari > Advanced > Experimental > WebRTC H265
**Audio**
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- MSE/MP4 audio codecs: `AAC`
**Apple devices**
- all Apple devices don't support MP4 stream (they only support progressive loading of static files)
- iPhones don't support MSE technology because it competes with the HLS technology, invented by Apple
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
Now you have stream with two sources - **RTSP and FFmpeg**:
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
```
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
![](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.
## TIPS
**Using apps for low RTSP delay**
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
## FAQ ## FAQ
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?** **Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**

View File

@@ -1,23 +1,40 @@
ARG BUILD_FROM ARG BUILD_FROM
FROM $BUILD_FROM
RUN apk add --no-cache git go ffmpeg FROM $BUILD_FROM as build
ARG BUILD_ARCH # 1. Build go2rtc
RUN apk add --no-cache git go
RUN git clone https://github.com/AlexxIT/go2rtc \ RUN git clone https://github.com/AlexxIT/go2rtc \
&& cd go2rtc \ && cd go2rtc \
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin && 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 # https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \ RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \ elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
&& cd go2rtc \ && cd go2rtc \
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \ && curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
&& unzip ngrok -d /usr/local/bin && unzip ngrok
RUN rm -r /go2rtc
# 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 / COPY run.sh /
RUN chmod a+x /run.sh RUN chmod a+x /run.sh

View File

@@ -2,17 +2,7 @@
set +e set +e
# add the feature for update to any version # set cwd for go2rtc (for config file, Hass integration, etc)
if [ -f "/config/go2rtc.version" ]; then
branch=`cat /config/go2rtc.version`
echo "Update to version $branch"
git clone --depth 1 --branch "$branch" https://github.com/AlexxIT/go2rtc \
&& cd go2rtc \
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin \
&& rm -r /go2rtc && rm /config/go2rtc.version
fi
# set cwd for go2rtc (for config file, Hass itegration, etc)
cd /config cd /config
# add the feature to override go2rtc binary from Hass config folder # add the feature to override go2rtc binary from Hass config folder

View File

@@ -17,6 +17,7 @@ func Init() {
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
BasePath string `yaml:"base_path"` BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"` StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
} `yaml:"api"` } `yaml:"api"`
} }
@@ -34,10 +35,10 @@ func Init() {
log = app.GetLogger("api") log = app.GetLogger("api")
initStatic(cfg.Mod.StaticDir) initStatic(cfg.Mod.StaticDir)
initWS() initWS(cfg.Mod.Origin)
HandleFunc("/api/streams", streamsHandler) HandleFunc("api/streams", streamsHandler)
HandleFunc("/api/ws", apiWS) HandleFunc("api/ws", apiWS)
// ensure we can listen without errors // ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen) listener, err := net.Listen("tcp", cfg.Mod.Listen)
@@ -48,16 +49,33 @@ func Init() {
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen") log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
go func() {
s := http.Server{} s := http.Server{}
s.Handler = http.DefaultServeMux
if log.Trace().Enabled() {
s.Handler = middlewareLog(s.Handler)
}
if cfg.Mod.Origin == "*" {
s.Handler = middlewareCORS(s.Handler)
}
go func() {
if err = s.Serve(listener); err != nil { 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) { 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) { func HandleWS(msgType string, handler WSHandler) {
@@ -68,12 +86,32 @@ var basePath string
var log zerolog.Logger var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler) var wsHandlers = make(map[string]WSHandler)
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api] %s %s", r.Method, r.URL)
next.ServeHTTP(w, r)
})
}
func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
next.ServeHTTP(w, r)
})
}
func streamsHandler(w http.ResponseWriter, r *http.Request) { func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
name := r.URL.Query().Get("name")
if name == "" {
name = src
}
switch r.Method { switch r.Method {
case "PUT": case "PUT":
streams.Get(src) streams.New(name, src)
return return
case "DELETE": case "DELETE":
streams.Delete(src) streams.Delete(src)
@@ -86,13 +124,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
} else { } else {
v = streams.All() v = streams.All()
} }
data, err := json.Marshal(v)
if err != nil { e := json.NewEncoder(w)
log.Error().Err(err).Msg("[api.streams] marshal") e.SetIndent("", " ")
} _ = e.Encode(v)
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.streams] write")
}
} }
func apiWS(w http.ResponseWriter, r *http.Request) { func apiWS(w http.ResponseWriter, r *http.Request) {

View File

@@ -14,13 +14,13 @@ func initStatic(staticDir string) {
root = http.FS(www.Static) root = http.FS(www.Static)
} }
base := len(basePath)
fileServer := http.FileServer(root) fileServer := http.FileServer(root)
HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
if basePath != "" { if base > 0 {
r.URL.Path = r.URL.Path[len(basePath):] r.URL.Path = r.URL.Path[base:]
} }
fileServer.ServeHTTP(w, r) fileServer.ServeHTTP(w, r)
}) })
} }

View File

@@ -4,35 +4,19 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"net/http" "net/http"
"net/url"
"strings"
"sync" "sync"
) )
func initWS() { func initWS(origin string) {
wsUp = &websocket.Upgrader{ wsUp = &websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 512000, WriteBufferSize: 512000,
} }
if origin == "*" {
wsUp.CheckOrigin = func(r *http.Request) bool { wsUp.CheckOrigin = func(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true 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
} }
} }
@@ -64,22 +48,14 @@ func (ctx *Context) Close() {
func (ctx *Context) Write(msg interface{}) { func (ctx *Context) Write(msg interface{}) {
ctx.mu.Lock() ctx.mu.Lock()
defer ctx.mu.Unlock()
var err error if data, ok := msg.([]byte); ok {
_ = ctx.Conn.WriteMessage(websocket.BinaryMessage, data)
switch msg := msg.(type) { } else {
case *streamer.Message: _ = ctx.Conn.WriteJSON(msg)
err = ctx.Conn.WriteJSON(msg)
case []byte:
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
default:
return
} }
if err != nil { ctx.mu.Unlock()
//panic(err) // TODO: fix panic
}
} }
func (ctx *Context) Error(err error) { func (ctx *Context) Error(err error) {

View File

@@ -3,12 +3,16 @@ package app
import ( import (
"flag" "flag"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"io" "io"
"os" "os"
"runtime" "runtime"
) )
var Version = "0.1-rc.4"
var UserAgent = "go2rtc/" + Version
func Init() { func Init() {
config := flag.String( config := flag.String(
"config", "config",
@@ -30,10 +34,19 @@ func Init() {
} }
} }
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
path, _ := os.Getwd()
log.Debug().Str("cwd", path).Send()
}
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout var writer io.Writer = os.Stdout
// styles
format := cfg.Mod["format"]
if format != "json" { if format != "json" {
writer = zerolog.ConsoleWriter{ writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000", Out: writer, TimeFormat: "15:04:05.000",
@@ -43,18 +56,12 @@ func Init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
lvl, err := zerolog.ParseLevel(cfg.Mod["level"]) lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel { if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel lvl = zerolog.InfoLevel
} }
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl) return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
modules = cfg.Mod
path, _ := os.Getwd()
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
} }
func LoadConfig(v interface{}) { func LoadConfig(v interface{}) {
@@ -68,15 +75,13 @@ func LoadConfig(v interface{}) {
func GetLogger(module string) zerolog.Logger { func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok { if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s) lvl, err := zerolog.ParseLevel(s)
if err != nil { if err == nil {
log.Warn().Err(err).Msg("[log]")
return log
}
return log.Level(lvl) return log.Level(lvl)
} }
log.Warn().Err(err).Caller().Send()
}
return log return log.Logger
} }
// internal // internal
@@ -84,8 +89,5 @@ func GetLogger(module string) zerolog.Logger {
// data - config content // data - config content
var data []byte var data []byte
// log - main logger
var log zerolog.Logger
// modules log levels // modules log levels
var modules map[string]string var modules map[string]string

View File

@@ -10,8 +10,8 @@ import (
) )
func Init() { func Init() {
api.HandleFunc("/api/stack", stackHandler) api.HandleFunc("api/stack", stackHandler)
api.HandleFunc("/api/exit", exitHandler) api.HandleFunc("api/exit", exitHandler)
streams.HandleFunc("null", nullHandler) streams.HandleFunc("null", nullHandler)
} }

View File

@@ -21,6 +21,7 @@ var stackSkip = [][]byte{
[]byte("created by net/http.(*Server).Serve"), // TODO: why two? []byte("created by net/http.(*Server).Serve"), // TODO: why two?
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"), []byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
// webrtc/api.go // webrtc/api.go
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"), []byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),

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

@@ -4,15 +4,18 @@ import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp" "github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"time" "time"
) )
@@ -22,22 +25,22 @@ func Init() {
return return
} }
rtsp.OnProducer = func(prod streamer.Producer) bool { rtsp.HandleFunc(func(conn *pkg.Conn) bool {
if conn := prod.(*pkg.Conn); conn != nil { waitersMu.Lock()
if waiter := waiters[conn.URL.Path]; waiter != nil { waiter := waiters[conn.URL.Path]
waiter <- prod waitersMu.Unlock()
return true
} if waiter == nil {
}
return false return false
} }
waiter <- conn
return true
})
streams.HandleFunc("exec", Handle) streams.HandleFunc("exec", Handle)
log = app.GetLogger("exec") log = app.GetLogger("exec")
// TODO: add sync.Mutex
waiters = map[string]chan streamer.Producer{}
} }
func Handle(url string) (streamer.Producer, error) { func Handle(url string) (streamer.Producer, error) {
@@ -49,18 +52,27 @@ func Handle(url string) (streamer.Producer, error) {
) )
// remove `exec:` // remove `exec:`
args := QuoteSplit(url[5:]) args := shell.QuoteSplit(url[5:])
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() { if log.Trace().Enabled() {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
}
if log.Debug().Enabled() {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
} }
ch := make(chan streamer.Producer) ch := make(chan streamer.Producer)
waitersMu.Lock()
waiters[path] = ch waiters[path] = ch
defer delete(waiters, path) waitersMu.Unlock()
defer func() {
waitersMu.Lock()
delete(waiters, path)
waitersMu.Unlock()
}()
log.Debug().Str("url", url).Msg("[exec] run") log.Debug().Str("url", url).Msg("[exec] run")
@@ -71,11 +83,19 @@ func Handle(url string) (streamer.Producer, error) {
return nil, err return nil, err
} }
chErr := make(chan error)
go func() {
chErr <- cmd.Wait()
}()
select { select {
case <-time.After(time.Second * 15): case <-time.After(time.Second * 60):
_ = cmd.Process.Kill() _ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout") log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout") return nil, errors.New("timeout")
case err := <-chErr:
return nil, fmt.Errorf("exec: %s", err)
case prod := <-ch: case prod := <-ch:
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run") log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
return prod, nil return prod, nil
@@ -85,40 +105,5 @@ func Handle(url string) (streamer.Producer, error) {
// internal // internal
var log zerolog.Logger var log zerolog.Logger
var waiters map[string]chan streamer.Producer var waiters = map[string]chan streamer.Producer{}
var waitersMu sync.Mutex
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
}

View File

@@ -1,3 +1,17 @@
## FFplay output
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
- `7.11` - master clock, is the time from start of the stream/video
- `A-V` - av_diff, difference between audio and video timestamps
- `fd` - frames dropped
- `aq` - audio queue (0 - no delay)
- `vq` - video queue (0 - no delay)
- `sq` - subtitle queue
- `f` - timestamp error correction rate (Not 100% sure)
`M-V`, `M-A` means video stream only, audio stream only respectively.
## Devices Windows ## Devices Windows
``` ```
@@ -39,3 +53,9 @@
- https://html5test.com/ - https://html5test.com/
- https://trac.ffmpeg.org/wiki/Capture/Webcam - https://trac.ffmpeg.org/wiki/Capture/Webcam
- https://trac.ffmpeg.org/wiki/DirectShow - https://trac.ffmpeg.org/wiki/DirectShow
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
- https://github.com/tuupola/esp_video/blob/master/README.md
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
- https://slhck.info/video/2017/02/24/vbr-settings.html
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)

View File

@@ -15,7 +15,7 @@ import (
func Init() { func Init() {
log = app.GetLogger("exec") log = app.GetLogger("exec")
api.HandleFunc("/api/devices", handle) api.HandleFunc("api/devices", handle)
} }
func GetInput(src string) (string, error) { func GetInput(src string) (string, error) {

View File

@@ -4,9 +4,11 @@ import (
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec" "github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device" "github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"net/url" "net/url"
"strconv"
"strings" "strings"
) )
@@ -23,39 +25,48 @@ func Init() {
// inputs // inputs
"file": "-re -stream_loop -1 -i {input}", "file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}", "http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}", "rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
// output // output
"output": "-rtsp_transport tcp -f rtsp {output}", "output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
// `-g 30` - group of picture, GOP, keyframe interval // `-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` // `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency // `-tune zerolatency` - for minimal latency
// `-profile main -level 4.1` - most used streaming profile // `-profile main -level 4.1` - most used streaming profile
// `-pix_fmt yuv420p` - if input pix format 4:2:2 // `-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": "-c:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1 -pix_fmt:v yuv420p",
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency", "h264/ultra": "-c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency",
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency", "h264/high": "-c:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency",
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency", "h265": "-c:v libx265 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 5.1 -pix_fmt:v yuv420p",
"opus": "-codec:a libopus -ar 48000 -ac 2", "mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1", "opus": "-c:a libopus -ar:a 48000 -ac:a 2",
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1", "pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/48000": "-codec:a pcm_mulaw -ar 48000 -ac 1", "pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcma": "-codec:a pcm_alaw -ar 8000 -ac 1", "pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma/16000": "-codec:a pcm_alaw -ar 16000 -ac 1", "pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/48000": "-codec:a pcm_alaw -ar 48000 -ac 1", "pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"aac/16000": "-codec:a aac -ar 16000 -ac 1", "pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac": "-c:a aac", // keep sample rate and channels
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
} }
app.LoadConfig(&cfg) app.LoadConfig(&cfg)
tpl := cfg.Mod tpl := cfg.Mod
cmd := "exec:" + tpl["bin"] + " -hide_banner "
if app.GetLogger("exec").GetLevel() >= 0 {
cmd += "-v error "
}
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) { streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
s = s[7:] // remove `ffmpeg:` s = s[7:] // remove `ffmpeg:`
var query url.Values var query url.Values
var queryVideo, queryAudio bool var queryVideo, queryAudio bool
if i := strings.IndexByte(s, '#'); i > 0 { if i := strings.IndexByte(s, '#'); i > 0 {
query = parseQuery(s[i+1:]) query = parseQuery(s[i+1:])
queryVideo = query["video"] != nil queryVideo = query["video"] != nil
@@ -68,9 +79,9 @@ func Init() {
} }
var input string var input string
if i := strings.IndexByte(s, ':'); i > 0 { if i := strings.Index(s, "://"); i > 0 {
switch s[:i] { switch s[:i] {
case "http", "https": case "http", "https", "rtmp":
input = strings.Replace(tpl["http"], "{input}", s, 1) input = strings.Replace(tpl["http"], "{input}", s, 1)
case "rtsp", "rtsps": case "rtsp", "rtsps":
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp // https://ffmpeg.org/ffmpeg-protocols.html#rtsp
@@ -85,11 +96,19 @@ func Init() {
} }
input += strings.Replace(tpl["rtsp"], "{input}", s, 1) input += strings.Replace(tpl["rtsp"], "{input}", s, 1)
default:
input = "-i " + s
} }
} else if streams.Get(s) != nil {
s = "rtsp://localhost:" + rtsp.Port + "/" + s
switch {
case queryVideo && !queryAudio:
s += "?video"
case queryAudio && !queryVideo:
s += "?audio"
} }
input = strings.Replace(tpl["rtsp"], "{input}", s, 1)
if input == "" { } else if strings.HasPrefix(s, "device?") {
if strings.HasPrefix(s, "device?") {
var err error var err error
input, err = device.GetInput(s) input, err = device.GetInput(s)
if err != nil { if err != nil {
@@ -98,39 +117,76 @@ func Init() {
} else { } else {
input = strings.Replace(tpl["file"], "{input}", s, 1) input = strings.Replace(tpl["file"], "{input}", s, 1)
} }
if _, ok := query["async"]; ok {
input = "-use_wallclock_as_timestamps 1 -async 1 " + input
} }
s = "exec:" + tpl["bin"] + " -hide_banner " + input s = cmd + input
if query != nil { if query != nil {
for _, raw := range query["raw"] { for _, raw := range query["raw"] {
s += " " + raw s += " " + raw
} }
// TODO: multiple codecs via -map for _, rotate := range query["rotate"] {
// s += fmt.Sprintf(" -map 0:v:0 -c:v:%d copy", i) switch rotate {
case "90":
s += " -vf transpose=1" // 90 degrees clockwise
case "180":
s += " -vf transpose=1,transpose=1"
case "-90", "270":
s += " -vf transpose=2" // 90 degrees counterclockwise
}
break
}
switch len(query["video"]) {
case 0:
s += " -vn"
case 1:
if len(query["audio"]) > 1 {
s += " -map 0:v:0?"
}
for _, video := range query["video"] { for _, video := range query["video"] {
if video == "copy" { if video == "copy" {
s += " -codec:v copy" s += " -c:v copy"
} else { } else {
s += " " + tpl[video] s += " " + tpl[video]
} }
} }
default:
for i, video := range query["video"] {
if video == "copy" {
s += " -map 0:v:0? -c:v:" + strconv.Itoa(i) + " copy"
} else {
s += " -map 0:v:0? " + strings.ReplaceAll(tpl[video], ":v ", ":v:"+strconv.Itoa(i)+" ")
}
}
}
switch len(query["audio"]) {
case 0:
s += " -an"
case 1:
if len(query["video"]) > 1 {
s += " -map 0:a:0?"
}
for _, audio := range query["audio"] { for _, audio := range query["audio"] {
if audio == "copy" { if audio == "copy" {
s += " -codec:v copy" s += " -c:a copy"
} else { } else {
s += " " + tpl[audio] s += " " + tpl[audio]
} }
} }
default:
switch { for i, audio := range query["audio"] {
case queryVideo && !queryAudio: if audio == "copy" {
s += " -an" s += " -map 0:a:0? -c:a:" + strconv.Itoa(i) + " copy"
case queryAudio && !queryVideo: } else {
s += " -vn" s += " -map 0:a:0? " + strings.ReplaceAll(tpl[audio], ":a ", ":a:"+strconv.Itoa(i)+" ")
}
}
} }
} else { } else {
s += " -c copy" s += " -c copy"

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

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

View File

@@ -1,20 +1,14 @@
package hass package hass
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"net/http"
"net/url"
"os" "os"
"path" "path"
"strings"
) )
func Init() { func Init() {
@@ -28,11 +22,7 @@ func Init() {
log = app.GetLogger("hass") log = app.GetLogger("hass")
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/ initAPI()
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
api.HandleFunc("/stream", handler)
// support load cameras from Hass config file // support load cameras from Hass config file
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries") filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
@@ -78,73 +68,13 @@ func Init() {
continue continue
} }
log.Info().Str("url", "hass:" + entrie.Title).Msg("[hass] load stream") log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
//streams.Get("hass:" + entrie.Title) //streams.Get("hass:" + entrie.Title)
} }
} }
var log zerolog.Logger var log zerolog.Logger
func handler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("[api.hass] parse form")
return
}
src := r.FormValue("url")
src, err := url.QueryUnescape(src)
if err != nil {
log.Error().Err(err).Msg("[api.hass] query unescape")
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
if err != nil {
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
return
}
// check if stream links to our rtsp server
if strings.HasPrefix(src, "rtsp://") {
i := strings.IndexByte(src[7:], '/')
if i > 0 && streams.Has(src[8+i:]) {
src = src[8+i:]
}
}
stream := streams.Get(src)
if stream == nil {
log.Error().Str("url", src).Msg("[api.hass] unsupported source")
return
}
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
return
}
resp := struct {
Answer string `json:"sdp64"`
}{
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
}
data, err := json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("[api.hass] marshal JSON")
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.hass] write")
return
}
}
type entries struct { type entries struct {
Data struct { Data struct {
Entries []struct { Entries []struct {

View File

@@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/cmd/app/store" "github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns" "github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@@ -54,97 +54,80 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
items = append(items, device) items = append(items, device)
} }
data, err := json.Marshal(items) _ = json.NewEncoder(w).Encode(items)
if err != nil {
log.Error().Err(err).Msg("[api.homekit]")
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.homekit]")
}
case "POST": case "POST":
// TODO: post params... // TODO: post params...
id := r.URL.Query().Get("id") id := r.URL.Query().Get("id")
pin := r.URL.Query().Get("pin") 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") name := r.URL.Query().Get("name")
dict := store.GetDict("streams") if err := hkPair(id, pin, name); err != nil {
dict[name] = client.URL() log.Error().Err(err).Caller().Send()
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())) _, err = w.Write([]byte(err.Error()))
} }
streams.New(name, client.URL())
case "DELETE": case "DELETE":
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
if err := hkDelete(src); err != nil {
log.Error().Err(err).Caller().Send()
_, err = w.Write([]byte(err.Error()))
}
}
}
func hkPair(deviceID, pin, name string) (err error) {
var conn *hap.Conn
if conn, err = hap.Pair(deviceID, pin); err != nil {
return
}
streams.New(name, conn.URL())
dict := store.GetDict("streams") dict := store.GetDict("streams")
for name, rawURL := range dict { dict[name] = conn.URL()
if name != src {
return store.Set("streams", dict)
}
func hkDelete(name string) (err error) {
dict := store.GetDict("streams")
for key, rawURL := range dict {
if key != name {
continue continue
} }
client, err := homekit.NewClient(rawURL.(string)) var conn *hap.Conn
if err != nil {
// log error if conn, err = hap.NewConn(rawURL.(string)); err != nil {
log.Error().Err(err).Msg("[api.homekit] new client")
// response error
_, err = w.Write([]byte(err.Error()))
return return
} }
if err = client.Dial(); err != nil { if err = conn.Dial(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] client dial")
// response error
_, err = w.Write([]byte(err.Error()))
return return
} }
go client.Handle() go func() {
if err = conn.Handle(); err != nil {
log.Warn().Err(err).Caller().Send()
}
}()
if err = client.ListPairings(); err != nil { if err = conn.ListPairings(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
return return
} }
if err = client.DeletePairing(client.ClientID); err != nil { if err = conn.DeletePairing(conn.ClientID); err != nil {
// log error log.Error().Err(err).Caller().Send()
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
} }
delete(dict, name) delete(dict, name)
if err = store.Set("streams", dict); err != nil { return store.Set("streams", dict)
// log error
log.Error().Err(err).Msg("[api.homekit] store set")
// response error
_, err = w.Write([]byte(err.Error()))
} }
return return nil
}
}
} }
type Device struct { type Device struct {

View File

@@ -3,6 +3,7 @@ package homekit
import ( import (
"github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
@@ -14,26 +15,18 @@ func Init() {
streams.HandleFunc("homekit", streamHandler) streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("/api/homekit", apiHandler) api.HandleFunc("api/homekit", apiHandler)
} }
var log zerolog.Logger var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) { func streamHandler(url string) (streamer.Producer, error) {
client, err := homekit.NewClient(url) conn, err := homekit.NewClient(url, srtp.Server)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = client.Dial(); err != nil { if err = conn.Dial();err!=nil{
return nil, err return nil, err
} }
return conn, nil
// 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
} }

View File

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

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
})
}

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

@@ -0,0 +1,89 @@
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/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
}
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return
}
exit := make(chan []byte)
cons := &mjpeg.Consumer{}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
exit <- msg
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
data := <-exit
stream.RemoveConsumer(cons)
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handlerStream(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
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")
}

View File

@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
) )
func Init() { func Init() {
@@ -16,8 +17,8 @@ func Init() {
api.HandleWS(MsgTypeMSE, handlerWS) api.HandleWS(MsgTypeMSE, handlerWS)
api.HandleFunc("/api/frame.mp4", handlerKeyframe) api.HandleFunc("api/frame.mp4", handlerKeyframe)
api.HandleFunc("/api/stream.mp4", handlerMP4) api.HandleFunc("api/stream.mp4", handlerMP4)
} }
var log zerolog.Logger var log zerolog.Logger
@@ -28,68 +29,66 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
} }
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
stream := streams.Get(src) stream := streams.GetOrNew(src)
if stream == nil { if stream == nil {
return return
} }
exit := make(chan []byte) exit := make(chan []byte)
cons := &mp4.Consumer{} cons := &mp4.Keyframe{}
cons.Listen(func(msg interface{}) { cons.Listen(func(msg interface{}) {
switch msg := msg.(type) { if data, ok := msg.([]byte); ok && exit != nil {
case []byte: exit <- data
exit <- msg exit = nil
} }
}) })
if err := stream.AddConsumer(cons); err != nil { if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.keyframe] add consumer") log.Error().Err(err).Caller().Send()
return return
} }
w.Header().Set("Content-Type", cons.MimeType()) data := <-exit
data := cons.Init()
data = append(data, <-exit...)
stream.RemoveConsumer(cons) stream.RemoveConsumer(cons)
// Apple Safari won't show frame without length // Apple Safari won't show frame without length
w.Header().Set("Content-Length", strconv.Itoa(len(data))) w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("Content-Type", cons.MimeType)
if _, err := w.Write(data); err != nil { if _, err := w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.keyframe] add consumer") log.Error().Err(err).Caller().Send()
} }
} }
func handlerMP4(w http.ResponseWriter, r *http.Request) { func handlerMP4(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api.mp4] %s %+v", r.Method, r.Header)
if isChromeFirst(w, r) || isSafari(w, r) { if isChromeFirst(w, r) || isSafari(w, r) {
return return
} }
log.Trace().Msgf("[api.mp4] %+v", r)
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
stream := streams.Get(src) stream := streams.GetOrNew(src)
if stream == nil { if stream == nil {
return return
} }
exit := make(chan struct{}) exit := make(chan error)
cons := &mp4.Consumer{} cons := &mp4.Consumer{}
cons.Listen(func(msg interface{}) { cons.Listen(func(msg interface{}) {
switch msg := msg.(type) { if data, ok := msg.([]byte); ok {
case []byte: if _, err := w.Write(data); err != nil && exit != nil {
if _, err := w.Write(msg); err != nil { exit <- err
exit <- struct{}{} exit = nil
} }
} }
}) })
if err := stream.AddConsumer(cons); err != nil { if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mp4] add consumer") log.Error().Err(err).Caller().Send()
return return
} }
@@ -97,15 +96,38 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", cons.MimeType()) w.Header().Set("Content-Type", cons.MimeType())
data := cons.Init() data, err := cons.Init()
if _, err := w.Write(data); err != nil { if err != nil {
log.Error().Err(err).Msg("[api.mp4] write") log.Error().Err(err).Caller().Send()
return return
} }
<-exit if _, err = w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
return
}
log.Trace().Msg("[api.mp4] close") cons.Start()
var duration *time.Timer
if s := r.URL.Query().Get("duration"); s != "" {
if i, _ := strconv.Atoi(s); i > 0 {
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
if exit != nil {
exit <- nil
exit = nil
}
})
}
}
err = <-exit
log.Trace().Err(err).Caller().Send()
if duration != nil {
duration.Stop()
}
} }
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool { func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {

View File

@@ -5,13 +5,16 @@ import (
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
) )
const MsgTypeMSE = "mse" // fMP4 const MsgTypeMSE = "mse" // fMP4
const packetSize = 8192
func handlerWS(ctx *api.Context, msg *streamer.Message) { func handlerWS(ctx *api.Context, msg *streamer.Message) {
src := ctx.Request.URL.Query().Get("src") src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src) stream := streams.GetOrNew(src)
if stream == nil { if stream == nil {
return return
} }
@@ -20,15 +23,22 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
cons.UserAgent = ctx.Request.UserAgent() cons.UserAgent = ctx.Request.UserAgent()
cons.RemoteAddr = ctx.Request.RemoteAddr cons.RemoteAddr = ctx.Request.RemoteAddr
if codecs, ok := msg.Value.(string); ok {
cons.Medias = parseMedias(codecs)
}
cons.Listen(func(msg interface{}) { cons.Listen(func(msg interface{}) {
switch msg.(type) { if data, ok := msg.([]byte); ok {
case *streamer.Message, []byte: for len(data) > packetSize {
ctx.Write(msg) ctx.Write(data[:packetSize])
data = data[packetSize:]
}
ctx.Write(data)
} }
}) })
if err := stream.AddConsumer(cons); err != nil { if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.mse] add consumer") log.Warn().Err(err).Caller().Send()
ctx.Error(err) ctx.Error(err)
return return
} }
@@ -37,9 +47,55 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
stream.RemoveConsumer(cons) stream.RemoveConsumer(cons)
}) })
ctx.Write(&streamer.Message{ ctx.Write(&streamer.Message{Type: MsgTypeMSE, Value: cons.MimeType()})
Type: MsgTypeMSE, Value: cons.MimeType(),
})
ctx.Write(cons.Init()) data, err := cons.Init()
if err != nil {
log.Warn().Err(err).Caller().Send()
ctx.Error(err)
return
}
ctx.Write(data)
cons.Start()
}
func parseMedias(codecs string) (medias []*streamer.Media) {
var videos []*streamer.Codec
var audios []*streamer.Codec
for _, name := range strings.Split(codecs, ",") {
switch name {
case "avc1.640029":
codec := &streamer.Codec{Name: streamer.CodecH264}
videos = append(videos, codec)
case "hvc1.1.6.L153.B0":
codec := &streamer.Codec{Name: streamer.CodecH265}
videos = append(videos, codec)
case "mp4a.40.2":
codec := &streamer.Codec{Name: streamer.CodecAAC}
audios = append(audios, codec)
}
}
if videos != nil {
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil {
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
} }

View File

@@ -8,6 +8,8 @@ import (
func Init() { func Init() {
streams.HandleFunc("rtmp", handle) streams.HandleFunc("rtmp", handle)
streams.HandleFunc("http", handle)
streams.HandleFunc("https", handle)
} }
func handle(url string) (streamer.Producer, error) { func handle(url string) (streamer.Producer, error) {

View File

@@ -8,6 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"net" "net"
"strings"
) )
func Init() { func Init() {
@@ -31,27 +32,61 @@ func Init() {
// RTSP server support // RTSP server support
address := conf.Mod.Listen address := conf.Mod.Listen
if address != "" { if address == "" {
return
}
ln, err := net.Listen("tcp", address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
_, Port, _ = net.SplitHostPort(address) _, Port, _ = net.SplitHostPort(address)
go worker(address) log.Info().Str("addr", address).Msg("[rtsp] listen")
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
} }
go tcpHandler(conn)
}
}()
}
type Handler func(conn *rtsp.Conn) bool
func HandleFunc(handler Handler) {
handlers = append(handlers, handler)
} }
var Port string var Port string
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
// internal // internal
var log zerolog.Logger var log zerolog.Logger
var handlers []Handler
func rtspHandler(url string) (streamer.Producer, error) { 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) conn, err := rtsp.NewClient(url)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conn.UserAgent = app.UserAgent
if log.Trace().Enabled() { if log.Trace().Enabled() {
conn.Listen(func(msg interface{}) { conn.Listen(func(msg interface{}) {
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -67,13 +102,17 @@ func rtspHandler(url string) (streamer.Producer, error) {
return nil, err return nil, err
} }
conn.Backchannel = true conn.Backchannel = backchannel
if err = conn.Describe(); err != nil { if err = conn.Describe(); err != nil {
if !backchannel {
return nil, err
}
// second try without backchannel, we need to reconnect // second try without backchannel, we need to reconnect
conn.Backchannel = false
if err = conn.Dial(); err != nil { if err = conn.Dial(); err != nil {
return nil, err return nil, err
} }
conn.Backchannel = false
if err = conn.Describe(); err != nil { if err = conn.Describe(); err != nil {
return nil, err return nil, err
} }
@@ -82,24 +121,13 @@ func rtspHandler(url string) (streamer.Producer, error) {
return conn, nil return conn, nil
} }
func worker(address string) { func tcpHandler(c net.Conn) {
srv, err := tcp.NewServer(address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
log.Info().Str("addr", address).Msg("[rtsp] listen")
srv.Listen(func(msg interface{}) {
switch msg.(type) {
case net.Conn:
var name string var name string
var onDisconnect func() var closer func()
trace := log.Trace().Enabled() trace := log.Trace().Enabled()
conn := rtsp.NewServer(msg.(net.Conn)) conn := rtsp.NewServer(c)
conn.Listen(func(msg interface{}) { conn.Listen(func(msg interface{}) {
if trace { if trace {
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -112,46 +140,50 @@ func worker(address string) {
switch msg { switch msg {
case rtsp.MethodDescribe: case rtsp.MethodDescribe:
if len(conn.URL.Path) == 0 {
log.Warn().Msg("[rtsp] server empty URL on DESCRIBE")
return
}
name = conn.URL.Path[1:] name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new consumer") stream := streams.Get(name)
stream := streams.Get(name) // TODO: rewrite
if stream == nil { if stream == nil {
return return
} }
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
initMedias(conn) initMedias(conn)
if err = stream.AddConsumer(conn); err != nil { if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]") log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return return
} }
onDisconnect = func() { closer = func() {
stream.RemoveConsumer(conn) stream.RemoveConsumer(conn)
} }
case rtsp.MethodAnnounce: case rtsp.MethodAnnounce:
if OnProducer != nil { if len(conn.URL.Path) == 0 {
if OnProducer(conn) { log.Warn().Msg("[rtsp] server empty URL on ANNOUNCE")
return return
} }
}
name = conn.URL.Path[1:] name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new producer") stream := streams.Get(name)
if stream == nil {
str := streams.Get(conn.URL.Path[1:])
if str == nil {
return return
} }
str.AddProducer(conn) log.Debug().Str("stream", name).Msg("[rtsp] new producer")
onDisconnect = func() { stream.AddProducer(conn)
str.RemoveProducer(conn)
closer = func() {
stream.RemoveProducer(conn)
} }
case streamer.StatePlaying: case streamer.StatePlaying:
@@ -159,24 +191,29 @@ func worker(address string) {
} }
}) })
if err = conn.Accept(); err != nil { if err := conn.Accept(); err != nil {
log.Warn().Err(err).Msg("[rtsp] accept") log.Warn().Err(err).Caller().Send()
_ = conn.Close()
return return
} }
if err = conn.Handle(); err != nil { for _, handler := range handlers {
//log.Warn().Err(err).Msg("[rtsp] handle server") if handler(conn) {
return
}
} }
if onDisconnect != nil { if closer != nil {
onDisconnect() if err := conn.Handle(); err != nil {
log.Debug().Err(err).Caller().Send()
} }
closer()
log.Debug().Str("stream", name).Msg("[rtsp] disconnect") log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
} }
})
srv.Serve() _ = conn.Close()
} }
func initMedias(conn *rtsp.Conn) { func initMedias(conn *rtsp.Conn) {
@@ -184,16 +221,27 @@ func initMedias(conn *rtsp.Conn) {
for key, value := range conn.URL.Query() { for key, value := range conn.URL.Query() {
switch key { switch key {
case streamer.KindVideo, streamer.KindAudio: case streamer.KindVideo, streamer.KindAudio:
for _, value := range value { for _, name := range value {
name = strings.ToUpper(name)
// check aliases
switch name {
case "COPY":
name = "" // pass empty codecs list
case "MJPEG":
name = streamer.CodecJPEG
case "AAC":
name = streamer.CodecAAC
}
media := &streamer.Media{ media := &streamer.Media{
Kind: key, Direction: streamer.DirectionRecvonly, Kind: key, Direction: streamer.DirectionRecvonly,
} }
switch value { // empty codecs match all codecs
case "", "copy": // pass empty codecs list if name != "" {
default: // empty clock rate and channels match any values
codec := streamer.NewCodec(value) media.Codecs = []*streamer.Codec{{Name: name}}
media.Codecs = append(media.Codecs, codec)
} }
conn.Medias = append(conn.Medias, media) conn.Medias = append(conn.Medias, media)

View File

@@ -3,7 +3,6 @@ package srtp
import ( import (
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/srtp" "github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/rs/zerolog"
"net" "net"
) )
@@ -24,36 +23,23 @@ func Init() {
return return
} }
log = app.GetLogger("srtp") log := app.GetLogger("srtp")
// create SRTP server (endpoint) for receiving video from HomeKit camera // create SRTP server (endpoint) for receiving video from HomeKit camera
conn, err := net.ListenPacket("udp", cfg.Mod.Listen) conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("[srtp] listen") log.Warn().Err(err).Caller().Send()
} }
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen") log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
_, Port, _ = net.SplitHostPort(cfg.Mod.Listen)
// run server // run server
go func() { go func() {
server = &srtp.Server{} Server = &srtp.Server{}
if err = server.Serve(conn); err != nil { if err = Server.Serve(conn); err != nil {
log.Warn().Err(err).Msg("[srtp] serve") log.Warn().Err(err).Caller().Send()
} }
}() }()
} }
var log zerolog.Logger var Server *srtp.Server
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

@@ -4,30 +4,36 @@ import (
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings" "strings"
"sync"
) )
type Handler func(url string) (streamer.Producer, error) type Handler func(url string) (streamer.Producer, error)
var handlers map[string]Handler var handlers = map[string]Handler{}
var handlersMu sync.Mutex
func HandleFunc(scheme string, handler Handler) { func HandleFunc(scheme string, handler Handler) {
if handlers == nil { handlersMu.Lock()
handlers = make(map[string]Handler)
}
handlers[scheme] = handler handlers[scheme] = handler
handlersMu.Unlock()
}
func getHandler(url string) Handler {
i := strings.IndexByte(url, ':')
if i <= 0 { // TODO: i < 4 ?
return nil
}
handlersMu.Lock()
defer handlersMu.Unlock()
return handlers[url[:i]]
} }
func HasProducer(url string) bool { func HasProducer(url string) bool {
i := strings.IndexByte(url, ':') return getHandler(url) != nil
if i <= 0 { // TODO: i < 4 ?
return false
}
return handlers[url[:i]] != nil
} }
func GetProducer(url string) (streamer.Producer, error) { func GetProducer(url string) (streamer.Producer, error) {
i := strings.IndexByte(url, ':') handler := getHandler(url)
handler := handlers[url[:i]]
if handler == nil { if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url) return nil, fmt.Errorf("unsupported scheme: %s", url)
} }

View File

@@ -2,7 +2,9 @@ package streams
import ( import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync" "sync"
"time"
) )
type state byte type state byte
@@ -12,30 +14,41 @@ const (
stateMedias stateMedias
stateTracks stateTracks
stateStart stateStart
stateExternal
) )
type Producer struct { type Producer struct {
streamer.Element streamer.Element
url string url string
template string
element streamer.Producer element streamer.Producer
tracks []*streamer.Track tracks []*streamer.Track
state state state state
mx sync.Mutex mu sync.Mutex
restart *time.Timer
}
func (p *Producer) SetSource(s string) {
if p.template == "" {
p.template = p.url
}
p.url = strings.Replace(p.template, "{input}", s, 1)
} }
func (p *Producer) GetMedias() []*streamer.Media { func (p *Producer) GetMedias() []*streamer.Media {
p.mx.Lock() p.mu.Lock()
defer p.mx.Unlock() defer p.mu.Unlock()
if p.state == stateNone { if p.state == stateNone {
log.Debug().Str("url", p.url).Msg("[streams] probe producer") log.Debug().Msgf("[streams] probe producer url=%s", p.url)
var err error var err error
p.element, err = GetProducer(p.url) p.element, err = GetProducer(p.url)
if err != nil || p.element == nil { if err != nil || p.element == nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer") log.Error().Err(err).Caller().Send()
return nil return nil
} }
@@ -46,55 +59,127 @@ func (p *Producer) GetMedias() []*streamer.Media {
} }
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
p.mx.Lock() p.mu.Lock()
defer p.mx.Unlock() defer p.mu.Unlock()
if p.state == stateMedias { if p.state == stateNone {
p.state = stateTracks return nil
} }
track := p.element.GetTrack(media, codec) for _, track := range p.tracks {
if track.Codec == codec {
for _, t := range p.tracks {
if track == t {
return track return track
} }
} }
track := p.element.GetTrack(media, codec)
if track == nil {
return nil
}
p.tracks = append(p.tracks, track) p.tracks = append(p.tracks, track)
if p.state == stateMedias {
p.state = stateTracks
}
return track return track
} }
// internals // internals
func (p *Producer) start() { func (p *Producer) start() {
p.mx.Lock() p.mu.Lock()
defer p.mx.Unlock() defer p.mu.Unlock()
if p.state != stateTracks { if p.state != stateTracks {
return return
} }
log.Debug().Str("url", p.url).Msg("[streams] start producer") log.Debug().Msgf("[streams] start producer url=%s", p.url)
p.state = stateStart p.state = stateStart
go p.element.Start() go func() {
// safe read element while mu locked
if err := p.element.Start(); err != nil {
log.Warn().Err(err).Caller().Send()
}
p.reconnect()
}()
}
func (p *Producer) reconnect() {
p.mu.Lock()
defer p.mu.Unlock()
if p.state != stateStart {
log.Trace().Msgf("[streams] stop reconnect url=%s", p.url)
return
}
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
var err error
p.element, err = GetProducer(p.url)
if err != nil || p.element == nil {
log.Debug().Err(err).Caller().Send()
// TODO: dynamic timeout
p.restart = time.AfterFunc(30*time.Second, p.reconnect)
return
}
medias := p.element.GetMedias()
// convert all old producer tracks to new tracks
for i, oldTrack := range p.tracks {
// match new element medias with old track codec
for _, media := range medias {
codec := media.MatchCodec(oldTrack.Codec)
if codec == nil {
continue
}
// move sink from old track to new track
newTrack := p.element.GetTrack(media, codec)
newTrack.GetSink(oldTrack)
p.tracks[i] = newTrack
break
}
}
go func() {
if err = p.element.Start(); err != nil {
log.Debug().Err(err).Caller().Send()
}
p.reconnect()
}()
} }
func (p *Producer) stop() { func (p *Producer) stop() {
p.mx.Lock() p.mu.Lock()
defer p.mu.Unlock()
log.Debug().Str("url", p.url).Msg("[streams] stop producer") switch p.state {
case stateExternal:
log.Debug().Msgf("[streams] can't stop external producer")
return
case stateNone:
log.Debug().Msgf("[streams] can't stop none producer")
return
}
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
if p.element != nil { if p.element != nil {
_ = p.element.Stop() _ = p.element.Stop()
p.element = nil p.element = nil
} else {
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
} }
p.tracks = nil if p.restart != nil {
p.state = stateNone p.restart.Stop()
p.restart = nil
}
p.mx.Unlock() p.state = stateNone
p.tracks = nil
} }

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"sync"
) )
type Consumer struct { type Consumer struct {
@@ -14,46 +15,57 @@ type Consumer struct {
type Stream struct { type Stream struct {
producers []*Producer producers []*Producer
consumers []*Consumer consumers []*Consumer
mu sync.Mutex
} }
func NewStream(source interface{}) *Stream { func NewStream(source interface{}) *Stream {
s := new(Stream)
switch source := source.(type) { switch source := source.(type) {
case string: case string:
s := new(Stream)
prod := &Producer{url: source} prod := &Producer{url: source}
s.producers = append(s.producers, prod) s.producers = append(s.producers, prod)
return s
case []interface{}: case []interface{}:
s := new(Stream)
for _, source := range source { for _, source := range source {
prod := &Producer{url: source.(string)} prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod) s.producers = append(s.producers, prod)
} }
return s
case *Stream:
return source
case map[string]interface{}: case map[string]interface{}:
return NewStream(source["url"]) return NewStream(source["url"])
case nil: case nil:
return new(Stream)
default: default:
panic("wrong source type") 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) { func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
ic := len(s.consumers) ic := len(s.consumers)
consumer := &Consumer{element: cons} consumer := &Consumer{element: cons}
var producers []*Producer // matched producers for consumer
// Step 1. Get consumer medias // Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() { for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia). log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer:%d:%d candidate", ic, icc) Msgf("[streams] consumer=%d candidate=%d", ic, icc)
producers: producers:
for ip, prod := range s.producers { for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet) // Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() { for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia). log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer:%d:%d candidate", ip, ipc) Msgf("[streams] producer=%d candidate=%d", ip, ipc)
// Step 3. Match consumer/producer codecs list // Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia) prodCodec := prodMedia.MatchMedia(consMedia)
@@ -72,20 +84,24 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
consTrack := consumer.element.AddTrack(consMedia, prodTrack) consTrack := consumer.element.AddTrack(consMedia, prodTrack)
consumer.tracks = append(consumer.tracks, consTrack) consumer.tracks = append(consumer.tracks, consTrack)
producers = append(producers, prod)
break producers break producers
} }
} }
} }
} }
// can't match tracks for consumer if len(producers) == 0 {
if len(consumer.tracks) == 0 { s.stopProducers()
return errors.New("couldn't find the matching tracks") return errors.New("couldn't find the matching tracks")
} }
s.mu.Lock()
s.consumers = append(s.consumers, consumer) s.consumers = append(s.consumers, consumer)
s.mu.Unlock()
for _, prod := range s.producers { // there may be duplicates, but that's not a problem
for _, prod := range producers {
prod.start() prod.start()
} }
@@ -93,6 +109,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
} }
func (s *Stream) RemoveConsumer(cons streamer.Consumer) { func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
s.mu.Lock()
for i, consumer := range s.consumers { for i, consumer := range s.consumers {
if consumer.element == cons { if consumer.element == cons {
// remove consumer pads from all producers // remove consumer pads from all producers
@@ -104,50 +121,60 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
break break
} }
} }
s.mu.Unlock()
for _, producer := range s.producers { s.stopProducers()
var sink bool
for _, track := range producer.tracks {
if len(track.Sink) > 0 {
sink = true
}
}
if !sink {
producer.stop()
}
}
} }
func (s *Stream) AddProducer(prod streamer.Producer) { func (s *Stream) AddProducer(prod streamer.Producer) {
producer := &Producer{element: prod, state: stateTracks} producer := &Producer{element: prod, state: stateExternal}
s.mu.Lock()
s.producers = append(s.producers, producer) s.producers = append(s.producers, producer)
s.mu.Unlock()
} }
func (s *Stream) RemoveProducer(prod streamer.Producer) { func (s *Stream) RemoveProducer(prod streamer.Producer) {
s.mu.Lock()
for i, producer := range s.producers { for i, producer := range s.producers {
if producer.element == prod { if producer.element == prod {
s.removeProducer(i) s.removeProducer(i)
break break
} }
} }
s.mu.Unlock()
} }
func (s *Stream) Active() bool { func (s *Stream) stopProducers() {
if len(s.consumers) > 0 { s.mu.Lock()
return true producers:
} for _, producer := range s.producers {
for _, track := range producer.tracks {
for _, prod := range s.producers { if track.HasSink() {
if prod.element != nil { continue producers
return true
} }
} }
producer.stop()
return false }
s.mu.Unlock()
} }
//func (s *Stream) Active() bool {
// if len(s.consumers) > 0 {
// return true
// }
//
// for _, prod := range s.producers {
// if prod.element != nil {
// return true
// }
// }
//
// return false
//}
func (s *Stream) MarshalJSON() ([]byte, error) { func (s *Stream) MarshalJSON() ([]byte, error) {
var v []interface{} var v []interface{}
s.mu.Lock()
for _, prod := range s.producers { for _, prod := range s.producers {
if prod.element != nil { if prod.element != nil {
v = append(v, prod.element) v = append(v, prod.element)
@@ -157,6 +184,7 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
// cons.element always not nil // cons.element always not nil
v = append(v, cons.element) v = append(v, cons.element)
} }
s.mu.Unlock()
if len(v) == 0 { if len(v) == 0 {
v = nil v = nil
} }

View File

@@ -24,10 +24,19 @@ func Init() {
} }
} }
func Get(src string) *Stream { func Get(name string) *Stream {
return streams[name]
}
func New(name string, source interface{}) *Stream {
stream := NewStream(source)
streams[name] = stream
return stream
}
func GetOrNew(src string) *Stream {
if stream, ok := streams[src]; ok { if stream, ok := streams[src]; ok {
return stream return stream
} }
if !HasProducer(src) { if !HasProducer(src) {
@@ -35,17 +44,8 @@ func Get(src string) *Stream {
} }
log.Info().Str("url", src).Msg("[streams] create new stream") log.Info().Str("url", src).Msg("[streams] create new stream")
stream := NewStream(src)
streams[src] = stream
return stream
}
func Has(src string) bool { return New(src, src)
return streams[src] != nil
}
func New(name string, source interface{}) {
streams[name] = NewStream(source)
} }
func Delete(name string) { func Delete(name string) {

View File

@@ -5,7 +5,6 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
"strings"
) )
var candidates []string var candidates []string
@@ -14,7 +13,27 @@ func AddCandidate(address string) {
candidates = append(candidates, address) candidates = append(candidates, address)
} }
func addCanditates(answer string) (string, error) { func asyncCandidates(ctx *api.Context) {
for _, address := range candidates {
address, err := webrtc.LookupIP(address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
ctx.Write(&streamer.Message{Type: webrtc.MsgTypeCandidate, Value: cand})
}
}
func syncCanditates(answer string) (string, error) {
if len(candidates) == 0 { if len(candidates) == 0 {
return answer, nil return answer, nil
} }
@@ -32,16 +51,12 @@ func addCanditates(answer string) (string, error) {
} }
for _, address := range candidates { for _, address := range candidates {
if strings.HasPrefix(address, "stun:") { var err error
ip, err := webrtc.GetPublicIP() address, err = webrtc.LookupIP(address)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("[webrtc] public IP") log.Warn().Err(err).Msg("[webrtc] candidate")
continue continue
} }
address = ip.String() + address[4:]
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
}
cand, err := webrtc.NewCandidate(address) cand, err := webrtc.NewCandidate(address)
if err != nil { if err != nil {

View File

@@ -8,7 +8,9 @@ import (
"github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3" pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"io/ioutil"
"net" "net"
"net/http"
) )
func Init() { func Init() {
@@ -31,7 +33,7 @@ func Init() {
address := cfg.Mod.Listen address := cfg.Mod.Listen
pionAPI, err := webrtc.NewAPI(address) pionAPI, err := webrtc.NewAPI(address)
if pionAPI == nil { if pionAPI == nil {
log.Error().Err(err).Msg("[webrtc] init API") log.Error().Err(err).Caller().Msg("webrtc.NewAPI")
return return
} }
@@ -53,8 +55,10 @@ func Init() {
candidates = cfg.Mod.Candidates candidates = cfg.Mod.Candidates
api.HandleWS(webrtc.MsgTypeOffer, offerHandler) api.HandleWS(webrtc.MsgTypeOffer, asyncHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler) api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
api.HandleFunc("api/webrtc", syncHandler)
} }
var Port string var Port string
@@ -62,7 +66,7 @@ var log zerolog.Logger
var NewPConn func() (*pion.PeerConnection, error) var NewPConn func() (*pion.PeerConnection, error)
func offerHandler(ctx *api.Context, msg *streamer.Message) { func asyncHandler(ctx *api.Context, msg *streamer.Message) {
src := ctx.Request.URL.Query().Get("src") src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src) stream := streams.Get(src)
if stream == nil { if stream == nil {
@@ -77,7 +81,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
conn := new(webrtc.Conn) conn := new(webrtc.Conn)
conn.Conn, err = NewPConn() conn.Conn, err = NewPConn()
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn") log.Error().Err(err).Caller().Msg("NewPConn")
return return
} }
@@ -100,14 +104,14 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
log.Trace().Msgf("[webrtc] offer:\n%s", offer) log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil { if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer") log.Warn().Err(err).Caller().Msg("conn.SetOffer")
ctx.Error(err) ctx.Error(err)
return return
} }
// 2. AddConsumer, so we get new tracks // 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil { if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer") log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
_ = conn.Conn.Close() _ = conn.Conn.Close()
ctx.Error(err) ctx.Error(err)
return return
@@ -116,25 +120,46 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
conn.Init() conn.Init()
// exchange sdp without waiting all candidates // 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) log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer") log.Error().Err(err).Caller().Msg("conn.GetAnswer")
ctx.Error(err) ctx.Error(err)
return return
} }
ctx.Write(&streamer.Message{
Type: webrtc.MsgTypeAnswer, Value: answer,
})
ctx.Consumer = conn ctx.Consumer = conn
ctx.Write(&streamer.Message{Type: webrtc.MsgTypeAnswer, Value: answer})
asyncCandidates(ctx)
}
func syncHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
return
}
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Caller().Msg("ExchangeSDP")
return
}
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Caller().Msg("w.Write")
}
} }
func ExchangeSDP( func ExchangeSDP(
@@ -144,7 +169,7 @@ func ExchangeSDP(
conn := new(webrtc.Conn) conn := new(webrtc.Conn)
conn.Conn, err = NewPConn() conn.Conn, err = NewPConn()
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn") log.Error().Err(err).Caller().Msg("NewPConn")
return return
} }
@@ -162,13 +187,13 @@ func ExchangeSDP(
log.Trace().Msgf("[webrtc] offer:\n%s", offer) log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil { if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer") log.Warn().Err(err).Caller().Msg("conn.SetOffer")
return return
} }
// 2. AddConsumer, so we get new tracks // 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil { if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer") log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
_ = conn.Conn.Close() _ = conn.Conn.Close()
return return
} }
@@ -179,12 +204,12 @@ func ExchangeSDP(
//answer, err := conn.ExchangeSDP(offer, false) //answer, err := conn.ExchangeSDP(offer, false)
answer, err = conn.GetCompleteAnswer() answer, err = conn.GetCompleteAnswer()
if err == nil { if err == nil {
answer, err = addCanditates(answer) answer, err = syncCanditates(answer)
} }
log.Trace().Msgf("[webrtc] answer\n%s", answer) log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer") log.Error().Err(err).Caller().Msg("conn.GetCompleteAnswer")
} }
return return

View File

@@ -1,58 +0,0 @@
package main
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/pion/rtp"
"os"
"time"
)
func main() {
client, err := rtsp.NewClient(os.Args[1])
if err != nil {
panic(err)
}
if err = client.Dial(); err != nil {
panic(err)
}
if err = client.Describe(); err != nil {
panic(err)
}
for _, media := range client.GetMedias() {
fmt.Printf("Media: %v\n", media)
if media.AV() {
track := client.GetTrack(media, media.Codecs[0])
fmt.Printf("Track: %v, %v\n", track, track.Codec)
track.Bind(func(packet *rtp.Packet) error {
nalUnitType := packet.Payload[0] & 0x1F
fmt.Printf(
"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
packet.PayloadType, packet.SSRC,
)
return nil
})
}
}
if err = client.Play(); err != nil {
panic(err)
}
time.AfterFunc(time.Second*5, func() {
if err = client.Close(); err != nil {
panic(err)
}
})
if err = client.Handle(); err != nil {
panic(err)
}
fmt.Println("The End")
}

8
go.mod
View File

@@ -53,9 +53,7 @@ replace (
// windows support: https://github.com/brutella/dnssd/pull/35 // 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 github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
// RTP tlv8 fix // RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
// MSE update // fix reading AAC config bytes
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
// AES_256_CM_HMAC_SHA1_80 support
github.com/pion/srtp/v2 v2.0.10 => github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10
) )

15
go.sum
View File

@@ -1,9 +1,7 @@
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78= github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0= github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg= github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
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/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0= github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs= github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
@@ -103,6 +101,8 @@ github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw= github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA= github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
@@ -121,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 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= 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/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 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M= 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= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -245,6 +245,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

12
main.go
View File

@@ -4,10 +4,13 @@ import (
"github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/debug" "github.com/AlexxIT/go2rtc/cmd/debug"
"github.com/AlexxIT/go2rtc/cmd/echo"
"github.com/AlexxIT/go2rtc/cmd/exec" "github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg" "github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass" "github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/homekit" "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/mp4"
"github.com/AlexxIT/go2rtc/cmd/ngrok" "github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp" "github.com/AlexxIT/go2rtc/cmd/rtmp"
@@ -24,20 +27,25 @@ func main() {
app.Init() // init config and logs app.Init() // init config and logs
streams.Init() // load streams list streams.Init() // load streams list
api.Init() // init HTTP API server
echo.Init()
rtsp.Init() // add support RTSP client and RTSP server rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server) exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme) ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme hass.Init() // add support hass scheme
api.Init() // init HTTP API server
webrtc.Init() webrtc.Init()
mp4.Init() mp4.Init()
mjpeg.Init()
srtp.Init() srtp.Init()
homekit.Init() homekit.Init()
ivideon.Init()
ngrok.Init() ngrok.Init()
debug.Init() debug.Init()

View File

@@ -3,3 +3,6 @@
- https://www.wowza.com/blog/streaming-protocols - https://www.wowza.com/blog/streaming-protocols
- https://vimeo.com/blog/post/rtmp-stream/ - https://vimeo.com/blog/post/rtmp-stream/
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a - https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a
- [Android Supported media formats](https://developer.android.com/guide/topics/media/media-formats)
- [THEOplayer](https://www.theoplayer.com/test-your-stream-hls-dash-hesp)
- [How Generate DTS/PTS](https://www.ramugedia.com/how-generate-dts-pts-from-elementary-stream)

20
pkg/aac/README.md Normal file
View File

@@ -0,0 +1,20 @@
## AAC-LD and AAC-ELD
Codec | Rate | QuickTime | ffmpeg | VLC
------|------|-----------|--------|----
AAC-LD | 8000 | yes | no | no
AAC-LD | 16000 | yes | no | no
AAC-LD | 22050 | yes | yes | no
AAC-LD | 24000 | yes | yes | no
AAC-LD | 32000 | yes | yes | no
AAC-ELD | 8000 | yes | no | no
AAC-ELD | 16000 | yes | no | no
AAC-ELD | 22050 | yes | yes | yes
AAC-ELD | 24000 | yes | yes | yes
AAC-ELD | 32000 | yes | yes | yes
## Useful links
- [4.6.20 Enhanced Low Delay Codec](https://csclub.uwaterloo.ca/~ehashman/ISO14496-3-2009.pdf)
- https://stackoverflow.com/questions/40014508/aac-adts-for-aacobject-eld-packets
- https://code.videolan.org/videolan/vlc/-/blob/master/modules/packetizer/mpeg4audio.c

57
pkg/aac/rtp.go Normal file
View File

@@ -0,0 +1,57 @@
package aac
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
const RTPPacketVersionAAC = 0
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
// support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = packet.Payload[2+headersSize:]
return push(&clone)
}
}
}
func RTPPay(mtu uint16) streamer.WrapperFunc {
sequencer := rtp.NewRandomSequencer()
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != RTPPacketVersionAAC {
return push(packet)
}
// support ONLY one unit in payload
size := uint16(len(packet.Payload))
// 2 bytes header size + 2 bytes first payload size
payload := make([]byte, 2+2+size)
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], size<<3)
copy(payload[4:], packet.Payload)
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
return push(&clone)
}
}
}

View File

@@ -24,7 +24,7 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
panic("you shall not pass!") panic("you shall not pass!")
} }
track := &streamer.Track{Codec: codec, Direction: media.Direction} track := streamer.NewTrack(codec, media.Direction)
switch media.Direction { switch media.Direction {
case streamer.DirectionSendonly: case streamer.DirectionSendonly:

View File

@@ -1,3 +1,22 @@
# H264
Access Unit (AU) can contain one or multiple NAL Unit:
1. [SEI,] SPS, PPS, IFrame, [IFrame...]
2. BFrame, [BFrame...]
3. IFrame, [IFrame...]
## RTP H264
Camera | NALu
-------|-----
EZVIZ C3S | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 1t, 1t, 1t
Sonoff GK-200MP2-B | 28:28:28 -> 5t, 1t, 1t, 1t
Dahua IPC-K42 | 7f, 8f, 28:28:28 -> 5t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg copy | 5t, 1t, 1t, 28:28:28 -> 1t, 28:28:28 -> 1t
FFmpeg h264 | 24 -> 6:5:5:5:5t, 24 -> 1:1:1:1t, 28:28:28 -> 5f, 28:28:28 -> 5f, 28:28:28 -> 5t
FFmpeg resize | 6f, 28:28:28 -> 5f, 28... -> 5t, 24 -> 1:1f, 24 -> 1:1t
## WebRTC ## WebRTC
Video codec | Media string | Device Video codec | Media string | Device
@@ -25,3 +44,4 @@ H.264/high | avc1.6400xx | FFmpeg superfast
- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels) - [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter) - [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) - [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/)

View File

@@ -6,55 +6,71 @@ import (
"github.com/pion/rtp" "github.com/pion/rtp"
) )
const PayloadTypeAVC = 255 func EncodeAVC(nals ...[]byte) (avc []byte) {
var i, n int
func IsAVC(codec *streamer.Codec) bool { for _, nal := range nals {
return codec.PayloadType == PayloadTypeAVC if i = len(nal); i > 0 {
} n += 4 + i
}
}
avc = make([]byte, n)
n = 0
for _, nal := range nals {
if i = len(nal); i > 0 {
binary.BigEndian.PutUint32(avc[n:], uint32(i))
n += 4 + copy(avc[n+4:], nal)
}
}
func EncodeAVC(raw []byte) (avc []byte) {
avc = make([]byte, len(raw)+4)
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
copy(avc[4:], raw)
return return
} }
func RepairAVC(track *streamer.Track) streamer.WrapperFunc { func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
sps, pps := GetParameterSet(track.Codec.FmtpLine) sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps) ps := EncodeAVC(sps, pps)
pps = EncodeAVC(pps)
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) (err error) { return func(packet *rtp.Packet) (err error) {
naluType := NALUType(packet.Payload) if NALUType(packet.Payload) == NALUTypeIFrame {
switch naluType { packet.Payload = Join(ps, packet.Payload)
case NALUTypeSPS:
sps = packet.Payload
return
case NALUTypePPS:
pps = packet.Payload
return
} }
return push(packet)
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)) + 4
// check if multiple items in one packet
if size < len(data) {
nals = append(nals, data[:size])
data = data[size:]
} else {
nals = append(nals, data)
break
}
}
return nals
}
func Types(data []byte) []byte {
var types []byte
for {
types = append(types, NALUType(data))
size := 4 + int(binary.BigEndian.Uint32(data))
if size < len(data) {
data = data[size:]
} else {
break
}
}
return types
}

View File

@@ -2,24 +2,49 @@ package h264
import ( import (
"encoding/base64" "encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings" "strings"
) )
const ( const (
NALUTypePFrame = 1 NALUTypePFrame = 1 // Coded slice of a non-IDR picture
NALUTypeIFrame = 5 NALUTypeIFrame = 5 // Coded slice of an IDR picture
NALUTypeSEI = 6 NALUTypeSEI = 6 // Supplemental enhancement information (SEI)
NALUTypeSPS = 7 NALUTypeSPS = 7 // Sequence parameter set
NALUTypePPS = 8 NALUTypePPS = 8 // Picture parameter set
NALUTypeAUD = 9 // Access unit delimiter
) )
func NALUType(b []byte) byte { func NALUType(b []byte) byte {
return b[4] & 0x1F return b[4] & 0x1F
} }
// IsKeyframe - check if any NALU in one AU is Keyframe
func IsKeyframe(b []byte) bool { func IsKeyframe(b []byte) bool {
return NALUType(b) == NALUTypeIFrame for {
switch NALUType(b) {
case NALUTypePFrame:
return false
case NALUTypeIFrame:
return true
}
size := int(binary.BigEndian.Uint32(b)) + 4
if size < len(b) {
b = b[size:]
continue
} else {
return false
}
}
}
func Join(ps, iframe []byte) []byte {
b := make([]byte, len(ps)+len(iframe))
i := copy(b, ps)
copy(b[i:], iframe)
return b
} }
func GetProfileLevelID(fmtp string) string { func GetProfileLevelID(fmtp string) string {

View File

@@ -13,92 +13,70 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
depack := &codecs.H264Packet{IsAVC: true} depack := &codecs.H264Packet{IsAVC: true}
sps, pps := GetParameterSet(track.Codec.FmtpLine) sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps) ps := EncodeAVC(sps, pps)
pps = EncodeAVC(pps)
var buffer []byte buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error { return func(packet *rtp.Packet) error {
//nalUnitType := packet.Payload[0] & 0x1F //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
//fmt.Printf(
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC,
//)
// NALu packets can be split in different ways: payload, err := depack.Unmarshal(packet.Payload)
// - single type 7 and type 8 packets if len(payload) == 0 || err != nil {
// - join type 7 and type 8 packet (type 24)
// - split type 5 on multiple 28 packets
// - split type 5 on multiple separate 28 packets
units, err := depack.Unmarshal(packet.Payload)
if len(units) == 0 || err != nil {
return nil return nil
} }
for len(units) > 0 { // Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
i := int(binary.BigEndian.Uint32(units)) + 4 if packet.Marker {
unit := units[:i] // NAL Unit with AVC header switch NALUType(payload) {
units = units[i:] case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...)
unitType := NALUType(unit) return nil
//fmt.Printf("[H264] type: %2d, size: %6d\n", unitType, i) }
switch unitType {
case NALUTypeSPS:
//println("new SPS")
sps = unit
continue
case NALUTypePPS:
//println("new PPS")
pps = unit
continue
case NALUTypeSEI:
// some unnecessary text information
continue
} }
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1` if len(buf) == 0 {
// and every NALU will be sliced to multiple NALUs // Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) {
case NALUTypeIFrame:
// fix IFrame without SPS,PPS
buf = append(buf, ps...)
case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload))
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) {
return nil
}
payload = payload[i:]
if NALUType(payload) == NALUTypeIFrame {
buf = append(buf, ps...)
}
}
}
// collect all NALs for Access Unit
if !packet.Marker { if !packet.Marker {
buffer = append(buffer, unit...) buf = append(buf, payload...)
continue
}
if buffer != nil {
buffer = append(buffer, unit...)
unit = buffer
buffer = nil
}
var clone rtp.Packet
if unitType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
if err = push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = unit
if err = push(&clone); err != nil {
return err
}
}
return nil return nil
} }
if len(buf) > 0 {
payload = append(buf, payload...)
buf = buf[:0]
}
//log.Printf("[AVC] %v, len: %d", Types(payload), len(payload))
clone := *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = payload
return push(&clone)
}
} }
} }
@@ -109,17 +87,19 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error { return func(packet *rtp.Packet) error {
if packet.Version == RTPPacketVersionAVC { if packet.Version != RTPPacketVersionAVC {
return push(packet)
}
payloads := payloader.Payload(mtu, packet.Payload) payloads := payloader.Payload(mtu, packet.Payload)
last := len(payloads) - 1
for i, payload := range payloads { for i, payload := range payloads {
clone := rtp.Packet{ clone := rtp.Packet{
Header: rtp.Header{ Header: rtp.Header{
Version: 2, Version: 2,
Marker: i == len(payloads)-1, Marker: i == last,
//PayloadType: packet.PayloadType,
SequenceNumber: sequencer.NextSequenceNumber(), SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp, Timestamp: packet.Timestamp,
//SSRC: packet.SSRC,
}, },
Payload: payload, Payload: payload,
} }
@@ -127,10 +107,8 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
return err return err
} }
} }
return nil
}
return push(packet) return nil
} }
} }
} }

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

@@ -0,0 +1,4 @@
## Useful links
- https://datatracker.ietf.org/doc/html/rfc7798
- [Add initial support for WebRTC HEVC](https://trac.webkit.org/changeset/259452/webkit)

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

@@ -0,0 +1,73 @@
package h265
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
const (
NALUTypePFrame = 1
NALUTypeIFrame = 19
NALUTypeIFrame2 = 20
NALUTypeIFrame3 = 21
NALUTypeVPS = 32
NALUTypeSPS = 33
NALUTypePPS = 34
NALUTypeFU = 49
)
func NALUType(b []byte) byte {
return (b[4] >> 1) & 0x3F
}
func IsKeyframe(b []byte) bool {
for {
switch NALUType(b) {
case NALUTypePFrame:
return false
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
return true
}
size := int(binary.BigEndian.Uint32(b)) + 4
if size < len(b) {
b = b[size:]
continue
} else {
return false
}
}
}
func Types(data []byte) []byte {
var types []byte
for {
types = append(types, NALUType(data))
size := 4 + int(binary.BigEndian.Uint32(data))
if size < len(data) {
data = data[size:]
} else {
break
}
}
return types
}
func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
if fmtp == "" {
return
}
s := streamer.Between(fmtp, "sprop-vps=", ";")
vps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-sps=", ";")
sps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-pps=", ";")
pps, _ = base64.StdEncoding.DecodeString(s)
return
}

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

@@ -0,0 +1,136 @@
package h265
import (
"encoding/binary"
"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)
//ps := h264.EncodeAVC(vps, sps, pps)
buf := make([]byte, 0, 512*1024) // 512K
var nuStart int
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
data := packet.Payload
nuType := (data[0] >> 1) & 0x3F
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
if nuType == NALUTypeFU {
switch data[2] >> 6 {
case 2: // begin
nuType = data[2] & 0x3F
// push PS data before keyframe
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
// buf = append(buf, ps...)
//}
nuStart = len(buf)
buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
buf = append(buf, data[3:]...)
return nil
case 0: // continue
buf = append(buf, data[3:]...)
return nil
case 1: // end
buf = append(buf, data[3:]...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
}
} else {
nuStart = len(buf)
buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, data...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
}
// collect all NAL Units for Access Unit
if !packet.Marker {
return nil
}
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
clone := *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = buf
buf = buf[:0]
return push(&clone)
}
}
}
// SafariPay - generate Safari friendly payload for H265
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
nuType := (data[4] >> 1) & 0b111111
//fmt.Printf("[H265] nut: %2d, size: %6d, data: %16x\n", nut, len(data), data[4:20])
switch {
case nuType >= h265parser.NAL_UNIT_VPS && nuType <= h265parser.NAL_UNIT_PPS:
buffer = append(buffer, data...)
return nil
case nuType >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nuType <= 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)
}
}
}

View File

@@ -1,4 +1,4 @@
# Homekit # Home Accessory Protocol
> PS. Character = Characteristic > PS. Character = Characteristic

View File

@@ -1,4 +1,4 @@
package homekit package hap
type Accessory struct { type Accessory struct {
AID int `json:"aid"` AID int `json:"aid"`

View File

@@ -2,22 +2,22 @@ package camera
import ( import (
"errors" "errors"
"github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/brutella/hap/characteristic" "github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp" "github.com/brutella/hap/rtp"
) )
type Client struct { type Client struct {
client *homekit.Client client *hap.Conn
} }
func NewClient(client *homekit.Client) *Client { func NewClient(client *hap.Conn) *Client {
return &Client{client: client} return &Client{client: client}
} }
func (c *Client) StartStream2(ses *Session) (err error) { func (c *Client) StartStream(ses *Session) (err error) {
// Step 1. Check if camera ready (free) to stream // Step 1. Check if camera ready (free) to stream
var srv *homekit.Service var srv *hap.Service
if srv, err = c.GetFreeStream(); err != nil { if srv, err = c.GetFreeStream(); err != nil {
return err return err
} }
@@ -35,8 +35,8 @@ func (c *Client) StartStream2(ses *Session) (err error) {
// GetFreeStream search free streaming service. // GetFreeStream search free streaming service.
// Usual every HomeKit camera can stream only to two clients simultaniosly. // Usual every HomeKit camera can stream only to two clients simultaniosly.
// So it has two similar services for streaming. // So it has two similar services for streaming.
func (c *Client) GetFreeStream() (srv *homekit.Service, err error) { func (c *Client) GetFreeStream() (srv *hap.Service, err error) {
var accs []*homekit.Accessory var accs []*hap.Accessory
if accs, err = c.client.GetAccessories(); err != nil { if accs, err = c.client.GetAccessories(); err != nil {
return return
} }
@@ -60,7 +60,7 @@ func (c *Client) GetFreeStream() (srv *homekit.Service, err error) {
} }
func (c *Client) SetupEndpoins( func (c *Client) SetupEndpoins(
srv *homekit.Service, req *rtp.SetupEndpoints, srv *hap.Service, req *rtp.SetupEndpoints,
) (res *rtp.SetupEndpointsResponse, err error) { ) (res *rtp.SetupEndpointsResponse, err error) {
// get setup endpoint character ID // get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSetupEndpoints) char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
@@ -87,7 +87,7 @@ func (c *Client) SetupEndpoins(
return return
} }
func (c *Client) SetConfig(srv *homekit.Service, config *rtp.StreamConfiguration) (err error) { func (c *Client) SetConfig(srv *hap.Service, config *rtp.StreamConfiguration) (err error) {
// get setup endpoint character ID // get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration) char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
char.Event = nil char.Event = nil

75
pkg/hap/camera/session.go Normal file
View File

@@ -0,0 +1,75 @@
package camera
import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/brutella/hap/rtp"
)
type Session struct {
Offer *rtp.SetupEndpoints
Answer *rtp.SetupEndpointsResponse
Config *rtp.StreamConfiguration
}
func NewSession(vp *rtp.VideoParameters, ap *rtp.AudioParameters) *Session {
vp.RTP = rtp.RTPParams{
PayloadType: 99,
Ssrc: RandomUint32(),
Bitrate: 2048,
Interval: 10,
MTU: 1200, // like WebRTC
}
ap.RTP = rtp.RTPParams{
PayloadType: 110,
Ssrc: RandomUint32(),
Bitrate: 32,
Interval: 10,
ComfortNoisePayloadType: 98,
MTU: 0,
}
sessionID := RandomBytes(16)
s := &Session{
Offer: &rtp.SetupEndpoints{
SessionId: sessionID,
Video: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
Audio: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &rtp.StreamConfiguration{
Command: rtp.SessionControlCommand{
Identifier: sessionID,
Type: rtp.SessionControlCommandTypeStart,
},
Video: *vp,
Audio: *ap,
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = rtp.Addr{
IPAddr: host,
VideoRtpPort: port,
AudioRtpPort: port,
}
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = cryptorand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = cryptorand.Read(data)
return binary.BigEndian.Uint32(data)
}

View File

@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bytes" "bytes"

733
pkg/hap/conn.go Normal file
View File

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

View File

@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"crypto/rand" "crypto/rand"
@@ -29,13 +29,13 @@ func GenerateUUID() string {
} }
type PairVerifyPayload struct { type PairVerifyPayload struct {
Method byte `tlv8:"0"` Method byte `tlv8:"0,optional"`
Identifier string `tlv8:"1"` Identifier string `tlv8:"1,optional"`
PublicKey []byte `tlv8:"3"` PublicKey []byte `tlv8:"3,optional"`
EncryptedData []byte `tlv8:"5"` EncryptedData []byte `tlv8:"5,optional"`
State byte `tlv8:"6"` State byte `tlv8:"6,optional"`
Status byte `tlv8:"7"` Status byte `tlv8:"7,optional"`
Signature []byte `tlv8:"10"` Signature []byte `tlv8:"10,optional"`
} }
//func (c *Character) Unmarshal(value interface{}) error { //func (c *Character) Unmarshal(value interface{}) error {

View File

@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bufio" "bufio"
@@ -23,7 +23,7 @@ const (
UriResource = "/resource" UriResource = "/resource"
) )
func (c *Client) Write(p []byte) (r io.Reader, err error) { func (c *Conn) Write(p []byte) (r io.Reader, err error) {
if c.secure == nil { if c.secure == nil {
if _, err = c.conn.Write(p); err == nil { if _, err = c.conn.Write(p); err == nil {
r = bufio.NewReader(c.conn) r = bufio.NewReader(c.conn)
@@ -36,7 +36,7 @@ func (c *Client) Write(p []byte) (r io.Reader, err error) {
return return
} }
func (c *Client) Do(req *http.Request) (*http.Response, error) { func (c *Conn) Do(req *http.Request) (*http.Response, error) {
if c.secure == nil { if c.secure == nil {
// insecure requests // insecure requests
if err := req.Write(c.conn); err != nil { if err := req.Write(c.conn); err != nil {
@@ -56,7 +56,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
return http.ReadResponse(buf, req) return http.ReadResponse(buf, req)
} }
func (c *Client) Get(uri string) (*http.Response, error) { func (c *Conn) Get(uri string) (*http.Response, error) {
req, err := http.NewRequest( req, err := http.NewRequest(
"GET", "http://"+c.DeviceAddress+uri, nil, "GET", "http://"+c.DeviceAddress+uri, nil,
) )
@@ -66,7 +66,7 @@ func (c *Client) Get(uri string) (*http.Response, error) {
return c.Do(req) return c.Do(req)
} }
func (c *Client) Post(uri string, data []byte) (*http.Response, error) { func (c *Conn) Post(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest( req, err := http.NewRequest(
"POST", "http://"+c.DeviceAddress+uri, "POST", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data), bytes.NewReader(data),
@@ -85,7 +85,7 @@ func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
return c.Do(req) return c.Do(req)
} }
func (c *Client) Put(uri string, data []byte) (*http.Response, error) { func (c *Conn) Put(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest( req, err := http.NewRequest(
"PUT", "http://"+c.DeviceAddress+uri, "PUT", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data), bytes.NewReader(data),
@@ -102,7 +102,7 @@ func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
return c.Do(req) return c.Do(req)
} }
func (c *Client) Handle() (err error) { func (c *Conn) Handle() (err error) {
defer func() { defer func() {
if c.conn == nil { if c.conn == nil {
err = nil err = nil

View File

@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bufio" "bufio"

View File

@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"encoding/binary" "encoding/binary"

View File

@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bufio" "bufio"

View File

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

View File

@@ -1,732 +1,265 @@
package homekit package homekit
import ( import (
"bufio"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns" "github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap" "github.com/brutella/hap/characteristic"
"github.com/brutella/hap/chacha20poly1305" "github.com/brutella/hap/rtp"
"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"
"net/http"
"net/url" "net/url"
"strings"
) )
// Client for HomeKit. DevicePublic can be null.
type Client struct { type Client struct {
streamer.Element streamer.Element
DeviceAddress string // including port conn *hap.Conn
DeviceID string exit chan error
DevicePublic []byte server *srtp.Server
ClientID string url string
ClientPrivate []byte
OnEvent func(res *http.Response) medias []*streamer.Media
Output func(msg interface{}) tracks []*streamer.Track
conn net.Conn sessions []*srtp.Session
secure *Secure
httpResponse chan *bufio.Reader
} }
func NewClient(rawURL string) (*Client, error) { func NewClient(rawURL string, server *srtp.Server) (*Client, error) {
u, err := url.Parse(rawURL) u, err := url.Parse(rawURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
query := u.Query() query := u.Query()
c := &Client{ c := &hap.Conn{
DeviceAddress: u.Host, DeviceAddress: u.Host,
DeviceID: query.Get("device_id"), DeviceID: query.Get("device_id"),
DevicePublic: DecodeKey(query.Get("device_public")), DevicePublic: hap.DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"), ClientID: query.Get("client_id"),
ClientPrivate: DecodeKey(query.Get("client_private")), ClientPrivate: hap.DecodeKey(query.Get("client_private")),
} }
return c, nil return &Client{conn: c, server: server}, 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 { func (c *Client) Dial() error {
// update device host before dial if err := c.conn.Dial(); err != nil {
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 return err
} }
// STEP M1: send our session public to device c.exit = make(chan error)
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. generate payload go func() {
// important not include other fields //start goroutine for reading responses from camera
requestM1 := struct { c.exit <- c.conn.Handle()
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 return nil
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) GetMedias() []*streamer.Media {
func (c *Client) Pair(mfi bool, pin string) (err error) { if c.medias == nil {
pin = strings.ReplaceAll(pin, "-", "") c.medias = c.getMedias()
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 return c.medias
reqM1 := struct { }
Method byte `tlv8:"0"`
State byte `tlv8:"6"` func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
}{ for _, track := range c.tracks {
State: hap.M1, if track.Codec == codec {
return track
} }
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 track := streamer.NewTrack(codec, media.Direction)
res, err := c.Post(UriPairSetup, buf) c.tracks = append(c.tracks, track)
if err != nil { return track
return }
func (c *Client) Start() error {
if c.tracks == nil {
return errors.New("producer without tracks")
} }
// STEP M2. Read response // get our server local IP-address
resM2 := struct { host, _, err := net.SplitHostPort(c.conn.LocalAddr())
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 { if err != nil {
return err return err
} }
// STEP M3. Send request // TODO: set right config
res, err = c.Post(UriPairSetup, buf) vp := &rtp.VideoParameters{
if err != nil { CodecType: rtp.VideoCodecType_H264,
return 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,
},
} }
// STEP M4. Read response ap := &rtp.AudioParameters{
resM4 := struct { CodecType: rtp.AudioCodecType_AAC_ELD,
Proof []byte `tlv8:"4"` // server proof CodecParams: rtp.AudioCodecParameters{
State byte `tlv8:"6"` Channels: 1,
Error byte `tlv8:"7"` Bitrate: rtp.AudioCodecBitrateVariable,
}{} Samplerate: rtp.AudioCodecSampleRate16Khz,
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil { // packet time=20 => AAC-ELD packet size=480
return // packet time=30 => AAC-ELD packet size=480
} // packet time=40 => AAC-ELD packet size=480
if resM4.Error == 2 { // packet time=60 => AAC-LD packet size=960
return fmt.Errorf("wrong PIN: %s", pin) PacketTime: 40,
} },
if resM4.State != 4 || resM4.Error > 0 {
return fmt.Errorf("wrong M4: %+v", resM4)
} }
// STEP M4. Verify response // setup HomeKit stream session
if !session.VerifyServerAuthenticator(resM4.Proof) { hkSession := camera.NewSession(vp, ap)
return errors.New("verify server auth fail") hkSession.SetLocalEndpoint(host, c.server.Port())
}
// STEP M5. Generate signature // create client for processing camera accessory
saltKey, err := hkdf.Sha512( cam := camera.NewClient(c.conn)
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"), // try to start HomeKit stream
[]byte("Pair-Setup-Controller-Sign-Info"), if err = cam.StartStream(hkSession); err != nil {
)
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 return err
} }
// STEP M5. Send request // SRTP Video Session
res, err = c.Post(UriPairSetup, buf) vs := &srtp.Session{
if err != nil { LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
return RemoteSSRC: hkSession.Answer.SsrcVideo,
} }
if err = vs.SetKeys(
// STEP M6. Read response hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
resM6 := struct { hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
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 { ); 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 return err
} }
res, err := c.Post("/pairings", data) // SRTP Audio Session
if err != nil { as := &srtp.Session{
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcAudio,
}
if err = as.SetKeys(
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
); err != nil {
return err return err
} }
data, err = io.ReadAll(res.Body) for _, track := range c.tracks {
// TODO: don't know how to fix array of items switch track.Codec.Name {
var pRes struct { case streamer.CodecH264:
State byte `tlv8:"6"` vs.Track = track
Identifier string `tlv8:"1"` case streamer.CodecELD:
PublicKey []byte `tlv8:"3"` as.Track = track
Permission byte `tlv8:"11"`
} }
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
} }
return nil c.server.AddSession(vs)
c.server.AddSession(as)
c.sessions = []*srtp.Session{vs, as}
return <-c.exit
} }
func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error { func (c *Client) Stop() error {
pReq := struct { err := c.conn.Close()
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"` for _, session := range c.sessions {
PublicKey []byte `tlv8:"3"` c.server.RemoveSession(session)
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 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 { func (c *Client) getMedias() []*streamer.Media {
reqM1 := struct { var medias []*streamer.Media
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) accs, err := c.conn.GetAccessories()
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 { if err != nil {
return nil return nil
} }
return data
acc := accs[0]
// get supported video config (not really necessary)
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
v1 := &rtp.VideoStreamConfiguration{}
if err = char.ReadTLV8(v1); err != nil {
return nil
}
for _, hkCodec := range v1.Codecs {
codec := &streamer.Codec{ClockRate: 90000}
switch hkCodec.Type {
case rtp.VideoCodecType_H264:
codec.Name = streamer.CodecH264
codec.FmtpLine = "profile-level-id=420029"
default:
fmt.Printf("unknown codec: %d", hkCodec.Type)
continue
}
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 {
return nil
}
for _, hkCodec := range v2.Codecs {
codec := &streamer.Codec{
Channels: uint16(hkCodec.Parameters.Channels),
}
switch hkCodec.Parameters.Samplerate {
case rtp.AudioCodecSampleRate8Khz:
codec.ClockRate = 8000
case rtp.AudioCodecSampleRate16Khz:
codec.ClockRate = 16000
case rtp.AudioCodecSampleRate24Khz:
codec.ClockRate = 24000
default:
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
}
switch hkCodec.Type {
case rtp.AudioCodecType_AAC_ELD:
codec.Name = streamer.CodecELD
// only this value supported by FFmpeg
codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
default:
fmt.Printf("unknown codec: %d", hkCodec.Type)
continue
}
media := &streamer.Media{
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
return medias
} }

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

@@ -0,0 +1,3 @@
## Useful links
- https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779

100
pkg/httpflv/httpflv.go Normal file
View File

@@ -0,0 +1,100 @@
package httpflv
import (
"bufio"
"errors"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/flv/flvio"
"github.com/deepch/vdk/utils/bits/pio"
"io"
"net/http"
)
func Dial(uri string) (*Conn, error) {
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
c := Conn{
conn: res.Body,
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
buf: make([]byte, 256),
}
if _, err = io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
return nil, err
}
flags, n, err := flvio.ParseFileHeader(c.buf)
if err != nil {
return nil, err
}
if flags&flvio.FILE_HAS_VIDEO == 0 {
return nil, errors.New("not supported")
}
if _, err = c.reader.Discard(n); err != nil {
return nil, err
}
return &c, nil
}
type Conn struct {
conn io.ReadCloser
reader *bufio.Reader
buf []byte
}
func (c *Conn) Streams() ([]av.CodecData, error) {
for {
tag, _, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return nil, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
continue
}
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
if err != nil {
return nil, err
}
return []av.CodecData{stream}, nil
}
}
func (c *Conn) ReadPacket() (av.Packet, error) {
for {
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return av.Packet{}, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
continue
}
return av.Packet{
Idx: 0,
Data: tag.Data,
CompositionTime: flvio.TsToTime(tag.CompositionTime),
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
Time: flvio.TsToTime(ts),
}, nil
}
}
func (c *Conn) Close() (err error) {
return c.conn.Close()
}

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

@@ -0,0 +1,283 @@
package ivideon
import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"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.Codec{
Name: streamer.CodecH264,
ClockRate: 90000,
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
PayloadType: streamer.PayloadTypeMP4,
}
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.NewTrack(codec, streamer.DirectionSendonly)
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
packet := &rtp.Packet{
// ivideon clockrate=1000, RTP clockrate=90000
Header: rtp.Header{Timestamp: ts * 90},
Payload: data[:entry.Size],
}
_ = track.WriteRTP(packet)
data = data[entry.Size:]
ts += entry.Duration
}
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

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

@@ -0,0 +1,95 @@
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)
}
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
// The maximum width is 2040 pixels.
w := uint16(packet.Payload[6]) << 3
h := uint16(packet.Payload[7]) << 3
// fix 2560x1920 and 2560x1440
if w == 512 && (h == 1920 || h == 1440) {
w = 2560
}
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
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...)
}

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

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

View File

@@ -3,6 +3,8 @@ package mp4
import ( import (
"encoding/binary" "encoding/binary"
"github.com/deepch/vdk/format/mp4/mp4io" "github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f"
"github.com/deepch/vdk/format/mp4f/mp4fio"
"time" "time"
) )
@@ -37,23 +39,15 @@ func MOOV() *mp4io.Movie {
SelectionDuration: time0, SelectionDuration: time0,
CurrentTime: time0, CurrentTime: time0,
}, },
MovieExtend: &mp4io.MovieExtend{ MovieExtend: &mp4io.MovieExtend{},
Tracks: []*mp4io.TrackExtend{
{
TrackId: 1,
DefaultSampleDescIdx: 1,
DefaultSampleDuration: 40,
},
},
},
} }
} }
func TRAK() *mp4io.Track { func TRAK(id int) *mp4io.Track {
return &mp4io.Track{ return &mp4io.Track{
// trak > tkhd // trak > tkhd
Header: &mp4io.TrackHeader{ Header: &mp4io.TrackHeader{
TrackId: int32(1), // change me TrackId: int32(id),
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
Duration: 0, // OK Duration: 0, // OK
Matrix: matrix, Matrix: matrix,
@@ -92,3 +86,15 @@ func TRAK() *mp4io.Track {
}, },
} }
} }
func ESDS(conf []byte) *mp4f.FDummy {
esds := &mp4fio.ElemStreamDesc{DecConfig: conf}
b := make([]byte, esds.Len())
esds.Marshal(b)
return &mp4f.FDummy{
Data: b,
Tag_: mp4io.Tag(uint32(mp4io.ESDS)),
}
}

View File

@@ -2,8 +2,9 @@ package mp4
import ( import (
"encoding/json" "encoding/json"
"fmt" "github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
@@ -11,6 +12,7 @@ import (
type Consumer struct { type Consumer struct {
streamer.Element streamer.Element
Medias []*streamer.Media
UserAgent string UserAgent string
RemoteAddr string RemoteAddr string
@@ -22,77 +24,124 @@ type Consumer struct {
} }
func (c *Consumer) GetMedias() []*streamer.Media { func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{ return []*streamer.Media{
{ {
Kind: streamer.KindVideo, Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly, Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{ Codecs: []*streamer.Codec{
{Name: streamer.CodecH264, ClockRate: 90000}, {Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC},
}, },
}, },
//{
// Kind: streamer.KindAudio,
// Direction: streamer.DirectionRecvonly,
// Codecs: []*streamer.Codec{
// {Name: streamer.CodecAAC, ClockRate: 16000},
// },
//},
} }
} }
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
trackID := byte(len(c.codecs))
c.codecs = append(c.codecs, track.Codec)
codec := track.Codec codec := track.Codec
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case streamer.CodecH264:
c.codecs = append(c.codecs, track.Codec)
push := func(packet *rtp.Packet) error { push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC { if packet.Version != h264.RTPPacketVersionAVC {
return nil return nil
} }
switch h264.NALUType(packet.Payload) {
case h264.NALUTypeIFrame:
c.start = true
case h264.NALUTypePFrame:
if !c.start { if !c.start {
return nil return nil
} }
default:
return nil
}
buf := c.muxer.Marshal(packet) buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf) c.send += len(buf)
c.Fire(buf) c.Fire(buf)
return nil return nil
} }
if !h264.IsAVC(codec) { var wrapper streamer.WrapperFunc
wrapper := h264.RTPDepay(track) if codec.IsMP4() {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if !c.start {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
if !codec.IsMP4() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecAAC:
push := func(packet *rtp.Packet) error {
if !c.start {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
if !codec.IsMP4() {
wrapper := aac.RTPDepay(track)
push = wrapper(push) push = wrapper(push)
} }
return track.Bind(push) return track.Bind(push)
} }
fmt.Printf("[rtmp] unsupported codec: %+v\n", track.Codec) panic("unsupported codec")
return nil
} }
func (c *Consumer) MimeType() string { func (c *Consumer) MimeType() string {
return c.muxer.MimeType(c.codecs) return c.muxer.MimeType(c.codecs)
} }
func (c *Consumer) Init() []byte { func (c *Consumer) Init() ([]byte, error) {
if c.muxer == nil {
c.muxer = &Muxer{} c.muxer = &Muxer{}
}
return c.muxer.GetInit(c.codecs) return c.muxer.GetInit(c.codecs)
} }
func (c *Consumer) Start() {
c.start = true
}
// //
func (c *Consumer) MarshalJSON() ([]byte, error) { func (c *Consumer) MarshalJSON() ([]byte, error) {

85
pkg/mp4/keyframe.go Normal file
View File

@@ -0,0 +1,85 @@
package mp4
import (
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
type Keyframe struct {
streamer.Element
MimeType string
}
func (c *Keyframe) GetMedias() []*streamer.Media {
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
}
}
func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
muxer := &Muxer{}
codecs := []*streamer.Codec{track.Codec}
init, err := muxer.GetInit(codecs)
if err != nil {
return nil
}
c.MimeType = muxer.MimeType(codecs)
switch track.Codec.Name {
case streamer.CodecH264:
push := func(packet *rtp.Packet) error {
if !h264.IsKeyframe(packet.Payload) {
return nil
}
buf := muxer.Marshal(0, packet)
c.Fire(append(init, buf...))
return nil
}
var wrapper streamer.WrapperFunc
if track.Codec.IsMP4() {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
push := func(packet *rtp.Packet) error {
if !h265.IsKeyframe(packet.Payload) {
return nil
}
buf := muxer.Marshal(0, packet)
c.Fire(append(init, buf...))
return nil
}
if !track.Codec.IsMP4() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}

View File

@@ -2,9 +2,13 @@ package mp4
import ( import (
"encoding/binary" "encoding/binary"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"github.com/deepch/vdk/format/fmp4/fmp4io" "github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/deepch/vdk/format/mp4/mp4io" "github.com/deepch/vdk/format/mp4/mp4io"
"github.com/deepch/vdk/format/mp4f/mp4fio" "github.com/deepch/vdk/format/mp4f/mp4fio"
@@ -13,47 +17,64 @@ import (
type Muxer struct { type Muxer struct {
fragIndex uint32 fragIndex uint32
dts uint64 dts []uint64
pts uint32 pts []uint32
data []byte //data []byte
total int //total int
} }
func (m *Muxer) MimeType(codecs []*streamer.Codec) string { func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
s := `video/mp4; codecs="` s := `video/mp4; codecs="`
for _, codec := range codecs { for i, codec := range codecs {
if i > 0 {
s += ","
}
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case streamer.CodecH264:
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
case streamer.CodecH265:
// H.265 profile=main level=5.1
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
s += "hvc1.1.6.L153.B0"
case streamer.CodecAAC:
s += "mp4a.40.2"
} }
} }
return s + `"` return s + `"`
} }
func (m *Muxer) GetInit(codecs []*streamer.Codec) []byte { func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
moov := MOOV() moov := MOOV()
for _, codec := range codecs { for i, codec := range codecs {
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine) sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
pps = []byte{0x68, 0xce, 0x38, 0x80}
}
// TODO: remove
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil { if err != nil {
return nil return nil, err
} }
width := codecData.Width() width := codecData.Width()
height := codecData.Height() height := codecData.Height()
trak := TRAK() trak := TRAK(i + 1)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Header.TrackWidth = float64(width) trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height) trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Video = &mp4io.VideoMediaInfo{ trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001, Flags: 0x000001,
} }
@@ -71,29 +92,105 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) []byte {
}, },
} }
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}
sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
}
codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
if err != nil {
return nil, err
}
width := codecData.Width()
height := codecData.Height()
trak := TRAK(i + 1)
trak.Header.TrackWidth = float64(width)
trak.Header.TrackHeight = float64(height)
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{ trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'}, 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}, Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
} }
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
Flags: 0x000001,
}
trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{
DataRefIdx: 1,
HorizontalResolution: 72,
VorizontalResolution: 72,
Width: int16(width),
Height: int16(height),
FrameCount: 1,
Depth: 24,
ColorTableId: -1,
Conf: &mp4io.HV1Conf{
Data: codecData.AVCDecoderConfRecordBytes(),
},
}
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil, err
}
trak := TRAK(i + 1)
trak.Header.AlternateGroup = 1
trak.Header.Duration = 0
trak.Header.Volume = 1
trak.Media.Header.TimeScale = int32(codec.ClockRate)
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'s', 'o', 'u', 'n'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
DataRefIdx: 1,
NumberOfChannels: int16(codec.Channels),
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
SampleRate: float64(codec.ClockRate),
Unknowns: []mp4io.Atom{ESDS(b)},
}
moov.Tracks = append(moov.Tracks, trak) moov.Tracks = append(moov.Tracks, trak)
} }
trex := &mp4io.TrackExtend{
TrackId: uint32(i + 1),
DefaultSampleDescIdx: 1,
DefaultSampleDuration: 0,
}
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
m.pts = append(m.pts, 0)
m.dts = append(m.dts, 0)
} }
data := make([]byte, moov.Len()) data := make([]byte, moov.Len())
moov.Marshal(data) moov.Marshal(data)
return append(FTYP(), data...) return append(FTYP(), data...), nil
} }
func (m *Muxer) Rewind() { //func (m *Muxer) Rewind() {
m.dts = 0 // m.dts = 0
m.pts = 0 // m.pts = 0
} //}
func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
trackID := uint8(1)
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
run := &mp4fio.TrackFragRun{ run := &mp4fio.TrackFragRun{
Flags: 0x000b05, Flags: 0x000b05,
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies), FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
@@ -108,12 +205,12 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
Tracks: []*mp4fio.TrackFrag{ Tracks: []*mp4fio.TrackFrag{
{ {
Header: &mp4fio.TrackFragHeader{ Header: &mp4fio.TrackFragHeader{
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID, 0x01, 0x01, 0x00, 0x00}, Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
}, },
DecodeTime: &mp4fio.TrackFragDecodeTime{ DecodeTime: &mp4fio.TrackFragDecodeTime{
Version: 1, Version: 1,
Flags: 0, Flags: 0,
Time: m.dts, Time: m.dts[trackID],
}, },
Run: run, Run: run,
}, },
@@ -121,15 +218,17 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
} }
entry := mp4io.TrackFragRunEntry{ entry := mp4io.TrackFragRunEntry{
Duration: 90000, //Duration: 90000,
Size: uint32(len(packet.Payload)), Size: uint32(len(packet.Payload)),
} }
newTime := packet.Timestamp newTime := packet.Timestamp
if m.pts > 0 { if m.pts[trackID] > 0 {
m.dts += uint64(newTime - m.pts) //m.dts += uint64(newTime - m.pts)
entry.Duration = newTime - m.pts[trackID]
m.dts[trackID] += uint64(entry.Duration)
} }
m.pts = newTime m.pts[trackID] = newTime
// important before moof.Len() // important before moof.Len()
run.Entries = append(run.Entries, entry) run.Entries = append(run.Entries, entry)
@@ -149,7 +248,7 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
m.fragIndex++ m.fragIndex++
m.total += moofLen + mdatLen //m.total += moofLen + mdatLen
return buf return buf
} }

164
pkg/mp4f/consumer.go Normal file
View File

@@ -0,0 +1,164 @@
package mp4f
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/mp4f"
"github.com/pion/rtp"
"time"
)
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
muxer *mp4f.Muxer
streams []av.CodecData
mimeType string
start bool
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264, ClockRate: 90000},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC, ClockRate: 16000},
},
},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
codec := track.Codec
trackID := int8(len(c.streams))
switch codec.Name {
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil
}
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
if !c.start {
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
ready, buf, _ := c.muxer.WritePacket(pkt, false)
if ready {
c.send += len(buf)
c.Fire(buf)
}
return nil
}
if !codec.IsMP4() {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
case streamer.CodecAAC:
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
c.mimeType += ",mp4a.40.2"
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if !c.start {
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
ready, buf, _ := c.muxer.WritePacket(pkt, false)
if ready {
c.send += len(buf)
c.Fire(buf)
}
return nil
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) MimeType() string {
return `video/mp4; codecs="` + c.mimeType + `"`
}
func (c *Consumer) Init() ([]byte, error) {
c.muxer = mp4f.NewMuxer(nil)
if err := c.muxer.WriteHeader(c.streams); err != nil {
return nil, err
}
_, data := c.muxer.GetInit(c.streams)
return data, nil
}
func (c *Consumer) Start() {
c.start = true
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MSE server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,
}
return json.Marshal(v)
}

View File

@@ -2,19 +2,26 @@ package rtmp
import ( import (
"encoding/base64" "encoding/base64"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/httpflv"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av" "github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser" "github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtmp" "github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp" "github.com/pion/rtp"
"strings"
"time" "time"
) )
// Conn for RTMP and RTMPT (flv over HTTP)
type Conn interface {
Streams() (streams []av.CodecData, err error)
ReadPacket() (pkt av.Packet, err error)
Close() (err error)
}
type Client struct { type Client struct {
streamer.Element streamer.Element
@@ -23,7 +30,7 @@ type Client struct {
medias []*streamer.Media medias []*streamer.Media
tracks []*streamer.Track tracks []*streamer.Track
conn *rtmp.Conn conn Conn
closed bool closed bool
receive int receive int
@@ -34,7 +41,12 @@ func NewClient(uri string) *Client {
} }
func (c *Client) Dial() (err error) { func (c *Client) Dial() (err error) {
if strings.HasPrefix(c.URI, "http") {
c.conn, err = httpflv.Dial(c.URI)
} else {
c.conn, err = rtmp.Dial(c.URI) c.conn, err = rtmp.Dial(c.URI)
}
if err != nil { if err != nil {
return return
} }
@@ -48,16 +60,20 @@ func (c *Client) Dial() (err error) {
for _, stream := range streams { for _, stream := range streams {
switch stream.Type() { switch stream.Type() {
case av.H264: case av.H264:
cd := stream.(h264parser.CodecData) info := stream.(h264parser.CodecData).RecordInfo
fmtp := "sprop-parameter-sets=" +
base64.StdEncoding.EncodeToString(cd.RecordInfo.SPS[0]) + "," + fmtp := fmt.Sprintf(
base64.StdEncoding.EncodeToString(cd.RecordInfo.PPS[0]) "profile-level-id=%02X%02X%02X;sprop-parameter-sets=%s,%s",
info.AVCProfileIndication, info.ProfileCompatibility, info.AVCLevelIndication,
base64.StdEncoding.EncodeToString(info.SPS[0]),
base64.StdEncoding.EncodeToString(info.PPS[0]),
)
codec := &streamer.Codec{ codec := &streamer.Codec{
Name: streamer.CodecH264, Name: streamer.CodecH264,
ClockRate: 90000, ClockRate: 90000,
FmtpLine: fmtp, FmtpLine: fmtp,
PayloadType: h264.PayloadTypeAVC, PayloadType: streamer.PayloadTypeMP4,
} }
media := &streamer.Media{ media := &streamer.Media{
@@ -67,26 +83,20 @@ func (c *Client) Dial() (err error) {
} }
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
track := &streamer.Track{ track := streamer.NewTrack(codec, media.Direction)
Codec: codec, Direction: media.Direction,
}
c.tracks = append(c.tracks, track) c.tracks = append(c.tracks, track)
case av.AAC: case av.AAC:
// TODO: fix support // TODO: fix support
cd := stream.(aacparser.CodecData) 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{ codec := &streamer.Codec{
Name: streamer.CodecAAC, Name: streamer.CodecAAC,
ClockRate: uint32(cd.Config.SampleRate), ClockRate: uint32(cd.Config.SampleRate),
Channels: uint16(cd.Config.ChannelConfig), Channels: uint16(cd.Config.ChannelConfig),
FmtpLine: fmtp, // a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
PayloadType: streamer.PayloadTypeMP4,
} }
media := &streamer.Media{ media := &streamer.Media{
@@ -96,9 +106,7 @@ func (c *Client) Dial() (err error) {
} }
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
track := &streamer.Track{ track := streamer.NewTrack(codec, media.Direction)
Codec: codec, Direction: media.Direction,
}
c.tracks = append(c.tracks, track) c.tracks = append(c.tracks, track)
default: default:
@@ -130,23 +138,15 @@ func (c *Client) Handle() (err error) {
track := c.tracks[int(pkt.Idx)] track := c.tracks[int(pkt.Idx)]
timestamp := uint32(pkt.Time / time.Duration(track.Codec.ClockRate)) // convert seconds to RTP timestamp
timestamp := uint32(pkt.Time * time.Duration(track.Codec.ClockRate) / time.Second)
var payloads [][]byte
if track.Codec.Name == streamer.CodecH264 {
payloads = splitAVC(pkt.Data)
} else {
payloads = [][]byte{pkt.Data}
}
for _, payload := range payloads {
packet := &rtp.Packet{ packet := &rtp.Packet{
Header: rtp.Header{Timestamp: timestamp}, Header: rtp.Header{Timestamp: timestamp},
Payload: payload, Payload: pkt.Data,
} }
_ = track.WriteRTP(packet) _ = track.WriteRTP(packet)
} }
}
} }
func (c *Client) Close() error { func (c *Client) Close() error {
@@ -156,21 +156,3 @@ func (c *Client) Close() error {
c.closed = true c.closed = true
return c.conn.Close() 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

@@ -32,7 +32,7 @@ func (c *Client) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{ v := map[string]interface{}{
streamer.JSONReceive: c.receive, streamer.JSONReceive: c.receive,
streamer.JSONType: "RTMP client producer", streamer.JSONType: "RTMP client producer",
streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(), //streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
"url": c.URI, "url": c.URI,
} }
for i, media := range c.medias { for i, media := range c.medias {

View File

@@ -7,6 +7,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/tcp"
@@ -43,7 +44,14 @@ const (
ModeServerConsumer ModeServerConsumer
) )
const KeepAlive = time.Second * 25 type State byte
const (
StateNone State = iota
StateConn
StateSetup
StatePlay
)
type Conn struct { type Conn struct {
streamer.Element streamer.Element
@@ -61,10 +69,11 @@ type Conn struct {
auth *tcp.Auth auth *tcp.Auth
conn net.Conn conn net.Conn
mode Mode
state State
reader *bufio.Reader reader *bufio.Reader
sequence int sequence int
uri string
mode Mode
tracks []*streamer.Track tracks []*streamer.Track
channels map[byte]*streamer.Track channels map[byte]*streamer.Track
@@ -76,24 +85,10 @@ type Conn struct {
} }
func NewClient(uri string) (*Conn, error) { func NewClient(uri string) (*Conn, error) {
var err error
c := new(Conn) 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.mode = ModeClientProducer
c.URL.User = nil c.uri = uri
return c, c.parseURI()
return c, nil
} }
func NewServer(conn net.Conn) *Conn { func NewServer(conn net.Conn) *Conn {
@@ -104,17 +99,29 @@ func NewServer(conn net.Conn) *Conn {
return c return c
} }
func (c *Conn) Dial() (err error) { func (c *Conn) parseURI() (err error) {
//if c.state != StateClientInit { c.URL, err = url.Parse(c.uri)
// panic("wrong state") if err != nil {
//} return err
if c.conn != nil && c.auth != nil {
c.auth.Reset()
} }
c.conn, err = net.DialTimeout( if strings.IndexByte(c.URL.Host, ':') < 0 {
"tcp", c.URL.Host, 10*time.Second, 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.conn != nil {
_ = c.parseURI()
}
c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
if err != nil { if err != nil {
return return
} }
@@ -136,6 +143,7 @@ func (c *Conn) Dial() (err error) {
} }
c.reader = bufio.NewReader(c.conn) c.reader = bufio.NewReader(c.conn)
c.state = StateConn
return nil return nil
} }
@@ -359,6 +367,24 @@ func (c *Conn) SetupMedia(
var res *tcp.Response var res *tcp.Response
res, err = c.Do(req) res, err = c.Do(req)
if err != nil { if err != nil {
// some Dahua/Amcrest cameras fail here because two simultaneous
// backchannel connections
if c.Backchannel {
c.Backchannel = false
if err := c.Dial(); err != nil {
return nil, err
}
if err := c.Describe(); err != nil {
return nil, err
}
for _, newMedia := range c.Medias {
if newMedia.Control == media.Control {
return c.SetupMedia(newMedia, newMedia.Codecs[0])
}
}
}
return nil, err return nil, err
} }
@@ -375,13 +401,17 @@ func (c *Conn) SetupMedia(
// we send our `interleaved`, but camera can answer with another // 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;interleaved=10-11;ssrc=10117CB7
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.123;source=192.168.10.12;interleaved=0 // Transport: RTP/AVP/TCP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1 // Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
s := res.Header.Get("Transport") s := res.Header.Get("Transport")
// TODO: rewrite // TODO: rewrite
if !strings.HasPrefix(s, "RTP/AVP/TCP;") { if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
// Escam Q6 has a bug:
// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1
if !strings.Contains(s, ";interleaved=") {
return nil, fmt.Errorf("wrong transport: %s", s) return nil, fmt.Errorf("wrong transport: %s", s)
} }
}
i := strings.Index(s, "interleaved=") i := strings.Index(s, "interleaved=")
if i < 0 { if i < 0 {
@@ -399,9 +429,7 @@ func (c *Conn) SetupMedia(
return nil, err return nil, err
} }
track := &streamer.Track{ track := streamer.NewTrack(codec, media.Direction)
Codec: codec, Direction: media.Direction,
}
switch track.Direction { switch track.Direction {
case streamer.DirectionSendonly: case streamer.DirectionSendonly:
@@ -414,44 +442,41 @@ func (c *Conn) SetupMedia(
track = c.bindTrack(track, byte(ch), codec.PayloadType) track = c.bindTrack(track, byte(ch), codec.PayloadType)
} }
c.state = StateSetup
c.tracks = append(c.tracks, track) c.tracks = append(c.tracks, track)
return track, nil return track, nil
} }
func (c *Conn) Play() (err error) { func (c *Conn) Play() (err error) {
if c.state != StateSetup {
return fmt.Errorf("RTSP PLAY from wrong state: %s", c.state)
}
req := &tcp.Request{Method: MethodPlay, URL: c.URL} req := &tcp.Request{Method: MethodPlay, URL: c.URL}
return c.Request(req) return c.Request(req)
} }
func (c *Conn) Teardown() (err error) { func (c *Conn) Teardown() (err error) {
//if c.state != StateClientPlay { // allow TEARDOWN from any state (ex. ANNOUNCE > SETUP)
// panic("wrong state")
//}
req := &tcp.Request{Method: MethodTeardown, URL: c.URL} req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
return c.Request(req) return c.Request(req)
} }
func (c *Conn) Close() error { func (c *Conn) Close() error {
if c.conn == nil { if c.state == StateNone {
return nil return nil
} }
if err := c.Teardown(); err != nil { if err := c.Teardown(); err != nil {
return err return err
} }
conn := c.conn c.state = StateNone
c.conn = nil return c.conn.Close()
return conn.Close()
} }
const transport = "RTP/AVP/TCP;unicast;interleaved=" const transport = "RTP/AVP/TCP;unicast;interleaved="
func (c *Conn) Accept() error { func (c *Conn) Accept() error {
//if c.state != StateServerInit {
// panic("wrong state")
//}
for { for {
req, err := tcp.ReadRequest(c.reader) req, err := tcp.ReadRequest(c.reader)
if err != nil { if err != nil {
@@ -492,9 +517,7 @@ func (c *Conn) Accept() error {
// TODO: fix someday... // TODO: fix someday...
c.channels = map[byte]*streamer.Track{} c.channels = map[byte]*streamer.Track{}
for i, media := range c.Medias { for i, media := range c.Medias {
track := &streamer.Track{ track := streamer.NewTrack(media.Codecs[0], media.Direction)
Codec: media.Codecs[0], Direction: media.Direction,
}
c.tracks = append(c.tracks, track) c.tracks = append(c.tracks, track)
c.channels[byte(i<<1)] = track c.channels[byte(i<<1)] = track
} }
@@ -554,8 +577,9 @@ func (c *Conn) Accept() error {
Request: req, Request: req,
} }
if tr[:len(transport)] == transport { if strings.HasPrefix(tr, transport) {
c.Session = "1" // TODO: fixme c.Session = "1" // TODO: fixme
c.state = StateSetup
res.Header.Set("Transport", tr[:len(transport)+3]) res.Header.Set("Transport", tr[:len(transport)+3])
} else { } else {
res.Status = "461 Unsupported transport" res.Status = "461 Unsupported transport"
@@ -576,17 +600,53 @@ func (c *Conn) Accept() error {
} }
func (c *Conn) Handle() (err error) { func (c *Conn) Handle() (err error) {
defer func() { if c.state != StateSetup {
if c.conn == nil { return fmt.Errorf("RTSP Handle from wrong state: %d", c.state)
err = nil
} }
//c.Fire(streamer.StateNull)
c.state = StatePlay
defer func() {
if c.state == StateNone {
err = nil
return
}
// may have gotten here because of the deadline
// so close the connection to stop keepalive
c.state = StateNone
_ = c.conn.Close()
}() }()
//c.Fire(streamer.StatePlaying) var timeout time.Duration
ts := time.Now().Add(KeepAlive)
switch c.mode {
case ModeClientProducer:
// polling frames from remote RTSP Server (ex Camera)
timeout = time.Second * 5
go c.keepalive()
case ModeServerProducer:
// polling frames from remote RTSP Client (ex FFmpeg)
timeout = time.Second * 15
case ModeServerConsumer:
// pushing frames to remote RTSP Client (ex VLC)
timeout = time.Second * 60
default:
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
}
for { for {
if c.state == StateNone {
return
}
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return
}
// we can read: // we can read:
// 1. RTP interleaved: `$` + 1B channel number + 2B size // 1. RTP interleaved: `$` + 1B channel number + 2B size
// 2. RTSP response: RTSP/1.0 200 OK // 2. RTSP response: RTSP/1.0 200 OK
@@ -598,22 +658,21 @@ func (c *Conn) Handle() (err error) {
} }
if buf4[0] != '$' { if buf4[0] != '$' {
if string(buf4) == "RTSP" { switch string(buf4) {
case "RTSP":
var res *tcp.Response var res *tcp.Response
res, err = tcp.ReadResponse(c.reader) if res, err = tcp.ReadResponse(c.reader); err != nil {
if err != nil {
return return
} }
c.Fire(res) c.Fire(res)
} else { case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
var req *tcp.Request var req *tcp.Request
req, err = tcp.ReadRequest(c.reader) if req, err = tcp.ReadRequest(c.reader); err != nil {
if err != nil {
return return
} }
c.Fire(req) c.Fire(req)
default:
return fmt.Errorf("RTSP wrong input")
} }
continue continue
} }
@@ -664,16 +723,19 @@ func (c *Conn) Handle() (err error) {
c.Fire(msg) c.Fire(msg)
} }
// keep-alive
now := time.Now()
if now.After(ts) {
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
// don't need to wait respose on this request
if err = c.Request(req); err != nil {
return err
} }
ts = now.Add(KeepAlive) }
func (c *Conn) keepalive() {
// TODO: rewrite to RTCP
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
for {
time.Sleep(time.Second * 25)
if c.state == StateNone {
return
}
if err := c.Request(req); err != nil {
return
} }
} }
} }
@@ -691,20 +753,16 @@ func (c *Conn) bindTrack(
track *streamer.Track, channel uint8, payloadType uint8, track *streamer.Track, channel uint8, payloadType uint8,
) *streamer.Track { ) *streamer.Track {
push := func(packet *rtp.Packet) error { push := func(packet *rtp.Packet) error {
if c.conn == nil { if c.state == StateNone {
return nil return nil
} }
packet.Header.PayloadType = payloadType packet.Header.PayloadType = payloadType
//packet.Header.PayloadType = 100
//packet.Header.PayloadType = 8
//packet.Header.PayloadType = 106
size := packet.MarshalSize() size := packet.MarshalSize()
data := make([]byte, 4+size) data := make([]byte, 4+size)
data[0] = '$' data[0] = '$'
data[1] = channel data[1] = channel
//data[1] = 10
binary.BigEndian.PutUint16(data[2:], uint16(size)) binary.BigEndian.PutUint16(data[2:], uint16(size))
if _, err := packet.MarshalTo(data[4:]); err != nil { if _, err := packet.MarshalTo(data[4:]); err != nil {
@@ -720,9 +778,15 @@ func (c *Conn) bindTrack(
return nil return nil
} }
if h264.IsAVC(track.Codec) { if track.Codec.IsMP4() {
switch track.Codec.Name {
case streamer.CodecH264:
wrapper := h264.RTPPay(1500) wrapper := h264.RTPPay(1500)
push = wrapper(push) push = wrapper(push)
case streamer.CodecAAC:
wrapper := aac.RTPPay(1500)
push = wrapper(push)
}
} }
return track.Bind(push) return track.Bind(push)

View File

@@ -2,6 +2,7 @@ package rtsp
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv" "strconv"
) )
@@ -19,6 +20,11 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
} }
} }
// can't setup new tracks from play state
if c.state == StatePlay {
return nil
}
track, err := c.SetupMedia(media, codec) track, err := c.SetupMedia(media, codec)
if err != nil { if err != nil {
return nil return nil
@@ -27,13 +33,16 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
} }
func (c *Conn) Start() error { func (c *Conn) Start() error {
if c.mode == ModeServerProducer { switch c.mode {
return nil case ModeClientProducer:
}
if err := c.Play(); err != nil { if err := c.Play(); err != nil {
return err return err
} }
case ModeServerProducer:
default:
return fmt.Errorf("start wrong mode: %d", c.mode)
}
return c.Handle() return c.Handle()
} }

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
}

View File

@@ -8,9 +8,19 @@ import (
// Server using same UDP port for SRTP and for SRTCP as the iPhone does // Server using same UDP port for SRTP and for SRTCP as the iPhone does
// this is not really necessary but anyway // this is not really necessary but anyway
type Server struct { type Server struct {
conn net.PacketConn
sessions map[uint32]*Session sessions map[uint32]*Session
} }
func (s *Server) Port() uint16 {
addr := s.conn.LocalAddr().(*net.UDPAddr)
return uint16(addr.Port)
}
func (s *Server) Close() error {
return s.conn.Close()
}
func (s *Server) AddSession(session *Session) { func (s *Server) AddSession(session *Session) {
if s.sessions == nil { if s.sessions == nil {
s.sessions = map[uint32]*Session{} s.sessions = map[uint32]*Session{}
@@ -23,6 +33,8 @@ func (s *Server) RemoveSession(session *Session) {
} }
func (s *Server) Serve(conn net.PacketConn) error { func (s *Server) Serve(conn net.PacketConn) error {
s.conn = conn
buf := make([]byte, 2048) buf := make([]byte, 2048)
for { for {
n, addr, err := conn.ReadFrom(buf) n, addr, err := conn.ReadFrom(buf)

View File

@@ -5,6 +5,7 @@ import (
"github.com/pion/rtcp" "github.com/pion/rtcp"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/srtp/v2" "github.com/pion/srtp/v2"
"time"
) )
type Session struct { type Session struct {
@@ -16,6 +17,14 @@ type Session struct {
Write func(b []byte) (int, error) Write func(b []byte) (int, error)
Track *streamer.Track Track *streamer.Track
lastSequence uint32
lastTimestamp uint32
//lastPacket *rtp.Packet
lastTime time.Time
jitter float64
//sequenceCycle uint16
totalLost uint32
} }
func (s *Session) SetKeys( func (s *Session) SetKeys(
@@ -37,13 +46,42 @@ func (s *Session) HandleRTP(data []byte) (err error) {
return return
} }
if s.Track == nil {
return
}
packet := &rtp.Packet{} packet := &rtp.Packet{}
if err = packet.Unmarshal(data); err != nil { if err = packet.Unmarshal(data); err != nil {
return return
} }
now := time.Now()
// https://www.ietf.org/rfc/rfc3550.txt
if s.lastTimestamp != 0 {
delta := packet.SequenceNumber - uint16(s.lastSequence)
// lost packet
if delta > 1 {
s.totalLost += uint32(delta - 1)
}
// D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si)
dTime := now.Sub(s.lastTime).Seconds()*float64(s.Track.Codec.ClockRate) -
float64(packet.Timestamp-s.lastTimestamp)
if dTime < 0 {
dTime = -dTime
}
// J(i) = J(i-1) + (|D(i-1,i)| - J(i-1))/16
s.jitter += (dTime - s.jitter) / 16
}
// keeping cycles (overflow)
s.lastSequence = s.lastSequence&0xFFFF0000 | uint32(packet.SequenceNumber)
s.lastTimestamp = packet.Timestamp
s.lastTime = now
_ = s.Track.WriteRTP(packet) _ = s.Track.WriteRTP(packet)
//s.Output(core.RTP{Channel: s.Channel, Packet: packet})
return return
} }
@@ -60,7 +98,6 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
} }
_ = packets _ = packets
//s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets})
if header.Type == rtcp.TypeSenderReport { if header.Type == rtcp.TypeSenderReport {
err = s.KeepAlive() err = s.KeepAlive()
@@ -70,9 +107,25 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
} }
func (s *Session) KeepAlive() (err error) { func (s *Session) KeepAlive() (err error) {
var data []byte
// we can send empty receiver response, but should send it to hold the connection
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC} rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
if s.lastTimestamp > 0 {
//log.Printf("[RTCP] ssrc=%d seq=%d lost=%d jit=%.2f", s.RemoteSSRC, s.lastSequence, s.totalLost, s.jitter)
rep.Reports = []rtcp.ReceptionReport{{
SSRC: s.RemoteSSRC,
LastSequenceNumber: s.lastSequence,
LastSenderReport: s.lastTimestamp,
FractionLost: 0, // TODO
TotalLost: s.totalLost,
Delay: 0, // send just after receive
Jitter: uint32(s.jitter),
}}
}
// we can send empty receiver response, but should send it to hold the connection
var data []byte
if data, err = rep.Marshal(); err != nil { if data, err = rep.Marshal(); err != nil {
return return
} }
@@ -90,8 +143,8 @@ func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
switch len(masterKey) { switch len(masterKey) {
case 16: case 16:
return srtp.ProtectionProfileAes128CmHmacSha1_80 return srtp.ProtectionProfileAes128CmHmacSha1_80
case 32: //case 32:
return srtp.ProtectionProfileAes256CmHmacSha1_80 // return srtp.ProtectionProfileAes256CmHmacSha1_80
} }
return 0 return 0
} }

View File

@@ -25,6 +25,7 @@ const (
CodecVP8 = "VP8" CodecVP8 = "VP8"
CodecVP9 = "VP9" CodecVP9 = "VP9"
CodecAV1 = "AV1" CodecAV1 = "AV1"
CodecJPEG = "JPEG" // payloadType: 26
CodecPCMU = "PCMU" // payloadType: 0 CodecPCMU = "PCMU" // payloadType: 0
CodecPCMA = "PCMA" // payloadType: 8 CodecPCMA = "PCMA" // payloadType: 8
@@ -32,13 +33,17 @@ const (
CodecOpus = "OPUS" // payloadType: 111 CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722" CodecG722 = "G722"
CodecMPA = "MPA" // payload: 14 CodecMPA = "MPA" // payload: 14
CodecELD = "ELD" // AAC-ELD
) )
const PayloadTypeMP4 byte = 255
func GetKind(name string) string { func GetKind(name string) string {
switch name { switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1: case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
return KindVideo return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA: case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA, CodecELD:
return KindAudio return KindAudio
} }
return "" return ""
@@ -74,13 +79,13 @@ func (m *Media) AV() bool {
return m.Kind == KindVideo || m.Kind == KindAudio return m.Kind == KindVideo || m.Kind == KindAudio
} }
func (m *Media) MatchCodec(codec *Codec) bool { func (m *Media) MatchCodec(codec *Codec) *Codec {
for _, c := range m.Codecs { for _, c := range m.Codecs {
if c.Match(codec) { if c.Match(codec) {
return true return c
} }
} }
return false return nil
} }
func (m *Media) MatchMedia(media *Media) *Codec { func (m *Media) MatchMedia(media *Media) *Codec {
@@ -126,20 +131,6 @@ type Codec struct {
PayloadType uint8 PayloadType uint8
} }
func NewCodec(name string) *Codec {
name = strings.ToUpper(name)
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
return &Codec{Name: name, ClockRate: 90000}
case CodecPCMU, CodecPCMA:
return &Codec{Name: name, ClockRate: 8000}
case CodecOpus:
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
}
panic(fmt.Sprintf("unsupported codec: %s", name))
}
func (c *Codec) String() string { func (c *Codec) String() string {
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate) s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
if c.Channels > 0 { if c.Channels > 0 {
@@ -148,6 +139,10 @@ func (c *Codec) String() string {
return s return s
} }
func (c *Codec) IsMP4() bool {
return c.PayloadType == PayloadTypeMP4
}
func (c *Codec) Clone() *Codec { func (c *Codec) Clone() *Codec {
clone := *c clone := *c
return &clone return &clone
@@ -194,13 +189,19 @@ func MarshalSDP(medias []*Media) ([]byte, error) {
} }
codec := media.Codecs[0] codec := media.Codecs[0]
name := codec.Name
if name == CodecELD {
name = CodecAAC
}
md := &sdp.MediaDescription{ md := &sdp.MediaDescription{
MediaName: sdp.MediaName{ MediaName: sdp.MediaName{
Media: media.Kind, Media: media.Kind,
Protos: []string{"RTP", "AVP"}, Protos: []string{"RTP", "AVP"},
}, },
} }
md.WithCodec(payloadType, codec.Name, codec.ClockRate, codec.Channels, codec.FmtpLine) md.WithCodec(payloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
sd.MediaDescriptions = append(sd.MediaDescriptions, md) sd.MediaDescriptions = append(sd.MediaDescriptions, md)
@@ -257,6 +258,7 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
} }
if c.Name == "" { if c.Name == "" {
// https://en.wikipedia.org/wiki/RTP_payload_formats
switch payloadType { switch payloadType {
case "0": case "0":
c.Name = CodecPCMU c.Name = CodecPCMU
@@ -267,6 +269,9 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
case "14": case "14":
c.Name = CodecMPA c.Name = CodecMPA
c.ClockRate = 44100 c.ClockRate = 44100
case "26":
c.Name = CodecJPEG
c.ClockRate = 90000
default: default:
c.Name = payloadType c.Name = payloadType
} }

View File

@@ -12,41 +12,60 @@ type WrapperFunc func(push WriterFunc) WriterFunc
type Track struct { type Track struct {
Codec *Codec Codec *Codec
Direction string Direction string
Sink map[*Track]WriterFunc sink map[*Track]WriterFunc
mx sync.Mutex sinkMu *sync.RWMutex
}
func NewTrack(codec *Codec, direction string) *Track {
return &Track{Codec: codec, Direction: direction, sinkMu: new(sync.RWMutex)}
} }
func (t *Track) String() string { func (t *Track) String() string {
s := t.Codec.String() s := t.Codec.String()
s += fmt.Sprintf(", sinks=%d", len(t.Sink)) t.sinkMu.RLock()
s += fmt.Sprintf(", sinks=%d", len(t.sink))
t.sinkMu.RUnlock()
return s return s
} }
func (t *Track) WriteRTP(p *rtp.Packet) error { func (t *Track) WriteRTP(p *rtp.Packet) error {
t.mx.Lock() t.sinkMu.RLock()
for _, f := range t.Sink { for _, f := range t.sink {
_ = f(p) _ = f(p)
} }
t.mx.Unlock() t.sinkMu.RUnlock()
return nil return nil
} }
func (t *Track) Bind(w WriterFunc) *Track { func (t *Track) Bind(w WriterFunc) *Track {
if t.Sink == nil { t.sinkMu.Lock()
t.Sink = map[*Track]WriterFunc{}
if t.sink == nil {
t.sink = map[*Track]WriterFunc{}
} }
clone := &Track{ clone := *t
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink, t.sink[&clone] = w
}
t.mx.Lock() t.sinkMu.Unlock()
t.Sink[clone] = w
t.mx.Unlock() return &clone
return clone
} }
func (t *Track) Unbind() { func (t *Track) Unbind() {
t.mx.Lock() t.sinkMu.Lock()
delete(t.Sink, t) delete(t.sink, t)
t.mx.Unlock() t.sinkMu.Unlock()
}
func (t *Track) GetSink(from *Track) {
t.sinkMu.Lock()
t.sink = from.sink
t.sinkMu.Unlock()
}
func (t *Track) HasSink() bool {
t.sinkMu.RLock()
defer t.sinkMu.RUnlock()
return len(t.sink) > 0
} }

View File

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

View File

@@ -86,10 +86,6 @@ func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
PayloadType: 98, //123, PayloadType: 98, //123,
}, },
// macOS Safari 15.1 // 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}, RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback},
PayloadType: 100, PayloadType: 100,

View File

@@ -1,7 +1,6 @@
package webrtc package webrtc
import ( import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
) )
@@ -58,8 +57,7 @@ func (c *Conn) Init() {
} }
} }
fmt.Printf("TODO: webrtc ontrack %+v\n", remote) //fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
fmt.Printf("TODO: webrtc ontrack %#v\n", remote)
}) })
// OK connection: // OK connection:
@@ -100,7 +98,24 @@ func (c *Conn) SetOffer(offer string) (err error) {
return return
} }
rawSDP := []byte(c.Conn.RemoteDescription().SDP) rawSDP := []byte(c.Conn.RemoteDescription().SDP)
c.medias, err = streamer.UnmarshalSDP(rawSDP) medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
return
}
// sort medias, so video will always be before audio
// and ignore application media from Hass default lovelace card
for _, media := range medias {
if media.Kind == streamer.KindVideo {
c.medias = append(c.medias, media)
}
}
for _, media := range medias {
if media.Kind == streamer.KindAudio {
c.medias = append(c.medias, media)
}
}
return return
} }

View File

@@ -3,6 +3,7 @@ package webrtc
import ( import (
"encoding/json" "encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
@@ -51,16 +52,26 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
return trackLocal.WriteRTP(packet) return trackLocal.WriteRTP(packet)
} }
if codec.Name == streamer.CodecH264 { switch codec.Name {
case streamer.CodecH264:
wrapper := h264.RTPPay(1200) wrapper := h264.RTPPay(1200)
push = wrapper(push) push = wrapper(push)
if h264.IsAVC(codec) { if codec.IsMP4() {
wrapper = h264.RepairAVC(track) wrapper = h264.RepairAVC(track)
} else { } else {
wrapper = h264.RTPDepay(track) 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) track = track.Bind(push)

View File

@@ -1,12 +1,15 @@
package webrtc package webrtc
import ( import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/ice/v2" "github.com/pion/ice/v2"
"github.com/pion/stun" "github.com/pion/stun"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"net" "net"
"strconv" "strconv"
"strings"
"time"
) )
func NewCandidate(address string) (string, error) { func NewCandidate(address string) (string, error) {
@@ -34,13 +37,47 @@ func NewCandidate(address string) (string, error) {
return "candidate:" + cand.Marshal(), nil return "candidate:" + cand.Marshal(), nil
} }
func LookupIP(address string) (string, error) {
if strings.HasPrefix(address, "stun:") {
ip, err := GetCachedPublicIP()
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 // GetPublicIP example from https://github.com/pion/stun
func GetPublicIP() (net.IP, error) { func GetPublicIP() (net.IP, error) {
c, err := stun.Dial("udp", "stun.l.google.com:19302") conn, err := net.Dial("udp", "stun.l.google.com:19302")
if err != nil { if err != nil {
return nil, err return nil, err
} }
c, err := stun.NewClient(conn)
if err != nil {
return nil, err
}
if err = conn.SetDeadline(time.Now().Add(time.Second * 3)); err != nil {
return nil, err
}
var res stun.Event var res stun.Event
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest) message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
@@ -63,6 +100,33 @@ func GetPublicIP() (net.IP, error) {
return xorAddr.IP, nil return xorAddr.IP, nil
} }
var cachedIP net.IP
var cachedTS time.Time
func GetCachedPublicIP() (net.IP, error) {
now := time.Now()
if now.After(cachedTS) {
newIP, err := GetPublicIP()
if err == nil {
cachedIP = newIP
cachedTS = now.Add(time.Minute * 5)
} else if cachedIP == nil {
return nil, err
}
}
return cachedIP, nil
}
func IsIP(host string) bool {
for _, i := range host {
if i >= 'A' {
return false
}
}
return true
}
func MimeType(codec *streamer.Codec) string { func MimeType(codec *streamer.Codec) string {
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case streamer.CodecH264:

View File

@@ -53,3 +53,6 @@ pc.ontrack = ev => {
- https://www.webrtc-experiment.com/DetectRTC/ - https://www.webrtc-experiment.com/DetectRTC/
- https://divtable.com/table-styler/ - https://divtable.com/table-styler/
- https://www.chromium.org/audio-video/ - https://www.chromium.org/audio-video/
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html

View File

@@ -45,7 +45,9 @@
'video/mp4; codecs="avc1.640032"', 'video/mp4; codecs="avc1.640032"',
'video/mp4; codecs="avc1.640C32"', 'video/mp4; codecs="avc1.640C32"',
'video/mp4; codecs="avc1.F4001F"', '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"); const video = document.createElement("video");

View File

@@ -69,6 +69,8 @@
// '<a href="video.html?src={name}">video</a>', // '<a href="video.html?src={name}">video</a>',
'<a href="api/stream.mp4?src={name}">mp4</a>', '<a href="api/stream.mp4?src={name}">mp4</a>',
'<a href="api/frame.mp4?src={name}">frame</a>', '<a href="api/frame.mp4?src={name}">frame</a>',
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
'<a href="api/streams?src={name}">info</a>', '<a href="api/streams?src={name}">info</a>',
]; ];

View File

@@ -3,115 +3,26 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - MSE</title> <title>go2rtc - MSE</title>
<script src="video-rtc.js"></script>
<style> <style>
body { body {
background: black;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
html, body { html, body, video {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
#video {
width: 100%;
height: 100%;
background: black;
}
</style> </style>
</head> </head>
<body> <body>
<!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video>
<script> <script>
const video = document.querySelector('#video'); const url = new URL("api/ws" + location.search, location.href);
const video = document.createElement("video-rtc");
// support api_path video.src = url.toString();
const baseUrl = location.origin + location.pathname.substr( document.body.appendChild(video);
0, location.pathname.lastIndexOf("/")
);
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
ws.binaryType = "arraybuffer";
let mediaSource;
ws.onopen = () => {
console.log("Start WS");
// https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.onsourceopen = () => {
console.debug("mediaSource.onsourceopen");
mediaSource.onsourceopen = null;
URL.revokeObjectURL(video.src);
ws.send(JSON.stringify({"type": "mse"}));
};
};
let sourceBuffer, queueBuffer = [];
ws.onmessage = ev => {
if (typeof ev.data === 'string') {
const data = JSON.parse(ev.data);
console.debug("ws.onmessage", data);
if (data.type === "mse") {
sourceBuffer = mediaSource.addSourceBuffer(data.value);
// important: segments supports TrackFragDecodeTime
// sequence supports only TrackFragRunEntry Duration
sourceBuffer.mode = "segments";
sourceBuffer.onupdateend = () => {
if (!sourceBuffer.updating && queueBuffer.length > 0) {
sourceBuffer.appendBuffer(queueBuffer.shift());
}
}
}
} else {
if (sourceBuffer.updating) {
queueBuffer.push(ev.data)
} else {
sourceBuffer.appendBuffer(ev.data);
}
}
}
let offsetTime = 1, noWaiting = 0;
setInterval(() => {
if (video.paused || video.seekable.length === 0) return;
if (noWaiting < 0) {
offsetTime = Math.min(offsetTime * 1.1, 5);
console.debug("offset time up:", offsetTime);
} else if (noWaiting >= 30) {
noWaiting = 0;
offsetTime = Math.max(offsetTime * 0.9, 0.5);
console.debug("offset time down:", offsetTime);
}
noWaiting += 1;
const endTime = video.seekable.end(video.seekable.length - 1);
let playbackRate = (endTime - video.currentTime) / offsetTime;
if (playbackRate < 0.1) {
// video.currentTime = endTime - offsetTime;
playbackRate = 0.1;
} else if (playbackRate > 10) {
// video.currentTime = endTime - offsetTime;
playbackRate = 10;
}
// https://github.com/GoogleChrome/developer.chrome.com/issues/135
video.playbackRate = playbackRate;
}, 1000);
video.onwaiting = () => {
const endTime = video.seekable.end(video.seekable.length - 1);
video.currentTime = endTime - offsetTime;
noWaiting = -1;
}
</script> </script>
</body> </body>
</html> </html>

374
www/video-rtc.js Normal file
View File

@@ -0,0 +1,374 @@
/**
* Common function for processing MSE and MSE2 data.
* @param ms MediaSource
*/
function MediaSourceHandler(ms) {
let sb, qb = [];
return ev => {
if (typeof ev.data === "string") {
const msg = JSON.parse(ev.data);
if (msg.type === "mse") {
if (!MediaSource.isTypeSupported(msg.value)) {
console.warn("Not supported: " + msg.value)
return;
}
sb = ms.addSourceBuffer(msg.value);
sb.mode = "segments"; // segments or sequence
sb.addEventListener("updateend", () => {
if (!sb.updating && qb.length > 0) {
try {
sb.appendBuffer(qb.shift());
} catch (e) {
// console.warn(e);
}
}
});
}
} else if (sb.updating || qb.length > 0) {
qb.push(ev.data);
// console.debug("buffer:", qb.length);
} else {
try {
sb.appendBuffer(ev.data);
} catch (e) {
// console.warn(e);
}
}
}
}
/**
* Dedicated Worker Handler for MSE2 https://chromestatus.com/feature/5177263249162240
*/
if (typeof importScripts == "function") {
// protect below code (class VideoRTC) from fail inside Worker
HTMLElement = Object;
customElements = {define: Function()};
const ms = new MediaSource();
ms.addEventListener("sourceopen", ev => {
postMessage({type: ev.type});
}, {once: true});
onmessage = MediaSourceHandler(ms);
postMessage({type: "handle", value: ms.handle}, [ms.handle]);
}
/**
* Video player for MSE and WebRTC connections.
*
* All modern web technologies are supported in almost any browser except Apple Safari.
*
* Support:
* - RTCPeerConnection for Safari iOS 11.0+
* - IntersectionObserver for Safari iOS 12.2+
* - MediaSource in Workers for Chrome 108+
*
* Doesn't support:
* - MediaSource for Safari iOS all
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
*/
class VideoRTC extends HTMLElement {
DISCONNECT_TIMEOUT = 5000;
RECONNECT_TIMEOUT = 30000;
CODECS = [
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen)
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC
"mp4a.40.5", // AAC HE
"mp4a.69", // MP3
"mp4a.6B", // MP3
];
/**
* Enable MediaSource in Workers mode.
* @type {boolean}
*/
MSE2 = true;
/**
* Run stream when not displayed on the screen. Default `false`.
* @type {boolean}
*/
background = false;
/**
* Run stream only when player in the viewport. Stop when user scroll out player.
* Value is percentage of visibility from `0` (not visible) to `1` (full visible).
* Default `0` - disable;
* @type {number}
*/
intersectionThreshold = 0;
/**
* Run stream only when browser page on the screen. Stop when user change browser
* tab or minimise browser windows.
* @type {boolean}
*/
visibilityCheck = true;
/**
* @type {HTMLVideoElement}
*/
video = null;
/**
* @type {RTCPeerConnection}
*/
pc = null;
/**
* @type {WebSocket}
*/
ws = null;
/**
* Internal WebSocket connection state. Values: CLOSED, CONNECTING, OPEN
* @type {number}
*/
wsState = WebSocket.CLOSED;
/**
* Internal WebSocket URL.
* @type {string}
*/
wsURL = "";
/**
* Internal disconnect TimeoutID.
* @type {number}
*/
disconnectTimeout = 0;
/**
* Internal reconnect TimeoutID.
* @type {number}
*/
reconnectTimeout = 0;
constructor() {
super();
console.debug("this.constructor");
this.video = document.createElement("video");
this.video.controls = true;
this.video.playsInline = true;
}
/** public properties **/
/**
* Set video source (WebSocket URL). Support relative path.
* @param value
*/
set src(value) {
if (value.startsWith("/")) {
value = "ws" + location.origin.substr(4) + value;
} else if (value.startsWith("http")) {
value = "ws" + value.substr(4);
}
this.wsURL = value;
if (this.isConnected) this.connectedCallback();
}
/**
* Play video. Support automute when autoplay blocked.
* https://developer.chrome.com/blog/autoplay/
*/
play() {
this.video.play().catch(er => {
if (er.name === "NotAllowedError" && !this.video.muted) {
this.video.muted = true;
this.video.play();
}
});
}
get codecs() {
return this.CODECS.filter(value => {
return MediaSource.isTypeSupported(`video/mp4; codecs="${value}"`);
}).join();
}
/**
* `CustomElement`. Invoked each time the custom element is appended into a
* document-connected element.
*/
connectedCallback() {
console.debug("this.connectedCallback", this.wsState);
if (this.disconnectTimeout) {
clearTimeout(this.disconnectTimeout);
this.disconnectTimeout = 0;
}
// because video autopause on disconnected from DOM
const seek = this.video.seekable;
if (seek.length > 0) {
this.video.currentTime = seek.end(seek.length - 1);
this.play();
}
if (!this.wsURL || this.wsState !== WebSocket.CLOSED) return;
// CLOSED => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.internalInit();
this.internalConnect();
}
/**
* `CustomElement`. Invoked each time the custom element is disconnected from the
* document's DOM.
*/
disconnectedCallback() {
console.debug("this.disconnectedCallback", this.wsState);
if (this.background || this.disconnectTimeout ||
this.wsState === WebSocket.CLOSED) return;
this.disconnectTimeout = setTimeout(() => {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = 0;
}
this.disconnectTimeout = 0;
// CONNECTING, OPEN => CLOSED
this.wsState = WebSocket.CLOSED;
if (this.ws) {
this.ws.close();
this.ws = null;
}
}, this.DISCONNECT_TIMEOUT);
}
internalInit() {
if (this.childElementCount) return;
this.appendChild(this.video);
if (this.background) return;
if ("hidden" in document && this.visibilityCheck) {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.disconnectedCallback();
} else if (this.isConnected) {
this.connectedCallback();
}
})
}
if ("IntersectionObserver" in window && this.intersectionThreshold) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) {
this.disconnectedCallback();
} else if (this.isConnected) {
this.connectedCallback();
}
});
}, {threshold: this.intersectionThreshold});
observer.observe(this);
}
}
internalConnect() {
if (this.wsState !== WebSocket.CONNECTING) return;
if (this.ws) throw "connect with non null WebSocket";
const ts = Date.now();
this.ws = new WebSocket(this.wsURL);
this.ws.binaryType = "arraybuffer";
this.ws.addEventListener("open", () => {
console.debug("ws.open", this.wsState);
if (this.wsState !== WebSocket.CONNECTING) return;
// CONNECTING => OPEN
this.wsState = WebSocket.OPEN;
});
this.ws.addEventListener("close", () => {
console.debug("ws.close", this.wsState);
if (this.wsState === WebSocket.CLOSED) return;
// CONNECTING, OPEN => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.ws = null;
// reconnect no more than once every X seconds
const delay = Math.max(this.RECONNECT_TIMEOUT - (Date.now() - ts), 0);
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = 0;
this.internalConnect();
}, delay);
});
if ("MediaSource" in window && this.MSE2) {
if (MediaSource.canConstructInDedicatedWorker) {
this.internalMSE2();
} else {
this.internalMSE();
}
}
// TODO: this.internalRTC();
}
internalMSE() {
console.debug("this.internalMSE");
this.ws.addEventListener("open", () => {
const ms = new MediaSource();
ms.addEventListener("sourceopen", () => {
URL.revokeObjectURL(this.video.src);
this.ws.send(JSON.stringify({type: "mse", value: this.codecs}));
}, {once: true});
this.video.src = URL.createObjectURL(ms);
this.play();
this.ws.addEventListener("message", MediaSourceHandler(ms));
});
}
internalMSE2() {
console.debug("this.internalMSE2");
const worker = new Worker("video-rtc.js");
worker.addEventListener("message", ev => {
if (ev.data.type === "handle") {
this.video.srcObject = ev.data.value;
this.play();
} else if (ev.data.type === "sourceopen") {
this.ws.send(JSON.stringify({type: "mse", value: this.codecs}));
}
});
this.ws.addEventListener("message", ev => {
if (typeof ev.data === "string") {
worker.postMessage(ev.data);
} else {
worker.postMessage(ev.data, [ev.data]);
}
});
this.ws.addEventListener("close", () => {
worker.terminate();
});
}
internalRTC() {
if (!("RTCPeerConnection" in window)) return; // macOS Desktop app
}
}
customElements.define("video-rtc", VideoRTC);

View File

@@ -25,12 +25,12 @@
<body> <body>
<video id="video" autoplay controls playsinline muted></video> <video id="video" autoplay controls playsinline muted></video>
<script> <script>
function init(stream) {
// support api_path
const baseUrl = location.origin + location.pathname.substr( const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/") 0, location.pathname.lastIndexOf("/")
); );
function init(stream) {
// support api_path
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`); const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
ws.onopen = () => { ws.onopen = () => {
console.debug('ws.onopen'); console.debug('ws.onopen');
@@ -51,11 +51,6 @@
pc.addIceCandidate({candidate: msg.value, sdpMid: ''}); pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
} else if (msg.type === 'webrtc/answer') { } else if (msg.type === 'webrtc/answer') {
pc.setRemoteDescription({type: 'answer', sdp: msg.value}); pc.setRemoteDescription({type: 'answer', sdp: msg.value});
pc.getTransceivers().forEach(t => {
if (t.receiver.track.kind === 'audio') {
t.currentDirection
}
})
} }
} }