diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e709fad7..fba8c851 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,87 +15,87 @@ jobs: env: { CGO_ENABLED: 0 } steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: { go-version: '1.21' } - name: Build go2rtc_win64 env: { GOOS: windows, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_win64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_win64, path: go2rtc.exe } - name: Build go2rtc_win32 env: { GOOS: windows, GOARCH: 386 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_win32 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_win32, path: go2rtc.exe } - name: Build go2rtc_win_arm64 env: { GOOS: windows, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_win_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_win_arm64, path: go2rtc.exe } - name: Build go2rtc_linux_amd64 env: { GOOS: linux, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_linux_amd64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_linux_amd64, path: go2rtc } - name: Build go2rtc_linux_i386 env: { GOOS: linux, GOARCH: 386 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_linux_i386 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_linux_i386, path: go2rtc } - name: Build go2rtc_linux_arm64 env: { GOOS: linux, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_linux_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_linux_arm64, path: go2rtc } - name: Build go2rtc_linux_arm env: { GOOS: linux, GOARCH: arm, GOARM: 7 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_linux_arm - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_linux_arm, path: go2rtc } - name: Build go2rtc_linux_armv6 env: { GOOS: linux, GOARCH: arm, GOARM: 6 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_linux_armv6 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_linux_armv6, path: go2rtc } - name: Build go2rtc_linux_mipsel env: { GOOS: linux, GOARCH: mipsle } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_linux_mipsel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_linux_mipsel, path: go2rtc } - name: Build go2rtc_mac_amd64 env: { GOOS: darwin, GOARCH: amd64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_mac_amd64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_mac_amd64, path: go2rtc } - name: Build go2rtc_mac_arm64 env: { GOOS: darwin, GOARCH: arm64 } run: go build -ldflags "-s -w" -trimpath - name: Upload go2rtc_mac_arm64 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: { name: go2rtc_mac_arm64, path: go2rtc } docker-master: @@ -103,11 +103,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ github.repository }} tags: | @@ -116,20 +116,20 @@ jobs: type=match,pattern=v(.*),group=1 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . platforms: | @@ -148,11 +148,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Docker meta id: meta-hw - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ github.repository }} flavor: | @@ -164,20 +164,20 @@ jobs: type=match,pattern=v(.*),group=1 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: hardware.Dockerfile diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index f3d85c4d..4d0e2e67 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -25,13 +25,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: './website' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a98a83e5..b23faf53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,10 +21,10 @@ jobs: GOARCH: ${{ matrix.arch }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: '1.21' @@ -70,13 +70,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . platforms: linux/${{ matrix.platform }} @@ -89,7 +89,7 @@ jobs: - name: Build and push Hardware if: matrix.platform == 'amd64' - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . file: hardware.Dockerfile diff --git a/README.md b/README.md index aaed9410..8016f6b1 100644 --- a/README.md +++ b/README.md @@ -146,13 +146,13 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support Latest, but maybe unstable version: -- Binary: GitHub > [Actions](https://github.com/AlexxIT/go2rtc/actions) > [Build and Push](https://github.com/AlexxIT/go2rtc/actions/workflows/build.yml) > latest run > Artifacts section (you should be logged in to GitHub) +- Binary: [latest nightly release](https://nightly.link/AlexxIT/go2rtc/workflows/build/master) - Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions - Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions ## Configuration -- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory +- by default go2rtc will search `go2rtc.yaml` in the current work directory - `api` server will start on default **1984 port** (TCP) - `rtsp` server will start on default **8554 port** (TCP) - `webrtc` will use port **8555** (TCP/UDP) for connections @@ -230,7 +230,7 @@ streams: 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 - glichy_camera: ffmpeg:rstp://username:password@192.168.1.123/live/ch00_1 + glichy_camera: ffmpeg:rtsp://username:password@192.168.1.123/live/ch00_1 ``` **Recommendations** @@ -265,7 +265,7 @@ streams: #### Source: RTMP -You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). +You can get stream from RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module). ```yaml streams: @@ -579,7 +579,7 @@ streams: Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this fomat. -The Nest API only allows you to get a link to a stream for 5 minutes. So every 5 minutes the stream will be reconnected. +**Important.** The Nest API only allows you to get a link to a stream for 5 minutes. Do not use this with Frigate! If the stream expires, Frigate will consume all available ram on your machine within seconds. It's recommended to use [Nest source](#source-nest) - it supports extending the stream. ```yaml streams: @@ -610,7 +610,7 @@ streams: *[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)* -Currently only WebRTC cameras are supported. Stream reconnects every 5 minutes. +Currently only WebRTC cameras are supported. For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](#source-hass). But if you can somehow get the below parameters - Nest/WebRTC source will work without Hass. @@ -640,7 +640,7 @@ This source type support four connection formats. **whep** -[WebRTC/WHEP](https://www.ietf.org/id/draft-murillo-whep-02.html) - is an unapproved standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc. +[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) - is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc. **go2rtc** @@ -1352,6 +1352,7 @@ streams: **Distributions** - [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc) +- [Arch User Repository](https://linux-packages.com/aur/package/go2rtc) - [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc) - [NixOS](https://search.nixos.org/packages?query=go2rtc) - [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/) diff --git a/hardware.Dockerfile b/hardware.Dockerfile index 238ede69..0aa85374 100644 --- a/hardware.Dockerfile +++ b/hardware.Dockerfile @@ -53,7 +53,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t python3 curl jq \ intel-media-va-driver-non-free \ mesa-va-drivers \ - libasound2-plugins + libasound2-plugins && \ + apt-get clean && rm -rf /var/lib/apt/lists/* COPY --link --from=rootfs / / diff --git a/internal/api/api.go b/internal/api/api.go index db23360a..d61653e4 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -11,6 +11,7 @@ import ( "strings" "sync" "syscall" + "time" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/shell" @@ -96,7 +97,10 @@ func listen(network, address string) { Port = ln.Addr().(*net.TCPAddr).Port } - server := http.Server{Handler: Handler} + server := http.Server{ + Handler: Handler, + ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds + } if err = server.Serve(ln); err != nil { log.Fatal().Err(err).Msg("[api] serve") } @@ -126,8 +130,9 @@ func tlsListen(network, address, certFile, keyFile string) { log.Info().Str("addr", address).Msg("[api] tls listen") server := &http.Server{ - Handler: Handler, - TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, + Handler: Handler, + TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, + ReadHeaderTimeout: 5 * time.Second, } if err = server.ServeTLS(ln, "", ""); err != nil { log.Fatal().Err(err).Msg("[api] tls serve") diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 36dacfaa..6bc5698a 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "strings" "sync" "time" @@ -108,7 +109,7 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) { waitersMu.Unlock() }() - log.Debug().Str("url", url).Msg("[exec] run") + log.Debug().Str("url", url).Str("cmd", fmt.Sprintf("%s", strings.Join(cmd.Args, " "))).Msg("[exec] run") ts := time.Now() diff --git a/pkg/hap/camera/accessory.go b/pkg/hap/camera/accessory.go index fca77de8..42037d96 100644 --- a/pkg/hap/camera/accessory.go +++ b/pkg/hap/camera/accessory.go @@ -62,6 +62,7 @@ func ServiceCameraRTPStreamManagement() *hap.Service { VideoAttrs: []VideoAttrs{ {Width: 1920, Height: 1080, Framerate: 30}, {Width: 1280, Height: 720, Framerate: 30}, // important for iPhones + {Width: 320, Height: 240, Framerate: 15}, // apple watch }, }, }, diff --git a/pkg/ivideon/client.go b/pkg/ivideon/client.go index 0158f08d..c1b055b8 100644 --- a/pkg/ivideon/client.go +++ b/pkg/ivideon/client.go @@ -132,6 +132,9 @@ func (c *Client) Handle() error { case "stream-init": continue + case "metadata": + continue + case "fragment": _, data, err = c.conn.ReadMessage() if err != nil { @@ -183,6 +186,9 @@ func (c *Client) getTracks() error { } switch msg.Type { + case "metadata": + continue + case "stream-init": s := msg.CodecString i := strings.IndexByte(s, '.') diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 9c7f4546..5e0d3407 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -14,6 +14,13 @@ import ( type API struct { Token string ExpiresAt time.Time + + StreamProjectID string + StreamDeviceID string + StreamSessionID string + StreamExpiresAt time.Time + + extendTimer *time.Timer } type Auth struct { @@ -159,7 +166,7 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { Results struct { Answer string `json:"answerSdp"` ExpiresAt time.Time `json:"expiresAt"` - MediaSessionId string `json:"mediaSessionId"` + MediaSessionID string `json:"mediaSessionId"` } `json:"results"` } @@ -167,9 +174,65 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { return "", err } + a.StreamProjectID = projectID + a.StreamDeviceID = deviceID + a.StreamSessionID = resv.Results.MediaSessionID + a.StreamExpiresAt = resv.Results.ExpiresAt + return resv.Results.Answer, nil } +func (a *API) ExtendStream() error { + var reqv struct { + Command string `json:"command"` + Params struct { + MediaSessionID string `json:"mediaSessionId"` + } `json:"params"` + } + reqv.Command = "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream" + reqv.Params.MediaSessionID = a.StreamSessionID + + 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) + } + + var resv struct { + Results struct { + ExpiresAt time.Time `json:"expiresAt"` + MediaSessionID string `json:"mediaSessionId"` + } `json:"results"` + } + + if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { + return err + } + + a.StreamSessionID = resv.Results.MediaSessionID + a.StreamExpiresAt = resv.Results.ExpiresAt + + return nil +} + type Device struct { Name string `json:"name"` Type string `json:"type"` @@ -203,3 +266,23 @@ type Device struct { // DisplayName string `json:"displayName"` //} `json:"parentRelations"` } + +func (a *API) StartExtendStreamTimer() { + // Calculate the duration until 30 seconds before the stream expires + duration := time.Until(a.StreamExpiresAt.Add(-30 * time.Second)) + a.extendTimer = time.AfterFunc(duration, func() { + if err := a.ExtendStream(); err != nil { + return + } + duration = time.Until(a.StreamExpiresAt.Add(-30 * time.Second)) + a.extendTimer.Reset(duration) + }) + +} + +func (a *API) StopExtendStreamTimer() { + if a.extendTimer == nil { + return + } + a.extendTimer.Stop() +} diff --git a/pkg/nest/client.go b/pkg/nest/client.go index b2b0c964..cb73cc98 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -11,6 +11,7 @@ import ( type Client struct { conn *webrtc.Conn + api *API } func NewClient(rawURL string) (*Client, error) { @@ -74,7 +75,7 @@ func NewClient(rawURL string) (*Client, error) { return nil, err } - return &Client{conn: conn}, nil + return &Client{conn: conn, api: nestAPI}, nil } func (c *Client) GetMedias() []*core.Media { @@ -90,10 +91,12 @@ func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Rece } func (c *Client) Start() error { + c.api.StartExtendStreamTimer() return c.conn.Start() } func (c *Client) Stop() error { + c.api.StopExtendStreamTimer() return c.conn.Stop() } diff --git a/pkg/tapo/client.go b/pkg/tapo/client.go index 5f1f2465..6955fa4d 100644 --- a/pkg/tapo/client.go +++ b/pkg/tapo/client.go @@ -281,7 +281,7 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) { auth := res.Header.Get("WWW-Authenticate") if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") { - return nil, nil, err + return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode) } if password == "" { diff --git a/www/add.html b/www/add.html index 2b1bb9d7..3058f8dc 100644 --- a/www/add.html +++ b/www/add.html @@ -5,11 +5,6 @@
diff --git a/www/editor.html b/www/editor.html index c24e7f95..3e0de699 100644 --- a/www/editor.html +++ b/www/editor.html @@ -4,7 +4,7 @@