mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-28 05:02:11 +08:00
Compare commits
86 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4c2ebd20bc | ||
![]() |
440c7bd6e1 | ||
![]() |
74c3510a10 | ||
![]() |
c2748fc77b | ||
![]() |
d334551591 | ||
![]() |
cfe20925ac | ||
![]() |
5b39f78ace | ||
![]() |
b965c191b7 | ||
![]() |
7057b4846f | ||
![]() |
a746b96adc | ||
![]() |
b7718b33b8 | ||
![]() |
69b17230f3 | ||
![]() |
e2ecd909ab | ||
![]() |
ea79da0d53 | ||
![]() |
e64919838c | ||
![]() |
162b11213d | ||
![]() |
d27acbd7e3 | ||
![]() |
a692ecd7c1 | ||
![]() |
98c5366ba9 | ||
![]() |
3eaaa3fcfa | ||
![]() |
7409b32836 | ||
![]() |
e2cfdf8419 | ||
![]() |
4c0929d854 | ||
![]() |
258a0ffb91 | ||
![]() |
999e81c2dd | ||
![]() |
8c6729027b | ||
![]() |
d3bd5eeab5 | ||
![]() |
dbbf2ea310 | ||
![]() |
b8234e0c76 | ||
![]() |
96cd753e27 | ||
![]() |
c522e5bb08 | ||
![]() |
a16d8acc30 | ||
![]() |
684878b4b1 | ||
![]() |
140a742cee | ||
![]() |
1b518b94fd | ||
![]() |
5678121c50 | ||
![]() |
4915f12bde | ||
![]() |
bb91240b95 | ||
![]() |
b1d5d53832 | ||
![]() |
31fbbf91bb | ||
![]() |
a3f72fbab9 | ||
![]() |
fae59c7992 | ||
![]() |
aff34f1d21 | ||
![]() |
65e7efa775 | ||
![]() |
3c3e9d282b | ||
![]() |
bd51069086 | ||
![]() |
1ddf7f1a6c | ||
![]() |
0e281e36d3 | ||
![]() |
3d6472cfb1 | ||
![]() |
7c31fa2ffd | ||
![]() |
0ed9d2410a | ||
![]() |
1c89e7945e | ||
![]() |
48635ae341 | ||
![]() |
fdb316910f | ||
![]() |
e29f2594fa | ||
![]() |
c3da7584b0 | ||
![]() |
1e247cba92 | ||
![]() |
01631d9eb0 | ||
![]() |
4b27d119f0 | ||
![]() |
dd55c03dc2 | ||
![]() |
a4eab1944a | ||
![]() |
eea413a36c | ||
![]() |
cdd42a8ed2 | ||
![]() |
4815ce1baf | ||
![]() |
e6d3939c78 | ||
![]() |
220b9ca318 | ||
![]() |
d625620dfd | ||
![]() |
dd503f3410 | ||
![]() |
3e8e87bfcc | ||
![]() |
64d218886e | ||
![]() |
e91ccc211e | ||
![]() |
9f8a219483 | ||
![]() |
b617796941 | ||
![]() |
77888fe086 | ||
![]() |
7bc3534bcb | ||
![]() |
77bc0630d6 | ||
![]() |
2f68711405 | ||
![]() |
b8cab5db60 | ||
![]() |
eae01be71f | ||
![]() |
0127115180 | ||
![]() |
aef84cef6b | ||
![]() |
d478436758 | ||
![]() |
f77db44529 | ||
![]() |
149d1bf235 | ||
![]() |
b650475b10 | ||
![]() |
16e5406156 |
205
README.md
205
README.md
@@ -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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**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
|
||||||
@@ -176,13 +150,35 @@ streams:
|
|||||||
|
|
||||||
#### 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.
|
||||||
@@ -207,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
|
||||||
|
|
||||||
@@ -279,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
|
||||||
@@ -306,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/`.
|
||||||
@@ -318,14 +337,15 @@ 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
|
||||||
|
|
||||||
@@ -334,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
|
||||||
@@ -493,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
|
||||||
|
|
||||||
@@ -553,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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**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?**
|
||||||
|
@@ -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")
|
||||||
|
|
||||||
|
s := http.Server{}
|
||||||
|
s.Handler = http.DefaultServeMux
|
||||||
|
|
||||||
|
if log.Trace().Enabled() {
|
||||||
|
s.Handler = middlewareLog(s.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Mod.Origin == "*" {
|
||||||
|
s.Handler = middlewareCORS(s.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s := http.Server{}
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
|
||||||
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Trace().Stringer("url", r.URL).Msgf("[api] %s", r.Method)
|
|
||||||
http.DefaultServeMux.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.Serve(listener); err != nil {
|
if err = s.Serve(listener); err != nil {
|
||||||
log.Fatal().Err(err).Msg("[api] serve")
|
log.Fatal().Err(err).Msg("[api] serve")
|
||||||
}
|
}
|
||||||
@@ -75,18 +76,30 @@ 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")
|
||||||
name := r.URL.Query().Get("name")
|
name := r.URL.Query().Get("name")
|
||||||
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = src
|
name = 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
149
cmd/api/ws.go
149
cmd/api/ws.go
@@ -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,77 +8,131 @@ 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,
|
||||||
}
|
}
|
||||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
|
||||||
origin := r.Header["Origin"]
|
switch origin {
|
||||||
if len(origin) == 0 {
|
case "":
|
||||||
return true
|
// same origin + ignore port
|
||||||
}
|
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||||
o, err := url.Parse(origin[0])
|
origin := r.Header["Origin"]
|
||||||
if err != nil {
|
if len(origin) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
o, err := url.Parse(origin[0])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if o.Host == r.Host {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||||
|
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||||
|
return o.Host[:i] == r.Host
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if o.Host == r.Host {
|
case "*":
|
||||||
|
// any origin
|
||||||
|
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
log.Trace().Msgf("[api.ws] origin: %s, host: %s", o.Host, r.Host)
|
|
||||||
// some users change Nginx external port using Docker port
|
|
||||||
// so origin will be with a port and host without
|
|
||||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
|
||||||
return o.Host[:i] == r.Host
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
onClose []func()
|
mx sync.Mutex
|
||||||
mu sync.Mutex
|
|
||||||
|
onChange func()
|
||||||
|
onWrite func(msg interface{})
|
||||||
|
onClose []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
_ = ctx.Conn.WriteMessage(websocket.BinaryMessage, data)
|
|
||||||
} else {
|
|
||||||
_ = ctx.Conn.WriteJSON(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,9 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Version = "0.1-rc.5"
|
||||||
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
config := flag.String(
|
config := flag.String(
|
||||||
"config",
|
"config",
|
||||||
@@ -35,9 +38,10 @@ func Init() {
|
|||||||
|
|
||||||
modules = cfg.Mod
|
modules = cfg.Mod
|
||||||
|
|
||||||
|
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
path, _ := os.Getwd()
|
path, _ := os.Getwd()
|
||||||
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
|
log.Debug().Str("cwd", path).Send()
|
||||||
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
func NewLogger(format string, level string) zerolog.Logger {
|
||||||
|
@@ -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"
|
||||||
@@ -56,6 +57,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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 -timeout 5000000 -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,52 +96,97 @@ 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)
|
||||||
|
} else if strings.HasPrefix(s, "device?") {
|
||||||
|
var err error
|
||||||
|
input, err = device.GetInput(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input = strings.Replace(tpl["file"], "{input}", s, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if input == "" {
|
if _, ok := query["async"]; ok {
|
||||||
if strings.HasPrefix(s, "device?") {
|
input = "-use_wallclock_as_timestamps 1 -async 1 " + input
|
||||||
var err error
|
|
||||||
input, err = device.GetInput(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
input = strings.Replace(tpl["file"], "{input}", s, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s = "exec:" + tpl["bin"] + " -hide_banner " + input
|
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":
|
||||||
for _, video := range query["video"] {
|
s += " -vf transpose=1" // 90 degrees clockwise
|
||||||
if video == "copy" {
|
case "180":
|
||||||
s += " -codec:v copy"
|
s += " -vf transpose=1,transpose=1"
|
||||||
} else {
|
case "-90", "270":
|
||||||
s += " " + tpl[video]
|
s += " -vf transpose=2" // 90 degrees counterclockwise
|
||||||
}
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, audio := range query["audio"] {
|
switch len(query["video"]) {
|
||||||
if audio == "copy" {
|
case 0:
|
||||||
s += " -codec:a copy"
|
|
||||||
} else {
|
|
||||||
s += " " + tpl[audio]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case queryVideo && !queryAudio:
|
|
||||||
s += " -an"
|
|
||||||
case queryAudio && !queryVideo:
|
|
||||||
s += " -vn"
|
s += " -vn"
|
||||||
|
case 1:
|
||||||
|
if len(query["audio"]) > 1 {
|
||||||
|
s += " -map 0:v:0?"
|
||||||
|
}
|
||||||
|
for _, video := range query["video"] {
|
||||||
|
if video == "copy" {
|
||||||
|
s += " -c:v copy"
|
||||||
|
} else {
|
||||||
|
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"] {
|
||||||
|
if audio == "copy" {
|
||||||
|
s += " -c:a copy"
|
||||||
|
} else {
|
||||||
|
s += " " + tpl[audio]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
for i, audio := range query["audio"] {
|
||||||
|
if audio == "copy" {
|
||||||
|
s += " -map 0:a:0? -c:a:" + strconv.Itoa(i) + " copy"
|
||||||
|
} else {
|
||||||
|
s += " -map 0:a:0? " + strings.ReplaceAll(tpl[audio], ":a ", ":a:"+strconv.Itoa(i)+" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s += " -c copy"
|
s += " -c copy"
|
||||||
|
@@ -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,91 +54,82 @@ 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")
|
||||||
dict := store.GetDict("streams")
|
if err := hkDelete(src); err != nil {
|
||||||
for name, rawURL := range dict {
|
log.Error().Err(err).Caller().Send()
|
||||||
if name != src {
|
_, err = w.Write([]byte(err.Error()))
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := homekit.NewClient(rawURL.(string))
|
|
||||||
if err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] new client")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Dial(); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] client dial")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go client.Handle()
|
|
||||||
|
|
||||||
if err = client.ListPairings(); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.DeletePairing(client.ClientID); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(dict, name)
|
|
||||||
|
|
||||||
if err = store.Set("streams", dict); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] store set")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[name] = conn.URL()
|
||||||
|
|
||||||
|
return store.Set("streams", dict)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hkDelete(name string) (err error) {
|
||||||
|
dict := store.GetDict("streams")
|
||||||
|
for key, rawURL := range dict {
|
||||||
|
if key != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn *hap.Conn
|
||||||
|
|
||||||
|
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = conn.Dial(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err = conn.Handle(); err != nil {
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = conn.ListPairings(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = conn.DeletePairing(conn.ClientID); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(dict, name)
|
||||||
|
|
||||||
|
return store.Set("streams", dict)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
@@ -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
|
|
||||||
}
|
}
|
||||||
|
@@ -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
56
cmd/http/http.go
Normal 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)
|
||||||
|
}
|
@@ -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"
|
||||||
@@ -12,12 +13,15 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
||||||
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
||||||
|
|
||||||
|
api.HandleWS("mjpeg", handlerWS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +44,12 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "image/jpeg")
|
h := w.Header()
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
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 {
|
if _, err := w.Write(data); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -54,23 +62,25 @@ 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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,11 +89,41 @@ func handlerStream(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
|
||||||
|
}
|
||||||
|
@@ -15,7 +15,8 @@ import (
|
|||||||
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)
|
||||||
@@ -31,12 +32,13 @@ 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.Keyframe{}
|
cons := &mp4.Segment{OnlyKeyframe: true}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
if data, ok := msg.([]byte); ok && exit != nil {
|
if data, ok := msg.([]byte); ok && exit != nil {
|
||||||
exit <- data
|
exit <- data
|
||||||
@@ -63,15 +65,16 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,57 +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
|
|
||||||
|
|
||||||
const packetSize = 8192
|
|
||||||
|
|
||||||
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{}) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
for len(data) > packetSize {
|
|
||||||
ctx.Write(data[:packetSize])
|
|
||||||
data = data[packetSize:]
|
|
||||||
}
|
|
||||||
ctx.Write(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
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).Caller().Send()
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Write(data)
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
}
|
|
133
cmd/mp4/ws.go
Normal file
133
cmd/mp4/ws.go
Normal 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
|
||||||
|
}
|
@@ -8,8 +8,6 @@ import (
|
|||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("rtmp", handle)
|
streams.HandleFunc("rtmp", handle)
|
||||||
streams.HandleFunc("http", handle)
|
|
||||||
streams.HandleFunc("https", handle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (streamer.Producer, error) {
|
func handle(url string) (streamer.Producer, error) {
|
||||||
@@ -17,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
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,9 @@ import (
|
|||||||
func Init() {
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +54,13 @@ func Init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go tcpHandler(conn)
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -85,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) {
|
||||||
@@ -119,13 +129,12 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpHandler(c net.Conn) {
|
func tcpHandler(conn *rtsp.Conn) {
|
||||||
var name string
|
var name string
|
||||||
var closer func()
|
var closer func()
|
||||||
|
|
||||||
trace := log.Trace().Enabled()
|
trace := log.Trace().Enabled()
|
||||||
|
|
||||||
conn := rtsp.NewServer(c)
|
|
||||||
conn.Listen(func(msg interface{}) {
|
conn.Listen(func(msg interface{}) {
|
||||||
if trace {
|
if trace {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
@@ -138,6 +147,11 @@ func tcpHandler(c net.Conn) {
|
|||||||
|
|
||||||
switch msg {
|
switch msg {
|
||||||
case rtsp.MethodDescribe:
|
case rtsp.MethodDescribe:
|
||||||
|
if len(conn.URL.Path) == 0 {
|
||||||
|
log.Warn().Msg("[rtsp] server empty URL on DESCRIBE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
name = conn.URL.Path[1:]
|
name = conn.URL.Path[1:]
|
||||||
|
|
||||||
stream := streams.Get(name)
|
stream := streams.Get(name)
|
||||||
@@ -159,6 +173,11 @@ func tcpHandler(c net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case rtsp.MethodAnnounce:
|
case rtsp.MethodAnnounce:
|
||||||
|
if len(conn.URL.Path) == 0 {
|
||||||
|
log.Warn().Msg("[rtsp] server empty URL on ANNOUNCE")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
name = conn.URL.Path[1:]
|
name = conn.URL.Path[1:]
|
||||||
|
|
||||||
stream := streams.Get(name)
|
stream := streams.Get(name)
|
||||||
|
@@ -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)
|
|
||||||
}
|
|
||||||
|
@@ -14,6 +14,7 @@ const (
|
|||||||
stateMedias
|
stateMedias
|
||||||
stateTracks
|
stateTracks
|
||||||
stateStart
|
stateStart
|
||||||
|
stateExternal
|
||||||
)
|
)
|
||||||
|
|
||||||
type Producer struct {
|
type Producer struct {
|
||||||
@@ -71,11 +72,6 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't get new tracks after start
|
|
||||||
if p.state == stateStart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
track := p.element.GetTrack(media, codec)
|
track := p.element.GetTrack(media, codec)
|
||||||
if track == nil {
|
if track == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -162,6 +158,16 @@ func (p *Producer) reconnect() {
|
|||||||
|
|
||||||
func (p *Producer) stop() {
|
func (p *Producer) stop() {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
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)
|
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
|
||||||
|
|
||||||
@@ -176,6 +182,4 @@ func (p *Producer) stop() {
|
|||||||
|
|
||||||
p.state = stateNone
|
p.state = stateNone
|
||||||
p.tracks = nil
|
p.tracks = nil
|
||||||
|
|
||||||
p.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,9 @@ 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"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,6 +57,8 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
consumer := &Consumer{element: cons}
|
consumer := &Consumer{element: cons}
|
||||||
var producers []*Producer // matched producers for consumer
|
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).
|
||||||
@@ -67,6 +71,8 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
log.Trace().Stringer("media", prodMedia).
|
log.Trace().Stringer("media", prodMedia).
|
||||||
Msgf("[streams] producer=%d candidate=%d", 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)
|
||||||
if prodCodec != nil {
|
if prodCodec != nil {
|
||||||
@@ -92,7 +98,12 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(producers) == 0 {
|
if len(producers) == 0 {
|
||||||
return errors.New("couldn't find the matching tracks")
|
s.stopProducers()
|
||||||
|
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.mu.Lock()
|
||||||
@@ -110,11 +121,6 @@ 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()
|
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 {
|
||||||
@@ -125,28 +131,13 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, producer := range s.producers {
|
|
||||||
if producer == nil {
|
|
||||||
log.Warn().Msgf("empty producer: %+v\n", s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var sink bool
|
|
||||||
for _, track := range producer.tracks {
|
|
||||||
if track.HasSink() {
|
|
||||||
sink = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !sink {
|
|
||||||
producer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
s.stopProducers()
|
||||||
}
|
}
|
||||||
|
|
||||||
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.mu.Lock()
|
||||||
s.producers = append(s.producers, producer)
|
s.producers = append(s.producers, producer)
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -163,6 +154,20 @@ func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Stream) stopProducers() {
|
||||||
|
s.mu.Lock()
|
||||||
|
producers:
|
||||||
|
for _, producer := range s.producers {
|
||||||
|
for _, track := range producer.tracks {
|
||||||
|
if track.HasSink() {
|
||||||
|
continue producers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
producer.stop()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
//func (s *Stream) Active() bool {
|
//func (s *Stream) Active() bool {
|
||||||
// if len(s.consumers) > 0 {
|
// if len(s.consumers) > 0 {
|
||||||
// return true
|
// return true
|
||||||
@@ -221,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
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/ioutil"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@@ -33,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ 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)
|
api.HandleFunc("api/webrtc", syncHandler)
|
||||||
}
|
}
|
||||||
@@ -66,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")
|
||||||
@@ -81,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})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -104,41 +106,35 @@ 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
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Write(&streamer.Message{
|
tr.Consumer = conn
|
||||||
Type: webrtc.MsgTypeAnswer, Value: answer,
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.Consumer = conn
|
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
|
||||||
|
|
||||||
|
asyncCandidates(tr)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -149,21 +145,21 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get offer
|
// get offer
|
||||||
offer, err := ioutil.ReadAll(r.Body)
|
offer, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
|
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Msg("ExchangeSDP")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// send SDP to client
|
// send SDP to client
|
||||||
if _, err = w.Write([]byte(answer)); err != nil {
|
if _, err = w.Write([]byte(answer)); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Msg("w.Write")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,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
|
||||||
}
|
}
|
||||||
@@ -209,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
|
||||||
|
@@ -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
14
go.mod
@@ -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
64
go.sum
@@ -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=
|
||||||
|
2
main.go
2
main.go
@@ -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()
|
||||||
|
@@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
- 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
20
pkg/aac/README.md
Normal 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
|
@@ -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:
|
||||||
|
@@ -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)
|
||||||
|
@@ -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) {
|
||||||
|
172
pkg/h265/rtp.go
172
pkg/h265/rtp.go
@@ -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
|
data := packet.Payload
|
||||||
//fmt.Printf(
|
nuType := (data[0] >> 1) & 0x3F
|
||||||
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n",
|
//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)
|
||||||
// track.Codec.Name, nut, len(packet.Payload), packet.Timestamp,
|
|
||||||
// packet.PayloadType, packet.SSRC, packet.SequenceNumber,
|
|
||||||
//)
|
|
||||||
|
|
||||||
switch nut {
|
if nuType == NALUTypeFU {
|
||||||
case h265parser.NAL_UNIT_UNSPECIFIED_49:
|
|
||||||
data := packet.Payload
|
|
||||||
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)
|
||||||
return nil
|
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||||
case h265parser.NAL_UNIT_SPS:
|
buf = append(buf, data...)
|
||||||
sps = packet.Payload
|
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
|
||||||
return nil
|
|
||||||
case h265parser.NAL_UNIT_PPS:
|
|
||||||
pps = packet.Payload
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
//panic("not implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var clone rtp.Packet
|
// collect all NAL Units for Access Unit
|
||||||
|
if !packet.Marker {
|
||||||
nut = (packet.Payload[0] >> 1) & 0x3f
|
return nil
|
||||||
if nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA {
|
|
||||||
clone = *packet
|
|
||||||
clone.Version = h264.RTPPacketVersionAVC
|
|
||||||
clone.Payload = h264.EncodeAVC(vps)
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
clone = *packet
|
|
||||||
clone.Version = h264.RTPPacketVersionAVC
|
|
||||||
clone.Payload = h264.EncodeAVC(sps)
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
clone = *packet
|
|
||||||
clone.Version = h264.RTPPacketVersionAVC
|
|
||||||
clone.Payload = h264.EncodeAVC(pps)
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone = *packet
|
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
clone.Version = h264.RTPPacketVersionAVC
|
clone.Version = h264.RTPPacketVersionAVC
|
||||||
clone.Payload = h264.EncodeAVC(packet.Payload)
|
clone.Payload = buf
|
||||||
|
|
||||||
|
buf = buf[:0]
|
||||||
|
|
||||||
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:]) {
|
||||||
default:
|
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||||
data = append([]byte{2}, data...)
|
start = 3
|
||||||
start = 0
|
default:
|
||||||
|
if start == 0 {
|
||||||
|
start = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += size
|
||||||
}
|
}
|
||||||
|
|
||||||
for len(data) > 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
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# Homekit
|
# Home Accessory Protocol
|
||||||
|
|
||||||
> PS. Character = Characteristic
|
> PS. Character = Characteristic
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
type Accessory struct {
|
type Accessory struct {
|
||||||
AID int `json:"aid"`
|
AID int `json:"aid"`
|
@@ -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
75
pkg/hap/camera/session.go
Normal 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)
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
733
pkg/hap/conn.go
Normal file
733
pkg/hap/conn.go
Normal 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
|
||||||
|
}
|
@@ -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 {
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
@@ -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)
|
|
||||||
}
|
|
@@ -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"`
|
|
||||||
}{
|
return nil
|
||||||
State: hap.M1,
|
}
|
||||||
PublicKey: sessionPublic[:],
|
|
||||||
}
|
func (c *Client) GetMedias() []*streamer.Media {
|
||||||
// 2. pack payload to TLV8
|
if c.medias == nil {
|
||||||
buf, err := tlv8.Marshal(requestM1)
|
c.medias = c.getMedias()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. send request
|
return c.medias
|
||||||
resp, err := c.Post(UriPairVerify, buf)
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M2: unpack deviceID from response
|
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||||
responseM2 := PairVerifyPayload{}
|
for _, track := range c.tracks {
|
||||||
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
|
if track.Codec == codec {
|
||||||
return err
|
return track
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
track := streamer.NewTrack(codec, media.Direction)
|
||||||
// 1. generate signature with our private key
|
c.tracks = append(c.tracks, track)
|
||||||
// (our session + our ID + device session)
|
return track
|
||||||
buf = nil
|
}
|
||||||
buf = append(buf, sessionPublic[:]...)
|
|
||||||
buf = append(buf, []byte(c.ClientID)...)
|
func (c *Client) Start() error {
|
||||||
buf = append(buf, responseM2.PublicKey[:]...)
|
if c.tracks == nil {
|
||||||
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
|
return errors.New("producer without tracks")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get our server local IP-address
|
||||||
|
host, _, err := net.SplitHostPort(c.conn.LocalAddr())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. generate payload
|
// TODO: set right config
|
||||||
payloadM3 := struct {
|
vp := &rtp.VideoParameters{
|
||||||
Identifier string `tlv8:"1"`
|
CodecType: rtp.VideoCodecType_H264,
|
||||||
Signature []byte `tlv8:"10"`
|
CodecParams: rtp.VideoCodecParameters{
|
||||||
}{
|
Profiles: []rtp.VideoCodecProfile{
|
||||||
Identifier: c.ClientID,
|
{Id: rtp.VideoCodecProfileMain},
|
||||||
Signature: signature,
|
},
|
||||||
|
Levels: []rtp.VideoCodecLevel{
|
||||||
|
{Level: rtp.VideoCodecLevel4},
|
||||||
|
},
|
||||||
|
Packetizations: []rtp.VideoCodecPacketization{
|
||||||
|
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Attributes: rtp.VideoCodecAttributes{
|
||||||
|
Width: 1920, Height: 1080, Framerate: 30,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
// 3. pack payload to TLV8
|
|
||||||
buf, err = tlv8.Marshal(payloadM3)
|
ap := &rtp.AudioParameters{
|
||||||
if err != nil {
|
CodecType: rtp.AudioCodecType_AAC_ELD,
|
||||||
|
CodecParams: rtp.AudioCodecParameters{
|
||||||
|
Channels: 1,
|
||||||
|
Bitrate: rtp.AudioCodecBitrateVariable,
|
||||||
|
Samplerate: rtp.AudioCodecSampleRate16Khz,
|
||||||
|
// packet time=20 => AAC-ELD packet size=480
|
||||||
|
// packet time=30 => AAC-ELD packet size=480
|
||||||
|
// packet time=40 => AAC-ELD packet size=480
|
||||||
|
// packet time=60 => AAC-LD packet size=960
|
||||||
|
PacketTime: 40,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup HomeKit stream session
|
||||||
|
hkSession := camera.NewSession(vp, ap)
|
||||||
|
hkSession.SetLocalEndpoint(host, c.server.Port())
|
||||||
|
|
||||||
|
// create client for processing camera accessory
|
||||||
|
cam := camera.NewClient(c.conn)
|
||||||
|
// try to start HomeKit stream
|
||||||
|
if err = cam.StartStream(hkSession); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. encrypt payload with session key
|
// SRTP Video Session
|
||||||
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
|
vs := &srtp.Session{
|
||||||
sessionKey[:], []byte("PV-Msg03"), buf, nil,
|
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
|
||||||
)
|
RemoteSSRC: hkSession.Answer.SsrcVideo,
|
||||||
|
|
||||||
// 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
|
if err = vs.SetKeys(
|
||||||
buf, err = tlv8.Marshal(requestM3)
|
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
|
||||||
if err != nil {
|
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = c.Post(UriPairVerify, buf)
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP M4. Read response
|
for _, track := range c.tracks {
|
||||||
responseM4 := PairVerifyPayload{}
|
switch track.Codec.Name {
|
||||||
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
|
case streamer.CodecH264:
|
||||||
return err
|
vs.Track = track
|
||||||
|
case streamer.CodecELD:
|
||||||
|
as.Track = track
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. check response state
|
c.server.AddSession(vs)
|
||||||
if responseM4.State != 4 || responseM4.Status != 0 {
|
c.server.AddSession(as)
|
||||||
return fmt.Errorf("wrong M4 response: %+v", responseM4)
|
|
||||||
|
c.sessions = []*srtp.Session{vs, as}
|
||||||
|
|
||||||
|
return <-c.exit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Stop() error {
|
||||||
|
err := c.conn.Close()
|
||||||
|
|
||||||
|
for _, session := range c.sessions {
|
||||||
|
c.server.RemoveSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
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) {
|
var medias []*streamer.Media
|
||||||
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)
|
accs, err := c.conn.GetAccessories()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M1. Generate request
|
|
||||||
reqM1 := struct {
|
|
||||||
Method byte `tlv8:"0"`
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
}{
|
|
||||||
State: hap.M1,
|
|
||||||
}
|
|
||||||
if mfi {
|
|
||||||
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
|
|
||||||
}
|
|
||||||
buf, err := tlv8.Marshal(reqM1)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M1. Send request
|
|
||||||
res, err := c.Post(UriPairSetup, buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M2. Read response
|
|
||||||
resM2 := struct {
|
|
||||||
Salt []byte `tlv8:"2"`
|
|
||||||
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
Error byte `tlv8:"7"`
|
|
||||||
}{}
|
|
||||||
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resM2.State != 2 || resM2.Error > 0 {
|
|
||||||
return fmt.Errorf("wrong M2: %+v", resM2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M3. Generate session using pin
|
|
||||||
username := []byte("Pair-Setup")
|
|
||||||
|
|
||||||
SRP, err := srp.NewSRP(
|
|
||||||
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SRP.SaltLength = 16
|
|
||||||
|
|
||||||
// username: "Pair-Setup"
|
|
||||||
// password: PIN (with dashes)
|
|
||||||
session := SRP.NewClientSession(username, []byte(pin))
|
|
||||||
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M3. Generate request
|
|
||||||
reqM3 := struct {
|
|
||||||
PublicKey []byte `tlv8:"3"`
|
|
||||||
Proof []byte `tlv8:"4"`
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
}{
|
|
||||||
PublicKey: session.GetA(), // client public key, aka session.A
|
|
||||||
Proof: session.ComputeAuthenticator(),
|
|
||||||
State: hap.M3,
|
|
||||||
}
|
|
||||||
buf, err = tlv8.Marshal(reqM3)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M3. Send request
|
|
||||||
res, err = c.Post(UriPairSetup, buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M4. Read response
|
|
||||||
resM4 := struct {
|
|
||||||
Proof []byte `tlv8:"4"` // server proof
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
Error byte `tlv8:"7"`
|
|
||||||
}{}
|
|
||||||
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resM4.Error == 2 {
|
|
||||||
return fmt.Errorf("wrong PIN: %s", pin)
|
|
||||||
}
|
|
||||||
if resM4.State != 4 || resM4.Error > 0 {
|
|
||||||
return fmt.Errorf("wrong M4: %+v", resM4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M4. Verify response
|
|
||||||
if !session.VerifyServerAuthenticator(resM4.Proof) {
|
|
||||||
return errors.New("verify server auth fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M5. Generate signature
|
|
||||||
saltKey, err := hkdf.Sha512(
|
|
||||||
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
|
|
||||||
[]byte("Pair-Setup-Controller-Sign-Info"),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = nil
|
|
||||||
buf = append(buf, saltKey[:]...)
|
|
||||||
buf = append(buf, []byte(c.ClientID)...)
|
|
||||||
buf = append(buf, c.ClientPublic()...)
|
|
||||||
|
|
||||||
signature, err := ed25519.Signature(c.ClientPrivate, buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M5. Generate payload
|
|
||||||
msgM5 := struct {
|
|
||||||
Identifier string `tlv8:"1"`
|
|
||||||
PublicKey []byte `tlv8:"3"`
|
|
||||||
Signature []byte `tlv8:"10"`
|
|
||||||
}{
|
|
||||||
Identifier: c.ClientID,
|
|
||||||
PublicKey: c.ClientPublic(),
|
|
||||||
Signature: signature,
|
|
||||||
}
|
|
||||||
buf, err = tlv8.Marshal(msgM5)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M5. Encrypt payload
|
|
||||||
sessionKey, err := hkdf.Sha512(
|
|
||||||
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
|
|
||||||
[]byte("Pair-Setup-Encrypt-Info"),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
|
|
||||||
sessionKey[:], []byte("PS-Msg05"), buf, nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
// STEP M5. Generate request
|
|
||||||
reqM5 := struct {
|
|
||||||
EncryptedData []byte `tlv8:"5"`
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
}{
|
|
||||||
EncryptedData: append(buf, mac[:]...),
|
|
||||||
State: hap.M5,
|
|
||||||
}
|
|
||||||
buf, err = tlv8.Marshal(reqM5)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M5. Send request
|
|
||||||
res, err = c.Post(UriPairSetup, buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M6. Read response
|
|
||||||
resM6 := struct {
|
|
||||||
EncryptedData []byte `tlv8:"5"`
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
Error byte `tlv8:"7"`
|
|
||||||
}{}
|
|
||||||
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resM6.State != 6 || resM6.Error > 0 {
|
|
||||||
return fmt.Errorf("wrong M6: %+v", resM2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M6. Decrypt payload
|
|
||||||
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
|
|
||||||
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
|
|
||||||
|
|
||||||
buf, err = chacha20poly1305.DecryptAndVerify(
|
|
||||||
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msgM6 := struct {
|
|
||||||
Identifier []byte `tlv8:"1"`
|
|
||||||
PublicKey []byte `tlv8:"3"`
|
|
||||||
Signature []byte `tlv8:"10"`
|
|
||||||
}{}
|
|
||||||
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M6. Verify payload
|
|
||||||
if saltKey, err = hkdf.Sha512(
|
|
||||||
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
|
|
||||||
[]byte("Pair-Setup-Accessory-Sign-Info"),
|
|
||||||
); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = nil
|
|
||||||
buf = append(buf, saltKey[:]...)
|
|
||||||
buf = append(buf, msgM6.Identifier...)
|
|
||||||
buf = append(buf, msgM6.PublicKey...)
|
|
||||||
|
|
||||||
if !ed25519.ValidateSignature(
|
|
||||||
msgM6.PublicKey[:], buf, msgM6.Signature,
|
|
||||||
) {
|
|
||||||
return errors.New("wrong server signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.DeviceID != string(msgM6.Identifier) {
|
|
||||||
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.DevicePublic = msgM6.PublicKey
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
if c.conn == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
conn := c.conn
|
|
||||||
c.conn = nil
|
|
||||||
return conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetAccessories() ([]*Accessory, error) {
|
acc := accs[0]
|
||||||
res, err := c.Get("/accessories")
|
|
||||||
if err != nil {
|
// get supported video config (not really necessary)
|
||||||
return nil, err
|
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
|
||||||
|
v1 := &rtp.VideoStreamConfiguration{}
|
||||||
|
if err = char.ReadTLV8(v1); err != nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(res.Body)
|
for _, hkCodec := range v1.Codecs {
|
||||||
if err != nil {
|
codec := &streamer.Codec{ClockRate: 90000}
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p := Accessories{}
|
switch hkCodec.Type {
|
||||||
if err = json.Unmarshal(data, &p); err != nil {
|
case rtp.VideoCodecType_H264:
|
||||||
return nil, err
|
codec.Name = streamer.CodecH264
|
||||||
}
|
codec.FmtpLine = "profile-level-id=420029"
|
||||||
|
default:
|
||||||
for _, accs := range p.Accessories {
|
fmt.Printf("unknown codec: %d", hkCodec.Type)
|
||||||
for _, serv := range accs.Services {
|
continue
|
||||||
for _, char := range serv.Characters {
|
|
||||||
char.AID = accs.AID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return p.Accessories, nil
|
media := &streamer.Media{
|
||||||
}
|
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
|
||||||
|
Codecs: []*streamer.Codec{codec},
|
||||||
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
|
medias = append(medias, media)
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
if data, err = json.Marshal(Characters{characters}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var res *http.Response
|
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
|
||||||
if res, err = c.Put("/characteristics", data); err != nil {
|
v2 := &rtp.AudioStreamConfiguration{}
|
||||||
return
|
if err = char.ReadTLV8(v2); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode >= 400 {
|
|
||||||
return errors.New("wrong response status")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetImage(width, height int) ([]byte, error) {
|
|
||||||
res, err := c.Post(
|
|
||||||
"/resource", []byte(fmt.Sprintf(
|
|
||||||
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
|
|
||||||
width, height,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (c *Client) onEventData(r io.Reader) error {
|
|
||||||
// if c.OnEvent == nil {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// data, err := io.ReadAll(r)
|
|
||||||
//
|
|
||||||
// ch := Characters{}
|
|
||||||
// if err = json.Unmarshal(data, &ch); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// c.OnEvent(ch.Characters)
|
|
||||||
//
|
|
||||||
// return nil
|
|
||||||
//}
|
|
||||||
|
|
||||||
func (c *Client) ListPairings() error {
|
|
||||||
pReq := struct {
|
|
||||||
Method byte `tlv8:"0"`
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
}{
|
|
||||||
Method: hap.MethodListPairings,
|
|
||||||
State: hap.M1,
|
|
||||||
}
|
|
||||||
data, err := tlv8.Marshal(pReq)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.Post("/pairings", data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err = io.ReadAll(res.Body)
|
|
||||||
// TODO: don't know how to fix array of items
|
|
||||||
var pRes struct {
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
Identifier string `tlv8:"1"`
|
|
||||||
PublicKey []byte `tlv8:"3"`
|
|
||||||
Permission byte `tlv8:"11"`
|
|
||||||
}
|
|
||||||
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
|
|
||||||
pReq := struct {
|
|
||||||
Method byte `tlv8:"0"`
|
|
||||||
Identifier string `tlv8:"1"`
|
|
||||||
PublicKey []byte `tlv8:"3"`
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
Permission byte `tlv8:"11"`
|
|
||||||
}{
|
|
||||||
Method: hap.MethodAddPairing,
|
|
||||||
Identifier: clientID,
|
|
||||||
PublicKey: clientPublic,
|
|
||||||
State: hap.M1,
|
|
||||||
Permission: hap.PermissionUser,
|
|
||||||
}
|
|
||||||
if admin {
|
|
||||||
pReq.Permission = hap.PermissionAdmin
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := tlv8.Marshal(pReq)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.Post("/pairings", data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err = io.ReadAll(res.Body)
|
|
||||||
var pRes struct {
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
Unknown byte `tlv8:"7"`
|
|
||||||
}
|
|
||||||
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) DeletePairing(id string) error {
|
|
||||||
reqM1 := struct {
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
Method byte `tlv8:"0"`
|
|
||||||
Identifier string `tlv8:"1"`
|
|
||||||
}{
|
|
||||||
State: hap.M1,
|
|
||||||
Method: hap.MethodDeletePairing,
|
|
||||||
Identifier: id,
|
|
||||||
}
|
|
||||||
data, err := tlv8.Marshal(reqM1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.Post("/pairings", data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err = io.ReadAll(res.Body)
|
|
||||||
var resM2 struct {
|
|
||||||
State byte `tlv8:"6"`
|
|
||||||
}
|
|
||||||
if err = tlv8.Unmarshal(data, &resM2); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resM2.State != hap.M2 {
|
|
||||||
return errors.New("wrong state")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) LocalAddr() string {
|
|
||||||
return c.conn.LocalAddr().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeKey(s string) []byte {
|
|
||||||
if s == "" {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
data, err := hex.DecodeString(s)
|
|
||||||
if err != nil {
|
for _, hkCodec := range v2.Codecs {
|
||||||
return nil
|
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 data
|
|
||||||
|
return medias
|
||||||
}
|
}
|
||||||
|
@@ -22,13 +22,17 @@ func Dial(uri string) (*Conn, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Accept(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Accept(res *http.Response) (*Conn, error) {
|
||||||
c := Conn{
|
c := Conn{
|
||||||
conn: res.Body,
|
conn: res.Body,
|
||||||
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
||||||
buf: make([]byte, 256),
|
buf: make([]byte, 256),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
if _, err := io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +53,9 @@ func Dial(uri string) (*Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
conn io.ReadCloser
|
conn io.ReadCloser
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
buf []byte
|
buf []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Streams() ([]av.CodecData, error) {
|
func (c *Conn) Streams() ([]av.CodecData, error) {
|
||||||
|
@@ -165,7 +165,7 @@ func (c *Client) getTracks() error {
|
|||||||
Name: streamer.CodecH264,
|
Name: streamer.CodecH264,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
|
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
|
||||||
PayloadType: streamer.PayloadTypeMP4,
|
PayloadType: streamer.PayloadTypeRAW,
|
||||||
}
|
}
|
||||||
|
|
||||||
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
||||||
@@ -192,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
|
||||||
|
@@ -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
152
pkg/mjpeg/client.go
Normal 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)
|
||||||
|
}
|
@@ -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
212
pkg/mjpeg/rtp.go
Normal 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
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
@@ -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
|
||||||
|
@@ -12,17 +12,29 @@ 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,
|
||||||
@@ -49,13 +61,18 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
codec := track.Codec
|
codec := track.Codec
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
|
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 {
|
||||||
return nil
|
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.wait = waitNone
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
@@ -66,23 +83,28 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wrapper streamer.WrapperFunc
|
var wrapper streamer.WrapperFunc
|
||||||
if codec.IsMP4() {
|
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.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 {
|
||||||
return nil
|
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.wait = waitNone
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
@@ -92,7 +114,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !codec.IsMP4() {
|
if codec.IsRTP() {
|
||||||
wrapper := h265.RTPDepay(track)
|
wrapper := h265.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
@@ -101,7 +123,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
|
|
||||||
case streamer.CodecAAC:
|
case streamer.CodecAAC:
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
if !c.start {
|
if c.wait != waitNone {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +134,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !codec.IsMP4() {
|
if codec.IsRTP() {
|
||||||
wrapper := aac.RTPDepay(track)
|
wrapper := aac.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
@@ -133,7 +155,9 @@ func (c *Consumer) Init() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Start() {
|
func (c *Consumer) Start() {
|
||||||
c.start = true
|
if c.wait == waitInit {
|
||||||
|
c.wait = waitKeyframe
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@@ -3,12 +3,10 @@ package mp4
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
|
||||||
"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/av"
|
||||||
"github.com/deepch/vdk/codec/aacparser"
|
|
||||||
"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"
|
||||||
@@ -21,8 +19,6 @@ 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 {
|
||||||
@@ -37,8 +33,9 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
|||||||
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:
|
case streamer.CodecAAC:
|
||||||
s += "mp4a.40.2"
|
s += "mp4a.40.2"
|
||||||
}
|
}
|
||||||
@@ -98,7 +95,10 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
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)
|
||||||
@@ -143,11 +143,6 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
codecData, err := aacparser.ParseMPEG4AudioConfigBytes(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
trak := TRAK(i + 1)
|
trak := TRAK(i + 1)
|
||||||
trak.Header.AlternateGroup = 1
|
trak.Header.AlternateGroup = 1
|
||||||
trak.Header.Duration = 0
|
trak.Header.Duration = 0
|
||||||
@@ -162,9 +157,9 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
|
|
||||||
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
||||||
DataRefIdx: 1,
|
DataRefIdx: 1,
|
||||||
NumberOfChannels: int16(codecData.ChannelLayout.Count()),
|
NumberOfChannels: int16(codec.Channels),
|
||||||
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
||||||
SampleRate: float64(codecData.SampleRate),
|
SampleRate: float64(codec.ClockRate),
|
||||||
Unknowns: []mp4io.Atom{ESDS(b)},
|
Unknowns: []mp4io.Atom{ESDS(b)},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +183,13 @@ 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(trackID byte, packet *rtp.Packet) []byte {
|
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||||
run := &mp4fio.TrackFragRun{
|
run := &mp4fio.TrackFragRun{
|
||||||
@@ -221,15 +219,16 @@ func (m *Muxer) Marshal(trackID byte, 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[trackID] > 0 {
|
if m.pts[trackID] > 0 {
|
||||||
//m.dts += uint64(newTime - m.pts)
|
|
||||||
entry.Duration = newTime - m.pts[trackID]
|
entry.Duration = newTime - m.pts[trackID]
|
||||||
m.dts[trackID] += uint64(entry.Duration)
|
m.dts[trackID] += uint64(entry.Duration)
|
||||||
|
} else {
|
||||||
|
// important, or Safari will fail with first frame
|
||||||
|
entry.Duration = 1
|
||||||
}
|
}
|
||||||
m.pts[trackID] = newTime
|
m.pts[trackID] = newTime
|
||||||
|
|
||||||
|
@@ -7,13 +7,20 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Keyframe struct {
|
type Segment struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
MimeType string
|
Medias []*streamer.Media
|
||||||
|
MimeType string
|
||||||
|
OnlyKeyframe bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Keyframe) GetMedias() []*streamer.Media {
|
func (c *Segment) GetMedias() []*streamer.Media {
|
||||||
|
if c.Medias != nil {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
// default medias
|
||||||
return []*streamer.Media{
|
return []*streamer.Media{
|
||||||
{
|
{
|
||||||
Kind: streamer.KindVideo,
|
Kind: streamer.KindVideo,
|
||||||
@@ -26,7 +33,7 @@ func (c *Keyframe) GetMedias() []*streamer.Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
muxer := &Muxer{}
|
muxer := &Muxer{}
|
||||||
|
|
||||||
codecs := []*streamer.Codec{track.Codec}
|
codecs := []*streamer.Codec{track.Codec}
|
||||||
@@ -40,22 +47,53 @@ func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
|
|
||||||
switch track.Codec.Name {
|
switch track.Codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
push := func(packet *rtp.Packet) error {
|
var push streamer.WriterFunc
|
||||||
if !h264.IsKeyframe(packet.Payload) {
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
buf := muxer.Marshal(0, packet)
|
push = func(packet *rtp.Packet) error {
|
||||||
c.Fire(append(init, buf...))
|
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...)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
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
|
var wrapper streamer.WrapperFunc
|
||||||
if track.Codec.IsMP4() {
|
if track.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)
|
||||||
|
|
||||||
@@ -73,7 +111,7 @@ func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !track.Codec.IsMP4() {
|
if track.Codec.IsRTP() {
|
||||||
wrapper := h265.RTPDepay(track)
|
wrapper := h265.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
@@ -89,7 +89,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !h264.IsAVC(codec) {
|
if !codec.IsRAW() {
|
||||||
wrapper := h264.RTPDepay(track)
|
wrapper := h264.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ func (c *Consumer) Init() ([]byte, error) {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Start() {
|
func (c *Consumer) Start() {
|
||||||
c.start = true
|
c.start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/rtmp"
|
"github.com/deepch/vdk/format/rtmp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"strings"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,16 +41,20 @@ func NewClient(uri string) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Dial() (err error) {
|
func (c *Client) Dial() (err error) {
|
||||||
if strings.HasPrefix(c.URI, "http") {
|
c.conn, err = rtmp.Dial(c.URI)
|
||||||
c.conn, err = httpflv.Dial(c.URI)
|
return
|
||||||
} else {
|
}
|
||||||
c.conn, err = rtmp.Dial(c.URI)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Accept - convert http.Response to Client
|
||||||
|
func Accept(res *http.Response) (*Client, error) {
|
||||||
|
conn, err := httpflv.Accept(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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 {
|
||||||
@@ -73,7 +77,7 @@ func (c *Client) Dial() (err error) {
|
|||||||
Name: streamer.CodecH264,
|
Name: streamer.CodecH264,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
FmtpLine: fmtp,
|
FmtpLine: fmtp,
|
||||||
PayloadType: streamer.PayloadTypeMP4,
|
PayloadType: streamer.PayloadTypeRAW,
|
||||||
}
|
}
|
||||||
|
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
@@ -83,9 +87,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)
|
||||||
|
|
||||||
case av.AAC:
|
case av.AAC:
|
||||||
@@ -98,7 +100,7 @@ func (c *Client) Dial() (err error) {
|
|||||||
Channels: uint16(cd.Config.ChannelConfig),
|
Channels: uint16(cd.Config.ChannelConfig),
|
||||||
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
// 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),
|
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
|
||||||
PayloadType: streamer.PayloadTypeMP4,
|
PayloadType: streamer.PayloadTypeRAW,
|
||||||
}
|
}
|
||||||
|
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
@@ -108,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:
|
||||||
|
107
pkg/rtsp/conn.go
107
pkg/rtsp/conn.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
"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"
|
||||||
@@ -44,6 +45,15 @@ const (
|
|||||||
ModeServerConsumer
|
ModeServerConsumer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type State byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateNone State = iota
|
||||||
|
StateConn
|
||||||
|
StateSetup
|
||||||
|
StatePlay
|
||||||
|
)
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
@@ -59,9 +69,9 @@ type Conn struct {
|
|||||||
// internal
|
// internal
|
||||||
|
|
||||||
auth *tcp.Auth
|
auth *tcp.Auth
|
||||||
closed bool
|
|
||||||
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,9 +123,6 @@ 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()
|
||||||
}
|
}
|
||||||
@@ -137,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
|
||||||
}
|
}
|
||||||
@@ -391,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
|
||||||
@@ -422,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:
|
||||||
@@ -437,33 +454,35 @@ 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.closed {
|
if c.state == StateNone {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := c.Teardown(); err != nil {
|
if err := c.Teardown(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.closed = true
|
c.state = StateNone
|
||||||
return c.conn.Close()
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,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 {
|
||||||
@@ -510,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
|
||||||
}
|
}
|
||||||
@@ -574,6 +602,7 @@ func (c *Conn) Accept() error {
|
|||||||
|
|
||||||
if strings.HasPrefix(tr, 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"
|
||||||
@@ -594,14 +623,22 @@ func (c *Conn) Accept() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Handle() (err error) {
|
func (c *Conn) Handle() (err error) {
|
||||||
|
if c.state != StateSetup {
|
||||||
|
return fmt.Errorf("RTSP Handle from wrong state: %d", c.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state = StatePlay
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if c.closed {
|
if c.state == StateNone {
|
||||||
err = nil
|
err = nil
|
||||||
} else {
|
return
|
||||||
// may have gotten here because of the deadline
|
|
||||||
// so close the connection to stop keepalive
|
|
||||||
_ = c.conn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// may have gotten here because of the deadline
|
||||||
|
// so close the connection to stop keepalive
|
||||||
|
c.state = StateNone
|
||||||
|
_ = c.conn.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var timeout time.Duration
|
var timeout time.Duration
|
||||||
@@ -625,7 +662,7 @@ func (c *Conn) Handle() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if c.closed {
|
if c.state == StateNone {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,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
|
||||||
}
|
}
|
||||||
@@ -718,7 +754,7 @@ func (c *Conn) keepalive() {
|
|||||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Second * 25)
|
time.Sleep(time.Second * 25)
|
||||||
if c.closed {
|
if c.state == StateNone {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := c.Request(req); err != nil {
|
if err := c.Request(req); err != nil {
|
||||||
@@ -740,13 +776,15 @@ 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.closed {
|
if c.state == StateNone {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
packet.Header.PayloadType = payloadType
|
packet.Header.PayloadType = payloadType
|
||||||
|
|
||||||
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
|
||||||
@@ -765,7 +803,7 @@ func (c *Conn) bindTrack(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if track.Codec.IsMP4() {
|
if !track.Codec.IsRTP() {
|
||||||
switch track.Codec.Name {
|
switch track.Codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
wrapper := h264.RTPPay(1500)
|
wrapper := h264.RTPPay(1500)
|
||||||
@@ -773,6 +811,9 @@ func (c *Conn) bindTrack(
|
|||||||
case streamer.CodecAAC:
|
case streamer.CodecAAC:
|
||||||
wrapper := aac.RTPPay(1500)
|
wrapper := aac.RTPPay(1500)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
|
case streamer.CodecJPEG:
|
||||||
|
wrapper := mjpeg.RTPPay()
|
||||||
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,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
|
||||||
|
@@ -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)
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/pion/rtcp"
|
"github.com/pion/rtcp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"github.com/pion/srtp/v2"
|
"github.com/pion/srtp/v2"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
@@ -16,6 +17,14 @@ type Session struct {
|
|||||||
|
|
||||||
Write func(b []byte) (int, error)
|
Write func(b []byte) (int, error)
|
||||||
Track *streamer.Track
|
Track *streamer.Track
|
||||||
|
|
||||||
|
lastSequence uint32
|
||||||
|
lastTimestamp uint32
|
||||||
|
//lastPacket *rtp.Packet
|
||||||
|
lastTime time.Time
|
||||||
|
jitter float64
|
||||||
|
//sequenceCycle uint16
|
||||||
|
totalLost uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) SetKeys(
|
func (s *Session) SetKeys(
|
||||||
@@ -37,13 +46,42 @@ func (s *Session) HandleRTP(data []byte) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Track == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
packet := &rtp.Packet{}
|
packet := &rtp.Packet{}
|
||||||
if err = packet.Unmarshal(data); err != nil {
|
if err = packet.Unmarshal(data); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// https://www.ietf.org/rfc/rfc3550.txt
|
||||||
|
if s.lastTimestamp != 0 {
|
||||||
|
delta := packet.SequenceNumber - uint16(s.lastSequence)
|
||||||
|
|
||||||
|
// lost packet
|
||||||
|
if delta > 1 {
|
||||||
|
s.totalLost += uint32(delta - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si)
|
||||||
|
dTime := now.Sub(s.lastTime).Seconds()*float64(s.Track.Codec.ClockRate) -
|
||||||
|
float64(packet.Timestamp-s.lastTimestamp)
|
||||||
|
if dTime < 0 {
|
||||||
|
dTime = -dTime
|
||||||
|
}
|
||||||
|
// J(i) = J(i-1) + (|D(i-1,i)| - J(i-1))/16
|
||||||
|
s.jitter += (dTime - s.jitter) / 16
|
||||||
|
}
|
||||||
|
|
||||||
|
// keeping cycles (overflow)
|
||||||
|
s.lastSequence = s.lastSequence&0xFFFF0000 | uint32(packet.SequenceNumber)
|
||||||
|
s.lastTimestamp = packet.Timestamp
|
||||||
|
s.lastTime = now
|
||||||
|
|
||||||
_ = s.Track.WriteRTP(packet)
|
_ = s.Track.WriteRTP(packet)
|
||||||
//s.Output(core.RTP{Channel: s.Channel, Packet: packet})
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,7 +98,6 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = packets
|
_ = packets
|
||||||
//s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets})
|
|
||||||
|
|
||||||
if header.Type == rtcp.TypeSenderReport {
|
if header.Type == rtcp.TypeSenderReport {
|
||||||
err = s.KeepAlive()
|
err = s.KeepAlive()
|
||||||
@@ -70,9 +107,25 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) KeepAlive() (err error) {
|
func (s *Session) KeepAlive() (err error) {
|
||||||
var data []byte
|
|
||||||
// we can send empty receiver response, but should send it to hold the connection
|
|
||||||
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
|
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
|
||||||
|
|
||||||
|
if s.lastTimestamp > 0 {
|
||||||
|
//log.Printf("[RTCP] ssrc=%d seq=%d lost=%d jit=%.2f", s.RemoteSSRC, s.lastSequence, s.totalLost, s.jitter)
|
||||||
|
|
||||||
|
rep.Reports = []rtcp.ReceptionReport{{
|
||||||
|
SSRC: s.RemoteSSRC,
|
||||||
|
LastSequenceNumber: s.lastSequence,
|
||||||
|
LastSenderReport: s.lastTimestamp,
|
||||||
|
FractionLost: 0, // TODO
|
||||||
|
TotalLost: s.totalLost,
|
||||||
|
Delay: 0, // send just after receive
|
||||||
|
Jitter: uint32(s.jitter),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can send empty receiver response, but should send it to hold the connection
|
||||||
|
|
||||||
|
var data []byte
|
||||||
if data, err = rep.Marshal(); err != nil {
|
if data, err = rep.Marshal(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -90,8 +143,8 @@ func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
|
|||||||
switch len(masterKey) {
|
switch len(masterKey) {
|
||||||
case 16:
|
case 16:
|
||||||
return srtp.ProtectionProfileAes128CmHmacSha1_80
|
return srtp.ProtectionProfileAes128CmHmacSha1_80
|
||||||
//case 32:
|
//case 32:
|
||||||
// return srtp.ProtectionProfileAes256CmHmacSha1_80
|
// return srtp.ProtectionProfileAes256CmHmacSha1_80
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -33,15 +33,17 @@ const (
|
|||||||
CodecOpus = "OPUS" // payloadType: 111
|
CodecOpus = "OPUS" // payloadType: 111
|
||||||
CodecG722 = "G722"
|
CodecG722 = "G722"
|
||||||
CodecMPA = "MPA" // payload: 14
|
CodecMPA = "MPA" // payload: 14
|
||||||
|
|
||||||
|
CodecELD = "ELD" // AAC-ELD
|
||||||
)
|
)
|
||||||
|
|
||||||
const PayloadTypeMP4 byte = 255
|
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 ""
|
||||||
@@ -137,8 +139,8 @@ func (c *Codec) String() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) IsMP4() bool {
|
func (c *Codec) IsRTP() bool {
|
||||||
return c.PayloadType == PayloadTypeMP4
|
return c.PayloadType != PayloadTypeRAW
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Clone() *Codec {
|
func (c *Codec) Clone() *Codec {
|
||||||
@@ -187,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)
|
||||||
|
|
||||||
|
@@ -13,12 +13,18 @@ type Track struct {
|
|||||||
Codec *Codec
|
Codec *Codec
|
||||||
Direction string
|
Direction string
|
||||||
sink map[*Track]WriterFunc
|
sink map[*Track]WriterFunc
|
||||||
sinkMu sync.RWMutex
|
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()
|
||||||
|
t.sinkMu.RLock()
|
||||||
s += fmt.Sprintf(", sinks=%d", len(t.sink))
|
s += fmt.Sprintf(", sinks=%d", len(t.sink))
|
||||||
|
t.sinkMu.RUnlock()
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,14 +44,12 @@ func (t *Track) Bind(w WriterFunc) *Track {
|
|||||||
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.sinkMu.Unlock()
|
t.sinkMu.Unlock()
|
||||||
|
|
||||||
return clone
|
return &clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) Unbind() {
|
func (t *Track) Unbind() {
|
||||||
@@ -55,7 +59,9 @@ func (t *Track) Unbind() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) GetSink(from *Track) {
|
func (t *Track) GetSink(from *Track) {
|
||||||
|
t.sinkMu.Lock()
|
||||||
t.sink = from.sink
|
t.sink = from.sink
|
||||||
|
t.sinkMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) HasSink() bool {
|
func (t *Track) HasSink() bool {
|
||||||
|
@@ -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
68
pkg/tcp/request.go
Normal 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
|
||||||
|
}
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 codec.IsMP4() {
|
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) {
|
||||||
|
@@ -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`
|
||||||
|
@@ -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
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
@SET GOOS=linux
|
|
||||||
@SET GOARCH=amd64
|
|
||||||
@cd ..
|
|
||||||
del go2rtc
|
|
||||||
go build -ldflags "-s -w" -trimpath
|
|
@@ -1,5 +0,0 @@
|
|||||||
@SET GOOS=linux
|
|
||||||
@SET GOARCH=arm
|
|
||||||
@cd ..
|
|
||||||
del go2rtc
|
|
||||||
go build -ldflags "-s -w" -trimpath
|
|
@@ -1,5 +0,0 @@
|
|||||||
@ECHO OFF
|
|
||||||
@SET GOOS=linux
|
|
||||||
@SET GOARCH=mipsle
|
|
||||||
cd ..
|
|
||||||
go build -ldflags "-s -w" -trimpath && upx-3.95 go2rtc
|
|
@@ -1,5 +0,0 @@
|
|||||||
@SET GOOS=darwin
|
|
||||||
@SET GOARCH=amd64
|
|
||||||
@cd ..
|
|
||||||
del go2rtc
|
|
||||||
go build -ldflags "-s -w" -trimpath
|
|
@@ -1,4 +0,0 @@
|
|||||||
@SET GOOS=windows
|
|
||||||
@SET GOARCH=amd64
|
|
||||||
cd ..
|
|
||||||
go build -ldflags "-w -s" -trimpath
|
|
@@ -55,3 +55,4 @@ pc.ontrack = ev => {
|
|||||||
- https://www.chromium.org/audio-video/
|
- https://www.chromium.org/audio-video/
|
||||||
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
|
- 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://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
|
||||||
|
114
www/index.html
114
www/index.html
@@ -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>',
|
||||||
];
|
];
|
||||||
|
|
||||||
function reload() {
|
document.querySelector("#add")
|
||||||
fetch(`${baseUrl}/api/streams`).then(r => {
|
.addEventListener("click", () => {
|
||||||
r.json().then(data => {
|
const src = document.querySelector("#src");
|
||||||
let html = '';
|
const url = new URL("api/streams", location.href);
|
||||||
|
url.searchParams.set("src", src.value);
|
||||||
|
fetch(url, {method: "PUT"}).then(reload);
|
||||||
|
});
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(data)) {
|
document.querySelector(".controls > button")
|
||||||
const online = value !== null ? value.length : 0
|
.addEventListener("click", () => {
|
||||||
html += `<tr><td>${name || 'default'}</td><td>${online}</td><td>`;
|
const url = new URL("stream.html", location.href);
|
||||||
links.forEach(link => {
|
|
||||||
html += link.replace('{name}', encodeURIComponent(name)) + ' ';
|
|
||||||
})
|
|
||||||
html += `<a href="#" onclick="deleteStream('${name}')">delete</a>`;
|
|
||||||
html += `</td></tr>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = document.getElementById('streams').getElementsByTagName('tbody')[0];
|
const streams = document.querySelectorAll("#streams input");
|
||||||
content.innerHTML = html
|
streams.forEach(i => {
|
||||||
|
if (i.checked) url.searchParams.append("src", i.name);
|
||||||
});
|
});
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteStream(src) {
|
if (!url.searchParams.has("src")) return;
|
||||||
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src)}`, {method: 'DELETE'}).then(reload);
|
|
||||||
}
|
|
||||||
|
|
||||||
const addButton = document.querySelector('a#add');
|
let mode = document.querySelectorAll(".controls input");
|
||||||
addButton.onclick = () => {
|
mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(",");
|
||||||
let src = document.querySelector('input#src');
|
|
||||||
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src.value)}`, {method: 'PUT'}).then(reload);
|
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() {
|
||||||
|
const url = new URL("api/streams", location.href);
|
||||||
|
fetch(url).then(r => r.json()).then(data => {
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(data)) {
|
||||||
|
const online = value ? value.length : 0;
|
||||||
|
const links = templates.map(link => {
|
||||||
|
return link.replace("{name}", encodeURIComponent(name));
|
||||||
|
}).join(" ");
|
||||||
|
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
|
99
www/mse.html
99
www/mse.html
@@ -1,99 +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>
|
|
||||||
// support api_path
|
|
||||||
const baseUrl = location.origin + location.pathname.substr(
|
|
||||||
0, location.pathname.lastIndexOf("/")
|
|
||||||
);
|
|
||||||
const video = document.querySelector('#video');
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
let mediaSource, sourceBuffer, queueBuffer = [];
|
|
||||||
|
|
||||||
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
|
|
||||||
ws.binaryType = "arraybuffer";
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
mediaSource = new MediaSource();
|
|
||||||
video.src = URL.createObjectURL(mediaSource);
|
|
||||||
mediaSource.onsourceopen = () => {
|
|
||||||
mediaSource.onsourceopen = null;
|
|
||||||
URL.revokeObjectURL(video.src);
|
|
||||||
ws.send(JSON.stringify({"type": "mse"}));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
sourceBuffer.mode = "segments"; // segments or sequence
|
|
||||||
sourceBuffer.onupdateend = () => {
|
|
||||||
if (!sourceBuffer.updating && queueBuffer.length > 0) {
|
|
||||||
try {
|
|
||||||
sourceBuffer.appendBuffer(queueBuffer.shift());
|
|
||||||
} catch (e) {
|
|
||||||
// console.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (sourceBuffer.updating || queueBuffer.length > 0) {
|
|
||||||
queueBuffer.push(ev.data);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
sourceBuffer.appendBuffer(ev.data);
|
|
||||||
} catch (e) {
|
|
||||||
// console.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video.seekable.length > 0) {
|
|
||||||
const delay = video.seekable.end(video.seekable.length - 1) - video.currentTime;
|
|
||||||
if (delay < 1) {
|
|
||||||
video.playbackRate = 1;
|
|
||||||
} else if (delay > 10) {
|
|
||||||
video.playbackRate = 10;
|
|
||||||
} else if (delay > 2) {
|
|
||||||
video.playbackRate = Math.floor(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
video.onpause = () => {
|
|
||||||
ws.close();
|
|
||||||
setTimeout(init, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
61
www/stream.html
Normal file
61
www/stream.html
Normal 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
584
www/video-rtc.js
Normal 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);
|
@@ -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>
|
|
Reference in New Issue
Block a user