Compare commits

...

127 Commits

Author SHA1 Message Date
Alexey Khit
4c2ebd20bc Update version to 0.1-rc.5 2022-12-06 12:19:29 +03:00
Alexey Khit
440c7bd6e1 Recover link to 2-way audio on Web UI 2022-12-06 12:15:22 +03:00
Alexey Khit
74c3510a10 Trace to log supported MP4 codecs 2022-12-06 10:51:42 +03:00
Alexey Khit
c2748fc77b Add check H264 High@5.1 for JS player 2022-12-06 10:48:42 +03:00
Alexey Khit
d334551591 Update main Web UI page 2022-12-06 01:34:24 +03:00
Alexey Khit
cfe20925ac Big rewrite JS player WebSocket processing 2022-12-05 23:54:40 +03:00
Alexey Khit
5b39f78ace Update error msg for fail codecs negotiation 2022-12-05 23:52:28 +03:00
Alexey Khit
b965c191b7 Adds errors output to API 2022-12-05 20:03:26 +03:00
Alexey Khit
7057b4846f Code refactoring 2022-12-05 00:47:46 +03:00
Alexey Khit
a746b96adc Remove old video.html file 2022-12-04 23:24:44 +03:00
Alexey Khit
b7718b33b8 Rewrite WS transport handler 2022-12-04 23:24:20 +03:00
Alexey Khit
69b17230f3 Collect producers codecs during negotiation 2022-12-04 23:16:34 +03:00
Alexey Khit
e2ecd909ab Update stream HTML page 2022-12-04 22:38:44 +03:00
Alexey Khit
ea79da0d53 Rename modes to mode in JS player 2022-12-04 22:38:21 +03:00
Alexey Khit
e64919838c Update MP4 over WebSocket support 2022-12-04 22:37:15 +03:00
Alexey Khit
162b11213d Allow JS player URL to http 2022-12-04 20:09:46 +03:00
Alexey Khit
d27acbd7e3 Fix MSE buffering 2022-12-04 18:38:15 +03:00
Alexey Khit
a692ecd7c1 Fix H265 for WebRTC in Safari 2022-12-03 11:44:26 +03:00
Alexey Khit
98c5366ba9 Fix supported codecs check in JS player 2022-12-03 09:39:45 +03:00
Alexey Khit
3eaaa3fcfa Add support URL as JS player src 2022-12-03 09:39:23 +03:00
Alexey Khit
7409b32836 Fix support old iOS version 2022-12-02 23:12:37 +03:00
Alexey Khit
e2cfdf8419 Support WebRTC, MSE, MSE2, MP4, MJPEG in JS player 2022-12-02 21:37:17 +03:00
Alexey Khit
4c0929d854 Support MP4 over WebSocket 2022-12-02 21:33:51 +03:00
Alexey Khit
258a0ffb91 Support MJPEG over WebSocket 2022-12-02 21:31:41 +03:00
Alexey Khit
999e81c2dd Code refactoring for webrtc candidates 2022-12-01 23:41:12 +03:00
Alexey Khit
8c6729027b Add feature to SETUP new RTSP tracks after PLAY 2022-12-01 23:37:11 +03:00
Alexey Khit
d3bd5eeab5 Added a blank payloader for MJPEG RTSP 2022-12-01 23:20:31 +03:00
Alexey Khit
dbbf2ea310 Update readme about Dahua bug 2022-12-01 14:43:13 +03:00
Alexey Khit
b8234e0c76 Update codecs table in readme 2022-12-01 13:46:59 +03:00
Alexey Khit
96cd753e27 Adds about RTSP server password to readme 2022-12-01 13:35:46 +03:00
Alexey Khit
c522e5bb08 Update docs about new sources 2022-12-01 13:02:29 +03:00
Alexey Khit
a16d8acc30 Add support HTTP JPEG and MJPEG sources 2022-12-01 13:01:48 +03:00
Alexey Khit
684878b4b1 Skip check RTSP auth for localhost requests 2022-11-29 21:04:38 +03:00
Alexey Khit
140a742cee Fix MSE2 check in JS 2022-11-27 10:22:58 +03:00
Alexey Khit
1b518b94fd Add support auth for RTSP server 2022-11-27 10:08:37 +03:00
Alexey Khit
5678121c50 Fix build for mac arm64 2022-11-27 09:57:27 +03:00
Alexey Khit
4915f12bde Add build for win arm64 2022-11-27 09:57:04 +03:00
Alexey Khit
bb91240b95 Remove unnecessary build scripts 2022-11-27 09:54:46 +03:00
Alexey Khit
b1d5d53832 Update to go 1.19 2022-11-27 09:53:11 +03:00
Alexey Khit
31fbbf91bb Remove websocket error on disconnect 2022-11-25 10:04:51 +03:00
Alexey Khit
a3f72fbab9 Fix websocket origin check with wrong port 2022-11-25 10:04:13 +03:00
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
95 changed files with 4693 additions and 2452 deletions

213
README.md
View File

