diff --git a/docker/Dockerfile b/docker/Dockerfile index 8d064f21..9efded4b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 diff --git a/docker/hardware.Dockerfile b/docker/hardware.Dockerfile index a80d08d7..563843b5 100644 --- a/docker/hardware.Dockerfile +++ b/docker/hardware.Dockerfile @@ -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 diff --git a/docker/rockchip.Dockerfile b/docker/rockchip.Dockerfile index 949db83b..6ab924ee 100644 --- a/docker/rockchip.Dockerfile +++ b/docker/rockchip.Dockerfile @@ -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 diff --git a/internal/doorbird/README.md b/internal/doorbird/README.md new file mode 100644 index 00000000..7c31efae --- /dev/null +++ b/internal/doorbird/README.md @@ -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 +``` diff --git a/internal/multitrans/README.md b/internal/multitrans/README.md new file mode 100644 index 00000000..6201f8b6 --- /dev/null +++ b/internal/multitrans/README.md @@ -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 +``` diff --git a/internal/multitrans/multitrans.go b/internal/multitrans/multitrans.go new file mode 100644 index 00000000..31e6a9a4 --- /dev/null +++ b/internal/multitrans/multitrans.go @@ -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) +} diff --git a/main.go b/main.go index 35984e40..def7ee35 100644 --- a/main.go +++ b/main.go @@ -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}, diff --git a/pkg/core/codec.go b/pkg/core/codec.go index f38275e8..11276bc7 100644 --- a/pkg/core/codec.go +++ b/pkg/core/codec.go @@ -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 diff --git a/pkg/multitrans/client.go b/pkg/multitrans/client.go new file mode 100644 index 00000000..d71269c1 --- /dev/null +++ b/pkg/multitrans/client.go @@ -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() +} diff --git a/www/codecs.html b/www/codecs.html deleted file mode 100644 index f0983f02..00000000 --- a/www/codecs.html +++ /dev/null @@ -1,65 +0,0 @@ - - -
- -