mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-28 21:22:13 +08:00
Compare commits
69 Commits
v0.1-beta.
...
v0.1-rc.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4b27d119f0 | ||
![]() |
dd55c03dc2 | ||
![]() |
a4eab1944a | ||
![]() |
eea413a36c | ||
![]() |
cdd42a8ed2 | ||
![]() |
4815ce1baf | ||
![]() |
e6d3939c78 | ||
![]() |
220b9ca318 | ||
![]() |
d625620dfd | ||
![]() |
dd503f3410 | ||
![]() |
3e8e87bfcc | ||
![]() |
64d218886e | ||
![]() |
e91ccc211e | ||
![]() |
9f8a219483 | ||
![]() |
b617796941 | ||
![]() |
77888fe086 | ||
![]() |
7bc3534bcb | ||
![]() |
77bc0630d6 | ||
![]() |
2f68711405 | ||
![]() |
b8cab5db60 | ||
![]() |
eae01be71f | ||
![]() |
0127115180 | ||
![]() |
aef84cef6b | ||
![]() |
d478436758 | ||
![]() |
f77db44529 | ||
![]() |
149d1bf235 | ||
![]() |
b650475b10 | ||
![]() |
16e5406156 | ||
![]() |
49f6233bde | ||
![]() |
78c5c70c73 | ||
![]() |
32651c74ab | ||
![]() |
5c64d1f847 | ||
![]() |
717af29630 | ||
![]() |
ea18475d31 | ||
![]() |
701a9c69ec | ||
![]() |
c06253c8b2 | ||
![]() |
3a07e9fa03 | ||
![]() |
e1bc30fab3 | ||
![]() |
d16ae0972f | ||
![]() |
8b93c97e69 | ||
![]() |
d8158bc1e3 | ||
![]() |
f4f588d2c6 | ||
![]() |
e287b52808 | ||
![]() |
ff96257252 | ||
![]() |
909f21b7e4 | ||
![]() |
7d6a5b44f8 | ||
![]() |
278f7696b6 | ||
![]() |
3cbf2465ae | ||
![]() |
e9ea7a0b1f | ||
![]() |
0231fc3a90 | ||
![]() |
9ef2633840 | ||
![]() |
5a8df3e90a | ||
![]() |
a31cbec3eb | ||
![]() |
54f547977e | ||
![]() |
65d91e02bd | ||
![]() |
7fc3f0f641 | ||
![]() |
7725d5ed31 | ||
![]() |
6c1b9daa8b | ||
![]() |
6d432574bf | ||
![]() |
616f69c88b | ||
![]() |
f72440712b | ||
![]() |
ceed146fb8 | ||
![]() |
f17dadbbbf | ||
![]() |
3d4514eab9 | ||
![]() |
2629dccb81 | ||
![]() |
04f1aa2900 | ||
![]() |
0dacdea1c3 | ||
![]() |
24082b1616 | ||
![]() |
7964b1743b |
34
README.md
34
README.md
@@ -140,8 +140,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
|
||||||
@@ -172,6 +172,8 @@ streams:
|
|||||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
|
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**PS.** For disable bachannel just add `#backchannel=0` to end of RTSP link.
|
||||||
|
|
||||||
#### Source: RTMP
|
#### Source: RTMP
|
||||||
|
|
||||||
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
|
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
|
||||||
@@ -304,6 +306,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/`.
|
||||||
@@ -321,9 +325,9 @@ api:
|
|||||||
static_dir: "" # folder for static files (custom web interface)
|
static_dir: "" # folder for static files (custom web interface)
|
||||||
```
|
```
|
||||||
|
|
||||||
**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
|
||||||
|
|
||||||
@@ -385,13 +389,13 @@ ngrok:
|
|||||||
command: ...
|
command: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Own TCP-tunnel**
|
**Hard tech way 1. Own TCP-tunnel**
|
||||||
|
|
||||||
If you have personal VPS, you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
|
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create TCP-tunnel and setup in the same way as "Static public IP". But use your VPS IP-address in YAML config.
|
||||||
|
|
||||||
**Using TURN-server**
|
**Hard tech way 2. Using TURN-server**
|
||||||
|
|
||||||
TODO...
|
If you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can install TURN server (e.g. [coturn](https://github.com/coturn/coturn), config [example](https://github.com/AlexxIT/WebRTC/wiki/Coturn-Example)).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webrtc:
|
webrtc:
|
||||||
@@ -564,13 +568,23 @@ iPad Safari | H264, H265* | H264 | no
|
|||||||
Android Chrome | H264 | H264 | 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`
|
|
||||||
- MSE/MP4 audio codecs: not supported yet (should be: `AAC`)
|
|
||||||
- Chrome H265: [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
|
- 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`
|
||||||
|
|
||||||
|
## 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?**
|
||||||
|
@@ -64,22 +64,14 @@ func (ctx *Context) Close() {
|
|||||||
|
|
||||||
func (ctx *Context) Write(msg interface{}) {
|
func (ctx *Context) Write(msg interface{}) {
|
||||||
ctx.mu.Lock()
|
ctx.mu.Lock()
|
||||||
defer ctx.mu.Unlock()
|
|
||||||
|
|
||||||
var err error
|
if data, ok := msg.([]byte); ok {
|
||||||
|
_ = ctx.Conn.WriteMessage(websocket.BinaryMessage, data)
|
||||||
switch msg := msg.(type) {
|
} else {
|
||||||
case *streamer.Message:
|
_ = ctx.Conn.WriteJSON(msg)
|
||||||
err = ctx.Conn.WriteJSON(msg)
|
|
||||||
case []byte:
|
|
||||||
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
ctx.mu.Unlock()
|
||||||
//panic(err) // TODO: fix panic
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Error(err error) {
|
func (ctx *Context) Error(err error) {
|
||||||
|
@@ -3,12 +3,16 @@ package app
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Version = "0.1-rc.3"
|
||||||
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
config := flag.String(
|
config := flag.String(
|
||||||
"config",
|
"config",
|
||||||
@@ -30,10 +34,19 @@ func Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
||||||
|
|
||||||
|
modules = cfg.Mod
|
||||||
|
|
||||||
|
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
path, _ := os.Getwd()
|
||||||
|
log.Debug().Str("cwd", path).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(format string, level string) zerolog.Logger {
|
||||||
var writer io.Writer = os.Stdout
|
var writer io.Writer = os.Stdout
|
||||||
|
|
||||||
// styles
|
|
||||||
format := cfg.Mod["format"]
|
|
||||||
if format != "json" {
|
if format != "json" {
|
||||||
writer = zerolog.ConsoleWriter{
|
writer = zerolog.ConsoleWriter{
|
||||||
Out: writer, TimeFormat: "15:04:05.000",
|
Out: writer, TimeFormat: "15:04:05.000",
|
||||||
@@ -43,18 +56,12 @@ func Init() {
|
|||||||
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(cfg.Mod["level"])
|
lvl, err := zerolog.ParseLevel(level)
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
if err != nil || lvl == zerolog.NoLevel {
|
||||||
lvl = zerolog.InfoLevel
|
lvl = zerolog.InfoLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
||||||
|
|
||||||
modules = cfg.Mod
|
|
||||||
|
|
||||||
path, _ := os.Getwd()
|
|
||||||
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
|
|
||||||
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(v interface{}) {
|
func LoadConfig(v interface{}) {
|
||||||
@@ -68,15 +75,13 @@ func LoadConfig(v interface{}) {
|
|||||||
func GetLogger(module string) zerolog.Logger {
|
func GetLogger(module string) zerolog.Logger {
|
||||||
if s, ok := modules[module]; ok {
|
if s, ok := modules[module]; ok {
|
||||||
lvl, err := zerolog.ParseLevel(s)
|
lvl, err := zerolog.ParseLevel(s)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
log.Warn().Err(err).Msg("[log]")
|
return log.Level(lvl)
|
||||||
return log
|
|
||||||
}
|
}
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
return log.Level(lvl)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return log
|
return log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
@@ -84,8 +89,5 @@ func GetLogger(module string) zerolog.Logger {
|
|||||||
// data - config content
|
// data - config content
|
||||||
var data []byte
|
var data []byte
|
||||||
|
|
||||||
// log - main logger
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
// modules log levels
|
// modules log levels
|
||||||
var modules map[string]string
|
var modules map[string]string
|
||||||
|
@@ -21,6 +21,7 @@ var stackSkip = [][]byte{
|
|||||||
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
|
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
|
||||||
|
|
||||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
|
||||||
|
[]byte("created by github.com/AlexxIT/go2rtc/cmd/srtp.Init"),
|
||||||
|
|
||||||
// webrtc/api.go
|
// webrtc/api.go
|
||||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,22 +25,22 @@ func Init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rtsp.OnProducer = func(prod streamer.Producer) bool {
|
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
|
||||||
if conn := prod.(*pkg.Conn); conn != nil {
|
waitersMu.Lock()
|
||||||
if waiter := waiters[conn.URL.Path]; waiter != nil {
|
waiter := waiters[conn.URL.Path]
|
||||||
waiter <- prod
|
waitersMu.Unlock()
|
||||||
return true
|
|
||||||
}
|
if waiter == nil {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
waiter <- conn
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
streams.HandleFunc("exec", Handle)
|
streams.HandleFunc("exec", Handle)
|
||||||
|
|
||||||
log = app.GetLogger("exec")
|
log = app.GetLogger("exec")
|
||||||
|
|
||||||
// TODO: add sync.Mutex
|
|
||||||
waiters = map[string]chan streamer.Producer{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Handle(url string) (streamer.Producer, error) {
|
func Handle(url string) (streamer.Producer, error) {
|
||||||
@@ -55,13 +57,22 @@ func Handle(url string) (streamer.Producer, error) {
|
|||||||
|
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
|
}
|
||||||
|
if log.Debug().Enabled() {
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := make(chan streamer.Producer)
|
ch := make(chan streamer.Producer)
|
||||||
|
|
||||||
|
waitersMu.Lock()
|
||||||
waiters[path] = ch
|
waiters[path] = ch
|
||||||
defer delete(waiters, path)
|
waitersMu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
waitersMu.Lock()
|
||||||
|
delete(waiters, path)
|
||||||
|
waitersMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
log.Debug().Str("url", url).Msg("[exec] run")
|
log.Debug().Str("url", url).Msg("[exec] run")
|
||||||
|
|
||||||
@@ -72,11 +83,19 @@ func Handle(url string) (streamer.Producer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chErr := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
chErr <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 15):
|
case <-time.After(time.Second * 60):
|
||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
log.Error().Str("url", url).Msg("[exec] timeout")
|
||||||
return nil, errors.New("timeout")
|
return nil, errors.New("timeout")
|
||||||
|
case err := <-chErr:
|
||||||
|
return nil, fmt.Errorf("exec: %s", err)
|
||||||
case prod := <-ch:
|
case prod := <-ch:
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
||||||
return prod, nil
|
return prod, nil
|
||||||
@@ -86,4 +105,5 @@ func Handle(url string) (streamer.Producer, error) {
|
|||||||
// internal
|
// internal
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var waiters map[string]chan streamer.Producer
|
var waiters = map[string]chan streamer.Producer{}
|
||||||
|
var waitersMu sync.Mutex
|
||||||
|
@@ -1,3 +1,17 @@
|
|||||||
|
## FFplay output
|
||||||
|
|
||||||
|
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
|
||||||
|
|
||||||
|
- `7.11` - master clock, is the time from start of the stream/video
|
||||||
|
- `A-V` - av_diff, difference between audio and video timestamps
|
||||||
|
- `fd` - frames dropped
|
||||||
|
- `aq` - audio queue (0 - no delay)
|
||||||
|
- `vq` - video queue (0 - no delay)
|
||||||
|
- `sq` - subtitle queue
|
||||||
|
- `f` - timestamp error correction rate (Not 100% sure)
|
||||||
|
|
||||||
|
`M-V`, `M-A` means video stream only, audio stream only respectively.
|
||||||
|
|
||||||
## Devices Windows
|
## Devices Windows
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -41,3 +55,7 @@
|
|||||||
- https://trac.ffmpeg.org/wiki/DirectShow
|
- https://trac.ffmpeg.org/wiki/DirectShow
|
||||||
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
|
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
|
||||||
- https://github.com/tuupola/esp_video/blob/master/README.md
|
- https://github.com/tuupola/esp_video/blob/master/README.md
|
||||||
|
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
|
||||||
|
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
|
||||||
|
- https://slhck.info/video/2017/02/24/vbr-settings.html
|
||||||
|
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)
|
||||||
|
@@ -4,9 +4,11 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,40 +25,48 @@ func Init() {
|
|||||||
// inputs
|
// inputs
|
||||||
"file": "-re -stream_loop -1 -i {input}",
|
"file": "-re -stream_loop -1 -i {input}",
|
||||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||||
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -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 ultrafast -tune:v zerolatency",
|
||||||
"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"
|
||||||
|
@@ -55,6 +55,10 @@ func initAPI() {
|
|||||||
// /stream/{id}/channel/0/webrtc
|
// /stream/{id}/channel/0/webrtc
|
||||||
default:
|
default:
|
||||||
i := strings.IndexByte(r.RequestURI[8:], '/')
|
i := strings.IndexByte(r.RequestURI[8:], '/')
|
||||||
|
if i <= 0 {
|
||||||
|
log.Warn().Msgf("wrong request: %s", r.RequestURI)
|
||||||
|
return
|
||||||
|
}
|
||||||
name := r.RequestURI[8 : 8+i]
|
name := r.RequestURI[8 : 8+i]
|
||||||
|
|
||||||
stream := streams.Get(name)
|
stream := streams.Get(name)
|
||||||
|
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
|
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -54,91 +54,82 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
items = append(items, device)
|
items = append(items, device)
|
||||||
}
|
}
|
||||||
|
|
||||||
_= json.NewEncoder(w).Encode(items)
|
_ = json.NewEncoder(w).Encode(items)
|
||||||
|
|
||||||
case "POST":
|
case "POST":
|
||||||
// TODO: post params...
|
// TODO: post params...
|
||||||
|
|
||||||
id := r.URL.Query().Get("id")
|
id := r.URL.Query().Get("id")
|
||||||
pin := r.URL.Query().Get("pin")
|
pin := r.URL.Query().Get("pin")
|
||||||
|
|
||||||
client, err := homekit.Pair(id, pin)
|
|
||||||
if err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] pair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
name := r.URL.Query().Get("name")
|
name := r.URL.Query().Get("name")
|
||||||
dict := store.GetDict("streams")
|
if err := hkPair(id, pin, name); err != nil {
|
||||||
dict[name] = client.URL()
|
log.Error().Err(err).Caller().Send()
|
||||||
if err = store.Set("streams", dict); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] save to store")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
_, err = w.Write([]byte(err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
streams.New(name, client.URL())
|
|
||||||
|
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
dict := store.GetDict("streams")
|
if err := hkDelete(src); err != nil {
|
||||||
for name, rawURL := range dict {
|
log.Error().Err(err).Caller().Send()
|
||||||
if name != src {
|
_, err = w.Write([]byte(err.Error()))
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := homekit.NewClient(rawURL.(string))
|
|
||||||
if err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] new client")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Dial(); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] client dial")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go client.Handle()
|
|
||||||
|
|
||||||
if err = client.ListPairings(); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.DeletePairing(client.ClientID); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(dict, name)
|
|
||||||
|
|
||||||
if err = store.Set("streams", dict); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] store set")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hkPair(deviceID, pin, name string) (err error) {
|
||||||
|
var conn *hap.Conn
|
||||||
|
|
||||||
|
if conn, err = hap.Pair(deviceID, pin); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.New(name, conn.URL())
|
||||||
|
|
||||||
|
dict := store.GetDict("streams")
|
||||||
|
dict[name] = conn.URL()
|
||||||
|
|
||||||
|
return store.Set("streams", dict)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hkDelete(name string) (err error) {
|
||||||
|
dict := store.GetDict("streams")
|
||||||
|
for key, rawURL := range dict {
|
||||||
|
if key != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn *hap.Conn
|
||||||
|
|
||||||
|
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = conn.Dial(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err = conn.Handle(); err != nil {
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = conn.ListPairings(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = conn.DeletePairing(conn.ClientID); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(dict, name)
|
||||||
|
|
||||||
|
return store.Set("streams", dict)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
@@ -3,6 +3,7 @@ package homekit
|
|||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
@@ -20,20 +21,12 @@ func Init() {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func streamHandler(url string) (streamer.Producer, error) {
|
func streamHandler(url string) (streamer.Producer, error) {
|
||||||
client, err := homekit.NewClient(url)
|
conn, err := homekit.NewClient(url, srtp.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = client.Dial(); err != nil {
|
if err = conn.Dial();err!=nil{
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return conn, nil
|
||||||
// start gorutine for reading responses from camera
|
|
||||||
go func() {
|
|
||||||
if err = client.Handle(); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[homekit] client")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return &Producer{client: client}, nil
|
|
||||||
}
|
}
|
||||||
|
@@ -1,189 +0,0 @@
|
|||||||
package homekit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit/camera"
|
|
||||||
pkg "github.com/AlexxIT/go2rtc/pkg/srtp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/brutella/hap/characteristic"
|
|
||||||
"github.com/brutella/hap/rtp"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Producer struct {
|
|
||||||
streamer.Element
|
|
||||||
|
|
||||||
client *homekit.Client
|
|
||||||
medias []*streamer.Media
|
|
||||||
tracks []*streamer.Track
|
|
||||||
|
|
||||||
sessions []*pkg.Session
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) GetMedias() []*streamer.Media {
|
|
||||||
if c.medias == nil {
|
|
||||||
c.medias = c.getMedias()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.medias
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
|
||||||
for _, track := range c.tracks {
|
|
||||||
if track.Codec == codec {
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
track := &streamer.Track{Codec: codec, Direction: media.Direction}
|
|
||||||
c.tracks = append(c.tracks, track)
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) Start() error {
|
|
||||||
if c.tracks == nil {
|
|
||||||
return errors.New("producer without tracks")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get our server local IP-address
|
|
||||||
host, _, err := net.SplitHostPort(c.client.LocalAddr())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get our server SRTP port
|
|
||||||
port, err := strconv.Atoi(srtp.Port)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup HomeKit stream session
|
|
||||||
hkSession := camera.NewSession()
|
|
||||||
hkSession.SetLocalEndpoint(host, uint16(port))
|
|
||||||
|
|
||||||
// create client for processing camera accessory
|
|
||||||
cam := camera.NewClient(c.client)
|
|
||||||
// try to start HomeKit stream
|
|
||||||
if err = cam.StartStream2(hkSession); err != nil {
|
|
||||||
panic(err) // TODO: fixme
|
|
||||||
}
|
|
||||||
|
|
||||||
// SRTP Video Session
|
|
||||||
vs := &pkg.Session{
|
|
||||||
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
|
|
||||||
RemoteSSRC: hkSession.Answer.SsrcVideo,
|
|
||||||
Track: c.tracks[0],
|
|
||||||
}
|
|
||||||
if err = vs.SetKeys(
|
|
||||||
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
|
|
||||||
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SRTP Audio Session
|
|
||||||
as := &pkg.Session{
|
|
||||||
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
|
|
||||||
RemoteSSRC: hkSession.Answer.SsrcAudio,
|
|
||||||
Track: &streamer.Track{},
|
|
||||||
}
|
|
||||||
if err = as.SetKeys(
|
|
||||||
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
|
|
||||||
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
srtp.AddSession(vs)
|
|
||||||
srtp.AddSession(as)
|
|
||||||
|
|
||||||
c.sessions = []*pkg.Session{vs, as}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) Stop() error {
|
|
||||||
err := c.client.Close()
|
|
||||||
|
|
||||||
for _, session := range c.sessions {
|
|
||||||
srtp.RemoveSession(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) getMedias() []*streamer.Media {
|
|
||||||
var medias []*streamer.Media
|
|
||||||
|
|
||||||
accs, err := c.client.GetAccessories()
|
|
||||||
acc := accs[0]
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get supported video config (not really necessary)
|
|
||||||
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
|
|
||||||
v1 := &rtp.VideoStreamConfiguration{}
|
|
||||||
if err = char.ReadTLV8(v1); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, hkCodec := range v1.Codecs {
|
|
||||||
codec := &streamer.Codec{ClockRate: 90000}
|
|
||||||
|
|
||||||
switch hkCodec.Type {
|
|
||||||
case rtp.VideoCodecType_H264:
|
|
||||||
codec.Name = streamer.CodecH264
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &streamer.Media{
|
|
||||||
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
|
|
||||||
Codecs: []*streamer.Codec{codec},
|
|
||||||
}
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
|
|
||||||
v2 := &rtp.AudioStreamConfiguration{}
|
|
||||||
if err = char.ReadTLV8(v2); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, hkCodec := range v2.Codecs {
|
|
||||||
codec := &streamer.Codec{
|
|
||||||
Channels: uint16(hkCodec.Parameters.Channels),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch hkCodec.Type {
|
|
||||||
case rtp.AudioCodecType_AAC_ELD:
|
|
||||||
codec.Name = streamer.CodecAAC
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch hkCodec.Parameters.Samplerate {
|
|
||||||
case rtp.AudioCodecSampleRate8Khz:
|
|
||||||
codec.ClockRate = 8000
|
|
||||||
case rtp.AudioCodecSampleRate16Khz:
|
|
||||||
codec.ClockRate = 16000
|
|
||||||
case rtp.AudioCodecSampleRate24Khz:
|
|
||||||
codec.ClockRate = 24000
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &streamer.Media{
|
|
||||||
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
|
|
||||||
Codecs: []*streamer.Codec{codec},
|
|
||||||
}
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
return medias
|
|
||||||
}
|
|
@@ -10,12 +10,47 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/stream.mjpeg", handler)
|
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
||||||
|
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||||
|
src := r.URL.Query().Get("src")
|
||||||
|
stream := streams.GetOrNew(src)
|
||||||
|
if stream == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exit := make(chan []byte)
|
||||||
|
|
||||||
|
cons := &mjpeg.Consumer{}
|
||||||
|
cons.Listen(func(msg interface{}) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case []byte:
|
||||||
|
exit <- msg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := <-exit
|
||||||
|
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "image/jpeg")
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
||||||
|
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.GetOrNew(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -35,35 +36,29 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
exit := make(chan []byte)
|
exit := make(chan []byte)
|
||||||
|
|
||||||
cons := &mp4.Consumer{}
|
cons := &mp4.Keyframe{}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
switch msg := msg.(type) {
|
if data, ok := msg.([]byte); ok && exit != nil {
|
||||||
case []byte:
|
exit <- data
|
||||||
exit <- msg
|
exit = nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.keyframe] add consumer")
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer stream.RemoveConsumer(cons)
|
data := <-exit
|
||||||
|
|
||||||
w.Header().Set("Content-Type", cons.MimeType())
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
data, err := cons.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.keyframe] init")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data = append(data, <-exit...)
|
|
||||||
|
|
||||||
// Apple Safari won't show frame without length
|
// Apple Safari won't show frame without length
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
|
w.Header().Set("Content-Type", cons.MimeType)
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
if _, err := w.Write(data); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.keyframe] add consumer")
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,20 +75,20 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := make(chan struct{})
|
exit := make(chan error)
|
||||||
|
|
||||||
cons := &mp4.Consumer{}
|
cons := &mp4.Consumer{}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
switch msg := msg.(type) {
|
if data, ok := msg.([]byte); ok {
|
||||||
case []byte:
|
if _, err := w.Write(data); err != nil && exit != nil {
|
||||||
if _, err := w.Write(msg); err != nil {
|
exit <- err
|
||||||
exit <- struct{}{}
|
exit = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.mp4] add consumer")
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,18 +98,36 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
data, err := cons.Init()
|
data, err := cons.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("[api.mp4] init")
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = w.Write(data); err != nil {
|
if _, err = w.Write(data); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.mp4] write")
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
<-exit
|
cons.Start()
|
||||||
|
|
||||||
log.Trace().Msg("[api.mp4] close")
|
var duration *time.Timer
|
||||||
|
if s := r.URL.Query().Get("duration"); s != "" {
|
||||||
|
if i, _ := strconv.Atoi(s); i > 0 {
|
||||||
|
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
||||||
|
if exit != nil {
|
||||||
|
exit <- nil
|
||||||
|
exit = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = <-exit
|
||||||
|
|
||||||
|
log.Trace().Err(err).Caller().Send()
|
||||||
|
|
||||||
|
if duration != nil {
|
||||||
|
duration.Stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
|
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
@@ -9,6 +9,8 @@ import (
|
|||||||
|
|
||||||
const MsgTypeMSE = "mse" // fMP4
|
const MsgTypeMSE = "mse" // fMP4
|
||||||
|
|
||||||
|
const packetSize = 8192
|
||||||
|
|
||||||
func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
||||||
src := ctx.Request.URL.Query().Get("src")
|
src := ctx.Request.URL.Query().Get("src")
|
||||||
stream := streams.GetOrNew(src)
|
stream := streams.GetOrNew(src)
|
||||||
@@ -21,14 +23,17 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
|||||||
cons.RemoteAddr = ctx.Request.RemoteAddr
|
cons.RemoteAddr = ctx.Request.RemoteAddr
|
||||||
|
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
switch msg.(type) {
|
if data, ok := msg.([]byte); ok {
|
||||||
case *streamer.Message, []byte:
|
for len(data) > packetSize {
|
||||||
ctx.Write(msg)
|
ctx.Write(data[:packetSize])
|
||||||
|
data = data[packetSize:]
|
||||||
|
}
|
||||||
|
ctx.Write(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Warn().Err(err).Msg("[api.mse] add consumer")
|
log.Warn().Err(err).Caller().Send()
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -37,16 +42,16 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
|||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.Write(&streamer.Message{
|
ctx.Write(&streamer.Message{Type: MsgTypeMSE, Value: cons.MimeType()})
|
||||||
Type: MsgTypeMSE, Value: cons.MimeType(),
|
|
||||||
})
|
|
||||||
|
|
||||||
data, err := cons.Init()
|
data, err := cons.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("[api.mse] init")
|
log.Warn().Err(err).Caller().Send()
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Write(data)
|
ctx.Write(data)
|
||||||
|
|
||||||
|
cons.Start()
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,8 @@ import (
|
|||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("rtmp", handle)
|
streams.HandleFunc("rtmp", handle)
|
||||||
|
streams.HandleFunc("http", handle)
|
||||||
|
streams.HandleFunc("https", handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (streamer.Producer, error) {
|
func handle(url string) (streamer.Producer, error) {
|
||||||
|
212
cmd/rtsp/rtsp.go
212
cmd/rtsp/rtsp.go
@@ -32,20 +32,43 @@ func Init() {
|
|||||||
|
|
||||||
// RTSP server support
|
// RTSP server support
|
||||||
address := conf.Mod.Listen
|
address := conf.Mod.Listen
|
||||||
if address != "" {
|
if address == "" {
|
||||||
_, Port, _ = net.SplitHostPort(address)
|
return
|
||||||
|
|
||||||
go worker(address)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[rtsp] listen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, Port, _ = net.SplitHostPort(address)
|
||||||
|
|
||||||
|
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go tcpHandler(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler func(conn *rtsp.Conn) bool
|
||||||
|
|
||||||
|
func HandleFunc(handler Handler) {
|
||||||
|
handlers = append(handlers, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
var Port string
|
var Port string
|
||||||
|
|
||||||
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
|
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
var handlers []Handler
|
||||||
|
|
||||||
func rtspHandler(url string) (streamer.Producer, error) {
|
func rtspHandler(url string) (streamer.Producer, error) {
|
||||||
backchannel := true
|
backchannel := true
|
||||||
@@ -62,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) {
|
||||||
@@ -84,10 +109,10 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// second try without backchannel, we need to reconnect
|
// second try without backchannel, we need to reconnect
|
||||||
|
conn.Backchannel = false
|
||||||
if err = conn.Dial(); err != nil {
|
if err = conn.Dial(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
conn.Backchannel = false
|
|
||||||
if err = conn.Describe(); err != nil {
|
if err = conn.Describe(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -96,101 +121,89 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func worker(address string) {
|
func tcpHandler(c net.Conn) {
|
||||||
srv, err := tcp.NewServer(address)
|
var name string
|
||||||
if err != nil {
|
var closer func()
|
||||||
log.Error().Err(err).Msg("[rtsp] listen")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
trace := log.Trace().Enabled()
|
||||||
|
|
||||||
srv.Listen(func(msg interface{}) {
|
conn := rtsp.NewServer(c)
|
||||||
switch msg.(type) {
|
conn.Listen(func(msg interface{}) {
|
||||||
case net.Conn:
|
if trace {
|
||||||
var name string
|
switch msg := msg.(type) {
|
||||||
var onDisconnect func()
|
case *tcp.Request:
|
||||||
|
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
|
||||||
|
case *tcp.Response:
|
||||||
|
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trace := log.Trace().Enabled()
|
switch msg {
|
||||||
|
case rtsp.MethodDescribe:
|
||||||
|
name = conn.URL.Path[1:]
|
||||||
|
|
||||||
conn := rtsp.NewServer(msg.(net.Conn))
|
stream := streams.Get(name)
|
||||||
conn.Listen(func(msg interface{}) {
|
if stream == nil {
|
||||||
if trace {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *tcp.Request:
|
|
||||||
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
|
|
||||||
case *tcp.Response:
|
|
||||||
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg {
|
|
||||||
case rtsp.MethodDescribe:
|
|
||||||
name = conn.URL.Path[1:]
|
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
|
||||||
|
|
||||||
stream := streams.Get(name) // TODO: rewrite
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initMedias(conn)
|
|
||||||
|
|
||||||
if err = stream.AddConsumer(conn); err != nil {
|
|
||||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onDisconnect = func() {
|
|
||||||
stream.RemoveConsumer(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
case rtsp.MethodAnnounce:
|
|
||||||
if OnProducer != nil {
|
|
||||||
if OnProducer(conn) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
name = conn.URL.Path[1:]
|
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
|
||||||
|
|
||||||
stream := streams.Get(name)
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AddProducer(conn)
|
|
||||||
|
|
||||||
onDisconnect = func() {
|
|
||||||
stream.RemoveProducer(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
case streamer.StatePlaying:
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] start")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err = conn.Accept(); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[rtsp] accept")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = conn.Handle(); err != nil {
|
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
||||||
//log.Warn().Err(err).Msg("[rtsp] handle server")
|
|
||||||
|
initMedias(conn)
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(conn); err != nil {
|
||||||
|
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if onDisconnect != nil {
|
closer = func() {
|
||||||
onDisconnect()
|
stream.RemoveConsumer(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
|
case rtsp.MethodAnnounce:
|
||||||
|
name = conn.URL.Path[1:]
|
||||||
|
|
||||||
|
stream := streams.Get(name)
|
||||||
|
if stream == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
||||||
|
|
||||||
|
stream.AddProducer(conn)
|
||||||
|
|
||||||
|
closer = func() {
|
||||||
|
stream.RemoveProducer(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
case streamer.StatePlaying:
|
||||||
|
log.Debug().Str("stream", name).Msg("[rtsp] start")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
srv.Serve()
|
if err := conn.Accept(); err != nil {
|
||||||
|
log.Warn().Err(err).Caller().Send()
|
||||||
|
_ = conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, handler := range handlers {
|
||||||
|
if handler(conn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if closer != nil {
|
||||||
|
if err := conn.Handle(); err != nil {
|
||||||
|
log.Debug().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
closer()
|
||||||
|
|
||||||
|
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initMedias(conn *rtsp.Conn) {
|
func initMedias(conn *rtsp.Conn) {
|
||||||
@@ -198,16 +211,27 @@ func initMedias(conn *rtsp.Conn) {
|
|||||||
for key, value := range conn.URL.Query() {
|
for key, value := range conn.URL.Query() {
|
||||||
switch key {
|
switch key {
|
||||||
case streamer.KindVideo, streamer.KindAudio:
|
case streamer.KindVideo, streamer.KindAudio:
|
||||||
for _, value := range value {
|
for _, name := range value {
|
||||||
|
name = strings.ToUpper(name)
|
||||||
|
|
||||||
|
// check aliases
|
||||||
|
switch name {
|
||||||
|
case "COPY":
|
||||||
|
name = "" // pass empty codecs list
|
||||||
|
case "MJPEG":
|
||||||
|
name = streamer.CodecJPEG
|
||||||
|
case "AAC":
|
||||||
|
name = streamer.CodecAAC
|
||||||
|
}
|
||||||
|
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
Kind: key, Direction: streamer.DirectionRecvonly,
|
Kind: key, Direction: streamer.DirectionRecvonly,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch value {
|
// empty codecs match all codecs
|
||||||
case "", "copy": // pass empty codecs list
|
if name != "" {
|
||||||
default:
|
// empty clock rate and channels match any values
|
||||||
codec := streamer.NewCodec(value)
|
media.Codecs = []*streamer.Codec{{Name: name}}
|
||||||
media.Codecs = append(media.Codecs, codec)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Medias = append(conn.Medias, media)
|
conn.Medias = append(conn.Medias, media)
|
||||||
|
@@ -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)
|
|
||||||
}
|
|
||||||
|
@@ -4,30 +4,36 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler func(url string) (streamer.Producer, error)
|
type Handler func(url string) (streamer.Producer, error)
|
||||||
|
|
||||||
var handlers map[string]Handler
|
var handlers = map[string]Handler{}
|
||||||
|
var handlersMu sync.Mutex
|
||||||
|
|
||||||
func HandleFunc(scheme string, handler Handler) {
|
func HandleFunc(scheme string, handler Handler) {
|
||||||
if handlers == nil {
|
handlersMu.Lock()
|
||||||
handlers = make(map[string]Handler)
|
|
||||||
}
|
|
||||||
handlers[scheme] = handler
|
handlers[scheme] = handler
|
||||||
|
handlersMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHandler(url string) Handler {
|
||||||
|
i := strings.IndexByte(url, ':')
|
||||||
|
if i <= 0 { // TODO: i < 4 ?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
handlersMu.Lock()
|
||||||
|
defer handlersMu.Unlock()
|
||||||
|
return handlers[url[:i]]
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasProducer(url string) bool {
|
func HasProducer(url string) bool {
|
||||||
i := strings.IndexByte(url, ':')
|
return getHandler(url) != nil
|
||||||
if i <= 0 { // TODO: i < 4 ?
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return handlers[url[:i]] != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetProducer(url string) (streamer.Producer, error) {
|
func GetProducer(url string) (streamer.Producer, error) {
|
||||||
i := strings.IndexByte(url, ':')
|
handler := getHandler(url)
|
||||||
handler := handlers[url[:i]]
|
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
return nil, fmt.Errorf("unsupported scheme: %s", url)
|
return nil, fmt.Errorf("unsupported scheme: %s", url)
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type state byte
|
type state byte
|
||||||
@@ -13,6 +14,7 @@ const (
|
|||||||
stateMedias
|
stateMedias
|
||||||
stateTracks
|
stateTracks
|
||||||
stateStart
|
stateStart
|
||||||
|
stateExternal
|
||||||
)
|
)
|
||||||
|
|
||||||
type Producer struct {
|
type Producer struct {
|
||||||
@@ -24,8 +26,9 @@ type Producer struct {
|
|||||||
element streamer.Producer
|
element streamer.Producer
|
||||||
tracks []*streamer.Track
|
tracks []*streamer.Track
|
||||||
|
|
||||||
state state
|
state state
|
||||||
mx sync.Mutex
|
mu sync.Mutex
|
||||||
|
restart *time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) SetSource(s string) {
|
func (p *Producer) SetSource(s string) {
|
||||||
@@ -36,16 +39,16 @@ func (p *Producer) SetSource(s string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) GetMedias() []*streamer.Media {
|
func (p *Producer) GetMedias() []*streamer.Media {
|
||||||
p.mx.Lock()
|
p.mu.Lock()
|
||||||
defer p.mx.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
if p.state == stateNone {
|
if p.state == stateNone {
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
|
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
p.element, err = GetProducer(p.url)
|
p.element, err = GetProducer(p.url)
|
||||||
if err != nil || p.element == nil {
|
if err != nil || p.element == nil {
|
||||||
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
|
log.Error().Err(err).Caller().Send()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,59 +59,127 @@ func (p *Producer) GetMedias() []*streamer.Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||||
p.mx.Lock()
|
p.mu.Lock()
|
||||||
defer p.mx.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
if p.state == stateMedias {
|
if p.state == stateNone {
|
||||||
p.state = stateTracks
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
track := p.element.GetTrack(media, codec)
|
for _, track := range p.tracks {
|
||||||
|
if track.Codec == codec {
|
||||||
for _, t := range p.tracks {
|
|
||||||
if track == t {
|
|
||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
track := p.element.GetTrack(media, codec)
|
||||||
|
if track == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
p.tracks = append(p.tracks, track)
|
p.tracks = append(p.tracks, track)
|
||||||
|
|
||||||
|
if p.state == stateMedias {
|
||||||
|
p.state = stateTracks
|
||||||
|
}
|
||||||
|
|
||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
// internals
|
// internals
|
||||||
|
|
||||||
func (p *Producer) start() {
|
func (p *Producer) start() {
|
||||||
p.mx.Lock()
|
p.mu.Lock()
|
||||||
defer p.mx.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
if p.state != stateTracks {
|
if p.state != stateTracks {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] start producer")
|
log.Debug().Msgf("[streams] start producer url=%s", p.url)
|
||||||
|
|
||||||
p.state = stateStart
|
p.state = stateStart
|
||||||
go func() {
|
go func() {
|
||||||
|
// safe read element while mu locked
|
||||||
if err := p.element.Start(); err != nil {
|
if err := p.element.Start(); err != nil {
|
||||||
log.Warn().Err(err).Str("url", p.url).Msg("[streams] start")
|
log.Warn().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
|
p.reconnect()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) reconnect() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.state != stateStart {
|
||||||
|
log.Trace().Msgf("[streams] stop reconnect url=%s", p.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
p.element, err = GetProducer(p.url)
|
||||||
|
if err != nil || p.element == nil {
|
||||||
|
log.Debug().Err(err).Caller().Send()
|
||||||
|
// TODO: dynamic timeout
|
||||||
|
p.restart = time.AfterFunc(30*time.Second, p.reconnect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
medias := p.element.GetMedias()
|
||||||
|
|
||||||
|
// convert all old producer tracks to new tracks
|
||||||
|
for i, oldTrack := range p.tracks {
|
||||||
|
// match new element medias with old track codec
|
||||||
|
for _, media := range medias {
|
||||||
|
codec := media.MatchCodec(oldTrack.Codec)
|
||||||
|
if codec == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// move sink from old track to new track
|
||||||
|
newTrack := p.element.GetTrack(media, codec)
|
||||||
|
newTrack.GetSink(oldTrack)
|
||||||
|
p.tracks[i] = newTrack
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err = p.element.Start(); err != nil {
|
||||||
|
log.Debug().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
p.reconnect()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) stop() {
|
func (p *Producer) stop() {
|
||||||
p.mx.Lock()
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
|
switch p.state {
|
||||||
|
case stateExternal:
|
||||||
|
log.Debug().Msgf("[streams] can't stop external producer")
|
||||||
|
return
|
||||||
|
case stateNone:
|
||||||
|
log.Debug().Msgf("[streams] can't stop none producer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
|
||||||
|
|
||||||
if p.element != nil {
|
if p.element != nil {
|
||||||
_ = p.element.Stop()
|
_ = p.element.Stop()
|
||||||
p.element = nil
|
p.element = nil
|
||||||
} else {
|
|
||||||
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
|
|
||||||
}
|
}
|
||||||
p.tracks = nil
|
if p.restart != nil {
|
||||||
p.state = stateNone
|
p.restart.Stop()
|
||||||
|
p.restart = nil
|
||||||
|
}
|
||||||
|
|
||||||
p.mx.Unlock()
|
p.state = stateNone
|
||||||
|
p.tracks = nil
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
@@ -14,6 +15,7 @@ type Consumer struct {
|
|||||||
type Stream struct {
|
type Stream struct {
|
||||||
producers []*Producer
|
producers []*Producer
|
||||||
consumers []*Consumer
|
consumers []*Consumer
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStream(source interface{}) *Stream {
|
func NewStream(source interface{}) *Stream {
|
||||||
@@ -51,18 +53,19 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
ic := len(s.consumers)
|
ic := len(s.consumers)
|
||||||
|
|
||||||
consumer := &Consumer{element: cons}
|
consumer := &Consumer{element: cons}
|
||||||
|
var producers []*Producer // matched producers for consumer
|
||||||
|
|
||||||
// Step 1. Get consumer medias
|
// Step 1. Get consumer medias
|
||||||
for icc, consMedia := range cons.GetMedias() {
|
for icc, consMedia := range cons.GetMedias() {
|
||||||
log.Trace().Stringer("media", consMedia).
|
log.Trace().Stringer("media", consMedia).
|
||||||
Msgf("[streams] consumer:%d:%d candidate", ic, icc)
|
Msgf("[streams] consumer=%d candidate=%d", ic, icc)
|
||||||
|
|
||||||
producers:
|
producers:
|
||||||
for ip, prod := range s.producers {
|
for ip, prod := range s.producers {
|
||||||
// Step 2. Get producer medias (not tracks yet)
|
// Step 2. Get producer medias (not tracks yet)
|
||||||
for ipc, prodMedia := range prod.GetMedias() {
|
for ipc, prodMedia := range prod.GetMedias() {
|
||||||
log.Trace().Stringer("media", prodMedia).
|
log.Trace().Stringer("media", prodMedia).
|
||||||
Msgf("[streams] producer:%d:%d candidate", ip, ipc)
|
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
|
||||||
|
|
||||||
// Step 3. Match consumer/producer codecs list
|
// Step 3. Match consumer/producer codecs list
|
||||||
prodCodec := prodMedia.MatchMedia(consMedia)
|
prodCodec := prodMedia.MatchMedia(consMedia)
|
||||||
@@ -81,20 +84,24 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
||||||
|
|
||||||
consumer.tracks = append(consumer.tracks, consTrack)
|
consumer.tracks = append(consumer.tracks, consTrack)
|
||||||
|
producers = append(producers, prod)
|
||||||
break producers
|
break producers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't match tracks for consumer
|
if len(producers) == 0 {
|
||||||
if len(consumer.tracks) == 0 {
|
s.stopProducers()
|
||||||
return errors.New("couldn't find the matching tracks")
|
return errors.New("couldn't find the matching tracks")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
s.consumers = append(s.consumers, consumer)
|
s.consumers = append(s.consumers, consumer)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
for _, prod := range s.producers {
|
// there may be duplicates, but that's not a problem
|
||||||
|
for _, prod := range producers {
|
||||||
prod.start()
|
prod.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,12 +109,8 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
||||||
|
s.mu.Lock()
|
||||||
for i, consumer := range s.consumers {
|
for i, consumer := range s.consumers {
|
||||||
if consumer == nil {
|
|
||||||
log.Warn().Msgf("empty consumer: %+v\n", s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if consumer.element == cons {
|
if consumer.element == cons {
|
||||||
// remove consumer pads from all producers
|
// remove consumer pads from all producers
|
||||||
for _, track := range consumer.tracks {
|
for _, track := range consumer.tracks {
|
||||||
@@ -118,55 +121,60 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
for _, producer := range s.producers {
|
s.stopProducers()
|
||||||
if producer == nil {
|
|
||||||
log.Warn().Msgf("empty producer: %+v\n", s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var sink bool
|
|
||||||
for _, track := range producer.tracks {
|
|
||||||
if len(track.Sink) > 0 {
|
|
||||||
sink = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !sink {
|
|
||||||
producer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) AddProducer(prod streamer.Producer) {
|
func (s *Stream) AddProducer(prod streamer.Producer) {
|
||||||
producer := &Producer{element: prod, state: stateTracks}
|
producer := &Producer{element: prod, state: stateExternal}
|
||||||
|
s.mu.Lock()
|
||||||
s.producers = append(s.producers, producer)
|
s.producers = append(s.producers, producer)
|
||||||
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
||||||
|
s.mu.Lock()
|
||||||
for i, producer := range s.producers {
|
for i, producer := range s.producers {
|
||||||
if producer.element == prod {
|
if producer.element == prod {
|
||||||
s.removeProducer(i)
|
s.removeProducer(i)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) Active() bool {
|
func (s *Stream) stopProducers() {
|
||||||
if len(s.consumers) > 0 {
|
s.mu.Lock()
|
||||||
return true
|
producers:
|
||||||
}
|
for _, producer := range s.producers {
|
||||||
|
for _, track := range producer.tracks {
|
||||||
for _, prod := range s.producers {
|
if track.HasSink() {
|
||||||
if prod.element != nil {
|
continue producers
|
||||||
return true
|
}
|
||||||
}
|
}
|
||||||
|
producer.stop()
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//func (s *Stream) Active() bool {
|
||||||
|
// if len(s.consumers) > 0 {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for _, prod := range s.producers {
|
||||||
|
// if prod.element != nil {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
|
|
||||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||||
var v []interface{}
|
var v []interface{}
|
||||||
|
s.mu.Lock()
|
||||||
for _, prod := range s.producers {
|
for _, prod := range s.producers {
|
||||||
if prod.element != nil {
|
if prod.element != nil {
|
||||||
v = append(v, prod.element)
|
v = append(v, prod.element)
|
||||||
@@ -176,6 +184,7 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
|
|||||||
// cons.element always not nil
|
// cons.element always not nil
|
||||||
v = append(v, cons.element)
|
v = append(v, cons.element)
|
||||||
}
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
if len(v) == 0 {
|
if len(v) == 0 {
|
||||||
v = nil
|
v = nil
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,9 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
pion "github.com/pion/webrtc/v3"
|
pion "github.com/pion/webrtc/v3"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -55,6 +57,8 @@ func Init() {
|
|||||||
|
|
||||||
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
|
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
|
||||||
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
|
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
|
||||||
|
|
||||||
|
api.HandleFunc("api/webrtc", syncHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
var Port string
|
var Port string
|
||||||
@@ -137,6 +141,32 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
|||||||
ctx.Consumer = conn
|
ctx.Consumer = conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
url := r.URL.Query().Get("src")
|
||||||
|
stream := streams.Get(url)
|
||||||
|
if stream == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get offer
|
||||||
|
offer, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
answer, err := ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send SDP to client
|
||||||
|
if _, err = w.Write([]byte(answer)); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ExchangeSDP(
|
func ExchangeSDP(
|
||||||
stream *streams.Stream, offer string, userAgent string,
|
stream *streams.Stream, offer string, userAgent string,
|
||||||
) (answer string, err error) {
|
) (answer string, err error) {
|
||||||
|
@@ -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")
|
|
||||||
}
|
|
4
go.mod
4
go.mod
@@ -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
|
||||||
)
|
)
|
||||||
|
9
go.sum
9
go.sum
@@ -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=
|
||||||
|
@@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
- https://www.wowza.com/blog/streaming-protocols
|
- https://www.wowza.com/blog/streaming-protocols
|
||||||
- https://vimeo.com/blog/post/rtmp-stream/
|
- https://vimeo.com/blog/post/rtmp-stream/
|
||||||
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a
|
- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a
|
||||||
|
- [Android Supported media formats](https://developer.android.com/guide/topics/media/media-formats)
|
||||||
|
- [THEOplayer](https://www.theoplayer.com/test-your-stream-hls-dash-hesp)
|
||||||
|
- [How Generate DTS/PTS](https://www.ramugedia.com/how-generate-dts-pts-from-elementary-stream)
|
||||||
|
20
pkg/aac/README.md
Normal file
20
pkg/aac/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
## AAC-LD and AAC-ELD
|
||||||
|
|
||||||
|
Codec | Rate | QuickTime | ffmpeg | VLC
|
||||||
|
------|------|-----------|--------|----
|
||||||
|
AAC-LD | 8000 | yes | no | no
|
||||||
|
AAC-LD | 16000 | yes | no | no
|
||||||
|
AAC-LD | 22050 | yes | yes | no
|
||||||
|
AAC-LD | 24000 | yes | yes | no
|
||||||
|
AAC-LD | 32000 | yes | yes | no
|
||||||
|
AAC-ELD | 8000 | yes | no | no
|
||||||
|
AAC-ELD | 16000 | yes | no | no
|
||||||
|
AAC-ELD | 22050 | yes | yes | yes
|
||||||
|
AAC-ELD | 24000 | yes | yes | yes
|
||||||
|
AAC-ELD | 32000 | yes | yes | yes
|
||||||
|
|
||||||
|
## Useful links
|
||||||
|
|
||||||
|
- [4.6.20 Enhanced Low Delay Codec](https://csclub.uwaterloo.ca/~ehashman/ISO14496-3-2009.pdf)
|
||||||
|
- https://stackoverflow.com/questions/40014508/aac-adts-for-aacobject-eld-packets
|
||||||
|
- https://code.videolan.org/videolan/vlc/-/blob/master/modules/packetizer/mpeg4audio.c
|
57
pkg/aac/rtp.go
Normal file
57
pkg/aac/rtp.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package aac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const RTPPacketVersionAAC = 0
|
||||||
|
|
||||||
|
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||||
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
|
return func(packet *rtp.Packet) error {
|
||||||
|
// support ONLY 2 bytes header size!
|
||||||
|
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
|
||||||
|
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
|
||||||
|
|
||||||
|
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Version = RTPPacketVersionAAC
|
||||||
|
clone.Payload = packet.Payload[2+headersSize:]
|
||||||
|
return push(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
||||||
|
sequencer := rtp.NewRandomSequencer()
|
||||||
|
|
||||||
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
|
return func(packet *rtp.Packet) error {
|
||||||
|
if packet.Version != RTPPacketVersionAAC {
|
||||||
|
return push(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// support ONLY one unit in payload
|
||||||
|
size := uint16(len(packet.Payload))
|
||||||
|
// 2 bytes header size + 2 bytes first payload size
|
||||||
|
payload := make([]byte, 2+2+size)
|
||||||
|
payload[1] = 16 // header size in bits
|
||||||
|
binary.BigEndian.PutUint16(payload[2:], size<<3)
|
||||||
|
copy(payload[4:], packet.Payload)
|
||||||
|
|
||||||
|
clone := rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
|
Timestamp: packet.Timestamp,
|
||||||
|
},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
return push(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,12 +6,6 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PayloadTypeAVC = 255
|
|
||||||
|
|
||||||
func IsAVC(codec *streamer.Codec) bool {
|
|
||||||
return codec.PayloadType == PayloadTypeAVC
|
|
||||||
}
|
|
||||||
|
|
||||||
func EncodeAVC(nals ...[]byte) (avc []byte) {
|
func EncodeAVC(nals ...[]byte) (avc []byte) {
|
||||||
var i, n int
|
var i, n int
|
||||||
|
|
||||||
|
@@ -8,11 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NALUTypePFrame = 1
|
NALUTypePFrame = 1 // Coded slice of a non-IDR picture
|
||||||
NALUTypeIFrame = 5
|
NALUTypeIFrame = 5 // Coded slice of an IDR picture
|
||||||
NALUTypeSEI = 6
|
NALUTypeSEI = 6 // Supplemental enhancement information (SEI)
|
||||||
NALUTypeSPS = 7
|
NALUTypeSPS = 7 // Sequence parameter set
|
||||||
NALUTypePPS = 8
|
NALUTypePPS = 8 // Picture parameter set
|
||||||
|
NALUTypeAUD = 9 // Access unit delimiter
|
||||||
)
|
)
|
||||||
|
|
||||||
func NALUType(b []byte) byte {
|
func NALUType(b []byte) byte {
|
||||||
|
@@ -19,11 +19,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
|
|
||||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
return func(packet *rtp.Packet) error {
|
return func(packet *rtp.Packet) error {
|
||||||
//fmt.Printf(
|
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||||
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v\n",
|
|
||||||
// track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp,
|
|
||||||
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
|
||||||
//)
|
|
||||||
|
|
||||||
payload, err := depack.Unmarshal(packet.Payload)
|
payload, err := depack.Unmarshal(packet.Payload)
|
||||||
if len(payload) == 0 || err != nil {
|
if len(payload) == 0 || err != nil {
|
||||||
@@ -40,11 +36,13 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
|
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
|
||||||
|
// Amcrest IP4M-1051: 9, 6, 1
|
||||||
switch NALUType(payload) {
|
switch NALUType(payload) {
|
||||||
case NALUTypeIFrame:
|
case NALUTypeIFrame:
|
||||||
// fix IFrame without SPS,PPS
|
// fix IFrame without SPS,PPS
|
||||||
buf = append(buf, ps...)
|
buf = append(buf, ps...)
|
||||||
case NALUTypeSEI:
|
case NALUTypeSEI, NALUTypeAUD:
|
||||||
// fix ffmpeg with transcoding first frame
|
// fix ffmpeg with transcoding first frame
|
||||||
i := int(4 + binary.BigEndian.Uint32(payload))
|
i := int(4 + binary.BigEndian.Uint32(payload))
|
||||||
|
|
||||||
@@ -72,10 +70,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
buf = buf[:0]
|
buf = buf[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Printf(
|
//log.Printf("[AVC] %v, len: %d", Types(payload), len(payload))
|
||||||
// "[AVC] %v, len: %d, %v\n", Types(payload), len(payload),
|
|
||||||
// reflect.ValueOf(buf).Pointer() == reflect.ValueOf(payload).Pointer(),
|
|
||||||
//)
|
|
||||||
|
|
||||||
clone := *packet
|
clone := *packet
|
||||||
clone.Version = RTPPacketVersionAVC
|
clone.Version = RTPPacketVersionAVC
|
||||||
@@ -92,29 +87,28 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
|
|||||||
|
|
||||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
return func(packet *rtp.Packet) error {
|
return func(packet *rtp.Packet) error {
|
||||||
if packet.Version == RTPPacketVersionAVC {
|
if packet.Version != RTPPacketVersionAVC {
|
||||||
payloads := payloader.Payload(mtu, packet.Payload)
|
return push(packet)
|
||||||
last := len(payloads) - 1
|
|
||||||
for i, payload := range payloads {
|
|
||||||
clone := rtp.Packet{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: i == last,
|
|
||||||
//PayloadType: packet.PayloadType,
|
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
|
||||||
Timestamp: packet.Timestamp,
|
|
||||||
//SSRC: packet.SSRC,
|
|
||||||
},
|
|
||||||
Payload: payload,
|
|
||||||
}
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return push(packet)
|
payloads := payloader.Payload(mtu, packet.Payload)
|
||||||
|
last := len(payloads) - 1
|
||||||
|
for i, payload := range payloads {
|
||||||
|
clone := rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: i == last,
|
||||||
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
|
Timestamp: packet.Timestamp,
|
||||||
|
},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
if err := push(&clone); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# Homekit
|
# Home Accessory Protocol
|
||||||
|
|
||||||
> PS. Character = Characteristic
|
> PS. Character = Characteristic
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
type Accessory struct {
|
type Accessory struct {
|
||||||
AID int `json:"aid"`
|
AID int `json:"aid"`
|
@@ -2,22 +2,22 @@ package camera
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
"github.com/brutella/hap/characteristic"
|
"github.com/brutella/hap/characteristic"
|
||||||
"github.com/brutella/hap/rtp"
|
"github.com/brutella/hap/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
client *homekit.Client
|
client *hap.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(client *homekit.Client) *Client {
|
func NewClient(client *hap.Conn) *Client {
|
||||||
return &Client{client: client}
|
return &Client{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) StartStream2(ses *Session) (err error) {
|
func (c *Client) StartStream(ses *Session) (err error) {
|
||||||
// Step 1. Check if camera ready (free) to stream
|
// Step 1. Check if camera ready (free) to stream
|
||||||
var srv *homekit.Service
|
var srv *hap.Service
|
||||||
if srv, err = c.GetFreeStream(); err != nil {
|
if srv, err = c.GetFreeStream(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -35,8 +35,8 @@ func (c *Client) StartStream2(ses *Session) (err error) {
|
|||||||
// GetFreeStream search free streaming service.
|
// GetFreeStream search free streaming service.
|
||||||
// Usual every HomeKit camera can stream only to two clients simultaniosly.
|
// Usual every HomeKit camera can stream only to two clients simultaniosly.
|
||||||
// So it has two similar services for streaming.
|
// So it has two similar services for streaming.
|
||||||
func (c *Client) GetFreeStream() (srv *homekit.Service, err error) {
|
func (c *Client) GetFreeStream() (srv *hap.Service, err error) {
|
||||||
var accs []*homekit.Accessory
|
var accs []*hap.Accessory
|
||||||
if accs, err = c.client.GetAccessories(); err != nil {
|
if accs, err = c.client.GetAccessories(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func (c *Client) GetFreeStream() (srv *homekit.Service, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetupEndpoins(
|
func (c *Client) SetupEndpoins(
|
||||||
srv *homekit.Service, req *rtp.SetupEndpoints,
|
srv *hap.Service, req *rtp.SetupEndpoints,
|
||||||
) (res *rtp.SetupEndpointsResponse, err error) {
|
) (res *rtp.SetupEndpointsResponse, err error) {
|
||||||
// get setup endpoint character ID
|
// get setup endpoint character ID
|
||||||
char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
|
char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
|
||||||
@@ -87,7 +87,7 @@ func (c *Client) SetupEndpoins(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetConfig(srv *homekit.Service, config *rtp.StreamConfiguration) (err error) {
|
func (c *Client) SetConfig(srv *hap.Service, config *rtp.StreamConfiguration) (err error) {
|
||||||
// get setup endpoint character ID
|
// get setup endpoint character ID
|
||||||
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
|
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
|
||||||
char.Event = nil
|
char.Event = nil
|
75
pkg/hap/camera/session.go
Normal file
75
pkg/hap/camera/session.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package camera
|
||||||
|
|
||||||
|
import (
|
||||||
|
cryptorand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"github.com/brutella/hap/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
Offer *rtp.SetupEndpoints
|
||||||
|
Answer *rtp.SetupEndpointsResponse
|
||||||
|
Config *rtp.StreamConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(vp *rtp.VideoParameters, ap *rtp.AudioParameters) *Session {
|
||||||
|
vp.RTP = rtp.RTPParams{
|
||||||
|
PayloadType: 99,
|
||||||
|
Ssrc: RandomUint32(),
|
||||||
|
Bitrate: 2048,
|
||||||
|
Interval: 10,
|
||||||
|
MTU: 1200, // like WebRTC
|
||||||
|
}
|
||||||
|
ap.RTP = rtp.RTPParams{
|
||||||
|
PayloadType: 110,
|
||||||
|
Ssrc: RandomUint32(),
|
||||||
|
Bitrate: 32,
|
||||||
|
Interval: 10,
|
||||||
|
ComfortNoisePayloadType: 98,
|
||||||
|
MTU: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID := RandomBytes(16)
|
||||||
|
s := &Session{
|
||||||
|
Offer: &rtp.SetupEndpoints{
|
||||||
|
SessionId: sessionID,
|
||||||
|
Video: rtp.CryptoSuite{
|
||||||
|
MasterKey: RandomBytes(16),
|
||||||
|
MasterSalt: RandomBytes(14),
|
||||||
|
},
|
||||||
|
Audio: rtp.CryptoSuite{
|
||||||
|
MasterKey: RandomBytes(16),
|
||||||
|
MasterSalt: RandomBytes(14),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Config: &rtp.StreamConfiguration{
|
||||||
|
Command: rtp.SessionControlCommand{
|
||||||
|
Identifier: sessionID,
|
||||||
|
Type: rtp.SessionControlCommandTypeStart,
|
||||||
|
},
|
||||||
|
Video: *vp,
|
||||||
|
Audio: *ap,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) SetLocalEndpoint(host string, port uint16) {
|
||||||
|
s.Offer.ControllerAddr = rtp.Addr{
|
||||||
|
IPAddr: host,
|
||||||
|
VideoRtpPort: port,
|
||||||
|
AudioRtpPort: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomBytes(size int) []byte {
|
||||||
|
data := make([]byte, size)
|
||||||
|
_, _ = cryptorand.Read(data)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomUint32() uint32 {
|
||||||
|
data := make([]byte, 4)
|
||||||
|
_, _ = cryptorand.Read(data)
|
||||||
|
return binary.BigEndian.Uint32(data)
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
733
pkg/hap/conn.go
Normal file
733
pkg/hap/conn.go
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
package hap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/brutella/hap"
|
||||||
|
"github.com/brutella/hap/chacha20poly1305"
|
||||||
|
"github.com/brutella/hap/curve25519"
|
||||||
|
"github.com/brutella/hap/ed25519"
|
||||||
|
"github.com/brutella/hap/hkdf"
|
||||||
|
"github.com/brutella/hap/tlv8"
|
||||||
|
"github.com/tadglines/go-pkgs/crypto/srp"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Conn for HomeKit. DevicePublic can be null.
|
||||||
|
type Conn struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
DeviceAddress string // including port
|
||||||
|
DeviceID string
|
||||||
|
DevicePublic []byte
|
||||||
|
ClientID string
|
||||||
|
ClientPrivate []byte
|
||||||
|
|
||||||
|
OnEvent func(res *http.Response)
|
||||||
|
Output func(msg interface{})
|
||||||
|
|
||||||
|
conn net.Conn
|
||||||
|
secure *Secure
|
||||||
|
httpResponse chan *bufio.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(rawURL string) (*Conn, error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := u.Query()
|
||||||
|
c := &Conn{
|
||||||
|
DeviceAddress: u.Host,
|
||||||
|
DeviceID: query.Get("device_id"),
|
||||||
|
DevicePublic: DecodeKey(query.Get("device_public")),
|
||||||
|
ClientID: query.Get("client_id"),
|
||||||
|
ClientPrivate: DecodeKey(query.Get("client_private")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pair(deviceID, pin string) (*Conn, error) {
|
||||||
|
entry := mdns.GetEntry(deviceID)
|
||||||
|
if entry == nil {
|
||||||
|
return nil, errors.New("can't find device via mDNS")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Conn{
|
||||||
|
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
|
||||||
|
DeviceID: deviceID,
|
||||||
|
ClientID: GenerateUUID(),
|
||||||
|
ClientPrivate: GenerateKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var mfi bool
|
||||||
|
for _, field := range entry.InfoFields {
|
||||||
|
if field[:2] == "ff" {
|
||||||
|
if field[3] == '1' {
|
||||||
|
mfi = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, c.Pair(mfi, pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ClientPublic() []byte {
|
||||||
|
return c.ClientPrivate[32:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) URL() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
|
||||||
|
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) DialAndServe() error {
|
||||||
|
if err := c.Dial(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Handle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Dial() error {
|
||||||
|
// update device host before dial
|
||||||
|
if host := mdns.GetAddress(c.DeviceID); host != "" {
|
||||||
|
c.DeviceAddress = host
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, time.Second*5)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M1: send our session public to device
|
||||||
|
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
||||||
|
|
||||||
|
// 1. generate payload
|
||||||
|
// important not include other fields
|
||||||
|
requestM1 := struct {
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
PublicKey []byte `tlv8:"3"`
|
||||||
|
}{
|
||||||
|
State: hap.M1,
|
||||||
|
PublicKey: sessionPublic[:],
|
||||||
|
}
|
||||||
|
// 2. pack payload to TLV8
|
||||||
|
buf, err := tlv8.Marshal(requestM1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. send request
|
||||||
|
resp, err := c.Post(UriPairVerify, buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M2: unpack deviceID from response
|
||||||
|
responseM2 := PairVerifyPayload{}
|
||||||
|
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. generate session shared key
|
||||||
|
var deviceSessionPublic [32]byte
|
||||||
|
copy(deviceSessionPublic[:], responseM2.PublicKey)
|
||||||
|
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
|
||||||
|
sessionKey, err := hkdf.Sha512(
|
||||||
|
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
|
||||||
|
[]byte("Pair-Verify-Encrypt-Info"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. decrypt M2 response with session key
|
||||||
|
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
|
||||||
|
var mac [16]byte
|
||||||
|
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
|
||||||
|
|
||||||
|
buf, err = chacha20poly1305.DecryptAndVerify(
|
||||||
|
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. unpack payload from TLV8
|
||||||
|
payloadM2 := PairVerifyPayload{}
|
||||||
|
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. verify signature for M2 response with device public
|
||||||
|
// device session + device id + our session
|
||||||
|
if c.DevicePublic != nil {
|
||||||
|
buf = nil
|
||||||
|
buf = append(buf, responseM2.PublicKey[:]...)
|
||||||
|
buf = append(buf, []byte(payloadM2.Identifier)...)
|
||||||
|
buf = append(buf, sessionPublic[:]...)
|
||||||
|
if !ed25519.ValidateSignature(
|
||||||
|
c.DevicePublic[:], buf, payloadM2.Signature,
|
||||||
|
) {
|
||||||
|
return errors.New("device public signature invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M3: send our clientID to device
|
||||||
|
// 1. generate signature with our private key
|
||||||
|
// (our session + our ID + device session)
|
||||||
|
buf = nil
|
||||||
|
buf = append(buf, sessionPublic[:]...)
|
||||||
|
buf = append(buf, []byte(c.ClientID)...)
|
||||||
|
buf = append(buf, responseM2.PublicKey[:]...)
|
||||||
|
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. generate payload
|
||||||
|
payloadM3 := struct {
|
||||||
|
Identifier string `tlv8:"1"`
|
||||||
|
Signature []byte `tlv8:"10"`
|
||||||
|
}{
|
||||||
|
Identifier: c.ClientID,
|
||||||
|
Signature: signature,
|
||||||
|
}
|
||||||
|
// 3. pack payload to TLV8
|
||||||
|
buf, err = tlv8.Marshal(payloadM3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. encrypt payload with session key
|
||||||
|
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
|
||||||
|
sessionKey[:], []byte("PV-Msg03"), buf, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 4. generate request
|
||||||
|
requestM3 := struct {
|
||||||
|
EncryptedData []byte `tlv8:"5"`
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
}{
|
||||||
|
State: hap.M3,
|
||||||
|
EncryptedData: append(msg, mac[:]...),
|
||||||
|
}
|
||||||
|
// 5. pack payload to TLV8
|
||||||
|
buf, err = tlv8.Marshal(requestM3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = c.Post(UriPairVerify, buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M4. Read response
|
||||||
|
responseM4 := PairVerifyPayload{}
|
||||||
|
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. check response state
|
||||||
|
if responseM4.State != 4 || responseM4.Status != 0 {
|
||||||
|
return fmt.Errorf("wrong M4 response: %+v", responseM4)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.secure, err = NewSecure(sessionShared, false)
|
||||||
|
//c.secure.Buffer = bytes.NewBuffer(nil)
|
||||||
|
c.secure.Conn = c.conn
|
||||||
|
|
||||||
|
c.httpResponse = make(chan *bufio.Reader, 10)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
|
||||||
|
func (c *Conn) Pair(mfi bool, pin string) (err error) {
|
||||||
|
pin = strings.ReplaceAll(pin, "-", "")
|
||||||
|
if len(pin) != 8 {
|
||||||
|
return fmt.Errorf("wrong PIN format: %s", pin)
|
||||||
|
}
|
||||||
|
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:]
|
||||||
|
|
||||||
|
c.conn, err = net.Dial("tcp", c.DeviceAddress)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M1. Generate request
|
||||||
|
reqM1 := struct {
|
||||||
|
Method byte `tlv8:"0"`
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
}{
|
||||||
|
State: hap.M1,
|
||||||
|
}
|
||||||
|
if mfi {
|
||||||
|
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
|
||||||
|
}
|
||||||
|
buf, err := tlv8.Marshal(reqM1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M1. Send request
|
||||||
|
res, err := c.Post(UriPairSetup, buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M2. Read response
|
||||||
|
resM2 := struct {
|
||||||
|
Salt []byte `tlv8:"2"`
|
||||||
|
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Error byte `tlv8:"7"`
|
||||||
|
}{}
|
||||||
|
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resM2.State != 2 || resM2.Error > 0 {
|
||||||
|
return fmt.Errorf("wrong M2: %+v", resM2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M3. Generate session using pin
|
||||||
|
username := []byte("Pair-Setup")
|
||||||
|
|
||||||
|
SRP, err := srp.NewSRP(
|
||||||
|
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SRP.SaltLength = 16
|
||||||
|
|
||||||
|
// username: "Pair-Setup"
|
||||||
|
// password: PIN (with dashes)
|
||||||
|
session := SRP.NewClientSession(username, []byte(pin))
|
||||||
|
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M3. Generate request
|
||||||
|
reqM3 := struct {
|
||||||
|
PublicKey []byte `tlv8:"3"`
|
||||||
|
Proof []byte `tlv8:"4"`
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
}{
|
||||||
|
PublicKey: session.GetA(), // client public key, aka session.A
|
||||||
|
Proof: session.ComputeAuthenticator(),
|
||||||
|
State: hap.M3,
|
||||||
|
}
|
||||||
|
buf, err = tlv8.Marshal(reqM3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M3. Send request
|
||||||
|
res, err = c.Post(UriPairSetup, buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M4. Read response
|
||||||
|
resM4 := struct {
|
||||||
|
Proof []byte `tlv8:"4"` // server proof
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Error byte `tlv8:"7"`
|
||||||
|
}{}
|
||||||
|
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resM4.Error == 2 {
|
||||||
|
return fmt.Errorf("wrong PIN: %s", pin)
|
||||||
|
}
|
||||||
|
if resM4.State != 4 || resM4.Error > 0 {
|
||||||
|
return fmt.Errorf("wrong M4: %+v", resM4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M4. Verify response
|
||||||
|
if !session.VerifyServerAuthenticator(resM4.Proof) {
|
||||||
|
return errors.New("verify server auth fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M5. Generate signature
|
||||||
|
saltKey, err := hkdf.Sha512(
|
||||||
|
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
|
||||||
|
[]byte("Pair-Setup-Controller-Sign-Info"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = nil
|
||||||
|
buf = append(buf, saltKey[:]...)
|
||||||
|
buf = append(buf, []byte(c.ClientID)...)
|
||||||
|
buf = append(buf, c.ClientPublic()...)
|
||||||
|
|
||||||
|
signature, err := ed25519.Signature(c.ClientPrivate, buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M5. Generate payload
|
||||||
|
msgM5 := struct {
|
||||||
|
Identifier string `tlv8:"1"`
|
||||||
|
PublicKey []byte `tlv8:"3"`
|
||||||
|
Signature []byte `tlv8:"10"`
|
||||||
|
}{
|
||||||
|
Identifier: c.ClientID,
|
||||||
|
PublicKey: c.ClientPublic(),
|
||||||
|
Signature: signature,
|
||||||
|
}
|
||||||
|
buf, err = tlv8.Marshal(msgM5)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M5. Encrypt payload
|
||||||
|
sessionKey, err := hkdf.Sha512(
|
||||||
|
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
|
||||||
|
[]byte("Pair-Setup-Encrypt-Info"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
|
||||||
|
sessionKey[:], []byte("PS-Msg05"), buf, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// STEP M5. Generate request
|
||||||
|
reqM5 := struct {
|
||||||
|
EncryptedData []byte `tlv8:"5"`
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
}{
|
||||||
|
EncryptedData: append(buf, mac[:]...),
|
||||||
|
State: hap.M5,
|
||||||
|
}
|
||||||
|
buf, err = tlv8.Marshal(reqM5)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M5. Send request
|
||||||
|
res, err = c.Post(UriPairSetup, buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M6. Read response
|
||||||
|
resM6 := struct {
|
||||||
|
EncryptedData []byte `tlv8:"5"`
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Error byte `tlv8:"7"`
|
||||||
|
}{}
|
||||||
|
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resM6.State != 6 || resM6.Error > 0 {
|
||||||
|
return fmt.Errorf("wrong M6: %+v", resM2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M6. Decrypt payload
|
||||||
|
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
|
||||||
|
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
|
||||||
|
|
||||||
|
buf, err = chacha20poly1305.DecryptAndVerify(
|
||||||
|
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgM6 := struct {
|
||||||
|
Identifier []byte `tlv8:"1"`
|
||||||
|
PublicKey []byte `tlv8:"3"`
|
||||||
|
Signature []byte `tlv8:"10"`
|
||||||
|
}{}
|
||||||
|
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP M6. Verify payload
|
||||||
|
if saltKey, err = hkdf.Sha512(
|
||||||
|
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
|
||||||
|
[]byte("Pair-Setup-Accessory-Sign-Info"),
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = nil
|
||||||
|
buf = append(buf, saltKey[:]...)
|
||||||
|
buf = append(buf, msgM6.Identifier...)
|
||||||
|
buf = append(buf, msgM6.PublicKey...)
|
||||||
|
|
||||||
|
if !ed25519.ValidateSignature(
|
||||||
|
msgM6.PublicKey[:], buf, msgM6.Signature,
|
||||||
|
) {
|
||||||
|
return errors.New("wrong server signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DeviceID != string(msgM6.Identifier) {
|
||||||
|
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DevicePublic = msgM6.PublicKey
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Close() error {
|
||||||
|
if c.conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conn := c.conn
|
||||||
|
c.conn = nil
|
||||||
|
return conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) GetAccessories() ([]*Accessory, error) {
|
||||||
|
res, err := c.Get("/accessories")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := Accessories{}
|
||||||
|
if err = json.Unmarshal(data, &p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, accs := range p.Accessories {
|
||||||
|
for _, serv := range accs.Services {
|
||||||
|
for _, char := range serv.Characters {
|
||||||
|
char.AID = accs.AID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Accessories, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) GetCharacters(query string) ([]*Character, error) {
|
||||||
|
res, err := c.Get("/characteristics?id=" + query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := Characters{}
|
||||||
|
if err = json.Unmarshal(data, &ch); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ch.Characters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) GetCharacter(char *Character) error {
|
||||||
|
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
|
||||||
|
chars, err := c.GetCharacters(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
char.Value = chars[0].Value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) PutCharacters(characters ...*Character) (err error) {
|
||||||
|
for i, char := range characters {
|
||||||
|
if char.Event != nil {
|
||||||
|
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
|
||||||
|
} else {
|
||||||
|
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
|
||||||
|
}
|
||||||
|
characters[i] = char
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
if data, err = json.Marshal(Characters{characters}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *http.Response
|
||||||
|
if res, err = c.Put("/characteristics", data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
return errors.New("wrong response status")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) GetImage(width, height int) ([]byte, error) {
|
||||||
|
res, err := c.Post(
|
||||||
|
"/resource", []byte(fmt.Sprintf(
|
||||||
|
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
|
||||||
|
width, height,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (c *Client) onEventData(r io.Reader) error {
|
||||||
|
// if c.OnEvent == nil {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// data, err := io.ReadAll(r)
|
||||||
|
//
|
||||||
|
// ch := Characters{}
|
||||||
|
// if err = json.Unmarshal(data, &ch); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// c.OnEvent(ch.Characters)
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
//}
|
||||||
|
|
||||||
|
func (c *Conn) ListPairings() error {
|
||||||
|
pReq := struct {
|
||||||
|
Method byte `tlv8:"0"`
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
}{
|
||||||
|
Method: hap.MethodListPairings,
|
||||||
|
State: hap.M1,
|
||||||
|
}
|
||||||
|
data, err := tlv8.Marshal(pReq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Post("/pairings", data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = io.ReadAll(res.Body)
|
||||||
|
// TODO: don't know how to fix array of items
|
||||||
|
var pRes struct {
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Identifier string `tlv8:"1"`
|
||||||
|
PublicKey []byte `tlv8:"3"`
|
||||||
|
Permission byte `tlv8:"11"`
|
||||||
|
}
|
||||||
|
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
|
||||||
|
pReq := struct {
|
||||||
|
Method byte `tlv8:"0"`
|
||||||
|
Identifier string `tlv8:"1"`
|
||||||
|
PublicKey []byte `tlv8:"3"`
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Permission byte `tlv8:"11"`
|
||||||
|
}{
|
||||||
|
Method: hap.MethodAddPairing,
|
||||||
|
Identifier: clientID,
|
||||||
|
PublicKey: clientPublic,
|
||||||
|
State: hap.M1,
|
||||||
|
Permission: hap.PermissionUser,
|
||||||
|
}
|
||||||
|
if admin {
|
||||||
|
pReq.Permission = hap.PermissionAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := tlv8.Marshal(pReq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Post("/pairings", data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = io.ReadAll(res.Body)
|
||||||
|
var pRes struct {
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Unknown byte `tlv8:"7"`
|
||||||
|
}
|
||||||
|
if err = tlv8.Unmarshal(data, &pRes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) DeletePairing(id string) error {
|
||||||
|
reqM1 := struct {
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
Method byte `tlv8:"0"`
|
||||||
|
Identifier string `tlv8:"1"`
|
||||||
|
}{
|
||||||
|
State: hap.M1,
|
||||||
|
Method: hap.MethodDeletePairing,
|
||||||
|
Identifier: id,
|
||||||
|
}
|
||||||
|
data, err := tlv8.Marshal(reqM1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Post("/pairings", data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = io.ReadAll(res.Body)
|
||||||
|
var resM2 struct {
|
||||||
|
State byte `tlv8:"6"`
|
||||||
|
}
|
||||||
|
if err = tlv8.Unmarshal(data, &resM2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resM2.State != hap.M2 {
|
||||||
|
return errors.New("wrong state")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) LocalAddr() string {
|
||||||
|
return c.conn.LocalAddr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeKey(s string) []byte {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -29,13 +29,13 @@ func GenerateUUID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PairVerifyPayload struct {
|
type PairVerifyPayload struct {
|
||||||
Method byte `tlv8:"0"`
|
Method byte `tlv8:"0,optional"`
|
||||||
Identifier string `tlv8:"1"`
|
Identifier string `tlv8:"1,optional"`
|
||||||
PublicKey []byte `tlv8:"3"`
|
PublicKey []byte `tlv8:"3,optional"`
|
||||||
EncryptedData []byte `tlv8:"5"`
|
EncryptedData []byte `tlv8:"5,optional"`
|
||||||
State byte `tlv8:"6"`
|
State byte `tlv8:"6,optional"`
|
||||||
Status byte `tlv8:"7"`
|
Status byte `tlv8:"7,optional"`
|
||||||
Signature []byte `tlv8:"10"`
|
Signature []byte `tlv8:"10,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (c *Character) Unmarshal(value interface{}) error {
|
//func (c *Character) Unmarshal(value interface{}) error {
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -23,7 +23,7 @@ const (
|
|||||||
UriResource = "/resource"
|
UriResource = "/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) Write(p []byte) (r io.Reader, err error) {
|
func (c *Conn) Write(p []byte) (r io.Reader, err error) {
|
||||||
if c.secure == nil {
|
if c.secure == nil {
|
||||||
if _, err = c.conn.Write(p); err == nil {
|
if _, err = c.conn.Write(p); err == nil {
|
||||||
r = bufio.NewReader(c.conn)
|
r = bufio.NewReader(c.conn)
|
||||||
@@ -36,7 +36,7 @@ func (c *Client) Write(p []byte) (r io.Reader, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
func (c *Conn) Do(req *http.Request) (*http.Response, error) {
|
||||||
if c.secure == nil {
|
if c.secure == nil {
|
||||||
// insecure requests
|
// insecure requests
|
||||||
if err := req.Write(c.conn); err != nil {
|
if err := req.Write(c.conn); err != nil {
|
||||||
@@ -56,7 +56,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|||||||
return http.ReadResponse(buf, req)
|
return http.ReadResponse(buf, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Get(uri string) (*http.Response, error) {
|
func (c *Conn) Get(uri string) (*http.Response, error) {
|
||||||
req, err := http.NewRequest(
|
req, err := http.NewRequest(
|
||||||
"GET", "http://"+c.DeviceAddress+uri, nil,
|
"GET", "http://"+c.DeviceAddress+uri, nil,
|
||||||
)
|
)
|
||||||
@@ -66,7 +66,7 @@ func (c *Client) Get(uri string) (*http.Response, error) {
|
|||||||
return c.Do(req)
|
return c.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
|
func (c *Conn) Post(uri string, data []byte) (*http.Response, error) {
|
||||||
req, err := http.NewRequest(
|
req, err := http.NewRequest(
|
||||||
"POST", "http://"+c.DeviceAddress+uri,
|
"POST", "http://"+c.DeviceAddress+uri,
|
||||||
bytes.NewReader(data),
|
bytes.NewReader(data),
|
||||||
@@ -85,7 +85,7 @@ func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
|
|||||||
return c.Do(req)
|
return c.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
|
func (c *Conn) Put(uri string, data []byte) (*http.Response, error) {
|
||||||
req, err := http.NewRequest(
|
req, err := http.NewRequest(
|
||||||
"PUT", "http://"+c.DeviceAddress+uri,
|
"PUT", "http://"+c.DeviceAddress+uri,
|
||||||
bytes.NewReader(data),
|
bytes.NewReader(data),
|
||||||
@@ -102,7 +102,7 @@ func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
|
|||||||
return c.Do(req)
|
return c.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Handle() (err error) {
|
func (c *Conn) Handle() (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if c.conn == nil {
|
if c.conn == nil {
|
||||||
err = nil
|
err = nil
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
@@ -1,4 +1,4 @@
|
|||||||
package homekit
|
package hap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
@@ -1,103 +0,0 @@
|
|||||||
package camera
|
|
||||||
|
|
||||||
import (
|
|
||||||
cryptorand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"github.com/brutella/hap/rtp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
Offer *rtp.SetupEndpoints
|
|
||||||
Answer *rtp.SetupEndpointsResponse
|
|
||||||
Config *rtp.StreamConfiguration
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSession() *Session {
|
|
||||||
sessionID := RandomBytes(16)
|
|
||||||
s := &Session{
|
|
||||||
Offer: &rtp.SetupEndpoints{
|
|
||||||
SessionId: sessionID,
|
|
||||||
Video: rtp.CryptoSuite{
|
|
||||||
MasterKey: RandomBytes(16),
|
|
||||||
MasterSalt: RandomBytes(14),
|
|
||||||
},
|
|
||||||
Audio: rtp.CryptoSuite{
|
|
||||||
MasterKey: RandomBytes(16),
|
|
||||||
MasterSalt: RandomBytes(14),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &rtp.StreamConfiguration{
|
|
||||||
Command: rtp.SessionControlCommand{
|
|
||||||
Identifier: sessionID,
|
|
||||||
Type: rtp.SessionControlCommandTypeStart,
|
|
||||||
},
|
|
||||||
Video: rtp.VideoParameters{
|
|
||||||
CodecType: rtp.VideoCodecType_H264,
|
|
||||||
CodecParams: rtp.VideoCodecParameters{
|
|
||||||
Profiles: []rtp.VideoCodecProfile{
|
|
||||||
{Id: rtp.VideoCodecProfileMain},
|
|
||||||
},
|
|
||||||
Levels: []rtp.VideoCodecLevel{
|
|
||||||
{Level: rtp.VideoCodecLevel4},
|
|
||||||
},
|
|
||||||
Packetizations: []rtp.VideoCodecPacketization{
|
|
||||||
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Attributes: rtp.VideoCodecAttributes{
|
|
||||||
Width: 1920, Height: 1080, Framerate: 30,
|
|
||||||
},
|
|
||||||
RTP: rtp.RTPParams{
|
|
||||||
PayloadType: 99,
|
|
||||||
Ssrc: RandomUint32(),
|
|
||||||
Bitrate: 299,
|
|
||||||
Interval: 0.5,
|
|
||||||
ComfortNoisePayloadType: 98,
|
|
||||||
MTU: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Audio: rtp.AudioParameters{
|
|
||||||
CodecType: rtp.AudioCodecType_AAC_ELD,
|
|
||||||
CodecParams: rtp.AudioCodecParameters{
|
|
||||||
Channels: 1,
|
|
||||||
Bitrate: rtp.AudioCodecBitrateVariable,
|
|
||||||
Samplerate: rtp.AudioCodecSampleRate16Khz,
|
|
||||||
PacketTime: 30,
|
|
||||||
},
|
|
||||||
RTP: rtp.RTPParams{
|
|
||||||
PayloadType: 110,
|
|
||||||
Ssrc: RandomUint32(),
|
|
||||||
Bitrate: 24,
|
|
||||||
Interval: 5,
|
|
||||||
MTU: 13,
|
|
||||||
},
|
|
||||||
ComfortNoise: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) SetLocalEndpoint(host string, port uint16) {
|
|
||||||
s.Offer.ControllerAddr = rtp.Addr{
|
|
||||||
IPAddr: host,
|
|
||||||
VideoRtpPort: port,
|
|
||||||
AudioRtpPort: port,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) SetVideo() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func RandomBytes(size int) []byte {
|
|
||||||
data := make([]byte, size)
|
|
||||||
_, _ = cryptorand.Read(data)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func RandomUint32() uint32 {
|
|
||||||
data := make([]byte, 4)
|
|
||||||
_, _ = cryptorand.Read(data)
|
|
||||||
return binary.BigEndian.Uint32(data)
|
|
||||||
}
|
|
@@ -1,732 +1,265 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/brutella/hap"
|
"github.com/brutella/hap/characteristic"
|
||||||
"github.com/brutella/hap/chacha20poly1305"
|
"github.com/brutella/hap/rtp"
|
||||||
"github.com/brutella/hap/curve25519"
|
|
||||||
"github.com/brutella/hap/ed25519"
|
|
||||||
"github.com/brutella/hap/hkdf"
|
|
||||||
"github.com/brutella/hap/tlv8"
|
|
||||||
"github.com/tadglines/go-pkgs/crypto/srp"
|
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client for HomeKit. DevicePublic can be null.
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
DeviceAddress string // including port
|
conn *hap.Conn
|
||||||
DeviceID string
|
exit chan error
|
||||||
DevicePublic []byte
|
server *srtp.Server
|
||||||
ClientID string
|
url string
|
||||||
ClientPrivate []byte
|
|
||||||
|
|
||||||
OnEvent func(res *http.Response)
|
medias []*streamer.Media
|
||||||
Output func(msg interface{})
|
tracks []*streamer.Track
|
||||||
|
|
||||||
conn net.Conn
|
sessions []*srtp.Session
|
||||||
secure *Secure
|
|
||||||
httpResponse chan *bufio.Reader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(rawURL string) (*Client, error) {
|
func NewClient(rawURL string, server *srtp.Server) (*Client, error) {
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
query := u.Query()
|
query := u.Query()
|
||||||
c := &Client{
|
c := &hap.Conn{
|
||||||
DeviceAddress: u.Host,
|
DeviceAddress: u.Host,
|
||||||
DeviceID: query.Get("device_id"),
|
DeviceID: query.Get("device_id"),
|
||||||
DevicePublic: DecodeKey(query.Get("device_public")),
|
DevicePublic: hap.DecodeKey(query.Get("device_public")),
|
||||||
ClientID: query.Get("client_id"),
|
ClientID: query.Get("client_id"),
|
||||||
ClientPrivate: DecodeKey(query.Get("client_private")),
|
ClientPrivate: hap.DecodeKey(query.Get("client_private")),
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return &Client{conn: c, server: server}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func Pair(deviceID, pin string) (*Client, error) {
|
|
||||||
entry := mdns.GetEntry(deviceID)
|
|
||||||
if entry == nil {
|
|
||||||
return nil, errors.New("can't find device via mDNS")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Client{
|
|
||||||
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
|
|
||||||
DeviceID: deviceID,
|
|
||||||
ClientID: GenerateUUID(),
|
|
||||||
ClientPrivate: GenerateKey(),
|
|
||||||
}
|
|
||||||
|
|
||||||
var mfi bool
|
|
||||||
for _, field := range entry.InfoFields {
|
|
||||||
if field[:2] == "ff" {
|
|
||||||
if field[3] == '1' {
|
|
||||||
mfi = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, c.Pair(mfi, pin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ClientPublic() []byte {
|
|
||||||
return c.ClientPrivate[32:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) URL() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
|
|
||||||
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) DialAndServe() error {
|
|
||||||
if err := c.Dial(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.Handle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Dial() error {
|
func (c *Client) Dial() error {
|
||||||
// update device host before dial
|
if err := c.conn.Dial(); err != nil {
|
||||||
if host := mdns.GetAddress(c.DeviceID); host != "" {
|
|
||||||
c.DeviceAddress = host
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
c.conn, err = net.Dial("tcp", c.DeviceAddress)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP M1: send our session public to device
|
c.exit = make(chan error)
|
||||||
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
|
||||||
|
|
||||||
// 1. generate payload
|
go func() {
|
||||||
// important not include other fields
|
//start goroutine for reading responses from camera
|
||||||
requestM1 := struct {
|
c.exit <- c.conn.Handle()
|
||||||
State byte `tlv8:"6"`
|
}()
|
||||||
PublicKey []byte `tlv8:"3"`
|
|
||||||
}{
|
return nil
|
||||||
State: hap.M1,
|
}
|
||||||
PublicKey: sessionPublic[:],
|
|
||||||
}
|
func (c *Client) GetMedias() []*streamer.Media {
|
||||||
// 2. pack payload to TLV8
|
if c.medias == nil {
|
||||||
buf, err := tlv8.Marshal(requestM1)
|
c.medias = c.getMedias()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. send request
|
return c.medias
|
||||||
resp, err := c.Post(UriPairVerify, buf)
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// STEP M2: unpack deviceID from response
|
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||||
responseM2 := PairVerifyPayload{}
|
for _, track := range c.tracks {
|
||||||
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
|
if track.Codec == codec {
|
||||||
return err
|
return track
|
||||||
}
|
|
||||||
|
|
||||||
// 1. generate session shared key
|
|
||||||
var deviceSessionPublic [32]byte
|
|
||||||
copy(deviceSessionPublic[:], responseM2.PublicKey)
|
|
||||||
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
|
|
||||||
sessionKey, err := hkdf.Sha512(
|
|
||||||
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
|
|
||||||
[]byte("Pair-Verify-Encrypt-Info"),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 2. decrypt M2 response with session key
|
|
||||||
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
|
|
||||||
var mac [16]byte
|
|
||||||
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
|
|
||||||
|
|
||||||
buf, err = chacha20poly1305.DecryptAndVerify(
|
|
||||||
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 3. unpack payload from TLV8
|
|
||||||
payloadM2 := PairVerifyPayload{}
|
|
||||||
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. verify signature for M2 response with device public
|
|
||||||
// device session + device id + our session
|
|
||||||
if c.DevicePublic != nil {
|
|
||||||
buf = nil
|
|
||||||
buf = append(buf, responseM2.PublicKey[:]...)
|
|
||||||
buf = append(buf, []byte(payloadM2.Identifier)...)
|
|
||||||
buf = append(buf, sessionPublic[:]...)
|
|
||||||
if !ed25519.ValidateSignature(
|
|
||||||
c.DevicePublic[:], buf, payloadM2.Signature,
|
|
||||||
) {
|
|
||||||
return errors.New("device public signature invalid")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP M3: send our clientID to device
|
track := &streamer.Track{Codec: codec, Direction: 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
|
||||||
}
|
}
|
||||||
|
3
pkg/httpflv/README.md
Normal file
3
pkg/httpflv/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
|
100
pkg/httpflv/httpflv.go
Normal file
100
pkg/httpflv/httpflv.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package httpflv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
|
"github.com/deepch/vdk/format/flv/flvio"
|
||||||
|
"github.com/deepch/vdk/utils/bits/pio"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dial(uri string) (*Conn, error) {
|
||||||
|
req, err := http.NewRequest("GET", uri, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Conn{
|
||||||
|
conn: res.Body,
|
||||||
|
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
||||||
|
buf: make([]byte, 256),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
flags, n, err := flvio.ParseFileHeader(c.buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&flvio.FILE_HAS_VIDEO == 0 {
|
||||||
|
return nil, errors.New("not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = c.reader.Discard(n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
conn io.ReadCloser
|
||||||
|
reader *bufio.Reader
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Streams() ([]av.CodecData, error) {
|
||||||
|
for {
|
||||||
|
tag, _, err := flvio.ReadTag(c.reader, c.buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []av.CodecData{stream}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReadPacket() (av.Packet, error) {
|
||||||
|
for {
|
||||||
|
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
|
||||||
|
if err != nil {
|
||||||
|
return av.Packet{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return av.Packet{
|
||||||
|
Idx: 0,
|
||||||
|
Data: tag.Data,
|
||||||
|
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
||||||
|
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
||||||
|
Time: flvio.TsToTime(ts),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Close() (err error) {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||||
@@ -162,9 +161,12 @@ func (c *Client) getTracks() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
codec := streamer.NewCodec(streamer.CodecH264)
|
codec := &streamer.Codec{
|
||||||
codec.FmtpLine = "profile-level-id=" + msg.CodecString[i+1:]
|
Name: streamer.CodecH264,
|
||||||
codec.PayloadType = h264.PayloadTypeAVC
|
ClockRate: 90000,
|
||||||
|
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
|
||||||
|
PayloadType: streamer.PayloadTypeMP4,
|
||||||
|
}
|
||||||
|
|
||||||
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
@@ -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
|
||||||
|
@@ -3,6 +3,8 @@ package mp4
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"github.com/deepch/vdk/format/mp4/mp4io"
|
"github.com/deepch/vdk/format/mp4/mp4io"
|
||||||
|
"github.com/deepch/vdk/format/mp4f"
|
||||||
|
"github.com/deepch/vdk/format/mp4f/mp4fio"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,25 +39,17 @@ func MOOV() *mp4io.Movie {
|
|||||||
SelectionDuration: time0,
|
SelectionDuration: time0,
|
||||||
CurrentTime: time0,
|
CurrentTime: time0,
|
||||||
},
|
},
|
||||||
MovieExtend: &mp4io.MovieExtend{
|
MovieExtend: &mp4io.MovieExtend{},
|
||||||
Tracks: []*mp4io.TrackExtend{
|
|
||||||
{
|
|
||||||
TrackId: 1,
|
|
||||||
DefaultSampleDescIdx: 1,
|
|
||||||
DefaultSampleDuration: 40,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TRAK() *mp4io.Track {
|
func TRAK(id int) *mp4io.Track {
|
||||||
return &mp4io.Track{
|
return &mp4io.Track{
|
||||||
// trak > tkhd
|
// trak > tkhd
|
||||||
Header: &mp4io.TrackHeader{
|
Header: &mp4io.TrackHeader{
|
||||||
TrackId: int32(1), // change me
|
TrackId: int32(id),
|
||||||
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
|
Flags: 0x0007, // 7 ENABLED IN-MOVIE IN-PREVIEW
|
||||||
Duration: 0, // OK
|
Duration: 0, // OK
|
||||||
Matrix: matrix,
|
Matrix: matrix,
|
||||||
CreateTime: time0,
|
CreateTime: time0,
|
||||||
ModifyTime: time0,
|
ModifyTime: time0,
|
||||||
@@ -92,3 +86,15 @@ func TRAK() *mp4io.Track {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ESDS(conf []byte) *mp4f.FDummy {
|
||||||
|
esds := &mp4fio.ElemStreamDesc{DecConfig: conf}
|
||||||
|
|
||||||
|
b := make([]byte, esds.Len())
|
||||||
|
esds.Marshal(b)
|
||||||
|
|
||||||
|
return &mp4f.FDummy{
|
||||||
|
Data: b,
|
||||||
|
Tag_: mp4io.Tag(uint32(mp4io.ESDS)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -2,7 +2,7 @@ package mp4
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
@@ -28,44 +28,37 @@ func (c *Consumer) GetMedias() []*streamer.Media {
|
|||||||
Kind: streamer.KindVideo,
|
Kind: streamer.KindVideo,
|
||||||
Direction: streamer.DirectionRecvonly,
|
Direction: streamer.DirectionRecvonly,
|
||||||
Codecs: []*streamer.Codec{
|
Codecs: []*streamer.Codec{
|
||||||
{Name: streamer.CodecH264, ClockRate: 90000},
|
{Name: streamer.CodecH264},
|
||||||
{Name: streamer.CodecH265, ClockRate: 90000},
|
{Name: streamer.CodecH265},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: streamer.KindAudio,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecAAC},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
//{
|
|
||||||
// Kind: streamer.KindAudio,
|
|
||||||
// Direction: streamer.DirectionRecvonly,
|
|
||||||
// Codecs: []*streamer.Codec{
|
|
||||||
// {Name: streamer.CodecAAC, ClockRate: 16000},
|
|
||||||
// },
|
|
||||||
//},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
|
trackID := byte(len(c.codecs))
|
||||||
|
c.codecs = append(c.codecs, track.Codec)
|
||||||
|
|
||||||
codec := track.Codec
|
codec := track.Codec
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
c.codecs = append(c.codecs, track.Codec)
|
|
||||||
|
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
if packet.Version != h264.RTPPacketVersionAVC {
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.muxer == nil {
|
if !c.start {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.start {
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
if h264.IsKeyframe(packet.Payload) {
|
|
||||||
c.start = true
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := c.muxer.Marshal(packet)
|
|
||||||
c.send += len(buf)
|
c.send += len(buf)
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
@@ -73,7 +66,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wrapper streamer.WrapperFunc
|
var wrapper streamer.WrapperFunc
|
||||||
if h264.IsAVC(codec) {
|
if codec.IsMP4() {
|
||||||
wrapper = h264.RepairAVC(track)
|
wrapper = h264.RepairAVC(track)
|
||||||
} else {
|
} else {
|
||||||
wrapper = h264.RTPDepay(track)
|
wrapper = h264.RTPDepay(track)
|
||||||
@@ -83,39 +76,51 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
|
|
||||||
case streamer.CodecH265:
|
case streamer.CodecH265:
|
||||||
c.codecs = append(c.codecs, track.Codec)
|
|
||||||
|
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
if packet.Version != h264.RTPPacketVersionAVC {
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.start {
|
if !c.start {
|
||||||
if h265.IsKeyframe(packet.Payload) {
|
return nil
|
||||||
c.start = true
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
c.send += len(buf)
|
c.send += len(buf)
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !h264.IsAVC(codec) {
|
if !codec.IsMP4() {
|
||||||
wrapper := h265.RTPDepay(track)
|
wrapper := h265.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if !c.start {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
|
c.send += len(buf)
|
||||||
|
c.Fire(buf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !codec.IsMP4() {
|
||||||
|
wrapper := aac.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[rtmp] unsupported codec: %+v\n", track.Codec)
|
panic("unsupported codec")
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) MimeType() string {
|
func (c *Consumer) MimeType() string {
|
||||||
@@ -123,12 +128,14 @@ func (c *Consumer) MimeType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Init() ([]byte, error) {
|
func (c *Consumer) Init() ([]byte, error) {
|
||||||
if c.muxer == nil {
|
c.muxer = &Muxer{}
|
||||||
c.muxer = &Muxer{}
|
|
||||||
}
|
|
||||||
return c.muxer.GetInit(c.codecs)
|
return c.muxer.GetInit(c.codecs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Start() {
|
||||||
|
c.start = true
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||||
|
85
pkg/mp4/keyframe.go
Normal file
85
pkg/mp4/keyframe.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package mp4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Keyframe struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
MimeType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Keyframe) GetMedias() []*streamer.Media {
|
||||||
|
return []*streamer.Media{
|
||||||
|
{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecH264},
|
||||||
|
{Name: streamer.CodecH265},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
|
muxer := &Muxer{}
|
||||||
|
|
||||||
|
codecs := []*streamer.Codec{track.Codec}
|
||||||
|
|
||||||
|
init, err := muxer.GetInit(codecs)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.MimeType = muxer.MimeType(codecs)
|
||||||
|
|
||||||
|
switch track.Codec.Name {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if !h264.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := muxer.Marshal(0, packet)
|
||||||
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapper streamer.WrapperFunc
|
||||||
|
if track.Codec.IsMP4() {
|
||||||
|
wrapper = h264.RepairAVC(track)
|
||||||
|
} else {
|
||||||
|
wrapper = h264.RTPDepay(track)
|
||||||
|
}
|
||||||
|
push = wrapper(push)
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecH265:
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if !h265.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := muxer.Marshal(0, packet)
|
||||||
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !track.Codec.IsMP4() {
|
||||||
|
wrapper := h265.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unsupported codec")
|
||||||
|
}
|
105
pkg/mp4/muxer.go
105
pkg/mp4/muxer.go
@@ -2,10 +2,12 @@ package mp4
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"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/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/codec/h265parser"
|
"github.com/deepch/vdk/codec/h265parser"
|
||||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||||
@@ -16,22 +18,28 @@ import (
|
|||||||
|
|
||||||
type Muxer struct {
|
type Muxer struct {
|
||||||
fragIndex uint32
|
fragIndex uint32
|
||||||
dts uint64
|
dts []uint64
|
||||||
pts uint32
|
pts []uint32
|
||||||
data []byte
|
//data []byte
|
||||||
total int
|
//total int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||||
s := `video/mp4; codecs="`
|
s := `video/mp4; codecs="`
|
||||||
|
|
||||||
for _, codec := range codecs {
|
for i, codec := range codecs {
|
||||||
|
if i > 0 {
|
||||||
|
s += ","
|
||||||
|
}
|
||||||
|
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||||
case streamer.CodecH265:
|
case streamer.CodecH265:
|
||||||
// +Safari +Chrome +Edge -iOS15 -Android13
|
// +Safari +Chrome +Edge -iOS15 -Android13
|
||||||
s += "hvc1.1.6.L93.B0" // hev1.1.6.L93.B0
|
s += "hvc1.1.6.L93.B0" // hev1.1.6.L93.B0
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
s += "mp4a.40.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +49,7 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
|||||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||||
moov := MOOV()
|
moov := MOOV()
|
||||||
|
|
||||||
for _, codec := range codecs {
|
for i, codec := range codecs {
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||||
@@ -59,11 +67,14 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
width := codecData.Width()
|
width := codecData.Width()
|
||||||
height := codecData.Height()
|
height := codecData.Height()
|
||||||
|
|
||||||
trak := TRAK()
|
trak := TRAK(i + 1)
|
||||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
|
||||||
trak.Header.TrackWidth = float64(width)
|
trak.Header.TrackWidth = float64(width)
|
||||||
trak.Header.TrackHeight = float64(height)
|
trak.Header.TrackHeight = float64(height)
|
||||||
|
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||||
|
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||||
|
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||||
|
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||||
|
}
|
||||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||||
Flags: 0x000001,
|
Flags: 0x000001,
|
||||||
}
|
}
|
||||||
@@ -81,11 +92,6 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
|
||||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
|
||||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
|
||||||
}
|
|
||||||
|
|
||||||
moov.Tracks = append(moov.Tracks, trak)
|
moov.Tracks = append(moov.Tracks, trak)
|
||||||
|
|
||||||
case streamer.CodecH265:
|
case streamer.CodecH265:
|
||||||
@@ -102,11 +108,14 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
width := codecData.Width()
|
width := codecData.Width()
|
||||||
height := codecData.Height()
|
height := codecData.Height()
|
||||||
|
|
||||||
trak := TRAK()
|
trak := TRAK(i + 1)
|
||||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
|
||||||
trak.Header.TrackWidth = float64(width)
|
trak.Header.TrackWidth = float64(width)
|
||||||
trak.Header.TrackHeight = float64(height)
|
trak.Header.TrackHeight = float64(height)
|
||||||
|
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||||
|
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||||
|
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||||
|
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||||
|
}
|
||||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||||
Flags: 0x000001,
|
Flags: 0x000001,
|
||||||
}
|
}
|
||||||
@@ -124,13 +133,47 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moov.Tracks = append(moov.Tracks, trak)
|
||||||
|
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
trak := TRAK(i + 1)
|
||||||
|
trak.Header.AlternateGroup = 1
|
||||||
|
trak.Header.Duration = 0
|
||||||
|
trak.Header.Volume = 1
|
||||||
|
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||||
|
|
||||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
SubType: [4]byte{'s', 'o', 'u', 'n'},
|
||||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||||
}
|
}
|
||||||
|
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
|
||||||
|
|
||||||
|
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
||||||
|
DataRefIdx: 1,
|
||||||
|
NumberOfChannels: int16(codec.Channels),
|
||||||
|
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
||||||
|
SampleRate: float64(codec.ClockRate),
|
||||||
|
Unknowns: []mp4io.Atom{ESDS(b)},
|
||||||
|
}
|
||||||
|
|
||||||
moov.Tracks = append(moov.Tracks, trak)
|
moov.Tracks = append(moov.Tracks, trak)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trex := &mp4io.TrackExtend{
|
||||||
|
TrackId: uint32(i + 1),
|
||||||
|
DefaultSampleDescIdx: 1,
|
||||||
|
DefaultSampleDuration: 0,
|
||||||
|
}
|
||||||
|
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
|
||||||
|
|
||||||
|
m.pts = append(m.pts, 0)
|
||||||
|
m.dts = append(m.dts, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make([]byte, moov.Len())
|
data := make([]byte, moov.Len())
|
||||||
@@ -139,14 +182,12 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
return append(FTYP(), data...), nil
|
return append(FTYP(), data...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) Rewind() {
|
//func (m *Muxer) Rewind() {
|
||||||
m.dts = 0
|
// m.dts = 0
|
||||||
m.pts = 0
|
// m.pts = 0
|
||||||
}
|
//}
|
||||||
|
|
||||||
func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
|
||||||
trackID := uint8(1)
|
|
||||||
|
|
||||||
|
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||||
run := &mp4fio.TrackFragRun{
|
run := &mp4fio.TrackFragRun{
|
||||||
Flags: 0x000b05,
|
Flags: 0x000b05,
|
||||||
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
|
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
|
||||||
@@ -161,12 +202,12 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
|||||||
Tracks: []*mp4fio.TrackFrag{
|
Tracks: []*mp4fio.TrackFrag{
|
||||||
{
|
{
|
||||||
Header: &mp4fio.TrackFragHeader{
|
Header: &mp4fio.TrackFragHeader{
|
||||||
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID, 0x01, 0x01, 0x00, 0x00},
|
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
|
||||||
},
|
},
|
||||||
DecodeTime: &mp4fio.TrackFragDecodeTime{
|
DecodeTime: &mp4fio.TrackFragDecodeTime{
|
||||||
Version: 1,
|
Version: 1,
|
||||||
Flags: 0,
|
Flags: 0,
|
||||||
Time: m.dts,
|
Time: m.dts[trackID],
|
||||||
},
|
},
|
||||||
Run: run,
|
Run: run,
|
||||||
},
|
},
|
||||||
@@ -179,12 +220,12 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newTime := packet.Timestamp
|
newTime := packet.Timestamp
|
||||||
if m.pts > 0 {
|
if m.pts[trackID] > 0 {
|
||||||
//m.dts += uint64(newTime - m.pts)
|
//m.dts += uint64(newTime - m.pts)
|
||||||
entry.Duration = newTime - m.pts
|
entry.Duration = newTime - m.pts[trackID]
|
||||||
m.dts += uint64(entry.Duration)
|
m.dts[trackID] += uint64(entry.Duration)
|
||||||
}
|
}
|
||||||
m.pts = newTime
|
m.pts[trackID] = newTime
|
||||||
|
|
||||||
// important before moof.Len()
|
// important before moof.Len()
|
||||||
run.Entries = append(run.Entries, entry)
|
run.Entries = append(run.Entries, entry)
|
||||||
@@ -204,7 +245,7 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
|||||||
|
|
||||||
m.fragIndex++
|
m.fragIndex++
|
||||||
|
|
||||||
m.total += moofLen + mdatLen
|
//m.total += moofLen + mdatLen
|
||||||
|
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
164
pkg/mp4f/consumer.go
Normal file
164
pkg/mp4f/consumer.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package mp4f
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/aacparser"
|
||||||
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
|
"github.com/deepch/vdk/format/mp4f"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Consumer struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
|
muxer *mp4f.Muxer
|
||||||
|
streams []av.CodecData
|
||||||
|
mimeType string
|
||||||
|
start bool
|
||||||
|
|
||||||
|
send int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||||
|
return []*streamer.Media{
|
||||||
|
{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecH264, ClockRate: 90000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: streamer.KindAudio,
|
||||||
|
Direction: streamer.DirectionRecvonly,
|
||||||
|
Codecs: []*streamer.Codec{
|
||||||
|
{Name: streamer.CodecAAC, ClockRate: 16000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
|
codec := track.Codec
|
||||||
|
trackID := int8(len(c.streams))
|
||||||
|
|
||||||
|
switch codec.Name {
|
||||||
|
case streamer.CodecH264:
|
||||||
|
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||||
|
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||||
|
c.streams = append(c.streams, stream)
|
||||||
|
|
||||||
|
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||||
|
|
||||||
|
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||||
|
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.start {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt.Data = packet.Payload
|
||||||
|
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||||
|
if pkt.Time > 0 {
|
||||||
|
pkt.Duration = newTime - pkt.Time
|
||||||
|
}
|
||||||
|
pkt.Time = newTime
|
||||||
|
|
||||||
|
ready, buf, _ := c.muxer.WritePacket(pkt, false)
|
||||||
|
if ready {
|
||||||
|
c.send += len(buf)
|
||||||
|
c.Fire(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !codec.IsMP4() {
|
||||||
|
wrapper := h264.RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
|
||||||
|
|
||||||
|
c.mimeType += ",mp4a.40.2"
|
||||||
|
c.streams = append(c.streams, stream)
|
||||||
|
|
||||||
|
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||||
|
|
||||||
|
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||||
|
|
||||||
|
push := func(packet *rtp.Packet) error {
|
||||||
|
if !c.start {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt.Data = packet.Payload
|
||||||
|
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||||
|
if pkt.Time > 0 {
|
||||||
|
pkt.Duration = newTime - pkt.Time
|
||||||
|
}
|
||||||
|
pkt.Time = newTime
|
||||||
|
|
||||||
|
ready, buf, _ := c.muxer.WritePacket(pkt, false)
|
||||||
|
if ready {
|
||||||
|
c.send += len(buf)
|
||||||
|
c.Fire(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.Bind(push)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unsupported codec")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) MimeType() string {
|
||||||
|
return `video/mp4; codecs="` + c.mimeType + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Init() ([]byte, error) {
|
||||||
|
c.muxer = mp4f.NewMuxer(nil)
|
||||||
|
if err := c.muxer.WriteHeader(c.streams); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, data := c.muxer.GetInit(c.streams)
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Consumer) Start() {
|
||||||
|
c.start = true
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"type": "MSE server consumer",
|
||||||
|
"send": c.send,
|
||||||
|
"remote_addr": c.RemoteAddr,
|
||||||
|
"user_agent": c.UserAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(v)
|
||||||
|
}
|
@@ -4,16 +4,24 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/httpflv"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/av"
|
"github.com/deepch/vdk/av"
|
||||||
"github.com/deepch/vdk/codec/aacparser"
|
"github.com/deepch/vdk/codec/aacparser"
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/rtmp"
|
"github.com/deepch/vdk/format/rtmp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Conn for RTMP and RTMPT (flv over HTTP)
|
||||||
|
type Conn interface {
|
||||||
|
Streams() (streams []av.CodecData, err error)
|
||||||
|
ReadPacket() (pkt av.Packet, err error)
|
||||||
|
Close() (err error)
|
||||||
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
@@ -22,7 +30,7 @@ type Client struct {
|
|||||||
medias []*streamer.Media
|
medias []*streamer.Media
|
||||||
tracks []*streamer.Track
|
tracks []*streamer.Track
|
||||||
|
|
||||||
conn *rtmp.Conn
|
conn Conn
|
||||||
closed bool
|
closed bool
|
||||||
|
|
||||||
receive int
|
receive int
|
||||||
@@ -33,7 +41,12 @@ func NewClient(uri string) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Dial() (err error) {
|
func (c *Client) Dial() (err error) {
|
||||||
c.conn, err = rtmp.Dial(c.URI)
|
if strings.HasPrefix(c.URI, "http") {
|
||||||
|
c.conn, err = httpflv.Dial(c.URI)
|
||||||
|
} else {
|
||||||
|
c.conn, err = rtmp.Dial(c.URI)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,7 +73,7 @@ func (c *Client) Dial() (err error) {
|
|||||||
Name: streamer.CodecH264,
|
Name: streamer.CodecH264,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
FmtpLine: fmtp,
|
FmtpLine: fmtp,
|
||||||
PayloadType: h264.PayloadTypeAVC,
|
PayloadType: streamer.PayloadTypeMP4,
|
||||||
}
|
}
|
||||||
|
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
@@ -79,17 +92,13 @@ func (c *Client) Dial() (err error) {
|
|||||||
// TODO: fix support
|
// TODO: fix support
|
||||||
cd := stream.(aacparser.CodecData)
|
cd := stream.(aacparser.CodecData)
|
||||||
|
|
||||||
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
|
||||||
fmtp := fmt.Sprintf(
|
|
||||||
"config=%s",
|
|
||||||
hex.EncodeToString(cd.ConfigBytes),
|
|
||||||
)
|
|
||||||
|
|
||||||
codec := &streamer.Codec{
|
codec := &streamer.Codec{
|
||||||
Name: streamer.CodecAAC,
|
Name: streamer.CodecAAC,
|
||||||
ClockRate: uint32(cd.Config.SampleRate),
|
ClockRate: uint32(cd.Config.SampleRate),
|
||||||
Channels: uint16(cd.Config.ChannelConfig),
|
Channels: uint16(cd.Config.ChannelConfig),
|
||||||
FmtpLine: fmtp,
|
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
||||||
|
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
|
||||||
|
PayloadType: streamer.PayloadTypeMP4,
|
||||||
}
|
}
|
||||||
|
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
|
@@ -32,7 +32,7 @@ func (c *Client) MarshalJSON() ([]byte, error) {
|
|||||||
v := map[string]interface{}{
|
v := map[string]interface{}{
|
||||||
streamer.JSONReceive: c.receive,
|
streamer.JSONReceive: c.receive,
|
||||||
streamer.JSONType: "RTMP client producer",
|
streamer.JSONType: "RTMP client producer",
|
||||||
streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
|
//streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
|
||||||
"url": c.URI,
|
"url": c.URI,
|
||||||
}
|
}
|
||||||
for i, media := range c.medias {
|
for i, media := range c.medias {
|
||||||
|
163
pkg/rtsp/conn.go
163
pkg/rtsp/conn.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
@@ -43,7 +44,14 @@ const (
|
|||||||
ModeServerConsumer
|
ModeServerConsumer
|
||||||
)
|
)
|
||||||
|
|
||||||
const KeepAlive = time.Second * 25
|
type State byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateNone State = iota
|
||||||
|
StateConn
|
||||||
|
StateSetup
|
||||||
|
StatePlay
|
||||||
|
)
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
@@ -62,6 +70,7 @@ type Conn struct {
|
|||||||
auth *tcp.Auth
|
auth *tcp.Auth
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
mode Mode
|
mode Mode
|
||||||
|
state State
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
sequence int
|
sequence int
|
||||||
uri string
|
uri string
|
||||||
@@ -108,16 +117,11 @@ func (c *Conn) parseURI() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Dial() (err error) {
|
func (c *Conn) Dial() (err error) {
|
||||||
//if c.state != StateClientInit {
|
|
||||||
// panic("wrong state")
|
|
||||||
//}
|
|
||||||
if c.conn != nil {
|
if c.conn != nil {
|
||||||
_ = c.parseURI()
|
_ = c.parseURI()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.conn, err = net.DialTimeout(
|
c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
|
||||||
"tcp", c.URL.Host, 10*time.Second,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -139,6 +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
|
||||||
}
|
}
|
||||||
@@ -362,21 +367,25 @@ func (c *Conn) SetupMedia(
|
|||||||
var res *tcp.Response
|
var res *tcp.Response
|
||||||
res, err = c.Do(req)
|
res, err = c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Dahua VTO2111D fail on this step because of backchannel
|
// some Dahua/Amcrest cameras fail here because two simultaneous
|
||||||
|
// backchannel connections
|
||||||
if c.Backchannel {
|
if c.Backchannel {
|
||||||
if err = c.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.Backchannel = false
|
c.Backchannel = false
|
||||||
if err = c.Describe(); err != nil {
|
if err := c.Dial(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res, err = c.Do(req)
|
if err := c.Describe(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newMedia := range c.Medias {
|
||||||
|
if newMedia.Control == media.Control {
|
||||||
|
return c.SetupMedia(newMedia, newMedia.Codecs[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Session == "" {
|
if c.Session == "" {
|
||||||
@@ -435,44 +444,41 @@ func (c *Conn) SetupMedia(
|
|||||||
track = c.bindTrack(track, byte(ch), codec.PayloadType)
|
track = c.bindTrack(track, byte(ch), codec.PayloadType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.state = StateSetup
|
||||||
c.tracks = append(c.tracks, track)
|
c.tracks = append(c.tracks, track)
|
||||||
|
|
||||||
return track, nil
|
return track, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Play() (err error) {
|
func (c *Conn) Play() (err error) {
|
||||||
|
if c.state != StateSetup {
|
||||||
|
return fmt.Errorf("RTSP PLAY from wrong state: %s", c.state)
|
||||||
|
}
|
||||||
|
|
||||||
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
|
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
|
||||||
return c.Request(req)
|
return c.Request(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Teardown() (err error) {
|
func (c *Conn) Teardown() (err error) {
|
||||||
//if c.state != StateClientPlay {
|
// allow TEARDOWN from any state (ex. ANNOUNCE > SETUP)
|
||||||
// panic("wrong state")
|
|
||||||
//}
|
|
||||||
|
|
||||||
req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
|
req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
|
||||||
return c.Request(req)
|
return c.Request(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Close() error {
|
func (c *Conn) Close() error {
|
||||||
if c.conn == nil {
|
if c.state == StateNone {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := c.Teardown(); err != nil {
|
if err := c.Teardown(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn := c.conn
|
c.state = StateNone
|
||||||
c.conn = nil
|
return c.conn.Close()
|
||||||
return conn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
||||||
|
|
||||||
func (c *Conn) Accept() error {
|
func (c *Conn) Accept() error {
|
||||||
//if c.state != StateServerInit {
|
|
||||||
// panic("wrong state")
|
|
||||||
//}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
req, err := tcp.ReadRequest(c.reader)
|
req, err := tcp.ReadRequest(c.reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -575,8 +581,9 @@ func (c *Conn) Accept() error {
|
|||||||
Request: req,
|
Request: req,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tr[:len(transport)] == transport {
|
if strings.HasPrefix(tr, transport) {
|
||||||
c.Session = "1" // TODO: fixme
|
c.Session = "1" // TODO: fixme
|
||||||
|
c.state = StateSetup
|
||||||
res.Header.Set("Transport", tr[:len(transport)+3])
|
res.Header.Set("Transport", tr[:len(transport)+3])
|
||||||
} else {
|
} else {
|
||||||
res.Status = "461 Unsupported transport"
|
res.Status = "461 Unsupported transport"
|
||||||
@@ -597,17 +604,53 @@ 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.conn == nil {
|
if c.state == StateNone {
|
||||||
err = nil
|
err = nil
|
||||||
|
return
|
||||||
}
|
}
|
||||||
//c.Fire(streamer.StateNull)
|
|
||||||
|
// may have gotten here because of the deadline
|
||||||
|
// so close the connection to stop keepalive
|
||||||
|
c.state = StateNone
|
||||||
|
_ = c.conn.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
//c.Fire(streamer.StatePlaying)
|
var timeout time.Duration
|
||||||
ts := time.Now().Add(KeepAlive)
|
|
||||||
|
switch c.mode {
|
||||||
|
case ModeClientProducer:
|
||||||
|
// polling frames from remote RTSP Server (ex Camera)
|
||||||
|
timeout = time.Second * 5
|
||||||
|
go c.keepalive()
|
||||||
|
|
||||||
|
case ModeServerProducer:
|
||||||
|
// polling frames from remote RTSP Client (ex FFmpeg)
|
||||||
|
timeout = time.Second * 15
|
||||||
|
|
||||||
|
case ModeServerConsumer:
|
||||||
|
// pushing frames to remote RTSP Client (ex VLC)
|
||||||
|
timeout = time.Second * 60
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
if c.state == StateNone {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// we can read:
|
// we can read:
|
||||||
// 1. RTP interleaved: `$` + 1B channel number + 2B size
|
// 1. RTP interleaved: `$` + 1B channel number + 2B size
|
||||||
// 2. RTSP response: RTSP/1.0 200 OK
|
// 2. RTSP response: RTSP/1.0 200 OK
|
||||||
@@ -619,22 +662,21 @@ func (c *Conn) Handle() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if buf4[0] != '$' {
|
if buf4[0] != '$' {
|
||||||
if string(buf4) == "RTSP" {
|
switch string(buf4) {
|
||||||
|
case "RTSP":
|
||||||
var res *tcp.Response
|
var res *tcp.Response
|
||||||
res, err = tcp.ReadResponse(c.reader)
|
if res, err = tcp.ReadResponse(c.reader); err != nil {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Fire(res)
|
c.Fire(res)
|
||||||
} else {
|
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
||||||
var req *tcp.Request
|
var req *tcp.Request
|
||||||
req, err = tcp.ReadRequest(c.reader)
|
if req, err = tcp.ReadRequest(c.reader); err != nil {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Fire(req)
|
c.Fire(req)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("RTSP wrong input")
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -685,16 +727,19 @@ func (c *Conn) Handle() (err error) {
|
|||||||
|
|
||||||
c.Fire(msg)
|
c.Fire(msg)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// keep-alive
|
func (c *Conn) keepalive() {
|
||||||
now := time.Now()
|
// TODO: rewrite to RTCP
|
||||||
if now.After(ts) {
|
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
for {
|
||||||
// don't need to wait respose on this request
|
time.Sleep(time.Second * 25)
|
||||||
if err = c.Request(req); err != nil {
|
if c.state == StateNone {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
ts = now.Add(KeepAlive)
|
if err := c.Request(req); err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,20 +757,16 @@ func (c *Conn) bindTrack(
|
|||||||
track *streamer.Track, channel uint8, payloadType uint8,
|
track *streamer.Track, channel uint8, payloadType uint8,
|
||||||
) *streamer.Track {
|
) *streamer.Track {
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
if c.conn == nil {
|
if c.state == StateNone {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
packet.Header.PayloadType = payloadType
|
packet.Header.PayloadType = payloadType
|
||||||
//packet.Header.PayloadType = 100
|
|
||||||
//packet.Header.PayloadType = 8
|
|
||||||
//packet.Header.PayloadType = 106
|
|
||||||
|
|
||||||
size := packet.MarshalSize()
|
size := packet.MarshalSize()
|
||||||
|
|
||||||
data := make([]byte, 4+size)
|
data := make([]byte, 4+size)
|
||||||
data[0] = '$'
|
data[0] = '$'
|
||||||
data[1] = channel
|
data[1] = channel
|
||||||
//data[1] = 10
|
|
||||||
binary.BigEndian.PutUint16(data[2:], uint16(size))
|
binary.BigEndian.PutUint16(data[2:], uint16(size))
|
||||||
|
|
||||||
if _, err := packet.MarshalTo(data[4:]); err != nil {
|
if _, err := packet.MarshalTo(data[4:]); err != nil {
|
||||||
@@ -741,9 +782,15 @@ func (c *Conn) bindTrack(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if h264.IsAVC(track.Codec) {
|
if track.Codec.IsMP4() {
|
||||||
wrapper := h264.RTPPay(1500)
|
switch track.Codec.Name {
|
||||||
push = wrapper(push)
|
case streamer.CodecH264:
|
||||||
|
wrapper := h264.RTPPay(1500)
|
||||||
|
push = wrapper(push)
|
||||||
|
case streamer.CodecAAC:
|
||||||
|
wrapper := aac.RTPPay(1500)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
|
@@ -2,6 +2,7 @@ package rtsp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
@@ -19,6 +20,11 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// can't setup new tracks from play state
|
||||||
|
if c.state == StatePlay {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
track, err := c.SetupMedia(media, codec)
|
track, err := c.SetupMedia(media, codec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -27,13 +33,16 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Start() error {
|
func (c *Conn) Start() error {
|
||||||
if c.mode == ModeServerProducer {
|
switch c.mode {
|
||||||
return nil
|
case ModeClientProducer:
|
||||||
|
if err := c.Play(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case ModeServerProducer:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("start wrong mode: %d", c.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Play(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.Handle()
|
return c.Handle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,9 +8,19 @@ import (
|
|||||||
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
|
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
|
||||||
// this is not really necessary but anyway
|
// this is not really necessary but anyway
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
conn net.PacketConn
|
||||||
sessions map[uint32]*Session
|
sessions map[uint32]*Session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) Port() uint16 {
|
||||||
|
addr := s.conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
return uint16(addr.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Close() error {
|
||||||
|
return s.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) AddSession(session *Session) {
|
func (s *Server) AddSession(session *Session) {
|
||||||
if s.sessions == nil {
|
if s.sessions == nil {
|
||||||
s.sessions = map[uint32]*Session{}
|
s.sessions = map[uint32]*Session{}
|
||||||
@@ -23,6 +33,8 @@ func (s *Server) RemoveSession(session *Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Serve(conn net.PacketConn) error {
|
func (s *Server) Serve(conn net.PacketConn) error {
|
||||||
|
s.conn = conn
|
||||||
|
|
||||||
buf := make([]byte, 2048)
|
buf := make([]byte, 2048)
|
||||||
for {
|
for {
|
||||||
n, addr, err := conn.ReadFrom(buf)
|
n, addr, err := conn.ReadFrom(buf)
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/pion/rtcp"
|
"github.com/pion/rtcp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"github.com/pion/srtp/v2"
|
"github.com/pion/srtp/v2"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
@@ -16,6 +17,14 @@ type Session struct {
|
|||||||
|
|
||||||
Write func(b []byte) (int, error)
|
Write func(b []byte) (int, error)
|
||||||
Track *streamer.Track
|
Track *streamer.Track
|
||||||
|
|
||||||
|
lastSequence uint32
|
||||||
|
lastTimestamp uint32
|
||||||
|
//lastPacket *rtp.Packet
|
||||||
|
lastTime time.Time
|
||||||
|
jitter float64
|
||||||
|
//sequenceCycle uint16
|
||||||
|
totalLost uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) SetKeys(
|
func (s *Session) SetKeys(
|
||||||
@@ -37,13 +46,42 @@ func (s *Session) HandleRTP(data []byte) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Track == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
packet := &rtp.Packet{}
|
packet := &rtp.Packet{}
|
||||||
if err = packet.Unmarshal(data); err != nil {
|
if err = packet.Unmarshal(data); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// https://www.ietf.org/rfc/rfc3550.txt
|
||||||
|
if s.lastTimestamp != 0 {
|
||||||
|
delta := packet.SequenceNumber - uint16(s.lastSequence)
|
||||||
|
|
||||||
|
// lost packet
|
||||||
|
if delta > 1 {
|
||||||
|
s.totalLost += uint32(delta - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si)
|
||||||
|
dTime := now.Sub(s.lastTime).Seconds()*float64(s.Track.Codec.ClockRate) -
|
||||||
|
float64(packet.Timestamp-s.lastTimestamp)
|
||||||
|
if dTime < 0 {
|
||||||
|
dTime = -dTime
|
||||||
|
}
|
||||||
|
// J(i) = J(i-1) + (|D(i-1,i)| - J(i-1))/16
|
||||||
|
s.jitter += (dTime - s.jitter) / 16
|
||||||
|
}
|
||||||
|
|
||||||
|
// keeping cycles (overflow)
|
||||||
|
s.lastSequence = s.lastSequence&0xFFFF0000 | uint32(packet.SequenceNumber)
|
||||||
|
s.lastTimestamp = packet.Timestamp
|
||||||
|
s.lastTime = now
|
||||||
|
|
||||||
_ = s.Track.WriteRTP(packet)
|
_ = s.Track.WriteRTP(packet)
|
||||||
//s.Output(core.RTP{Channel: s.Channel, Packet: packet})
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,7 +98,6 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = packets
|
_ = packets
|
||||||
//s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets})
|
|
||||||
|
|
||||||
if header.Type == rtcp.TypeSenderReport {
|
if header.Type == rtcp.TypeSenderReport {
|
||||||
err = s.KeepAlive()
|
err = s.KeepAlive()
|
||||||
@@ -70,9 +107,25 @@ func (s *Session) HandleRTCP(data []byte) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) KeepAlive() (err error) {
|
func (s *Session) KeepAlive() (err error) {
|
||||||
var data []byte
|
|
||||||
// we can send empty receiver response, but should send it to hold the connection
|
|
||||||
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
|
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
|
||||||
|
|
||||||
|
if s.lastTimestamp > 0 {
|
||||||
|
//log.Printf("[RTCP] ssrc=%d seq=%d lost=%d jit=%.2f", s.RemoteSSRC, s.lastSequence, s.totalLost, s.jitter)
|
||||||
|
|
||||||
|
rep.Reports = []rtcp.ReceptionReport{{
|
||||||
|
SSRC: s.RemoteSSRC,
|
||||||
|
LastSequenceNumber: s.lastSequence,
|
||||||
|
LastSenderReport: s.lastTimestamp,
|
||||||
|
FractionLost: 0, // TODO
|
||||||
|
TotalLost: s.totalLost,
|
||||||
|
Delay: 0, // send just after receive
|
||||||
|
Jitter: uint32(s.jitter),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can send empty receiver response, but should send it to hold the connection
|
||||||
|
|
||||||
|
var data []byte
|
||||||
if data, err = rep.Marshal(); err != nil {
|
if data, err = rep.Marshal(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -90,8 +143,8 @@ func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
|
|||||||
switch len(masterKey) {
|
switch len(masterKey) {
|
||||||
case 16:
|
case 16:
|
||||||
return srtp.ProtectionProfileAes128CmHmacSha1_80
|
return srtp.ProtectionProfileAes128CmHmacSha1_80
|
||||||
//case 32:
|
//case 32:
|
||||||
// return srtp.ProtectionProfileAes256CmHmacSha1_80
|
// return srtp.ProtectionProfileAes256CmHmacSha1_80
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@@ -33,13 +33,17 @@ const (
|
|||||||
CodecOpus = "OPUS" // payloadType: 111
|
CodecOpus = "OPUS" // payloadType: 111
|
||||||
CodecG722 = "G722"
|
CodecG722 = "G722"
|
||||||
CodecMPA = "MPA" // payload: 14
|
CodecMPA = "MPA" // payload: 14
|
||||||
|
|
||||||
|
CodecELD = "ELD" // AAC-ELD
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PayloadTypeMP4 byte = 255
|
||||||
|
|
||||||
func GetKind(name string) string {
|
func GetKind(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||||
return KindVideo
|
return KindVideo
|
||||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
|
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA, CodecELD:
|
||||||
return KindAudio
|
return KindAudio
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@@ -75,13 +79,13 @@ func (m *Media) AV() bool {
|
|||||||
return m.Kind == KindVideo || m.Kind == KindAudio
|
return m.Kind == KindVideo || m.Kind == KindAudio
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Media) MatchCodec(codec *Codec) bool {
|
func (m *Media) MatchCodec(codec *Codec) *Codec {
|
||||||
for _, c := range m.Codecs {
|
for _, c := range m.Codecs {
|
||||||
if c.Match(codec) {
|
if c.Match(codec) {
|
||||||
return true
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Media) MatchMedia(media *Media) *Codec {
|
func (m *Media) MatchMedia(media *Media) *Codec {
|
||||||
@@ -127,22 +131,6 @@ type Codec struct {
|
|||||||
PayloadType uint8
|
PayloadType uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCodec(name string) *Codec {
|
|
||||||
name = strings.ToUpper(name)
|
|
||||||
switch name {
|
|
||||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
|
||||||
return &Codec{Name: name, ClockRate: 90000}
|
|
||||||
case CodecPCMU, CodecPCMA:
|
|
||||||
return &Codec{Name: name, ClockRate: 8000}
|
|
||||||
case CodecOpus:
|
|
||||||
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
|
|
||||||
case "MJPEG":
|
|
||||||
return &Codec{Name: CodecJPEG, ClockRate: 90000}
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(fmt.Sprintf("unsupported codec: %s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) String() string {
|
func (c *Codec) String() string {
|
||||||
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
|
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
|
||||||
if c.Channels > 0 {
|
if c.Channels > 0 {
|
||||||
@@ -151,6 +139,10 @@ func (c *Codec) String() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Codec) IsMP4() bool {
|
||||||
|
return c.PayloadType == PayloadTypeMP4
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Codec) Clone() *Codec {
|
func (c *Codec) Clone() *Codec {
|
||||||
clone := *c
|
clone := *c
|
||||||
return &clone
|
return &clone
|
||||||
@@ -197,13 +189,19 @@ func MarshalSDP(medias []*Media) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
codec := media.Codecs[0]
|
codec := media.Codecs[0]
|
||||||
|
|
||||||
|
name := codec.Name
|
||||||
|
if name == CodecELD {
|
||||||
|
name = CodecAAC
|
||||||
|
}
|
||||||
|
|
||||||
md := &sdp.MediaDescription{
|
md := &sdp.MediaDescription{
|
||||||
MediaName: sdp.MediaName{
|
MediaName: sdp.MediaName{
|
||||||
Media: media.Kind,
|
Media: media.Kind,
|
||||||
Protos: []string{"RTP", "AVP"},
|
Protos: []string{"RTP", "AVP"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
md.WithCodec(payloadType, codec.Name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
md.WithCodec(payloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||||
|
|
||||||
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
||||||
|
|
||||||
|
@@ -12,44 +12,54 @@ type WrapperFunc func(push WriterFunc) WriterFunc
|
|||||||
type Track struct {
|
type Track struct {
|
||||||
Codec *Codec
|
Codec *Codec
|
||||||
Direction string
|
Direction string
|
||||||
Sink map[*Track]WriterFunc
|
sink map[*Track]WriterFunc
|
||||||
mx sync.Mutex
|
sinkMu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) String() string {
|
func (t *Track) String() string {
|
||||||
s := t.Codec.String()
|
s := t.Codec.String()
|
||||||
s += fmt.Sprintf(", sinks=%d", len(t.Sink))
|
s += fmt.Sprintf(", sinks=%d", len(t.sink))
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
||||||
t.mx.Lock()
|
t.sinkMu.RLock()
|
||||||
for _, f := range t.Sink {
|
for _, f := range t.sink {
|
||||||
_ = f(p)
|
_ = f(p)
|
||||||
}
|
}
|
||||||
t.mx.Unlock()
|
t.sinkMu.RUnlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) Bind(w WriterFunc) *Track {
|
func (t *Track) Bind(w WriterFunc) *Track {
|
||||||
t.mx.Lock()
|
t.sinkMu.Lock()
|
||||||
|
|
||||||
if t.Sink == nil {
|
if t.sink == nil {
|
||||||
t.Sink = map[*Track]WriterFunc{}
|
t.sink = map[*Track]WriterFunc{}
|
||||||
}
|
}
|
||||||
|
|
||||||
clone := &Track{
|
clone := &Track{
|
||||||
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
|
Codec: t.Codec, Direction: t.Direction, sink: t.sink,
|
||||||
}
|
}
|
||||||
t.Sink[clone] = w
|
t.sink[clone] = w
|
||||||
|
|
||||||
t.mx.Unlock()
|
t.sinkMu.Unlock()
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) Unbind() {
|
func (t *Track) Unbind() {
|
||||||
t.mx.Lock()
|
t.sinkMu.Lock()
|
||||||
delete(t.Sink, t)
|
delete(t.sink, t)
|
||||||
t.mx.Unlock()
|
t.sinkMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) GetSink(from *Track) {
|
||||||
|
t.sink = from.sink
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) HasSink() bool {
|
||||||
|
t.sinkMu.RLock()
|
||||||
|
defer t.sinkMu.RUnlock()
|
||||||
|
return len(t.sink) > 0
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -100,6 +101,12 @@ func (c *Conn) SetOffer(offer string) (err error) {
|
|||||||
}
|
}
|
||||||
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
||||||
c.medias, err = streamer.UnmarshalSDP(rawSDP)
|
c.medias, err = streamer.UnmarshalSDP(rawSDP)
|
||||||
|
|
||||||
|
// sort medias, so video will always be before audio
|
||||||
|
sort.Slice(c.medias, func(i, j int) bool {
|
||||||
|
return c.medias[i].Kind == streamer.KindVideo
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -57,7 +57,7 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
|
|||||||
wrapper := h264.RTPPay(1200)
|
wrapper := h264.RTPPay(1200)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
|
|
||||||
if h264.IsAVC(codec) {
|
if codec.IsMP4() {
|
||||||
wrapper = h264.RepairAVC(track)
|
wrapper = h264.RepairAVC(track)
|
||||||
} else {
|
} else {
|
||||||
wrapper = h264.RTPDepay(track)
|
wrapper = h264.RTPDepay(track)
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewCandidate(address string) (string, error) {
|
func NewCandidate(address string) (string, error) {
|
||||||
@@ -38,7 +39,7 @@ func NewCandidate(address string) (string, error) {
|
|||||||
|
|
||||||
func LookupIP(address string) (string, error) {
|
func LookupIP(address string) (string, error) {
|
||||||
if strings.HasPrefix(address, "stun:") {
|
if strings.HasPrefix(address, "stun:") {
|
||||||
ip, err := GetPublicIP()
|
ip, err := GetCachedPublicIP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -63,11 +64,20 @@ func LookupIP(address string) (string, error) {
|
|||||||
|
|
||||||
// GetPublicIP example from https://github.com/pion/stun
|
// GetPublicIP example from https://github.com/pion/stun
|
||||||
func GetPublicIP() (net.IP, error) {
|
func GetPublicIP() (net.IP, error) {
|
||||||
c, err := stun.Dial("udp", "stun.l.google.com:19302")
|
conn, err := net.Dial("udp", "stun.l.google.com:19302")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c, err := stun.NewClient(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = conn.SetDeadline(time.Now().Add(time.Second * 3)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var res stun.Event
|
var res stun.Event
|
||||||
|
|
||||||
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||||
@@ -90,6 +100,24 @@ func GetPublicIP() (net.IP, error) {
|
|||||||
return xorAddr.IP, nil
|
return xorAddr.IP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cachedIP net.IP
|
||||||
|
var cachedTS time.Time
|
||||||
|
|
||||||
|
func GetCachedPublicIP() (net.IP, error) {
|
||||||
|
now := time.Now()
|
||||||
|
if now.After(cachedTS) {
|
||||||
|
newIP, err := GetPublicIP()
|
||||||
|
if err == nil {
|
||||||
|
cachedIP = newIP
|
||||||
|
cachedTS = now.Add(time.Minute * 5)
|
||||||
|
} else if cachedIP == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
func IsIP(host string) bool {
|
func IsIP(host string) bool {
|
||||||
for _, i := range host {
|
for _, i := range host {
|
||||||
if i >= 'A' {
|
if i >= 'A' {
|
||||||
|
@@ -53,3 +53,6 @@ pc.ontrack = ev => {
|
|||||||
- https://www.webrtc-experiment.com/DetectRTC/
|
- https://www.webrtc-experiment.com/DetectRTC/
|
||||||
- https://divtable.com/table-styler/
|
- https://divtable.com/table-styler/
|
||||||
- https://www.chromium.org/audio-video/
|
- https://www.chromium.org/audio-video/
|
||||||
|
- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
|
||||||
|
- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
|
||||||
|
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html
|
||||||
|
122
www/mse.html
122
www/mse.html
@@ -25,93 +25,75 @@
|
|||||||
<!-- muted is important for autoplay -->
|
<!-- muted is important for autoplay -->
|
||||||
<video id="video" autoplay controls playsinline muted></video>
|
<video id="video" autoplay controls playsinline muted></video>
|
||||||
<script>
|
<script>
|
||||||
const video = document.querySelector('#video');
|
|
||||||
|
|
||||||
// support api_path
|
// support api_path
|
||||||
const baseUrl = location.origin + location.pathname.substr(
|
const baseUrl = location.origin + location.pathname.substr(
|
||||||
0, location.pathname.lastIndexOf("/")
|
0, location.pathname.lastIndexOf("/")
|
||||||
);
|
);
|
||||||
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
|
const video = document.querySelector('#video');
|
||||||
ws.binaryType = "arraybuffer";
|
|
||||||
|
|
||||||
let mediaSource;
|
function init() {
|
||||||
|
let mediaSource, sourceBuffer, queueBuffer = [];
|
||||||
|
|
||||||
ws.onopen = () => {
|
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
|
||||||
console.log("Start WS");
|
ws.binaryType = "arraybuffer";
|
||||||
|
|
||||||
// https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
|
ws.onopen = () => {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
|
mediaSource = new MediaSource();
|
||||||
mediaSource = new MediaSource();
|
video.src = URL.createObjectURL(mediaSource);
|
||||||
video.src = URL.createObjectURL(mediaSource);
|
mediaSource.onsourceopen = () => {
|
||||||
mediaSource.onsourceopen = () => {
|
mediaSource.onsourceopen = null;
|
||||||
console.debug("mediaSource.onsourceopen");
|
URL.revokeObjectURL(video.src);
|
||||||
|
ws.send(JSON.stringify({"type": "mse"}));
|
||||||
mediaSource.onsourceopen = null;
|
};
|
||||||
URL.revokeObjectURL(video.src);
|
|
||||||
ws.send(JSON.stringify({"type": "mse"}));
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let sourceBuffer, queueBuffer = [];
|
ws.onmessage = ev => {
|
||||||
|
if (typeof ev.data === 'string') {
|
||||||
|
const data = JSON.parse(ev.data);
|
||||||
|
console.debug("ws.onmessage", data);
|
||||||
|
|
||||||
ws.onmessage = ev => {
|
if (data.type === "mse") {
|
||||||
if (typeof ev.data === 'string') {
|
sourceBuffer = mediaSource.addSourceBuffer(data.value);
|
||||||
const data = JSON.parse(ev.data);
|
sourceBuffer.mode = "segments"; // segments or sequence
|
||||||
console.debug("ws.onmessage", data);
|
sourceBuffer.onupdateend = () => {
|
||||||
|
if (!sourceBuffer.updating && queueBuffer.length > 0) {
|
||||||
if (data.type === "mse") {
|
try {
|
||||||
sourceBuffer = mediaSource.addSourceBuffer(data.value);
|
sourceBuffer.appendBuffer(queueBuffer.shift());
|
||||||
// important: segments supports TrackFragDecodeTime
|
} catch (e) {
|
||||||
// sequence supports only TrackFragRunEntry Duration
|
// console.warn(e);
|
||||||
sourceBuffer.mode = "segments";
|
}
|
||||||
sourceBuffer.onupdateend = () => {
|
}
|
||||||
if (!sourceBuffer.updating && queueBuffer.length > 0) {
|
|
||||||
sourceBuffer.appendBuffer(queueBuffer.shift());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (sourceBuffer.updating || queueBuffer.length > 0) {
|
||||||
} else {
|
queueBuffer.push(ev.data);
|
||||||
if (sourceBuffer.updating) {
|
|
||||||
queueBuffer.push(ev.data)
|
|
||||||
} else {
|
} else {
|
||||||
sourceBuffer.appendBuffer(ev.data);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let offsetTime = 1, noWaiting = 0;
|
init();
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
if (video.paused || video.seekable.length === 0) return;
|
|
||||||
|
|
||||||
if (noWaiting < 0) {
|
|
||||||
offsetTime = Math.min(offsetTime * 1.1, 5);
|
|
||||||
console.debug("offset time up:", offsetTime);
|
|
||||||
} else if (noWaiting >= 30) {
|
|
||||||
noWaiting = 0;
|
|
||||||
offsetTime = Math.max(offsetTime * 0.9, 0.5);
|
|
||||||
console.debug("offset time down:", offsetTime);
|
|
||||||
}
|
|
||||||
noWaiting += 1;
|
|
||||||
|
|
||||||
const endTime = video.seekable.end(video.seekable.length - 1);
|
|
||||||
let playbackRate = (endTime - video.currentTime) / offsetTime;
|
|
||||||
if (playbackRate < 0.1) {
|
|
||||||
// video.currentTime = endTime - offsetTime;
|
|
||||||
playbackRate = 0.1;
|
|
||||||
} else if (playbackRate > 10) {
|
|
||||||
// video.currentTime = endTime - offsetTime;
|
|
||||||
playbackRate = 10;
|
|
||||||
}
|
|
||||||
// https://github.com/GoogleChrome/developer.chrome.com/issues/135
|
|
||||||
video.playbackRate = playbackRate;
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
video.onwaiting = () => {
|
|
||||||
const endTime = video.seekable.end(video.seekable.length - 1);
|
|
||||||
video.currentTime = endTime - offsetTime;
|
|
||||||
noWaiting = -1;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -25,12 +25,12 @@
|
|||||||
<body>
|
<body>
|
||||||
<video id="video" autoplay controls playsinline muted></video>
|
<video id="video" autoplay controls playsinline muted></video>
|
||||||
<script>
|
<script>
|
||||||
|
const baseUrl = location.origin + location.pathname.substr(
|
||||||
|
0, location.pathname.lastIndexOf("/")
|
||||||
|
);
|
||||||
|
|
||||||
function init(stream) {
|
function init(stream) {
|
||||||
// support api_path
|
// support api_path
|
||||||
const baseUrl = location.origin + location.pathname.substr(
|
|
||||||
0, location.pathname.lastIndexOf("/")
|
|
||||||
);
|
|
||||||
|
|
||||||
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
|
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.debug('ws.onopen');
|
console.debug('ws.onopen');
|
||||||
@@ -51,11 +51,6 @@
|
|||||||
pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
|
pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
|
||||||
} else if (msg.type === 'webrtc/answer') {
|
} else if (msg.type === 'webrtc/answer') {
|
||||||
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
|
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
|
||||||
pc.getTransceivers().forEach(t => {
|
|
||||||
if (t.receiver.track.kind === 'audio') {
|
|
||||||
t.currentDirection
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user