Merge branch 'AlexxIT:master' into wyze
This commit is contained in:
@@ -38,7 +38,7 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
|
||||
- creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev)
|
||||
|
||||
> [!CAUTION]
|
||||
> There is NO existing website for go2rtc project other than this GitHub repository. The website go2rtc[.]com is in no way associated with the authors of this project.
|
||||
> The official website of the project is this GitHub repository and go2rtc.org (hosted on GitHub Pages). The website go2rtc[.]com is in no way associated with the authors of this project.
|
||||
|
||||
---
|
||||
|
||||
@@ -378,9 +378,11 @@ But you can override them via YAML config. You can also add your own formats to
|
||||
```yaml
|
||||
ffmpeg:
|
||||
bin: ffmpeg # path to ffmpeg binary
|
||||
global: "-hide_banner"
|
||||
timeout: 5 # default timeout in seconds for rtsp inputs
|
||||
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
|
||||
mycodec: "-any args that supported by ffmpeg..."
|
||||
myinput: "-fflags nobuffer -flags low_delay -timeout 5000000 -i {input}"
|
||||
myinput: "-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}"
|
||||
myraw: "-ss 00:00:20"
|
||||
```
|
||||
|
||||
@@ -390,9 +392,10 @@ ffmpeg:
|
||||
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
|
||||
- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
|
||||
- This will greatly increase the CPU of the server, even with hardware acceleration
|
||||
- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`)
|
||||
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
|
||||
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
|
||||
- You can use raw input value (ex. `#input=-timeout 5000000 -i {input}`)
|
||||
- You can use raw input value (ex. `#input=-timeout {timeout} -i {input}`)
|
||||
- You can add your own input templates
|
||||
|
||||
Read more about [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
|
||||
@@ -1122,7 +1125,7 @@ webtorrent:
|
||||
src: rtsp-dahua1 # stream name from streams section
|
||||
```
|
||||
|
||||
Link example: https://alexxit.github.io/go2rtc/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio
|
||||
Link example: https://go2rtc.org/webtorrent/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio
|
||||
|
||||
### Module: ngrok
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ Fill free to make any API design proposals.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
|
||||
Interactive [OpenAPI](https://go2rtc.org/api/).
|
||||
|
||||
`www/stream.html` - universal viewer with support params in URL:
|
||||
|
||||
|
||||
+687
-83
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@ import (
|
||||
var MemoryLog = newBuffer()
|
||||
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
Logger.Trace().Str("module", module).Msgf("[log] init")
|
||||
|
||||
if s, ok := modules[module]; ok {
|
||||
lvl, err := zerolog.ParseLevel(s)
|
||||
if err == nil {
|
||||
|
||||
@@ -58,15 +58,15 @@ func Init() {
|
||||
}
|
||||
|
||||
var defaults = map[string]string{
|
||||
"bin": "ffmpeg",
|
||||
"global": "-hide_banner",
|
||||
"bin": "ffmpeg",
|
||||
"global": "-hide_banner",
|
||||
"timeout": "5",
|
||||
|
||||
// inputs
|
||||
"file": "-re -i {input}",
|
||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
|
||||
|
||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
|
||||
"file": "-re -i {input}",
|
||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
|
||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -i {input}",
|
||||
|
||||
// output
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||
@@ -169,6 +169,13 @@ func inputTemplate(name, s string, query url.Values) string {
|
||||
} else {
|
||||
template = defaults[name]
|
||||
}
|
||||
if strings.Contains(template, "{timeout}") {
|
||||
timeout := query.Get("timeout")
|
||||
if timeout == "" {
|
||||
timeout = defaults["timeout"]
|
||||
}
|
||||
template = strings.Replace(template, "{timeout}", timeout+"000000", 1)
|
||||
}
|
||||
return strings.Replace(template, "{input}", s, 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,11 @@ func TestParseArgsIpCam(t *testing.T) {
|
||||
source: "rtmp://example.com#input=rtsp/udp",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] custom timeout",
|
||||
source: "rtsp://example.com#timeout=10",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -43,6 +44,11 @@ func streamOnvif(rawURL string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Append hash-based arguments to the retrieved URI
|
||||
if i := strings.IndexByte(rawURL, '#'); i > 0 {
|
||||
uri += rawURL[i:]
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[onvif] new uri=%s", uri)
|
||||
|
||||
if err = streams.Validate(uri); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -33,7 +34,13 @@ func Init() {
|
||||
|
||||
log = app.GetLogger("webrtc")
|
||||
|
||||
filters = cfg.Mod.Filters
|
||||
if log.Debug().Enabled() {
|
||||
itfs, _ := net.Interfaces()
|
||||
for _, itf := range itfs {
|
||||
addrs, _ := itf.Addrs()
|
||||
log.Debug().Msgf("[webrtc] interface %+v addrs %v", itf, addrs)
|
||||
}
|
||||
}
|
||||
|
||||
address, network, _ := strings.Cut(cfg.Mod.Listen, "/")
|
||||
for _, candidate := range cfg.Mod.Candidates {
|
||||
@@ -50,10 +57,19 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
webrtc.OnNewListener = func(ln any) {
|
||||
switch ln := ln.(type) {
|
||||
case *net.TCPListener:
|
||||
log.Info().Stringer("addr", ln.Addr()).Msg("[webrtc] listen tcp")
|
||||
case *net.UDPConn:
|
||||
log.Info().Stringer("addr", ln.LocalAddr()).Msg("[webrtc] listen udp")
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// create pionAPI with custom codecs list and custom network settings
|
||||
serverAPI, err = webrtc.NewServerAPI(network, address, &filters)
|
||||
serverAPI, err = webrtc.NewServerAPI(network, address, &cfg.Mod.Filters)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
@@ -63,7 +79,6 @@ func Init() {
|
||||
clientAPI = serverAPI
|
||||
|
||||
if address != "" {
|
||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[webrtc] listen")
|
||||
clientAPI, _ = webrtc.NewAPI()
|
||||
}
|
||||
|
||||
|
||||
+123
-38
@@ -1,7 +1,9 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/xnet"
|
||||
@@ -27,6 +29,69 @@ type Filters struct {
|
||||
UDPPorts []uint16 `yaml:"udp_ports"`
|
||||
}
|
||||
|
||||
func (f *Filters) Network(protocol string) string {
|
||||
if f == nil || f.Networks == nil {
|
||||
return protocol
|
||||
}
|
||||
v4 := slices.Contains(f.Networks, protocol+"4")
|
||||
v6 := slices.Contains(f.Networks, protocol+"6")
|
||||
if v4 && v6 {
|
||||
return protocol
|
||||
} else if v4 {
|
||||
return protocol + "4"
|
||||
} else if v6 {
|
||||
return protocol + "6"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *Filters) NetIPs() (ips []net.IP) {
|
||||
itfs, _ := net.Interfaces()
|
||||
for _, itf := range itfs {
|
||||
if itf.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
if !f.IncludeLoopback() && itf.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if !f.InterfaceFilter(itf.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, _ := itf.Addrs()
|
||||
for _, addr := range addrs {
|
||||
ip := parseNetAddr(addr)
|
||||
if ip == nil || !f.IPFilter(ip) {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseNetAddr(addr net.Addr) net.IP {
|
||||
switch addr := addr.(type) {
|
||||
case *net.IPNet:
|
||||
return addr.IP
|
||||
case *net.IPAddr:
|
||||
return addr.IP
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Filters) IncludeLoopback() bool {
|
||||
return f != nil && f.Loopback
|
||||
}
|
||||
|
||||
func (f *Filters) InterfaceFilter(name string) bool {
|
||||
return f == nil || f.Interfaces == nil || slices.Contains(f.Interfaces, name)
|
||||
}
|
||||
|
||||
func (f *Filters) IPFilter(ip net.IP) bool {
|
||||
return f == nil || f.IPs == nil || core.Contains(f.IPs, ip.String())
|
||||
}
|
||||
|
||||
func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error) {
|
||||
// for debug logs add to env: `PION_LOG_DEBUG=all`
|
||||
m := &webrtc.MediaEngine{}
|
||||
@@ -99,48 +164,17 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error
|
||||
_ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1])
|
||||
}
|
||||
|
||||
//if len(hosts) != 0 {
|
||||
// // support only: host, srflx
|
||||
// if candidateType, err := webrtc.NewICECandidateType(hosts[0]); err == nil {
|
||||
// s.SetNAT1To1IPs(hosts[1:], candidateType)
|
||||
// } else {
|
||||
// s.SetNAT1To1IPs(hosts, 0) // 0 = host
|
||||
// }
|
||||
//}
|
||||
|
||||
// If you don't specify an address, this won't cause an error.
|
||||
// Connections can still be established using random UDP addresses.
|
||||
if address != "" {
|
||||
// Both newMux functions respect filters and do not raise an error
|
||||
// if the port cannot be listened on.
|
||||
if network == "" || network == "tcp" {
|
||||
if ln, err := net.Listen("tcp", address); err == nil {
|
||||
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
}
|
||||
tcpMux := newTCPMux(address, filters)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
}
|
||||
|
||||
if network == "" || network == "udp" {
|
||||
// UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead
|
||||
var udpMux ice.UDPMux
|
||||
if port := xnet.ParseUnspecifiedPort(address); port != 0 {
|
||||
var networks []ice.NetworkType
|
||||
for _, ntype := range networkTypes {
|
||||
networks = append(networks, ice.NetworkType(ntype))
|
||||
}
|
||||
|
||||
var err error
|
||||
if udpMux, err = ice.NewMultiUDPMuxFromPort(
|
||||
port,
|
||||
ice.UDPMuxFromPortWithInterfaceFilter(interfaceFilter),
|
||||
ice.UDPMuxFromPortWithIPFilter(ipFilter),
|
||||
ice.UDPMuxFromPortWithNetworks(networks...),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
ln, err := net.ListenPacket("udp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln})
|
||||
}
|
||||
udpMux := newUDPMux(address, filters)
|
||||
s.SetICEUDPMux(udpMux)
|
||||
}
|
||||
}
|
||||
@@ -152,6 +186,57 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error
|
||||
), nil
|
||||
}
|
||||
|
||||
// OnNewListener temporary ugly solution for log
|
||||
var OnNewListener = func(ln any) {}
|
||||
|
||||
func newTCPMux(address string, filters *Filters) ice.TCPMux {
|
||||
networkTCP := filters.Network("tcp") // tcp or tcp4 or tcp6
|
||||
if ln, _ := net.Listen(networkTCP, address); ln != nil {
|
||||
OnNewListener(ln)
|
||||
return webrtc.NewICETCPMux(nil, ln, 8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newUDPMux(address string, filters *Filters) ice.UDPMux {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UDPMux should not listening on unspecified address.
|
||||
// So we will create a listener on all available interfaces.
|
||||
// We can't use ice.NewMultiUDPMuxFromPort, because it sometimes crashes with an error:
|
||||
// listen udp [***]:8555: bind: cannot assign requested address
|
||||
var addrs []string
|
||||
if host == "" {
|
||||
for _, ip := range filters.NetIPs() {
|
||||
addrs = append(addrs, fmt.Sprintf("%s:%s", ip, port))
|
||||
}
|
||||
} else {
|
||||
addrs = []string{address}
|
||||
}
|
||||
|
||||
networkUDP := filters.Network("udp") // udp or udp4 or udp6
|
||||
|
||||
var muxes []ice.UDPMux
|
||||
for _, addr := range addrs {
|
||||
if ln, _ := net.ListenPacket(networkUDP, addr); ln != nil {
|
||||
OnNewListener(ln)
|
||||
mux := ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln})
|
||||
muxes = append(muxes, mux)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(muxes) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return muxes[0]
|
||||
}
|
||||
return ice.NewMultiUDPMuxDefault(muxes...)
|
||||
}
|
||||
|
||||
func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
|
||||
+201
-84
@@ -1,6 +1,7 @@
|
||||
package cs2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -10,33 +11,27 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func Dial(host string) (*Conn, error) {
|
||||
conn, err := net.ListenUDP("udp", nil)
|
||||
func Dial(host, transport string) (*Conn, error) {
|
||||
conn, err := handshake(host, transport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, isTCP := conn.(*tcpConn)
|
||||
|
||||
c := &Conn{
|
||||
conn: conn,
|
||||
addr: &net.UDPAddr{IP: net.ParseIP(host), Port: 32108},
|
||||
conn: conn,
|
||||
isTCP: isTCP,
|
||||
rawCh0: make(chan []byte, 10),
|
||||
rawCh2: make(chan []byte, 100),
|
||||
}
|
||||
|
||||
if err = c.handshake(); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.rawCh0 = make(chan []byte, 10)
|
||||
c.rawCh2 = make(chan []byte, 100)
|
||||
|
||||
go c.worker()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
conn net.Conn
|
||||
isTCP bool
|
||||
|
||||
err error
|
||||
seqCh0 uint16
|
||||
@@ -53,30 +48,58 @@ const (
|
||||
magicDrw = 0xD1
|
||||
msgLanSearch = 0x30
|
||||
msgPunchPkt = 0x41
|
||||
msgP2PRdy = 0x42
|
||||
msgP2PRdyUDP = 0x42
|
||||
msgP2PRdyTCP = 0x43
|
||||
msgDrw = 0xD0
|
||||
msgDrwAck = 0xD1
|
||||
msgAlive = 0xE0
|
||||
msgPing = 0xE0
|
||||
msgPong = 0xE1
|
||||
msgClose = 0xF1
|
||||
)
|
||||
|
||||
func (c *Conn) handshake() error {
|
||||
_ = c.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
buf, err := c.WriteAndWait([]byte{magic, msgLanSearch, 0, 0}, msgPunchPkt)
|
||||
func handshake(host, transport string) (net.Conn, error) {
|
||||
conn, err := newUDPConn(host, 32108)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: read punch: %w", "cs2", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = c.WriteAndWait(buf, msgP2PRdy)
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
req := []byte{magic, msgLanSearch, 0, 0}
|
||||
res, err := conn.(*udpConn).WriteUntil(req, func(res []byte) bool {
|
||||
return res[1] == msgPunchPkt
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: read ready: %w", "cs2", err)
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = c.Write([]byte{magic, msgAlive, 0, 0})
|
||||
var msgUDP, msgTCP byte
|
||||
|
||||
_ = c.SetDeadline(time.Time{})
|
||||
if transport == "" || transport == "udp" {
|
||||
msgUDP = msgP2PRdyUDP
|
||||
}
|
||||
if transport == "" || transport == "tcp" {
|
||||
msgTCP = msgP2PRdyTCP
|
||||
}
|
||||
|
||||
return nil
|
||||
res, err = conn.(*udpConn).WriteUntil(res, func(res []byte) bool {
|
||||
return res[1] == msgUDP || res[1] == msgTCP
|
||||
})
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
|
||||
if res[1] == msgTCP {
|
||||
_ = conn.Close()
|
||||
//host := fmt.Sprintf("%d.%d.%d.%d:%d", b[31], b[30], b[29], b[28], uint16(b[27])<<8|uint16(b[26]))
|
||||
return newTCPConn(conn.RemoteAddr().String())
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Conn) worker() {
|
||||
@@ -85,38 +108,41 @@ func (c *Conn) worker() {
|
||||
close(c.rawCh2)
|
||||
}()
|
||||
|
||||
chAck := make([]uint16, 4)
|
||||
chAck := make([]uint16, 4) // only for UDP
|
||||
buf := make([]byte, 1200)
|
||||
var ch2WaitSize int
|
||||
var ch2WaitData []byte
|
||||
var keepaliveTS time.Time
|
||||
|
||||
for {
|
||||
n, addr, err := c.conn.ReadFromUDP(buf)
|
||||
n, err := c.conn.Read(buf)
|
||||
if err != nil {
|
||||
c.err = fmt.Errorf("%s: %w", "cs2", err)
|
||||
return
|
||||
}
|
||||
|
||||
if string(addr.IP) != string(c.addr.IP) || n < 8 || buf[0] != magic {
|
||||
continue // skip messages from another IP
|
||||
}
|
||||
|
||||
//log.Printf("<- %x", buf[:n])
|
||||
|
||||
switch buf[1] {
|
||||
case msgDrw:
|
||||
ch := buf[5]
|
||||
seqHI := buf[6]
|
||||
seqLO := buf[7]
|
||||
|
||||
if chAck[ch] != uint16(seqHI)<<8|uint16(seqLO) {
|
||||
continue
|
||||
}
|
||||
chAck[ch]++
|
||||
if c.isTCP {
|
||||
// For TCP we should using ping/pong.
|
||||
if now := time.Now(); now.After(keepaliveTS) {
|
||||
_, _ = c.conn.Write([]byte{magic, msgPing, 0, 0})
|
||||
keepaliveTS = now.Add(5 * time.Second)
|
||||
}
|
||||
} else {
|
||||
// For UDP we should using ack.
|
||||
seqHI := buf[6]
|
||||
seqLO := buf[7]
|
||||
|
||||
ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO}
|
||||
if _, err = c.conn.WriteToUDP(ack, c.addr); err != nil {
|
||||
return
|
||||
if chAck[ch] != uint16(seqHI)<<8|uint16(seqLO) {
|
||||
continue
|
||||
}
|
||||
chAck[ch]++
|
||||
|
||||
ack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO}
|
||||
_, _ = c.conn.Write(ack)
|
||||
}
|
||||
|
||||
switch ch {
|
||||
@@ -152,9 +178,12 @@ func (c *Conn) worker() {
|
||||
continue
|
||||
}
|
||||
|
||||
case msgP2PRdy: // skip it
|
||||
case msgPing:
|
||||
_, _ = c.conn.Write([]byte{magic, msgPong, 0, 0})
|
||||
continue
|
||||
case msgDrwAck:
|
||||
case msgPong, msgP2PRdyUDP, msgP2PRdyTCP, msgClose:
|
||||
continue // skip it
|
||||
case msgDrwAck: // only for UDP
|
||||
if c.cmdAck != nil {
|
||||
c.cmdAck()
|
||||
}
|
||||
@@ -165,42 +194,15 @@ func (c *Conn) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Write(req []byte) error {
|
||||
//log.Printf("-> %x", req)
|
||||
_, err := c.conn.WriteToUDP(req, c.addr)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) WriteAndWait(req []byte, waitMsg uint8) ([]byte, error) {
|
||||
var t *time.Timer
|
||||
t = time.AfterFunc(1, func() {
|
||||
if err := c.Write(req); err == nil && t != nil {
|
||||
t.Reset(time.Second)
|
||||
}
|
||||
})
|
||||
defer t.Stop()
|
||||
|
||||
buf := make([]byte, 1200)
|
||||
|
||||
for {
|
||||
n, addr, err := c.conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(addr.IP) != string(c.addr.IP) || n < 16 {
|
||||
continue // skip messages from another IP
|
||||
}
|
||||
|
||||
if buf[0] == magic && buf[1] == waitMsg {
|
||||
c.addr.Port = addr.Port
|
||||
return buf[:n], nil
|
||||
}
|
||||
func (c *Conn) Protocol() string {
|
||||
if c.isTCP {
|
||||
return "cs2+tcp"
|
||||
}
|
||||
return "cs2+udp"
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.addr
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *Conn) SetDeadline(t time.Time) error {
|
||||
@@ -232,6 +234,14 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error {
|
||||
c.cmdMu.Lock()
|
||||
defer c.cmdMu.Unlock()
|
||||
|
||||
req := marshalCmd(0, c.seqCh0, uint32(cmd), data)
|
||||
c.seqCh0++
|
||||
|
||||
if c.isTCP {
|
||||
_, err := c.conn.Write(req)
|
||||
return err
|
||||
}
|
||||
|
||||
var repeat atomic.Int32
|
||||
repeat.Store(5)
|
||||
|
||||
@@ -243,11 +253,8 @@ func (c *Conn) WriteCommand(cmd uint16, data []byte) error {
|
||||
timeout.Reset(1)
|
||||
}
|
||||
|
||||
req := marshalCmd(0, c.seqCh0, uint32(cmd), data)
|
||||
c.seqCh0++
|
||||
|
||||
for {
|
||||
if err := c.Write(req); err != nil {
|
||||
if _, err := c.conn.Write(req); err != nil {
|
||||
return err
|
||||
}
|
||||
<-timeout.C
|
||||
@@ -285,7 +292,8 @@ func (c *Conn) WritePacket(data []byte) error {
|
||||
binary.BigEndian.PutUint32(req[8:], n)
|
||||
copy(req[offset:], data)
|
||||
|
||||
return c.Write(req)
|
||||
_, err := c.conn.Write(req)
|
||||
return err
|
||||
}
|
||||
|
||||
func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte {
|
||||
@@ -313,3 +321,112 @@ func marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte {
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func newUDPConn(host string, port int) (net.Conn, error) {
|
||||
// We using raw net.UDPConn, because RemoteAddr should be changed during handshake.
|
||||
conn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", host)
|
||||
if err != nil {
|
||||
addr = &net.UDPAddr{IP: net.ParseIP(host), Port: port}
|
||||
}
|
||||
|
||||
return &udpConn{UDPConn: conn, addr: addr}, nil
|
||||
}
|
||||
|
||||
type udpConn struct {
|
||||
*net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
}
|
||||
|
||||
func (c *udpConn) Read(p []byte) (n int, err error) {
|
||||
var addr *net.UDPAddr
|
||||
for {
|
||||
n, addr, err = c.UDPConn.ReadFromUDP(p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if string(addr.IP) == string(c.addr.IP) || n >= 8 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpConn) Write(req []byte) (n int, err error) {
|
||||
//log.Printf("-> %x", req)
|
||||
return c.UDPConn.WriteToUDP(req, c.addr)
|
||||
}
|
||||
|
||||
func (c *udpConn) RemoteAddr() net.Addr {
|
||||
return c.addr
|
||||
}
|
||||
|
||||
func (c *udpConn) WriteUntil(req []byte, ok func(res []byte) bool) ([]byte, error) {
|
||||
var t *time.Timer
|
||||
t = time.AfterFunc(1, func() {
|
||||
if _, err := c.Write(req); err == nil && t != nil {
|
||||
t.Reset(time.Second)
|
||||
}
|
||||
})
|
||||
defer t.Stop()
|
||||
|
||||
buf := make([]byte, 1200)
|
||||
|
||||
for {
|
||||
n, addr, err := c.UDPConn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if string(addr.IP) != string(c.addr.IP) || n < 16 {
|
||||
continue // skip messages from another IP
|
||||
}
|
||||
|
||||
if ok(buf[:n]) {
|
||||
c.addr.Port = addr.Port
|
||||
return buf[:n], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTCPConn(addr string) (net.Conn, error) {
|
||||
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tcpConn{conn.(*net.TCPConn), bufio.NewReader(conn)}, nil
|
||||
}
|
||||
|
||||
type tcpConn struct {
|
||||
*net.TCPConn
|
||||
rd *bufio.Reader
|
||||
}
|
||||
|
||||
func (c *tcpConn) Read(p []byte) (n int, err error) {
|
||||
tmp := make([]byte, 8)
|
||||
if _, err = io.ReadFull(c.rd, tmp); err != nil {
|
||||
return
|
||||
}
|
||||
n = int(binary.BigEndian.Uint16(tmp))
|
||||
if len(p) < n {
|
||||
return 0, fmt.Errorf("tcp: buffer too small")
|
||||
}
|
||||
_, err = io.ReadFull(c.rd, p[:n])
|
||||
//log.Printf("<- %x%x", tmp, p[:n])
|
||||
return
|
||||
}
|
||||
|
||||
func (c *tcpConn) Write(req []byte) (n int, err error) {
|
||||
n = len(req)
|
||||
buf := make([]byte, 8+n)
|
||||
binary.BigEndian.PutUint16(buf, uint16(n))
|
||||
buf[2] = 0x68
|
||||
copy(buf[8:], req)
|
||||
//log.Printf("-> %x", buf)
|
||||
_, err = c.TCPConn.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
+24
-11
@@ -33,7 +33,7 @@ func Dial(rawURL string) (*Client, error) {
|
||||
|
||||
switch s := query.Get("vendor"); s {
|
||||
case "cs2":
|
||||
c.conn, err = cs2.Dial(u.Host)
|
||||
c.conn, err = cs2.Dial(u.Host, query.Get("transport"))
|
||||
case "tutk":
|
||||
c.conn, err = tutk.Dial(u.Host, query.Get("uid"))
|
||||
default:
|
||||
@@ -63,6 +63,7 @@ const (
|
||||
)
|
||||
|
||||
type Conn interface {
|
||||
Protocol() string
|
||||
ReadCommand() (cmd uint16, data []byte, err error)
|
||||
WriteCommand(cmd uint16, data []byte) error
|
||||
ReadPacket() ([]byte, error)
|
||||
@@ -77,6 +78,10 @@ type Client struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func (c *Client) Protocol() string {
|
||||
return c.conn.Protocol()
|
||||
}
|
||||
|
||||
func (c *Client) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
@@ -89,16 +94,6 @@ func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Protocol() string {
|
||||
switch c.conn.(type) {
|
||||
case *cs2.Conn:
|
||||
return "cs2+udp"
|
||||
case *tutk.Conn:
|
||||
return "tutk+udp"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const (
|
||||
cmdAuthReq = 0x100
|
||||
cmdAuthRes = 0x101
|
||||
@@ -276,6 +271,24 @@ type Packet struct {
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func (p *Packet) SampleRate() uint32 {
|
||||
// flag: 1 0011 000 - sample rate 16000
|
||||
// flag: 100 00 01 0000 000 - sample rate 8000
|
||||
v := (p.Flags >> 3) & 0b1111
|
||||
if v != 0 {
|
||||
return 16000
|
||||
}
|
||||
return 8000
|
||||
}
|
||||
|
||||
//func (p *Packet) AudioUnknown1() byte {
|
||||
// return byte((p.Flags >> 7) & 0b11)
|
||||
//}
|
||||
//
|
||||
//func (p *Packet) AudioUnknown2() byte {
|
||||
// return byte((p.Flags >> 9) & 0b11)
|
||||
//}
|
||||
|
||||
func GenerateKey() ([]byte, []byte, error) {
|
||||
public, private, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
|
||||
@@ -110,7 +110,7 @@ func probe(client *miss.Client, channel, quality, audio uint8) ([]*core.Media, e
|
||||
}
|
||||
case miss.CodecPCMA:
|
||||
if acodec == nil {
|
||||
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: 8000}
|
||||
acodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate()}
|
||||
}
|
||||
case miss.CodecOPUS:
|
||||
if acodec == nil {
|
||||
|
||||
@@ -249,6 +249,10 @@ func (c *Conn) WriteAndWait(req []byte, ok func(req, res []byte) bool) ([]byte,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Protocol() string {
|
||||
return "tutk+udp"
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.addr
|
||||
}
|
||||
|
||||
+16
-177
@@ -1,189 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>go2rtc - WebTorrent</title>
|
||||
<meta charset="UTF-8">
|
||||
<title>go2rtc</title>
|
||||
<meta http-equiv="refresh" content="2; URL='https://github.com/AlexxIT/go2rtc'"/>
|
||||
<style>
|
||||
body {
|
||||
background-color: black;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body, video {
|
||||
body, html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video" autoplay controls playsinline muted></video>
|
||||
<div id="login">
|
||||
<input id="share" type="text" placeholder="share">
|
||||
<input id="pwd" type="text" placeholder="password">
|
||||
<button id="connect">connect</button>
|
||||
</div>
|
||||
<script>
|
||||
async function PeerConnection(media) {
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
|
||||
})
|
||||
|
||||
const localTracks = []
|
||||
|
||||
if (/camera|microphone/.test(media)) {
|
||||
const tracks = await getMediaTracks('user', {
|
||||
video: media.indexOf('camera') >= 0,
|
||||
audio: media.indexOf('microphone') >= 0,
|
||||
})
|
||||
tracks.forEach(track => {
|
||||
pc.addTransceiver(track, {direction: 'sendonly'})
|
||||
if (track.kind === 'video') localTracks.push(track)
|
||||
})
|
||||
}
|
||||
|
||||
if (media.indexOf('display') >= 0) {
|
||||
const tracks = await getMediaTracks('display', {
|
||||
video: true,
|
||||
audio: media.indexOf('speaker') >= 0,
|
||||
})
|
||||
tracks.forEach(track => {
|
||||
pc.addTransceiver(track, {direction: 'sendonly'})
|
||||
if (track.kind === 'video') localTracks.push(track)
|
||||
})
|
||||
}
|
||||
|
||||
if (/video|audio/.test(media)) {
|
||||
const tracks = ['video', 'audio']
|
||||
.filter(kind => media.indexOf(kind) >= 0)
|
||||
.map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track)
|
||||
localTracks.push(...tracks)
|
||||
}
|
||||
|
||||
document.getElementById('video').srcObject = new MediaStream(localTracks)
|
||||
|
||||
return pc
|
||||
}
|
||||
|
||||
async function getMediaTracks(media, constraints) {
|
||||
try {
|
||||
const stream = media === 'user'
|
||||
? await navigator.mediaDevices.getUserMedia(constraints)
|
||||
: await navigator.mediaDevices.getDisplayMedia(constraints)
|
||||
return stream.getTracks()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function getOffer(pc, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pc.addEventListener('icegatheringstatechange', () => {
|
||||
if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp)
|
||||
})
|
||||
|
||||
pc.createOffer().then(offer => pc.setLocalDescription(offer))
|
||||
|
||||
setTimeout(() => resolve(pc.localDescription.sdp), timeout || 5000)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function decode(buffer) {
|
||||
return String.fromCharCode(...new Uint8Array(buffer))
|
||||
}
|
||||
|
||||
function encode(string) {
|
||||
return Uint8Array.from(string, c => c.charCodeAt(0))
|
||||
}
|
||||
|
||||
async function cipher(share, pwd) {
|
||||
const hash = await crypto.subtle.digest('SHA-256', encode(share))
|
||||
const nonce = (Date.now() * 1000000).toString(36)
|
||||
|
||||
const ivData = await crypto.subtle.digest('SHA-256', encode(share + ':' + nonce))
|
||||
const keyData = await crypto.subtle.digest('SHA-256', encode(nonce + ':' + pwd))
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', keyData, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt'],
|
||||
)
|
||||
|
||||
return {
|
||||
hash: btoa(decode(hash)),
|
||||
nonce: nonce,
|
||||
encrypt: async function (plaintext) {
|
||||
const cryptotext = await crypto.subtle.encrypt(
|
||||
{name: 'AES-GCM', iv: ivData.slice(0, 12), additionalData: encode(nonce)},
|
||||
key, encode(plaintext),
|
||||
)
|
||||
return btoa(decode(cryptotext))
|
||||
},
|
||||
decrypt: async function (cryptotext) {
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{name: 'AES-GCM', iv: ivData.slice(0, 12), additionalData: encode(nonce)},
|
||||
key, encode(atob(cryptotext)),
|
||||
)
|
||||
return decode(plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
async function connect(share, pwd, media, tracker) {
|
||||
const crypto = await cipher(share, pwd)
|
||||
const pc = await PeerConnection(media || 'video+audio')
|
||||
const offer = await crypto.encrypt(await getOffer(pc))
|
||||
|
||||
const ws = new WebSocket(tracker || 'wss://tracker.openwebtorrent.com/')
|
||||
ws.addEventListener('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
action: 'announce',
|
||||
info_hash: crypto.hash,
|
||||
peer_id: Math.random().toString(36).substring(2),
|
||||
offers: [{
|
||||
offer_id: crypto.nonce,
|
||||
offer: {type: 'offer', sdp: offer},
|
||||
}],
|
||||
numwant: 1,
|
||||
}))
|
||||
})
|
||||
|
||||
ws.addEventListener('message', async (ev) => {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if (!msg.answer) return
|
||||
|
||||
const answer = await crypto.decrypt(msg.answer.sdp)
|
||||
await pc.setRemoteDescription({type: 'answer', sdp: answer})
|
||||
|
||||
ws.close()
|
||||
})
|
||||
}
|
||||
|
||||
document.getElementById('connect').addEventListener('click', () => {
|
||||
const share = document.getElementById('share').value
|
||||
const pwd = document.getElementById('pwd').value
|
||||
connect(share, pwd)
|
||||
document.getElementById('login').style.display = 'none'
|
||||
})
|
||||
|
||||
if (location.hash) {
|
||||
const params = new URLSearchParams(location.hash.substring(1))
|
||||
const share = params.get('share')
|
||||
const pwd = params.get('pwd')
|
||||
const media = params.get('media')
|
||||
const tracker = params.get('tr')
|
||||
connect(share, pwd, media, tracker)
|
||||
document.getElementById('login').style.display = 'none'
|
||||
}
|
||||
</script>
|
||||
<img src="https://raw.githubusercontent.com/AlexxIT/go2rtc/master/assets/logo.gif" alt="go2rtc">
|
||||
<a href="https://github.com/AlexxIT/go2rtc">github.com/AlexxIT/go2rtc</a>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+326
-63
@@ -24,7 +24,26 @@
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
"error",
|
||||
"fatal",
|
||||
"panic",
|
||||
"disabled"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"rtsp://username:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif",
|
||||
"rtsp://username:password@192.168.1.123/stream1",
|
||||
"rtsp://username:password@192.168.1.123/h264Preview_01_main",
|
||||
"rtmp://192.168.1.123/bcs/channel0_main.bcs?channel=0&stream=0&user=username&password=password",
|
||||
"http://192.168.1.123/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password",
|
||||
"http://username:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1",
|
||||
"ffmpeg:media.mp4#video=h264#hardware#width=1920#height=1080#rotate=180#audio=copy",
|
||||
"ffmpeg:virtual?video=testsrc&size=4K#video=h264#hardware#bitrate=50M",
|
||||
"exec:ffmpeg -re -i media.mp4 -c copy -rtsp_transport tcp -f rtsp {output}",
|
||||
"onvif://username:password@192.168.1.123:80?subtype=0",
|
||||
"tapo://password@192.168.1.123:8800?channel=0&subtype=0"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -33,13 +52,14 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listen": {
|
||||
"type": "string",
|
||||
"default": ":1984",
|
||||
"examples": [
|
||||
"127.0.0.1:8080"
|
||||
],
|
||||
"$ref": "#/definitions/listen"
|
||||
"127.0.0.1:1984"
|
||||
]
|
||||
},
|
||||
"username": {
|
||||
"description": "Basic auth for WebUI",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"admin"
|
||||
@@ -48,24 +68,35 @@
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"local_auth": {
|
||||
"description": "Enable auth check for localhost requests",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"base_path": {
|
||||
"description": "API prefix for serving on suburl (/api => /rtc/api)",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"/go2rtc"
|
||||
"/rtc"
|
||||
]
|
||||
},
|
||||
"static_dir": {
|
||||
"description": "Folder for static files (custom web interface)",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"/var/www"
|
||||
"www"
|
||||
]
|
||||
},
|
||||
"origin": {
|
||||
"description": "Allow CORS requests (only * supported)",
|
||||
"type": "string",
|
||||
"const": "*"
|
||||
"enum": [
|
||||
"*",
|
||||
""
|
||||
]
|
||||
},
|
||||
"tls_listen": {
|
||||
"$ref": "#/definitions/listen"
|
||||
"type": "string"
|
||||
},
|
||||
"tls_cert": {
|
||||
"type": "string",
|
||||
@@ -86,6 +117,111 @@
|
||||
"examples": [
|
||||
"/tmp/go2rtc.sock"
|
||||
]
|
||||
},
|
||||
"allow_paths": {
|
||||
"description": "Allow only these HTTP paths (full paths, including base_path)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"/api",
|
||||
"/api/streams",
|
||||
"/api/webrtc"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"modules": {
|
||||
"description": "Enable only these modules (empty / omitted means all)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"api",
|
||||
"ws",
|
||||
"http",
|
||||
"rtsp",
|
||||
"webrtc",
|
||||
"mp4",
|
||||
"hls",
|
||||
"mjpeg",
|
||||
"hass",
|
||||
"homekit",
|
||||
"onvif",
|
||||
"rtmp",
|
||||
"webtorrent",
|
||||
"wyoming",
|
||||
"echo",
|
||||
"exec",
|
||||
"expr",
|
||||
"ffmpeg",
|
||||
"alsa",
|
||||
"v4l2",
|
||||
"bubble",
|
||||
"doorbird",
|
||||
"dvrip",
|
||||
"eseecloud",
|
||||
"flussonic",
|
||||
"gopro",
|
||||
"isapi",
|
||||
"ivideon",
|
||||
"mpegts",
|
||||
"nest",
|
||||
"ring",
|
||||
"roborock",
|
||||
"tapo",
|
||||
"tuya",
|
||||
"xiaomi",
|
||||
"yandex",
|
||||
"debug",
|
||||
"ngrok",
|
||||
"pinggy",
|
||||
"srtp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"description": "Config variables that can be referenced as ${NAME} / ${NAME:default}",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"echo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_paths": {
|
||||
"description": "Allow only these binaries for echo: URLs (exact cmd name/path)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_paths": {
|
||||
"description": "Allow only these binaries for exec: URLs (exact cmd name/path)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"ffmpeg",
|
||||
"/usr/bin/ffmpeg"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -95,6 +231,26 @@
|
||||
"bin": {
|
||||
"type": "string",
|
||||
"default": "ffmpeg"
|
||||
},
|
||||
"global": {
|
||||
"type": "string",
|
||||
"default": "-hide_banner"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"default": "-re -i {input}"
|
||||
},
|
||||
"http": {
|
||||
"type": "string",
|
||||
"default": "-fflags nobuffer -flags low_delay -i {input}"
|
||||
},
|
||||
"rtsp": {
|
||||
"type": "string",
|
||||
"default": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}"
|
||||
},
|
||||
"rtsp/udp": {
|
||||
"type": "string",
|
||||
"default": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {
|
||||
@@ -117,12 +273,25 @@
|
||||
"homekit": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"properties": {
|
||||
"pin": {
|
||||
"description": "HomeKit pairing PIN",
|
||||
"type": "string",
|
||||
"default": "19550224",
|
||||
"pattern": "^[0-9]{8}$"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{8}$"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{3}-[0-9]{2}-[0-9]{3}$"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
@@ -133,6 +302,29 @@
|
||||
"device_private": {
|
||||
"type": "string"
|
||||
},
|
||||
"category_id": {
|
||||
"description": "Accessory category: `bridge`, `doorbell` or numeric ID",
|
||||
"type": "string",
|
||||
"default": "camera",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"bridge",
|
||||
"camera",
|
||||
"doorbell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"pairings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -146,9 +338,11 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"description": "Log format: color/json/text or empty for autodetect",
|
||||
"type": "string",
|
||||
"default": "color",
|
||||
"enum": [
|
||||
"",
|
||||
"color",
|
||||
"json",
|
||||
"text"
|
||||
@@ -160,12 +354,26 @@
|
||||
"$ref": "#/definitions/log_level"
|
||||
},
|
||||
"output": {
|
||||
"description": "Log output: stdout/stderr/file[:path] or empty (memory only)",
|
||||
"type": "string",
|
||||
"default": "stdout",
|
||||
"enum": [
|
||||
"",
|
||||
"stdout",
|
||||
"stderr"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"",
|
||||
"stdout",
|
||||
"stderr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^file(:.+)?$",
|
||||
"examples": [
|
||||
"file",
|
||||
"file:go2rtc.log"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
@@ -215,6 +423,9 @@
|
||||
"homekit": {
|
||||
"$ref": "#/definitions/log_level"
|
||||
},
|
||||
"mjpeg": {
|
||||
"$ref": "#/definitions/log_level"
|
||||
},
|
||||
"mp4": {
|
||||
"$ref": "#/definitions/log_level"
|
||||
},
|
||||
@@ -238,6 +449,9 @@
|
||||
},
|
||||
"webtorrent": {
|
||||
"$ref": "#/definitions/log_level"
|
||||
},
|
||||
"wyoming": {
|
||||
"$ref": "#/definitions/log_level"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -253,6 +467,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pinggy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tunnel": {
|
||||
"description": "Expose local address via Pinggy",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"http://127.0.0.1:1984",
|
||||
"tcp://192.168.1.123:554"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"preload": {
|
||||
"description": "Preload streams on startup (map stream name => probe query, default `video&audio`)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"video&audio",
|
||||
"video"
|
||||
]
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -277,10 +515,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listen": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
":1935"
|
||||
],
|
||||
"$ref": "#/definitions/listen"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -288,8 +526,8 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listen": {
|
||||
"default": ":8554",
|
||||
"$ref": "#/definitions/listen"
|
||||
"type": "string",
|
||||
"default": ":8554"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
@@ -314,75 +552,56 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listen": {
|
||||
"default": ":8443",
|
||||
"$ref": "#/definitions/listen"
|
||||
"type": "string",
|
||||
"default": ":8443"
|
||||
}
|
||||
}
|
||||
},
|
||||
"streams": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"title": "Stream",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Source",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"rtsp://username:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif",
|
||||
"rtsp://username:password@192.168.1.123/stream1",
|
||||
"rtsp://username:password@192.168.1.123/h264Preview_01_main",
|
||||
"rtmp://192.168.1.123/bcs/channel0_main.bcs?channel=0&stream=0&user=username&password=password",
|
||||
"http://192.168.1.123/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password",
|
||||
"http://username:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1",
|
||||
"ffmpeg:media.mp4#video=h264#hardware#width=1920#height=1080#rotate=180#audio=copy",
|
||||
"ffmpeg:virtual?video=testsrc&size=4K#video=h264#hardware#bitrate=50M",
|
||||
"bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0",
|
||||
"dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0",
|
||||
"exec:ffmpeg -re -i media.mp4 -c copy -rtsp_transport tcp -f rtsp {output}",
|
||||
"isapi://username:password@192.168.1.123:80/",
|
||||
"kasa://username:password@192.168.1.123:19443/https/stream/mixed",
|
||||
"onvif://username:password@192.168.1.123:80?subtype=0",
|
||||
"tapo://password@192.168.1.123:8800?channel=0&subtype=0",
|
||||
"webtorrent:?share=xxx&pwd=xxx"
|
||||
]
|
||||
"$ref": "#/definitions/source"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"description": "Source",
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/source"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"xiaomi": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"webrtc": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listen": {
|
||||
"default": ":8555/tcp",
|
||||
"type": "string",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": ":[0-9]{1,5}(/tcp|/udp)?$"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": ""
|
||||
}
|
||||
"default": ":8555",
|
||||
"examples": [
|
||||
":8555/udp"
|
||||
]
|
||||
},
|
||||
"candidates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/listen/anyOf/0"
|
||||
},
|
||||
"examples": [
|
||||
"216.58.210.174:8555",
|
||||
"stun:8555",
|
||||
"home.duckdns.org:8555"
|
||||
]
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"216.58.210.174:8555",
|
||||
"stun:8555",
|
||||
"home.duckdns.org:8555"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ice_servers": {
|
||||
"type": "array",
|
||||
@@ -436,13 +655,13 @@
|
||||
"description": "Use only these network types",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"tcp4",
|
||||
"tcp6",
|
||||
"udp4",
|
||||
"udp6"
|
||||
],
|
||||
"type": "string"
|
||||
]
|
||||
}
|
||||
},
|
||||
"udp_ports": {
|
||||
@@ -472,7 +691,8 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pwd": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"minLength": 4
|
||||
},
|
||||
"src": {
|
||||
"type": "string"
|
||||
@@ -481,6 +701,49 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"wyoming": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listen": {
|
||||
"description": "Listen address for Wyoming server",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Optional satellite name (default: stream name)",
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"description": "Optional mode: mic / snd / default",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"",
|
||||
"mic",
|
||||
"snd"
|
||||
]
|
||||
},
|
||||
"event": {
|
||||
"description": "Event handlers (map event type => expr script)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"wake_uri": {
|
||||
"description": "Optional WAKE service URI (ex. tcp://host:port?name=...)",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"tcp://192.168.1.23:10400"
|
||||
]
|
||||
},
|
||||
"vad_threshold": {
|
||||
"description": "Optional VAD threshold (0.1..3.5 typical)",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>webtorrent - go2rtc</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: black;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body, video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video" autoplay controls playsinline muted></video>
|
||||
<div id="login">
|
||||
<input id="share" type="text" placeholder="share">
|
||||
<input id="pwd" type="text" placeholder="password">
|
||||
<button id="connect">connect</button>
|
||||
</div>
|
||||
<script>
|
||||
async function PeerConnection(media) {
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
|
||||
});
|
||||
|
||||
const localTracks = [];
|
||||
|
||||
if (/camera|microphone/.test(media)) {
|
||||
const tracks = await getMediaTracks('user', {
|
||||
video: media.indexOf('camera') >= 0,
|
||||
audio: media.indexOf('microphone') >= 0,
|
||||
});
|
||||
tracks.forEach(track => {
|
||||
pc.addTransceiver(track, {direction: 'sendonly'});
|
||||
if (track.kind === 'video') localTracks.push(track);
|
||||
});
|
||||
}
|
||||
|
||||
if (media.indexOf('display') >= 0) {
|
||||
const tracks = await getMediaTracks('display', {
|
||||
video: true,
|
||||
audio: media.indexOf('speaker') >= 0,
|
||||
});
|
||||
tracks.forEach(track => {
|
||||
pc.addTransceiver(track, {direction: 'sendonly'});
|
||||
if (track.kind === 'video') localTracks.push(track);
|
||||
});
|
||||
}
|
||||
|
||||
if (/video|audio/.test(media)) {
|
||||
const tracks = ['video', 'audio']
|
||||
.filter(kind => media.indexOf(kind) >= 0)
|
||||
.map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track);
|
||||
localTracks.push(...tracks);
|
||||
}
|
||||
|
||||
document.getElementById('video').srcObject = new MediaStream(localTracks);
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
async function getMediaTracks(media, constraints) {
|
||||
try {
|
||||
const stream = media === 'user'
|
||||
? await navigator.mediaDevices.getUserMedia(constraints)
|
||||
: await navigator.mediaDevices.getDisplayMedia(constraints);
|
||||
return stream.getTracks();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getOffer(pc, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pc.addEventListener('icegatheringstatechange', () => {
|
||||
if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp);
|
||||
});
|
||||
|
||||
pc.createOffer().then(offer => pc.setLocalDescription(offer));
|
||||
|
||||
setTimeout(() => resolve(pc.localDescription.sdp), timeout || 5000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function decode(buffer) {
|
||||
return String.fromCharCode(...new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
function encode(string) {
|
||||
return Uint8Array.from(string, c => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
async function cipher(share, pwd) {
|
||||
const hash = await crypto.subtle.digest('SHA-256', encode(share));
|
||||
const nonce = (Date.now() * 1000000).toString(36);
|
||||
|
||||
const ivData = await crypto.subtle.digest('SHA-256', encode(share + ':' + nonce));
|
||||
const keyData = await crypto.subtle.digest('SHA-256', encode(nonce + ':' + pwd));
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', keyData, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt'],
|
||||
);
|
||||
|
||||
return {
|
||||
hash: btoa(decode(hash)),
|
||||
nonce: nonce,
|
||||
encrypt: async function (plaintext) {
|
||||
const cryptotext = await crypto.subtle.encrypt(
|
||||
{name: 'AES-GCM', iv: ivData.slice(0, 12), additionalData: encode(nonce)},
|
||||
key, encode(plaintext),
|
||||
);
|
||||
return btoa(decode(cryptotext));
|
||||
},
|
||||
decrypt: async function (cryptotext) {
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{name: 'AES-GCM', iv: ivData.slice(0, 12), additionalData: encode(nonce)},
|
||||
key, encode(atob(cryptotext)),
|
||||
);
|
||||
return decode(plaintext);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
async function connect(share, pwd, media, tracker) {
|
||||
const crypto = await cipher(share, pwd);
|
||||
const pc = await PeerConnection(media || 'video+audio');
|
||||
const offer = await crypto.encrypt(await getOffer(pc));
|
||||
|
||||
const ws = new WebSocket(tracker || 'wss://tracker.openwebtorrent.com/');
|
||||
ws.addEventListener('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
action: 'announce',
|
||||
info_hash: crypto.hash,
|
||||
peer_id: Math.random().toString(36).substring(2),
|
||||
offers: [{
|
||||
offer_id: crypto.nonce,
|
||||
offer: {type: 'offer', sdp: offer},
|
||||
}],
|
||||
numwant: 1,
|
||||
}));
|
||||
});
|
||||
|
||||
ws.addEventListener('message', async (ev) => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (!msg.answer) return;
|
||||
|
||||
const answer = await crypto.decrypt(msg.answer.sdp);
|
||||
await pc.setRemoteDescription({type: 'answer', sdp: answer});
|
||||
|
||||
ws.close();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('connect').addEventListener('click', () => {
|
||||
const share = document.getElementById('share').value;
|
||||
const pwd = document.getElementById('pwd').value;
|
||||
connect(share, pwd);
|
||||
document.getElementById('login').style.display = 'none';
|
||||
});
|
||||
|
||||
if (location.hash) {
|
||||
const params = new URLSearchParams(location.hash.substring(1));
|
||||
const share = params.get('share');
|
||||
const pwd = params.get('pwd');
|
||||
const media = params.get('media');
|
||||
const tracker = params.get('tr');
|
||||
connect(share, pwd, media, tracker);
|
||||
document.getElementById('login').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+3
-3
@@ -81,11 +81,11 @@ https://webrtc.org/getting-started/unified-plan-transition-guide?hl=en
|
||||
|
||||
```html
|
||||
<!-- iOS Safari -->
|
||||
<link rel="apple-touch-icon" href="https://alexxit.github.io/go2rtc/icons/apple-touch-icon-180x180.png" sizes="180x180">
|
||||
<link rel="apple-touch-icon" href="https://go2rtc.org/icons/apple-touch-icon-180x180.png" sizes="180x180">
|
||||
<!-- Classic, desktop browsers -->
|
||||
<link rel="icon" href="https://alexxit.github.io/go2rtc/icons/favicon.ico">
|
||||
<link rel="icon" href="https://go2rtc.org/icons/favicon.ico">
|
||||
<!-- Android Chrome -->
|
||||
<link rel="manifest" href="https://alexxit.github.io/go2rtc/manifest.json">
|
||||
<link rel="manifest" href="https://go2rtc.org/manifest.json">
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
+1198
-35
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -186,7 +186,7 @@ Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx</pre>
|
||||
document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}`;
|
||||
|
||||
const share = document.getElementById('shareget');
|
||||
share.href = `https://alexxit.github.io/go2rtc/#${share.dataset.auth}&media=${media}`;
|
||||
share.href = `https://go2rtc.org/webtorrent/#${share.dataset.auth}&media=${media}`;
|
||||
}
|
||||
|
||||
function share(method) {
|
||||
|
||||
Reference in New Issue
Block a user