From 2b8ced9c59acd83992678fa3e965ce4a41651b7f Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Mon, 6 May 2024 08:06:08 +0400 Subject: [PATCH 01/87] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0293f3e..8d332487 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,14 +102,14 @@ jobs: env: { GOOS: freebsd, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_amd64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_amd64, path: go2rtc } - name: Build go2rtc_freebsd_arm64 env: { GOOS: freebsd, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_arm64, path: go2rtc } docker-master: From 3ce4624aee32771d0e793765ad6bee046c95bbdf Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 24 May 2024 10:03:51 -0300 Subject: [PATCH 02/87] Add note about requesting multiple backchannel on Dahua Doorbell --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0ec1f0d0..f4532d29 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ streams: sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0 dahua_camera: - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif - - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1 + - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1#backchannel=0 amcrest_doorbell: - rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0 unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK @@ -241,7 +241,7 @@ streams: **Recommendations** - **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file -- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52) +- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52). Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell - **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation - **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81) - **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html) From 4cc28977cb43a07f5f57ae8113e98dbd8d62c094 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 24 May 2024 10:10:21 -0300 Subject: [PATCH 03/87] Add note about `unicast=true&proto=Onvif` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4532d29..fbde0ec8 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ streams: **Recommendations** - **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file -- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52). Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell +- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52). Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for 2-way audio as this makes the doorbell accept multiple codecs for the incoming audio - **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation - **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81) - **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html) From 562046c278e18c5055f49c48f49b261d430fb574 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 24 May 2024 10:12:34 -0300 Subject: [PATCH 04/87] Fix link to audio codec change tip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fbde0ec8..6e37b198 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ streams: **Recommendations** - **Amcrest Doorbell** users may want to disable two way audio, because with an active stream you won't have a call button working. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file -- **Dahua Doorbell** users may want to change backchannel [audio codec](https://github.com/AlexxIT/go2rtc/issues/52). Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for 2-way audio as this makes the doorbell accept multiple codecs for the incoming audio +- **Dahua Doorbell** users may want to change [audio codec](https://github.com/AlexxIT/go2rtc/issues/49#issuecomment-2127107379) for proper 2-way audio. Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for 2-way audio as this makes the doorbell accept multiple codecs for the incoming audio - **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful unusable stream implementation - **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81) - **TP-Link Tapo** users may skip login and password, because go2rtc support login [without them](https://drmnsamoliu.github.io/video.html) From bce0b4a8a0dbf8a0a968a82cf062279de9ea42e6 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 6 Jun 2024 18:19:17 +0300 Subject: [PATCH 05/87] feat(logging): add file output option for logging configuration --- README.md | 1 + internal/app/log.go | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index c31ed748..faabfa6b 100644 --- a/README.md +++ b/README.md @@ -1213,6 +1213,7 @@ log: rtsp: warn streams: error webrtc: fatal + output: stdout # Available output options are: stdout, stderr, or a file path. ``` ## Security diff --git a/internal/app/log.go b/internal/app/log.go index 222f6f2b..79a5cbae 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -24,6 +24,19 @@ func NewLogger(config map[string]string) zerolog.Logger { writer = os.Stderr case "stdout": writer = os.Stdout + case "file": + filePath := config["file"] + if filePath == "" { + filePath = "go2rtc.log" + } + file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Error().Msgf("failed to open log file %s: %v", filePath, err) + } + defer file.Close() + writer = file + default: + writer = os.Stdout } timeFormat := config["time"] From e46fc13feaf5291ab89ddc6b2dfa617d9596b022 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 6 Jun 2024 18:25:30 +0300 Subject: [PATCH 06/87] fix(log): ensure fallback to stdout if log file open fails --- internal/app/log.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/app/log.go b/internal/app/log.go index 79a5cbae..b64c39c0 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -32,9 +32,10 @@ func NewLogger(config map[string]string) zerolog.Logger { file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Error().Msgf("failed to open log file %s: %v", filePath, err) + writer = os.Stdout + } else { + writer = file } - defer file.Close() - writer = file default: writer = os.Stdout } From ac798d9d6d507a9e1f4fef6019b4dcd4c9d4a790 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 6 Jun 2024 19:07:09 +0300 Subject: [PATCH 07/87] fix(log): handle log file open error by writing to stdout --- internal/app/log.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/log.go b/internal/app/log.go index b64c39c0..dd29ad06 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -31,7 +31,7 @@ func NewLogger(config map[string]string) zerolog.Logger { } file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - log.Error().Msgf("failed to open log file %s: %v", filePath, err) + os.Stdout.WriteString("Error: Failed to open log file: " + err.Error() + ". Log output is set to stdout now.\n") writer = os.Stdout } else { writer = file From 0e5b293b1ff3090007b8b8b961cec3c5cdef2ddf Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Wed, 19 Jun 2024 12:19:21 +0300 Subject: [PATCH 08/87] fix(network): preserve selected nodes and edges on data reload --- www/network.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/network.html b/www/network.html index 7a4ff229..180c9711 100644 --- a/www/network.html +++ b/www/network.html @@ -57,9 +57,14 @@ const positions = network.getPositions(); const viewPosition = network.getViewPosition(); const scale = network.getScale(); + const selectedNodes = network.getSelectedNodes(); + const selectedEdges = network.getSelectedEdges(); network.setData(data); + network.selectNodes(selectedNodes); + network.selectEdges(selectedEdges); + for (const nodeId in positions) { network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); } From 56e2c6650dbb73c1a8c861094c550e183f0ed9d8 Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Sat, 22 Jun 2024 19:15:07 +0400 Subject: [PATCH 09/87] Update build.cmd --- scripts/build.cmd | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/build.cmd b/scripts/build.cmd index 54565b2d..8b355f76 100644 --- a/scripts/build.cmd +++ b/scripts/build.cmd @@ -56,3 +56,13 @@ go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc @SET GOARCH=arm64 @SET FILENAME=go2rtc_mac_arm64.zip go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc + +@SET GOOS=freebsd +@SET GOARCH=amd64 +@SET FILENAME=go2rtc_freebsd_amd64.zip +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc + +@SET GOOS=freebsd +@SET GOARCH=arm64 +@SET FILENAME=go2rtc_freebsd_arm64.zip +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc From c47427633c1c984f79180b29ebee85197a5bf11e Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Sat, 22 Jun 2024 19:20:39 +0400 Subject: [PATCH 10/87] Update build.sh --- scripts/build.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 0814ba48..e365eb54 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -79,4 +79,16 @@ go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc export GOOS=darwin export GOARCH=arm64 FILENAME="go2rtc_mac_arm64.zip" -go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc \ No newline at end of file +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc + +# FreeBSD amd64 +export GOOS=freebsd +export GOARCH=amd64 +FILENAME="go2rtc_freebsd_amd64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc + +# FreeBSD arm64 +export GOOS=freebsd +export GOARCH=arm64 +FILENAME="go2rtc_freebsd_arm64.zip" +go build -ldflags "-s -w" -trimpath && 7z a -mx9 -bso0 -sdel $FILENAME go2rtc From a04b7eed28fb344dd949d257e3c375ead7a4b71e Mon Sep 17 00:00:00 2001 From: Rob van Oostenrijk Date: Sat, 22 Jun 2024 19:23:14 +0400 Subject: [PATCH 11/87] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c31ed748..33c14cca 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 - `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks)) - `go2rtc_mac_amd64.zip` - Mac Intel 64-bit - `go2rtc_mac_arm64.zip` - Mac ARM 64-bit +- `go2rtc_freebsd_amd64.zip` - FreeBSD Intel 64-bit +- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. From 5b0781253ffb8c690226c7dc8d044fbdd722ed6a Mon Sep 17 00:00:00 2001 From: Jamal Fanaian Date: Thu, 11 Jul 2024 17:54:04 -0700 Subject: [PATCH 12/87] Add support for Nest cameras with RTSP --- pkg/nest/api.go | 180 ++++++++++++++++++++++++++++++++++++++++++--- pkg/nest/client.go | 92 +++++++++++++++++++---- 2 files changed, 248 insertions(+), 24 deletions(-) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 5e0d3407..8aae2df2 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -17,9 +17,15 @@ type API struct { StreamProjectID string StreamDeviceID string - StreamSessionID string StreamExpiresAt time.Time + // WebRTC + StreamSessionID string + + // RTSP + StreamToken string + StreamExtensionToken string + extendTimer *time.Timer } @@ -112,7 +118,14 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { continue } - if device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols[0] != "WEB_RTC" { + supported := false + for _, protocol := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { + if (protocol == "WEB_RTC" || protocol == "RTSP") { + supported = true + break + } + } + if !supported { continue } @@ -122,12 +135,44 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { } name := device.Traits.SdmDevicesTraitsInfo.CustomName + // Devices configured through the Nest app use the container/room name as opposed to the customName trait + if name == "" && len(device.ParentRelations) > 0 { + name = device.ParentRelations[0].DisplayName + } devices[name] = device.Name[i+1:] } return devices, nil } +func (a *API) GetDevice(projectID, deviceID string) (Device, error) { + uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices/" + deviceID + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + return Device{}, err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return Device{}, err + } + + if res.StatusCode != 200 { + return Device{}, errors.New("nest: wrong status: " + res.Status) + } + + var device Device + + if err = json.NewDecoder(res.Body).Decode(&device); err != nil { + return Device{}, err + } + + return device, nil +} + func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { var reqv struct { Command string `json:"command"` @@ -186,11 +231,20 @@ func (a *API) ExtendStream() error { var reqv struct { Command string `json:"command"` Params struct { - MediaSessionID string `json:"mediaSessionId"` + MediaSessionID string `json:"mediaSessionId,omitempty"` + StreamExtensionToken string `json:"streamExtensionToken,omitempty"` } `json:"params"` } - reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream" - reqv.Params.MediaSessionID = a.StreamSessionID + + if a.StreamToken != "" { + // RTSP + reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendRtspStream" + reqv.Params.StreamExtensionToken = a.StreamExtensionToken + } else { + // WebRTC + reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream" + reqv.Params.MediaSessionID = a.StreamSessionID + } b, err := json.Marshal(reqv) if err != nil { @@ -220,6 +274,8 @@ func (a *API) ExtendStream() error { Results struct { ExpiresAt time.Time `json:"expiresAt"` MediaSessionID string `json:"mediaSessionId"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` } `json:"results"` } @@ -229,6 +285,111 @@ func (a *API) ExtendStream() error { a.StreamSessionID = resv.Results.MediaSessionID a.StreamExpiresAt = resv.Results.ExpiresAt + a.StreamExtensionToken = resv.Results.StreamExtensionToken + a.StreamToken = resv.Results.StreamToken + + return nil +} + +func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) { + var reqv struct { + Command string `json:"command"` + Params struct {} `json:"params"` + } + reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateRtspStream" + + b, err := json.Marshal(reqv) + if err != nil { + return "", err + } + + uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + projectID + "/devices/" + deviceID + ":executeCommand" + req, err := http.NewRequest("POST", uri, bytes.NewReader(b)) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return "", err + } + + if res.StatusCode != 200 { + return "", errors.New("nest: wrong status: " + res.Status) + } + + var resv struct { + Results struct { + StreamURLs map[string]string `json:"streamUrls"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` + ExpiresAt time.Time `json:"expiresAt"` + } `json:"results"` + } + + if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { + return "", err + } + + if _, ok := resv.Results.StreamURLs["rtspUrl"]; !ok { + return "", errors.New("nest: failed to generate rtsp url") + } + + a.StreamProjectID = projectID + a.StreamDeviceID = deviceID + a.StreamToken = resv.Results.StreamToken + a.StreamExtensionToken = resv.Results.StreamExtensionToken + a.StreamExpiresAt = resv.Results.ExpiresAt + + return resv.Results.StreamURLs["rtspUrl"], nil +} + +func (a *API) StopRTSPStream() error { + if a.StreamProjectID == "" || a.StreamDeviceID == "" { + return errors.New("nest: tried to stop rtsp stream without a project or device ID") + } + + var reqv struct { + Command string `json:"command"` + Params struct { + StreamExtensionToken string `json:"streamExtensionToken"` + } `json:"params"` + } + reqv.Command = "sdm.devices.commands.CameraLiveStream.StopRtspStream" + reqv.Params.StreamExtensionToken = a.StreamExtensionToken + + b, err := json.Marshal(reqv) + if err != nil { + return err + } + + uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + a.StreamProjectID + "/devices/" + a.StreamDeviceID + ":executeCommand" + req, err := http.NewRequest("POST", uri, bytes.NewReader(b)) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return err + } + + if res.StatusCode != 200 { + return errors.New("nest: wrong status: " + res.Status) + } + + a.StreamProjectID = "" + a.StreamDeviceID = "" + a.StreamExtensionToken = "" + a.StreamToken = "" return nil } @@ -261,10 +422,10 @@ type Device struct { //SdmDevicesTraitsCameraClipPreview struct { //} `json:"sdm.devices.traits.CameraClipPreview"` } `json:"traits"` - //ParentRelations []struct { - // Parent string `json:"parent"` - // DisplayName string `json:"displayName"` - //} `json:"parentRelations"` + ParentRelations []struct { + Parent string `json:"parent"` + DisplayName string `json:"displayName"` + } `json:"parentRelations"` } func (a *API) StartExtendStreamTimer() { @@ -277,7 +438,6 @@ func (a *API) StartExtendStreamTimer() { duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second)) a.extendTimer.Reset(duration) }) - } func (a *API) StopExtendStreamTimer() { diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 0b243384..5fc589cb 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -5,16 +5,22 @@ import ( "net/url" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" ) -type Client struct { +type WebRTCClient struct { conn *webrtc.Conn api *API } -func Dial(rawURL string) (*Client, error) { +type RTSPClient struct { + conn *rtsp.Conn + api *API +} + +func Dial(rawURL string) (core.Producer, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -36,6 +42,49 @@ func Dial(rawURL string) (*Client, error) { return nil, err } + device, err := nestAPI.GetDevice(projectID, deviceID) + if err != nil { + return nil, err + } + + for _, proto := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { + if proto == "WEB_RTC" { + return rtcConn(nestAPI, rawURL, projectID, deviceID) + } else if proto == "RTSP" { + return rtspConn(nestAPI, rawURL, projectID, deviceID) + } + } + + return nil, errors.New("nest: unsupported camera") +} + +func (c *WebRTCClient) GetMedias() []*core.Media { + return c.conn.GetMedias() +} + +func (c *WebRTCClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { + return c.conn.GetTrack(media, codec) +} + +func (c *WebRTCClient) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + return c.conn.AddTrack(media, codec, track) +} + +func (c *WebRTCClient) Start() error { + c.api.StartExtendStreamTimer() + return c.conn.Start() +} + +func (c *WebRTCClient) Stop() error { + c.api.StopExtendStreamTimer() + return c.conn.Stop() +} + +func (c *WebRTCClient) MarshalJSON() ([]byte, error) { + return c.conn.MarshalJSON() +} + +func rtcConn(nestAPI *API, rawURL, projectID, deviceID string) (*WebRTCClient, error) { rtcAPI, err := webrtc.NewAPI() if err != nil { return nil, err @@ -77,31 +126,46 @@ func Dial(rawURL string) (*Client, error) { return nil, err } - return &Client{conn: conn, api: nestAPI}, nil + return &WebRTCClient{conn: conn, api: nestAPI}, nil } -func (c *Client) GetMedias() []*core.Media { - return c.conn.GetMedias() +func rtspConn(nestAPI *API, rawURL, projectID, deviceID string) (*RTSPClient, error) { + rtspURL, err := nestAPI.GenerateRtspStream(projectID, deviceID) + if err != nil { + return nil, err + } + + rtspClient := rtsp.NewClient(rtspURL) + if err := rtspClient.Dial(); err != nil { + return nil, err + } + if err := rtspClient.Describe(); err != nil { + return nil, err + } + + return &RTSPClient{conn: rtspClient, api: nestAPI}, nil } -func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { +func (c *RTSPClient) GetMedias() []*core.Media { + result := c.conn.GetMedias() + return result +} + +func (c *RTSPClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) { return c.conn.GetTrack(media, codec) } -func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { - return c.conn.AddTrack(media, codec, track) -} - -func (c *Client) Start() error { +func (c *RTSPClient) Start() error { c.api.StartExtendStreamTimer() return c.conn.Start() } -func (c *Client) Stop() error { +func (c *RTSPClient) Stop() error { + c.api.StopRTSPStream() c.api.StopExtendStreamTimer() return c.conn.Stop() } -func (c *Client) MarshalJSON() ([]byte, error) { +func (c *RTSPClient) MarshalJSON() ([]byte, error) { return c.conn.MarshalJSON() -} +} \ No newline at end of file From e1021a96af447a9448f92100f49a3a865f1d36a2 Mon Sep 17 00:00:00 2001 From: Jamal Fanaian Date: Thu, 11 Jul 2024 17:58:31 -0700 Subject: [PATCH 13/87] go fmt --- pkg/nest/api.go | 26 +++++++++++++------------- pkg/nest/client.go | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 8aae2df2..80a421ba 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -23,7 +23,7 @@ type API struct { StreamSessionID string // RTSP - StreamToken string + StreamToken string StreamExtensionToken string extendTimer *time.Timer @@ -120,7 +120,7 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { supported := false for _, protocol := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { - if (protocol == "WEB_RTC" || protocol == "RTSP") { + if protocol == "WEB_RTC" || protocol == "RTSP" { supported = true break } @@ -231,7 +231,7 @@ func (a *API) ExtendStream() error { var reqv struct { Command string `json:"command"` Params struct { - MediaSessionID string `json:"mediaSessionId,omitempty"` + MediaSessionID string `json:"mediaSessionId,omitempty"` StreamExtensionToken string `json:"streamExtensionToken,omitempty"` } `json:"params"` } @@ -272,10 +272,10 @@ func (a *API) ExtendStream() error { var resv struct { Results struct { - ExpiresAt time.Time `json:"expiresAt"` - MediaSessionID string `json:"mediaSessionId"` - StreamExtensionToken string `json:"streamExtensionToken"` - StreamToken string `json:"streamToken"` + ExpiresAt time.Time `json:"expiresAt"` + MediaSessionID string `json:"mediaSessionId"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` } `json:"results"` } @@ -293,8 +293,8 @@ func (a *API) ExtendStream() error { func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) { var reqv struct { - Command string `json:"command"` - Params struct {} `json:"params"` + Command string `json:"command"` + Params struct{} `json:"params"` } reqv.Command = "sdm.devices.commands.CameraLiveStream.GenerateRtspStream" @@ -324,10 +324,10 @@ func (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) { var resv struct { Results struct { - StreamURLs map[string]string `json:"streamUrls"` - StreamExtensionToken string `json:"streamExtensionToken"` - StreamToken string `json:"streamToken"` - ExpiresAt time.Time `json:"expiresAt"` + StreamURLs map[string]string `json:"streamUrls"` + StreamExtensionToken string `json:"streamExtensionToken"` + StreamToken string `json:"streamToken"` + ExpiresAt time.Time `json:"expiresAt"` } `json:"results"` } diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 5fc589cb..6d867dea 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -17,7 +17,7 @@ type WebRTCClient struct { type RTSPClient struct { conn *rtsp.Conn - api *API + api *API } func Dial(rawURL string) (core.Producer, error) { @@ -168,4 +168,4 @@ func (c *RTSPClient) Stop() error { func (c *RTSPClient) MarshalJSON() ([]byte, error) { return c.conn.MarshalJSON() -} \ No newline at end of file +} From 13dd3084c20b3bea53506382f0820ea2c8203415 Mon Sep 17 00:00:00 2001 From: Jamal Fanaian Date: Thu, 11 Jul 2024 18:47:05 -0700 Subject: [PATCH 14/87] Carry protocol info in stream URL --- internal/nest/init.go | 8 +++++--- pkg/nest/api.go | 41 ++++++++++------------------------------- pkg/nest/client.go | 15 +++++++++------ 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/internal/nest/init.go b/internal/nest/init.go index 01682414..8289af73 100644 --- a/internal/nest/init.go +++ b/internal/nest/init.go @@ -2,6 +2,7 @@ package nest import ( "net/http" + "strings" "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/streams" @@ -38,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) { var items []*api.Source - for name, deviceID := range devices { - query.Set("device_id", deviceID) + for _, device := range devices { + query.Set("device_id", device.DeviceID) + query.Set("protocols", strings.Join(device.Protocols, ",")) items = append(items, &api.Source{ - Name: name, URL: "nest:?" + query.Encode(), + Name: device.Name, URL: "nest:?" + query.Encode(), }) } diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 80a421ba..9e32cbcf 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -33,6 +33,12 @@ type Auth struct { AccessToken string } +type DeviceInfo struct { + Name string + DeviceID string + Protocols []string +} + var cache = map[string]*API{} var cacheMu sync.Mutex @@ -84,7 +90,7 @@ func NewAPI(clientID, clientSecret, refreshToken string) (*API, error) { return api, nil } -func (a *API) GetDevices(projectID string) (map[string]string, error) { +func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) { uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices" req, err := http.NewRequest("GET", uri, nil) if err != nil { @@ -111,7 +117,7 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { return nil, err } - devices := map[string]string{} + devices := make([]DeviceInfo, 0, len(resv.Devices)) for _, device := range resv.Devices { if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 { @@ -139,40 +145,13 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { if name == "" && len(device.ParentRelations) > 0 { name = device.ParentRelations[0].DisplayName } - devices[name] = device.Name[i+1:] + + devices = append(devices, DeviceInfo{Name: name, DeviceID: device.Name[i+1:], Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols}) } return devices, nil } -func (a *API) GetDevice(projectID, deviceID string) (Device, error) { - uri := "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectID + "/devices/" + deviceID - req, err := http.NewRequest("GET", uri, nil) - if err != nil { - return Device{}, err - } - - req.Header.Set("Authorization", "Bearer "+a.Token) - - client := &http.Client{Timeout: time.Second * 5000} - res, err := client.Do(req) - if err != nil { - return Device{}, err - } - - if res.StatusCode != 200 { - return Device{}, errors.New("nest: wrong status: " + res.Status) - } - - var device Device - - if err = json.NewDecoder(res.Body).Decode(&device); err != nil { - return Device{}, err - } - - return device, nil -} - func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { var reqv struct { Command string `json:"command"` diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 6d867dea..e692359c 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -3,6 +3,7 @@ package nest import ( "errors" "net/url" + "strings" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtsp" @@ -32,6 +33,12 @@ func Dial(rawURL string) (core.Producer, error) { refreshToken := query.Get("refresh_token") projectID := query.Get("project_id") deviceID := query.Get("device_id") + protocols := strings.Split(query.Get("protocols"), ",") + + // Default to WEB_RTC for backwards compataiility + if len(protocols) == 0 { + protocols = append(protocols, "WEB_RTC") + } if cliendID == "" || cliendSecret == "" || refreshToken == "" || projectID == "" || deviceID == "" { return nil, errors.New("nest: wrong query") @@ -42,12 +49,8 @@ func Dial(rawURL string) (core.Producer, error) { return nil, err } - device, err := nestAPI.GetDevice(projectID, deviceID) - if err != nil { - return nil, err - } - - for _, proto := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { + // Pick the first supported protocol in order of priority (WEB_RTC, RTSP) + for _, proto := range protocols { if proto == "WEB_RTC" { return rtcConn(nestAPI, rawURL, projectID, deviceID) } else if proto == "RTSP" { From c81caa4d2c7dbda9c78de2010d22b1b8e9dff39d Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 17 Jul 2024 13:37:56 +0300 Subject: [PATCH 15/87] Install ffplay in container --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b3888820..7a578980 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ FROM base # and other common tools for the echo source. # alsa-plugins-pulse for ALSA support (+0MB) # font-droid for FFmpeg drawtext filter (+2MB) -RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid +RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid # Hardware Acceleration for Intel CPU (+50MB) ARG TARGETARCH From 23e8f7e0aac12825ecd74ce4ce08b76a743a82b7 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 28 Jul 2024 05:34:49 +0300 Subject: [PATCH 16/87] refactor(api): move port extraction logic to Init function for prevent data race --- internal/api/api.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 86817bd0..419e2bdf 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -69,6 +69,8 @@ func Init() { } if cfg.Mod.Listen != "" { + _, port, _ := net.SplitHostPort(cfg.Mod.Listen) + Port, _ = strconv.Atoi(port) go listen("tcp", cfg.Mod.Listen) } @@ -92,10 +94,6 @@ func listen(network, address string) { log.Info().Str("addr", address).Msg("[api] listen") - if network == "tcp" { - Port = ln.Addr().(*net.TCPAddr).Port - } - server := http.Server{ Handler: Handler, ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds From f13aa21d0f83204f23050981f9d390523c356977 Mon Sep 17 00:00:00 2001 From: seydx Date: Sun, 3 Nov 2024 16:33:08 +0100 Subject: [PATCH 17/87] Add backchannel support for rtsp server --- internal/rtsp/rtsp.go | 18 ++++++++++++++++++ pkg/core/media.go | 4 ++++ pkg/rtsp/producer.go | 44 +++++++++++++++++++++++++++++-------------- pkg/rtsp/server.go | 29 +++++++++++++++++++++++----- 4 files changed, 76 insertions(+), 19 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 230bdece..377061e5 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -8,6 +8,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/tcp" @@ -184,6 +185,23 @@ func tcpHandler(conn *rtsp.Conn) { } } + if query.Get("backchannel") == "1" { + conn.Medias = append(conn.Medias, &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ + {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, + {Name: core.CodecPCM, ClockRate: 16000}, + {Name: core.CodecPCMA, ClockRate: 16000}, + {Name: core.CodecPCMU, ClockRate: 16000}, + {Name: core.CodecPCM, ClockRate: 8000}, + {Name: core.CodecPCMA, ClockRate: 8000}, + {Name: core.CodecPCMU, ClockRate: 8000}, + {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, + }, + }) + } + if s := query.Get("pkt_size"); s != "" { conn.PacketSize = uint16(core.Atoi(s)) } diff --git a/pkg/core/media.go b/pkg/core/media.go index 72ab58c6..a700bb62 100644 --- a/pkg/core/media.go +++ b/pkg/core/media.go @@ -141,6 +141,10 @@ func MarshalSDP(name string, medias []*Media) ([]byte, error) { } md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine) + if media.Direction != "" { + md.WithPropertyAttribute(media.Direction) + } + if media.ID != "" { md.WithValueAttribute("control", media.ID) } diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index de115808..323d9197 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -16,27 +16,43 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e } } - c.stateMu.Lock() - defer c.stateMu.Unlock() + switch c.mode { + case core.ModeActiveProducer: + c.stateMu.Lock() + defer c.stateMu.Unlock() - if c.state == StatePlay { - if err := c.Reconnect(); err != nil { + if c.state == StatePlay { + if err := c.Reconnect(); err != nil { + return nil, err + } + } + + channel, err := c.SetupMedia(media) + if err != nil { return nil, err } - } - channel, err := c.SetupMedia(media) - if err != nil { - return nil, err - } + c.state = StateSetup - c.state = StateSetup + track := core.NewReceiver(media, codec) + track.ID = channel + c.Receivers = append(c.Receivers, track) - track := core.NewReceiver(media, codec) - track.ID = channel - c.Receivers = append(c.Receivers, track) + return track, nil + case core.ModePassiveConsumer: + // Backchannel + c.stateMu.Lock() + defer c.stateMu.Unlock() - return track, nil + channel := byte(len(c.Senders)) * 2 + track := core.NewReceiver(media, codec) + track.ID = channel + c.Receivers = append(c.Receivers, track) + + return track, nil + default: + return nil, errors.New("rtsp: wrong mode for GetTrack") + } } func (c *Conn) Start() (err error) { diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 7953b0dc..1cddbec5 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -129,6 +129,16 @@ func (c *Conn) Accept() error { medias = append(medias, media) } + for i, track := range c.Receivers { + media := &core.Media{ + Kind: core.GetKind(track.Codec.Name), + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{track.Codec}, + ID: "trackID=" + strconv.Itoa(i+len(c.Senders)), + } + medias = append(medias, media) + } + res.Body, err = core.MarshalSDP(c.SessionName, medias) if err != nil { return err @@ -154,11 +164,20 @@ func (c *Conn) Accept() error { c.state = StateSetup if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { - // mark sender as SETUP - c.Senders[i].Media.ID = MethodSetup - tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1) - res.Header.Set("Transport", tr) + trackID := reqTrackID(req) + + if trackID >= 0 { + if trackID < len(c.Senders) { + c.Senders[trackID].Media.ID = MethodSetup + tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", trackID*2, trackID*2+1) + res.Header.Set("Transport", tr) + } else if trackID >= len(c.Senders) && trackID < len(c.Senders)+len(c.Receivers) { + c.Receivers[trackID-len(c.Senders)].Media.ID = MethodSetup + tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", trackID*2, trackID*2+1) + res.Header.Set("Transport", tr) + } else { + res.Status = "400 Bad Request" + } } else { res.Status = "400 Bad Request" } From 223f94077f675c5e95b9858d5e16508b9bdecb42 Mon Sep 17 00:00:00 2001 From: MrToan Date: Thu, 7 Nov 2024 08:49:06 +0700 Subject: [PATCH 18/87] Fix "panic: send on closed channel" --- pkg/core/track.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/core/track.go b/pkg/core/track.go index 8bc65374..b585fa98 100644 --- a/pkg/core/track.go +++ b/pkg/core/track.go @@ -3,7 +3,6 @@ package core import ( "encoding/json" "errors" - "github.com/pion/rtp" ) @@ -71,8 +70,9 @@ type Sender struct { Packets int `json:"packets,omitempty"` Drops int `json:"drops,omitempty"` - buf chan *Packet - done chan struct{} + buf chan *Packet + done chan struct{} + isClosed bool } func NewSender(media *Media, codec *Codec) *Sender { @@ -99,6 +99,11 @@ func NewSender(media *Media, codec *Codec) *Sender { s.Input = func(packet *Packet) { // writing to nil chan - OK, writing to closed chan - panic s.mu.Lock() + if s.isClosed { + s.Drops++ + s.mu.Unlock() + return + } select { case s.buf <- packet: s.Bytes += len(packet.Payload) @@ -165,10 +170,13 @@ func (s *Sender) State() string { func (s *Sender) Close() { // close buffer if exists - if buf := s.buf; buf != nil { + s.mu.Lock() + if buf := s.buf; buf != nil && !s.isClosed { + s.isClosed = true s.buf = nil defer close(buf) } + s.mu.Unlock() s.Node.Close() } From a8edaedc8b7518507eb1a5720d9963639a056b13 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 14 Nov 2024 19:39:26 +0300 Subject: [PATCH 19/87] Fix broken incoming sources after v1.9.7 #1458 --- internal/streams/play.go | 2 +- internal/streams/stream.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/streams/play.go b/internal/streams/play.go index 7ada66e6..9bec7258 100644 --- a/internal/streams/play.go +++ b/internal/streams/play.go @@ -103,7 +103,7 @@ func (s *Stream) Play(source string) error { } func (s *Stream) AddInternalProducer(conn core.Producer) { - producer := &Producer{conn: conn, state: stateInternal} + producer := &Producer{conn: conn, state: stateInternal, url: "internal"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() diff --git a/internal/streams/stream.go b/internal/streams/stream.go index e194e0ac..569e63ee 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -76,7 +76,7 @@ func (s *Stream) RemoveConsumer(cons core.Consumer) { } func (s *Stream) AddProducer(prod core.Producer) { - producer := &Producer{conn: prod, state: stateExternal} + producer := &Producer{conn: prod, state: stateExternal, url: "external"} s.mu.Lock() s.producers = append(s.producers, producer) s.mu.Unlock() From 29f7f1a57d8b4366d68c20eec090a7a5c641ff38 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:50:35 +0100 Subject: [PATCH 20/87] feat: accept rtsp client without interleaved parameter --- pkg/rtsp/server.go | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index c96125a2..b59d9abf 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -148,25 +148,30 @@ func (c *Conn) Accept() error { Request: req, } - const transport = "RTP/AVP/TCP;unicast;interleaved=" - if tr = core.Between(tr, "interleaved=", ";"); tr != "" { - c.session = core.RandString(8, 10) - c.state = StateSetup + const transport = "RTP/AVP/TCP;unicast" - if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { - // mark sender as SETUP - c.Senders[i].Media.ID = MethodSetup - tr = fmt.Sprintf("%d-%d", i*2, i*2+1) - res.Header.Set("Transport", transport+tr) + c.session = core.RandString(8, 10) + c.state = StateSetup + + if c.mode == core.ModePassiveConsumer { + if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { + // mark sender as SETUP + c.Senders[i].Media.ID = MethodSetup + interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) + + // Check if transport already contains the 'interleaved' parameter + if strings.Contains(transport, "interleaved=") { + // If so, just update the interleaved value + res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) } else { - res.Status = "400 Bad Request" + // Otherwise, append the interleaved parameter + res.Header.Set("Transport", transport+";interleaved="+interleaved) } } else { - res.Header.Set("Transport", transport+tr) + res.Status = "400 Bad Request" } } else { - res.Status = "461 Unsupported transport" + res.Header.Set("Transport", tr) } if err = c.WriteResponse(res); err != nil { From fd125ecc683daf8f526673aca3ead91e53540e37 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:28:13 +0100 Subject: [PATCH 21/87] fix: return 461 if client requested an invalid transport method --- pkg/rtsp/server.go | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index b59d9abf..f5967c5a 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -150,28 +150,35 @@ func (c *Conn) Accept() error { const transport = "RTP/AVP/TCP;unicast" - c.session = core.RandString(8, 10) - c.state = StateSetup + // Test if client requests unicast with TCP transport, otherwise return 461 Transport not supported + // This allows smart clients who initially requested UDP to fall back on TCP transport. + if strings.HasPrefix(tr, transport) { + + c.session = core.RandString(8, 10) + c.state = StateSetup - if c.mode == core.ModePassiveConsumer { - if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { - // mark sender as SETUP - c.Senders[i].Media.ID = MethodSetup - interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) - - // Check if transport already contains the 'interleaved' parameter - if strings.Contains(transport, "interleaved=") { - // If so, just update the interleaved value - res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) + if c.mode == core.ModePassiveConsumer { + if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { + // mark sender as SETUP + c.Senders[i].Media.ID = MethodSetup + interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) + + // Check if transport already contains the 'interleaved' parameter + if strings.Contains(transport, "interleaved=") { + // If so, just update the interleaved value + res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) + } else { + // Otherwise, append the interleaved parameter + res.Header.Set("Transport", transport+";interleaved="+interleaved) + } } else { - // Otherwise, append the interleaved parameter - res.Header.Set("Transport", transport+";interleaved="+interleaved) + res.Status = "400 Bad Request" } } else { - res.Status = "400 Bad Request" + res.Header.Set("Transport", tr) } - } else { - res.Header.Set("Transport", tr) + else { + res.Status = "461 Unsupported transport" } if err = c.WriteResponse(res); err != nil { From d881755503b4c30b338fd3924fd51cdeb1b460c7 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:30:10 +0100 Subject: [PATCH 22/87] chore: lint --- pkg/rtsp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index f5967c5a..d9a40881 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -156,7 +156,7 @@ func (c *Conn) Accept() error { c.session = core.RandString(8, 10) c.state = StateSetup - + if c.mode == core.ModePassiveConsumer { if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP From 4b80b2c233be089fb8ced21bc05f2c493cc9a3d9 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:36:18 +0100 Subject: [PATCH 23/87] fix: typo --- pkg/rtsp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index d9a40881..88d24e27 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -177,7 +177,7 @@ func (c *Conn) Accept() error { } else { res.Header.Set("Transport", tr) } - else { + } else { res.Status = "461 Unsupported transport" } From 6fa352f407c3466edafeffab1e56b0e84740f2d3 Mon Sep 17 00:00:00 2001 From: fmcloudconsulting <170678386+fmcloudconsulting@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:06:15 +0100 Subject: [PATCH 24/87] fix: don't require unicast param and fix typo (tr instead of transport) --- pkg/rtsp/server.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 88d24e27..cefaef1d 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -148,11 +148,9 @@ func (c *Conn) Accept() error { Request: req, } - const transport = "RTP/AVP/TCP;unicast" - - // Test if client requests unicast with TCP transport, otherwise return 461 Transport not supported + // Test if client requests TCP transport, otherwise return 461 Transport not supported // This allows smart clients who initially requested UDP to fall back on TCP transport. - if strings.HasPrefix(tr, transport) { + if strings.HasPrefix(tr, "RTP/AVP/TCP") { c.session = core.RandString(8, 10) c.state = StateSetup @@ -163,13 +161,13 @@ func (c *Conn) Accept() error { c.Senders[i].Media.ID = MethodSetup interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) - // Check if transport already contains the 'interleaved' parameter - if strings.Contains(transport, "interleaved=") { + // Check if tr already contains the 'interleaved' parameter + if strings.Contains(tr, "interleaved=") { // If so, just update the interleaved value - res.Header.Set("Transport", strings.Replace(transport, "interleaved=[^;]*", "interleaved="+interleaved, 1)) + res.Header.Set("Transport", strings.Replace(tr, "interleaved=[^;]*", "interleaved="+interleaved, 1)) } else { // Otherwise, append the interleaved parameter - res.Header.Set("Transport", transport+";interleaved="+interleaved) + res.Header.Set("Transport", tr+";interleaved="+interleaved) } } else { res.Status = "400 Bad Request" From 261a936bb84a0e3ec82e94cb87c029a3edd0364d Mon Sep 17 00:00:00 2001 From: Xiaokui Shu Date: Wed, 25 Dec 2024 17:09:23 -0500 Subject: [PATCH 25/87] Add rtsp server failed auth logging --- internal/rtsp/rtsp.go | 6 +++++- pkg/rtsp/server.go | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 0fe135f8..0777219d 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -1,6 +1,7 @@ package rtsp import ( + "fmt" "io" "net" "net/url" @@ -237,7 +238,10 @@ func tcpHandler(conn *rtsp.Conn) { }) if err := conn.Accept(); err != nil { - if err != io.EOF { + if err == rtsp.FailedAuth { + rAddr := conn.Connection.RemoteAddr + log.Warn().Msg(fmt.Sprintf("[rtsp] failed authentication from %s", rAddr)) + } else if err != io.EOF { log.WithLevel(level).Err(err).Caller().Send() } if closer != nil { diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index c96125a2..9527e155 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -13,6 +13,8 @@ import ( "github.com/AlexxIT/go2rtc/pkg/tcp" ) +var FailedAuth = errors.New("failed authentication") + func NewServer(conn net.Conn) *Conn { return &Conn{ Connection: core.Connection{ @@ -54,7 +56,13 @@ func (c *Conn) Accept() error { if err = c.WriteResponse(res); err != nil { return err } - continue + if req.Header.Get("Authorization") != "" { + // eliminate false positive: ffmpeg sends first request without + // authorization header even if the user provides credentials + return FailedAuth + } else { + continue + } } // Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN From 8e4088e08f18edf2d375d339032ea2a3ba69f93f Mon Sep 17 00:00:00 2001 From: Timo Christeleit Date: Wed, 8 Jan 2025 11:03:17 +0100 Subject: [PATCH 26/87] fix tapo h200 + d230 doorbell stream --- pkg/tapo/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 6ccafe4e..78bf5fce 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -291,7 +291,7 @@ func dial(req *http.Request, brand, username, password string) (net.Conn, *http. if err != nil { return nil, nil, err } - _ = res.Body.Close() // ignore response body + _, _ = io.Copy(io.Discard, res.Body) // ignore response body auth := res.Header.Get("WWW-Authenticate") From 9e673559c4e725628613dfd831e4fc08d09906e1 Mon Sep 17 00:00:00 2001 From: Xiaokui Shu Date: Wed, 8 Jan 2025 21:31:37 -0500 Subject: [PATCH 27/87] Improve log formatting with Msgf --- internal/rtsp/rtsp.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 0777219d..2e1d04e8 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -1,7 +1,6 @@ package rtsp import ( - "fmt" "io" "net" "net/url" @@ -239,8 +238,7 @@ func tcpHandler(conn *rtsp.Conn) { if err := conn.Accept(); err != nil { if err == rtsp.FailedAuth { - rAddr := conn.Connection.RemoteAddr - log.Warn().Msg(fmt.Sprintf("[rtsp] failed authentication from %s", rAddr)) + log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication") } else if err != io.EOF { log.WithLevel(level).Err(err).Caller().Send() } From 2ca97a42c5769f57c0605ba433701d8bf4f43eff Mon Sep 17 00:00:00 2001 From: Timo Christeleit Date: Thu, 9 Jan 2025 09:44:23 +0100 Subject: [PATCH 28/87] Update pkg/tapo/client.go Co-authored-by: Sergey Vilgelm <523825+SVilgelm@users.noreply.github.com> --- pkg/tapo/client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 78bf5fce..c19267ff 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -291,7 +291,8 @@ func dial(req *http.Request, brand, username, password string) (net.Conn, *http. if err != nil { return nil, nil, err } - _, _ = io.Copy(io.Discard, res.Body) // ignore response body + _, _ = io.Copy(io.Discard, res.Body) // discard leftovers + _ = res.Body.Close() // ignore response body auth := res.Header.Get("WWW-Authenticate") From 3e3988a67f00f872606675dc1a73863efe534655 Mon Sep 17 00:00:00 2001 From: seydx Date: Sat, 25 Jan 2025 16:11:39 +0100 Subject: [PATCH 29/87] minor improvements --- pkg/ring/client.go | 5 ++--- pkg/ring/snapshot.go | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pkg/ring/client.go b/pkg/ring/client.go index 7014213d..4c473276 100644 --- a/pkg/ring/client.go +++ b/pkg/ring/client.go @@ -514,7 +514,6 @@ func (c *Client) Stop() error { if c.prod != nil { _ = c.prod.Stop() - c.prod = nil } if c.ws != nil { @@ -537,6 +536,6 @@ func (c *Client) MarshalJSON() ([]byte, error) { if webrtcProd, ok := c.prod.(*webrtc.Conn); ok { return webrtcProd.MarshalJSON() } - - return nil, errors.New("ring: can't marshal") + + return json.Marshal(c.prod) } diff --git a/pkg/ring/snapshot.go b/pkg/ring/snapshot.go index 84da0fd3..f64e4f79 100644 --- a/pkg/ring/snapshot.go +++ b/pkg/ring/snapshot.go @@ -20,6 +20,7 @@ func NewSnapshotProducer(client *RingRestClient, camera *CameraData) *SnapshotPr ID: core.NewID(), FormatName: "ring/snapshot", Protocol: "https", + RemoteAddr: "app-snaps.ring.com", Medias: []*core.Media{ { Kind: core.KindVideo, @@ -43,7 +44,7 @@ func (p *SnapshotProducer) Start() error { // Fetch snapshot response, err := p.client.Request("GET", fmt.Sprintf("https://app-snaps.ring.com/snapshots/next/%d", int(p.camera.ID)), nil) if err != nil { - return fmt.Errorf("failed to get snapshot: %w", err) + return err } pkt := &rtp.Packet{ @@ -51,10 +52,7 @@ func (p *SnapshotProducer) Start() error { Payload: response, } - // Send to all receivers - for _, receiver := range p.Receivers { - receiver.WriteRTP(pkt) - } + p.Receivers[0].WriteRTP(pkt) return nil } From 36547a7343ff80c5167cc739ebce6dc67a766e43 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 Jan 2025 16:09:50 +0300 Subject: [PATCH 30/87] Add support H264, H265, NV12 for V4L2 source #1546 --- pkg/h264/annexb/annexb_test.go | 12 ++++++++++++ pkg/v4l2/device/device.go | 26 +++++++++++++++++--------- pkg/v4l2/device/formats.go | 26 ++++++++++++++++++++++++-- pkg/v4l2/producer.go | 33 +++++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go index 7220f570..cbc382fe 100644 --- a/pkg/h264/annexb/annexb_test.go +++ b/pkg/h264/annexb/annexb_test.go @@ -83,3 +83,15 @@ func TestDahua(t *testing.T) { n := naluTypes(b) require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) } + +func TestUSB(t *testing.T) { + s := "00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) + + s = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C" + b = EncodeToAVCC(decode(s)) + n = naluTypes(b) + require.Equal(t, []byte{0x41}, n) +} diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7f16fd23..c77d60f5 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -11,8 +11,9 @@ import ( ) type Device struct { - fd int - bufs [][]byte + fd int + bufs [][]byte + pixFmt uint32 } func Open(path string) (*Device, error) { @@ -119,6 +120,8 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) } func (d *Device) SetFormat(width, height, pixFmt uint32) error { + d.pixFmt = pixFmt + f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, pix: v4l2_pix_format{ @@ -196,7 +199,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(planarYUV bool) ([]byte, error) { +func (d *Device) Capture() ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -205,11 +208,16 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - buf := make([]byte, dec.bytesused) - if planarYUV { - YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) - } else { - copy(buf, d.bufs[dec.index][:dec.bytesused]) + src := d.bufs[dec.index][:dec.bytesused] + dst := make([]byte, dec.bytesused) + + switch d.pixFmt { + case V4L2_PIX_FMT_YUYV: + YUYVtoYUV(dst, src) + case V4L2_PIX_FMT_NV12: + NV12toYUV(dst, src) + default: + copy(dst, d.bufs[dec.index][:dec.bytesused]) } enc := v4l2_buffer{ @@ -221,7 +229,7 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - return buf, nil + return dst, nil } func (d *Device) Close() error { diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index fb54bbd1..a0b41082 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -2,7 +2,10 @@ package device const ( V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_NV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24 V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 + V4L2_PIX_FMT_H264 = 'H' | '2'<<8 | '6'<<16 | '4'<<24 + V4L2_PIX_FMT_HEVC = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24 ) type Format struct { @@ -13,11 +16,13 @@ type Format struct { var Formats = []Format{ {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_NV12, "Y/UV 4:2:0", "nv12"}, {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, + {V4L2_PIX_FMT_H264, "H.264", "h264"}, + {V4L2_PIX_FMT_HEVC, "HEVC", "hevc"}, } -// YUYV2YUV convert packed YUV to planar YUV -func YUYV2YUV(dst, src []byte) { +func YUYVtoYUV(dst, src []byte) { n := len(src) i0 := 0 iy := 0 @@ -38,3 +43,20 @@ func YUYV2YUV(dst, src []byte) { iv++ } } + +func NV12toYUV(dst, src []byte) { + n := len(src) + k := n / 6 + i0 := k * 4 + iu := i0 + iv := i0 + k + copy(dst, src[:i0]) // copy Y + for i0 < n { + dst[iu] = src[i0] + i0++ + iu++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 87199762..663d0a9e 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" "github.com/AlexxIT/go2rtc/pkg/v4l2/device" "github.com/pion/rtp" ) @@ -46,17 +47,29 @@ func Open(rawURL string) (*Producer, error) { } switch query.Get("input_format") { - case "mjpeg": - codec.Name = core.CodecJPEG - pixFmt = device.V4L2_PIX_FMT_MJPEG case "yuyv422": if codec.FmtpLine == "" { return nil, errors.New("v4l2: invalid video_size") } - codec.Name = core.CodecRAW codec.FmtpLine += ";colorspace=422" pixFmt = device.V4L2_PIX_FMT_YUYV + case "nv12": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=420mpeg2" // maybe 420jpeg + pixFmt = device.V4L2_PIX_FMT_NV12 + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "h264": + codec.Name = core.CodecH264 + pixFmt = device.V4L2_PIX_FMT_H264 + case "hevc": + codec.Name = core.CodecH265 + pixFmt = device.V4L2_PIX_FMT_HEVC default: return nil, errors.New("v4l2: invalid input_format") } @@ -93,10 +106,14 @@ func (c *Producer) Start() error { return err } - planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + var bitstream bool + switch c.Medias[0].Codecs[0].Name { + case core.CodecH264, core.CodecH265: + bitstream = true + } for { - buf, err := c.dev.Capture(planarYUV) + buf, err := c.dev.Capture() if err != nil { return err } @@ -107,6 +124,10 @@ func (c *Producer) Start() error { continue } + if bitstream { + buf = annexb.EncodeToAVCC(buf) + } + pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, Payload: buf, From 9b392a22e1a19992fe9baa8ae4c8977d3a7fb78d Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 11:01:44 +0300 Subject: [PATCH 31/87] Ignore unknown NAL unit types for RTP/H264 #1570 --- pkg/h264/rtp.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index b4a9dafb..d093254f 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -22,7 +22,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { buf := make([]byte, 0, 512*1024) // 512K return func(packet *rtp.Packet) { - //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) payload, err := depack.Unmarshal(packet.Payload) if len(payload) == 0 || err != nil { @@ -68,6 +68,9 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { payload = payload[i:] continue + case NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass + default: + return // skip any unknown NAL unit type } break } From b14aa4f0dcf9cb2a577195b4abb529004adef40d Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 14:46:37 +0300 Subject: [PATCH 32/87] Improve delay for MSE player --- www/video-rtc.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/www/video-rtc.js b/www/video-rtc.js index 52fb5dda..fb872b45 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -439,24 +439,30 @@ export class VideoRTC extends HTMLElement { const sb = ms.addSourceBuffer(msg.value); sb.mode = 'segments'; // segments or sequence sb.addEventListener('updateend', () => { - if (sb.updating) return; - - try { - if (bufLen > 0) { + if (!sb.updating && bufLen > 0) { + try { const data = buf.slice(0, bufLen); - bufLen = 0; sb.appendBuffer(data); - } else if (sb.buffered && sb.buffered.length) { - const end = sb.buffered.end(sb.buffered.length - 1) - 15; - const start = sb.buffered.start(0); - if (end > start) { - sb.remove(start, end); - ms.setLiveSeekableRange(end, end + 15); - } - // console.debug("VideoRTC.buffered", start, end); + bufLen = 0; + } catch (e) { + // console.debug(e); } - } catch (e) { - // console.debug(e); + } + + if (!sb.updating && sb.buffered && sb.buffered.length) { + const end = sb.buffered.end(sb.buffered.length - 1); + const start = end - 5; + const start0 = sb.buffered.start(0); + if (start > start0) { + sb.remove(start0, start); + ms.setLiveSeekableRange(start, end); + } + if (this.video.currentTime < start) { + this.video.currentTime = start; + } + const gap = end - this.video.currentTime; + this.video.playbackRate = gap > 0.1 ? gap : 0.1; + // console.debug('VideoRTC.buffered', gap, this.video.playbackRate, this.video.readyState); } }); @@ -468,7 +474,7 @@ export class VideoRTC extends HTMLElement { const b = new Uint8Array(data); buf.set(b, bufLen); bufLen += b.byteLength; - // console.debug("VideoRTC.buffer", b.byteLength, bufLen); + // console.debug('VideoRTC.buffer', b.byteLength, bufLen); } else { try { sb.appendBuffer(data); From b139b8fdd6f69f716d5ac8421501d0c8b9ab6031 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 15:41:30 +0300 Subject: [PATCH 33/87] Add readme for V4L2 module --- internal/v4l2/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/v4l2/README.md diff --git a/internal/v4l2/README.md b/internal/v4l2/README.md new file mode 100644 index 00000000..1c5dd390 --- /dev/null +++ b/internal/v4l2/README.md @@ -0,0 +1,39 @@ +# V4L2 + +What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux): + +- V4L2 (Video for Linux API version 2) works only in Linux +- supports USB cameras and other similar devices +- one device can only be connected to one software simultaneously +- cameras support a fixed list of formats, resolutions and frame rates +- basic cameras supports only RAW (non-compressed) pixel formats +- regular cameras supports MJPEG format (series of JPEG frames) +- advances cameras support H264 format (MSE/MP4, WebRTC compatible) +- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage +- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage +- H265 (HEVC) format is also supported (if the camera supports it) + +Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600Ñ…1200 + 640Ñ…480 + 640Ñ…480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%. + +Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**. + +## RAW format + +Example: + +```yaml +streams: + camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10 +``` + +Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured. + +``` +ffplay http://localhost:1984/api/stream.mjpeg?src=camera1 +``` + +**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth. + +``` +ffplay http://localhost:1984/api/stream.y4m?src=camera1 +``` From ece49a158e0f326decbafc1a1172d38ad844ed96 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 26 Jan 2025 16:09:50 +0300 Subject: [PATCH 34/87] Add support H264, H265, NV12 for V4L2 source #1546 --- pkg/h264/annexb/annexb_test.go | 12 ++++++++++++ pkg/v4l2/device/device.go | 26 +++++++++++++++++--------- pkg/v4l2/device/formats.go | 26 ++++++++++++++++++++++++-- pkg/v4l2/producer.go | 33 +++++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/pkg/h264/annexb/annexb_test.go b/pkg/h264/annexb/annexb_test.go index 7220f570..cbc382fe 100644 --- a/pkg/h264/annexb/annexb_test.go +++ b/pkg/h264/annexb/annexb_test.go @@ -83,3 +83,15 @@ func TestDahua(t *testing.T) { n := naluTypes(b) require.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n) } + +func TestUSB(t *testing.T) { + s := "00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00" + b := EncodeToAVCC(decode(s)) + n := naluTypes(b) + require.Equal(t, []byte{0x67, 0x68, 0x65}, n) + + s = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C" + b = EncodeToAVCC(decode(s)) + n = naluTypes(b) + require.Equal(t, []byte{0x41}, n) +} diff --git a/pkg/v4l2/device/device.go b/pkg/v4l2/device/device.go index 7f16fd23..c77d60f5 100644 --- a/pkg/v4l2/device/device.go +++ b/pkg/v4l2/device/device.go @@ -11,8 +11,9 @@ import ( ) type Device struct { - fd int - bufs [][]byte + fd int + bufs [][]byte + pixFmt uint32 } func Open(path string) (*Device, error) { @@ -119,6 +120,8 @@ func (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) } func (d *Device) SetFormat(width, height, pixFmt uint32) error { + d.pixFmt = pixFmt + f := v4l2_format{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, pix: v4l2_pix_format{ @@ -196,7 +199,7 @@ func (d *Device) StreamOff() (err error) { return ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)) } -func (d *Device) Capture(planarYUV bool) ([]byte, error) { +func (d *Device) Capture() ([]byte, error) { dec := v4l2_buffer{ typ: V4L2_BUF_TYPE_VIDEO_CAPTURE, memory: V4L2_MEMORY_MMAP, @@ -205,11 +208,16 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - buf := make([]byte, dec.bytesused) - if planarYUV { - YUYV2YUV(buf, d.bufs[dec.index][:dec.bytesused]) - } else { - copy(buf, d.bufs[dec.index][:dec.bytesused]) + src := d.bufs[dec.index][:dec.bytesused] + dst := make([]byte, dec.bytesused) + + switch d.pixFmt { + case V4L2_PIX_FMT_YUYV: + YUYVtoYUV(dst, src) + case V4L2_PIX_FMT_NV12: + NV12toYUV(dst, src) + default: + copy(dst, d.bufs[dec.index][:dec.bytesused]) } enc := v4l2_buffer{ @@ -221,7 +229,7 @@ func (d *Device) Capture(planarYUV bool) ([]byte, error) { return nil, err } - return buf, nil + return dst, nil } func (d *Device) Close() error { diff --git a/pkg/v4l2/device/formats.go b/pkg/v4l2/device/formats.go index fb54bbd1..a0b41082 100644 --- a/pkg/v4l2/device/formats.go +++ b/pkg/v4l2/device/formats.go @@ -2,7 +2,10 @@ package device const ( V4L2_PIX_FMT_YUYV = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24 + V4L2_PIX_FMT_NV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24 V4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24 + V4L2_PIX_FMT_H264 = 'H' | '2'<<8 | '6'<<16 | '4'<<24 + V4L2_PIX_FMT_HEVC = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24 ) type Format struct { @@ -13,11 +16,13 @@ type Format struct { var Formats = []Format{ {V4L2_PIX_FMT_YUYV, "YUV 4:2:2", "yuyv422"}, + {V4L2_PIX_FMT_NV12, "Y/UV 4:2:0", "nv12"}, {V4L2_PIX_FMT_MJPEG, "Motion-JPEG", "mjpeg"}, + {V4L2_PIX_FMT_H264, "H.264", "h264"}, + {V4L2_PIX_FMT_HEVC, "HEVC", "hevc"}, } -// YUYV2YUV convert packed YUV to planar YUV -func YUYV2YUV(dst, src []byte) { +func YUYVtoYUV(dst, src []byte) { n := len(src) i0 := 0 iy := 0 @@ -38,3 +43,20 @@ func YUYV2YUV(dst, src []byte) { iv++ } } + +func NV12toYUV(dst, src []byte) { + n := len(src) + k := n / 6 + i0 := k * 4 + iu := i0 + iv := i0 + k + copy(dst, src[:i0]) // copy Y + for i0 < n { + dst[iu] = src[i0] + i0++ + iu++ + dst[iv] = src[i0] + i0++ + iv++ + } +} diff --git a/pkg/v4l2/producer.go b/pkg/v4l2/producer.go index 87199762..663d0a9e 100644 --- a/pkg/v4l2/producer.go +++ b/pkg/v4l2/producer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" "github.com/AlexxIT/go2rtc/pkg/v4l2/device" "github.com/pion/rtp" ) @@ -46,17 +47,29 @@ func Open(rawURL string) (*Producer, error) { } switch query.Get("input_format") { - case "mjpeg": - codec.Name = core.CodecJPEG - pixFmt = device.V4L2_PIX_FMT_MJPEG case "yuyv422": if codec.FmtpLine == "" { return nil, errors.New("v4l2: invalid video_size") } - codec.Name = core.CodecRAW codec.FmtpLine += ";colorspace=422" pixFmt = device.V4L2_PIX_FMT_YUYV + case "nv12": + if codec.FmtpLine == "" { + return nil, errors.New("v4l2: invalid video_size") + } + codec.Name = core.CodecRAW + codec.FmtpLine += ";colorspace=420mpeg2" // maybe 420jpeg + pixFmt = device.V4L2_PIX_FMT_NV12 + case "mjpeg": + codec.Name = core.CodecJPEG + pixFmt = device.V4L2_PIX_FMT_MJPEG + case "h264": + codec.Name = core.CodecH264 + pixFmt = device.V4L2_PIX_FMT_H264 + case "hevc": + codec.Name = core.CodecH265 + pixFmt = device.V4L2_PIX_FMT_HEVC default: return nil, errors.New("v4l2: invalid input_format") } @@ -93,10 +106,14 @@ func (c *Producer) Start() error { return err } - planarYUV := c.Medias[0].Codecs[0].Name == core.CodecRAW + var bitstream bool + switch c.Medias[0].Codecs[0].Name { + case core.CodecH264, core.CodecH265: + bitstream = true + } for { - buf, err := c.dev.Capture(planarYUV) + buf, err := c.dev.Capture() if err != nil { return err } @@ -107,6 +124,10 @@ func (c *Producer) Start() error { continue } + if bitstream { + buf = annexb.EncodeToAVCC(buf) + } + pkt := &rtp.Packet{ Header: rtp.Header{Timestamp: core.Now90000()}, Payload: buf, From 645c11f0bd8dc8308be566d0e5ab0af8a520b609 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 11:01:44 +0300 Subject: [PATCH 35/87] Ignore unknown NAL unit types for RTP/H264 #1570 --- pkg/h264/rtp.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/h264/rtp.go b/pkg/h264/rtp.go index b4a9dafb..d093254f 100644 --- a/pkg/h264/rtp.go +++ b/pkg/h264/rtp.go @@ -22,7 +22,7 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { buf := make([]byte, 0, 512*1024) // 512K return func(packet *rtp.Packet) { - //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) + //log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker) payload, err := depack.Unmarshal(packet.Payload) if len(payload) == 0 || err != nil { @@ -68,6 +68,9 @@ func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc { payload = payload[i:] continue + case NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass + default: + return // skip any unknown NAL unit type } break } From f9a8c1969c76aeb3840c4dbf643475df2081198b Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 14:46:37 +0300 Subject: [PATCH 36/87] Improve delay for MSE player --- www/video-rtc.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/www/video-rtc.js b/www/video-rtc.js index 52fb5dda..fb872b45 100644 --- a/www/video-rtc.js +++ b/www/video-rtc.js @@ -439,24 +439,30 @@ export class VideoRTC extends HTMLElement { const sb = ms.addSourceBuffer(msg.value); sb.mode = 'segments'; // segments or sequence sb.addEventListener('updateend', () => { - if (sb.updating) return; - - try { - if (bufLen > 0) { + if (!sb.updating && bufLen > 0) { + try { const data = buf.slice(0, bufLen); - bufLen = 0; sb.appendBuffer(data); - } else if (sb.buffered && sb.buffered.length) { - const end = sb.buffered.end(sb.buffered.length - 1) - 15; - const start = sb.buffered.start(0); - if (end > start) { - sb.remove(start, end); - ms.setLiveSeekableRange(end, end + 15); - } - // console.debug("VideoRTC.buffered", start, end); + bufLen = 0; + } catch (e) { + // console.debug(e); } - } catch (e) { - // console.debug(e); + } + + if (!sb.updating && sb.buffered && sb.buffered.length) { + const end = sb.buffered.end(sb.buffered.length - 1); + const start = end - 5; + const start0 = sb.buffered.start(0); + if (start > start0) { + sb.remove(start0, start); + ms.setLiveSeekableRange(start, end); + } + if (this.video.currentTime < start) { + this.video.currentTime = start; + } + const gap = end - this.video.currentTime; + this.video.playbackRate = gap > 0.1 ? gap : 0.1; + // console.debug('VideoRTC.buffered', gap, this.video.playbackRate, this.video.readyState); } }); @@ -468,7 +474,7 @@ export class VideoRTC extends HTMLElement { const b = new Uint8Array(data); buf.set(b, bufLen); bufLen += b.byteLength; - // console.debug("VideoRTC.buffer", b.byteLength, bufLen); + // console.debug('VideoRTC.buffer', b.byteLength, bufLen); } else { try { sb.appendBuffer(data); From 1b0db3c8b00449920c81d304f7de5b55745688ca Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 2 Feb 2025 15:41:30 +0300 Subject: [PATCH 37/87] Add readme for V4L2 module --- internal/v4l2/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/v4l2/README.md diff --git a/internal/v4l2/README.md b/internal/v4l2/README.md new file mode 100644 index 00000000..1c5dd390 --- /dev/null +++ b/internal/v4l2/README.md @@ -0,0 +1,39 @@ +# V4L2 + +What you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux): + +- V4L2 (Video for Linux API version 2) works only in Linux +- supports USB cameras and other similar devices +- one device can only be connected to one software simultaneously +- cameras support a fixed list of formats, resolutions and frame rates +- basic cameras supports only RAW (non-compressed) pixel formats +- regular cameras supports MJPEG format (series of JPEG frames) +- advances cameras support H264 format (MSE/MP4, WebRTC compatible) +- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage +- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage +- H265 (HEVC) format is also supported (if the camera supports it) + +Tests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600Ñ…1200 + 640Ñ…480 + 640Ñ…480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%. + +Supported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**. + +## RAW format + +Example: + +```yaml +streams: + camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10 +``` + +Go2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured. + +``` +ffplay http://localhost:1984/api/stream.mjpeg?src=camera1 +``` + +**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth. + +``` +ffplay http://localhost:1984/api/stream.y4m?src=camera1 +``` From ad8c025393e47454b59a398d7877360b9ed447ff Mon Sep 17 00:00:00 2001 From: seydx Date: Sun, 3 Nov 2024 16:33:08 +0100 Subject: [PATCH 38/87] Add backchannel support for rtsp server --- pkg/rtsp/server.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index df2ebdb5..b7e65dac 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -139,6 +139,16 @@ func (c *Conn) Accept() error { medias = append(medias, media) } + for i, track := range c.Receivers { + media := &core.Media{ + Kind: core.GetKind(track.Codec.Name), + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{track.Codec}, + ID: "trackID=" + strconv.Itoa(i+len(c.Senders)), + } + medias = append(medias, media) + } + res.Body, err = core.MarshalSDP(c.SessionName, medias) if err != nil { return err From c39c9aa1da7bc75a17d4d96dd779f51b5314805b Mon Sep 17 00:00:00 2001 From: Thomas Purchas Date: Thu, 6 Feb 2025 23:56:03 +0000 Subject: [PATCH 39/87] Handle malformed fmtp lines --- pkg/rtsp/helpers.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 346ecf73..3445b1d8 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -75,6 +75,23 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { if codec.FmtpLine == "" { codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions) } + case core.CodecH265: + if codec.FmtpLine != "" { + // All three parameters are needed for a valid fmtp line. If we're missing one + // then discard the entire line. The bitstream should contain the data in NAL units + // + // Some camera brands (notable Hikvision) don't include the vps property, rendering the entire + // line invalid, because the sps property references the non-existent vps proper. This invalid + // data will cause FFmpeg to crash with a `Could not write header (incorrect codec parameters ?): Invalid data found when processing input` + // error when attempting to repackage the HEVC stream into outgoing RTSP stream. Removing the + // fmtp line forces FFmpeg to rely on the bitstream directly, fixing this issue. + valid := strings.Contains(codec.FmtpLine, "sprop-vps=") + valid = valid && strings.Contains(codec.FmtpLine, "sprop-sps=") + valid = valid && strings.Contains(codec.FmtpLine, "sprop-pps=") + if !valid { + codec.FmtpLine = "" + } + } case core.CodecOpus: // fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587 codec.ClockRate = 48000 From da809bb9d74a846ac66715aef91a9badc1421f93 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 2 Feb 2025 22:15:19 +0000 Subject: [PATCH 40/87] Update build.yml Fix Build binaries This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3`. Learn more: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/ --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bb01ca7..0bc21d11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,14 +102,14 @@ jobs: env: { GOOS: freebsd, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_amd64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_amd64, path: go2rtc } - name: Build go2rtc_freebsd_arm64 env: { GOOS: freebsd, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_freebsd_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_freebsd_arm64, path: go2rtc } docker-master: From e935885cd346bb4e57e9c94ef37001d908a0a1bb Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Feb 2025 10:12:32 +0300 Subject: [PATCH 41/87] Update general H265 support for WebRTC #1439 --- pkg/webrtc/consumer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/webrtc/consumer.go b/pkg/webrtc/consumer.go index fb90442c..e9d7b2e5 100644 --- a/pkg/webrtc/consumer.go +++ b/pkg/webrtc/consumer.go @@ -56,11 +56,11 @@ func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiv } case core.CodecH265: - // SafariPay because it is the only browser in the world - // that supports WebRTC + H265 - sender.Handler = h265.SafariPay(1200, sender.Handler) + sender.Handler = h265.RTPPay(1200, sender.Handler) if track.Codec.IsRTP() { sender.Handler = h265.RTPDepay(track.Codec, sender.Handler) + } else { + sender.Handler = h265.RepairAVCC(track.Codec, sender.Handler) } case core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML: From be2864c34b42be22da73ab8a31d1a98f103843f2 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 17 Feb 2025 17:07:36 +0300 Subject: [PATCH 42/87] Code refactoring after #1588 --- pkg/rtsp/helpers.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 3445b1d8..952730bb 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -77,18 +77,11 @@ func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) { } case core.CodecH265: if codec.FmtpLine != "" { - // All three parameters are needed for a valid fmtp line. If we're missing one - // then discard the entire line. The bitstream should contain the data in NAL units - // - // Some camera brands (notable Hikvision) don't include the vps property, rendering the entire - // line invalid, because the sps property references the non-existent vps proper. This invalid - // data will cause FFmpeg to crash with a `Could not write header (incorrect codec parameters ?): Invalid data found when processing input` - // error when attempting to repackage the HEVC stream into outgoing RTSP stream. Removing the - // fmtp line forces FFmpeg to rely on the bitstream directly, fixing this issue. - valid := strings.Contains(codec.FmtpLine, "sprop-vps=") - valid = valid && strings.Contains(codec.FmtpLine, "sprop-sps=") - valid = valid && strings.Contains(codec.FmtpLine, "sprop-pps=") - if !valid { + // all three parameters are needed for a valid fmtp line + // https://github.com/AlexxIT/go2rtc/pull/1588 + if !strings.Contains(codec.FmtpLine, "sprop-vps=") || + !strings.Contains(codec.FmtpLine, "sprop-sps=") || + !strings.Contains(codec.FmtpLine, "sprop-pps=") { codec.FmtpLine = "" } } From 65c87d5e0f599c2698d0515b6f9efae0fa6587c6 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Mon, 17 Feb 2025 18:07:31 -0300 Subject: [PATCH 43/87] Fix typo in RTMP docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e87e35a0..d30768d0 100644 --- a/README.md +++ b/README.md @@ -881,7 +881,7 @@ Read more about [codecs filters](#codecs-filters). You can get any stream as RTMP-stream: `rtmp://192.168.1.123/{stream_name}`. Only H264/AAC codecs supported right now. -[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has differnt problems with this format. +[Incoming stream](#incoming-sources) in RTMP-format tested only with [OBS Studio](https://obsproject.com/) and Dahua camera. Different FFmpeg versions has different problems with this format. ```yaml rtmp: From 02ac3a681432aec38ac3dbf8dcbd8db1e2fee9f5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Feb 2025 12:01:55 +0300 Subject: [PATCH 44/87] Code refactoring for RTSP auth --- internal/rtsp/rtsp.go | 3 ++- pkg/rtsp/server.go | 7 +++---- pkg/tcp/auth.go | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 2e1d04e8..c680dd07 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -1,6 +1,7 @@ package rtsp import ( + "errors" "io" "net" "net/url" @@ -237,7 +238,7 @@ func tcpHandler(conn *rtsp.Conn) { }) if err := conn.Accept(); err != nil { - if err == rtsp.FailedAuth { + if errors.Is(err, rtsp.FailedAuth) { log.Warn().Str("remote_addr", conn.Connection.RemoteAddr).Msg("[rtsp] failed authentication") } else if err != io.EOF { log.WithLevel(level).Err(err).Caller().Send() diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index 9527e155..d7e89f5f 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -47,7 +47,7 @@ func (c *Conn) Accept() error { c.Fire(req) - if !c.auth.Validate(req) { + if valid, empty := c.auth.Validate(req); !valid { res := &tcp.Response{ Status: "401 Unauthorized", Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}}, @@ -56,13 +56,12 @@ func (c *Conn) Accept() error { if err = c.WriteResponse(res); err != nil { return err } - if req.Header.Get("Authorization") != "" { + if empty { // eliminate false positive: ffmpeg sends first request without // authorization header even if the user provides credentials - return FailedAuth - } else { continue } + return FailedAuth } // Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN diff --git a/pkg/tcp/auth.go b/pkg/tcp/auth.go index ac212fcf..3eb26024 100644 --- a/pkg/tcp/auth.go +++ b/pkg/tcp/auth.go @@ -85,14 +85,14 @@ func (a *Auth) Write(req *Request) { } } -func (a *Auth) Validate(req *Request) bool { +func (a *Auth) Validate(req *Request) (valid, empty bool) { if a == nil { - return true + return true, true } header := req.Header.Get("Authorization") if header == "" { - return false + return false, true } if a.Method == AuthUnknown { @@ -100,7 +100,7 @@ func (a *Auth) Validate(req *Request) bool { a.header = "Basic " + B64(a.user, a.pass) } - return header == a.header + return header == a.header, false } func (a *Auth) ReadNone(res *Response) bool { From 637e65e5a059defcdb9ce80eaffec00a01ae3d5d Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Feb 2025 12:49:33 +0300 Subject: [PATCH 45/87] Code refactoring for RTSP transport header processing --- pkg/rtsp/server.go | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index cefaef1d..29f97b5c 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -141,17 +141,14 @@ func (c *Conn) Accept() error { } case MethodSetup: - tr := req.Header.Get("Transport") - res := &tcp.Response{ Header: map[string][]string{}, Request: req, } // Test if client requests TCP transport, otherwise return 461 Transport not supported - // This allows smart clients who initially requested UDP to fall back on TCP transport. - if strings.HasPrefix(tr, "RTP/AVP/TCP") { - + // This allows smart clients who initially requested UDP to fall back on TCP transport + if tr := req.Header.Get("Transport"); strings.HasPrefix(tr, "RTP/AVP/TCP") { c.session = core.RandString(8, 10) c.state = StateSetup @@ -159,16 +156,8 @@ func (c *Conn) Accept() error { if i := reqTrackID(req); i >= 0 && i < len(c.Senders) { // mark sender as SETUP c.Senders[i].Media.ID = MethodSetup - interleaved := fmt.Sprintf("%d-%d", i*2, i*2+1) - - // Check if tr already contains the 'interleaved' parameter - if strings.Contains(tr, "interleaved=") { - // If so, just update the interleaved value - res.Header.Set("Transport", strings.Replace(tr, "interleaved=[^;]*", "interleaved="+interleaved, 1)) - } else { - // Otherwise, append the interleaved parameter - res.Header.Set("Transport", tr+";interleaved="+interleaved) - } + tr = fmt.Sprintf("RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1) + res.Header.Set("Transport", tr) } else { res.Status = "400 Bad Request" } From b34d970076de676b0cc69b7c8716b8b2df4a00aa Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 18 Feb 2025 11:52:57 +0100 Subject: [PATCH 46/87] remove duplicated code --- pkg/rtsp/server.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index b7e65dac..df2ebdb5 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -139,16 +139,6 @@ func (c *Conn) Accept() error { medias = append(medias, media) } - for i, track := range c.Receivers { - media := &core.Media{ - Kind: core.GetKind(track.Codec.Name), - Direction: core.DirectionSendonly, - Codecs: []*core.Codec{track.Codec}, - ID: "trackID=" + strconv.Itoa(i+len(c.Senders)), - } - medias = append(medias, media) - } - res.Body, err = core.MarshalSDP(c.SessionName, medias) if err != nil { return err From 0a773c82aff1bc8a5cc2c058f845b14b7ca34f3e Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 18 Feb 2025 16:59:00 +0300 Subject: [PATCH 47/87] Code refactoring for RTSP backchannel --- internal/rtsp/rtsp.go | 18 ++++++++---------- pkg/rtsp/producer.go | 41 ++++++++++++++++++----------------------- pkg/rtsp/server.go | 18 ++++++------------ 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/internal/rtsp/rtsp.go b/internal/rtsp/rtsp.go index 5c023b71..4c9ca162 100644 --- a/internal/rtsp/rtsp.go +++ b/internal/rtsp/rtsp.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" - "github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/tcp" @@ -186,11 +185,11 @@ func tcpHandler(conn *rtsp.Conn) { } } - if query.Get("backchannel") == "1" { - conn.Medias = append(conn.Medias, &core.Media{ - Kind: core.KindAudio, - Direction: core.DirectionRecvonly, - Codecs: []*core.Codec{ + if query.Get("backchannel") == "1" { + conn.Medias = append(conn.Medias, &core.Media{ + Kind: core.KindAudio, + Direction: core.DirectionRecvonly, + Codecs: []*core.Codec{ {Name: core.CodecOpus, ClockRate: 48000, Channels: 2}, {Name: core.CodecPCM, ClockRate: 16000}, {Name: core.CodecPCMA, ClockRate: 16000}, @@ -198,10 +197,9 @@ func tcpHandler(conn *rtsp.Conn) { {Name: core.CodecPCM, ClockRate: 8000}, {Name: core.CodecPCMA, ClockRate: 8000}, {Name: core.CodecPCMU, ClockRate: 8000}, - {Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"}, - }, - }) - } + }, + }) + } if s := query.Get("pkt_size"); s != "" { conn.PacketSize = uint16(core.Atoi(s)) diff --git a/pkg/rtsp/producer.go b/pkg/rtsp/producer.go index 323d9197..3d818b62 100644 --- a/pkg/rtsp/producer.go +++ b/pkg/rtsp/producer.go @@ -16,43 +16,38 @@ func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, e } } - switch c.mode { - case core.ModeActiveProducer: - c.stateMu.Lock() - defer c.stateMu.Unlock() + c.stateMu.Lock() + defer c.stateMu.Unlock() + var channel byte + + switch c.mode { + case core.ModeActiveProducer: if c.state == StatePlay { if err := c.Reconnect(); err != nil { return nil, err } } - channel, err := c.SetupMedia(media) + var err error + channel, err = c.SetupMedia(media) if err != nil { return nil, err } c.state = StateSetup + case core.ModePassiveConsumer: + // Backchannel + channel = byte(len(c.Senders)) * 2 + default: + return nil, errors.New("rtsp: wrong mode for GetTrack") + } - track := core.NewReceiver(media, codec) - track.ID = channel - c.Receivers = append(c.Receivers, track) + track := core.NewReceiver(media, codec) + track.ID = channel + c.Receivers = append(c.Receivers, track) - return track, nil - case core.ModePassiveConsumer: - // Backchannel - c.stateMu.Lock() - defer c.stateMu.Unlock() - - channel := byte(len(c.Senders)) * 2 - track := core.NewReceiver(media, codec) - track.ID = channel - c.Receivers = append(c.Receivers, track) - - return track, nil - default: - return nil, errors.New("rtsp: wrong mode for GetTrack") - } + return track, nil } func (c *Conn) Start() (err error) { diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index df2ebdb5..f4aea614 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -164,20 +164,14 @@ func (c *Conn) Accept() error { c.state = StateSetup if c.mode == core.ModePassiveConsumer { - trackID := reqTrackID(req) - - if trackID >= 0 { - if trackID < len(c.Senders) { - c.Senders[trackID].Media.ID = MethodSetup - tr = fmt.Sprintf("%d-%d", trackID*2, trackID*2+1) - res.Header.Set("Transport", transport+tr) - } else if trackID >= len(c.Senders) && trackID < len(c.Senders)+len(c.Receivers) { - c.Receivers[trackID-len(c.Senders)].Media.ID = MethodSetup - tr = fmt.Sprintf("%d-%d", trackID*2, trackID*2+1) - res.Header.Set("Transport", transport+tr) + if i := reqTrackID(req); i >= 0 && i < len(c.Senders)+len(c.Receivers) { + if i < len(c.Senders) { + c.Senders[i].Media.ID = MethodSetup } else { - res.Status = "400 Bad Request" + c.Receivers[i-len(c.Senders)].Media.ID = MethodSetup } + tr = fmt.Sprintf("%d-%d", i*2, i*2+1) + res.Header.Set("Transport", transport+tr) } else { res.Status = "400 Bad Request" } From 1abb3c8c22ab20fd483c8628ccedb938f4ff7c73 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 22 Feb 2025 11:39:32 +0300 Subject: [PATCH 48/87] Code refactoring for Nest RTSP source --- pkg/nest/api.go | 18 ++++++------------ pkg/nest/client.go | 19 +++++-------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 9e32cbcf..4ca1e8b8 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -120,21 +120,11 @@ func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) { devices := make([]DeviceInfo, 0, len(resv.Devices)) for _, device := range resv.Devices { + // only RTSP and WEB_RTC available (both supported) if len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 { continue } - supported := false - for _, protocol := range device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols { - if protocol == "WEB_RTC" || protocol == "RTSP" { - supported = true - break - } - } - if !supported { - continue - } - i := strings.LastIndexByte(device.Name, '/') if i <= 0 { continue @@ -146,7 +136,11 @@ func (a *API) GetDevices(projectID string) ([]DeviceInfo, error) { name = device.ParentRelations[0].DisplayName } - devices = append(devices, DeviceInfo{Name: name, DeviceID: device.Name[i+1:], Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols}) + devices = append(devices, DeviceInfo{ + Name: name, + DeviceID: device.Name[i+1:], + Protocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols, + }) } return devices, nil diff --git a/pkg/nest/client.go b/pkg/nest/client.go index e692359c..93c4ce64 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -33,12 +33,6 @@ func Dial(rawURL string) (core.Producer, error) { refreshToken := query.Get("refresh_token") projectID := query.Get("project_id") deviceID := query.Get("device_id") - protocols := strings.Split(query.Get("protocols"), ",") - - // Default to WEB_RTC for backwards compataiility - if len(protocols) == 0 { - protocols = append(protocols, "WEB_RTC") - } if cliendID == "" || cliendSecret == "" || refreshToken == "" || projectID == "" || deviceID == "" { return nil, errors.New("nest: wrong query") @@ -49,16 +43,13 @@ func Dial(rawURL string) (core.Producer, error) { return nil, err } - // Pick the first supported protocol in order of priority (WEB_RTC, RTSP) - for _, proto := range protocols { - if proto == "WEB_RTC" { - return rtcConn(nestAPI, rawURL, projectID, deviceID) - } else if proto == "RTSP" { - return rtspConn(nestAPI, rawURL, projectID, deviceID) - } + protocols := strings.Split(query.Get("protocols"), ",") + if len(protocols) > 0 && protocols[0] == "RTSP" { + return rtspConn(nestAPI, rawURL, projectID, deviceID) } - return nil, errors.New("nest: unsupported camera") + // Default to WEB_RTC for backwards compataiility + return rtcConn(nestAPI, rawURL, projectID, deviceID) } func (c *WebRTCClient) GetMedias() []*core.Media { From 6fb59949a24ddd782dbbdebcdc2381a0105fc96e Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 23 Feb 2025 20:56:48 +0300 Subject: [PATCH 49/87] Rewrite exec handler --- internal/exec/closer.go | 39 ------------------------ internal/exec/exec.go | 60 +++++++++++++++++++++++-------------- pkg/shell/command.go | 59 ++++++++++++++++++++++++++++++++++++ pkg/shell/procattr.go | 7 +++++ pkg/shell/procattr_linux.go | 6 ++++ pkg/stdin/backchannel.go | 6 +--- pkg/stdin/client.go | 7 ++--- 7 files changed, 113 insertions(+), 71 deletions(-) delete mode 100644 internal/exec/closer.go create mode 100644 pkg/shell/command.go create mode 100644 pkg/shell/procattr.go create mode 100644 pkg/shell/procattr_linux.go diff --git a/internal/exec/closer.go b/internal/exec/closer.go deleted file mode 100644 index 66d0e3ac..00000000 --- a/internal/exec/closer.go +++ /dev/null @@ -1,39 +0,0 @@ -package exec - -import ( - "errors" - "net/url" - "os" - "os/exec" - "syscall" - "time" - - "github.com/AlexxIT/go2rtc/pkg/core" -) - -// closer support custom killsignal with custom killtimeout -type closer struct { - cmd *exec.Cmd - query url.Values -} - -func (c *closer) Close() (err error) { - sig := os.Kill - if s := c.query.Get("killsignal"); s != "" { - sig = syscall.Signal(core.Atoi(s)) - } - - log.Trace().Msgf("[exec] kill with signal=%d", sig) - err = c.cmd.Process.Signal(sig) - - if s := c.query.Get("killtimeout"); s != "" { - timeout := time.Duration(core.Atoi(s)) * time.Second - timer := time.AfterFunc(timeout, func() { - log.Trace().Msgf("[exec] kill after timeout=%s", s) - _ = c.cmd.Process.Kill() - }) - defer timer.Stop() // stop timer if Wait ends before timeout - } - - return errors.Join(err, c.cmd.Wait()) -} diff --git a/internal/exec/exec.go b/internal/exec/exec.go index bce166e8..89add393 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -9,9 +9,9 @@ import ( "io" "net/url" "os" - "os/exec" "strings" "sync" + "syscall" "time" "github.com/AlexxIT/go2rtc/internal/app" @@ -49,7 +49,7 @@ func Init() { log = app.GetLogger("exec") } -func execHandle(rawURL string) (core.Producer, error) { +func execHandle(rawURL string) (prod core.Producer, err error) { rawURL, rawQuery, _ := strings.Cut(rawURL, "#") query := streams.ParseQuery(rawQuery) @@ -67,39 +67,55 @@ func execHandle(rawURL string) (core.Producer, error) { rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:] } - args := shell.QuoteSplit(rawURL[5:]) // remove `exec:` - cmd := exec.Command(args[0], args[1:]...) + cmd := shell.NewCommand(rawURL[5:]) // remove `exec:` cmd.Stderr = &logWriter{ buf: make([]byte, 512), debug: log.Debug().Enabled(), } + if s := query.Get("killsignal"); s != "" { + sig := syscall.Signal(core.Atoi(s)) + cmd.Cancel = func() error { + log.Debug().Msgf("[exec] kill with signal=%d", sig) + return cmd.Process.Signal(sig) + } + } + + if s := query.Get("killtimeout"); s != "" { + cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second + } + if query.Get("backchannel") == "1" { return stdin.NewClient(cmd) } - cl := &closer{cmd: cmd, query: query} - if path == "" { - return handlePipe(rawURL, cmd, cl) + prod, err = handlePipe(rawURL, cmd) + } else { + prod, err = handleRTSP(rawURL, cmd, path) } - return handleRTSP(rawURL, cmd, cl, path) + if err != nil { + _ = cmd.Close() + } + + return } -func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) { +func handlePipe(source string, cmd *shell.Command) (core.Producer, error) { stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } - rc := struct { + rd := struct { io.Reader io.Closer }{ // add buffer for pipe reader to reduce syscall bufio.NewReaderSize(stdout, core.BufferSize), - cl, + // stop cmd on close pipe call + cmd, } log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe") @@ -110,9 +126,8 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro return nil, err } - prod, err := magic.Open(rc) + prod, err := magic.Open(rd) if err != nil { - _ = rc.Close() return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr) } @@ -126,7 +141,7 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro return prod, nil } -func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) { +func handleRTSP(source string, cmd *shell.Command, path string) (core.Producer, error) { if log.Trace().Enabled() { cmd.Stdout = os.Stdout } @@ -152,23 +167,22 @@ func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.P return nil, err } - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() + timeout := time.NewTimer(30 * time.Second) + defer timeout.Stop() select { - case <-time.After(time.Minute): + case <-timeout.C: + // haven't received data from app in timeout log.Error().Str("source", source).Msg("[exec] timeout") - _ = cl.Close() return nil, errors.New("exec: timeout") - case <-done: - // limit message size + case <-cmd.Done(): + // app fail before we receive any data return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr) case prod := <-waiter: + // app started successfully log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp") setRemoteInfo(prod, source, cmd.Args) - prod.OnClose = cl.Close + prod.OnClose = cmd.Close return prod, nil } } diff --git a/pkg/shell/command.go b/pkg/shell/command.go new file mode 100644 index 00000000..b7c81899 --- /dev/null +++ b/pkg/shell/command.go @@ -0,0 +1,59 @@ +package shell + +import ( + "context" + "os/exec" +) + +// Command like exec.Cmd, but with support: +// - io.Closer interface +// - Wait from multiple places +// - Done channel +type Command struct { + *exec.Cmd + ctx context.Context + cancel context.CancelFunc + err error +} + +func NewCommand(s string) *Command { + ctx, cancel := context.WithCancel(context.Background()) + args := QuoteSplit(s) + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + cmd.SysProcAttr = procAttr + return &Command{cmd, ctx, cancel, nil} +} + +func (c *Command) Start() error { + if err := c.Cmd.Start(); err != nil { + return err + } + + go func() { + c.err = c.Cmd.Wait() + c.cancel() // release context resources + }() + + return nil +} + +func (c *Command) Wait() error { + <-c.ctx.Done() + return c.err +} + +func (c *Command) Run() error { + if err := c.Start(); err != nil { + return err + } + return c.Wait() +} + +func (c *Command) Done() <-chan struct{} { + return c.ctx.Done() +} + +func (c *Command) Close() error { + c.cancel() + return nil +} diff --git a/pkg/shell/procattr.go b/pkg/shell/procattr.go new file mode 100644 index 00000000..fffdc2a4 --- /dev/null +++ b/pkg/shell/procattr.go @@ -0,0 +1,7 @@ +//go:build !linux + +package shell + +import "syscall" + +var procAttr *syscall.SysProcAttr diff --git a/pkg/shell/procattr_linux.go b/pkg/shell/procattr_linux.go new file mode 100644 index 00000000..cef1d152 --- /dev/null +++ b/pkg/shell/procattr_linux.go @@ -0,0 +1,6 @@ +package shell + +import "syscall" + +// will stop child if parent died (even with SIGKILL) +var procAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM} diff --git a/pkg/stdin/backchannel.go b/pkg/stdin/backchannel.go index b9a4a6d4..b154a291 100644 --- a/pkg/stdin/backchannel.go +++ b/pkg/stdin/backchannel.go @@ -2,7 +2,6 @@ package stdin import ( "encoding/json" - "errors" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/pion/rtp" @@ -42,10 +41,7 @@ func (c *Client) Stop() (err error) { if c.sender != nil { c.sender.Close() } - if c.cmd.Process == nil { - return nil - } - return errors.Join(c.cmd.Process.Kill(), c.cmd.Wait()) + return c.cmd.Close() } func (c *Client) MarshalJSON() ([]byte, error) { diff --git a/pkg/stdin/client.go b/pkg/stdin/client.go index 09e525ad..a77d4459 100644 --- a/pkg/stdin/client.go +++ b/pkg/stdin/client.go @@ -1,21 +1,20 @@ package stdin import ( - "os/exec" - "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/shell" ) // Deprecated: should be rewritten to core.Connection type Client struct { - cmd *exec.Cmd + cmd *shell.Command medias []*core.Media sender *core.Sender send int } -func NewClient(cmd *exec.Cmd) (*Client, error) { +func NewClient(cmd *shell.Command) (*Client, error) { c := &Client{ cmd: cmd, medias: []*core.Media{ From b881c52118b46bc9c1503a21a2beb9e08cf6405c Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 12:44:09 +0300 Subject: [PATCH 50/87] Code refactoring for FreeBSD binaries --- .gitignore | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 04ae894a..52fe9c86 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ go2rtc.yaml go2rtc.json +go2rtc_freebsd* go2rtc_linux* go2rtc_mac* go2rtc_win* diff --git a/README.md b/README.md index 21e760a6..926b046b 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2 - `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks)) - `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit - `go2rtc_mac_arm64.zip` - macOS ARM 64-bit -- `go2rtc_freebsd_amd64.zip` - FreeBSD Intel 64-bit +- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit - `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac. From 7fd0ec8ce677e30dd4fb4fdb40cc4ae51b1ceed4 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 15:21:37 +0300 Subject: [PATCH 51/87] Code refactoring for logs to file --- README.md | 1 - internal/app/log.go | 19 ++++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index faabfa6b..c31ed748 100644 --- a/README.md +++ b/README.md @@ -1213,7 +1213,6 @@ log: rtsp: warn streams: error webrtc: fatal - output: stdout # Available output options are: stdout, stderr, or a file path. ``` ## Security diff --git a/internal/app/log.go b/internal/app/log.go index dd29ad06..713110e8 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -3,6 +3,7 @@ package app import ( "io" "os" + "strings" "github.com/mattn/go-isatty" "github.com/rs/zerolog" @@ -19,25 +20,17 @@ var MemoryLog = newBuffer(16) func NewLogger(config map[string]string) zerolog.Logger { var writer io.Writer - switch config["output"] { + switch output, path, _ := strings.Cut(config["output"], ":"); output { case "stderr": writer = os.Stderr case "stdout": writer = os.Stdout case "file": - filePath := config["file"] - if filePath == "" { - filePath = "go2rtc.log" + if path == "" { + path = "go2rtc.log" } - file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - os.Stdout.WriteString("Error: Failed to open log file: " + err.Error() + ". Log output is set to stdout now.\n") - writer = os.Stdout - } else { - writer = file - } - default: - writer = os.Stdout + // if fail - only MemoryLog will be available + writer, _ = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) } timeFormat := config["time"] From 4b4a1644ff06566ebb4c67492bba6dee8c5c2446 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 16:21:12 +0300 Subject: [PATCH 52/87] Code refactoring for network view --- www/network.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/www/network.html b/www/network.html index 180c9711..79875012 100644 --- a/www/network.html +++ b/www/network.html @@ -58,18 +58,16 @@ const viewPosition = network.getViewPosition(); const scale = network.getScale(); const selectedNodes = network.getSelectedNodes(); - const selectedEdges = network.getSelectedEdges(); network.setData(data); - network.selectNodes(selectedNodes); - network.selectEdges(selectedEdges); - for (const nodeId in positions) { network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y); } network.moveTo({position: viewPosition, scale: scale}); + + network.selectNodes(selectedNodes); } } catch (error) { console.error('Error fetching or updating network data:', error); From 6ee52474e1957bb8785f7d2f77d718e75b313e94 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 18:13:16 +0300 Subject: [PATCH 53/87] Code refactoring for panic: send on closed channel --- pkg/core/track.go | 21 +++++++---------- pkg/core/track_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 pkg/core/track_test.go diff --git a/pkg/core/track.go b/pkg/core/track.go index b585fa98..d3f1467d 100644 --- a/pkg/core/track.go +++ b/pkg/core/track.go @@ -3,6 +3,7 @@ package core import ( "encoding/json" "errors" + "github.com/pion/rtp" ) @@ -70,9 +71,8 @@ type Sender struct { Packets int `json:"packets,omitempty"` Drops int `json:"drops,omitempty"` - buf chan *Packet - done chan struct{} - isClosed bool + buf chan *Packet + done chan struct{} } func NewSender(media *Media, codec *Codec) *Sender { @@ -99,11 +99,6 @@ func NewSender(media *Media, codec *Codec) *Sender { s.Input = func(packet *Packet) { // writing to nil chan - OK, writing to closed chan - panic s.mu.Lock() - if s.isClosed { - s.Drops++ - s.mu.Unlock() - return - } select { case s.buf <- packet: s.Bytes += len(packet.Payload) @@ -145,6 +140,7 @@ func (s *Sender) Start() { s.done = make(chan struct{}) go func() { + // for range on nil chan is OK for packet := range s.buf { s.Output(packet) } @@ -153,7 +149,7 @@ func (s *Sender) Start() { } func (s *Sender) Wait() { - if done := s.done; s.done != nil { + if done := s.done; done != nil { <-done } } @@ -171,10 +167,9 @@ func (s *Sender) State() string { func (s *Sender) Close() { // close buffer if exists s.mu.Lock() - if buf := s.buf; buf != nil && !s.isClosed { - s.isClosed = true - s.buf = nil - defer close(buf) + if s.buf != nil { + close(s.buf) // exit from for range loop + s.buf = nil // prevent writing to closed chan } s.mu.Unlock() diff --git a/pkg/core/track_test.go b/pkg/core/track_test.go new file mode 100644 index 00000000..cf877d49 --- /dev/null +++ b/pkg/core/track_test.go @@ -0,0 +1,53 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSenser(t *testing.T) { + recv := make(chan *Packet) // blocking receiver + + sender := NewSender(nil, &Codec{}) + sender.Output = func(packet *Packet) { + recv <- packet + } + require.Equal(t, "new", sender.State()) + + sender.Start() + require.Equal(t, "connected", sender.State()) + + sender.Input(&Packet{}) + sender.Input(&Packet{}) + + require.Equal(t, 2, sender.Packets) + require.Equal(t, 0, sender.Drops) + + // important to read one before close + // because goroutine in Start() can run with nil chan + // it's OK in real life, but bad for test + _, ok := <-recv + require.True(t, ok) + + sender.Close() + require.Equal(t, "closed", sender.State()) + + sender.Input(&Packet{}) + + require.Equal(t, 2, sender.Packets) + require.Equal(t, 1, sender.Drops) + + // read 2nd + _, ok = <-recv + require.True(t, ok) + + // read 3rd + select { + case <-recv: + ok = true + default: + ok = false + } + require.False(t, ok) +} From 90544ba71318a420e89529f1457b6fa0aac26148 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 21:02:33 +0300 Subject: [PATCH 54/87] Fix panic for concurrent streams map read and map write #1612 --- internal/homekit/api.go | 8 +++--- internal/homekit/homekit.go | 5 ++-- internal/onvif/onvif.go | 4 +-- internal/streams/stream.go | 5 ++-- internal/streams/streams.go | 51 ++++++++++++++++++++++++------------- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/internal/homekit/api.go b/internal/homekit/api.go index abd8e97c..0ee4d057 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -112,7 +112,7 @@ func apiUnpair(id string) error { return errors.New(api.StreamNotFound) } - rawURL := findHomeKitURL(stream) + rawURL := findHomeKitURL(stream.Sources()) if rawURL == "" { return errors.New("not homekit source") } @@ -128,10 +128,10 @@ func apiUnpair(id string) error { func findHomeKitURLs() map[string]*url.URL { urls := map[string]*url.URL{} - for id, stream := range streams.Streams() { - if rawURL := findHomeKitURL(stream); rawURL != "" { + for name, sources := range streams.GetAllSources() { + if rawURL := findHomeKitURL(sources); rawURL != "" { if u, err := url.Parse(rawURL); err == nil { - urls[id] = u + urls[name] = u } } } diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index 743aeab9..632607d4 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -79,7 +79,7 @@ func Init() { Handler: homekit.ServerHandler(srv), } - if url := findHomeKitURL(stream); url != "" { + if url := findHomeKitURL(stream.Sources()); url != "" { // 1. Act as transparent proxy for HomeKit camera dial := func() (net.Conn, error) { client, err := homekit.Dial(url, srtp.Server) @@ -186,8 +186,7 @@ func hapPairVerify(w http.ResponseWriter, r *http.Request) { } } -func findHomeKitURL(stream *streams.Stream) string { - sources := stream.Sources() +func findHomeKitURL(sources []string) string { if len(sources) == 0 { return "" } diff --git a/internal/onvif/onvif.go b/internal/onvif/onvif.go index d332ca38..0d0319a7 100644 --- a/internal/onvif/onvif.go +++ b/internal/onvif/onvif.go @@ -99,11 +99,11 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { }) case onvif.MediaGetVideoSources: - b = onvif.GetVideoSourcesResponse(streams.GetAll()) + b = onvif.GetVideoSourcesResponse(streams.GetAllNames()) case onvif.MediaGetProfiles: // important for Hass: H264 codec, width, height - b = onvif.GetProfilesResponse(streams.GetAll()) + b = onvif.GetProfilesResponse(streams.GetAllNames()) case onvif.MediaGetProfile: token := onvif.FindTagValue(b, "ProfileToken") diff --git a/internal/streams/stream.go b/internal/streams/stream.go index 569e63ee..984c73ed 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -47,11 +47,12 @@ func NewStream(source any) *Stream { } } -func (s *Stream) Sources() (sources []string) { +func (s *Stream) Sources() []string { + sources := make([]string, 0, len(s.producers)) for _, prod := range s.producers { sources = append(sources, prod.url) } - return + return sources } func (s *Stream) SetSource(source string) { diff --git a/internal/streams/streams.go b/internal/streams/streams.go index b1038423..dcbaba28 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -42,10 +42,6 @@ func Init() { }) } -func Get(name string) *Stream { - return streams[name] -} - var sanitize = regexp.MustCompile(`\s`) // Validate - not allow creating dynamic streams with spaces in the source @@ -68,6 +64,7 @@ func New(name string, sources ...string) *Stream { streamsMu.Lock() streams[name] = stream streamsMu.Unlock() + return stream } @@ -124,7 +121,7 @@ func GetOrPatch(query url.Values) *Stream { } // check if src is stream name - if stream, ok := streams[source]; ok { + if stream := Get(source); stream != nil { return stream } @@ -139,21 +136,41 @@ func GetOrPatch(query url.Values) *Stream { return Patch(source, source) } -func GetAll() (names []string) { +var log zerolog.Logger + +// streams map + +var streams = map[string]*Stream{} +var streamsMu sync.Mutex + +func Get(name string) *Stream { + streamsMu.Lock() + defer streamsMu.Unlock() + return streams[name] +} + +func Delete(name string) { + streamsMu.Lock() + defer streamsMu.Unlock() + delete(streams, name) +} + +func GetAllNames() []string { + streamsMu.Lock() + names := make([]string, 0, len(streams)) for name := range streams { names = append(names, name) } - return + streamsMu.Unlock() + return names } -func Streams() map[string]*Stream { - return streams +func GetAllSources() map[string][]string { + streamsMu.Lock() + sources := make(map[string][]string, len(streams)) + for name, stream := range streams { + sources[name] = stream.Sources() + } + streamsMu.Unlock() + return sources } - -func Delete(id string) { - delete(streams, id) -} - -var log zerolog.Logger -var streams = map[string]*Stream{} -var streamsMu sync.Mutex From 45b223a2efd27afaaaaf6920c72302fcd6e47798 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 21:55:10 +0300 Subject: [PATCH 55/87] Fix panic on reading nil TLV8 #1507 --- pkg/hap/camera/accessory_test.go | 9 +++++++++ pkg/hap/character.go | 13 ++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/hap/camera/accessory_test.go b/pkg/hap/camera/accessory_test.go index e9894f0f..3f5dcd71 100644 --- a/pkg/hap/camera/accessory_test.go +++ b/pkg/hap/camera/accessory_test.go @@ -2,12 +2,21 @@ package camera import ( "encoding/base64" + "strings" "testing" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/stretchr/testify/require" ) +func TestNilCharacter(t *testing.T) { + var res SetupEndpoints + char := &hap.Character{} + err := char.ReadTLV8(&res) + require.NotNil(t, err) + require.NotNil(t, strings.Contains(err.Error(), "can't read value")) +} + type testTLV8 struct { name string value string diff --git a/pkg/hap/character.go b/pkg/hap/character.go index 4cd8857c..afa321e2 100644 --- a/pkg/hap/character.go +++ b/pkg/hap/character.go @@ -3,6 +3,7 @@ package hap import ( "bytes" "encoding/json" + "fmt" "io" "net/http" @@ -126,11 +127,17 @@ func (c *Character) Write(v any) (err error) { // ReadTLV8 value to right struct func (c *Character) ReadTLV8(v any) (err error) { - return tlv8.UnmarshalBase64(c.Value.(string), v) + if s, ok := c.Value.(string); ok { + return tlv8.UnmarshalBase64(s, v) + } + return fmt.Errorf("hap: can't read value: %v", v) } -func (c *Character) ReadBool() bool { - return c.Value.(bool) +func (c *Character) ReadBool() (bool, error) { + if v, ok := c.Value.(bool); ok { + return v, nil + } + return false, fmt.Errorf("hap: can't read value: %v", c.Value) } func (c *Character) String() string { From effff6f88d1bf08168a49b63939cbcd148c23a96 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 24 Feb 2025 22:04:14 +0300 Subject: [PATCH 56/87] Fix concurrent SRTP sessions map read and map write #1489 --- pkg/srtp/server.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/srtp/server.go b/pkg/srtp/server.go index 0632375f..1e396f2e 100644 --- a/pkg/srtp/server.go +++ b/pkg/srtp/server.go @@ -65,6 +65,13 @@ func (s *Server) DelSession(session *Session) { s.mu.Unlock() } +func (s *Server) GetSession(ssrc uint32) (session *Session) { + s.mu.Lock() + session = s.sessions[ssrc] + s.mu.Unlock() + return +} + func (s *Server) handle() error { b := make([]byte, 2048) for { @@ -80,14 +87,14 @@ func (s *Server) handle() error { case 99, 110, 0x80 | 99, 0x80 | 110: // this is default position for SSRC in RTP packet ssrc := binary.BigEndian.Uint32(b[8:]) - if session, ok := s.sessions[ssrc]; ok { + if session := s.GetSession(ssrc); session != nil { session.ReadRTP(b[:n]) } case 200, 201, 202, 203, 204, 205, 206, 207: // this is default position for SSRC in RTCP packet ssrc := binary.BigEndian.Uint32(b[4:]) - if session, ok := s.sessions[ssrc]; ok { + if session := s.GetSession(ssrc); session != nil { session.ReadRTCP(b[:n]) } } From c50738005d715c5dc27de2aec332d7f26eb86d35 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 25 Feb 2025 16:16:38 +0300 Subject: [PATCH 57/87] Update mDNS server handler --- pkg/mdns/client.go | 41 ++++++---- pkg/mdns/server.go | 197 +++++++++++++++++++++++---------------------- 2 files changed, 126 insertions(+), 112 deletions(-) diff --git a/pkg/mdns/client.go b/pkg/mdns/client.go index a9ea0f1a..6816f919 100644 --- a/pkg/mdns/client.go +++ b/pkg/mdns/client.go @@ -13,7 +13,10 @@ import ( "github.com/miekg/dns" // awesome library for parsing mDNS records ) -const ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol +const ( + ServiceDNSSD = "_services._dns-sd._udp.local." + ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol +) type ServiceEntry struct { Name string `json:"name,omitempty"` @@ -153,6 +156,7 @@ type Browser struct { Service string Addr net.Addr + Nets []*net.IPNet Recv net.PacketConn Sends []net.PacketConn @@ -165,7 +169,7 @@ type Browser struct { // Receiver will get multicast responses on senders requests. func (b *Browser) ListenMulticastUDP() error { // 1. Collect IPv4 interfaces - ip4s, err := InterfacesIP4() + nets, err := IPNets() if err != nil { return err } @@ -182,11 +186,12 @@ func (b *Browser) ListenMulticastUDP() error { ctx := context.Background() - for _, ip4 := range ip4s { - conn, err := lc1.ListenPacket(ctx, "udp4", ip4.String()+":5353") // same port important + for _, ipn := range nets { + conn, err := lc1.ListenPacket(ctx, "udp4", ipn.IP.String()+":5353") // same port important if err != nil { continue } + b.Nets = append(b.Nets, ipn) b.Sends = append(b.Sends, conn) } @@ -365,35 +370,39 @@ func NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) { return } -func InterfacesIP4() ([]net.IP, error) { +// Common docker addresses (class B): +// https://en.wikipedia.org/wiki/Private_network +// - docker0 172.17.0.1/16 +// - br-xxxx 172.18.0.1/16 +// - hassio 172.30.32.1/23 +var docker = net.IPNet{ + IP: []byte{172, 16, 0, 0}, + Mask: []byte{255, 240, 0, 0}, +} + +func IPNets() ([]*net.IPNet, error) { intfs, err := net.Interfaces() if err != nil { return nil, err } - var ips []net.IP + var nets []*net.IPNet -loop: for _, intf := range intfs { if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 { continue } - addrs, err := intf.Addrs() - if err != nil { - continue - } - + addrs, _ := intf.Addrs() for _, addr := range addrs { switch v := addr.(type) { case *net.IPNet: - if ip := v.IP.To4(); ip != nil { - ips = append(ips, ip) - continue loop + if ip := v.IP.To4(); ip != nil && !docker.Contains(ip) { + nets = append(nets, v) } } } } - return ips, nil + return nets, nil } diff --git a/pkg/mdns/server.go b/pkg/mdns/server.go index ec31886e..802c07cd 100644 --- a/pkg/mdns/server.go +++ b/pkg/mdns/server.go @@ -20,7 +20,11 @@ func Serve(service string, entries []*ServiceEntry) error { } func (b *Browser) Serve(entries []*ServiceEntry) error { - var msg dns.Msg + names := make(map[string]*ServiceEntry, len(entries)) + for _, entry := range entries { + name := entry.name() + "." + b.Service + names[name] = entry + } buf := make([]byte, 1500) for { @@ -29,129 +33,130 @@ func (b *Browser) Serve(entries []*ServiceEntry) error { break } - if err = msg.Unpack(buf[:n]); err != nil { + var req dns.Msg // request + if err = req.Unpack(buf[:n]); err != nil { continue } - if !HasQuestionPTP(&msg, b.Service) { + // skip messages without Questions + if req.Question == nil { continue } remoteIP := addr.(*net.UDPAddr).IP - localIP := MatchLocalIP(remoteIP) + localIP := b.MatchLocalIP(remoteIP) + + // skip messages from unknown networks (can be docker network) if localIP == nil { continue } - answer, err := NewDNSAnswer(entries, b.Service, localIP).Pack() + var res dns.Msg // response + for _, q := range req.Question { + if q.Qtype != dns.TypePTR || q.Qclass != dns.ClassINET { + continue + } + + if q.Name == ServiceDNSSD { + AppendDNSSD(&res, b.Service) + } else if q.Name == b.Service { + for _, entry := range entries { + AppendEntry(&res, entry, b.Service, localIP) + } + } else if entry, ok := names[q.Name]; ok { + AppendEntry(&res, entry, b.Service, localIP) + } + } + + if res.Answer == nil { + continue + } + + res.MsgHdr.Response = true + res.MsgHdr.Authoritative = true + + data, err := res.Pack() if err != nil { continue } for _, send := range b.Sends { - _, _ = send.WriteTo(answer, MulticastAddr) + _, _ = send.WriteTo(data, MulticastAddr) } } return nil } -func HasQuestionPTP(msg *dns.Msg, name string) bool { - for _, q := range msg.Question { - if q.Qtype == dns.TypePTR && q.Name == name { - return true +func (b *Browser) MatchLocalIP(remote net.IP) net.IP { + for _, ipn := range b.Nets { + if ipn.Contains(remote) { + return ipn.IP } } - return false + return nil } -func NewDNSAnswer(entries []*ServiceEntry, service string, ip net.IP) *dns.Msg { - msg := dns.Msg{ - MsgHdr: dns.MsgHdr{ - Response: true, - Authoritative: true, +func AppendDNSSD(msg *dns.Msg, service string) { + msg.Answer = append( + msg.Answer, + &dns.PTR{ + Hdr: dns.RR_Header{ + Name: ServiceDNSSD, // _services._dns-sd._udp.local. + Rrtype: dns.TypePTR, // 12 + Class: dns.ClassINET, // 1 + Ttl: 4500, + }, + Ptr: service, // _home-assistant._tcp.local. }, - } - - for _, entry := range entries { - ptrName := entry.name() + "." + service - srvName := entry.name() + ".local." - - msg.Answer = append( - msg.Answer, - &dns.PTR{ - Hdr: dns.RR_Header{ - Name: service, - Rrtype: dns.TypePTR, - Class: dns.ClassINET, - Ttl: 4500, - }, - Ptr: ptrName, - }, - ) - msg.Extra = append( - msg.Extra, - &dns.TXT{ - Hdr: dns.RR_Header{ - Name: ptrName, - Rrtype: dns.TypeTXT, - Class: ClassCacheFlush, - Ttl: 4500, - }, - Txt: entry.TXT(), - }, - &dns.SRV{ - Hdr: dns.RR_Header{ - Name: ptrName, - Rrtype: dns.TypeSRV, - Class: ClassCacheFlush, - Ttl: 120, - Rdlength: 0, - }, - Port: entry.Port, - Target: srvName, - }, - &dns.A{ - Hdr: dns.RR_Header{ - Name: srvName, - Rrtype: dns.TypeA, - Class: ClassCacheFlush, - Ttl: 120, - Rdlength: 0, - }, - A: ip, - }, - ) - } - - return &msg + ) } -func MatchLocalIP(remote net.IP) net.IP { - intfs, err := net.Interfaces() - if err != nil { - return nil - } +func AppendEntry(msg *dns.Msg, entry *ServiceEntry, service string, ip net.IP) { + ptrName := entry.name() + "." + service + srvName := entry.name() + ".local." - for _, intf := range intfs { - if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 { - continue - } - - addrs, err := intf.Addrs() - if err != nil { - continue - } - - for _, addr := range addrs { - switch v := addr.(type) { - case *net.IPNet: - if local := v.IP.To4(); local != nil && v.Contains(remote) { - return local - } - } - } - } - - return nil + msg.Answer = append( + msg.Answer, + &dns.PTR{ + Hdr: dns.RR_Header{ + Name: service, // _home-assistant._tcp.local. + Rrtype: dns.TypePTR, // 12 + Class: dns.ClassINET, // 1 + Ttl: 4500, + }, + Ptr: ptrName, // Home\ Assistant._home-assistant._tcp.local. + }, + ) + msg.Extra = append( + msg.Extra, + &dns.TXT{ + Hdr: dns.RR_Header{ + Name: ptrName, // Home\ Assistant._home-assistant._tcp.local. + Rrtype: dns.TypeTXT, // 16 + Class: ClassCacheFlush, // 32769 + Ttl: 4500, + }, + Txt: entry.TXT(), + }, + &dns.SRV{ + Hdr: dns.RR_Header{ + Name: ptrName, // Home\ Assistant._home-assistant._tcp.local. + Rrtype: dns.TypeSRV, // 33 + Class: ClassCacheFlush, // 32769 + Ttl: 120, + }, + Port: entry.Port, // 8123 + Target: srvName, // 963f1fa82b7142809711cebe7c826322.local. + }, + &dns.A{ + Hdr: dns.RR_Header{ + Name: srvName, // 963f1fa82b7142809711cebe7c826322.local. + Rrtype: dns.TypeA, // 1 + Class: ClassCacheFlush, // 32769 + Ttl: 120, + }, + A: ip, + }, + ) } From 7d37f645ba667d536263f0c7e1aa058880034886 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 25 Feb 2025 16:42:18 +0300 Subject: [PATCH 58/87] Improved limited HomeKit server support for open source projects --- internal/homekit/homekit.go | 54 ++++++++++++++++++++----------------- pkg/hap/server_pairing.go | 15 +++++++++++ 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index 632607d4..b4237211 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -118,8 +118,8 @@ func Init() { servers[host] = srv } - api.HandleFunc(hap.PathPairSetup, hapPairSetup) - api.HandleFunc(hap.PathPairVerify, hapPairVerify) + api.HandleFunc(hap.PathPairSetup, hapHandler) + api.HandleFunc(hap.PathPairVerify, hapHandler) log.Trace().Msgf("[homekit] mdns: %s", entries) @@ -148,32 +148,19 @@ func streamHandler(rawURL string) (core.Producer, error) { return client, err } -func hapPairSetup(w http.ResponseWriter, r *http.Request) { - srv, ok := servers[r.Host] - if !ok { - log.Error().Msg("[homekit] unknown host: " + r.Host) - return +func resolve(host string) *server { + if len(servers) == 1 { + for _, srv := range servers { + return srv + } } - - conn, rw, err := w.(http.Hijacker).Hijack() - if err != nil { - return - } - - defer conn.Close() - - if err = srv.hap.PairSetup(r, rw, conn); err != nil { - log.Error().Err(err).Caller().Send() + if srv, ok := servers[host]; ok { + return srv } + return nil } -func hapPairVerify(w http.ResponseWriter, r *http.Request) { - srv, ok := servers[r.Host] - if !ok { - log.Error().Msg("[homekit] unknown host: " + r.Host) - return - } - +func hapHandler(w http.ResponseWriter, r *http.Request) { conn, rw, err := w.(http.Hijacker).Hijack() if err != nil { return @@ -181,7 +168,24 @@ func hapPairVerify(w http.ResponseWriter, r *http.Request) { defer conn.Close() - if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF { + // Can support multiple HomeKit cameras on single port ONLY for Apple devices. + // Doesn't support Home Assistant and any other open source projects + // because they don't send the host header in requests. + srv := resolve(r.Host) + if srv == nil { + log.Error().Msg("[homekit] unknown host: " + r.Host) + _ = hap.WriteBackoff(rw) + return + } + + switch r.RequestURI { + case hap.PathPairSetup: + err = srv.hap.PairSetup(r, rw, conn) + case hap.PathPairVerify: + err = srv.hap.PairVerify(r, rw, conn) + } + + if err != nil && err != io.EOF { log.Error().Err(err).Caller().Send() } } diff --git a/pkg/hap/server_pairing.go b/pkg/hap/server_pairing.go index 31d2f626..77895c10 100644 --- a/pkg/hap/server_pairing.go +++ b/pkg/hap/server_pairing.go @@ -235,3 +235,18 @@ func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []b } return w.Flush() } + +func WriteBackoff(rw *bufio.ReadWriter) error { + plainM2 := struct { + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + }{ + State: StateM2, + Error: 3, // BackoffError + } + body, err := tlv8.Marshal(plainM2) + if err != nil { + return err + } + return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body) +} From e304f4f34fee3edc0a2f526df507ede97540c007 Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 25 Feb 2025 12:11:06 +0300 Subject: [PATCH 59/87] Add support creality format for webrtc client #1600 --- internal/webrtc/client.go | 2 + internal/webrtc/client_creality.go | 110 +++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 internal/webrtc/client_creality.go diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index d42c51dd..a5af8bb6 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -54,6 +54,8 @@ func streamsHandler(rawURL string) (core.Producer, error) { } else if format == "wyze" { // https://github.com/mrlt8/docker-wyze-bridge return wyzeClient(rawURL) + } else if format == "creality" { + return crealityClient(rawURL) } else { return whepClient(rawURL) } diff --git a/internal/webrtc/client_creality.go b/internal/webrtc/client_creality.go new file mode 100644 index 00000000..0a3685a9 --- /dev/null +++ b/internal/webrtc/client_creality.go @@ -0,0 +1,110 @@ +package webrtc + +import ( + "encoding/base64" + "encoding/json" + "io" + "net/http" + "strings" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/webrtc" +) + +// https://github.com/AlexxIT/go2rtc/issues/1600 +func crealityClient(url string) (core.Producer, error) { + pc, err := PeerConnection(true) + if err != nil { + return nil, err + } + + prod := webrtc.NewConn(pc) + prod.FormatName = "webrtc/creality" + prod.Mode = core.ModeActiveProducer + prod.Protocol = "http" + prod.URL = url + + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + } + + // TODO: return webrtc.SessionDescription + offer, err := prod.CreateCompleteOffer(medias) + if err != nil { + return nil, err + } + + body, err := offerToB64(offer) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "plain/text") + + // TODO: change http.DefaultClient settings + client := http.Client{Timeout: time.Second * 5000} + defer client.CloseIdleConnections() + + res, err := client.Do(req) + if err != nil { + return nil, err + } + + answer, err := answerFromB64(res.Body) + if err != nil { + return nil, err + } + + if err = prod.SetAnswer(answer); err != nil { + return nil, err + } + + return prod, nil +} + +func offerToB64(sdp string) (io.Reader, error) { + // JS object + v := map[string]string{ + "type": "offer", + "sdp": sdp, + } + + // bytes + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + + // base64, why? who knows... + s := base64.StdEncoding.EncodeToString(b) + + return strings.NewReader(s), nil +} + +func answerFromB64(r io.Reader) (string, error) { + // base64 + b, err := io.ReadAll(r) + if err != nil { + return "", err + } + + // bytes + if b, err = base64.StdEncoding.DecodeString(string(b)); err != nil { + return "", err + } + + // JS object + var v map[string]string + if err = json.Unmarshal(b, &v); err != nil { + return "", err + } + + // string "v=0..." + return v["sdp"], nil +} From 71173da5ad61028c2d8494192e8259b1fcf0ea3e Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 26 Feb 2025 15:52:04 +0300 Subject: [PATCH 60/87] Add useful links to webrtc readme --- internal/webrtc/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/webrtc/README.md b/internal/webrtc/README.md index 4fb0072f..1dfe5569 100644 --- a/internal/webrtc/README.md +++ b/internal/webrtc/README.md @@ -11,7 +11,9 @@ If an external connection via STUN is used: - Uses [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology to bypass NAT even if you not open your server to the World - For about 20% of users, the techology will not work because of the [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/) -- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate +- UDP is not suitable for transmitting 2K and 4K high bitrate video over open networks because of the high loss rate: + - https://habr.com/ru/companies/flashphoner/articles/480006/ + - https://www.youtube.com/watch?v=FXVg2ckuKfs ## Default config From 5cf2ac4c3e39cb604b8ec0b52b849b9918beaef5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 26 Feb 2025 17:00:05 +0300 Subject: [PATCH 61/87] Fix escape quotes for DOT format #1603 --- internal/streams/dot.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/streams/dot.go b/internal/streams/dot.go index c54a733a..e0417972 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -171,5 +171,6 @@ func (c *conn) label() string { if c.UserAgent != "" { sb.WriteString("\nuser_agent=" + c.UserAgent) } - return sb.String() + // escape quotes https://github.com/AlexxIT/go2rtc/issues/1603 + return strings.ReplaceAll(sb.String(), `"`, `'`) } From 2a5355b1f8c0fe99a5b92f127f5adacaf8bef571 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 26 Feb 2025 21:34:46 +0300 Subject: [PATCH 62/87] Fix WebRTC server with static UDP port --- pkg/mdns/client.go | 13 ++--------- pkg/net2/net.go | 32 +++++++++++++++++++++++++++ pkg/webrtc/api.go | 55 ++++++++++++++++++++++++++++++++-------------- 3 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 pkg/net2/net.go diff --git a/pkg/mdns/client.go b/pkg/mdns/client.go index 6816f919..0e74952f 100644 --- a/pkg/mdns/client.go +++ b/pkg/mdns/client.go @@ -10,6 +10,7 @@ import ( "syscall" "time" + "github.com/AlexxIT/go2rtc/pkg/net2" "github.com/miekg/dns" // awesome library for parsing mDNS records ) @@ -370,16 +371,6 @@ func NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) { return } -// Common docker addresses (class B): -// https://en.wikipedia.org/wiki/Private_network -// - docker0 172.17.0.1/16 -// - br-xxxx 172.18.0.1/16 -// - hassio 172.30.32.1/23 -var docker = net.IPNet{ - IP: []byte{172, 16, 0, 0}, - Mask: []byte{255, 240, 0, 0}, -} - func IPNets() ([]*net.IPNet, error) { intfs, err := net.Interfaces() if err != nil { @@ -397,7 +388,7 @@ func IPNets() ([]*net.IPNet, error) { for _, addr := range addrs { switch v := addr.(type) { case *net.IPNet: - if ip := v.IP.To4(); ip != nil && !docker.Contains(ip) { + if ip := v.IP.To4(); ip != nil && !net2.Docker.Contains(ip) { nets = append(nets, v) } } diff --git a/pkg/net2/net.go b/pkg/net2/net.go new file mode 100644 index 00000000..6d02ad89 --- /dev/null +++ b/pkg/net2/net.go @@ -0,0 +1,32 @@ +package net2 + +import ( + "net" + "strconv" +) + +// Docker has common docker addresses (class B): +// https://en.wikipedia.org/wiki/Private_network +// - docker0 172.17.0.1/16 +// - br-xxxx 172.18.0.1/16 +// - hassio 172.30.32.1/23 +var Docker = net.IPNet{ + IP: []byte{172, 16, 0, 0}, + Mask: []byte{255, 240, 0, 0}, +} + +// ParseUnspecifiedPort will return port if address is unspecified +// ex. ":8555" or "0.0.0.0:8555" +func ParseUnspecifiedPort(address string) int { + host, port, err := net.SplitHostPort(address) + if err != nil { + return 0 + } + + if host != "" && host != "0.0.0.0" && host != "[::]" { + return 0 + } + + i, _ := strconv.Atoi(port) + return i +} diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index 0361e6b4..7fb68af7 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -4,6 +4,8 @@ import ( "net" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/net2" + "github.com/pion/ice/v2" "github.com/pion/interceptor" "github.com/pion/webrtc/v3" ) @@ -44,39 +46,44 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error // fix https://github.com/pion/webrtc/pull/2407 s.SetDTLSInsecureSkipHelloVerify(true) + var interfaceFilter func(name string) bool if filters != nil && filters.Interfaces != nil { - s.SetIncludeLoopbackCandidate(true) - s.SetInterfaceFilter(func(name string) bool { + interfaceFilter = func(name string) bool { return core.Contains(filters.Interfaces, name) - }) + } } else { - // disable listen on Hassio docker interfaces - s.SetInterfaceFilter(func(name string) bool { - return name != "hassio" && name != "docker0" - }) + // default interfaces - all, except loopback } + s.SetInterfaceFilter(interfaceFilter) + var ipFilter func(ip net.IP) bool if filters != nil && filters.IPs != nil { - s.SetIncludeLoopbackCandidate(true) - s.SetIPFilter(func(ip net.IP) bool { + ipFilter = func(ip net.IP) bool { return core.Contains(filters.IPs, ip.String()) - }) + } + } else { + // default ips - all, except loopback and docker + ipFilter = func(ip net.IP) bool { + return !net2.Docker.Contains(ip) + } } + s.SetIPFilter(ipFilter) + var networkTypes []webrtc.NetworkType if filters != nil && filters.Networks != nil { - var networkTypes []webrtc.NetworkType for _, s := range filters.Networks { if networkType, err := webrtc.NewNetworkType(s); err == nil { networkTypes = append(networkTypes, networkType) } } - s.SetNetworkTypes(networkTypes) } else { - s.SetNetworkTypes([]webrtc.NetworkType{ + // default network types - all + networkTypes = []webrtc.NetworkType{ webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6, webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6, - }) + } } + s.SetNetworkTypes(networkTypes) if filters != nil && len(filters.UDPPorts) == 2 { _ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1]) @@ -100,10 +107,24 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error } if network == "" || network == "udp" { - if ln, err := net.ListenPacket("udp", address); err == nil { - udpMux := webrtc.NewICEUDPMux(nil, ln) - s.SetICEUDPMux(udpMux) + // UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead + var udpMux ice.UDPMux + if port := net2.ParseUnspecifiedPort(address); port != 0 { + var networks []ice.NetworkType + for _, ntype := range networkTypes { + networks = append(networks, ice.NetworkType(ntype)) + } + + udpMux, _ = ice.NewMultiUDPMuxFromPort( + port, + ice.UDPMuxFromPortWithInterfaceFilter(interfaceFilter), + ice.UDPMuxFromPortWithIPFilter(ipFilter), + ice.UDPMuxFromPortWithNetworks(networks...), + ) + } else if ln, err := net.ListenPacket("udp", address); err == nil { + udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln}) } + s.SetICEUDPMux(udpMux) } } From 858c04bacf2f71337f46cd075fc6c89c6056b575 Mon Sep 17 00:00:00 2001 From: Alex X Date: Wed, 26 Feb 2025 21:39:42 +0300 Subject: [PATCH 63/87] Fix situation when WebRTC candidate pair changes multiple times #1282 --- pkg/webrtc/conn.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/webrtc/conn.go b/pkg/webrtc/conn.go index 5bc16ede..0845bdda 100644 --- a/pkg/webrtc/conn.go +++ b/pkg/webrtc/conn.go @@ -51,6 +51,10 @@ func NewConn(pc *webrtc.PeerConnection) *Conn { } pc.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange( func(pair *webrtc.ICECandidatePair) { + // fix situation when candidate pair changes multiple times + if i := strings.IndexByte(c.Protocol, '+'); i > 0 { + c.Protocol = c.Protocol[:i] + } c.Protocol += "+" + pair.Remote.Protocol.String() c.RemoteAddr = fmt.Sprintf( "%s:%d %s", sanitizeIP6(pair.Remote.Address), pair.Remote.Port, pair.Remote.Typ, From 934d43b52505df6c9f64228010921421fcdfee6b Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 27 Feb 2025 14:30:56 +0300 Subject: [PATCH 64/87] Update WebRTC server operation in closed docker containers --- internal/webrtc/candidates.go | 6 ++++++ internal/webrtc/webrtc.go | 2 +- pkg/mdns/client.go | 33 ++++----------------------------- pkg/webrtc/api.go | 18 ++++++++++++++---- pkg/{net2 => xnet}/net.go | 34 +++++++++++++++++++++++++++++++++- 5 files changed, 58 insertions(+), 35 deletions(-) rename pkg/{net2 => xnet}/net.go (50%) diff --git a/internal/webrtc/candidates.go b/internal/webrtc/candidates.go index adbfb4a7..a15c4e7d 100644 --- a/internal/webrtc/candidates.go +++ b/internal/webrtc/candidates.go @@ -7,6 +7,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" + "github.com/AlexxIT/go2rtc/pkg/xnet" pion "github.com/pion/webrtc/v3" ) @@ -73,6 +74,11 @@ func FilterCandidate(candidate *pion.ICECandidate) bool { return false } + // remove any Docker-like IP from candidates + if ip := net.ParseIP(candidate.Address); ip != nil && xnet.Docker.Contains(ip) { + return false + } + // host candidate should be in the hosts list if candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil { if !core.Contains(filters.Candidates, candidate.Address) { diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index fe25c919..989600f9 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -24,7 +24,7 @@ func Init() { } `yaml:"webrtc"` } - cfg.Mod.Listen = ":8555/tcp" + cfg.Mod.Listen = ":8555" cfg.Mod.IceServers = []pion.ICEServer{ {URLs: []string{"stun:stun.l.google.com:19302"}}, } diff --git a/pkg/mdns/client.go b/pkg/mdns/client.go index 0e74952f..e7abb50d 100644 --- a/pkg/mdns/client.go +++ b/pkg/mdns/client.go @@ -10,7 +10,7 @@ import ( "syscall" "time" - "github.com/AlexxIT/go2rtc/pkg/net2" + "github.com/AlexxIT/go2rtc/pkg/xnet" "github.com/miekg/dns" // awesome library for parsing mDNS records ) @@ -170,7 +170,9 @@ type Browser struct { // Receiver will get multicast responses on senders requests. func (b *Browser) ListenMulticastUDP() error { // 1. Collect IPv4 interfaces - nets, err := IPNets() + nets, err := xnet.IPNets(func(ip net.IP) bool { + return !xnet.Docker.Contains(ip) + }) if err != nil { return err } @@ -370,30 +372,3 @@ func NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) { return } - -func IPNets() ([]*net.IPNet, error) { - intfs, err := net.Interfaces() - if err != nil { - return nil, err - } - - var nets []*net.IPNet - - for _, intf := range intfs { - if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 { - continue - } - - addrs, _ := intf.Addrs() - for _, addr := range addrs { - switch v := addr.(type) { - case *net.IPNet: - if ip := v.IP.To4(); ip != nil && !net2.Docker.Contains(ip) { - nets = append(nets, v) - } - } - } - } - - return nets, nil -} diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index 7fb68af7..013a2f25 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -4,7 +4,7 @@ import ( "net" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/net2" + "github.com/AlexxIT/go2rtc/pkg/xnet" "github.com/pion/ice/v2" "github.com/pion/interceptor" "github.com/pion/webrtc/v3" @@ -20,6 +20,7 @@ func NewAPI() (*webrtc.API, error) { type Filters struct { Candidates []string `yaml:"candidates"` + Loopback bool `yaml:"loopback"` Interfaces []string `yaml:"interfaces"` IPs []string `yaml:"ips"` Networks []string `yaml:"networks"` @@ -46,6 +47,10 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error // fix https://github.com/pion/webrtc/pull/2407 s.SetDTLSInsecureSkipHelloVerify(true) + if filters != nil && filters.Loopback { + s.SetIncludeLoopbackCandidate(true) + } + var interfaceFilter func(name string) bool if filters != nil && filters.Interfaces != nil { interfaceFilter = func(name string) bool { @@ -62,9 +67,14 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error return core.Contains(filters.IPs, ip.String()) } } else { - // default ips - all, except loopback and docker + // try filter all Docker-like interfaces ipFilter = func(ip net.IP) bool { - return !net2.Docker.Contains(ip) + return !xnet.Docker.Contains(ip) + } + // if there are no such interfaces - disable the filter + // the user will need to enable port forwarding + if nets, _ := xnet.IPNets(ipFilter); len(nets) == 0 { + ipFilter = nil } } s.SetIPFilter(ipFilter) @@ -109,7 +119,7 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error if network == "" || network == "udp" { // UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead var udpMux ice.UDPMux - if port := net2.ParseUnspecifiedPort(address); port != 0 { + if port := xnet.ParseUnspecifiedPort(address); port != 0 { var networks []ice.NetworkType for _, ntype := range networkTypes { networks = append(networks, ice.NetworkType(ntype)) diff --git a/pkg/net2/net.go b/pkg/xnet/net.go similarity index 50% rename from pkg/net2/net.go rename to pkg/xnet/net.go index 6d02ad89..16361503 100644 --- a/pkg/net2/net.go +++ b/pkg/xnet/net.go @@ -1,4 +1,4 @@ -package net2 +package xnet import ( "net" @@ -30,3 +30,35 @@ func ParseUnspecifiedPort(address string) int { i, _ := strconv.Atoi(port) return i } + +func IPNets(ipFilter func(ip net.IP) bool) ([]*net.IPNet, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + var nets []*net.IPNet + + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + + addrs, _ := iface.Addrs() // range on nil slice is OK + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + ip := v.IP.To4() + if ip == nil { + continue + } + if ipFilter != nil && !ipFilter(ip) { + continue + } + nets = append(nets, v) + } + } + } + + return nets, nil +} From ae896002019022db6cb872caf59d9f35a045c48c Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 27 Feb 2025 15:01:05 +0300 Subject: [PATCH 65/87] Fix WebUI editor after Save --- www/editor.html | 1 + 1 file changed, 1 insertion(+) diff --git a/www/editor.html b/www/editor.html index fdb56213..cb455f4d 100644 --- a/www/editor.html +++ b/www/editor.html @@ -46,6 +46,7 @@ r = await fetch('api/config', {method: 'POST', body: editor.getValue()}); if (r.ok) { alert('OK'); + dump = editor.getValue(); await fetch('api/restart', {method: 'POST'}); } else { alert(await r.text()); From 8d70233d83d6c26df9dbe44b3da719b677e9066e Mon Sep 17 00:00:00 2001 From: Daniel Ruggeri Date: Thu, 27 Feb 2025 06:35:30 -0600 Subject: [PATCH 66/87] Correct slight syntax error in example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce8ac05e..65ebf2b7 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,7 @@ streams: mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264 # [RTSP] video with rotation, should be transcoded, so select H264 - rotate: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90 + rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90 ``` All trascoding formats has [built-in templates](https://github.com/AlexxIT/go2rtc/blob/master/internal/ffmpeg/ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`. From a628ecf72b2ba27c7859bb3172f0c72853d0e798 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 27 Feb 2025 15:56:40 +0300 Subject: [PATCH 67/87] Update readme about new WebRTC default settings and filters logic --- internal/webrtc/README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/internal/webrtc/README.md b/internal/webrtc/README.md index 1dfe5569..d50ed06c 100644 --- a/internal/webrtc/README.md +++ b/internal/webrtc/README.md @@ -19,7 +19,7 @@ If an external connection via STUN is used: ```yaml webrtc: - listen: ":8555/tcp" + listen: ":8555" ice_servers: - urls: [ "stun:stun.l.google.com:19302" ] ``` @@ -31,7 +31,7 @@ webrtc: ```yaml webrtc: # fix local TCP or UDP or both ports for WebRTC media - listen: ":8555/tcp" # address of your local server + listen: ":8555" # address of your local server # add additional host candidates manually # order is important, the first will have a higher priority @@ -55,17 +55,20 @@ webrtc: # including candidates from the `listen` option # use `candidates: []` to remove all auto discovery candidates candidates: [ 192.168.1.123 ] + + # enable localhost candidates + loopback: true # list of network types to be used for connection # including candidates from the `listen` option networks: [ udp4, udp6, tcp4, tcp6 ] # list of interfaces to be used for connection - # not related to the `listen` option + # including interfaces from unspecified `listen` option (empty host) interfaces: [ eno1 ] # list of host IP-addresses to be used for connection - # not related to the `listen` option + # including IPs from unspecified `listen` option (empty host) ips: [ 192.168.1.123 ] # range for random UDP ports [min, max] to be used for connection @@ -73,14 +76,16 @@ webrtc: udp_ports: [ 50000, 50100 ] ``` -By default go2rtc uses **fixed TCP** port and multiple **random UDP** ports for each WebRTC connection - `listen: ":8555/tcp"`. +By default go2rtc uses **fixed TCP** port and **fixed UDP** ports for each **direct** WebRTC connection - `listen: ":8555"`. -You can set **fixed TCP** and **fixed UDP** port for all connections - `listen: ":8555"`. This may has lower performance, but it's your choice. +You can set **fixed TCP** and **random UDP** port for all connections - `listen: ":8555/tcp"`. Don't know why, but you can disable TCP port and leave only random UDP ports - `listen: ""`. ## Config filters +**Importan!** By default go2rtc exclude all Docker-like candidates (`172.16.0.0/12`). This can not be disabled. + Filters allow you to exclude unnecessary candidates. Extra candidates don't make your connection worse or better. But the wrong filter settings can break everything. Skip this setting if you don't understand it. For example, go2rtc is installed on the host system. And there are unnecessary interfaces. You can keep only the relevant via `interfaces` or `ips` options. You can also exclude IPv6 candidates if your server supports them but your home network does not. @@ -99,8 +104,6 @@ For example, go2rtc inside closed docker container (ex. [Frigate](https://frigat webrtc: listen: ":8555" # use fixed TCP and UDP ports candidates: [ 192.168.1.2:8555 ] # add manual host candidate (use docker port forwarding) - filters: - candidates: [] # skip all internal docker candidates ``` ## Userful links From 8cd1ab5c8f82206a3587f53c4a02693e05a4bb16 Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 27 Feb 2025 21:05:10 +0300 Subject: [PATCH 68/87] Update go build version to 1.24 --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- Dockerfile | 2 +- hardware.Dockerfile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0bc21d11..739c4e17 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 - with: { go-version: '1.22' } + with: { go-version: '1.24' } - name: Build go2rtc_win64 env: { GOOS: windows, GOARCH: amd64 } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc47bdb5..f2089dec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.24' - name: Build Go binary run: go build -ldflags "-s -w" -trimpath -o ./go2rtc diff --git a/Dockerfile b/Dockerfile index 4bc74d79..ba436825 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # 0. Prepare images ARG PYTHON_VERSION="3.11" -ARG GO_VERSION="1.22" +ARG GO_VERSION="1.24" # 1. Download ngrok binary (for support arm/v6) diff --git a/hardware.Dockerfile b/hardware.Dockerfile index 2254f9be..e75a97cd 100644 --- a/hardware.Dockerfile +++ b/hardware.Dockerfile @@ -4,7 +4,7 @@ # only debian 13 (trixie) has latest ffmpeg # https://packages.debian.org/trixie/ffmpeg ARG DEBIAN_VERSION="trixie-slim" -ARG GO_VERSION="1.22-bookworm" +ARG GO_VERSION="1.24-bookworm" ARG NGROK_VERSION="3" FROM debian:${DEBIAN_VERSION} AS base From 3c612e284e0e1a560623c0a80388919febef09fd Mon Sep 17 00:00:00 2001 From: Alex X Date: Thu, 27 Feb 2025 21:43:30 +0300 Subject: [PATCH 69/87] Update dependencies --- go.mod | 24 +++++++++++----------- go.sum | 51 ++++++++++++++++++++--------------------------- scripts/README.md | 6 ++++++ 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index 5f0a193b..45d5327c 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,15 @@ go 1.20 require ( github.com/asticode/go-astits v1.13.0 github.com/expr-lang/expr v1.16.9 + github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/mattn/go-isatty v0.0.20 - github.com/miekg/dns v1.1.62 + github.com/miekg/dns v1.1.63 github.com/pion/ice/v2 v2.3.37 github.com/pion/interceptor v0.1.37 github.com/pion/rtcp v1.2.15 - github.com/pion/rtp v1.8.10 - github.com/pion/sdp/v3 v3.0.9 + github.com/pion/rtp v1.8.11 + github.com/pion/sdp/v3 v3.0.10 github.com/pion/srtp/v2 v2.0.20 github.com/pion/stun v0.6.1 github.com/pion/webrtc/v3 v3.3.5 @@ -21,30 +22,29 @@ require ( github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f github.com/stretchr/testify v1.10.0 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.33.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/asticode/go-astikit v0.45.0 // indirect + github.com/asticode/go-astikit v0.52.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/logging v0.2.2 // indirect + github.com/pion/logging v0.2.3 // indirect github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.35 // indirect + github.com/pion/sctp v1.8.36 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/tools v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index c75ffced..a0fdcb88 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= -github.com/asticode/go-astikit v0.45.0 h1:08to/jrbod9tchF2bJ9moW+RTDK7DBUxLdIeSE7v7Sw= -github.com/asticode/go-astikit v0.45.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= +github.com/asticode/go-astikit v0.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y= +github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -23,14 +23,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= -github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= @@ -40,8 +41,9 @@ github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0= github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= @@ -50,12 +52,12 @@ github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9 github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU= -github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA= -github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg= -github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= -github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= +github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk= +github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= +github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0= +github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= +github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= @@ -90,13 +92,11 @@ github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDf github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= @@ -110,13 +110,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -125,13 +124,13 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -146,8 +145,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -166,14 +165,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/scripts/README.md b/scripts/README.md index acc6e0c9..3832475c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -6,6 +6,12 @@ Go 1.21 support only Windows 10 and macOS 10.15. So we will set `go 1.20` (minimum version) inside `go.mod` file. And will use env `GOTOOLCHAIN=go1.20.14` for building `win32` and `mac_amd64` binaries. All other binaries will use latest go version. +``` +golang.org/x/crypto v0.33.0 +golang.org/x/mod v0.20.0 // indirect +golang.org/x/tools v0.24.0 // indirect +``` + ## Build - UPX-3.96 pack broken bin for `linux_mipsel` From 57cd791348544802cd1135c6e0f3484ba1dc5ed7 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 1 Mar 2025 20:00:59 +0300 Subject: [PATCH 70/87] Fix ONVIF client GetCapabilities request --- pkg/onvif/client.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index cb6221e1..936f65f6 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -159,9 +159,12 @@ func (c *Client) GetServiceCapabilities() ([]byte, error) { } func (c *Client) DeviceRequest(operation string) ([]byte, error) { - if operation == DeviceGetServices { + switch operation { + case DeviceGetServices: operation = `true` - } else { + case DeviceGetCapabilities: + operation = `All` + default: operation = `` } return c.Request(c.deviceURL, operation) From 39c14e6556cf09e8d36b3d65137047ca880d6a0c Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 1 Mar 2025 21:35:53 +0300 Subject: [PATCH 71/87] Fix support streaming to YouTube #1574 --- pkg/rtmp/README.md | 7 +++++++ pkg/rtmp/conn.go | 21 +++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pkg/rtmp/README.md b/pkg/rtmp/README.md index 4196d570..f6e66328 100644 --- a/pkg/rtmp/README.md +++ b/pkg/rtmp/README.md @@ -1,3 +1,10 @@ +## Tests + +- go2rtc rtmp client => Reolink +- go2rtc rtmp server <= Dahua +- go2rtc rtmp publish => YouTube +- go2rtc rtmp publish => Telegram + ## Logs ``` diff --git a/pkg/rtmp/conn.go b/pkg/rtmp/conn.go index 2083a148..70e2aec1 100644 --- a/pkg/rtmp/conn.go +++ b/pkg/rtmp/conn.go @@ -46,7 +46,7 @@ func (c *Conn) Close() error { return c.conn.Close() } -func (c *Conn) readResponse(transID float64) ([]any, error) { +func (c *Conn) readResponse(wait func(items []any) bool) ([]any, error) { for { msgType, _, b, err := c.readMessage() if err != nil { @@ -59,7 +59,7 @@ func (c *Conn) readResponse(transID float64) ([]any, error) { c.rdPacketSize = binary.BigEndian.Uint32(b) case TypeCommand: items, _ := amf.NewReader(b).ReadItems() - if len(items) >= 3 && (items[1] == transID || items[1] == float64(0)) { + if wait(items) { return items, nil } } @@ -250,7 +250,9 @@ func (c *Conn) writeConnect() error { return err } - v, err := c.readResponse(1) + v, err := c.readResponse(func(items []any) bool { + return len(items) >= 3 && items[0] == "_result" && items[1] == float64(1) + }) if err != nil { return err } @@ -280,7 +282,9 @@ func (c *Conn) writeCreateStream() error { return err } - v, err := c.readResponse(4) + v, err := c.readResponse(func(items []any) bool { + return len(items) >= 3 && items[0] == "_result" && items[1] == float64(4) + }) if err != nil { return err } @@ -301,7 +305,10 @@ func (c *Conn) writePublish() error { return err } - v, err := c.readResponse(5) + // YouTube can response with "onBWDone 0" + v, err := c.readResponse(func(items []any) bool { + return len(items) >= 3 && items[0] == "onStatus" + }) if err != nil { return nil } @@ -321,7 +328,9 @@ func (c *Conn) writePlay() error { } // Reolink response with ID=0, other software respose with ID=5 - v, err := c.readResponse(5) + v, err := c.readResponse(func(items []any) bool { + return len(items) >= 3 && items[0] == "onStatus" + }) if err != nil { return nil } From 47b740ff350b33e0b567504384cc2adaa8b1dcea Mon Sep 17 00:00:00 2001 From: hsakoh <20980395+hsakoh@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:35:02 +0900 Subject: [PATCH 72/87] =?UTF-8?q?=EF=BB=BFAdd=20client=20for=20SwitchBot?= =?UTF-8?q?=20Camera=20WebRTC=20(supports=20special=20SessionDescription).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 ++++++--- internal/webrtc/client.go | 4 ++- internal/webrtc/kinesis.go | 24 +++++++++++--- internal/webrtc/switchbot.go | 62 ++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 internal/webrtc/switchbot.go diff --git a/README.md b/README.md index 65ebf2b7..cec2f247 100644 --- a/README.md +++ b/README.md @@ -682,13 +682,18 @@ Supports connection to [Wyze](https://www.wyze.com/) cameras, using WebRTC proto Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify signalling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer). +**switchbot** + +Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). (`Outdoor Spotlight Cam 1080P`,`Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`,`Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available .) + ```yaml streams: - webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 - webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 - webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] - webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze - webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] + webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 + webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 + webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] + webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze + webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] + webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=HD#client_id=...#ice_servers=[{...},{...}] ``` **PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language. diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index a5af8bb6..9f21f4e9 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -41,9 +41,11 @@ func streamsHandler(rawURL string) (core.Producer, error) { // https://aws.amazon.com/kinesis/video-streams/ // https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html // https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc - return kinesisClient(rawURL, query, "webrtc/kinesis") + return kinesisClient(rawURL, query, "webrtc/kinesis", &kinesisClientOpts{}) } else if format == "openipc" { return openIPCClient(rawURL, query) + } else if format == "switchbot" { + return switchbotClient(rawURL, query) } else { return go2rtcClient(rawURL) } diff --git a/internal/webrtc/kinesis.go b/internal/webrtc/kinesis.go index 2ea1cf7a..42f76dce 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -34,7 +34,12 @@ func (k kinesisResponse) String() string { return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload) } -func kinesisClient(rawURL string, query url.Values, format string) (core.Producer, error) { +type kinesisClientOpts struct { + SessionDescriptionModifier func(*pion.SessionDescription) ([]byte, error) + MediaModifier func() ([]*core.Media, error) +} + +func kinesisClient(rawURL string, query url.Values, format string, opts *kinesisClientOpts) (core.Producer, error) { // 1. Connect to signalign server conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil) if err != nil { @@ -112,6 +117,12 @@ func kinesisClient(rawURL string, query url.Values, format string) (core.Produce {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, } + if opts.MediaModifier != nil { + medias, err = opts.MediaModifier() + if err != nil { + return nil, err + } + } // 4. Create offer offer, err := prod.CreateOffer(medias) @@ -121,10 +132,15 @@ func kinesisClient(rawURL string, query url.Values, format string) (core.Produce // 5. Send offer req.Action = "SDP_OFFER" - req.Payload, _ = json.Marshal(pion.SessionDescription{ + sessionDescription := pion.SessionDescription{ Type: pion.SDPTypeOffer, SDP: offer, - }) + } + if opts.SessionDescriptionModifier != nil { + req.Payload, _ = opts.SessionDescriptionModifier(&sessionDescription) + } else { + req.Payload, _ = json.Marshal(sessionDescription) + } if err = conn.WriteJSON(req); err != nil { return nil, err } @@ -218,5 +234,5 @@ func wyzeClient(rawURL string) (core.Producer, error) { "ice_servers": []string{string(kvs.Servers)}, } - return kinesisClient(kvs.URL, query, "webrtc/wyze") + return kinesisClient(kvs.URL, query, "webrtc/wyze", &kinesisClientOpts{}) } diff --git a/internal/webrtc/switchbot.go b/internal/webrtc/switchbot.go new file mode 100644 index 00000000..09d0c5b1 --- /dev/null +++ b/internal/webrtc/switchbot.go @@ -0,0 +1,62 @@ +package webrtc + +import ( + "encoding/json" + "net/url" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/core" + pion "github.com/pion/webrtc/v3" +) + +// SessionDescription is used to expose local and remote session descriptions. +type SwitchBotSessionDescription struct { + Type string `json:"type"` + SDP string `json:"sdp"` + Resolution SwitchBotResolution `json:"resolution"` + PlayType int `json:"play_type"` +} + +func switchbotClient(rawURL string, query url.Values) (core.Producer, error) { + return kinesisClient(rawURL, query, "webrtc/switchbot", &kinesisClientOpts{ + SessionDescriptionModifier: func(sd *pion.SessionDescription) ([]byte, error) { + resolution, ok := parseSwitchBotResolution(query.Get("resolution")) + if !ok { + resolution = SwitchBotResolutionSD + } + json, err := json.Marshal(SwitchBotSessionDescription{ + Type: sd.Type.String(), + SDP: sd.SDP, + Resolution: resolution, + PlayType: 0, + }) + return json, err + }, + MediaModifier: func() ([]*core.Media, error) { + return []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + //{Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + //{Kind: core.KindAudio, Direction: core.DirectionSendRecv}, + //{Kind: "Data", Direction: core.DirectionSendRecv}, + }, nil + }, + }) +} + +type SwitchBotResolution int + +const ( + SwitchBotResolutionHD SwitchBotResolution = 0 + SwitchBotResolutionSD = 1 +) + +func parseSwitchBotResolution(str string) (SwitchBotResolution, bool) { + var ( + resolutionMap = map[string]SwitchBotResolution{ + "hd": SwitchBotResolutionHD, + "sd": SwitchBotResolutionSD, + } + ) + c, ok := resolutionMap[strings.ToLower(str)] + return c, ok +} From b8390331afdbad1b7289a853ec44fe2e1183b707 Mon Sep 17 00:00:00 2001 From: klutrem Date: Thu, 6 Mar 2025 16:03:44 +0300 Subject: [PATCH 73/87] feat: x-www-form-urlencoded support --- internal/webrtc/server.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/webrtc/server.go b/internal/webrtc/server.go index f7365afa..c73d67eb 100644 --- a/internal/webrtc/server.go +++ b/internal/webrtc/server.go @@ -1,9 +1,11 @@ package webrtc import ( + "encoding/base64" "encoding/json" "io" "net/http" + "net/url" "strconv" "strings" "time" @@ -62,8 +64,8 @@ func syncHandler(w http.ResponseWriter, r *http.Request) { // 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP) // 3. other - receive/response raw SDP func outputWebRTC(w http.ResponseWriter, r *http.Request) { - url := r.URL.Query().Get("src") - stream := streams.Get(url) + uri := r.URL.Query().Get("src") + stream := streams.Get(uri) if stream == nil { http.Error(w, api.StreamNotFound, http.StatusNotFound) return @@ -87,6 +89,28 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { } offer = desc.SDP + case "application/x-www-form-urlencoded": + body, err := io.ReadAll(r.Body) + if err != nil { + log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + values, err := url.ParseQuery(string(body)) + if err != nil { + log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + encodedOffer := values.Get("data") + decodedOffer, err := base64.StdEncoding.DecodeString(encodedOffer) + if err != nil { + log.Error().Err(err).Caller().Send() + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + offer = string(decodedOffer) + default: body, err := io.ReadAll(r.Body) if err != nil { @@ -124,6 +148,11 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { } err = json.NewEncoder(w).Encode(v) + case "application/x-www-form-urlencoded": + w.Header().Set("Content-Type", mediaType) + encodedAnswer := base64.StdEncoding.EncodeToString([]byte(answer)) + _, err = w.Write([]byte(encodedAnswer)) + case MimeSDP: w.Header().Set("Content-Type", mediaType) w.WriteHeader(http.StatusCreated) From 22bf8163cdb736b8b2d9f148bbe399af3a51b980 Mon Sep 17 00:00:00 2001 From: klutrem Date: Thu, 6 Mar 2025 16:08:43 +0300 Subject: [PATCH 74/87] returned url variable name --- internal/webrtc/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/webrtc/server.go b/internal/webrtc/server.go index c73d67eb..a59548b3 100644 --- a/internal/webrtc/server.go +++ b/internal/webrtc/server.go @@ -5,7 +5,7 @@ import ( "encoding/json" "io" "net/http" - "net/url" + urlParser "net/url" "strconv" "strings" "time" @@ -64,8 +64,8 @@ func syncHandler(w http.ResponseWriter, r *http.Request) { // 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP) // 3. other - receive/response raw SDP func outputWebRTC(w http.ResponseWriter, r *http.Request) { - uri := r.URL.Query().Get("src") - stream := streams.Get(uri) + url := r.URL.Query().Get("src") + stream := streams.Get(url) if stream == nil { http.Error(w, api.StreamNotFound, http.StatusNotFound) return @@ -96,7 +96,7 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - values, err := url.ParseQuery(string(body)) + values, err := urlParser.ParseQuery(string(body)) if err != nil { log.Error().Err(err).Caller().Send() http.Error(w, err.Error(), http.StatusBadRequest) From a15deedf0deeb6355a5cf334aff5916afb907d5d Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 7 Mar 2025 21:44:23 +0300 Subject: [PATCH 75/87] Fix YAML patch in some cases #1626 --- internal/app/config.go | 4 +- internal/homekit/api.go | 4 +- internal/homekit/server.go | 2 +- internal/streams/api.go | 4 +- pkg/yaml/yaml.go | 236 ++++++++++++++++++++----------------- pkg/yaml/yaml_test.go | 235 ++++++++++++++++-------------------- 6 files changed, 237 insertions(+), 248 deletions(-) diff --git a/internal/app/config.go b/internal/app/config.go index 8ae6d460..9d4480b7 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -18,7 +18,7 @@ func LoadConfig(v any) { } } -func PatchConfig(key string, value any, path ...string) error { +func PatchConfig(path []string, value any) error { if ConfigPath == "" { return errors.New("config file disabled") } @@ -26,7 +26,7 @@ func PatchConfig(key string, value any, path ...string) error { // empty config is OK b, _ := os.ReadFile(ConfigPath) - b, err := yaml.Patch(b, key, value, path...) + b, err := yaml.Patch(b, path, value) if err != nil { return err } diff --git a/internal/homekit/api.go b/internal/homekit/api.go index 0ee4d057..9f76c2d6 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -103,7 +103,7 @@ func apiPair(id, url string) error { streams.New(id, conn.URL()) - return app.PatchConfig(id, conn.URL(), "streams") + return app.PatchConfig([]string{"streams", id}, conn.URL()) } func apiUnpair(id string) error { @@ -123,7 +123,7 @@ func apiUnpair(id string) error { streams.Delete(id) - return app.PatchConfig(id, nil, "streams") + return app.PatchConfig([]string{"streams", id}, nil) } func findHomeKitURLs() map[string]*url.URL { diff --git a/internal/homekit/server.go b/internal/homekit/server.go index cb114fea..363a7047 100644 --- a/internal/homekit/server.go +++ b/internal/homekit/server.go @@ -222,7 +222,7 @@ func (s *server) DelPair(conn net.Conn, id string) { } func (s *server) PatchConfig() { - if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil { + if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil { log.Error().Err(err).Msgf( "[homekit] can't save %s pairings=%v", s.stream, s.pairings, ) diff --git a/internal/streams/api.go b/internal/streams/api.go index d6042974..061e61c2 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -53,7 +53,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { return } - if err := app.PatchConfig(name, query["src"], "streams"); err != nil { + if err := app.PatchConfig([]string{"streams", name}, query["src"]); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } @@ -96,7 +96,7 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { case "DELETE": delete(streams, src) - if err := app.PatchConfig(src, nil, "streams"); err != nil { + if err := app.PatchConfig([]string{"streams", src}, nil); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } } diff --git a/pkg/yaml/yaml.go b/pkg/yaml/yaml.go index 70b3baf0..4672cb4c 100644 --- a/pkg/yaml/yaml.go +++ b/pkg/yaml/yaml.go @@ -23,149 +23,157 @@ func Encode(v any, indent int) ([]byte, error) { return b.Bytes(), nil } -// Patch - change key/value pair in YAML file without break formatting -func Patch(src []byte, key string, value any, path ...string) ([]byte, error) { - nodeParent, err := FindParent(src, path...) +func Patch(in []byte, path []string, value any) ([]byte, error) { + out, err := patch(in, path, value) if err != nil { return nil, err } - var dst []byte - - if nodeParent != nil { - dst, err = AddOrReplace(src, key, value, nodeParent) - } else { - dst, err = AddToEnd(src, key, value, path...) - } - - if err = yaml.Unmarshal(dst, map[string]any{}); err != nil { + // validate + if err = yaml.Unmarshal(out, map[string]any{}); err != nil { return nil, err } - return dst, nil + return out, nil } -// FindParent - return YAML Node from path of keys (tree) -func FindParent(src []byte, path ...string) (*yaml.Node, error) { - if len(src) == 0 { - return nil, nil - } - +func patch(in []byte, path []string, value any) ([]byte, error) { var root yaml.Node - if err := yaml.Unmarshal(src, &root); err != nil { + if err := yaml.Unmarshal(in, &root); err != nil { + // invalid yaml return nil, err } - if root.Content == nil { - return nil, nil + // empty in + if len(root.Content) != 1 { + return addToEnd(in, path, value) } - parent := root.Content[0] // yaml.DocumentNode - for _, name := range path { - if parent == nil { - break - } - _, parent = FindChild(parent, name) + // yaml is not dict + if root.Content[0].Kind != yaml.MappingNode { + return nil, errors.New("yaml: can't patch") } - return parent, nil + + // dict items list + nodes := root.Content[0].Content + + n := len(path) - 1 + + // parent node key/value + pKey, pVal := findNode(nodes, path[:n]) + if pKey == nil { + // no parent node + return addToEnd(in, path, value) + } + + var paste []byte + + if value != nil { + // nil value means delete key + var err error + v := map[string]any{path[n]: value} + if paste, err = Encode(v, 2); err != nil { + return nil, err + } + } + + iKey, _ := findNode(pVal.Content, path[n:]) + if iKey != nil { + // key item not nil (replace value) + paste = addIndent(paste, iKey.Column-1) + + i0, i1 := nodeBounds(in, iKey) + return join(in[:i0], paste, in[i1:]), nil + } + + if pVal.Content != nil { + // parent value not nil (use first child indent) + paste = addIndent(paste, pVal.Column-1) + } else { + // parent value is nil (use parent indent + 2) + paste = addIndent(paste, pKey.Column+1) + } + + _, i1 := nodeBounds(in, pKey) + return join(in[:i1], paste, in[i1:]), nil } -// FindChild - search and return YAML key/value pair for current Node -func FindChild(node *yaml.Node, name string) (key, value *yaml.Node) { - for i, child := range node.Content { - if child.Value != name { - continue +func findNode(nodes []*yaml.Node, keys []string) (key, value *yaml.Node) { + for i, name := range keys { + for j := 0; j < len(nodes); j += 2 { + if nodes[j].Value == name { + if i < len(keys)-1 { + nodes = nodes[j+1].Content + break + } + return nodes[j], nodes[j+1] + } } - return child, node.Content[i+1] } - return nil, nil } -func FirstChild(node *yaml.Node) *yaml.Node { - if node.Content == nil { - return node - } - return node.Content[0] -} +func nodeBounds(in []byte, node *yaml.Node) (offset0, offset1 int) { + // start from next line after node + offset0 = lineOffset(in, node.Line) + offset1 = lineOffset(in, node.Line+1) -func LastChild(node *yaml.Node) *yaml.Node { - if node.Content == nil { - return node - } - return LastChild(node.Content[len(node.Content)-1]) -} - -func AddOrReplace(src []byte, key string, value any, nodeParent *yaml.Node) ([]byte, error) { - v := map[string]any{key: value} - put, err := Encode(v, 2) - if err != nil { - return nil, err + if offset1 < 0 { + return offset0, len(in) } - if nodeKey, nodeValue := FindChild(nodeParent, key); nodeKey != nil { - put = AddIndent(put, nodeKey.Column-1) - - i0 := LineOffset(src, nodeKey.Line) - i1 := LineOffset(src, LastChild(nodeValue).Line+1) - - if i1 < 0 { // no new line on the end of file - if value != nil { - return append(src[:i0], put...), nil + for i := offset1; i < len(in); { + indent, length := parseLine(in[i:]) + if indent+1 != length { + if node.Column < indent+1 { + offset1 = i + length + } else { + break } - return src[:i0], nil } - - dst := make([]byte, 0, len(src)+len(put)) - dst = append(dst, src[:i0]...) - if value != nil { - dst = append(dst, put...) - } - return append(dst, src[i1:]...), nil + i += length } - put = AddIndent(put, FirstChild(nodeParent).Column-1) - - i := LineOffset(src, LastChild(nodeParent).Line+1) - - if i < 0 { // no new line on the end of file - src = append(src, '\n') - if value != nil { - src = append(src, put...) - } - return src, nil - } - - dst := make([]byte, 0, len(src)+len(put)) - dst = append(dst, src[:i]...) - if value != nil { - dst = append(dst, put...) - } - return append(dst, src[i:]...), nil + return } -func AddToEnd(src []byte, key string, value any, path ...string) ([]byte, error) { - if len(path) > 1 || value == nil { - return nil, errors.New("config: path not exist") +func addToEnd(in []byte, path []string, value any) ([]byte, error) { + if len(path) != 2 || value == nil { + return nil, errors.New("yaml: path not exist") } v := map[string]map[string]any{ - path[0]: {key: value}, + path[0]: {path[1]: value}, } - put, err := Encode(v, 2) + paste, err := Encode(v, 2) if err != nil { return nil, err } - dst := make([]byte, 0, len(src)+len(put)+10) - dst = append(dst, src...) - if l := len(src); l > 0 && src[l-1] != '\n' { - dst = append(dst, '\n') - } - return append(dst, put...), nil + return join(in, paste), nil } -func AddPrefix(src, pre []byte) (dst []byte) { +func join(items ...[]byte) []byte { + n := len(items) - 1 + for _, b := range items { + n += len(b) + } + + buf := make([]byte, 0, n) + for _, b := range items { + if len(b) == 0 { + continue + } + if n = len(buf); n > 0 && buf[n-1] != '\n' { + buf = append(buf, '\n') + } + buf = append(buf, b...) + } + + return buf +} + +func addPrefix(src, pre []byte) (dst []byte) { for len(src) > 0 { dst = append(dst, pre...) i := bytes.IndexByte(src, '\n') + 1 @@ -180,21 +188,21 @@ func AddPrefix(src, pre []byte) (dst []byte) { return } -func AddIndent(src []byte, indent int) (dst []byte) { +func addIndent(in []byte, indent int) (dst []byte) { pre := make([]byte, indent) for i := 0; i < indent; i++ { pre[i] = ' ' } - return AddPrefix(src, pre) + return addPrefix(in, pre) } -func LineOffset(b []byte, line int) (offset int) { +func lineOffset(in []byte, line int) (offset int) { for l := 1; ; l++ { if l == line { return offset } - i := bytes.IndexByte(b[offset:], '\n') + 1 + i := bytes.IndexByte(in[offset:], '\n') + 1 if i == 0 { break } @@ -202,3 +210,21 @@ func LineOffset(b []byte, line int) (offset int) { } return -1 } + +func parseLine(b []byte) (indent int, length int) { + prefix := true + for ; length < len(b); length++ { + switch b[length] { + case ' ': + if prefix { + indent++ + } + case '\n': + length++ + return + default: + prefix = false + } + } + return +} diff --git a/pkg/yaml/yaml_test.go b/pkg/yaml/yaml_test.go index 3f4c45bb..264546af 100644 --- a/pkg/yaml/yaml_test.go +++ b/pkg/yaml/yaml_test.go @@ -7,140 +7,103 @@ import ( ) func TestPatch(t *testing.T) { - b := []byte(`# prefix`) - - // 1. Add first - b, err := Patch(b, "camera1", "url1", "streams") - require.Nil(t, err) - - require.Equal(t, `# prefix -streams: - camera1: url1 -`, string(b)) - - // 2. Add second - b, err = Patch(b, "camera2", []string{"url2", "url3"}, "streams") - require.Nil(t, err) - - require.Equal(t, `# prefix -streams: - camera1: url1 - camera2: - - url2 - - url3 -`, string(b)) - - // 3. Replace first - b, err = Patch(b, "camera1", "url4", "streams") - require.Nil(t, err) - - require.Equal(t, `# prefix -streams: - camera1: url4 - camera2: - - url2 - - url3 -`, string(b)) - - // 4. Replace second - b, err = Patch(b, "camera2", "url5", "streams") - require.Nil(t, err) - - require.Equal(t, `# prefix -streams: - camera1: url4 - camera2: url5 -`, string(b)) - - // 5. Delete first - b, err = Patch(b, "camera1", nil, "streams") - require.Nil(t, err) - - require.Equal(t, `# prefix -streams: - camera2: url5 -`, string(b)) -} - -func TestPatchParings(t *testing.T) { - b := []byte(`homekit: - camera1: - pin: 123-45-678 -streams: - camera1: url1 -`) - - // 1. Add new key - pairings := []string{"client1", "client2"} - - b, err := Patch(b, "pairings", pairings, "homekit", "camera1") - require.Nil(t, err) - - require.Equal(t, `homekit: - camera1: - pin: 123-45-678 - pairings: - - client1 - - client2 -streams: - camera1: url1 -`, string(b)) -} - -func TestPatch2(t *testing.T) { - b := []byte(`streams: - camera1: - - url1 - - url2 -`) - - b, err := Patch(b, "camera2", "url3", "streams") - require.Nil(t, err) - - require.Equal(t, `streams: - camera1: - - url1 - - url2 - camera2: url3 -`, string(b)) -} - -func TestNoNewLineEnd1(t *testing.T) { - b := []byte(`streams: - camera1: url4 - camera2: - - url2 - - url3`) - - b, err := Patch(b, "camera2", "url5", "streams") - require.Nil(t, err) - - require.Equal(t, `streams: - camera1: url4 - camera2: url5 -`, string(b)) -} - -func TestNoNewLineEnd2(t *testing.T) { - b := []byte(`streams: - camera1: url1 -homekit: - camera1: - pin: 123-45-678`) - - // 1. Add new key - pairings := []string{"client1", "client2"} - - b, err := Patch(b, "pairings", pairings, "homekit", "camera1") - require.Nil(t, err) - - require.Equal(t, `streams: - camera1: url1 -homekit: - camera1: - pin: 123-45-678 - pairings: - - client1 - - client2 -`, string(b)) + tests := []struct { + name string + src string + path []string + value any + expect string + }{ + { + name: "empty config", + src: "", + path: []string{"streams", "camera1"}, + value: "val1", + expect: "streams:\n camera1: val1\n", + }, + { + name: "empty main key", + src: "#dummy", + path: []string{"streams", "camera1"}, + value: "val1", + expect: "#dummy\nstreams:\n camera1: val1\n", + }, + { + name: "single line value", + src: "streams:\n camera1: url1\n camera2: url2", + path: []string{"streams", "camera1"}, + value: "val1", + expect: "streams:\n camera1: val1\n camera2: url2", + }, + { + name: "next line value", + src: "streams:\n camera1:\n url1\n camera2: url2", + path: []string{"streams", "camera1"}, + value: "val1", + expect: "streams:\n camera1: val1\n camera2: url2", + }, + { + name: "two lines value", + src: "streams:\n camera1: url1\n url2\n camera2: url2", + path: []string{"streams", "camera1"}, + value: "val1", + expect: "streams:\n camera1: val1\n camera2: url2", + }, + { + name: "next two lines value", + src: "streams:\n camera1:\n url1\n url2\n camera2: url2", + path: []string{"streams", "camera1"}, + value: "val1", + expect: "streams:\n camera1: val1\n camera2: url2", + }, + { + name: "add array", + src: "", + path: []string{"streams", "camera1"}, + value: []string{"val1", "val2"}, + expect: "streams:\n camera1:\n - val1\n - val2\n", + }, + { + name: "remove value", + src: "streams:\n camera1: url1\n camera2: url2", + path: []string{"streams", "camera1"}, + value: nil, + expect: "streams:\n camera2: url2", + }, + { + name: "add pairings", + src: "homekit:\n camera1:\nstreams:\n camera1: url1", + path: []string{"homekit", "camera1", "pairings"}, + value: []string{"val1"}, + expect: "homekit:\n camera1:\n pairings:\n - val1\nstreams:\n camera1: url1", + }, + { + name: "remove pairings", + src: "homekit:\n camera1:\n pairings:\n - val1\nstreams:\n camera1: url1", + path: []string{"homekit", "camera1", "pairings"}, + value: nil, + expect: "homekit:\n camera1:\nstreams:\n camera1: url1", + }, + { + name: "no new line", + src: "streams:\n camera1: url1", + path: []string{"streams", "camera1"}, + value: "val1", + expect: "streams:\n camera1: val1\n", + }, + { + name: "no new line", + src: "streams:\n camera1: url1\nhomekit:\n camera1:\n name: dummy", + path: []string{"homekit", "camera1", "pairings"}, + value: []string{"val1"}, + expect: "streams:\n camera1: url1\nhomekit:\n camera1:\n name: dummy\n pairings:\n - val1\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := Patch([]byte(tt.src), tt.path, tt.value) + require.NoError(t, err) + require.Equal(t, tt.expect, string(b)) + }) + } } From fe2e372997c50c4a6e9c4aef7dd3b3e53fb0c7a2 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 8 Mar 2025 07:31:49 +0300 Subject: [PATCH 76/87] Add examples to streams module readme --- internal/streams/README.md | 53 +++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/internal/streams/README.md b/internal/streams/README.md index 6bbc268a..64d3b8e7 100644 --- a/internal/streams/README.md +++ b/internal/streams/README.md @@ -1,8 +1,53 @@ -## Testing notes +## Examples ```yaml streams: - test1-basic: ffmpeg:virtual?video#video=h264 - test2-reconnect: ffmpeg:virtual?video&duration=10#video=h264 - test3-execkill: exec:./examples/rtsp_client/rtsp_client/rtsp_client {output} + # known RTSP sources + rtsp-dahua1: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif + rtsp-dahua2: rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=1 + rtsp-tplink1: rtsp://admin:password@192.168.10.91/stream1 + rtsp-tplink2: rtsp://admin:password@192.168.10.91/stream2 + rtsp-reolink1: rtsp://admin:password@192.168.10.92/h264Preview_01_main + rtsp-reolink2: rtsp://admin:password@192.168.10.92/h264Preview_01_sub + rtsp-sonoff1: rtsp://admin:password@192.168.10.93/av_stream/ch0 + rtsp-sonoff2: rtsp://admin:password@192.168.10.93/av_stream/ch1 + + # known RTMP sources + rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password + rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password + + # known HTTP sources + http-reolink1: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password + http-reolink2: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password + + # known ONVIF sources + onvif-dahua1: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000 + onvif-dahua2: onvif://admin:password@192.168.10.90?subtype=MediaProfile00001 + onvif-dahua3: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000&snapshot + onvif-tplink1: onvif://admin:password@192.168.10.91:2020?subtype=profile_1 + onvif-tplink2: onvif://admin:password@192.168.10.91:2020?subtype=profile_2 + onvif-reolink1: onvif://admin:password@192.168.10.92:8000?subtype=000 + onvif-reolink2: onvif://admin:password@192.168.10.92:8000?subtype=001 + onvif-reolink3: onvif://admin:password@192.168.10.92:8000?subtype=000&snapshot + onvif-openipc1: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_000 + onvif-openipc2: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_001 + + # some EXEC examples + exec-h264-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f h264 - + exec-flv-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f flv - + exec-mpegts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f mpegts - + exec-adts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f adts - + exec-mjpeg-pipe: exec:ffmpeg -re -i bbb.mp4 -c mjpeg -f mjpeg - + exec-hevc-pipe: exec:ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc - + exec-wav-pipe: exec:ffmpeg -re -i bbb.mp4 -c pcm_alaw -ar 8000 -ac 1 -f wav - + exec-y4m-pipe: exec:ffmpeg -re -i bbb.mp4 -c rawvideo -f yuv4mpegpipe - + exec-pcma-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_alaw -ar:a 8000 -ac:a 1 -f wav - + exec-pcmu-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -f wav - + exec-s16le-pipe: exec:ffmpeg -re -i numb.mp3 -c:a pcm_s16le -ar:a 16000 -ac:a 1 -f wav - + + # some FFmpeg examples + ffmpeg-video-h264: ffmpeg:virtual?video#video=h264 + ffmpeg-video-4K: ffmpeg:virtual?video&size=4K#video=h264 + ffmpeg-video-10s: ffmpeg:virtual?video&duration=10#video=h264 + ffmpeg-video-src2: ffmpeg:virtual?video=testsrc2&size=2K#video=h264 ``` From 830e476120c7cb1d79d644ca24b8fe3cef21e347 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 8 Mar 2025 14:11:29 +0300 Subject: [PATCH 77/87] Fix data race for memory logger #1487 --- internal/app/log.go | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/internal/app/log.go b/internal/app/log.go index 4a5a0ee4..b8ca4aa5 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -4,12 +4,13 @@ import ( "io" "os" "strings" + "sync" "github.com/mattn/go-isatty" "github.com/rs/zerolog" ) -var MemoryLog = newBuffer(16) +var MemoryLog = newBuffer() func GetLogger(module string) zerolog.Logger { if s, ok := modules[module]; ok { @@ -106,15 +107,19 @@ var modules = map[string]string{ "time": zerolog.TimeFormatUnixMs, } -const chunkSize = 1 << 16 +const ( + chunkCount = 16 + chunkSize = 1 << 16 +) type circularBuffer struct { chunks [][]byte r, w int + mu sync.Mutex } -func newBuffer(chunks int) *circularBuffer { - b := &circularBuffer{chunks: make([][]byte, 0, chunks)} +func newBuffer() *circularBuffer { + b := &circularBuffer{chunks: make([][]byte, 0, chunkCount)} // create first chunk b.chunks = append(b.chunks, make([]byte, 0, chunkSize)) return b @@ -123,16 +128,17 @@ func newBuffer(chunks int) *circularBuffer { func (b *circularBuffer) Write(p []byte) (n int, err error) { n = len(p) + b.mu.Lock() // check if chunk has size if len(b.chunks[b.w])+n > chunkSize { // increase write chunk index - if b.w++; b.w == cap(b.chunks) { + if b.w++; b.w == chunkCount { b.w = 0 } // check overflow if b.r == b.w { // increase read chunk index - if b.r++; b.r == cap(b.chunks) { + if b.r++; b.r == chunkCount { b.r = 0 } } @@ -147,29 +153,34 @@ func (b *circularBuffer) Write(p []byte) (n int, err error) { } b.chunks[b.w] = append(b.chunks[b.w], p...) + b.mu.Unlock() return } func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) { - for i := b.r; ; { - var nn int - if nn, err = w.Write(b.chunks[i]); err != nil { - return - } - n += int64(nn) + buf := make([]byte, 0, chunkCount*chunkSize) + // use temp buffer inside mutex because w.Write can take some time + b.mu.Lock() + for i := b.r; ; { + buf = append(buf, b.chunks[i]...) if i == b.w { break } - if i++; i == cap(b.chunks) { + if i++; i == chunkCount { i = 0 } } - return + b.mu.Unlock() + + nn, err := w.Write(buf) + return int64(nn), err } func (b *circularBuffer) Reset() { + b.mu.Lock() b.chunks[0] = b.chunks[0][:0] b.r = 0 b.w = 0 + b.mu.Unlock() } From c9724e2024744b94c2a0fb2167ca0fe643bfb617 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 07:20:40 +0300 Subject: [PATCH 78/87] Fix RTMP server handshake for FFmpeg #1318 --- pkg/rtmp/server.go | 52 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/pkg/rtmp/server.go b/pkg/rtmp/server.go index 3dcd4048..014b2853 100644 --- a/pkg/rtmp/server.go +++ b/pkg/rtmp/server.go @@ -2,10 +2,13 @@ package rtmp import ( "bufio" + "crypto/rand" "encoding/binary" + "errors" "fmt" "io" "net" + "time" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/flv/amf" @@ -34,23 +37,54 @@ func NewServer(conn net.Conn) (*Conn, error) { } func (c *Conn) serverHandshake() error { - b := make([]byte, 1+1536) - // read C0+C1 + // based on https://rtmp.veriskope.com/docs/spec/ + _ = c.conn.SetDeadline(time.Now().Add(core.ConnDeadline)) + + // read C0 + b := make([]byte, 1) if _, err := io.ReadFull(c.rd, b); err != nil { return err } - // write S0+S1, skip random + + if b[0] != 3 { + return errors.New("rtmp: wrong handshake") + } + + // write S0 + if _, err := c.conn.Write([]byte{3}); err != nil { + return err + } + + b = make([]byte, 1536) + + // write S1 + tsS1 := nowMS() + binary.BigEndian.PutUint32(b, tsS1) + binary.BigEndian.PutUint32(b[4:], 0) + _, _ = rand.Read(b[8:]) if _, err := c.conn.Write(b); err != nil { return err } - // read S1, skip check - if _, err := io.ReadFull(c.rd, make([]byte, 1536)); err != nil { + + // read C1 + if _, err := io.ReadFull(c.rd, b); err != nil { return err } - // write C1 - if _, err := c.conn.Write(b[1:]); err != nil { + + // write S2 + tsS2 := nowMS() + binary.BigEndian.PutUint32(b, tsS1) + binary.BigEndian.PutUint32(b[4:], tsS2) + if _, err := c.conn.Write(b); err != nil { return err } + + // read C2 + if _, err := io.ReadFull(c.rd, b); err != nil { + return err + } + + _ = c.conn.SetDeadline(time.Time{}) return nil } @@ -161,3 +195,7 @@ func (c *Conn) WriteStart() error { payload := amf.EncodeItems("onStatus", 0, nil, map[string]any{"code": code}) return c.writeMessage(3, TypeCommand, 0, payload) } + +func nowMS() uint32 { + return uint32(time.Now().UnixNano() / int64(time.Millisecond)) +} From d51b36e80d8faed6c8e95fc7b24b104ebc22e9a0 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 07:21:10 +0300 Subject: [PATCH 79/87] Add readme to RTMP module --- internal/rtmp/README.md | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 internal/rtmp/README.md diff --git a/internal/rtmp/README.md b/internal/rtmp/README.md new file mode 100644 index 00000000..900478df --- /dev/null +++ b/internal/rtmp/README.md @@ -0,0 +1,59 @@ +## Tested client + +| From | To | Comment | +|--------|---------------------------------|---------| +| go2rtc | Reolink RLC-520A fw. v3.1.0.801 | OK | + +**go2rtc.yaml** + +```yaml +streams: + rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password + rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password +``` + +## Tested server + +| From | To | Comment | +|------------------------|--------|---------------------| +| OBS 31.0.2 | go2rtc | OK | +| OpenIPC 2.5.03.02-lite | go2rtc | OK | +| FFmpeg 6.1 | go2rtc | OK | +| GoPro Black 12 | go2rtc | OK, 1080p, 5000kbps | + +**go2rtc.yaml** + +```yaml +rtmp: + listen: :1935 +streams: + tmp: +``` + +**OBS** + +Settings > Stream: + +- Service: Custom +- Server: rtmp://192.168.10.101/tmp +- Stream Key: +- Use auth: + +**OpenIPC** + +WebUI > Majestic > Settings > Outgoing + +- Enable +- Address: rtmp://192.168.10.101/tmp +- Save +- Restart + +**FFmpeg** + +```shell +ffmpeg -re -i bbb.mp4 -c copy -f flv rtmp://192.168.10.101/tmp +``` + +**GoPro** + +GoPro Quik > Camera > Translation > Other From 944e6f5569a5c443adccafb1203ea5982a39d9d6 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 11:40:23 +0300 Subject: [PATCH 80/87] Update Reolink links in the docs --- internal/rtmp/README.md | 3 ++- internal/streams/README.md | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/rtmp/README.md b/internal/rtmp/README.md index 900478df..662efa19 100644 --- a/internal/rtmp/README.md +++ b/internal/rtmp/README.md @@ -9,7 +9,8 @@ ```yaml streams: rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password - rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password + rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password + rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password ``` ## Tested server diff --git a/internal/streams/README.md b/internal/streams/README.md index 64d3b8e7..6f0c9772 100644 --- a/internal/streams/README.md +++ b/internal/streams/README.md @@ -14,11 +14,13 @@ streams: # known RTMP sources rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password - rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password + rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password + rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password # known HTTP sources http-reolink1: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password - http-reolink2: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password + http-reolink2: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_sub.bcs&user=admin&password=password + http-reolink3: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password # known ONVIF sources onvif-dahua1: onvif://admin:password@192.168.10.90?subtype=MediaProfile00000 From b6934922faa73b5f7711a07c69feedc59b11c7b8 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 16:26:10 +0300 Subject: [PATCH 81/87] Improve ONVIF server #1304 --- examples/onvif_client/README.md | 5 ++++ examples/onvif_client/main.go | 13 +++++---- internal/onvif/onvif.go | 11 +++++++- pkg/onvif/server.go | 48 +++++++++++++++++++++++++-------- 4 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 examples/onvif_client/README.md diff --git a/examples/onvif_client/README.md b/examples/onvif_client/README.md new file mode 100644 index 00000000..b4ae3383 --- /dev/null +++ b/examples/onvif_client/README.md @@ -0,0 +1,5 @@ +## Example + +```shell +go run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations +``` \ No newline at end of file diff --git a/examples/onvif_client/main.go b/examples/onvif_client/main.go index 03dd12ba..724d3252 100644 --- a/examples/onvif_client/main.go +++ b/examples/onvif_client/main.go @@ -2,7 +2,6 @@ package main import ( "log" - "net" "net/url" "os" @@ -41,7 +40,13 @@ func main() { onvif.DeviceGetSystemDateAndTime, onvif.DeviceSystemReboot: b, err = client.DeviceRequest(operation) - case onvif.MediaGetProfiles, onvif.MediaGetVideoSources: + case onvif.MediaGetProfiles, + onvif.MediaGetVideoEncoderConfigurations, + onvif.MediaGetVideoSources, + onvif.MediaGetVideoSourceConfigurations, + onvif.MediaGetAudioEncoderConfigurations, + onvif.MediaGetAudioSources, + onvif.MediaGetAudioSourceConfigurations: b, err = client.MediaRequest(operation) case onvif.MediaGetProfile: b, err = client.GetProfile(token) @@ -64,9 +69,7 @@ func main() { log.Fatal(err) } - host, _, _ := net.SplitHostPort(u.Host) - - if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil { + if err = os.WriteFile(u.Hostname()+"_"+operation+".xml", b, 0644); err != nil { log.Printf("%s\n", err) } } diff --git a/internal/onvif/onvif.go b/internal/onvif/onvif.go index 0d0319a7..6dfa633a 100644 --- a/internal/onvif/onvif.go +++ b/internal/onvif/onvif.go @@ -72,7 +72,11 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { onvif.DeviceGetNetworkDefaultGateway, onvif.DeviceGetNetworkProtocols, onvif.DeviceGetNTP, - onvif.DeviceGetScopes: + onvif.DeviceGetScopes, + onvif.MediaGetVideoEncoderConfigurations, + onvif.MediaGetAudioEncoderConfigurations, + onvif.MediaGetAudioSources, + onvif.MediaGetAudioSourceConfigurations: b = onvif.StaticResponse(operation) case onvif.DeviceGetCapabilities: @@ -109,6 +113,10 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { token := onvif.FindTagValue(b, "ProfileToken") b = onvif.GetProfileResponse(token) + case onvif.MediaGetVideoSourceConfigurations: + // important for Happytime Onvif Client + b = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames()) + case onvif.MediaGetVideoSourceConfiguration: token := onvif.FindTagValue(b, "ConfigurationToken") b = onvif.GetVideoSourceConfigurationResponse(token) @@ -129,6 +137,7 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) { default: http.Error(w, "unsupported operation", http.StatusBadRequest) + log.Warn().Msgf("[onvif] unsupported operation: %s", operation) log.Debug().Msgf("[onvif] unsupported request:\n%s", b) return } diff --git a/pkg/onvif/server.go b/pkg/onvif/server.go index db0bb2fb..54272798 100644 --- a/pkg/onvif/server.go +++ b/pkg/onvif/server.go @@ -179,18 +179,35 @@ func appendProfile(e *Envelope, tag, name string) { `) } +func GetVideoSourceConfigurationsResponse(names []string) []byte { + e := NewEnvelope() + e.Append(` +`) + for _, name := range names { + appendProfile(e, "Configurations", name) + } + e.Append(``) + return e.Bytes() +} + func GetVideoSourceConfigurationResponse(name string) []byte { e := NewEnvelope() e.Append(` - - VSC - `, name, ` - - -`) +`) + appendVideoSourceConfiguration(e, "Configuration", name) + e.Append(``) return e.Bytes() } +func appendVideoSourceConfiguration(e *Envelope, tag, name string) { + e.Append(` + VSC + `, name, ` + + +`) +} + func GetVideoSourcesResponse(names []string) []byte { e := NewEnvelope() e.Append(` @@ -226,11 +243,7 @@ func StaticResponse(operation string) []byte { e := NewEnvelope() e.Append(responses[operation]) - b := e.Bytes() - if operation == DeviceGetNetworkInterfaces { - println() - } - return b + return e.Bytes() } var responses = map[string]string{ @@ -249,4 +262,17 @@ var responses = map[string]string{ Fixedonvif://www.onvif.org/Profile/Streaming Fixedonvif://www.onvif.org/type/Network_Video_Transmitter `, + + MediaGetVideoEncoderConfigurations: ` + + VEC + H264 + 19201080 + + +`, + + MediaGetAudioEncoderConfigurations: ``, + MediaGetAudioSources: ``, + MediaGetAudioSourceConfigurations: ``, } From 3954a555f842f2d3de206bcc733cfd04a488baa5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 17:08:25 +0300 Subject: [PATCH 82/87] Fix extra slash for RTSP SETUP #1236 --- pkg/rtsp/client.go | 6 ++---- pkg/rtsp/helpers.go | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/rtsp/client.go b/pkg/rtsp/client.go index 352c00a1..7fc134fc 100644 --- a/pkg/rtsp/client.go +++ b/pkg/rtsp/client.go @@ -237,13 +237,11 @@ func (c *Conn) SetupMedia(media *core.Media) (byte, error) { rawURL := media.ID // control if !strings.Contains(rawURL, "://") { rawURL = c.URL.String() - if !strings.HasSuffix(rawURL, "/") { + // prefix check for https://github.com/AlexxIT/go2rtc/issues/1236 + if !strings.HasSuffix(rawURL, "/") && !strings.HasPrefix(media.ID, "/") { rawURL += "/" } rawURL += media.ID - } else if strings.HasPrefix(rawURL, "rtsp://rtsp://") { - // fix https://github.com/AlexxIT/go2rtc/issues/830 - rawURL = rawURL[7:] } trackURL, err := urlParse(rawURL) if err != nil { diff --git a/pkg/rtsp/helpers.go b/pkg/rtsp/helpers.go index 952730bb..d8ed1685 100644 --- a/pkg/rtsp/helpers.go +++ b/pkg/rtsp/helpers.go @@ -117,6 +117,7 @@ func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) strin // 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/ // 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/ func urlParse(rawURL string) (*url.URL, error) { + // fix https://github.com/AlexxIT/go2rtc/issues/830 if strings.HasPrefix(rawURL, "rtsp://rtsp://") { rawURL = rawURL[7:] } From c8f68f44af5846b026418c1904ddc559d1da2baa Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 17:26:06 +0300 Subject: [PATCH 83/87] Optimize imports --- internal/api/static.go | 3 ++- internal/webtorrent/tracker.go | 3 ++- main.go | 2 +- pkg/h265/helper.go | 1 + pkg/h265/payloader.go | 3 ++- pkg/hass/api.go | 3 ++- pkg/mdns/mdns_test.go | 3 ++- pkg/mjpeg/rtp.go | 5 +++-- pkg/ngrok/ngrok.go | 3 ++- pkg/pcm/v1/pcm_test.go | 3 ++- pkg/roborock/api.go | 3 ++- pkg/tcp/websocket/dial.go | 3 ++- pkg/webtorrent/server.go | 5 +++-- 13 files changed, 26 insertions(+), 14 deletions(-) diff --git a/internal/api/static.go b/internal/api/static.go index 9d9335ea..8de40073 100644 --- a/internal/api/static.go +++ b/internal/api/static.go @@ -1,8 +1,9 @@ package api import ( - "github.com/AlexxIT/go2rtc/www" "net/http" + + "github.com/AlexxIT/go2rtc/www" ) func initStatic(staticDir string) { diff --git a/internal/webtorrent/tracker.go b/internal/webtorrent/tracker.go index 4e5bd9b3..e504d80c 100644 --- a/internal/webtorrent/tracker.go +++ b/internal/webtorrent/tracker.go @@ -2,9 +2,10 @@ package webtorrent import ( "fmt" + "net/http" + "github.com/AlexxIT/go2rtc/pkg/webtorrent" "github.com/gorilla/websocket" - "net/http" ) var upgrader *websocket.Upgrader diff --git a/main.go b/main.go index db3983cc..5708f973 100644 --- a/main.go +++ b/main.go @@ -81,7 +81,7 @@ func main() { mpegts.Init() // mpegts passive source roborock.Init() // roborock source homekit.Init() // homekit source - ring.Init() // ring source + ring.Init() // ring source nest.Init() // nest source bubble.Init() // bubble source expr.Init() // expr source diff --git a/pkg/h265/helper.go b/pkg/h265/helper.go index 44605bde..c2ad94da 100644 --- a/pkg/h265/helper.go +++ b/pkg/h265/helper.go @@ -3,6 +3,7 @@ package h265 import ( "encoding/base64" "encoding/binary" + "github.com/AlexxIT/go2rtc/pkg/core" ) diff --git a/pkg/h265/payloader.go b/pkg/h265/payloader.go index 52ea2e4c..e4884118 100644 --- a/pkg/h265/payloader.go +++ b/pkg/h265/payloader.go @@ -2,8 +2,9 @@ package h265 import ( "encoding/binary" - "github.com/AlexxIT/go2rtc/pkg/h264" "math" + + "github.com/AlexxIT/go2rtc/pkg/h264" ) // diff --git a/pkg/hass/api.go b/pkg/hass/api.go index 6d5a9204..1dcedbb8 100644 --- a/pkg/hass/api.go +++ b/pkg/hass/api.go @@ -2,8 +2,9 @@ package hass import ( "errors" - "github.com/gorilla/websocket" "os" + + "github.com/gorilla/websocket" ) type API struct { diff --git a/pkg/mdns/mdns_test.go b/pkg/mdns/mdns_test.go index 956c9807..2d745208 100644 --- a/pkg/mdns/mdns_test.go +++ b/pkg/mdns/mdns_test.go @@ -1,8 +1,9 @@ package mdns import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestDiscovery(t *testing.T) { diff --git a/pkg/mjpeg/rtp.go b/pkg/mjpeg/rtp.go index 6f137f3e..7a9347ef 100644 --- a/pkg/mjpeg/rtp.go +++ b/pkg/mjpeg/rtp.go @@ -3,10 +3,11 @@ package mjpeg import ( "bytes" "encoding/binary" - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/pion/rtp" "image" "image/jpeg" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/pion/rtp" ) func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc { diff --git a/pkg/ngrok/ngrok.go b/pkg/ngrok/ngrok.go index 1a07d730..33fa0f89 100644 --- a/pkg/ngrok/ngrok.go +++ b/pkg/ngrok/ngrok.go @@ -3,10 +3,11 @@ package ngrok import ( "bufio" "encoding/json" - "github.com/AlexxIT/go2rtc/pkg/core" "io" "os/exec" "strings" + + "github.com/AlexxIT/go2rtc/pkg/core" ) type Ngrok struct { diff --git a/pkg/pcm/v1/pcm_test.go b/pkg/pcm/v1/pcm_test.go index 2db5d95c..07b70b97 100644 --- a/pkg/pcm/v1/pcm_test.go +++ b/pkg/pcm/v1/pcm_test.go @@ -1,9 +1,10 @@ package v1 import ( + "testing" + v2 "github.com/AlexxIT/go2rtc/pkg/pcm" "github.com/stretchr/testify/require" - "testing" ) func TestPCMUtoPCM(t *testing.T) { diff --git a/pkg/roborock/api.go b/pkg/roborock/api.go index 259be645..8876a85b 100644 --- a/pkg/roborock/api.go +++ b/pkg/roborock/api.go @@ -7,11 +7,12 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/AlexxIT/go2rtc/pkg/core" "net/http" "net/url" "strconv" "time" + + "github.com/AlexxIT/go2rtc/pkg/core" ) type UserInfo struct { diff --git a/pkg/tcp/websocket/dial.go b/pkg/tcp/websocket/dial.go index 737a5cbc..3e1fd481 100644 --- a/pkg/tcp/websocket/dial.go +++ b/pkg/tcp/websocket/dial.go @@ -5,10 +5,11 @@ import ( "crypto/sha1" "encoding/base64" "errors" - "github.com/AlexxIT/go2rtc/pkg/tcp" "net" "net/http" "strings" + + "github.com/AlexxIT/go2rtc/pkg/tcp" ) func Dial(address string) (net.Conn, error) { diff --git a/pkg/webtorrent/server.go b/pkg/webtorrent/server.go index e9e3a6b8..86b2be59 100644 --- a/pkg/webtorrent/server.go +++ b/pkg/webtorrent/server.go @@ -3,10 +3,11 @@ package webtorrent import ( "encoding/base64" "fmt" - "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/gorilla/websocket" "sync" "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/gorilla/websocket" ) type Server struct { From ccdb1479f7b2444962f1b798266e73eefac08251 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 17:46:13 +0300 Subject: [PATCH 84/87] Code refactoring for RtspToWeb format support #1632 --- internal/webrtc/server.go | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/internal/webrtc/server.go b/internal/webrtc/server.go index a59548b3..51565a74 100644 --- a/internal/webrtc/server.go +++ b/internal/webrtc/server.go @@ -5,7 +5,6 @@ import ( "encoding/json" "io" "net/http" - urlParser "net/url" "strconv" "strings" "time" @@ -64,8 +63,8 @@ func syncHandler(w http.ResponseWriter, r *http.Request) { // 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP) // 3. other - receive/response raw SDP func outputWebRTC(w http.ResponseWriter, r *http.Request) { - url := r.URL.Query().Get("src") - stream := streams.Get(url) + u := r.URL.Query().Get("src") + stream := streams.Get(u) if stream == nil { http.Error(w, api.StreamNotFound, http.StatusNotFound) return @@ -90,26 +89,19 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { offer = desc.SDP case "application/x-www-form-urlencoded": - body, err := io.ReadAll(r.Body) - if err != nil { + if err := r.ParseForm(); err != nil { log.Error().Err(err).Caller().Send() - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } - values, err := urlParser.ParseQuery(string(body)) + offerB64 := r.Form.Get("data") + b, err := base64.StdEncoding.DecodeString(offerB64) if err != nil { log.Error().Err(err).Caller().Send() http.Error(w, err.Error(), http.StatusBadRequest) return } - encodedOffer := values.Get("data") - decodedOffer, err := base64.StdEncoding.DecodeString(encodedOffer) - if err != nil { - log.Error().Err(err).Caller().Send() - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - offer = string(decodedOffer) + offer = string(b) default: body, err := io.ReadAll(r.Body) @@ -150,8 +142,8 @@ func outputWebRTC(w http.ResponseWriter, r *http.Request) { case "application/x-www-form-urlencoded": w.Header().Set("Content-Type", mediaType) - encodedAnswer := base64.StdEncoding.EncodeToString([]byte(answer)) - _, err = w.Write([]byte(encodedAnswer)) + answerB64 := base64.StdEncoding.EncodeToString([]byte(answer)) + _, err = w.Write([]byte(answerB64)) case MimeSDP: w.Header().Set("Content-Type", mediaType) From 117d767f0515d346ae53840fcef4307e3411addb Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 18:44:32 +0300 Subject: [PATCH 85/87] Code refactoring for SwitchBot format support #1629 --- internal/webrtc/client.go | 2 +- internal/webrtc/kinesis.go | 57 ++++++++++++------------- internal/webrtc/switchbot.go | 80 +++++++++++++----------------------- 3 files changed, 57 insertions(+), 82 deletions(-) diff --git a/internal/webrtc/client.go b/internal/webrtc/client.go index 9f21f4e9..106b603e 100644 --- a/internal/webrtc/client.go +++ b/internal/webrtc/client.go @@ -41,7 +41,7 @@ func streamsHandler(rawURL string) (core.Producer, error) { // https://aws.amazon.com/kinesis/video-streams/ // https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html // https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc - return kinesisClient(rawURL, query, "webrtc/kinesis", &kinesisClientOpts{}) + return kinesisClient(rawURL, query, "webrtc/kinesis", nil) } else if format == "openipc" { return openIPCClient(rawURL, query) } else if format == "switchbot" { diff --git a/internal/webrtc/kinesis.go b/internal/webrtc/kinesis.go index 42f76dce..b11d1d31 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -34,12 +34,10 @@ func (k kinesisResponse) String() string { return fmt.Sprintf("type=%s, payload=%s", k.Type, k.Payload) } -type kinesisClientOpts struct { - SessionDescriptionModifier func(*pion.SessionDescription) ([]byte, error) - MediaModifier func() ([]*core.Media, error) -} - -func kinesisClient(rawURL string, query url.Values, format string, opts *kinesisClientOpts) (core.Producer, error) { +func kinesisClient( + rawURL string, query url.Values, format string, + sdpOffer func(prod *webrtc.Conn, query url.Values) (any, error), +) (core.Producer, error) { // 1. Connect to signalign server conn, _, err := websocket.DefaultDialer.Dial(rawURL, nil) if err != nil { @@ -113,34 +111,33 @@ func kinesisClient(rawURL string, query url.Values, format string, opts *kinesis } }) - medias := []*core.Media{ - {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, - {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, - } - if opts.MediaModifier != nil { - medias, err = opts.MediaModifier() - if err != nil { + var payload any + + if sdpOffer == nil { + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + {Kind: core.KindAudio, Direction: core.DirectionRecvonly}, + } + + // 4. Create offer + var offer string + if offer, err = prod.CreateOffer(medias); err != nil { + return nil, err + } + + // 5. Send offer + payload = pion.SessionDescription{ + Type: pion.SDPTypeOffer, + SDP: offer, + } + } else { + if payload, err = sdpOffer(prod, query); err != nil { return nil, err } } - // 4. Create offer - offer, err := prod.CreateOffer(medias) - if err != nil { - return nil, err - } - - // 5. Send offer req.Action = "SDP_OFFER" - sessionDescription := pion.SessionDescription{ - Type: pion.SDPTypeOffer, - SDP: offer, - } - if opts.SessionDescriptionModifier != nil { - req.Payload, _ = opts.SessionDescriptionModifier(&sessionDescription) - } else { - req.Payload, _ = json.Marshal(sessionDescription) - } + req.Payload, _ = json.Marshal(payload) if err = conn.WriteJSON(req); err != nil { return nil, err } @@ -234,5 +231,5 @@ func wyzeClient(rawURL string) (core.Producer, error) { "ice_servers": []string{string(kvs.Servers)}, } - return kinesisClient(kvs.URL, query, "webrtc/wyze", &kinesisClientOpts{}) + return kinesisClient(kvs.URL, query, "webrtc/wyze", nil) } diff --git a/internal/webrtc/switchbot.go b/internal/webrtc/switchbot.go index 09d0c5b1..5ece88ae 100644 --- a/internal/webrtc/switchbot.go +++ b/internal/webrtc/switchbot.go @@ -1,62 +1,40 @@ package webrtc import ( - "encoding/json" "net/url" - "strings" "github.com/AlexxIT/go2rtc/pkg/core" - pion "github.com/pion/webrtc/v3" + "github.com/AlexxIT/go2rtc/pkg/webrtc" ) -// SessionDescription is used to expose local and remote session descriptions. -type SwitchBotSessionDescription struct { - Type string `json:"type"` - SDP string `json:"sdp"` - Resolution SwitchBotResolution `json:"resolution"` - PlayType int `json:"play_type"` -} - func switchbotClient(rawURL string, query url.Values) (core.Producer, error) { - return kinesisClient(rawURL, query, "webrtc/switchbot", &kinesisClientOpts{ - SessionDescriptionModifier: func(sd *pion.SessionDescription) ([]byte, error) { - resolution, ok := parseSwitchBotResolution(query.Get("resolution")) - if !ok { - resolution = SwitchBotResolutionSD - } - json, err := json.Marshal(SwitchBotSessionDescription{ - Type: sd.Type.String(), - SDP: sd.SDP, - Resolution: resolution, - PlayType: 0, - }) - return json, err - }, - MediaModifier: func() ([]*core.Media, error) { - return []*core.Media{ - {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, - //{Kind: core.KindAudio, Direction: core.DirectionRecvonly}, - //{Kind: core.KindAudio, Direction: core.DirectionSendRecv}, - //{Kind: "Data", Direction: core.DirectionSendRecv}, - }, nil - }, + return kinesisClient(rawURL, query, "webrtc/switchbot", func(prod *webrtc.Conn, query url.Values) (any, error) { + medias := []*core.Media{ + {Kind: core.KindVideo, Direction: core.DirectionRecvonly}, + } + + offer, err := prod.CreateOffer(medias) + if err != nil { + return nil, err + } + + v := struct { + Type string `json:"type"` + SDP string `json:"sdp"` + Resolution int `json:"resolution"` + PlayType int `json:"play_type"` + }{ + Type: "offer", + SDP: offer, + } + + switch query.Get("resolution") { + case "hd": + v.Resolution = 0 + case "sd": + v.Resolution = 1 + } + + return v, nil }) } - -type SwitchBotResolution int - -const ( - SwitchBotResolutionHD SwitchBotResolution = 0 - SwitchBotResolutionSD = 1 -) - -func parseSwitchBotResolution(str string) (SwitchBotResolution, bool) { - var ( - resolutionMap = map[string]SwitchBotResolution{ - "hd": SwitchBotResolutionHD, - "sd": SwitchBotResolutionSD, - } - ) - c, ok := resolutionMap[strings.ToLower(str)] - return c, ok -} From 761ff7ed5a513a2c1939a089536223ed68f2a5f5 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Mar 2025 18:48:18 +0300 Subject: [PATCH 86/87] Update readme for SwitchBot --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cec2f247..90a2537f 100644 --- a/README.md +++ b/README.md @@ -684,16 +684,16 @@ Supports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-str **switchbot** -Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). (`Outdoor Spotlight Cam 1080P`,`Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`,`Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available .) +Support connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available. ```yaml streams: - webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 - webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 - webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] - webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze - webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] - webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=HD#client_id=...#ice_servers=[{...},{...}] + webrtc-whep: webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1 + webrtc-go2rtc: webrtc:ws://192.168.1.123:1984/api/ws?src=camera1 + webrtc-openipc: webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{"urls":"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443"}] + webrtc-wyze: webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze + webrtc-kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}] + webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#client_id=...#ice_servers=[{...},{...}] ``` **PS.** For `kinesis` sources you can use [echo](#source-echo) to get connection params using `bash`/`python` or any other script language. From fa580c516efa95770be2137855c3c3cf19b8092b Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 10 Mar 2025 05:51:40 +0300 Subject: [PATCH 87/87] Update version to 1.9.9 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 5708f973..8a62bdb6 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,7 @@ import ( ) func main() { - app.Version = "1.9.8" + app.Version = "1.9.9" // 1. Core modules: app, api/ws, streams