@@ -9,6 +9,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- 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 from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS/HTTP](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg) - 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)
- first project in the World with support H265 for WebRTC in browser ([read more](https://github.com/AlexxIT/Blog/issues/5))
- 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
@@ -26,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)
@@ -127,7 +99,8 @@ 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
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot - [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
- [mjpeg](#module-mjpeg) - MJPEG Server
- [ffmpeg](#source-ffmpeg) - FFmpeg integration - [ffmpeg](#source-ffmpeg) - FFmpeg integration
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network) - [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
- [hass](#module-hass) - Home Assistant integration - [hass](#module-hass) - Home Assistant integration
@@ -141,7 +114,8 @@ 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` streams
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types) - [http](#source-http) - `HTTP-FLV`, `JPEG` (snapshots), `MJPEG` streams
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many 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 - [echo](#source-echo) - get stream link from bash or python
@@ -172,15 +146,39 @@ 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).
```yaml ```yaml
streams: streams:
rtmp_stream: rtmp://192.168.1.123/live/camera1 rtmp_stream: rtmp://192.168.1.123/live/camera1
``` ```
#### Source: HTTP
Support Content-Type:
- **HTTP-FLV** (`video/x-flv`) - same as RTMP, but over HTTP
- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
```yaml
streams:
# [HTTP-FLV] stream in video/x-flv format
http_flv: http://192.168.1.123:20880/api/camera/stream/780900131155/657617
# [JPEG] snapshots from Dahua camera, will be converted to MJPEG stream
dahua_snap: http://admin:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1
# [MJPEG] stream will be proxied without modification
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
```
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
#### Source: FFmpeg #### Source: FFmpeg
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream. You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
@@ -205,24 +203,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
@@ -277,6 +278,24 @@ 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 #### Source: Ivideon
@@ -304,6 +323,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/`.
@@ -317,13 +338,14 @@ 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
@@ -332,10 +354,15 @@ You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
- you can omit the codec filters, so one first video and one first audio will be selected - you can omit the codec filters, so one first video and one first audio will be selected
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected - you can set `?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
- you can enable external password protection for your RTSP streams
Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server)
```yaml ```yaml
rtsp: rtsp:
listen: ":8554" listen: ":8554" # RTSP Server TCP port, default - 8554
username: admin # optional, default - disabled
password: pass # optional, default - disabled
``` ```
### Module: WebRTC ### Module: WebRTC
@@ -385,13 +412,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:
@@ -491,20 +518,32 @@ Provides several features:
1. MSE stream (fMP4 over WebSocket) 1. MSE stream (fMP4 over WebSocket)
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/) 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 3. MP4 "file stream" - bad format for streaming because of high latency, doesn't work in Safari
### Module: MJPEG ### 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: **Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API.
You can receive an MJPEG stream in several ways:
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
- some cameras has HTTP link with [MJPEG stream](#source-http)
- some cameras has HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
With this example, your stream will have both H264 and MJPEG codecs:
```yaml ```yaml
streams: streams:
camera1: camera1:
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0 - rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
- ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=mjpeg - ffmpeg:camera1#video=mjpeg
``` ```
Example link to MJPEG: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1` API examples:
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
### Module: Log ### Module: Log
@@ -551,26 +590,70 @@ PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted m
`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. `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 | Device | WebRTC | MSE | MP4 |
-------|--------|-----|---- |---------------------|-------------|-------------|-------------|
*latency* | best | medium | bad | *latency* | best | medium | bad |
Desktop Chrome | H264 | H264, H265* | H264, H265* | Desktop Chrome 107+ | H264 | H264, H265* | H264, H265* |
Desktop Safari | H264, H265* | H264 | no | Desktop Edge | H264 | H264, H265* | H264, H265* |
Desktop Edge | H264 | H264, H265* | H264, H265* | Desktop Safari | H264, H265* | H264, H265 | **no!** |
Desktop Firefox | H264 | H264 | H264 | Desktop Firefox | H264 | H264 | H264 |
Desktop Opera | no | H264 | H264 | Android Chrome 107+ | H264 | H264, H265* | H264 |
iPhone Safari | H264, H265* | no | no | iPad Safari 13+ | H264, H265* | H264, H265 | **no!** |
iPad Safari | H264, H265* | H264 | no | iPhone Safari 13+ | H264, H265* | **no!** | **no!** |
Android Chrome | H264 | H264 | H264 | masOS Hass App | no | no | no |
masOS Hass App | no | no | no
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2` - Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- MSE/MP4 audio codecs: not supported yet (should be: `AAC`)
- Chrome H265: [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/) - 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 - Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265
- iOS Safari H265: Settings > Safari > Advanced > 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

@@ -4,8 +4,6 @@ import (
"encoding/json" "encoding/json"
"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/pkg/streamer"
"github.com/gorilla/websocket"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"net" "net"
"net/http" "net/http"
@@ -17,6 +15,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,7 +33,7 @@ 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)
@@ -48,16 +47,18 @@ 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() { if log.Trace().Enabled() {
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.Handler = middlewareLog(s.Handler)
log.Trace().Stringer("url", r.URL).Msgf("[api] %s", r.Method)
http.DefaultServeMux.ServeHTTP(w, r)
})
} }
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")
} }
@@ -75,13 +76,25 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(pattern, handler) http.HandleFunc(pattern, handler)
} }
func HandleWS(msgType string, handler WSHandler) { const StreamNotFound = "stream not found"
wsHandlers[msgType] = handler
}
var basePath string var basePath string
var log zerolog.Logger var log zerolog.Logger
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")
@@ -111,29 +124,3 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
e.SetIndent("", " ") e.SetIndent("", " ")
_ = e.Encode(v) _ = e.Encode(v)
} }
func apiWS(w http.ResponseWriter, r *http.Request) {
ctx := new(Context)
if err := ctx.Upgrade(w, r); err != nil {
log.Error().Err(err).Msg("[api.ws] upgrade")
return
}
defer ctx.Close()
for {
msg := new(streamer.Message)
if err := ctx.Conn.ReadJSON(msg); err != nil {
if websocket.IsUnexpectedCloseError(
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure,
) {
log.Error().Err(err).Msg("[api.ws] readJSON")
}
return
}
handler := wsHandlers[msg.Type]
if handler != nil {
handler(ctx, msg)
}
}
}

View File

@@ -1,7 +1,6 @@
package api package api
import ( import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"net/http" "net/http"
"net/url" "net/url"
@@ -9,11 +8,29 @@ import (
"sync" "sync"
) )
func initWS() { // Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
Value interface{} `json:"value,omitempty"`
}
type WSHandler func(tr *Transport, msg *Message) error
func HandleWS(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
var wsHandlers = make(map[string]WSHandler)
func initWS(origin string) {
wsUp = &websocket.Upgrader{ wsUp = &websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 512000, WriteBufferSize: 512000,
} }
switch origin {
case "":
// same origin + ignore port
wsUp.CheckOrigin = func(r *http.Request) bool { wsUp.CheckOrigin = func(r *http.Request) bool {
origin := r.Header["Origin"] origin := r.Header["Origin"]
if len(origin) == 0 { if len(origin) == 0 {
@@ -26,68 +43,96 @@ func initWS() {
if o.Host == r.Host { if o.Host == r.Host {
return true return true
} }
log.Trace().Msgf("[api.ws] origin: %s, host: %s", o.Host, r.Host) log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
// some users change Nginx external port using Docker port // https://github.com/AlexxIT/go2rtc/issues/118
// so origin will be with a port and host without
if i := strings.IndexByte(o.Host, ':'); i > 0 { if i := strings.IndexByte(o.Host, ':'); i > 0 {
return o.Host[:i] == r.Host return o.Host[:i] == r.Host
} }
return false return false
} }
case "*":
// any origin
wsUp.CheckOrigin = func(r *http.Request) bool {
return true
}
}
}
func apiWS(w http.ResponseWriter, r *http.Request) {
ws, err := wsUp.Upgrade(w, r, nil)
if err != nil {
origin := r.Header.Get("Origin")
log.Error().Err(err).Caller().Msgf("host=%s origin=%s", r.Host, origin)
return
}
tr := &Transport{Request: r}
tr.OnWrite(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
_ = ws.WriteMessage(websocket.BinaryMessage, data)
} else {
_ = ws.WriteJSON(msg)
}
})
for {
msg := new(Message)
if err = ws.ReadJSON(msg); err != nil {
log.Trace().Err(err).Caller().Send()
_ = ws.Close()
break
}
if handler := wsHandlers[msg.Type]; handler != nil {
go func() {
if err = handler(tr, msg); err != nil {
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
}
}()
}
}
tr.Close()
} }
var wsUp *websocket.Upgrader var wsUp *websocket.Upgrader
type WSHandler func(ctx *Context, msg *streamer.Message) type Transport struct {
type Context struct {
Conn *websocket.Conn
Request *http.Request Request *http.Request
Consumer interface{} // TODO: rewrite Consumer interface{} // TODO: rewrite
mx sync.Mutex
onChange func()
onWrite func(msg interface{})
onClose []func() onClose []func()
mu sync.Mutex
} }
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) { func (t *Transport) OnWrite(f func(msg interface{})) {
ctx.Conn, err = wsUp.Upgrade(w, r, nil) t.mx.Lock()
ctx.Request = r if t.onChange != nil {
return t.onChange()
}
t.onWrite = f
t.mx.Unlock()
} }
func (ctx *Context) Close() { func (t *Transport) Write(msg interface{}) {
for _, f := range ctx.onClose { t.mx.Lock()
t.onWrite(msg)
t.mx.Unlock()
}
func (t *Transport) Close() {
for _, f := range t.onClose {
f() f()
} }
_ = ctx.Conn.Close()
} }
func (ctx *Context) Write(msg interface{}) { func (t *Transport) OnChange(f func()) {
ctx.mu.Lock() t.onChange = f
defer ctx.mu.Unlock()
var err error
switch msg := msg.(type) {
case *streamer.Message:
err = ctx.Conn.WriteJSON(msg)
case []byte:
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
default:
return
}
if err != nil {
//panic(err) // TODO: fix panic
}
} }
func (ctx *Context) Error(err error) { func (t *Transport) OnClose(f func()) {
ctx.Write(&streamer.Message{ t.onClose = append(t.onClose, f)
Type: "error", Value: err.Error(),
})
}
func (ctx *Context) OnClose(f func()) {
ctx.onClose = append(ctx.onClose, f)
} }

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.5"
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

@@ -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"),

View File

@@ -4,6 +4,7 @@ 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"
@@ -14,6 +15,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"sync"
"time" "time"
) )
@@ -23,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) {
@@ -55,13 +57,22 @@ func Handle(url string) (streamer.Producer, error) {
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")
@@ -72,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
@@ -86,4 +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

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
``` ```
@@ -41,3 +55,7 @@
- https://trac.ffmpeg.org/wiki/DirectShow - https://trac.ffmpeg.org/wiki/DirectShow
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table - https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
- https://github.com/tuupola/esp_video/blob/master/README.md - 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

@@ -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,40 +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",
"mjpeg": "-codec:v mjpeg -force_duplicated_matrix 1 -huffman 0 -pix_fmt yuvj420p", "mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
"opus": "-codec:a libopus -ar 48000 -ac 2", "opus": "-c:a libopus -ar:a 48000 -ac:a 2",
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1", "pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1", "pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
"pcmu/48000": "-codec:a pcm_mulaw -ar 48000 -ac 1", "pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
"pcma": "-codec:a pcm_alaw -ar 8000 -ac 1", "pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
"pcma/16000": "-codec:a pcm_alaw -ar 16000 -ac 1", "pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
"pcma/48000": "-codec:a pcm_alaw -ar 48000 -ac 1", "pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
"aac/16000": "-codec:a aac -ar 16000 -ac 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
@@ -69,7 +79,7 @@ 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", "rtmp": case "http", "https", "rtmp":
input = strings.Replace(tpl["http"], "{input}", s, 1) input = strings.Replace(tpl["http"], "{input}", s, 1)
@@ -86,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 {
@@ -99,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:a 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"

View File

@@ -55,6 +55,10 @@ func initAPI() {
// /stream/{id}/channel/0/webrtc // /stream/{id}/channel/0/webrtc
default: default:
i := strings.IndexByte(r.RequestURI[8:], '/') i := strings.IndexByte(r.RequestURI[8:], '/')
if i <= 0 {
log.Warn().Msgf("wrong request: %s", r.RequestURI)
return
}
name := r.RequestURI[8 : 8+i] name := r.RequestURI[8 : 8+i]
stream := streams.Get(name) stream := streams.Get(name)

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,89 +54,80 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
items = append(items, device) items = append(items, device)
} }
_= json.NewEncoder(w).Encode(items) _ = json.NewEncoder(w).Encode(items)
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"
@@ -20,20 +21,12 @@ func Init() {
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
}

56
cmd/http/http.go Normal file
View File

@@ -0,0 +1,56 @@
package http
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net/http"
"strings"
)
func Init() {
streams.HandleFunc("http", handle)
streams.HandleFunc("https", handle)
}
func handle(url string) (streamer.Producer, error) {
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
res, err := tcp.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(res.Status)
}
ct := res.Header.Get("Content-Type")
if i := strings.IndexByte(ct, ';'); i > 0 {
ct = ct[:i]
}
switch ct {
case "image/jpeg", "multipart/x-mixed-replace":
return mjpeg.NewClient(res), nil
case "video/x-flv":
var conn *rtmp.Client
if conn, err = rtmp.Accept(res); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
}
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
}

View File

@@ -1,6 +1,7 @@
package mjpeg package mjpeg
import ( import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/mjpeg"
@@ -10,32 +11,76 @@ import (
) )
func Init() { func Init() {
api.HandleFunc("api/stream.mjpeg", handler) api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleWS("mjpeg", handlerWS)
}
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
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)
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(data)))
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if _, err := w.Write(data); err != nil {
log.Error().Err(err).Caller().Send()
}
} }
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
func handler(w http.ResponseWriter, r *http.Request) { func handlerStream(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src) stream := streams.GetOrNew(src)
if stream == nil { if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return return
} }
exit := make(chan struct{}) flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{} cons := &mjpeg.Consumer{}
cons.Listen(func(msg interface{}) { cons.Listen(func(msg interface{}) {
switch msg := msg.(type) { switch msg := msg.(type) {
case []byte: case []byte:
data := []byte(header + strconv.Itoa(len(msg))) data := []byte(header + strconv.Itoa(len(msg)))
data = append(data, 0x0D, 0x0A, 0x0D, 0x0A) data = append(data, '\r', '\n', '\r', '\n')
data = append(data, msg...) data = append(data, msg...)
data = append(data, 0x0D, 0x0A) data = append(data, '\r', '\n')
if _, err := w.Write(data); err != nil { // Chrome bug: mjpeg image always shows the second to last image
exit <- struct{}{} // https://bugs.chromium.org/p/chromium/issues/detail?id=527446
} _, _ = w.Write(data)
flusher.Flush()
} }
}) })
@@ -44,11 +89,41 @@ func handler(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", `multipart/x-mixed-replace; boundary=frame`) h := w.Header()
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
<-exit <-r.Context().Done()
stream.RemoveConsumer(cons) stream.RemoveConsumer(cons)
//log.Trace().Msg("[api.mjpeg] close") //log.Trace().Msg("[api.mjpeg] close")
} }
func handlerWS(tr *api.Transport, _ *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}

View File

@@ -9,12 +9,14 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
) )
func Init() { func Init() {
log = app.GetLogger("mp4") log = app.GetLogger("mp4")
api.HandleWS(MsgTypeMSE, handlerWS) api.HandleWS("mse", handlerWSMSE)
api.HandleWS("mp4", handlerWSMP4)
api.HandleFunc("api/frame.mp4", handlerKeyframe) api.HandleFunc("api/frame.mp4", handlerKeyframe)
api.HandleFunc("api/stream.mp4", handlerMP4) api.HandleFunc("api/stream.mp4", handlerMP4)
@@ -30,70 +32,66 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src) stream := streams.GetOrNew(src)
if stream == nil { if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return return
} }
exit := make(chan []byte) exit := make(chan []byte)
cons := &mp4.Consumer{} cons := &mp4.Segment{OnlyKeyframe: true}
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
} }
defer stream.RemoveConsumer(cons) data := <-exit
w.Header().Set("Content-Type", cons.MimeType()) stream.RemoveConsumer(cons)
data, err := cons.Init()
if err != nil {
log.Error().Err(err).Msg("[api.keyframe] init")
return
}
data = append(data, <-exit...)
// Apple Safari won't show frame without length // 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("[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.GetOrNew(src) stream := streams.GetOrNew(src)
if stream == nil { if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
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
} }
@@ -103,18 +101,36 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
data, err := cons.Init() data, err := cons.Init()
if err != nil { if err != nil {
log.Error().Err(err).Msg("[api.mp4] init") log.Error().Err(err).Caller().Send()
return return
} }
if _, err = w.Write(data); err != nil { if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.mp4] write") log.Error().Err(err).Caller().Send()
return return
} }
<-exit cons.Start()
log.Trace().Msg("[api.mp4] close") 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

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

133
cmd/mp4/ws.go Normal file
View File

@@ -0,0 +1,133 @@
package mp4
import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
const packetSize = 8192
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Consumer{}
cons.UserAgent = tr.Request.UserAgent()
cons.RemoteAddr = tr.Request.RemoteAddr
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
cons.Medias = parseMedias(codecs, true)
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
for len(data) > packetSize {
tr.Write(data[:packetSize])
data = data[packetSize:]
}
tr.Write(data)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Caller().Send()
return err
}
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
tr.Write(&api.Message{Type: "mse", Value: cons.MimeType()})
data, err := cons.Init()
if err != nil {
log.Warn().Err(err).Caller().Send()
return err
}
tr.Write(data)
cons.Start()
return nil
}
func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
src := tr.Request.URL.Query().Get("src")
stream := streams.GetOrNew(src)
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Segment{}
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
cons.Medias = parseMedias(codecs, false)
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
tr.Write(data)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return err
}
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}
func parseMedias(codecs string, parseAudio bool) (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 && parseAudio {
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
}

View File

@@ -15,5 +15,8 @@ func handle(url string) (streamer.Producer, error) {
if err := conn.Dial(); err != nil { if err := conn.Dial(); err != nil {
return nil, err return nil, err
} }
if err := conn.Describe(); err != nil {
return nil, err
}
return conn, nil return conn, nil
} }

View File

@@ -15,6 +15,8 @@ func Init() {
var conf struct { var conf struct {
Mod struct { Mod struct {
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
} `yaml:"rtsp"` } `yaml:"rtsp"`
} }
@@ -32,20 +34,49 @@ 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
} }
c := rtsp.NewServer(conn)
// skip check auth for localhost
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
c.Auth(conf.Mod.Username, conf.Mod.Password)
}
go tcpHandler(c)
}
}()
}
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 backchannel := true
@@ -62,6 +93,8 @@ func rtspHandler(url string) (streamer.Producer, error) {
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) {
@@ -84,10 +117,10 @@ func rtspHandler(url string) (streamer.Producer, error) {
} }
// 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
} }
@@ -96,24 +129,12 @@ func rtspHandler(url string) (streamer.Producer, error) {
return conn, nil return conn, nil
} }
func worker(address string) { func tcpHandler(conn *rtsp.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.Listen(func(msg interface{}) { conn.Listen(func(msg interface{}) {
if trace { if trace {
switch msg := msg.(type) { switch msg := msg.(type) {
@@ -126,45 +147,49 @@ func worker(address string) {
switch msg { switch msg {
case rtsp.MethodDescribe: case rtsp.MethodDescribe:
name = conn.URL.Path[1:] if len(conn.URL.Path) == 0 {
log.Warn().Msg("[rtsp] server empty URL on DESCRIBE")
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
stream := streams.Get(name) // TODO: rewrite
if stream == nil {
return return
} }
initMedias(conn)
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
}
onDisconnect = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if OnProducer != nil {
if OnProducer(conn) {
return
}
}
name = conn.URL.Path[1:] name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
stream := streams.Get(name) stream := streams.Get(name)
if stream == nil { if stream == nil {
return return
} }
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
initMedias(conn)
if err := stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
}
closer = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if len(conn.URL.Path) == 0 {
log.Warn().Msg("[rtsp] server empty URL on ANNOUNCE")
return
}
name = conn.URL.Path[1:]
stream := streams.Get(name)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
stream.AddProducer(conn) stream.AddProducer(conn)
onDisconnect = func() { closer = func() {
stream.RemoveProducer(conn) stream.RemoveProducer(conn)
} }
@@ -173,24 +198,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) {
@@ -198,16 +228,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

@@ -4,6 +4,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings" "strings"
"sync" "sync"
"time"
) )
type state byte type state byte
@@ -13,6 +14,7 @@ const (
stateMedias stateMedias
stateTracks stateTracks
stateStart stateStart
stateExternal
) )
type Producer struct { type Producer struct {
@@ -25,7 +27,8 @@ type Producer struct {
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) { func (p *Producer) SetSource(s string) {
@@ -36,16 +39,16 @@ func (p *Producer) SetSource(s string) {
} }
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
} }
@@ -56,59 +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 func() { go func() {
// safe read element while mu locked
if err := p.element.Start(); err != nil { if err := p.element.Start(); err != nil {
log.Warn().Err(err).Str("url", p.url).Msg("[streams] start") 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

@@ -3,7 +3,10 @@ package streams
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
) )
type Consumer struct { type Consumer struct {
@@ -14,6 +17,7 @@ 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 {
@@ -51,18 +55,23 @@ 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
var codecs string
// 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)
collectCodecs(prodMedia, &codecs)
// Step 3. Match consumer/producer codecs list // Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia) prodCodec := prodMedia.MatchMedia(consMedia)
@@ -81,20 +90,28 @@ 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") if len(codecs) > 0 {
return errors.New("codecs not match: " + codecs)
} else {
return fmt.Errorf("sources unavailable: %d", len(s.producers))
}
} }
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()
} }
@@ -102,12 +119,8 @@ 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 == nil {
log.Warn().Msgf("empty consumer: %+v\n", s)
continue
}
if consumer.element == cons { if consumer.element == cons {
// remove consumer pads from all producers // remove consumer pads from all producers
for _, track := range consumer.tracks { for _, track := range consumer.tracks {
@@ -118,55 +131,60 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
break break
} }
} }
s.mu.Unlock()
for _, producer := range s.producers { s.stopProducers()
if producer == nil {
log.Warn().Msgf("empty producer: %+v\n", s)
continue
}
var sink bool
for _, track := range producer.tracks {
if len(track.Sink) > 0 {
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)
@@ -176,6 +194,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
} }
@@ -207,3 +226,19 @@ func (s *Stream) removeProducer(i int) {
s.producers = append(s.producers[:i], s.producers[i+1:]...) s.producers = append(s.producers[:i], s.producers[i+1:]...)
} }
} }
func collectCodecs(media *streamer.Media, codecs *string) {
for _, codec := range media.Codecs {
name := codec.Name
if name == streamer.CodecAAC {
name = "AAC"
}
if strings.Contains(*codecs, name) {
continue
}
if len(*codecs) > 0 {
*codecs += ","
}
*codecs += name
}
}

View File

@@ -2,7 +2,6 @@ package webrtc
import ( import (
"github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/api"
"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"
) )
@@ -13,7 +12,27 @@ func AddCandidate(address string) {
candidates = append(candidates, address) candidates = append(candidates, address)
} }
func addCanditates(answer string) (string, error) { func asyncCandidates(tr *api.Transport) {
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")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
}
}
func syncCanditates(answer string) (string, error) {
if len(candidates) == 0 { if len(candidates) == 0 {
return answer, nil return answer, nil
} }
@@ -59,12 +78,14 @@ func addCanditates(answer string) (string, error) {
return string(data), nil return string(data), nil
} }
func candidateHandler(ctx *api.Context, msg *streamer.Message) { func candidateHandler(tr *api.Transport, msg *api.Message) error {
if ctx.Consumer == nil { if tr.Consumer == nil {
return return nil
} }
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil { if conn := tr.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote") s := msg.Value.(string)
conn.Push(msg) log.Trace().Str("candidate", s).Msg("[webrtc] remote")
conn.AddCandidate(s)
} }
return nil
} }

View File

@@ -1,14 +1,16 @@
package webrtc package webrtc
import ( import (
"errors"
"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/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"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"
"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/offer", asyncHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler) api.HandleWS("webrtc/candidate", candidateHandler)
api.HandleFunc("api/webrtc", syncHandler)
} }
var Port string var Port string
@@ -62,11 +66,11 @@ 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(tr *api.Transport, msg *api.Message) error {
src := ctx.Request.URL.Query().Get("src") src := tr.Request.URL.Query().Get("src")
stream := streams.Get(src) stream := streams.Get(src)
if stream == nil { if stream == nil {
return return errors.New(api.StreamNotFound)
} }
log.Debug().Str("url", src).Msg("[webrtc] new consumer") log.Debug().Str("url", src).Msg("[webrtc] new consumer")
@@ -77,21 +81,23 @@ 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().Send()
return return err
} }
conn.UserAgent = ctx.Request.UserAgent() conn.UserAgent = tr.Request.UserAgent()
conn.Listen(func(msg interface{}) { conn.Listen(func(msg interface{}) {
switch msg := msg.(type) { switch msg := msg.(type) {
case pion.PeerConnectionState: case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed { if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn) stream.RemoveConsumer(conn)
} }
case *streamer.Message: case *pion.ICECandidate:
// subscribe on webrtc server candidates if msg != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] local") s := msg.ToJSON().Candidate
ctx.Write(msg) log.Trace().Str("candidate", s).Msg("[webrtc] local")
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
}
} }
}) })
@@ -100,41 +106,61 @@ 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().Send()
ctx.Error(err) return err
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().Send()
_ = conn.Conn.Close() _ = conn.Conn.Close()
ctx.Error(err) return err
return
} }
conn.Init() conn.Init()
// exchange sdp without waiting all candidates // 3. 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().Send()
ctx.Error(err) return err
}
tr.Consumer = conn
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
asyncCandidates(tr)
return nil
}
func syncHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("src")
stream := streams.Get(url)
if stream == nil {
return return
} }
ctx.Write(&streamer.Message{ // get offer
Type: webrtc.MsgTypeAnswer, Value: answer, offer, err := io.ReadAll(r.Body)
}) if err != nil {
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
return
}
ctx.Consumer = conn 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 +170,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 +188,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 +205,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")
}

14
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/AlexxIT/go2rtc module github.com/AlexxIT/go2rtc
go 1.17 go 1.19
require ( require (
github.com/brutella/hap v0.0.17 github.com/brutella/hap v0.0.17
@@ -26,6 +26,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi v1.5.4 // indirect github.com/go-chi/chi v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.50 // indirect github.com/miekg/dns v1.1.50 // indirect
@@ -41,17 +42,18 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
golang.org/x/mod v0.4.2 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect golang.org/x/tools v0.1.11 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
) )
replace ( 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
// fix reading AAC config bytes
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
) )

64
go.sum
View File

@@ -1,21 +1,19 @@
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/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/brutella/dnssd v1.2.3 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/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepch/vdk v0.0.19 h1:r6xYyBTtXEIEh+csO0XHT00sI7xLF+hQFkJE9/go5II=
github.com/deepch/vdk v0.0.19/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -29,7 +27,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -37,13 +34,12 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
@@ -54,74 +50,49 @@ github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7Xn
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E= github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ= github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c= github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY= github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig= github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE= github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs= github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8= github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U= github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA= github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU= github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
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/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 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA= 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.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA= github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg= github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw= github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
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/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=
@@ -132,7 +103,6 @@ github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -145,28 +115,21 @@ github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw= github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs= golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -190,13 +153,11 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cO
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -210,8 +171,9 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -222,12 +184,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -242,9 +204,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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=

View File

@@ -9,6 +9,7 @@ import (
"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/http"
"github.com/AlexxIT/go2rtc/cmd/ivideon" "github.com/AlexxIT/go2rtc/cmd/ivideon"
"github.com/AlexxIT/go2rtc/cmd/mjpeg" "github.com/AlexxIT/go2rtc/cmd/mjpeg"
"github.com/AlexxIT/go2rtc/cmd/mp4" "github.com/AlexxIT/go2rtc/cmd/mp4"
@@ -40,6 +41,7 @@ func main() {
webrtc.Init() webrtc.Init()
mp4.Init() mp4.Init()
mjpeg.Init() mjpeg.Init()
http.Init()
srtp.Init() srtp.Init()
homekit.Init() homekit.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

@@ -6,12 +6,6 @@ import (
"github.com/pion/rtp" "github.com/pion/rtp"
) )
const PayloadTypeAVC = 255
func IsAVC(codec *streamer.Codec) bool {
return codec.PayloadType == PayloadTypeAVC
}
func EncodeAVC(nals ...[]byte) (avc []byte) { func EncodeAVC(nals ...[]byte) (avc []byte) {
var i, n int var i, n int

View File

@@ -8,11 +8,12 @@ import (
) )
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 {

View File

@@ -19,11 +19,7 @@ func RTPDepay(track *streamer.Track) 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 {
//fmt.Printf( //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)
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v\n",
// track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
//)
payload, err := depack.Unmarshal(packet.Payload) payload, err := depack.Unmarshal(packet.Payload)
if len(payload) == 0 || err != nil { if len(payload) == 0 || err != nil {
@@ -40,11 +36,13 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
} }
if len(buf) == 0 { if len(buf) == 0 {
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) { switch NALUType(payload) {
case NALUTypeIFrame: case NALUTypeIFrame:
// fix IFrame without SPS,PPS // fix IFrame without SPS,PPS
buf = append(buf, ps...) buf = append(buf, ps...)
case NALUTypeSEI: case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame // fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload)) i := int(4 + binary.BigEndian.Uint32(payload))
@@ -72,10 +70,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf = buf[:0] buf = buf[:0]
} }
//fmt.Printf( //log.Printf("[AVC] %v, len: %d", Types(payload), len(payload))
// "[AVC] %v, len: %d, %v\n", Types(payload), len(payload),
// reflect.ValueOf(buf).Pointer() == reflect.ValueOf(payload).Pointer(),
//)
clone := *packet clone := *packet
clone.Version = RTPPacketVersionAVC clone.Version = RTPPacketVersionAVC
@@ -92,7 +87,10 @@ 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 last := len(payloads) - 1
for i, payload := range payloads { for i, payload := range payloads {
@@ -100,10 +98,8 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
Header: rtp.Header{ Header: rtp.Header{
Version: 2, Version: 2,
Marker: i == last, 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,
} }
@@ -111,10 +107,8 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
return err return err
} }
} }
return nil
}
return push(packet) return nil
} }
} }
} }

View File

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

View File

@@ -2,19 +2,57 @@ package h265
import ( import (
"encoding/base64" "encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
) )
const ( const (
NALUnitTypeIFrame = 19 NALUTypePFrame = 1
NALUTypeIFrame = 19
NALUTypeIFrame2 = 20
NALUTypeIFrame3 = 21
NALUTypeVPS = 32
NALUTypeSPS = 33
NALUTypePPS = 34
NALUTypeFU = 49
) )
func NALUnitType(b []byte) byte { func NALUType(b []byte) byte {
return b[4] >> 1 return (b[4] >> 1) & 0x3F
} }
func IsKeyframe(b []byte) bool { func IsKeyframe(b []byte) bool {
return NALUnitType(b) == NALUnitTypeIFrame 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) { func GetParameterSet(fmtp string) (vps, sps, pps []byte) {

View File

@@ -1,84 +1,66 @@
package h265 package h265
import ( import (
"encoding/binary"
"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/deepch/vdk/codec/h265parser"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
func RTPDepay(track *streamer.Track) streamer.WrapperFunc { func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
vps, sps, pps := GetParameterSet(track.Codec.FmtpLine) //vps, sps, pps := GetParameterSet(track.Codec.FmtpLine)
//ps := h264.EncodeAVC(vps, sps, pps)
var buffer []byte buf := make([]byte, 0, 512*1024) // 512K
var nuStart int
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 {
nut := (packet.Payload[0] >> 1) & 0x3f
//fmt.Printf(
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n",
// track.Codec.Name, nut, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC, packet.SequenceNumber,
//)
switch nut {
case h265parser.NAL_UNIT_UNSPECIFIED_49:
data := packet.Payload 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 { switch data[2] >> 6 {
case 2: // begin case 2: // begin
buffer = []byte{ nuType = data[2] & 0x3F
(data[0] & 0x81) | (data[2] & 0x3f << 1), data[1],
} // push PS data before keyframe
buffer = append(buffer, data[3:]...) //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 return nil
case 0: // continue case 0: // continue
buffer = append(buffer, data[3:]...) buf = append(buf, data[3:]...)
return nil return nil
case 1: // end case 1: // end
packet.Payload = append(buffer, data[3:]...) buf = append(buf, data[3:]...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
} }
case h265parser.NAL_UNIT_VPS: } else {
vps = packet.Payload 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 return nil
case h265parser.NAL_UNIT_SPS:
sps = packet.Payload
return nil
case h265parser.NAL_UNIT_PPS:
pps = packet.Payload
return nil
default:
//panic("not implemented")
} }
var clone rtp.Packet //log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
nut = (packet.Payload[0] >> 1) & 0x3f clone := *packet
if nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA {
clone = *packet
clone.Version = h264.RTPPacketVersionAVC clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(vps) clone.Payload = buf
if err := push(&clone); err != nil {
return err
}
clone = *packet buf = buf[:0]
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(sps)
if err := push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(pps)
if err := push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = h264.EncodeAVC(packet.Payload)
return push(&clone) return push(&clone)
} }
@@ -86,68 +68,80 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
} }
// SafariPay - generate Safari friendly payload for H265 // SafariPay - generate Safari friendly payload for H265
// https://github.com/AlexxIT/Blog/issues/5
func SafariPay(mtu uint16) streamer.WrapperFunc { func SafariPay(mtu uint16) streamer.WrapperFunc {
sequencer := rtp.NewRandomSequencer() sequencer := rtp.NewRandomSequencer()
size := int(mtu - 12) // rtp.Header size size := int(mtu - 12) // rtp.Header size
var buffer []byte
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 != h264.RTPPacketVersionAVC { if packet.Version != h264.RTPPacketVersionAVC {
return push(packet) return push(packet)
} }
data := packet.Payload // protect original packets from modification
data[0] = 0 au := make([]byte, len(packet.Payload))
data[1] = 0 copy(au, packet.Payload)
data[2] = 0
data[3] = 1
var start byte var start byte
nut := (data[4] >> 1) & 0b111111 for i := 0; i < len(au); {
//fmt.Printf("[H265] nut: %2d, size: %6d, data: %16x\n", nut, len(data), data[4:20]) size := int(binary.BigEndian.Uint32(au[i:])) + 4
switch {
case nut >= h265parser.NAL_UNIT_VPS && nut <= h265parser.NAL_UNIT_PPS: // convert AVC to Annex-B
buffer = append(buffer, data...) au[i] = 0
return nil au[i+1] = 0
case nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA: au[i+2] = 0
buffer = append([]byte{3}, buffer...) au[i+3] = 1
data = append(buffer, data...)
start = 1 switch NALUType(au[i:]) {
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
start = 3
default: default:
data = append([]byte{2}, data...) if start == 0 {
start = 0 start = 2
}
}
i += size
}
// rtp.Packet payload
b := make([]byte, 1, size)
size-- // minus header byte
for au != nil {
b[0] = start
if start > 1 {
start -= 2
}
if len(au) > size {
b = append(b, au[:size]...)
au = au[size:]
} else {
b = append(b, au...)
au = nil
} }
for len(data) > size {
clone := rtp.Packet{ clone := rtp.Packet{
Header: rtp.Header{ Header: rtp.Header{
Version: 2, Version: 2,
Marker: false, Marker: au == nil,
SequenceNumber: sequencer.NextSequenceNumber(), SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp, Timestamp: packet.Timestamp,
}, },
Payload: data[:size], Payload: b,
} }
if err := push(&clone); err != nil { if err := push(&clone); err != nil {
return err return err
} }
data = append([]byte{start}, data[size:]...) b = b[:1] // clear buffer
} }
clone := rtp.Packet{ return nil
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

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

@@ -0,0 +1,104 @@
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
}
return Accept(res)
}
func Accept(res *http.Response) (*Conn, error) {
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()
}

View File

@@ -6,7 +6,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/fmp4/fmp4io" "github.com/deepch/vdk/format/fmp4/fmp4io"
@@ -162,9 +161,12 @@ func (c *Client) getTracks() error {
continue continue
} }
codec := streamer.NewCodec(streamer.CodecH264) codec := &streamer.Codec{
codec.FmtpLine = "profile-level-id=" + msg.CodecString[i+1:] Name: streamer.CodecH264,
codec.PayloadType = h264.PayloadTypeAVC ClockRate: 90000,
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
PayloadType: streamer.PayloadTypeRAW,
}
i = bytes.Index(msg.Data, []byte("avcC")) - 4 i = bytes.Index(msg.Data, []byte("avcC")) - 4
if i < 0 { if i < 0 {
@@ -190,10 +192,7 @@ func (c *Client) getTracks() error {
} }
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
track := &streamer.Track{ track := streamer.NewTrack(codec, streamer.DirectionSendonly)
Direction: streamer.DirectionSendonly,
Codec: codec,
}
c.tracks[msg.TrackID] = track c.tracks[msg.TrackID] = track
case "mp4a": // mp4a.40.2 case "mp4a": // mp4a.40.2

View File

@@ -2,3 +2,4 @@
- https://www.rfc-editor.org/rfc/rfc2435 - https://www.rfc-editor.org/rfc/rfc2435
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c - https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
- https://mjpeg.sanford.io/

152
pkg/mjpeg/client.go Normal file
View File

@@ -0,0 +1,152 @@
package mjpeg
import (
"bufio"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtp"
"io"
"net/http"
"net/textproto"
"strconv"
"strings"
"time"
)
type Client struct {
streamer.Element
UserAgent string
RemoteAddr string
closed bool
res *http.Response
track *streamer.Track
}
func NewClient(res *http.Response) *Client {
codec := &streamer.Codec{
Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW,
}
return &Client{
res: res,
track: streamer.NewTrack(codec, streamer.DirectionSendonly),
}
}
func (c *Client) GetMedias() []*streamer.Media {
return []*streamer.Media{{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{c.track.Codec},
}}
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
return c.track
}
func (c *Client) Start() error {
ct := c.res.Header.Get("Content-Type")
if ct == "image/jpeg" {
return c.startJPEG()
}
// added in go1.18
if _, s, ok := strings.Cut(ct, "boundary="); ok {
return c.startMJPEG(s)
}
return errors.New("wrong Content-Type: " + ct)
}
func (c *Client) Stop() error {
c.closed = true
return nil
}
func (c *Client) startJPEG() error {
buf, err := io.ReadAll(c.res.Body)
if err != nil {
return err
}
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
req := c.res.Request
for !c.closed {
res, err := tcp.Do(req)
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return errors.New("wrong status: " + res.Status)
}
buf, err = io.ReadAll(res.Body)
if err != nil {
return err
}
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
}
return nil
}
func (c *Client) startMJPEG(boundary string) error {
boundary = "--" + boundary
r := bufio.NewReader(c.res.Body)
tp := textproto.NewReader(r)
for !c.closed {
s, err := tp.ReadLine()
if err != nil {
return err
}
if s != boundary {
return errors.New("wrong boundary: " + s)
}
header, err := tp.ReadMIMEHeader()
if err != nil {
return err
}
s = header.Get("Content-Length")
if s == "" {
return errors.New("no content length")
}
size, err := strconv.Atoi(s)
if err != nil {
return err
}
buf := make([]byte, size)
if _, err = io.ReadFull(r, buf); err != nil {
return err
}
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
if _, err = r.Discard(2); err != nil {
return err
}
}
return nil
}
func now() uint32 {
return uint32(time.Now().UnixMilli() * 90)
}

View File

@@ -26,70 +26,15 @@ func (c *Consumer) GetMedias() []*streamer.Media {
} }
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
var header, payload []byte
push := func(packet *rtp.Packet) error { push := func(packet *rtp.Packet) error {
//fmt.Printf( c.Fire(packet.Payload)
// "[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 nil
} }
if track.Codec.IsRTP() {
wrapper := RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push) return track.Bind(push)
} }

212
pkg/mjpeg/rtp.go Normal file
View File

@@ -0,0 +1,212 @@
package mjpeg
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
var header, payload []byte
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", 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 {
return nil
}
b = append(header, payload...)
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
b = append(b, 0xFF, 0xD9)
}
header = nil
payload = nil
packet.Payload = b
return push(packet)
}
}
}
func RTPPay() streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
return nil
}
}
}
//func RTPPay() streamer.WrapperFunc {
// const packetSize = 1436
//
// sequencer := rtp.NewRandomSequencer()
//
// return func(push streamer.WriterFunc) streamer.WriterFunc {
// return func(packet *rtp.Packet) error {
// // reincode image to more common form
// img, err := jpeg.Decode(bytes.NewReader(packet.Payload))
// if err != nil {
// return err
// }
//
// wh := img.Bounds().Size()
// w := wh.X
// h := wh.Y
//
// if w > 2040 {
// w = 2040
// } else if w&3 > 0 {
// w &= 3
// }
// if h > 2040 {
// h = 2040
// } else if h&3 > 0 {
// h &= 3
// }
//
// if w != wh.X || h != wh.Y {
// x0 := (wh.X - w) / 2
// y0 := (wh.Y - h) / 2
// rect := image.Rect(x0, y0, x0+w, y0+h)
// img = img.(*image.YCbCr).SubImage(rect)
// }
//
// buf := bytes.NewBuffer(nil)
// if err = jpeg.Encode(buf, img, nil); err != nil {
// return err
// }
//
// h1 := make([]byte, 8)
// h1[4] = 1 // Type
// h1[5] = 255 // Q
//
// // MBZ=0, Precision=0, Length=128
// h2 := make([]byte, 4, 132)
// h2[3] = 128
//
// var jpgData []byte
//
// p := buf.Bytes()
//
// for jpgData == nil {
// // 2 bytes h1
// if p[0] != 0xFF {
// return nil
// }
//
// size := binary.BigEndian.Uint16(p[2:]) + 2
//
// // 2 bytes payload size (include 2 bytes)
// switch p[1] {
// case 0xD8: // 0. Start Of Image (size=0)
// p = p[2:]
// continue
// case 0xDB: // 1. Define Quantization Table (size=130)
// for i := uint16(4 + 1); i < size; i += 1 + 64 {
// h2 = append(h2, p[i:i+64]...)
// }
// case 0xC0: // 2. Start Of Frame (size=15)
// if p[4] != 8 {
// return nil
// }
// h := binary.BigEndian.Uint16(p[5:])
// w := binary.BigEndian.Uint16(p[7:])
// h1[6] = uint8(w >> 3)
// h1[7] = uint8(h >> 3)
// case 0xC4: // 3. Define Huffman Table (size=416)
// case 0xDA: // 4. Start Of Scan (size=10)
// jpgData = p[size:]
// }
//
// p = p[size:]
// }
//
// offset := 0
// p = make([]byte, 0)
//
// for jpgData != nil {
// p = p[:0]
//
// if offset > 0 {
// h1[1] = byte(offset >> 16)
// h1[2] = byte(offset >> 8)
// h1[3] = byte(offset)
// p = append(p, h1...)
// } else {
// p = append(p, h1...)
// p = append(p, h2...)
// }
//
// dataLen := packetSize - len(p)
// if dataLen < len(jpgData) {
// p = append(p, jpgData[:dataLen]...)
// jpgData = jpgData[dataLen:]
// offset += dataLen
// } else {
// p = append(p, jpgData...)
// jpgData = nil
// }
//
// clone := rtp.Packet{
// Header: rtp.Header{
// Version: 2,
// Marker: jpgData == nil,
// SequenceNumber: sequencer.NextSequenceNumber(),
// Timestamp: packet.Timestamp,
// },
// Payload: p,
// }
// if err := push(&clone); err != nil {
// return err
// }
// }
//
// return nil
// }
// }
//}

View File

@@ -21,3 +21,4 @@ Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressiv
- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec - https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
- https://jellyfin.org/docs/general/clients/codec-support.html - https://jellyfin.org/docs/general/clients/codec-support.html
- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding - 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,7 +2,7 @@ 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/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
@@ -12,60 +12,70 @@ import (
type Consumer struct { type Consumer struct {
streamer.Element streamer.Element
Medias []*streamer.Media
UserAgent string UserAgent string
RemoteAddr string RemoteAddr string
muxer *Muxer muxer *Muxer
codecs []*streamer.Codec codecs []*streamer.Codec
start bool wait byte
send int send int
} }
const (
waitNone byte = iota
waitKeyframe
waitInit
)
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, ClockRate: 90000}, {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) c.wait = waitInit
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
} }
if c.muxer == nil { if c.wait != waitNone {
return nil if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
}
if !c.start {
if h264.IsKeyframe(packet.Payload) {
c.start = true
} else {
return nil return nil
} }
c.wait = waitNone
} }
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)
@@ -73,49 +83,66 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
} }
var wrapper streamer.WrapperFunc var wrapper streamer.WrapperFunc
if h264.IsAVC(codec) { if codec.IsRTP() {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track) wrapper = h264.RTPDepay(track)
} else {
wrapper = h264.RepairAVC(track)
} }
push = wrapper(push) push = wrapper(push)
return track.Bind(push) return track.Bind(push)
case streamer.CodecH265: case streamer.CodecH265:
c.codecs = append(c.codecs, track.Codec) c.wait = waitInit
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
} }
if !c.start { if c.wait != waitNone {
if h265.IsKeyframe(packet.Payload) { if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
c.start = true
} else {
return nil return nil
} }
c.wait = waitNone
} }
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) { if codec.IsRTP() {
wrapper := h265.RTPDepay(track) wrapper := h265.RTPDepay(track)
push = wrapper(push) push = wrapper(push)
} }
return track.Bind(push)
case streamer.CodecAAC:
push := func(packet *rtp.Packet) error {
if c.wait != waitNone {
return nil
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
c.Fire(buf)
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
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 {
@@ -123,12 +150,16 @@ func (c *Consumer) MimeType() string {
} }
func (c *Consumer) Init() ([]byte, error) { 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() {
if c.wait == waitInit {
c.wait = waitKeyframe
}
}
// //
func (c *Consumer) MarshalJSON() ([]byte, error) { func (c *Consumer) MarshalJSON() ([]byte, error) {

View File

@@ -2,10 +2,11 @@ package mp4
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "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/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/codec/h265parser"
"github.com/deepch/vdk/format/fmp4/fmp4io" "github.com/deepch/vdk/format/fmp4/fmp4io"
@@ -16,22 +17,27 @@ import (
type Muxer struct { type Muxer struct {
fragIndex uint32 fragIndex uint32
dts uint64 dts []uint64
pts uint32 pts []uint32
data []byte
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: case streamer.CodecH265:
// +Safari +Chrome +Edge -iOS15 -Android13 // H.265 profile=main level=5.1
s += "hvc1.1.6.L93.B0" // hev1.1.6.L93.B0 // hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
s += "hvc1.1.6.L153.B0"
case streamer.CodecAAC:
s += "mp4a.40.2"
} }
} }
@@ -41,7 +47,7 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { 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)
@@ -59,11 +65,14 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
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,
} }
@@ -81,17 +90,15 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
}, },
} }
trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'},
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
}
moov.Tracks = append(moov.Tracks, trak) moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecH265: case streamer.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine) vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil { if sps == nil {
return nil, fmt.Errorf("empty SPS: %#v", codec) // 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) codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
@@ -102,11 +109,14 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
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,
} }
@@ -124,13 +134,47 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
}, },
} }
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{ trak.Media.Handler = &mp4io.HandlerRefer{
SubType: [4]byte{'v', 'i', 'd', 'e'}, 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}, 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())
@@ -139,14 +183,15 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
return append(FTYP(), data...), nil return append(FTYP(), data...), nil
} }
func (m *Muxer) Rewind() { func (m *Muxer) Reset() {
m.dts = 0 m.fragIndex = 0
m.pts = 0 for i := range m.dts {
m.dts[i] = 0
m.pts[i] = 0
}
} }
func (m *Muxer) Marshal(packet *rtp.Packet) []byte { func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
trackID := uint8(1)
run := &mp4fio.TrackFragRun{ run := &mp4fio.TrackFragRun{
Flags: 0x000b05, Flags: 0x000b05,
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies), FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
@@ -161,12 +206,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,
}, },
@@ -174,17 +219,18 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
} }
entry := mp4io.TrackFragRunEntry{ entry := mp4io.TrackFragRunEntry{
//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) entry.Duration = newTime - m.pts[trackID]
entry.Duration = newTime - m.pts m.dts[trackID] += uint64(entry.Duration)
m.dts += uint64(entry.Duration) } else {
// important, or Safari will fail with first frame
entry.Duration = 1
} }
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)
@@ -204,7 +250,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
} }

123
pkg/mp4/segment.go Normal file
View File

@@ -0,0 +1,123 @@
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 Segment struct {
streamer.Element
Medias []*streamer.Media
MimeType string
OnlyKeyframe bool
}
func (c *Segment) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
}
}
func (c *Segment) 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:
var push streamer.WriterFunc
if c.OnlyKeyframe {
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
}
} else {
var buf []byte
push = func(packet *rtp.Packet) error {
if h264.IsKeyframe(packet.Payload) {
// fist frame - send only IFrame
// other frames - send IFrame and all PFrames
if buf == nil {
buf = append(buf, init...)
b := muxer.Marshal(0, packet)
buf = append(buf, b...)
}
c.Fire(buf)
buf = buf[:0]
buf = append(buf, init...)
muxer.Reset()
}
if buf != nil {
b := muxer.Marshal(0, packet)
buf = append(buf, b...)
}
return nil
}
}
var wrapper streamer.WrapperFunc
if track.Codec.IsRTP() {
wrapper = h264.RTPDepay(track)
} else {
wrapper = h264.RepairAVC(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.IsRTP() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}

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.IsRAW() {
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

@@ -4,16 +4,24 @@ import (
"encoding/base64" "encoding/base64"
"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"
"net/http"
"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
@@ -22,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,10 +42,19 @@ func NewClient(uri string) *Client {
func (c *Client) Dial() (err error) { func (c *Client) Dial() (err error) {
c.conn, err = rtmp.Dial(c.URI) c.conn, err = rtmp.Dial(c.URI)
if err != nil {
return return
} }
// Accept - convert http.Response to Client
func Accept(res *http.Response) (*Client, error) {
conn, err := httpflv.Accept(res)
if err != nil {
return nil, err
}
return &Client{URI: res.Request.URL.String(), conn: conn}, nil
}
func (c *Client) Describe() (err error) {
// important to get SPS/PPS // important to get SPS/PPS
streams, err := c.conn.Streams() streams, err := c.conn.Streams()
if err != nil { if err != nil {
@@ -60,7 +77,7 @@ func (c *Client) Dial() (err error) {
Name: streamer.CodecH264, Name: streamer.CodecH264,
ClockRate: 90000, ClockRate: 90000,
FmtpLine: fmtp, FmtpLine: fmtp,
PayloadType: h264.PayloadTypeAVC, PayloadType: streamer.PayloadTypeRAW,
} }
media := &streamer.Media{ media := &streamer.Media{
@@ -70,26 +87,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.PayloadTypeRAW,
} }
media := &streamer.Media{ media := &streamer.Media{
@@ -99,9 +110,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:

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,7 +7,9 @@ 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/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtcp" "github.com/pion/rtcp"
@@ -43,7 +45,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
@@ -62,6 +71,7 @@ type Conn struct {
auth *tcp.Auth auth *tcp.Auth
conn net.Conn conn net.Conn
mode Mode mode Mode
state State
reader *bufio.Reader reader *bufio.Reader
sequence int sequence int
uri string uri string
@@ -90,6 +100,11 @@ func NewServer(conn net.Conn) *Conn {
return c return c
} }
func (c *Conn) Auth(username, password string) {
info := url.UserPassword(username, password)
c.auth = tcp.NewAuth(info)
}
func (c *Conn) parseURI() (err error) { func (c *Conn) parseURI() (err error) {
c.URL, err = url.Parse(c.uri) c.URL, err = url.Parse(c.uri)
if err != nil { if err != nil {
@@ -108,16 +123,11 @@ func (c *Conn) parseURI() (err error) {
} }
func (c *Conn) Dial() (err error) { func (c *Conn) Dial() (err error) {
//if c.state != StateClientInit {
// panic("wrong state")
//}
if c.conn != nil { if c.conn != nil {
_ = c.parseURI() _ = c.parseURI()
} }
c.conn, err = net.DialTimeout( c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
"tcp", c.URL.Host, 10*time.Second,
)
if err != nil { if err != nil {
return return
} }
@@ -139,6 +149,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
} }
@@ -362,22 +373,26 @@ 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 {
// Dahua VTO2111D fail on this step because of backchannel // some Dahua/Amcrest cameras fail here because two simultaneous
// backchannel connections
if c.Backchannel { if c.Backchannel {
if err = c.Dial(); err != nil {
return nil, err
}
c.Backchannel = false c.Backchannel = false
if err = c.Describe(); err != nil { if err := c.Dial(); err != nil {
return nil, err return nil, err
} }
res, err = c.Do(req) if err := c.Describe(); err != nil {
return nil, err
} }
if err != nil { for _, newMedia := range c.Medias {
return nil, err if newMedia.Control == media.Control {
return c.SetupMedia(newMedia, newMedia.Codecs[0])
} }
} }
}
return nil, err
}
if c.Session == "" { if c.Session == "" {
// Session: 216525287999;timeout=60 // Session: 216525287999;timeout=60
@@ -389,6 +404,12 @@ func (c *Conn) SetupMedia(
} }
} }
// in case the track has already been setup before
if codec == nil {
c.state = StateSetup
return nil, nil
}
// 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
@@ -420,9 +441,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:
@@ -435,44 +454,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 {
@@ -486,6 +502,17 @@ func (c *Conn) Accept() error {
c.Fire(req) c.Fire(req)
if !c.auth.Validate(req) {
res := &tcp.Response{
Status: "401 Unauthorized",
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
}
if err = c.Response(res); err != nil {
return err
}
continue
}
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN // Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN // Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
switch req.Method { switch req.Method {
@@ -513,9 +540,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
} }
@@ -575,8 +600,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"
@@ -597,17 +623,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
@@ -619,22 +681,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
} }
@@ -685,16 +746,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
} }
} }
} }
@@ -712,20 +776,18 @@ 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()
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
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 {
@@ -741,9 +803,18 @@ func (c *Conn) bindTrack(
return nil return nil
} }
if h264.IsAVC(track.Codec) { if !track.Codec.IsRTP() {
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)
case streamer.CodecJPEG:
wrapper := mjpeg.RTPPay()
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,12 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
} }
} }
// can't setup new tracks from play state - forcing a reconnection feature
if c.state == StatePlay {
go c.Close()
return streamer.NewTrack(codec, media.Direction)
}
track, err := c.SetupMedia(media, codec) track, err := c.SetupMedia(media, codec)
if err != nil { if err != nil {
return nil return nil
@@ -27,13 +34,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()
} }

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

View File

@@ -12,14 +12,6 @@ const (
JSONSend = "send" JSONSend = "send"
) )
// Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
Value interface{} `json:"value,omitempty"`
}
// other
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

@@ -33,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 PayloadTypeRAW byte = 255
func GetKind(name string) string { func GetKind(name string) string {
switch name { switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG: 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 ""
@@ -75,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 {
@@ -127,22 +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, CodecJPEG:
return &Codec{Name: name, ClockRate: 90000}
case CodecPCMU, CodecPCMA:
return &Codec{Name: name, ClockRate: 8000}
case CodecOpus:
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
case "MJPEG":
return &Codec{Name: CodecJPEG, ClockRate: 90000}
}
panic(fmt.Sprintf("unsupported codec: %s", name))
}
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 {
@@ -151,6 +139,10 @@ func (c *Codec) String() string {
return s return s
} }
func (c *Codec) IsRTP() bool {
return c.PayloadType != PayloadTypeRAW
}
func (c *Codec) Clone() *Codec { func (c *Codec) Clone() *Codec {
clone := *c clone := *c
return &clone return &clone
@@ -197,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)

View File

@@ -12,44 +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 {
t.mx.Lock() t.sinkMu.Lock()
if t.Sink == nil { if t.sink == nil {
t.Sink = map[*Track]WriterFunc{} t.sink = map[*Track]WriterFunc{}
} }
clone := &Track{ clone := *t
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink, t.sink[&clone] = w
}
t.Sink[clone] = w
t.mx.Unlock() t.sinkMu.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,6 +80,24 @@ func (a *Auth) Write(req *Request) {
} }
} }
func (a *Auth) Validate(req *Request) bool {
if a == nil {
return true
}
header := req.Header.Get("Authorization")
if header == "" {
return false
}
if a.Method == AuthUnknown {
a.Method = AuthBasic
a.header = "Basic " + B64(a.user, a.pass)
}
return header == a.header
}
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 {

68
pkg/tcp/request.go Normal file
View File

@@ -0,0 +1,68 @@
package tcp
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
)
// Do - http.Client with support Digest Authorization
func Do(req *http.Request) (*http.Response, error) {
// need to create new client each time to reset timeout
client := http.Client{Timeout: time.Second * 5000}
res, err := client.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode == http.StatusUnauthorized && req.URL.User != nil {
auth := res.Header.Get("WWW-Authenticate")
if !strings.HasPrefix(auth, "Digest") {
return nil, errors.New("unsupported auth: " + auth)
}
realm := Between(auth, `realm="`, `"`)
nonce := Between(auth, `nonce="`, `"`)
qop := Between(auth, `qop="`, `"`)
user := req.URL.User
username := user.Username()
password, _ := user.Password()
ha1 := HexMD5(username, realm, password)
uri := req.URL.RequestURI()
ha2 := HexMD5(req.Method, uri)
var header string
switch qop {
case "":
response := HexMD5(ha1, nonce, ha2)
header = fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
user, realm, nonce, uri, response,
)
case "auth":
nc := "00000001"
cnonce := "00000001" // TODO: random...
response := HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
header = fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
username, realm, nonce, uri, qop, nc, cnonce, response,
)
default:
return nil, errors.New("unsupported qop: " + auth)
}
req.Header.Set("Authorization", header)
res, err = client.Do(req)
if err != nil {
return nil, err
}
}
return res, nil
}

View File

@@ -1,18 +1,10 @@
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"
) )
const (
MsgTypeOffer = "webrtc/offer"
MsgTypeOfferComplete = "webrtc/offer-complete"
MsgTypeAnswer = "webrtc/answer"
MsgTypeCandidate = "webrtc/candidate"
)
type Conn struct { type Conn struct {
streamer.Element streamer.Element
@@ -29,11 +21,7 @@ type Conn struct {
func (c *Conn) Init() { func (c *Conn) Init() {
c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) { c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) {
if candidate != nil { c.Fire(candidate)
c.Fire(&streamer.Message{
Type: MsgTypeCandidate, Value: candidate.ToJSON().Candidate,
})
}
}) })
c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
@@ -58,7 +46,11 @@ func (c *Conn) Init() {
} }
} }
fmt.Printf("TODO: webrtc ontrack %+v\n", remote) //fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
})
c.Conn.OnDataChannel(func(channel *webrtc.DataChannel) {
c.Fire(channel)
}) })
// OK connection: // OK connection:
@@ -99,7 +91,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

@@ -57,10 +57,10 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
wrapper := h264.RTPPay(1200) wrapper := h264.RTPPay(1200)
push = wrapper(push) push = wrapper(push)
if h264.IsAVC(codec) { if codec.IsRTP() {
wrapper = h264.RepairAVC(track)
} else {
wrapper = h264.RTPDepay(track) wrapper = h264.RTPDepay(track)
} else {
wrapper = h264.RepairAVC(track)
} }
push = wrapper(push) push = wrapper(push)
@@ -108,14 +108,8 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
// //
func (c *Conn) Push(msg interface{}) { func (c *Conn) AddCandidate(candidate string) {
if msg := msg.(*streamer.Message); msg != nil { _ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{Candidate: candidate})
if msg.Type == MsgTypeCandidate {
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{
Candidate: msg.Value.(string),
})
}
}
} }
func (c *Conn) MarshalJSON() ([]byte, error) { func (c *Conn) MarshalJSON() ([]byte, error) {

View File

@@ -9,6 +9,7 @@ import (
"net" "net"
"strconv" "strconv"
"strings" "strings"
"time"
) )
func NewCandidate(address string) (string, error) { func NewCandidate(address string) (string, error) {
@@ -38,7 +39,7 @@ func NewCandidate(address string) (string, error) {
func LookupIP(address string) (string, error) { func LookupIP(address string) (string, error) {
if strings.HasPrefix(address, "stun:") { if strings.HasPrefix(address, "stun:") {
ip, err := GetPublicIP() ip, err := GetCachedPublicIP()
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -63,11 +64,20 @@ func LookupIP(address string) (string, error) {
// 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)
@@ -90,6 +100,24 @@ 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 { func IsIP(host string) bool {
for _, i := range host { for _, i := range host {
if i >= 'A' { if i >= 'A' {

View File

@@ -2,6 +2,7 @@
- UPX-3.96 pack broken bin for `linux_mipsel` - UPX-3.96 pack broken bin for `linux_mipsel`
- UPX-3.95 pack broken bin for `mac_amd64` - UPX-3.95 pack broken bin for `mac_amd64`
- UPX pack broken bin for `mac_arm64`
- UPX windows pack is recognised by anti-viruses as malicious - UPX windows pack is recognised by anti-viruses as malicious
- `aarch64` = `arm64` - `aarch64` = `arm64`
- `armv7` = `arm` - `armv7` = `arm`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -8,6 +8,10 @@
<title>go2rtc</title> <title>go2rtc</title>
<style> <style>
body {
font-family: Arial, Helvetica, sans-serif;
}
table { table {
background-color: white; background-color: white;
text-align: left; text-align: left;
@@ -36,9 +40,23 @@
text-align: center; text-align: center;
} }
label {
display: flex;
align-items: center;
}
.header { .header {
padding: 5px 5px; padding: 5px 5px;
} }
.controls {
display: flex;
padding: 5px;
}
.controls > label {
margin-left: 10px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -47,6 +65,13 @@
<input id="src" type="text" placeholder="url"> <input id="src" type="text" placeholder="url">
<a id="add" href="#">add</a> <a id="add" href="#">add</a>
</div> </div>
<div class="controls">
<button>stream</button>
<label><input type="checkbox" name="webrtc" checked>webrtc</label>
<label><input type="checkbox" name="mse" checked>mse</label>
<label><input type="checkbox" name="mp4" checked>mp4</label>
<label><input type="checkbox" name="mjpeg" checked>mjpeg</label>
</div>
<table id="streams"> <table id="streams">
<thead> <thead>
<tr> <tr>
@@ -59,50 +84,71 @@
</tbody> </tbody>
</table> </table>
<script> <script>
const baseUrl = location.origin + location.pathname.substr( const templates = [
0, location.pathname.lastIndexOf("/") '<a href="stream.html?src={name}">stream</a>',
); '<a href="webrtc.html?src={name}">2-way-aud</a>',
const links = [
'<a href="webrtc.html?src={name}">webrtc</a>',
'<a href="mse.html?src={name}">mse</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="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>', '<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
'<a href="api/streams?src={name}">info</a>', '<a href="api/streams?src={name}">info</a>',
'<a href="#" data-name="{name}">delete</a>',
]; ];
document.querySelector("#add")
.addEventListener("click", () => {
const src = document.querySelector("#src");
const url = new URL("api/streams", location.href);
url.searchParams.set("src", src.value);
fetch(url, {method: "PUT"}).then(reload);
});
document.querySelector(".controls > button")
.addEventListener("click", () => {
const url = new URL("stream.html", location.href);
const streams = document.querySelectorAll("#streams input");
streams.forEach(i => {
if (i.checked) url.searchParams.append("src", i.name);
});
if (!url.searchParams.has("src")) return;
let mode = document.querySelectorAll(".controls input");
mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(",");
window.location.href = `${url}&mode=${mode}`;
});
const tbody = document.querySelector("#streams > tbody");
tbody.addEventListener("click", ev => {
if (ev.target.innerText !== "delete") return;
ev.preventDefault();
const url = new URL("api/streams", location.href);
url.searchParams.set("src", ev.target.dataset.name);
fetch(url, {method: "DELETE"}).then(reload);
});
function reload() { function reload() {
fetch(`${baseUrl}/api/streams`).then(r => { const url = new URL("api/streams", location.href);
r.json().then(data => { fetch(url).then(r => r.json()).then(data => {
let html = ''; tbody.innerHTML = "";
for (const [name, value] of Object.entries(data)) { for (const [name, value] of Object.entries(data)) {
const online = value !== null ? value.length : 0 const online = value ? value.length : 0;
html += `<tr><td>${name || 'default'}</td><td>${online}</td><td>`; const links = templates.map(link => {
links.forEach(link => { return link.replace("{name}", encodeURIComponent(name));
html += link.replace('{name}', encodeURIComponent(name)) + ' '; }).join(" ");
})
html += `<a href="#" onclick="deleteStream('${name}')">delete</a>`;
html += `</td></tr>`;
}
let content = document.getElementById('streams').getElementsByTagName('tbody')[0]; const tr = document.createElement("tr");
content.innerHTML = html tr.dataset["id"] = name;
tr.innerHTML =
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
`<td>${online}</td><td>${links}</td>`;
tbody.appendChild(tr);
}
}); });
})
}
function deleteStream(src) {
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src)}`, {method: 'DELETE'}).then(reload);
}
const addButton = document.querySelector('a#add');
addButton.onclick = () => {
let src = document.querySelector('input#src');
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src.value)}`, {method: 'PUT'}).then(reload);
} }
reload(); reload();

View File

@@ -1,117 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - MSE</title>
<style>
body {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
#video {
width: 100%;
height: 100%;
background: black;
}
</style>
</head>
<body>
<!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video>
<script>
const video = document.querySelector('#video');
// support api_path
const baseUrl = location.origin + location.pathname.substr(
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>
</body>
</html>

61
www/stream.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - Stream</title>
<script src="video-rtc.js"></script>
<style>
body {
background: black;
margin: 0;
padding: 0;
display: flex;
}
html, body {
height: 100%;
width: 100%;
}
.flex {
flex-wrap: wrap;
align-content: flex-start;
align-items: flex-start;
}
</style>
</head>
<body>
<script>
const params = new URLSearchParams(location.search);
// support multiple streams and multiple modes
const streams = params.getAll("src");
const modes = params.getAll("mode");
if (modes.length === 0) modes.push("");
while (modes.length > streams.length) {
streams.push(streams[0]);
}
while (streams.length > modes.length) {
modes.push(modes[0]);
}
if (streams.length > 1) {
document.body.className = "flex";
}
const background = params.get("background") === "true";
const width = "1 0 " + (params.get("width") || "320px");
for (let i = 0; i < streams.length; i++) {
/** @type {VideoRTC} */
const video = document.createElement("video-rtc");
video.background = background;
video.mode = modes[i] || video.mode;
video.style.flex = width;
video.src = new URL("api/ws?src=" + streams[i], location.href);
document.body.appendChild(video);
}
</script>
</body>
</html>

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

@@ -0,0 +1,584 @@
/**
* 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+
*
* Doesn't support:
* - MediaSource for Safari iOS all
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
* - Public class fields because old Safari (before 14.0)
*/
class VideoRTC extends HTMLElement {
constructor() {
super();
this.DISCONNECT_TIMEOUT = 5000;
this.RECONNECT_TIMEOUT = 30000;
this.CODECS = [
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen)
"avc1.640033", // H.264 high 5.1 (Chromecast with Google TV)
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC
"mp4a.40.5", // AAC HE
"mp4a.69", // MP3
"mp4a.6B", // MP3
];
/**
* Supported modes (webrtc, mse, mp4, mjpeg).
* @type {string}
*/
this.mode = "webrtc,mse,mp4,mjpeg";
/**
* Run stream when not displayed on the screen. Default `false`.
* @type {boolean}
*/
this.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}
*/
this.intersectionThreshold = 0;
/**
* Run stream only when browser page on the screen. Stop when user change browser
* tab or minimise browser windows.
* @type {boolean}
*/
this.visibilityCheck = true;
/**
* @type {HTMLVideoElement}
*/
this.video = null;
/**
* @type {WebSocket}
*/
this.ws = null;
/**
* Internal WebSocket connection state. Values: CONNECTING, OPEN, CLOSED
* @type {number}
*/
this.wsState = WebSocket.CLOSED;
/**
* Internal WebSocket URL.
* @type {string|URL}
*/
this.url = "";
/**
* @type {RTCPeerConnection}
*/
this.pc = null;
/**
* @type {number}
*/
this.pcState = WebSocket.CLOSED;
this.pcConfig = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]};
/**
* Internal disconnect TimeoutID.
* @type {number}
*/
this.disconnectTimeout = 0;
/**
* Internal reconnect TimeoutID.
* @type {number}
*/
this.reconnectTimeout = 0;
/**
* Handler for receiving Binary from WebSocket
* @type {Function}
*/
this.ondata = null;
/**
* Handlers list for receiving JSON from WebSocket
* @type {Object.<string,Function>}}
*/
this.onmessage = null;
}
/** public properties **/
/**
* Set video source (WebSocket URL). Support relative path.
* @param {string|URL} value
*/
set src(value) {
if (typeof value !== "string") value = value.toString();
if (value.startsWith("http")) {
value = "ws" + value.substring(4);
} else if (value.startsWith("/")) {
value = "ws" + location.origin.substring(4) + value;
}
this.url = 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().catch(() => console.debug);
}
});
}
/**
* Send message to server via WebSocket
* @param {Object} value
*/
send(value) {
if (this.ws) this.ws.send(JSON.stringify(value));
}
get closed() {
return this.wsState === WebSocket.CLOSED && this.pcState === WebSocket.CLOSED;
}
codecs(type) {
const test = type === "mse"
? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`)
: codec => this.video.canPlayType(`video/mp4; codecs="${codec}"`);
return this.CODECS.filter(test).join();
}
/**
* `CustomElement`. Invoked each time the custom element is appended into a
* document-connected element.
*/
connectedCallback() {
console.debug("VideoRTC.connectedCallback", this.wsState, this.pcState);
if (this.disconnectTimeout) {
clearTimeout(this.disconnectTimeout);
this.disconnectTimeout = 0;
}
// because video autopause on disconnected from DOM
if (this.video) {
const seek = this.video.seekable;
if (seek.length > 0) {
this.video.currentTime = seek.end(seek.length - 1);
this.play();
}
}
if (!this.url || !this.closed) return;
// CLOSED => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.internalInit();
this.internalWS();
}
/**
* `CustomElement`. Invoked each time the custom element is disconnected from the
* document's DOM.
*/
disconnectedCallback() {
console.debug("VideoRTC.disconnectedCallback", this.wsState, this.pcState);
if (this.background || this.disconnectTimeout || this.closed) return;
this.disconnectTimeout = setTimeout(() => {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = 0;
}
this.disconnectTimeout = 0;
this.wsState = WebSocket.CLOSED;
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.pcState = WebSocket.CLOSED;
if (this.pc) {
this.pc.close();
this.pc = null;
}
}, this.DISCONNECT_TIMEOUT);
}
internalInit() {
if (this.childElementCount) return;
this.video = document.createElement("video");
this.video.controls = true;
this.video.playsInline = true;
this.video.preload = "auto";
this.appendChild(this.video);
// important for second video for mode MP4
this.style.display = "block";
this.style.position = "relative";
this.video.style.display = "block"; // fix bottom margin 4px
this.video.style.width = "100%";
this.video.style.height = "100%"
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);
}
}
internalWS() {
if (this.wsState !== WebSocket.CONNECTING) return;
if (this.ws) throw "connect with non null WebSocket";
const ts = Date.now();
this.ws = new WebSocket(this.url);
this.ws.binaryType = "arraybuffer";
this.ws.addEventListener("open", () => {
console.debug("VideoRTC.ws.open", this.wsState);
// CONNECTING => OPEN
this.wsState = WebSocket.OPEN;
this.ws.addEventListener("message", ev => {
if (typeof ev.data === "string") {
const msg = JSON.parse(ev.data);
for (const mode in this.onmessage) {
this.onmessage[mode](msg);
}
} else {
this.ondata(ev.data);
}
});
this.ondata = null;
this.onmessage = {};
let firstMode = "";
if (this.mode.indexOf("mse") >= 0 && "MediaSource" in window) { // iPhone
firstMode ||= "mse";
this.internalMSE();
} else if (this.mode.indexOf("mp4") >= 0) {
firstMode ||= "mp4";
this.internalMP4();
}
if (this.mode.indexOf("webrtc") >= 0 && "RTCPeerConnection" in window) { // macOS Desktop app
firstMode ||= "webrtc";
this.internalRTC();
}
if (this.mode.indexOf("mjpeg") >= 0) {
if (firstMode) {
this.onmessage["mjpeg"] = msg => {
if (msg.type !== "error" || msg.value.indexOf(firstMode) !== 0) return;
this.internalMJPEG();
}
} else {
this.internalMJPEG();
}
}
});
this.ws.addEventListener("close", () => {
console.debug("VideoRTC.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.internalWS();
}, delay);
});
}
internalMSE() {
console.debug("VideoRTC.internalMSE");
const ms = new MediaSource();
ms.addEventListener("sourceopen", () => {
console.debug("VideoRTC.ms.sourceopen");
URL.revokeObjectURL(this.video.src);
this.send({type: "mse", value: this.codecs("mse")});
}, {once: true});
this.video.src = URL.createObjectURL(ms);
this.video.srcObject = null;
this.play();
this.onmessage["mse"] = msg => {
if (msg.type !== "mse") return;
const sb = ms.addSourceBuffer(msg.value);
sb.mode = "segments"; // segments or sequence
sb.addEventListener("updateend", () => {
if (sb.updating) return;
if (bufLen > 0) {
try {
sb.appendBuffer(buf.slice(0, bufLen));
} catch (e) {
console.debug(e);
}
bufLen = 0;
} else if (sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 5;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
ms.setLiveSeekableRange(end, end + 5);
}
// console.debug("VideoRTC.buffered", start, end);
}
});
const buf = new Uint8Array(2 * 1024 * 1024);
let bufLen = 0;
this.ondata = data => {
if (sb.updating || bufLen > 0) {
const b = new Uint8Array(data);
buf.set(b, bufLen);
bufLen += b.byteLength;
// console.debug("VideoRTC.buffer", b.byteLength, bufLen);
} else {
try {
sb.appendBuffer(data);
} catch (e) {
// console.debug(e);
}
}
}
}
}
internalRTC() {
console.debug("VideoRTC.internalRTC");
const pc = new RTCPeerConnection(this.pcConfig);
let mseCodecs = "";
/** @type {HTMLVideoElement} */
const video2 = document.createElement("video");
video2.addEventListener("loadeddata", () => {
console.debug("VideoRTC.video.loadeddata", video2.readyState, pc.connectionState);
if (pc.connectionState === "connected" || pc.connectionState === "connecting") {
// Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE
let rtcPriority = 0, msePriority = 0;
/** @type {MediaStream} */
const rtc = video2.srcObject;
if (rtc.getVideoTracks().length > 0) rtcPriority += 0x220;
if (rtc.getAudioTracks().length > 0) rtcPriority += 0x102;
if (mseCodecs.indexOf("hvc1.") >= 0) msePriority += 0x230;
if (mseCodecs.indexOf("avc1.") >= 0) msePriority += 0x210;
if (mseCodecs.indexOf("mp4a.") >= 0) msePriority += 0x101;
if (rtcPriority >= msePriority) {
console.debug("VideoRTC.select RTC mode", rtcPriority, msePriority);
this.video.controls = true;
this.video.srcObject = rtc;
this.play();
this.pcState = WebSocket.OPEN;
this.wsState = WebSocket.CLOSED;
this.ws.close();
this.ws = null;
} else {
console.debug("VideoRTC.select MSE mode", rtcPriority, msePriority);
pc.close();
this.pcState = WebSocket.CLOSED;
this.pc = null;
}
}
video2.srcObject = null;
}, {once: true});
pc.addEventListener("icecandidate", ev => {
const candidate = ev.candidate ? ev.candidate.toJSON().candidate : "";
this.send({type: "webrtc/candidate", value: candidate});
});
pc.addEventListener("track", ev => {
console.debug("VideoRTC.pc.track", ev.streams.length);
// when stream already init
if (video2.srcObject !== null) return;
// when audio track not exist in Chrome
if (ev.streams.length === 0) return;
// when audio track not exist in Firefox
if (ev.streams[0].id[0] === '{') return;
video2.srcObject = ev.streams[0];
});
pc.addEventListener("connectionstatechange", () => {
console.debug("VideoRTC.pc.connectionstatechange", this.pc.connectionState);
if (pc.connectionState === "failed" || pc.connectionState === "disconnected") {
pc.close(); // stop next events
this.pcState = WebSocket.CLOSED;
this.pc = null;
if (this.wsState === WebSocket.CLOSED && this.isConnected) {
this.connectedCallback();
}
}
});
this.onmessage["webrtc"] = msg => {
switch (msg.type) {
case "webrtc/candidate":
pc.addIceCandidate({candidate: msg.value, sdpMid: ""}).catch(() => console.debug);
break;
case "webrtc/answer":
pc.setRemoteDescription({type: "answer", sdp: msg.value}).catch(() => console.debug);
break;
case "mse":
mseCodecs = msg.value;
break;
case "error":
if (msg.value.indexOf("webrtc/offer") < 0) return;
pc.close();
}
};
// Safari doesn't support "offerToReceiveVideo"
pc.addTransceiver("video", {direction: "recvonly"});
pc.addTransceiver("audio", {direction: "recvonly"});
pc.createOffer().then(offer => {
pc.setLocalDescription(offer).then(() => {
this.send({type: "webrtc/offer", value: offer.sdp});
});
});
this.pcState = WebSocket.CONNECTING;
this.pc = pc;
}
internalMJPEG() {
console.debug("VideoRTC.internalMJPEG");
this.ondata = data => {
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data);
};
this.send({type: "mjpeg"});
this.video.controls = false;
}
internalMP4() {
console.debug("VideoRTC.internalMP4");
/** @type {HTMLVideoElement} */
let video2;
this.ondata = data => {
// first video with default position (set container size)
// second video with position=absolute and top=0px
if (video2) {
this.removeChild(this.video);
this.video.src = "";
this.video = video2;
video2.style.position = "";
video2.style.top = "";
}
video2 = this.video.cloneNode();
video2.style.position = "absolute";
video2.style.top = "0px";
this.appendChild(video2);
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
video2.play().catch(() => console.log);
};
this.ws.addEventListener("close", () => {
if (!video2) return;
this.removeChild(video2);
video2.src = "";
});
this.send({type: "mp4", value: this.codecs("mp4")});
this.video.controls = false;
}
static btoa(buffer) {
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
let binary = "";
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
}
customElements.define("video-rtc", VideoRTC);

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - WebRTC</title>
<style>
body {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
#video {
/* video "container" size */
width: 100%;
height: 100%;
background: black;
}
</style>
</head>
<body>
<video id="video" autoplay controls playsinline muted></video>
<!--<video id="video" preload="auto" controls playsinline muted></video>-->
<script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const video = document.getElementById('video');
video.oncanplay = ev => console.log(ev.type, ev);
video.onplaying = ev => console.log(ev.type, ev);
video.onwaiting = ev => console.log(ev.type, ev);
video.onseeking = ev => console.log(ev.type, ev);
video.onloadeddata = ev => console.log(ev.type, ev);
video.oncanplaythrough = ev => console.log(ev.type, ev);
// video.ondurationchange = ev => console.log(ev.type, ev);
// video.ontimeupdate = ev => console.log(ev.type, ev);
video.onplay = ev => console.log(ev.type, ev);
video.onpause = ev => console.log(ev.type, ev);
video.onsuspended = ev => console.log(ev.type, ev);
video.onemptied = ev => console.log(ev.type, ev);
video.onstalled = ev => console.log(ev.type, ev);
console.log("start");
video.src = baseUrl + "/api/stream.mp4" + location.search;
</script>
</body>
</html>

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