From bc516bce7ddf45440791daacd60048e5d520c063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Simonsen?= Date: Wed, 3 Jan 2024 15:08:21 +0100 Subject: [PATCH 01/30] Adds automatic extention of nest stream before it expires. --- pkg/nest/api.go | 62 ++++++++++++++++++++++++++++++++++++++++------ pkg/nest/client.go | 39 ++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/pkg/nest/api.go b/pkg/nest/api.go index 9c7f4546..e260799e 100644 --- a/pkg/nest/api.go +++ b/pkg/nest/api.go @@ -121,7 +121,7 @@ func (a *API) GetDevices(projectID string) (map[string]string, error) { return devices, nil } -func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { +func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, string, time.Time, error) { var reqv struct { Command string `json:"command"` Params struct { @@ -133,14 +133,14 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { b, err := json.Marshal(reqv) if err != nil { - return "", err + return "", "", time.Time{}, 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 + return "", "", time.Time{}, err } req.Header.Set("Authorization", "Bearer "+a.Token) @@ -148,11 +148,11 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { client := &http.Client{Timeout: time.Second * 5000} res, err := client.Do(req) if err != nil { - return "", err + return "", "", time.Time{}, err } if res.StatusCode != 200 { - return "", errors.New("nest: wrong status: " + res.Status) + return "", "", time.Time{}, errors.New("nest: wrong status: " + res.Status) } var resv struct { @@ -164,10 +164,58 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) { } if err = json.NewDecoder(res.Body).Decode(&resv); err != nil { - return "", err + return "", "", time.Time{}, err } - return resv.Results.Answer, nil + return resv.Results.Answer, resv.Results.MediaSessionId, resv.Results.ExpiresAt, nil +} + +func (a *API) ExtendStream(projectID, deviceID, mediaSessionID string) (string, time.Time, 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 = mediaSessionID + + b, err := json.Marshal(reqv) + if err != nil { + return "", time.Time{}, 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 "", time.Time{}, err + } + + req.Header.Set("Authorization", "Bearer "+a.Token) + + client := &http.Client{Timeout: time.Second * 5000} + res, err := client.Do(req) + if err != nil { + return "", time.Time{}, err + } + + if res.StatusCode != 200 { + return "", time.Time{}, 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 "", time.Time{}, err + } + + return resv.Results.MediaSessionId, resv.Results.ExpiresAt, nil } type Device struct { diff --git a/pkg/nest/client.go b/pkg/nest/client.go index b2b0c964..6beabd81 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -4,13 +4,21 @@ import ( "errors" "net/url" + "time" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" ) type Client struct { - conn *webrtc.Conn + conn *webrtc.Conn + projectId string + deviceId string + mediaSessionId string + streamExpiresAt time.Time + nestApi *API + timer *time.Timer } func NewClient(rawURL string) (*Client, error) { @@ -64,7 +72,7 @@ func NewClient(rawURL string) (*Client, error) { } // 4. Exchange SDP via Hass - answer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer) + answer, mediaSessionId, expiresAt, err := nestAPI.ExchangeSDP(projectID, deviceID, offer) if err != nil { return nil, err } @@ -74,7 +82,7 @@ func NewClient(rawURL string) (*Client, error) { return nil, err } - return &Client{conn: conn}, nil + return &Client{conn: conn, deviceId: deviceID, projectId: projectID, mediaSessionId: mediaSessionId, streamExpiresAt: expiresAt, nestApi: nestAPI}, nil } func (c *Client) GetMedias() []*core.Media { @@ -90,10 +98,35 @@ func (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Rece } func (c *Client) Start() error { + c.StartExtendStreamTimer() + return c.conn.Start() } +func (c *Client) StartExtendStreamTimer() { + ontimer := func() { + c.ExtendStream() + c.StartExtendStreamTimer() + } + // Calculate the duration until 30 seconds before the stream expires + duration := time.Until(c.streamExpiresAt.Add(-30 * time.Second)) + + // Start the timer + c.timer = time.AfterFunc(duration, ontimer) +} + +func (c *Client) ExtendStream() error { + mediaSessionId, expiresAt, err := c.nestApi.ExtendStream(c.projectId, c.deviceId, c.mediaSessionId) + if err != nil { + return err + } + c.mediaSessionId = mediaSessionId + c.streamExpiresAt = expiresAt + return nil +} + func (c *Client) Stop() error { + c.timer.Stop() return c.conn.Stop() } From db190e69ed9a7d6a85313f57ee9c43897b765db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Simonsen?= Date: Wed, 3 Jan 2024 15:16:59 +0100 Subject: [PATCH 02/30] Updated README with more accurate information regarding nest integration. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2cfa2ed8..367f9048 100644 --- a/README.md +++ b/README.md @@ -579,7 +579,8 @@ 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. +The Nest API only allows you to get a link to a stream for 5 minutes, but a call to extend the stream is made before it expires. If for some reason the stream expires anyway, any streaming clients will lose connection. +Note: Do not use this with frigate. If the stream expires, Frigate will consume all available ram on your machine within seconds. ```yaml streams: From f8d9fccf74314de8b34ef7b31ffb23132dadd5a9 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 12 Jan 2024 07:43:12 +0300 Subject: [PATCH 03/30] fix(log-display): reverse log order to display newest first The The applyLogStyling function in log.html has been updated to reverse the array of log lines. After parsing the JSON data, reversing the array ensures that the most recent logs appear at the top of the list. This change enhances the readability for users by displaying the logs in a descending chronological order. --- www/log.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/log.html b/www/log.html index 138b03f4..0eb1c4f3 100644 --- a/www/log.html +++ b/www/log.html @@ -87,7 +87,7 @@ function applyLogStyling(jsonlines) { const KEYS = ['time', 'level', 'message']; - const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']'); + const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']').reverse(); return lines.map(line => { const ts = new Date(line['time']); const msg = Object.keys(line).reduce((msg, key) => { @@ -127,4 +127,4 @@ }, 5000); - \ No newline at end of file + From 6f9b8b732d33597923f2e2832113a7fb56eb150e Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Mon, 12 Feb 2024 05:02:21 +0000 Subject: [PATCH 04/30] Initial commit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aaed9410..d9b33957 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 an 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** From c0455a20aac3c96c7c27c9f4642681623a8f0f09 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sat, 17 Feb 2024 01:46:11 +0300 Subject: [PATCH 05/30] fix grammar Co-authored-by: Felipe Santos --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9b33957..192b37d4 100644 --- a/README.md +++ b/README.md @@ -640,7 +640,7 @@ This source type support four connection formats. **whep** -[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) - is an 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. +[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** From 6fbd141576e2708ffe494deaa2a53aba866410b9 Mon Sep 17 00:00:00 2001 From: civita <14911217+civita@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:18:53 -0800 Subject: [PATCH 06/30] pkg/hap/camera/accessory.go --- pkg/hap/camera/accessory.go | 1 + 1 file changed, 1 insertion(+) 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 }, }, }, From 14a9763c73dcbe0e46d602d8437aa46baaf94e88 Mon Sep 17 00:00:00 2001 From: Josip Janzic Date: Fri, 23 Feb 2024 16:39:29 +0000 Subject: [PATCH 07/30] Fix crash with tapo cameras not returning 201 --- 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 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 == "" { From 9f7448d255aef0615a4c57635d8241aca75e0513 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sat, 24 Feb 2024 13:14:51 +0300 Subject: [PATCH 08/30] ci: upgrade GitHub Actions to newer versions Updated various GitHub Actions used in the CI workflows (build.yml, gh-pages.yml, test.yml) to their latest major versions. This includes actions for checking out code, setting up Go, uploading artifacts, configuring Docker, and deploying to GitHub Pages. The update is part of routine maintenance to ensure compatibility with the latest features and improvements provided by these actions. --- .github/workflows/build.yml | 50 +++++++++++++++++----------------- .github/workflows/gh-pages.yml | 8 +++--- .github/workflows/test.yml | 14 +++++----- 3 files changed, 36 insertions(+), 36 deletions(-) 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 From 63de86a409b732d0b220491d6600f78d7f8ac85f Mon Sep 17 00:00:00 2001 From: pabst2k <54397544+pabst2k@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:52:27 +0100 Subject: [PATCH 09/30] Update README.md fix: Typo in url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aaed9410..e7b89154 100644 --- a/README.md +++ b/README.md @@ -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** From a50c99b8e560dec7540971be0cb0df893bae1afc Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 22 Mar 2024 16:49:25 +0300 Subject: [PATCH 10/30] feat(log): introduce toggle for reversing log order Added a button to the log page allowing users to toggle the order in which logs are displayed (normal or reversed). This feature enhances user experience by providing flexibility in viewing logs. The implementation involves a boolean flag `reverseOrder` to track the current state of log order and dynamically updates the button text to reflect the current mode. Additionally, the log fetching function now conditionally reverses the log array based on this flag, ensuring that the display order matches the user's preference. This change could significantly improve usability for users needing to analyze recent events without scrolling through the entire log history. --- www/log.html | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/www/log.html b/www/log.html index 0eb1c4f3..7517bddd 100644 --- a/www/log.html +++ b/www/log.html @@ -54,6 +54,7 @@
+

@@ -85,9 +86,14 @@ .replace(/\n/g, '
'); } + let reverseOrder = false; + function applyLogStyling(jsonlines) { const KEYS = ['time', 'level', 'message']; - const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']').reverse(); + const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']'); + if (reverseOrder) { + lines = lines.reverse(); + } return lines.map(line => { const ts = new Date(line['time']); const msg = Object.keys(line).reduce((msg, key) => { @@ -121,6 +127,14 @@ update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`; }); + // Toggle log order + const reverseBtn = document.getElementById('reverse'); + reverseBtn.addEventListener('click', () => { + reverseOrder = !reverseOrder; + reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`; + reload(); // Reload logs to apply the new order + }); + // Reload the logs every 5 seconds setInterval(() => { if (autoUpdateEnabled) reload(); From de70b0a8611cc23944f567bfa890a04a98d535a3 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 22 Mar 2024 17:08:12 +0300 Subject: [PATCH 11/30] some fixes --- www/log.html | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/www/log.html b/www/log.html index 7517bddd..42c1fcd8 100644 --- a/www/log.html +++ b/www/log.html @@ -54,7 +54,7 @@
- +

@@ -86,11 +86,18 @@ .replace(/\n/g, '
'); } + let reverseBtn = document.getElementById('reverse'); + let update = document.getElementById('update'); + let reverseOrder = false; + let autoUpdateEnabled = true; + + reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`; + update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`; function applyLogStyling(jsonlines) { const KEYS = ['time', 'level', 'message']; - const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']'); + let lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']'); if (reverseOrder) { lines = lines.reverse(); } @@ -118,17 +125,14 @@ reload(); - // Handle auto-update switch - let autoUpdateEnabled = true; - - const update = document.getElementById('update'); + update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`; update.addEventListener('click', () => { autoUpdateEnabled = !autoUpdateEnabled; update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`; }); // Toggle log order - const reverseBtn = document.getElementById('reverse'); + reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`; reverseBtn.addEventListener('click', () => { reverseOrder = !reverseOrder; reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`; From 31398a7e6bfd3abcd4ada82c73e783986a04cd4f Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 22 Mar 2024 18:08:47 +0300 Subject: [PATCH 12/30] feat(dark-mode): implement dark mode and centralize CSS Implemented a dark mode feature for the website, including a toggle button in the navigation bar that allows users to switch between light and dark themes. To support this feature, centralized common CSS styles (such as body, table, and button stylings) into main.js to ensure consistent application across all HTML pages. This change improves user experience by providing a visually comfortable alternative for low-light environments and centralizes styling rules for easier maintenance. - Added dark mode styles for body, table, buttons, and navigation elements in main.js. - Introduced a toggle mechanism in the navigation bar to switch between light and dark modes. - Utilized JavaScript to detect system theme preference (`prefers-color-scheme`) and persist user's theme choice using localStorage. - Removed duplicate and scattered CSS rules from individual HTML files (add.html, index.html, links.html, log.html) and centralized them in main.js to reduce redundancy and facilitate easier updates in the future. This update enhances accessibility and user preference compliance by allowing users to select their desired theme while simplifying CSS management across the website. --- www/add.html | 27 ----------- www/index.html | 28 ----------- www/links.html | 4 -- www/log.html | 23 ++------- www/main.js | 123 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 79 deletions(-) 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/index.html b/www/index.html index 4e4f9992..b5d3b449 100644 --- a/www/index.html +++ b/www/index.html @@ -8,39 +8,11 @@ go2rtc diff --git a/www/main.js b/www/main.js index 63dbde32..d00fca88 100644 --- a/www/main.js +++ b/www/main.js @@ -41,6 +41,77 @@ nav a:hover { nav li { display: inline; } + +body { + font-family: Arial, Helvetica, sans-serif; + background-color: white; +} +table { + background-color: white; + text-align: left; + border-collapse: collapse; +} +table thead { + background: #CFCFCF; + background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%); + border-bottom: 3px solid black; +} +table thead th { + font-size: 15px; + font-weight: bold; + color: black; + text-align: center; +} +table td, table th { + border: 1px solid black; + padding: 5px 5px; +} + +/* Dark mode styles */ +body.dark-mode { + background-color: #121212; + color: #e0e0e0; +} + +body.dark-mode nav ul { + background: #333; +} + +body.dark-mode nav a { + background: rgba(255, 255, 255, .1); + border-right: 1px solid #444; + color: #ccc; +} + +body.dark-mode nav a:hover { + background: #555; +} + +body.dark-mode table { + background-color: #222; + color: #ddd; +} + +body.dark-mode table thead { + background: linear-gradient(to bottom, #444 0%, #3d3d3d 66%, #333 100%); + border-bottom: 3px solid #888; +} +body.dark-mode table thead th { + font-size: 15px; + font-weight: bold; + color: #ddd; + text-align: center; +} +body.dark-mode table td, body.dark-mode table th { + border: 1px solid #444; +} + +body.dark-mode button { + background: rgba(255, 255, 255, .1); + border: 1px solid #444; + color: #ccc; +} + ` + document.body.innerHTML; + +const sunIcon = '☀️'; +const moonIcon = '🌕'; + +document.addEventListener('DOMContentLoaded', () => { + const darkModeToggle = document.getElementById('darkModeToggle'); + const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); + + const updateToggleButton = () => { + if (isDarkModeEnabled()) { + darkModeToggle.innerHTML = sunIcon; + darkModeToggle.setAttribute('aria-label', 'Enable light mode'); + } else { + darkModeToggle.innerHTML = moonIcon; + darkModeToggle.setAttribute('aria-label', 'Enable dark mode'); + } + }; + + const isDarkModeEnabled = () => document.body.classList.contains('dark-mode'); + + const updateDarkMode = () => { + if (prefersDarkScheme.matches) { + document.body.classList.add('dark-mode'); + } else { + document.body.classList.remove('dark-mode'); + } + }; + + updateDarkMode(); + updateToggleButton(); + + prefersDarkScheme.addListener(updateDarkMode); + + darkModeToggle.addEventListener('click', () => { + document.body.classList.toggle('dark-mode'); + if (document.body.classList.contains('dark-mode')) { + localStorage.setItem('darkMode', 'enabled'); + darkModeToggle.innerHTML = sunIcon; + } else { + localStorage.removeItem('darkMode'); + darkModeToggle.innerHTML = moonIcon; + } + }); + + if (localStorage.getItem('darkMode') === 'enabled' || prefersDarkScheme.matches) { + document.body.classList.add('dark-mode'); + } +}); From 20dd16badf25bc971bae37b34a2cb55281b1d24e Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sat, 23 Mar 2024 04:30:54 +0300 Subject: [PATCH 13/30] feat(dark-mode): improve contrast and visited link styles --- www/main.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/www/main.js b/www/main.js index d00fca88..2b94720d 100644 --- a/www/main.js +++ b/www/main.js @@ -77,16 +77,20 @@ body.dark-mode nav ul { background: #333; } -body.dark-mode nav a { - background: rgba(255, 255, 255, .1); - border-right: 1px solid #444; - color: #ccc; +body.dark-mode a { + background: rgba(45, 45, 45, .8); + border-right: 1px solid #2c2c2c; + color: #c7c7c7; } -body.dark-mode nav a:hover { +body.dark-mode a:hover { background: #555; } +body.dark-mode a:visited { + color: #999; +} + body.dark-mode table { background-color: #222; color: #ddd; From 4ddadc08cb3909decdb1ba4511cb7f126c6b7d10 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 9 Apr 2024 04:16:38 +0300 Subject: [PATCH 14/30] feat(editor-theme): dynamically set editor theme based on dark mode preference --- www/main.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/www/main.js b/www/main.js index 2b94720d..f779a659 100644 --- a/www/main.js +++ b/www/main.js @@ -138,6 +138,9 @@ document.addEventListener('DOMContentLoaded', () => { const darkModeToggle = document.getElementById('darkModeToggle'); const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); + const isDarkModeEnabled = () => document.body.classList.contains('dark-mode'); + + // Update the toggle button based on the dark mode state const updateToggleButton = () => { if (isDarkModeEnabled()) { darkModeToggle.innerHTML = sunIcon; @@ -148,33 +151,33 @@ document.addEventListener('DOMContentLoaded', () => { } }; - const isDarkModeEnabled = () => document.body.classList.contains('dark-mode'); - const updateDarkMode = () => { - if (prefersDarkScheme.matches) { + if (localStorage.getItem('darkMode') === 'enabled' || prefersDarkScheme.matches && localStorage.getItem('darkMode') !== 'disabled') { document.body.classList.add('dark-mode'); } else { document.body.classList.remove('dark-mode'); } + updateEditorTheme(); + updateToggleButton(); }; + // Update the editor theme based on the dark mode state + const updateEditorTheme = () => { + if (typeof editor !== 'undefined') { + editor.setTheme(isDarkModeEnabled() ? "ace/theme/tomorrow_night_eighties" : "ace/theme/github"); } + }; + + // Initial update for dark mode and toggle button updateDarkMode(); - updateToggleButton(); - prefersDarkScheme.addListener(updateDarkMode); + // Listen for changes in the system's color scheme preference + prefersDarkScheme.addEventListener('change', updateDarkMode); // Modern approach + // Toggle dark mode and update local storage on button click darkModeToggle.addEventListener('click', () => { - document.body.classList.toggle('dark-mode'); - if (document.body.classList.contains('dark-mode')) { - localStorage.setItem('darkMode', 'enabled'); - darkModeToggle.innerHTML = sunIcon; - } else { - localStorage.removeItem('darkMode'); - darkModeToggle.innerHTML = moonIcon; - } + const enabled = document.body.classList.toggle('dark-mode'); + localStorage.setItem('darkMode', enabled ? 'enabled' : 'disabled'); + updateToggleButton(); // Update the button after toggling + updateEditorTheme(); }); - - if (localStorage.getItem('darkMode') === 'enabled' || prefersDarkScheme.matches) { - document.body.classList.add('dark-mode'); - } }); From 51e20497acc2394088df3463b13edc308071f2ef Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 9 Apr 2024 09:32:59 +0300 Subject: [PATCH 15/30] fix(styles): implement flex layout for body element --- www/index.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/index.html b/www/index.html index b5d3b449..f4668a51 100644 --- a/www/index.html +++ b/www/index.html @@ -9,6 +9,13 @@ ` + + `` + `` + ``; - tbody.appendChild(tr); } + + // Remove old rows + existingIds.forEach(id => { + if (!fetchedIds.includes(id)) { + const trToRemove = tbody.querySelector(`tr[data-id="${id}"]`); + tbody.removeChild(trToRemove); + } + }); }); } // Auto-reload every 5 seconds setInterval(reload, 5000); - const url = new URL('api', location.href); - fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => { + const url2 = new URL('api', location.href); + fetch(url2, {cache: 'no-cache'}).then(r => r.json()).then(data => { const info = document.querySelector('.info'); info.innerText = `Version: ${data.version}, Config: ${data.config_path}`; }); From 5cf343cb691ba43345d9fb9dc10e655dc4f3b8c5 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 18 Apr 2024 02:56:32 +0300 Subject: [PATCH 23/30] feat(autoreload): change interval from 5 seconds to 1 second --- www/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/index.html b/www/index.html index 9af39bdf..35c4606d 100644 --- a/www/index.html +++ b/www/index.html @@ -161,8 +161,8 @@ }); } - // Auto-reload every 5 seconds - setInterval(reload, 5000); + // Auto-reload + setInterval(reload, 1000); const url2 = new URL('api', location.href); fetch(url2, {cache: 'no-cache'}).then(r => r.json()).then(data => { From d28ae5caea173c00f0357a8dbacc3eb1eb93aee9 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 18 Apr 2024 03:27:50 +0300 Subject: [PATCH 24/30] docs(readme): update link for latest binary download method --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aaed9410..e278a730 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ 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 From 8495c7350e1d0363db485182da911313745e61c3 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 20 Apr 2024 11:47:03 +0300 Subject: [PATCH 25/30] Add Arch dist to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index aaed9410..4bc74af7 100644 --- a/README.md +++ b/README.md @@ -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/) From 166287ce1b7c64509106af1689369568c63e53f4 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 20 Apr 2024 11:57:48 +0300 Subject: [PATCH 26/30] Rename constant back to old name --- www/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/index.html b/www/index.html index 35c4606d..a3015acf 100644 --- a/www/index.html +++ b/www/index.html @@ -164,8 +164,8 @@ // Auto-reload setInterval(reload, 1000); - const url2 = new URL('api', location.href); - fetch(url2, {cache: 'no-cache'}).then(r => r.json()).then(data => { + const url = new URL('api', location.href); + fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => { const info = document.querySelector('.info'); info.innerText = `Version: ${data.version}, Config: ${data.config_path}`; }); From a404c2c86c29c154e084e1934db70c815bb4952f Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sat, 20 Apr 2024 21:55:07 +0300 Subject: [PATCH 27/30] feat(editor): upgrade Ace editor version to 1.33.0 --- www/editor.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 @@ File Editor - +
${online} / info${links}