Compare commits

...

45 Commits

Author SHA1 Message Date
Alexey Khit fae59c7992 Update version to 0.1-rc.4 2022-11-24 02:00:31 +03:00
Alexey Khit aff34f1d21 Totally rewritten MSE player 2022-11-24 01:59:48 +03:00
Alexey Khit 65e7efa775 Support codecs negotiation for MSE 2022-11-23 21:45:10 +03:00
Alexey Khit 3c3e9d282b Update about H265 support in readme 2022-11-23 20:35:11 +03:00
Alexey Khit bd51069086 Add support CORS for API 2022-11-23 20:34:06 +03:00
Alexey Khit 1ddf7f1a6c Change trace log for stream.mp4 2022-11-23 12:57:11 +03:00
Alexey Khit 0e281e36d3 Fix race (concurency) for Track 2022-11-22 20:03:36 +03:00
Alexey Khit 3d6472cfb1 Update H265 preset for FFmpeg 2022-11-22 17:22:54 +03:00
Alexey Khit 7c31fa2ffd Fix empty SPS for MSE H265 2022-11-22 17:22:26 +03:00
Alexey Khit 0ed9d2410a Fix H265 support for MSE in Safari 2022-11-22 17:21:58 +03:00
Alexey Khit 1c89e7945e Remove printf for webrtc ontrack 2022-11-18 09:13:24 +03:00
Alexey Khit 48635ae341 Add two locks for Track 2022-11-18 09:12:48 +03:00
Alexey Khit fdb316910f Fix WebRTC async connection 2022-11-16 11:26:56 +03:00
Alexey Khit e29f2594fa Fix multiple transcoding when track not exists 2022-11-15 16:16:22 +03:00
Alexey Khit c3da7584b0 Add check on RTSP server requers with empty url path 2022-11-14 19:06:43 +03:00
Alexey Khit 1e247cba92 Igroner app media for WebRTC from Hass 2022-11-14 17:26:59 +03:00
Alexey Khit 01631d9eb0 Update readme 2022-11-14 14:57:43 +03:00
Alexey Khit 4b27d119f0 Update version to 0.1-rc.3 2022-11-14 09:50:47 +03:00
Alexey Khit dd55c03dc2 Add support multiple transcoding for ffmpeg 2022-11-14 02:26:14 +03:00
Alexey Khit a4eab1944a Add ffmpeg async option for HomeKit audio 2022-11-14 01:29:45 +03:00
Alexey Khit eea413a36c Support stream name as ffmpeg input 2022-11-14 01:22:07 +03:00
Alexey Khit cdd42a8ed2 Change HomeKit codec from AAC to ELD 2022-11-14 00:58:34 +03:00
Alexey Khit 4815ce1baf Fix stop ffmpeg without matching tracks 2022-11-14 00:58:34 +03:00
Alexey Khit e6d3939c78 Fix external producers 2022-11-13 21:33:09 +03:00
Alexey Khit 220b9ca318 Remove old example file 2022-11-13 20:54:19 +03:00
Alexey Khit d625620dfd Fix ffmpeg profile warning 2022-11-13 20:09:18 +03:00
Alexey Khit dd503f3410 Adds rotate template for ffmpeg 2022-11-13 20:05:54 +03:00
Alexey Khit 3e8e87bfcc Fix RTSP unknown response handler 2022-11-13 19:26:22 +03:00
Alexey Khit 64d218886e Add exec early exit handler 2022-11-13 19:24:26 +03:00
Alexey Khit e91ccc211e Change ffmpeg verbose level to error 2022-11-13 19:18:53 +03:00
Alexey Khit 9f8a219483 Exec stderr will show with debug log 2022-11-13 19:15:12 +03:00
Alexey Khit b617796941 Improve RTCP for HomeKit 2022-11-13 14:35:53 +03:00
Alexey Khit 77888fe086 Refactoring for HomeKit client 2022-11-11 22:47:14 +03:00
Alexey Khit 7bc3534bcb Add useful links to readmes 2022-11-11 22:44:54 +03:00
Alexey Khit 77bc0630d6 Add teaps to main readme 2022-11-11 22:44:49 +03:00
Alexey Khit 2f68711405 Fix MP4f test consumer 2022-11-11 22:44:34 +03:00
Alexey Khit b8cab5db60 Remove aacparser from MP4 muxer 2022-11-11 22:44:05 +03:00
Alexey Khit eae01be71f Add User-Agent to FFmpeg input and output 2022-11-11 18:04:31 +03:00
Alexey Khit 0127115180 Add User-Agent to go2rtc RTSP requests 2022-11-11 18:02:08 +03:00
Alexey Khit aef84cef6b Add go2rtc version info 2022-11-11 18:01:38 +03:00
Alexey Khit d478436758 Set video track for WebRTC always first 2022-11-11 16:33:08 +03:00
Alexey Khit f77db44529 Refactoring for HomeKit client 2022-11-11 13:24:09 +03:00
Alex X 149d1bf235 Merge pull request #101 from felipecrs/patch-2
Mention alternative method to import hass cameras
2022-11-09 20:34:31 +03:00
Felipe Santos b650475b10 Mention alternative method to import hass cameras 2022-11-09 13:00:30 -03:00
Alexey Khit 16e5406156 Update readme 2022-11-08 09:57:17 +03:00
58 changed files with 2168 additions and 1545 deletions
+94 -54
View File
@@ -9,6 +9,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS/HTTP](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams) - streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS/HTTP](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg) - streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit) - first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- first project in the World with support H265 for WebRTC in browser (only Safari)
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg) - on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- multi-source 2-way [codecs negotiation](#codecs-negotiation) - multi-source 2-way [codecs negotiation](#codecs-negotiation)
- mixing tracks from different sources to single stream - mixing tracks from different sources to single stream
@@ -26,35 +27,6 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- [MediaSoup](https://mediasoup.org/) framework routing idea - [MediaSoup](https://mediasoup.org/) framework routing idea
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap) - HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
Now you have stream with two sources - **RTSP and FFmpeg**:
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
```
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
![](assets/codecs.svg)
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## Fast start ## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) 1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
@@ -140,8 +112,8 @@ Available modules:
Available source types: Available source types:
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras - [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
- [rtmp](#source-rtmp) - `RTMP` streams - [rtmp](#source-rtmp) - `RTMP` and `HTTP-FLV` streams
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types) - [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and others)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam - [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration - [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [echo](#source-echo) - get stream link from bash or python - [echo](#source-echo) - get stream link from bash or python
@@ -207,24 +179,27 @@ streams:
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
# [MJPEG] video will be transcoded to H264 # [MJPEG] video will be transcoded to H264
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264 mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
# [RTSP] video with rotation, should be transcoded, so select H264 # [RTSP] video with rotation, should be transcoded, so select H264
rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#raw=-vf transpose=1#video=h264 rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
``` ```
All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac/16000`. All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/cmd/ffmpeg/ffmpeg.go): `h264`, `h264/ultra`, `h264/high`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
But you can override them via YAML config. You can also add your own formats to config and use them with source params. But you can override them via YAML config. You can also add your own formats to config and use them with source params.
```yaml ```yaml
ffmpeg: ffmpeg:
bin: ffmpeg # path to ffmpeg binary bin: ffmpeg # path to ffmpeg binary
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1" h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that support ffmpeg..." mycodec: "-any args that support ffmpeg..."
``` ```
Also you can use `raw` param for any additional FFmpeg arguments. As example for video rotation (`#raw=-vf transpose=1`). Remember that rotation is not possible without transcoding, so add supported codec as second param (`#video=h264`). - You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
#### Source: FFmpeg Device #### Source: FFmpeg Device
@@ -279,6 +254,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 +299,8 @@ streams:
aqara_g3: hass:Camera-Hub-G3-AB12 aqara_g3: hass:Camera-Hub-G3-AB12
``` ```
More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ONVIF](https://www.home-assistant.io/integrations/onvif/), and possibly others can also be imported by using [this method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-home-assistant-cameras-to-go2rtc-andor-frigate).
### Module: API ### Module: API
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`. The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
@@ -318,14 +313,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
@@ -555,24 +551,68 @@ PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted m
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 Safari | H264, H265* | H264, H265 | **no!**
Desktop Edge | H264 | H264, H265* | H264, H265* Desktop Edge | H264 | H264, H265* | H264, H265*
Desktop Firefox | H264 | H264 | H264 Desktop Firefox | H264 | H264 | H264
Desktop Opera | no | H264 | H264 iPad Safari 13+ | H264, H265* | H264, H265 | **no!**
iPhone Safari | H264, H265* | no | no iPhone Safari 13+ | H264, H265* | **no!** | **no!**
iPad Safari | H264, H265* | H264 | no Android Chrome 107+ | H264 | H264, H265* | H264
Android Chrome | H264 | H264 | H264 masOS Hass App | no | no | no
masOS Hass App | no | no | no
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2` - Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- MSE/MP4 audio codecs: not supported yet (should be: `AAC`)
- Chrome H265: [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/) - Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
- Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265 - Desktop Safari H265: Menu > Develop > Experimental > WebRTC H265
- iOS Safari H265: Settings > Safari > Advanced > Experimental > WebRTC H265 - iOS Safari H265: Settings > Safari > Advanced > Experimental > WebRTC H265
**Audio**
- WebRTC audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`
- MSE/MP4 audio codecs: `AAC`
**Apple devices**
- all Apple devices don't support MP4 stream (they only support progressive loading of static files)
- iPhones don't support MSE technology because it competes with the HLS technology, invented by Apple
- HLS is the worst technology for **live** streaming, it still exists only because of iPhones
## Codecs negotiation
For example, you want to watch RTSP-stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.
- this camera support 2-way audio standard **ONVIF Profile T**
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because its audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`
Now you have stream with two sources - **RTSP and FFmpeg**:
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus
```
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
![](assets/codecs.svg)
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
## TIPS
**Using apps for low RTSP delay**
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
## FAQ ## FAQ
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?** **Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
+29 -11
View File
@@ -17,6 +17,7 @@ func Init() {
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
BasePath string `yaml:"base_path"` BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"` StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
} `yaml:"api"` } `yaml:"api"`
} }
@@ -34,7 +35,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 +49,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")
} }
@@ -83,10 +86,25 @@ var basePath string
var log zerolog.Logger var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler) var wsHandlers = make(map[string]WSHandler)
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api] %s %s", r.Method, r.URL)
next.ServeHTTP(w, r)
})
}
func middlewareCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
next.ServeHTTP(w, r)
})
}
func streamsHandler(w http.ResponseWriter, r *http.Request) { func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
name := r.URL.Query().Get("name") name := r.URL.Query().Get("name")
if name == "" { if name == "" {
name = src name = src
} }
+4 -20
View File
@@ -4,35 +4,19 @@ import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"net/http" "net/http"
"net/url"
"strings"
"sync" "sync"
) )
func initWS() { func initWS(origin string) {
wsUp = &websocket.Upgrader{ wsUp = &websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 512000, WriteBufferSize: 512000,
} }
wsUp.CheckOrigin = func(r *http.Request) bool {
origin := r.Header["Origin"] if origin == "*" {
if len(origin) == 0 { wsUp.CheckOrigin = func(r *http.Request) bool {
return true return true
} }
o, err := url.Parse(origin[0])
if err != nil {
return false
}
if o.Host == r.Host {
return true
}
log.Trace().Msgf("[api.ws] origin: %s, host: %s", o.Host, r.Host)
// some users change Nginx external port using Docker port
// so origin will be with a port and host without
if i := strings.IndexByte(o.Host, ':'); i > 0 {
return o.Host[:i] == r.Host
}
return false
} }
} }
+6 -2
View File
@@ -10,6 +10,9 @@ import (
"runtime" "runtime"
) )
var Version = "0.1-rc.4"
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 {
+12 -1
View File
@@ -4,6 +4,7 @@ import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp" "github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
@@ -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
+18
View File
@@ -1,3 +1,17 @@
## FFplay output
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
- `7.11` - master clock, is the time from start of the stream/video
- `A-V` - av_diff, difference between audio and video timestamps
- `fd` - frames dropped
- `aq` - audio queue (0 - no delay)
- `vq` - video queue (0 - no delay)
- `sq` - subtitle queue
- `f` - timestamp error correction rate (Not 100% sure)
`M-V`, `M-A` means video stream only, audio stream only respectively.
## Devices Windows ## Devices Windows
``` ```
@@ -41,3 +55,7 @@
- https://trac.ffmpeg.org/wiki/DirectShow - https://trac.ffmpeg.org/wiki/DirectShow
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table - https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
- https://github.com/tuupola/esp_video/blob/master/README.md - https://github.com/tuupola/esp_video/blob/master/README.md
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
- https://slhck.info/video/2017/02/24/vbr-settings.html
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)
+102 -47
View File
@@ -4,9 +4,11 @@ import (
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec" "github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device" "github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"net/url" "net/url"
"strconv"
"strings" "strings"
) )
@@ -23,40 +25,48 @@ func Init() {
// inputs // inputs
"file": "-re -stream_loop -1 -i {input}", "file": "-re -stream_loop -1 -i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}", "http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -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"
+62 -71
View File
@@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/cmd/app/store" "github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns" "github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@@ -54,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"`
+4 -11
View File
@@ -3,6 +3,7 @@ package homekit
import ( import (
"github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
@@ -20,20 +21,12 @@ func Init() {
var log zerolog.Logger var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) { func streamHandler(url string) (streamer.Producer, error) {
client, err := homekit.NewClient(url) conn, err := homekit.NewClient(url, srtp.Server)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = client.Dial(); err != nil { if err = conn.Dial();err!=nil{
return nil, err return nil, err
} }
return conn, nil
// start gorutine for reading responses from camera
go func() {
if err = client.Handle(); err != nil {
log.Warn().Err(err).Msg("[homekit] client")
}
}()
return &Producer{client: client}, nil
} }
-189
View File
@@ -1,189 +0,0 @@
package homekit
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/homekit/camera"
pkg "github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
"net"
"strconv"
)
type Producer struct {
streamer.Element
client *homekit.Client
medias []*streamer.Media
tracks []*streamer.Track
sessions []*pkg.Session
}
func (c *Producer) GetMedias() []*streamer.Media {
if c.medias == nil {
c.medias = c.getMedias()
}
return c.medias
}
func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
track := &streamer.Track{Codec: codec, Direction: media.Direction}
c.tracks = append(c.tracks, track)
return track
}
func (c *Producer) Start() error {
if c.tracks == nil {
return errors.New("producer without tracks")
}
// get our server local IP-address
host, _, err := net.SplitHostPort(c.client.LocalAddr())
if err != nil {
return err
}
// get our server SRTP port
port, err := strconv.Atoi(srtp.Port)
if err != nil {
return err
}
// setup HomeKit stream session
hkSession := camera.NewSession()
hkSession.SetLocalEndpoint(host, uint16(port))
// create client for processing camera accessory
cam := camera.NewClient(c.client)
// try to start HomeKit stream
if err = cam.StartStream2(hkSession); err != nil {
panic(err) // TODO: fixme
}
// SRTP Video Session
vs := &pkg.Session{
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcVideo,
Track: c.tracks[0],
}
if err = vs.SetKeys(
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
); err != nil {
return err
}
// SRTP Audio Session
as := &pkg.Session{
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcAudio,
Track: &streamer.Track{},
}
if err = as.SetKeys(
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
); err != nil {
return err
}
srtp.AddSession(vs)
srtp.AddSession(as)
c.sessions = []*pkg.Session{vs, as}
return nil
}
func (c *Producer) Stop() error {
err := c.client.Close()
for _, session := range c.sessions {
srtp.RemoveSession(session)
}
return err
}
func (c *Producer) getMedias() []*streamer.Media {
var medias []*streamer.Media
accs, err := c.client.GetAccessories()
acc := accs[0]
if err != nil {
panic(err)
}
// get supported video config (not really necessary)
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
v1 := &rtp.VideoStreamConfiguration{}
if err = char.ReadTLV8(v1); err != nil {
panic(err)
}
for _, hkCodec := range v1.Codecs {
codec := &streamer.Codec{ClockRate: 90000}
switch hkCodec.Type {
case rtp.VideoCodecType_H264:
codec.Name = streamer.CodecH264
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
media := &streamer.Media{
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
v2 := &rtp.AudioStreamConfiguration{}
if err = char.ReadTLV8(v2); err != nil {
panic(err)
}
for _, hkCodec := range v2.Codecs {
codec := &streamer.Codec{
Channels: uint16(hkCodec.Parameters.Channels),
}
switch hkCodec.Type {
case rtp.AudioCodecType_AAC_ELD:
codec.Name = streamer.CodecAAC
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
switch hkCodec.Parameters.Samplerate {
case rtp.AudioCodecSampleRate8Khz:
codec.ClockRate = 8000
case rtp.AudioCodecSampleRate16Khz:
codec.ClockRate = 16000
case rtp.AudioCodecSampleRate24Khz:
codec.ClockRate = 24000
default:
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
}
media := &streamer.Media{
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
return medias
}
+2 -2
View File
@@ -63,12 +63,12 @@ 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("[api.mp4] %s %+v", r.Method, r.Header)
if isChromeFirst(w, r) || isSafari(w, r) { if isChromeFirst(w, r) || isSafari(w, r) {
return return
} }
log.Trace().Msgf("[api.mp4] %+v", r)
src := r.URL.Query().Get("src") src := r.URL.Query().Get("src")
stream := streams.GetOrNew(src) stream := streams.GetOrNew(src)
if stream == nil { if stream == nil {
+44
View File
@@ -5,6 +5,7 @@ import (
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
) )
const MsgTypeMSE = "mse" // fMP4 const MsgTypeMSE = "mse" // fMP4
@@ -22,6 +23,10 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
cons.UserAgent = ctx.Request.UserAgent() cons.UserAgent = ctx.Request.UserAgent()
cons.RemoteAddr = ctx.Request.RemoteAddr cons.RemoteAddr = ctx.Request.RemoteAddr
if codecs, ok := msg.Value.(string); ok {
cons.Medias = parseMedias(codecs)
}
cons.Listen(func(msg interface{}) { cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok { if data, ok := msg.([]byte); ok {
for len(data) > packetSize { for len(data) > packetSize {
@@ -55,3 +60,42 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
cons.Start() cons.Start()
} }
func parseMedias(codecs string) (medias []*streamer.Media) {
var videos []*streamer.Codec
var audios []*streamer.Codec
for _, name := range strings.Split(codecs, ",") {
switch name {
case "avc1.640029":
codec := &streamer.Codec{Name: streamer.CodecH264}
videos = append(videos, codec)
case "hvc1.1.6.L153.B0":
codec := &streamer.Codec{Name: streamer.CodecH265}
videos = append(videos, codec)
case "mp4a.40.2":
codec := &streamer.Codec{Name: streamer.CodecAAC}
audios = append(audios, codec)
}
}
if videos != nil {
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil {
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: audios,
}
medias = append(medias, media)
}
return
}
+12
View File
@@ -85,6 +85,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) {
@@ -138,6 +140,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 +166,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)
+6 -20
View File
@@ -3,7 +3,6 @@ package srtp
import ( import (
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/srtp" "github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/rs/zerolog"
"net" "net"
) )
@@ -24,36 +23,23 @@ func Init() {
return return
} }
log = app.GetLogger("srtp") log := app.GetLogger("srtp")
// create SRTP server (endpoint) for receiving video from HomeKit camera // create SRTP server (endpoint) for receiving video from HomeKit camera
conn, err := net.ListenPacket("udp", cfg.Mod.Listen) conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("[srtp] listen") log.Warn().Err(err).Caller().Send()
} }
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen") log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
_, Port, _ = net.SplitHostPort(cfg.Mod.Listen)
// run server // run server
go func() { go func() {
server = &srtp.Server{} Server = &srtp.Server{}
if err = server.Serve(conn); err != nil { if err = Server.Serve(conn); err != nil {
log.Warn().Err(err).Msg("[srtp] serve") log.Warn().Err(err).Caller().Send()
} }
}() }()
} }
var log zerolog.Logger var Server *srtp.Server
var server *srtp.Server
var Port string
func AddSession(session *srtp.Session) {
server.AddSession(session)
}
func RemoveSession(session *srtp.Session) {
server.RemoveSession(session)
}
+11 -7
View File
@@ -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()
} }
+18 -23
View File
@@ -92,6 +92,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
} }
if len(producers) == 0 { if len(producers) == 0 {
s.stopProducers()
return errors.New("couldn't find the matching tracks") return errors.New("couldn't find the matching tracks")
} }
@@ -110,11 +111,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 +121,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 +144,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
+21 -1
View File
@@ -13,7 +13,27 @@ func AddCandidate(address string) {
candidates = append(candidates, address) candidates = append(candidates, address)
} }
func addCanditates(answer string) (string, error) { func asyncCandidates(ctx *api.Context) {
for _, address := range candidates {
address, err := webrtc.LookupIP(address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Caller().Send()
continue
}
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
ctx.Write(&streamer.Message{Type: webrtc.MsgTypeCandidate, Value: cand})
}
}
func syncCanditates(answer string) (string, error) {
if len(candidates) == 0 { if len(candidates) == 0 {
return answer, nil return answer, nil
} }
+20 -25
View File
@@ -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,7 +55,7 @@ func Init() {
candidates = cfg.Mod.Candidates candidates = cfg.Mod.Candidates
api.HandleWS(webrtc.MsgTypeOffer, offerHandler) api.HandleWS(webrtc.MsgTypeOffer, asyncHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler) api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
api.HandleFunc("api/webrtc", syncHandler) api.HandleFunc("api/webrtc", syncHandler)
@@ -66,7 +66,7 @@ var log zerolog.Logger
var NewPConn func() (*pion.PeerConnection, error) var NewPConn func() (*pion.PeerConnection, error)
func offerHandler(ctx *api.Context, msg *streamer.Message) { func asyncHandler(ctx *api.Context, msg *streamer.Message) {
src := ctx.Request.URL.Query().Get("src") src := ctx.Request.URL.Query().Get("src")
stream := streams.Get(src) stream := streams.Get(src)
if stream == nil { if stream == nil {
@@ -81,7 +81,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
conn := new(webrtc.Conn) conn := new(webrtc.Conn)
conn.Conn, err = NewPConn() conn.Conn, err = NewPConn()
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn") log.Error().Err(err).Caller().Msg("NewPConn")
return return
} }
@@ -104,14 +104,14 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
log.Trace().Msgf("[webrtc] offer:\n%s", offer) log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil { if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer") log.Warn().Err(err).Caller().Msg("conn.SetOffer")
ctx.Error(err) ctx.Error(err)
return return
} }
// 2. AddConsumer, so we get new tracks // 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil { if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer") log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
_ = conn.Conn.Close() _ = conn.Conn.Close()
ctx.Error(err) ctx.Error(err)
return return
@@ -120,25 +120,20 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
conn.Init() conn.Init()
// exchange sdp without waiting all candidates // exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false) answer, err := conn.GetAnswer()
//answer, err := conn.GetAnswer()
answer, err := conn.GetCompleteAnswer()
if err == nil {
answer, err = addCanditates(answer)
}
log.Trace().Msgf("[webrtc] answer\n%s", answer) log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer") log.Error().Err(err).Caller().Msg("conn.GetAnswer")
ctx.Error(err) ctx.Error(err)
return return
} }
ctx.Write(&streamer.Message{
Type: webrtc.MsgTypeAnswer, Value: answer,
})
ctx.Consumer = conn ctx.Consumer = conn
ctx.Write(&streamer.Message{Type: webrtc.MsgTypeAnswer, Value: answer})
asyncCandidates(ctx)
} }
func syncHandler(w http.ResponseWriter, r *http.Request) { func syncHandler(w http.ResponseWriter, r *http.Request) {
@@ -151,19 +146,19 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
// get offer // get offer
offer, err := ioutil.ReadAll(r.Body) offer, err := ioutil.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 +169,7 @@ func ExchangeSDP(
conn := new(webrtc.Conn) conn := new(webrtc.Conn)
conn.Conn, err = NewPConn() conn.Conn, err = NewPConn()
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn") log.Error().Err(err).Caller().Msg("NewPConn")
return return
} }
@@ -192,13 +187,13 @@ func ExchangeSDP(
log.Trace().Msgf("[webrtc] offer:\n%s", offer) log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil { if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer") log.Warn().Err(err).Caller().Msg("conn.SetOffer")
return return
} }
// 2. AddConsumer, so we get new tracks // 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil { if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer") log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
_ = conn.Conn.Close() _ = conn.Conn.Close()
return return
} }
@@ -209,12 +204,12 @@ func ExchangeSDP(
//answer, err := conn.ExchangeSDP(offer, false) //answer, err := conn.ExchangeSDP(offer, false)
answer, err = conn.GetCompleteAnswer() answer, err = conn.GetCompleteAnswer()
if err == nil { if err == nil {
answer, err = addCanditates(answer) answer, err = syncCanditates(answer)
} }
log.Trace().Msgf("[webrtc] answer\n%s", answer) log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil { if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer") log.Error().Err(err).Caller().Msg("conn.GetCompleteAnswer")
} }
return return
-58
View File
@@ -1,58 +0,0 @@
package main
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/pion/rtp"
"os"
"time"
)
func main() {
client, err := rtsp.NewClient(os.Args[1])
if err != nil {
panic(err)
}
if err = client.Dial(); err != nil {
panic(err)
}
if err = client.Describe(); err != nil {
panic(err)
}
for _, media := range client.GetMedias() {
fmt.Printf("Media: %v\n", media)
if media.AV() {
track := client.GetTrack(media, media.Codecs[0])
fmt.Printf("Track: %v, %v\n", track, track.Codec)
track.Bind(func(packet *rtp.Packet) error {
nalUnitType := packet.Payload[0] & 0x1F
fmt.Printf(
"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
packet.PayloadType, packet.SSRC,
)
return nil
})
}
}
if err = client.Play(); err != nil {
panic(err)
}
time.AfterFunc(time.Second*5, func() {
if err = client.Close(); err != nil {
panic(err)
}
})
if err = client.Handle(); err != nil {
panic(err)
}
fmt.Println("The End")
}
+3 -1
View File
@@ -53,5 +53,7 @@ replace (
// windows support: https://github.com/brutella/dnssd/pull/35 // windows support: https://github.com/brutella/dnssd/pull/35
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1 github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
// RTP tlv8 fix // RTP tlv8 fix
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
// fix reading AAC config bytes
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
) )
+5 -4
View File
@@ -1,5 +1,7 @@
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78= github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0= github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
github.com/AlexxIT/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/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
@@ -7,8 +9,6 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1
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=
@@ -245,6 +245,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+4 -1
View File
@@ -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
View File
@@ -0,0 +1,20 @@
## AAC-LD and AAC-ELD
Codec | Rate | QuickTime | ffmpeg | VLC
------|------|-----------|--------|----
AAC-LD | 8000 | yes | no | no
AAC-LD | 16000 | yes | no | no
AAC-LD | 22050 | yes | yes | no
AAC-LD | 24000 | yes | yes | no
AAC-LD | 32000 | yes | yes | no
AAC-ELD | 8000 | yes | no | no
AAC-ELD | 16000 | yes | no | no
AAC-ELD | 22050 | yes | yes | yes
AAC-ELD | 24000 | yes | yes | yes
AAC-ELD | 32000 | yes | yes | yes
## Useful links
- [4.6.20 Enhanced Low Delay Codec](https://csclub.uwaterloo.ca/~ehashman/ISO14496-3-2009.pdf)
- https://stackoverflow.com/questions/40014508/aac-adts-for-aacobject-eld-packets
- https://code.videolan.org/videolan/vlc/-/blob/master/modules/packetizer/mpeg4audio.c
+1 -1
View File
@@ -24,7 +24,7 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
panic("you shall not pass!") panic("you shall not pass!")
} }
track := &streamer.Track{Codec: codec, Direction: media.Direction} track := streamer.NewTrack(codec, media.Direction)
switch media.Direction { switch media.Direction {
case streamer.DirectionSendonly: case streamer.DirectionSendonly:
+1
View File
@@ -1,3 +1,4 @@
## Useful links ## Useful links
- https://datatracker.ietf.org/doc/html/rfc7798 - https://datatracker.ietf.org/doc/html/rfc7798
- [Add initial support for WebRTC HEVC](https://trac.webkit.org/changeset/259452/webkit)
+42 -4
View File
@@ -2,19 +2,57 @@ package h265
import ( import (
"encoding/base64" "encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
) )
const ( const (
NALUnitTypeIFrame = 19 NALUTypePFrame = 1
NALUTypeIFrame = 19
NALUTypeIFrame2 = 20
NALUTypeIFrame3 = 21
NALUTypeVPS = 32
NALUTypeSPS = 33
NALUTypePPS = 34
NALUTypeFU = 49
) )
func NALUnitType(b []byte) byte { func NALUType(b []byte) byte {
return b[4] >> 1 return (b[4] >> 1) & 0x3F
} }
func IsKeyframe(b []byte) bool { func IsKeyframe(b []byte) bool {
return NALUnitType(b) == NALUnitTypeIFrame for {
switch NALUType(b) {
case NALUTypePFrame:
return false
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
return true
}
size := int(binary.BigEndian.Uint32(b)) + 4
if size < len(b) {
b = b[size:]
continue
} else {
return false
}
}
}
func Types(data []byte) []byte {
var types []byte
for {
types = append(types, NALUType(data))
size := 4 + int(binary.BigEndian.Uint32(data))
if size < len(data) {
data = data[size:]
} else {
break
}
}
return types
} }
func GetParameterSet(fmtp string) (vps, sps, pps []byte) { func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
+40 -57
View File
@@ -1,6 +1,7 @@
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/deepch/vdk/codec/h265parser"
@@ -8,77 +9,59 @@ import (
) )
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)
} }
@@ -106,13 +89,13 @@ func SafariPay(mtu uint16) streamer.WrapperFunc {
var start byte var start byte
nut := (data[4] >> 1) & 0b111111 nuType := (data[4] >> 1) & 0b111111
//fmt.Printf("[H265] nut: %2d, size: %6d, data: %16x\n", nut, len(data), data[4:20]) //fmt.Printf("[H265] nut: %2d, size: %6d, data: %16x\n", nut, len(data), data[4:20])
switch { switch {
case nut >= h265parser.NAL_UNIT_VPS && nut <= h265parser.NAL_UNIT_PPS: case nuType >= h265parser.NAL_UNIT_VPS && nuType <= h265parser.NAL_UNIT_PPS:
buffer = append(buffer, data...) buffer = append(buffer, data...)
return nil return nil
case nut >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nut <= h265parser.NAL_UNIT_CODED_SLICE_CRA: case nuType >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nuType <= h265parser.NAL_UNIT_CODED_SLICE_CRA:
buffer = append([]byte{3}, buffer...) buffer = append([]byte{3}, buffer...)
data = append(buffer, data...) data = append(buffer, data...)
start = 1 start = 1
+1 -1
View File
@@ -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
View File
@@ -0,0 +1,75 @@
package camera
import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/brutella/hap/rtp"
)
type Session struct {
Offer *rtp.SetupEndpoints
Answer *rtp.SetupEndpointsResponse
Config *rtp.StreamConfiguration
}
func NewSession(vp *rtp.VideoParameters, ap *rtp.AudioParameters) *Session {
vp.RTP = rtp.RTPParams{
PayloadType: 99,
Ssrc: RandomUint32(),
Bitrate: 2048,
Interval: 10,
MTU: 1200, // like WebRTC
}
ap.RTP = rtp.RTPParams{
PayloadType: 110,
Ssrc: RandomUint32(),
Bitrate: 32,
Interval: 10,
ComfortNoisePayloadType: 98,
MTU: 0,
}
sessionID := RandomBytes(16)
s := &Session{
Offer: &rtp.SetupEndpoints{
SessionId: sessionID,
Video: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
Audio: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &rtp.StreamConfiguration{
Command: rtp.SessionControlCommand{
Identifier: sessionID,
Type: rtp.SessionControlCommandTypeStart,
},
Video: *vp,
Audio: *ap,
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = rtp.Addr{
IPAddr: host,
VideoRtpPort: port,
AudioRtpPort: port,
}
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = cryptorand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = cryptorand.Read(data)
return binary.BigEndian.Uint32(data)
}
@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bytes" "bytes"
+733
View File
@@ -0,0 +1,733 @@
package hap
import (
"bufio"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
)
// Conn for HomeKit. DevicePublic can be null.
type Conn struct {
streamer.Element
DeviceAddress string // including port
DeviceID string
DevicePublic []byte
ClientID string
ClientPrivate []byte
OnEvent func(res *http.Response)
Output func(msg interface{})
conn net.Conn
secure *Secure
httpResponse chan *bufio.Reader
}
func NewConn(rawURL string) (*Conn, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
c := &Conn{
DeviceAddress: u.Host,
DeviceID: query.Get("device_id"),
DevicePublic: DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"),
ClientPrivate: DecodeKey(query.Get("client_private")),
}
return c, nil
}
func Pair(deviceID, pin string) (*Conn, error) {
entry := mdns.GetEntry(deviceID)
if entry == nil {
return nil, errors.New("can't find device via mDNS")
}
c := &Conn{
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
var mfi bool
for _, field := range entry.InfoFields {
if field[:2] == "ff" {
if field[3] == '1' {
mfi = true
}
break
}
}
return c, c.Pair(mfi, pin)
}
func (c *Conn) ClientPublic() []byte {
return c.ClientPrivate[32:]
}
func (c *Conn) URL() string {
return fmt.Sprintf(
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
)
}
func (c *Conn) DialAndServe() error {
if err := c.Dial(); err != nil {
return err
}
return c.Handle()
}
func (c *Conn) Dial() error {
// update device host before dial
if host := mdns.GetAddress(c.DeviceID); host != "" {
c.DeviceAddress = host
}
var err error
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, time.Second*5)
if err != nil {
return err
}
// STEP M1: send our session public to device
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. generate payload
// important not include other fields
requestM1 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
}{
State: hap.M1,
PublicKey: sessionPublic[:],
}
// 2. pack payload to TLV8
buf, err := tlv8.Marshal(requestM1)
if err != nil {
return err
}
// 3. send request
resp, err := c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M2: unpack deviceID from response
responseM2 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
return err
}
// 1. generate session shared key
var deviceSessionPublic [32]byte
copy(deviceSessionPublic[:], responseM2.PublicKey)
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
sessionKey, err := hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
)
// 2. decrypt M2 response with session key
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
var mac [16]byte
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
)
// 3. unpack payload from TLV8
payloadM2 := PairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
return err
}
// 4. verify signature for M2 response with device public
// device session + device id + our session
if c.DevicePublic != nil {
buf = nil
buf = append(buf, responseM2.PublicKey[:]...)
buf = append(buf, []byte(payloadM2.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
c.DevicePublic[:], buf, payloadM2.Signature,
) {
return errors.New("device public signature invalid")
}
}
// STEP M3: send our clientID to device
// 1. generate signature with our private key
// (our session + our ID + device session)
buf = nil
buf = append(buf, sessionPublic[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, responseM2.PublicKey[:]...)
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
if err != nil {
return err
}
// 2. generate payload
payloadM3 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
Signature: signature,
}
// 3. pack payload to TLV8
buf, err = tlv8.Marshal(payloadM3)
if err != nil {
return err
}
// 4. encrypt payload with session key
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg03"), buf, nil,
)
// 4. generate request
requestM3 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M3,
EncryptedData: append(msg, mac[:]...),
}
// 5. pack payload to TLV8
buf, err = tlv8.Marshal(requestM3)
if err != nil {
return err
}
resp, err = c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M4. Read response
responseM4 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
return err
}
// 1. check response state
if responseM4.State != 4 || responseM4.Status != 0 {
return fmt.Errorf("wrong M4 response: %+v", responseM4)
}
c.secure, err = NewSecure(sessionShared, false)
//c.secure.Buffer = bytes.NewBuffer(nil)
c.secure.Conn = c.conn
c.httpResponse = make(chan *bufio.Reader, 10)
return err
}
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
func (c *Conn) Pair(mfi bool, pin string) (err error) {
pin = strings.ReplaceAll(pin, "-", "")
if len(pin) != 8 {
return fmt.Errorf("wrong PIN format: %s", pin)
}
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:]
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return
}
// STEP M1. Generate request
reqM1 := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
State: hap.M1,
}
if mfi {
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
}
buf, err := tlv8.Marshal(reqM1)
if err != nil {
return
}
// STEP M1. Send request
res, err := c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M2. Read response
resM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
return
}
if resM2.State != 2 || resM2.Error > 0 {
return fmt.Errorf("wrong M2: %+v", resM2)
}
// STEP M3. Generate session using pin
username := []byte("Pair-Setup")
SRP, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
if err != nil {
return
}
SRP.SaltLength = 16
// username: "Pair-Setup"
// password: PIN (with dashes)
session := SRP.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
if err != nil {
return
}
// STEP M3. Generate request
reqM3 := struct {
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
PublicKey: session.GetA(), // client public key, aka session.A
Proof: session.ComputeAuthenticator(),
State: hap.M3,
}
buf, err = tlv8.Marshal(reqM3)
if err != nil {
return err
}
// STEP M3. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M4. Read response
resM4 := struct {
Proof []byte `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
return
}
if resM4.Error == 2 {
return fmt.Errorf("wrong PIN: %s", pin)
}
if resM4.State != 4 || resM4.Error > 0 {
return fmt.Errorf("wrong M4: %+v", resM4)
}
// STEP M4. Verify response
if !session.VerifyServerAuthenticator(resM4.Proof) {
return errors.New("verify server auth fail")
}
// STEP M5. Generate signature
saltKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
)
if err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, c.ClientPublic()...)
signature, err := ed25519.Signature(c.ClientPrivate, buf)
if err != nil {
return
}
// STEP M5. Generate payload
msgM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
PublicKey: c.ClientPublic(),
Signature: signature,
}
buf, err = tlv8.Marshal(msgM5)
if err != nil {
return
}
// STEP M5. Encrypt payload
sessionKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
)
if err != nil {
return
}
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg05"), buf, nil,
)
// STEP M5. Generate request
reqM5 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
EncryptedData: append(buf, mac[:]...),
State: hap.M5,
}
buf, err = tlv8.Marshal(reqM5)
if err != nil {
return err
}
// STEP M5. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M6. Read response
resM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
return
}
if resM6.State != 6 || resM6.Error > 0 {
return fmt.Errorf("wrong M6: %+v", resM2)
}
// STEP M6. Decrypt payload
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
)
if err != nil {
return
}
msgM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
return
}
// STEP M6. Verify payload
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, msgM6.Identifier...)
buf = append(buf, msgM6.PublicKey...)
if !ed25519.ValidateSignature(
msgM6.PublicKey[:], buf, msgM6.Signature,
) {
return errors.New("wrong server signature")
}
if c.DeviceID != string(msgM6.Identifier) {
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
}
c.DevicePublic = msgM6.PublicKey
return nil
}
func (c *Conn) Close() error {
if c.conn == nil {
return nil
}
conn := c.conn
c.conn = nil
return conn.Close()
}
func (c *Conn) GetAccessories() ([]*Accessory, error) {
res, err := c.Get("/accessories")
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
p := Accessories{}
if err = json.Unmarshal(data, &p); err != nil {
return nil, err
}
for _, accs := range p.Accessories {
for _, serv := range accs.Services {
for _, char := range serv.Characters {
char.AID = accs.AID
}
}
}
return p.Accessories, nil
}
func (c *Conn) GetCharacters(query string) ([]*Character, error) {
res, err := c.Get("/characteristics?id=" + query)
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return nil, err
}
return ch.Characters, nil
}
func (c *Conn) GetCharacter(char *Character) error {
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
chars, err := c.GetCharacters(query)
if err != nil {
return err
}
char.Value = chars[0].Value
return nil
}
func (c *Conn) PutCharacters(characters ...*Character) (err error) {
for i, char := range characters {
if char.Event != nil {
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
} else {
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
}
characters[i] = char
}
var data []byte
if data, err = json.Marshal(Characters{characters}); err != nil {
return
}
var res *http.Response
if res, err = c.Put("/characteristics", data); err != nil {
return
}
if res.StatusCode >= 400 {
return errors.New("wrong response status")
}
return
}
func (c *Conn) GetImage(width, height int) ([]byte, error) {
res, err := c.Post(
"/resource", []byte(fmt.Sprintf(
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
width, height,
)),
)
if err != nil {
return nil, err
}
return io.ReadAll(res.Body)
}
//func (c *Client) onEventData(r io.Reader) error {
// if c.OnEvent == nil {
// return nil
// }
//
// data, err := io.ReadAll(r)
//
// ch := Characters{}
// if err = json.Unmarshal(data, &ch); err != nil {
// return err
// }
//
// c.OnEvent(ch.Characters)
//
// return nil
//}
func (c *Conn) ListPairings() error {
pReq := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
Method: hap.MethodListPairings,
State: hap.M1,
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
// TODO: don't know how to fix array of items
var pRes struct {
State byte `tlv8:"6"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Conn) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
pReq := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}{
Method: hap.MethodAddPairing,
Identifier: clientID,
PublicKey: clientPublic,
State: hap.M1,
Permission: hap.PermissionUser,
}
if admin {
pReq.Permission = hap.PermissionAdmin
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var pRes struct {
State byte `tlv8:"6"`
Unknown byte `tlv8:"7"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Conn) DeletePairing(id string) error {
reqM1 := struct {
State byte `tlv8:"6"`
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
}{
State: hap.M1,
Method: hap.MethodDeletePairing,
Identifier: id,
}
data, err := tlv8.Marshal(reqM1)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var resM2 struct {
State byte `tlv8:"6"`
}
if err = tlv8.Unmarshal(data, &resM2); err != nil {
return err
}
if resM2.State != hap.M2 {
return errors.New("wrong state")
}
return nil
}
func (c *Conn) LocalAddr() string {
return c.conn.LocalAddr().String()
}
func DecodeKey(s string) []byte {
if s == "" {
return nil
}
data, err := hex.DecodeString(s)
if err != nil {
return nil
}
return data
}
@@ -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 {
+7 -7
View File
@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bufio" "bufio"
@@ -23,7 +23,7 @@ const (
UriResource = "/resource" UriResource = "/resource"
) )
func (c *Client) Write(p []byte) (r io.Reader, err error) { func (c *Conn) Write(p []byte) (r io.Reader, err error) {
if c.secure == nil { if c.secure == nil {
if _, err = c.conn.Write(p); err == nil { if _, err = c.conn.Write(p); err == nil {
r = bufio.NewReader(c.conn) r = bufio.NewReader(c.conn)
@@ -36,7 +36,7 @@ func (c *Client) Write(p []byte) (r io.Reader, err error) {
return return
} }
func (c *Client) Do(req *http.Request) (*http.Response, error) { func (c *Conn) Do(req *http.Request) (*http.Response, error) {
if c.secure == nil { if c.secure == nil {
// insecure requests // insecure requests
if err := req.Write(c.conn); err != nil { if err := req.Write(c.conn); err != nil {
@@ -56,7 +56,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
return http.ReadResponse(buf, req) return http.ReadResponse(buf, req)
} }
func (c *Client) Get(uri string) (*http.Response, error) { func (c *Conn) Get(uri string) (*http.Response, error) {
req, err := http.NewRequest( req, err := http.NewRequest(
"GET", "http://"+c.DeviceAddress+uri, nil, "GET", "http://"+c.DeviceAddress+uri, nil,
) )
@@ -66,7 +66,7 @@ func (c *Client) Get(uri string) (*http.Response, error) {
return c.Do(req) return c.Do(req)
} }
func (c *Client) Post(uri string, data []byte) (*http.Response, error) { func (c *Conn) Post(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest( req, err := http.NewRequest(
"POST", "http://"+c.DeviceAddress+uri, "POST", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data), bytes.NewReader(data),
@@ -85,7 +85,7 @@ func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
return c.Do(req) return c.Do(req)
} }
func (c *Client) Put(uri string, data []byte) (*http.Response, error) { func (c *Conn) Put(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest( req, err := http.NewRequest(
"PUT", "http://"+c.DeviceAddress+uri, "PUT", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data), bytes.NewReader(data),
@@ -102,7 +102,7 @@ func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
return c.Do(req) return c.Do(req)
} }
func (c *Client) Handle() (err error) { func (c *Conn) Handle() (err error) {
defer func() { defer func() {
if c.conn == nil { if c.conn == nil {
err = nil err = nil
@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bufio" "bufio"
+1 -1
View File
@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"encoding/binary" "encoding/binary"
+1 -1
View File
@@ -1,4 +1,4 @@
package homekit package hap
import ( import (
"bufio" "bufio"
-103
View File
@@ -1,103 +0,0 @@
package camera
import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/brutella/hap/rtp"
)
type Session struct {
Offer *rtp.SetupEndpoints
Answer *rtp.SetupEndpointsResponse
Config *rtp.StreamConfiguration
}
func NewSession() *Session {
sessionID := RandomBytes(16)
s := &Session{
Offer: &rtp.SetupEndpoints{
SessionId: sessionID,
Video: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
Audio: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &rtp.StreamConfiguration{
Command: rtp.SessionControlCommand{
Identifier: sessionID,
Type: rtp.SessionControlCommandTypeStart,
},
Video: rtp.VideoParameters{
CodecType: rtp.VideoCodecType_H264,
CodecParams: rtp.VideoCodecParameters{
Profiles: []rtp.VideoCodecProfile{
{Id: rtp.VideoCodecProfileMain},
},
Levels: []rtp.VideoCodecLevel{
{Level: rtp.VideoCodecLevel4},
},
Packetizations: []rtp.VideoCodecPacketization{
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
},
},
Attributes: rtp.VideoCodecAttributes{
Width: 1920, Height: 1080, Framerate: 30,
},
RTP: rtp.RTPParams{
PayloadType: 99,
Ssrc: RandomUint32(),
Bitrate: 299,
Interval: 0.5,
ComfortNoisePayloadType: 98,
MTU: 0,
},
},
Audio: rtp.AudioParameters{
CodecType: rtp.AudioCodecType_AAC_ELD,
CodecParams: rtp.AudioCodecParameters{
Channels: 1,
Bitrate: rtp.AudioCodecBitrateVariable,
Samplerate: rtp.AudioCodecSampleRate16Khz,
PacketTime: 30,
},
RTP: rtp.RTPParams{
PayloadType: 110,
Ssrc: RandomUint32(),
Bitrate: 24,
Interval: 5,
MTU: 13,
},
ComfortNoise: false,
},
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = rtp.Addr{
IPAddr: host,
VideoRtpPort: port,
AudioRtpPort: port,
}
}
func (s *Session) SetVideo() {
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = cryptorand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = cryptorand.Read(data)
return binary.BigEndian.Uint32(data)
}
+185 -652
View File
@@ -1,732 +1,265 @@
package homekit package homekit
import ( import (
"bufio"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns" "github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap" "github.com/brutella/hap/characteristic"
"github.com/brutella/hap/chacha20poly1305" "github.com/brutella/hap/rtp"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"io"
"net" "net"
"net/http"
"net/url" "net/url"
"strings"
) )
// Client for HomeKit. DevicePublic can be null.
type Client struct { type Client struct {
streamer.Element streamer.Element
DeviceAddress string // including port conn *hap.Conn
DeviceID string exit chan error
DevicePublic []byte server *srtp.Server
ClientID string url string
ClientPrivate []byte
OnEvent func(res *http.Response) medias []*streamer.Media
Output func(msg interface{}) tracks []*streamer.Track
conn net.Conn sessions []*srtp.Session
secure *Secure
httpResponse chan *bufio.Reader
} }
func NewClient(rawURL string) (*Client, error) { func NewClient(rawURL string, server *srtp.Server) (*Client, error) {
u, err := url.Parse(rawURL) u, err := url.Parse(rawURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
query := u.Query() query := u.Query()
c := &Client{ c := &hap.Conn{
DeviceAddress: u.Host, DeviceAddress: u.Host,
DeviceID: query.Get("device_id"), DeviceID: query.Get("device_id"),
DevicePublic: DecodeKey(query.Get("device_public")), DevicePublic: hap.DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"), ClientID: query.Get("client_id"),
ClientPrivate: DecodeKey(query.Get("client_private")), ClientPrivate: hap.DecodeKey(query.Get("client_private")),
} }
return c, nil return &Client{conn: c, server: server}, nil
}
func Pair(deviceID, pin string) (*Client, error) {
entry := mdns.GetEntry(deviceID)
if entry == nil {
return nil, errors.New("can't find device via mDNS")
}
c := &Client{
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
var mfi bool
for _, field := range entry.InfoFields {
if field[:2] == "ff" {
if field[3] == '1' {
mfi = true
}
break
}
}
return c, c.Pair(mfi, pin)
}
func (c *Client) ClientPublic() []byte {
return c.ClientPrivate[32:]
}
func (c *Client) URL() string {
return fmt.Sprintf(
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
)
}
func (c *Client) DialAndServe() error {
if err := c.Dial(); err != nil {
return err
}
return c.Handle()
} }
func (c *Client) Dial() error { func (c *Client) Dial() error {
// update device host before dial if err := c.conn.Dial(); err != nil {
if host := mdns.GetAddress(c.DeviceID); host != "" {
c.DeviceAddress = host
}
var err error
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return err return err
} }
// STEP M1: send our session public to device c.exit = make(chan error)
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. generate payload go func() {
// important not include other fields //start goroutine for reading responses from camera
requestM1 := struct { c.exit <- c.conn.Handle()
State byte `tlv8:"6"` }()
PublicKey []byte `tlv8:"3"`
}{ 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
} }
+1 -4
View File
@@ -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
+1
View File
@@ -21,3 +21,4 @@ Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressiv
- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec - https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
- https://jellyfin.org/docs/general/clients/codec-support.html - https://jellyfin.org/docs/general/clients/codec-support.html
- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding - https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding
- https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter
+6
View File
@@ -12,6 +12,7 @@ import (
type Consumer struct { type Consumer struct {
streamer.Element streamer.Element
Medias []*streamer.Media
UserAgent string UserAgent string
RemoteAddr string RemoteAddr string
@@ -23,6 +24,11 @@ type Consumer struct {
} }
func (c *Consumer) GetMedias() []*streamer.Media { func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{ return []*streamer.Media{
{ {
Kind: streamer.KindVideo, Kind: streamer.KindVideo,
+9 -12
View File
@@ -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"
@@ -37,8 +35,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 +97,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 +145,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 +159,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)},
} }
+1 -1
View File
@@ -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.IsMP4() {
wrapper := h264.RTPDepay(track) wrapper := h264.RTPDepay(track)
push = wrapper(push) push = wrapper(push)
} }
+2 -6
View File
@@ -83,9 +83,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:
@@ -108,9 +106,7 @@ func (c *Client) Dial() (err error) {
} }
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
track := &streamer.Track{ track := streamer.NewTrack(codec, media.Direction)
Codec: codec, Direction: media.Direction,
}
c.tracks = append(c.tracks, track) c.tracks = append(c.tracks, track)
default: default:
+45 -32
View File
@@ -44,6 +44,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 +68,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
@@ -108,9 +117,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 +143,7 @@ func (c *Conn) Dial() (err error) {
} }
c.reader = bufio.NewReader(c.conn) c.reader = bufio.NewReader(c.conn)
c.state = StateConn
return nil return nil
} }
@@ -422,9 +429,7 @@ func (c *Conn) SetupMedia(
return nil, err return nil, err
} }
track := &streamer.Track{ track := streamer.NewTrack(codec, media.Direction)
Codec: codec, Direction: media.Direction,
}
switch track.Direction { switch track.Direction {
case streamer.DirectionSendonly: case streamer.DirectionSendonly:
@@ -437,33 +442,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()
} }
@@ -510,9 +517,7 @@ func (c *Conn) Accept() error {
// TODO: fix someday... // TODO: fix someday...
c.channels = map[byte]*streamer.Track{} c.channels = map[byte]*streamer.Track{}
for i, media := range c.Medias { for i, media := range c.Medias {
track := &streamer.Track{ track := streamer.NewTrack(media.Codecs[0], media.Direction)
Codec: media.Codecs[0], Direction: media.Direction,
}
c.tracks = append(c.tracks, track) c.tracks = append(c.tracks, track)
c.channels[byte(i<<1)] = track c.channels[byte(i<<1)] = track
} }
@@ -574,6 +579,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 +600,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 +639,7 @@ func (c *Conn) Handle() (err error) {
} }
for { for {
if c.closed { if c.state == StateNone {
return return
} }
@@ -644,22 +658,21 @@ func (c *Conn) Handle() (err error) {
} }
if buf4[0] != '$' { if buf4[0] != '$' {
if string(buf4) == "RTSP" { switch string(buf4) {
case "RTSP":
var res *tcp.Response var res *tcp.Response
res, err = tcp.ReadResponse(c.reader) if res, err = tcp.ReadResponse(c.reader); err != nil {
if err != nil {
return return
} }
c.Fire(res) c.Fire(res)
} else { case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
var req *tcp.Request var req *tcp.Request
req, err = tcp.ReadRequest(c.reader) if req, err = tcp.ReadRequest(c.reader); err != nil {
if err != nil {
return return
} }
c.Fire(req) c.Fire(req)
default:
return fmt.Errorf("RTSP wrong input")
} }
continue continue
} }
@@ -718,7 +731,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,7 +753,7 @@ 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
+5
View File
@@ -20,6 +20,11 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
} }
} }
// can't setup new tracks from play state
if c.state == StatePlay {
return nil
}
track, err := c.SetupMedia(media, codec) track, err := c.SetupMedia(media, codec)
if err != nil { if err != nil {
return nil return nil
+12
View File
@@ -8,9 +8,19 @@ import (
// Server using same UDP port for SRTP and for SRTCP as the iPhone does // Server using same UDP port for SRTP and for SRTCP as the iPhone does
// this is not really necessary but anyway // this is not really necessary but anyway
type Server struct { type Server struct {
conn net.PacketConn
sessions map[uint32]*Session sessions map[uint32]*Session
} }
func (s *Server) Port() uint16 {
addr := s.conn.LocalAddr().(*net.UDPAddr)
return uint16(addr.Port)
}
func (s *Server) Close() error {
return s.conn.Close()
}
func (s *Server) AddSession(session *Session) { func (s *Server) AddSession(session *Session) {
if s.sessions == nil { if s.sessions == nil {
s.sessions = map[uint32]*Session{} s.sessions = map[uint32]*Session{}
@@ -23,6 +33,8 @@ func (s *Server) RemoveSession(session *Session) {
} }
func (s *Server) Serve(conn net.PacketConn) error { func (s *Server) Serve(conn net.PacketConn) error {
s.conn = conn
buf := make([]byte, 2048) buf := make([]byte, 2048)
for { for {
n, addr, err := conn.ReadFrom(buf) n, addr, err := conn.ReadFrom(buf)
+59 -6
View File
@@ -5,6 +5,7 @@ import (
"github.com/pion/rtcp" "github.com/pion/rtcp"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/srtp/v2" "github.com/pion/srtp/v2"
"time"
) )
type Session struct { type Session struct {
@@ -16,6 +17,14 @@ type Session struct {
Write func(b []byte) (int, error) Write func(b []byte) (int, error)
Track *streamer.Track Track *streamer.Track
lastSequence uint32
lastTimestamp uint32
//lastPacket *rtp.Packet
lastTime time.Time
jitter float64
//sequenceCycle uint16
totalLost uint32
} }
func (s *Session) SetKeys( func (s *Session) SetKeys(
@@ -37,13 +46,42 @@ func (s *Session) HandleRTP(data []byte) (err error) {
return return
} }
if s.Track == nil {
return
}
packet := &rtp.Packet{} packet := &rtp.Packet{}
if err = packet.Unmarshal(data); err != nil { if err = packet.Unmarshal(data); err != nil {
return return
} }
now := time.Now()
// https://www.ietf.org/rfc/rfc3550.txt
if s.lastTimestamp != 0 {
delta := packet.SequenceNumber - uint16(s.lastSequence)
// lost packet
if delta > 1 {
s.totalLost += uint32(delta - 1)
}
// D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si)
dTime := now.Sub(s.lastTime).Seconds()*float64(s.Track.Codec.ClockRate) -
float64(packet.Timestamp-s.lastTimestamp)
if dTime < 0 {
dTime = -dTime
}
// J(i) = J(i-1) + (|D(i-1,i)| - J(i-1))/16
s.jitter += (dTime - s.jitter) / 16
}
// keeping cycles (overflow)
s.lastSequence = s.lastSequence&0xFFFF0000 | uint32(packet.SequenceNumber)
s.lastTimestamp = packet.Timestamp
s.lastTime = now
_ = s.Track.WriteRTP(packet) _ = s.Track.WriteRTP(packet)
//s.Output(core.RTP{Channel: s.Channel, Packet: packet})
return return
} }
@@ -60,7 +98,6 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
} }
_ = packets _ = packets
//s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets})
if header.Type == rtcp.TypeSenderReport { if header.Type == rtcp.TypeSenderReport {
err = s.KeepAlive() err = s.KeepAlive()
@@ -70,9 +107,25 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
} }
func (s *Session) KeepAlive() (err error) { func (s *Session) KeepAlive() (err error) {
var data []byte
// we can send empty receiver response, but should send it to hold the connection
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC} rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
if s.lastTimestamp > 0 {
//log.Printf("[RTCP] ssrc=%d seq=%d lost=%d jit=%.2f", s.RemoteSSRC, s.lastSequence, s.totalLost, s.jitter)
rep.Reports = []rtcp.ReceptionReport{{
SSRC: s.RemoteSSRC,
LastSequenceNumber: s.lastSequence,
LastSenderReport: s.lastTimestamp,
FractionLost: 0, // TODO
TotalLost: s.totalLost,
Delay: 0, // send just after receive
Jitter: uint32(s.jitter),
}}
}
// we can send empty receiver response, but should send it to hold the connection
var data []byte
if data, err = rep.Marshal(); err != nil { if data, err = rep.Marshal(); err != nil {
return return
} }
@@ -90,8 +143,8 @@ func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
switch len(masterKey) { switch len(masterKey) {
case 16: case 16:
return srtp.ProtectionProfileAes128CmHmacSha1_80 return srtp.ProtectionProfileAes128CmHmacSha1_80
//case 32: //case 32:
// return srtp.ProtectionProfileAes256CmHmacSha1_80 // return srtp.ProtectionProfileAes256CmHmacSha1_80
} }
return 0 return 0
} }
+10 -2
View File
@@ -33,6 +33,8 @@ 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 PayloadTypeMP4 byte = 255
@@ -41,7 +43,7 @@ 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 ""
@@ -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)
+12 -6
View File
@@ -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 {
+19 -3
View File
@@ -1,7 +1,6 @@
package webrtc package webrtc
import ( import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
) )
@@ -58,7 +57,7 @@ func (c *Conn) Init() {
} }
} }
fmt.Printf("TODO: webrtc ontrack %+v\n", remote) //fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
}) })
// OK connection: // OK connection:
@@ -99,7 +98,24 @@ func (c *Conn) SetOffer(offer string) (err error) {
return return
} }
rawSDP := []byte(c.Conn.RemoteDescription().SDP) rawSDP := []byte(c.Conn.RemoteDescription().SDP)
c.medias, err = streamer.UnmarshalSDP(rawSDP) medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
return
}
// sort medias, so video will always be before audio
// and ignore application media from Hass default lovelace card
for _, media := range medias {
if media.Kind == streamer.KindVideo {
c.medias = append(c.medias, media)
}
}
for _, media := range medias {
if media.Kind == streamer.KindAudio {
c.medias = append(c.medias, media)
}
}
return return
} }
+1
View File
@@ -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
+7 -78
View File
@@ -3,97 +3,26 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - MSE</title> <title>go2rtc - MSE</title>
<script src="video-rtc.js"></script>
<style> <style>
body { body {
background: black;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
html, body { html, body, video {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
#video {
width: 100%;
height: 100%;
background: black;
}
</style> </style>
</head> </head>
<body> <body>
<!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video>
<script> <script>
// support api_path const url = new URL("api/ws" + location.search, location.href);
const baseUrl = location.origin + location.pathname.substr( const video = document.createElement("video-rtc");
0, location.pathname.lastIndexOf("/") video.src = url.toString();
); document.body.appendChild(video);
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> </script>
</body> </body>
</html> </html>
+374
View File
@@ -0,0 +1,374 @@
/**
* Common function for processing MSE and MSE2 data.
* @param ms MediaSource
*/
function MediaSourceHandler(ms) {
let sb, qb = [];
return ev => {
if (typeof ev.data === "string") {
const msg = JSON.parse(ev.data);
if (msg.type === "mse") {
if (!MediaSource.isTypeSupported(msg.value)) {
console.warn("Not supported: " + msg.value)
return;
}
sb = ms.addSourceBuffer(msg.value);
sb.mode = "segments"; // segments or sequence
sb.addEventListener("updateend", () => {
if (!sb.updating && qb.length > 0) {
try {
sb.appendBuffer(qb.shift());
} catch (e) {
// console.warn(e);
}
}
});
}
} else if (sb.updating || qb.length > 0) {
qb.push(ev.data);
// console.debug("buffer:", qb.length);
} else {
try {
sb.appendBuffer(ev.data);
} catch (e) {
// console.warn(e);
}
}
}
}
/**
* Dedicated Worker Handler for MSE2 https://chromestatus.com/feature/5177263249162240
*/
if (typeof importScripts == "function") {
// protect below code (class VideoRTC) from fail inside Worker
HTMLElement = Object;
customElements = {define: Function()};
const ms = new MediaSource();
ms.addEventListener("sourceopen", ev => {
postMessage({type: ev.type});
}, {once: true});
onmessage = MediaSourceHandler(ms);
postMessage({type: "handle", value: ms.handle}, [ms.handle]);
}
/**
* Video player for MSE and WebRTC connections.
*
* All modern web technologies are supported in almost any browser except Apple Safari.
*
* Support:
* - RTCPeerConnection for Safari iOS 11.0+
* - IntersectionObserver for Safari iOS 12.2+
* - MediaSource in Workers for Chrome 108+
*
* Doesn't support:
* - MediaSource for Safari iOS all
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
*/
class VideoRTC extends HTMLElement {
DISCONNECT_TIMEOUT = 5000;
RECONNECT_TIMEOUT = 30000;
CODECS = [
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen)
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC
"mp4a.40.5", // AAC HE
"mp4a.69", // MP3
"mp4a.6B", // MP3
];
/**
* Enable MediaSource in Workers mode.
* @type {boolean}
*/
MSE2 = true;
/**
* Run stream when not displayed on the screen. Default `false`.
* @type {boolean}
*/
background = false;
/**
* Run stream only when player in the viewport. Stop when user scroll out player.
* Value is percentage of visibility from `0` (not visible) to `1` (full visible).
* Default `0` - disable;
* @type {number}
*/
intersectionThreshold = 0;
/**
* Run stream only when browser page on the screen. Stop when user change browser
* tab or minimise browser windows.
* @type {boolean}
*/
visibilityCheck = true;
/**
* @type {HTMLVideoElement}
*/
video = null;
/**
* @type {RTCPeerConnection}
*/
pc = null;
/**
* @type {WebSocket}
*/
ws = null;
/**
* Internal WebSocket connection state. Values: CLOSED, CONNECTING, OPEN
* @type {number}
*/
wsState = WebSocket.CLOSED;
/**
* Internal WebSocket URL.
* @type {string}
*/
wsURL = "";
/**
* Internal disconnect TimeoutID.
* @type {number}
*/
disconnectTimeout = 0;
/**
* Internal reconnect TimeoutID.
* @type {number}
*/
reconnectTimeout = 0;
constructor() {
super();
console.debug("this.constructor");
this.video = document.createElement("video");
this.video.controls = true;
this.video.playsInline = true;
}
/** public properties **/
/**
* Set video source (WebSocket URL). Support relative path.
* @param value
*/
set src(value) {
if (value.startsWith("/")) {
value = "ws" + location.origin.substr(4) + value;
} else if (value.startsWith("http")) {
value = "ws" + value.substr(4);
}
this.wsURL = value;
if (this.isConnected) this.connectedCallback();
}
/**
* Play video. Support automute when autoplay blocked.
* https://developer.chrome.com/blog/autoplay/
*/
play() {
this.video.play().catch(er => {
if (er.name === "NotAllowedError" && !this.video.muted) {
this.video.muted = true;
this.video.play();
}
});
}
get codecs() {
return this.CODECS.filter(value => {
return MediaSource.isTypeSupported(`video/mp4; codecs="${value}"`);
}).join();
}
/**
* `CustomElement`. Invoked each time the custom element is appended into a
* document-connected element.
*/
connectedCallback() {
console.debug("this.connectedCallback", this.wsState);
if (this.disconnectTimeout) {
clearTimeout(this.disconnectTimeout);
this.disconnectTimeout = 0;
}
// because video autopause on disconnected from DOM
const seek = this.video.seekable;
if (seek.length > 0) {
this.video.currentTime = seek.end(seek.length - 1);
this.play();
}
if (!this.wsURL || this.wsState !== WebSocket.CLOSED) return;
// CLOSED => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.internalInit();
this.internalConnect();
}
/**
* `CustomElement`. Invoked each time the custom element is disconnected from the
* document's DOM.
*/
disconnectedCallback() {
console.debug("this.disconnectedCallback", this.wsState);
if (this.background || this.disconnectTimeout ||
this.wsState === WebSocket.CLOSED) return;
this.disconnectTimeout = setTimeout(() => {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = 0;
}
this.disconnectTimeout = 0;
// CONNECTING, OPEN => CLOSED
this.wsState = WebSocket.CLOSED;
if (this.ws) {
this.ws.close();
this.ws = null;
}
}, this.DISCONNECT_TIMEOUT);
}
internalInit() {
if (this.childElementCount) return;
this.appendChild(this.video);
if (this.background) return;
if ("hidden" in document && this.visibilityCheck) {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.disconnectedCallback();
} else if (this.isConnected) {
this.connectedCallback();
}
})
}
if ("IntersectionObserver" in window && this.intersectionThreshold) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) {
this.disconnectedCallback();
} else if (this.isConnected) {
this.connectedCallback();
}
});
}, {threshold: this.intersectionThreshold});
observer.observe(this);
}
}
internalConnect() {
if (this.wsState !== WebSocket.CONNECTING) return;
if (this.ws) throw "connect with non null WebSocket";
const ts = Date.now();
this.ws = new WebSocket(this.wsURL);
this.ws.binaryType = "arraybuffer";
this.ws.addEventListener("open", () => {
console.debug("ws.open", this.wsState);
if (this.wsState !== WebSocket.CONNECTING) return;
// CONNECTING => OPEN
this.wsState = WebSocket.OPEN;
});
this.ws.addEventListener("close", () => {
console.debug("ws.close", this.wsState);
if (this.wsState === WebSocket.CLOSED) return;
// CONNECTING, OPEN => CONNECTING
this.wsState = WebSocket.CONNECTING;
this.ws = null;
// reconnect no more than once every X seconds
const delay = Math.max(this.RECONNECT_TIMEOUT - (Date.now() - ts), 0);
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = 0;
this.internalConnect();
}, delay);
});
if ("MediaSource" in window && this.MSE2) {
if (MediaSource.canConstructInDedicatedWorker) {
this.internalMSE2();
} else {
this.internalMSE();
}
}
// TODO: this.internalRTC();
}
internalMSE() {
console.debug("this.internalMSE");
this.ws.addEventListener("open", () => {
const ms = new MediaSource();
ms.addEventListener("sourceopen", () => {
URL.revokeObjectURL(this.video.src);
this.ws.send(JSON.stringify({type: "mse", value: this.codecs}));
}, {once: true});
this.video.src = URL.createObjectURL(ms);
this.play();
this.ws.addEventListener("message", MediaSourceHandler(ms));
});
}
internalMSE2() {
console.debug("this.internalMSE2");
const worker = new Worker("video-rtc.js");
worker.addEventListener("message", ev => {
if (ev.data.type === "handle") {
this.video.srcObject = ev.data.value;
this.play();
} else if (ev.data.type === "sourceopen") {
this.ws.send(JSON.stringify({type: "mse", value: this.codecs}));
}
});
this.ws.addEventListener("message", ev => {
if (typeof ev.data === "string") {
worker.postMessage(ev.data);
} else {
worker.postMessage(ev.data, [ev.data]);
}
});
this.ws.addEventListener("close", () => {
worker.terminate();
});
}
internalRTC() {
if (!("RTCPeerConnection" in window)) return; // macOS Desktop app
}
}
customElements.define("video-rtc", VideoRTC);