Merge branch 'AlexxIT:master' into wyze

This commit is contained in:
seydx
2026-01-01 05:25:58 +01:00
committed by GitHub
19 changed files with 2826 additions and 511 deletions
+7 -4
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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 {
+14 -7
View File
@@ -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)
}
+5
View File
@@ -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) {
+6
View File
@@ -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 {
+18 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
}
}
}
}
+189
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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) {