Merge branch 'master' into wyze
This commit is contained in:
@@ -47,6 +47,7 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
|
||||
EXPOSE 1984 8554 8555 8555/udp
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
|
||||
@@ -49,6 +49,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
|
||||
EXPOSE 1984 8554 8555 8555/udp
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
|
||||
@@ -43,6 +43,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
ADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /usr/local/bin
|
||||
|
||||
EXPOSE 1984 8554 8555 8555/udp
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Doorbird
|
||||
|
||||
*[added in v1.9.8](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.11)*
|
||||
|
||||
This source type supports [Doorbird](https://www.doorbird.com/) devices including MJPEG stream, audio stream as well as two-way audio.
|
||||
|
||||
It is recommended to create a sepearate user within your doorbird setup for go2rtc. Minimum permissions for the user are:
|
||||
|
||||
- Watch always
|
||||
- API operator
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
doorbird1:
|
||||
- rtsp://admin:password@192.168.1.123:8557/mpeg/720p/media.amp # RTSP stream
|
||||
- doorbird://admin:password@192.168.1.123?media=video # MJPEG stream
|
||||
- doorbird://admin:password@192.168.1.123?media=audio # audio stream
|
||||
- doorbird://admin:password@192.168.1.123 # two-way audio
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
# Multitrans
|
||||
|
||||
**added in v1.9.14** by [@forrestsocool](https://github.com/forrestsocool)
|
||||
|
||||
Two-way audio support for Chinese version of [TP-Link cameras](https://www.tp-link.com.cn/list_2549.html).
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
tplink_cam:
|
||||
# video use standard RTSP
|
||||
- rtsp://admin:admin@192.168.1.202:554/stream1
|
||||
# two-way audio use MULTITRANS schema
|
||||
- multitrans://admin:admin@192.168.1.202:554
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
package multitrans
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/multitrans"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("multitrans", multitrans.Dial)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/mp4"
|
||||
"github.com/AlexxIT/go2rtc/internal/mpegts"
|
||||
"github.com/AlexxIT/go2rtc/internal/multitrans"
|
||||
"github.com/AlexxIT/go2rtc/internal/nest"
|
||||
"github.com/AlexxIT/go2rtc/internal/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/internal/onvif"
|
||||
@@ -96,6 +97,7 @@ func main() {
|
||||
{"isapi", isapi.Init},
|
||||
{"ivideon", ivideon.Init},
|
||||
{"mpegts", mpegts.Init},
|
||||
{"multitrans", multitrans.Init},
|
||||
{"nest", nest.Init},
|
||||
{"ring", ring.Init},
|
||||
{"roborock", roborock.Init},
|
||||
|
||||
+1
-1
@@ -277,7 +277,7 @@ func ParseCodecString(s string) *Codec {
|
||||
codec.ClockRate = uint32(Atoi(ss[1]))
|
||||
}
|
||||
if len(ss) >= 3 {
|
||||
codec.Channels = uint8(Atoi(ss[1]))
|
||||
codec.Channels = uint8(Atoi(ss[2]))
|
||||
}
|
||||
|
||||
return &codec
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package multitrans
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
core.Connection
|
||||
conn net.Conn
|
||||
rd *bufio.Reader
|
||||
closed core.Waiter
|
||||
}
|
||||
|
||||
func Dial(rawURL string) (core.Producer, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Port() == "" {
|
||||
u.Host += ":554"
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", u.Host, core.ConnDialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
conn: conn,
|
||||
rd: bufio.NewReader(conn),
|
||||
}
|
||||
|
||||
if err = c.handshake(u); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Connection = core.Connection{
|
||||
ID: core.NewID(),
|
||||
FormatName: "multitrans",
|
||||
Protocol: "rtsp",
|
||||
RemoteAddr: conn.RemoteAddr().String(),
|
||||
Source: rawURL,
|
||||
Medias: []*core.Media{
|
||||
{
|
||||
Kind: core.KindAudio,
|
||||
Direction: core.DirectionSendonly,
|
||||
Codecs: []*core.Codec{{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8}},
|
||||
},
|
||||
},
|
||||
Transport: conn,
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
|
||||
sender := core.NewSender(media, track.Codec)
|
||||
sender.Handler = func(packet *rtp.Packet) {
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: packet.Marker,
|
||||
PayloadType: 8,
|
||||
SequenceNumber: packet.SequenceNumber,
|
||||
Timestamp: packet.Timestamp,
|
||||
SSRC: packet.SSRC,
|
||||
},
|
||||
Payload: packet.Payload,
|
||||
}
|
||||
|
||||
// Encapsulate in RTSP Interleaved Frame (Channel 1)
|
||||
// $ + Channel(1 byte) + Length(2 bytes) + Packet
|
||||
size := 12 + len(clone.Payload)
|
||||
b := make([]byte, 4+size)
|
||||
b[0] = '$'
|
||||
b[1] = 1 // Channel 1 for audio
|
||||
b[2] = byte(size >> 8)
|
||||
b[3] = byte(size)
|
||||
if _, err := clone.MarshalTo(b[4:]); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := c.conn.Write(b); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
sender.HandleRTP(track)
|
||||
c.Senders = append(c.Senders, sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) handshake(u *url.URL) error {
|
||||
// Step 1: Get Challenge
|
||||
uid := uuid.New().String()
|
||||
|
||||
uri := fmt.Sprintf("rtsp://%s/multitrans", u.Host)
|
||||
data := fmt.Sprintf("MULTITRANS %s RTSP/1.0\r\nCSeq: 0\r\nX-Client-UUID: %s\r\n\r\n", uri, uid)
|
||||
|
||||
if _, err := c.conn.Write([]byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tcp.ReadResponse(c.rd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusUnauthorized {
|
||||
return errors.New("multitrans: expected 401, got " + res.Status)
|
||||
}
|
||||
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
realm := tcp.Between(auth, `realm="`, `"`)
|
||||
nonce := tcp.Between(auth, `nonce="`, `"`)
|
||||
|
||||
// Step 2: Send Auth
|
||||
user := u.User.Username()
|
||||
pass, _ := u.User.Password()
|
||||
|
||||
ha1 := tcp.HexMD5(user, realm, pass)
|
||||
ha2 := tcp.HexMD5("MULTITRANS", uri)
|
||||
response := tcp.HexMD5(ha1, nonce, ha2)
|
||||
|
||||
authHeader := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
||||
user, realm, nonce, uri, response)
|
||||
|
||||
data = fmt.Sprintf("MULTITRANS %s RTSP/1.0\r\nCSeq: 1\r\nAuthorization: %s\r\nX-Client-UUID: %s\r\n\r\n",
|
||||
uri, authHeader, uid)
|
||||
|
||||
if _, err = c.conn.Write([]byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err = tcp.ReadResponse(c.rd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New("multitrans: auth failed: " + res.Status)
|
||||
}
|
||||
|
||||
// Session: 7116520596809429228
|
||||
session := res.Header.Get("Session")
|
||||
if session == "" {
|
||||
return errors.New("multitrans: no session")
|
||||
}
|
||||
|
||||
return c.openTalkChannel(uri, session)
|
||||
}
|
||||
|
||||
func (c *Client) openTalkChannel(uri, session string) error {
|
||||
payload := `{"type":"request","seq":0,"params":{"method":"get","talk":{"mode":"full_duplex"}}}`
|
||||
|
||||
data := fmt.Sprintf("MULTITRANS %s RTSP/1.0\r\nCSeq: 2\r\nSession: %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s",
|
||||
uri, session, len(payload), payload)
|
||||
|
||||
if _, err := c.conn.Write([]byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tcp.ReadResponse(c.rd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New("multitrans: talkback failed: " + res.Status)
|
||||
}
|
||||
|
||||
// Python checks for "error_code":0 in body.
|
||||
if !bytes.Contains(res.Body, []byte(`"error_code":0`)) {
|
||||
return fmt.Errorf("multitrans: talkback error: %s", string(res.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
|
||||
return nil, core.ErrCantGetTrack
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
_ = c.closed.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
c.closed.Done(nil)
|
||||
return c.Connection.Stop()
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>codecs - go2rtc</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="out"></div>
|
||||
<script>
|
||||
const out = document.getElementById('out');
|
||||
|
||||
const print = (name, caps) => {
|
||||
out.innerText += name + '\n';
|
||||
caps.codecs.forEach((codec) => {
|
||||
out.innerText += [codec.mimeType, codec.channels, codec.clockRate, codec.sdpFmtpLine] + '\n';
|
||||
});
|
||||
out.innerText += '\n';
|
||||
};
|
||||
|
||||
if (RTCRtpReceiver.getCapabilities) {
|
||||
print('receiver video', RTCRtpReceiver.getCapabilities('video'));
|
||||
print('receiver audio', RTCRtpReceiver.getCapabilities('audio'));
|
||||
print('sender video', RTCRtpSender.getCapabilities('video'));
|
||||
print('sender audio', RTCRtpSender.getCapabilities('audio'));
|
||||
}
|
||||
|
||||
const types = [
|
||||
'video/mp4; codecs="avc1.42401E"',
|
||||
'video/mp4; codecs="avc1.42C01E"',
|
||||
'video/mp4; codecs="avc1.42E01E"',
|
||||
'video/mp4; codecs="avc1.42001E"',
|
||||
'video/mp4; codecs="avc1.4D401E"',
|
||||
'video/mp4; codecs="avc1.4D001E"',
|
||||
'video/mp4; codecs="avc1.640032"',
|
||||
'video/mp4; codecs="avc1.640C32"',
|
||||
'video/mp4; codecs="avc1.F4001F"',
|
||||
'video/mp4; codecs="hvc1.1.6.L93.B0"',
|
||||
'video/mp4; codecs="hev1.1.6.L93.B0"',
|
||||
'video/mp4; codecs="hev1.2.4.L120.B0"',
|
||||
'video/mp4; codecs="flac"',
|
||||
'video/mp4; codecs="opus"',
|
||||
'video/mp4; codecs="mp3"',
|
||||
'video/mp4; codecs="null"',
|
||||
'application/vnd.apple.mpegurl',
|
||||
];
|
||||
|
||||
const video = document.createElement('video');
|
||||
out.innerText += 'video.canPlayType\n';
|
||||
types.forEach(type => {
|
||||
out.innerText += `${type} = ${'MediaSource' in window && MediaSource.isTypeSupported(type)} / ${video.canPlayType(type)}\n`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -1205,7 +1205,7 @@
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const r = await fetch('https://go2rtc.org/schema.json', {cache: 'no-cache'});
|
||||
const r = await fetch('schema.json', {cache: 'no-cache'});
|
||||
if (r.ok) setupYamlHints(await r.json());
|
||||
} catch (e) {
|
||||
// ignore schema load errors
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@
|
||||
if (data.setup_code === undefined) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js';
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js';
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
/* global BigInt */
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>net - go2rtc</title>
|
||||
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vis-network@10.0.2/standalone/umd/vis-network.min.js"></script>
|
||||
<style>
|
||||
html, body, #network {
|
||||
height: 100%;
|
||||
|
||||
@@ -4,4 +4,5 @@ import "embed"
|
||||
|
||||
//go:embed *.html
|
||||
//go:embed *.js
|
||||
//go:embed *.json
|
||||
var Static embed.FS
|
||||
|
||||
Reference in New Issue
Block a user