Merge branch 'AlexxIT:master' into fix-new-stream-error
This commit is contained in:
+25
-25
@@ -15,87 +15,87 @@ jobs:
|
|||||||
env: { CGO_ENABLED: 0 }
|
env: { CGO_ENABLED: 0 }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with: { go-version: '1.21' }
|
with: { go-version: '1.21' }
|
||||||
|
|
||||||
- name: Build go2rtc_win64
|
- name: Build go2rtc_win64
|
||||||
env: { GOOS: windows, GOARCH: amd64 }
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_win64
|
- name: Upload go2rtc_win64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||||
|
|
||||||
- name: Build go2rtc_win32
|
- name: Build go2rtc_win32
|
||||||
env: { GOOS: windows, GOARCH: 386 }
|
env: { GOOS: windows, GOARCH: 386 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_win32
|
- name: Upload go2rtc_win32
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_win32, path: go2rtc.exe }
|
with: { name: go2rtc_win32, path: go2rtc.exe }
|
||||||
|
|
||||||
- name: Build go2rtc_win_arm64
|
- name: Build go2rtc_win_arm64
|
||||||
env: { GOOS: windows, GOARCH: arm64 }
|
env: { GOOS: windows, GOARCH: arm64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_win_arm64
|
- name: Upload go2rtc_win_arm64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
|
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_amd64
|
- name: Build go2rtc_linux_amd64
|
||||||
env: { GOOS: linux, GOARCH: amd64 }
|
env: { GOOS: linux, GOARCH: amd64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_amd64
|
- name: Upload go2rtc_linux_amd64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_amd64, path: go2rtc }
|
with: { name: go2rtc_linux_amd64, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_i386
|
- name: Build go2rtc_linux_i386
|
||||||
env: { GOOS: linux, GOARCH: 386 }
|
env: { GOOS: linux, GOARCH: 386 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_i386
|
- name: Upload go2rtc_linux_i386
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_i386, path: go2rtc }
|
with: { name: go2rtc_linux_i386, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_arm64
|
- name: Build go2rtc_linux_arm64
|
||||||
env: { GOOS: linux, GOARCH: arm64 }
|
env: { GOOS: linux, GOARCH: arm64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_arm64
|
- name: Upload go2rtc_linux_arm64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_arm64, path: go2rtc }
|
with: { name: go2rtc_linux_arm64, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_arm
|
- name: Build go2rtc_linux_arm
|
||||||
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
|
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_arm
|
- name: Upload go2rtc_linux_arm
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_arm, path: go2rtc }
|
with: { name: go2rtc_linux_arm, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_armv6
|
- name: Build go2rtc_linux_armv6
|
||||||
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
|
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_armv6
|
- name: Upload go2rtc_linux_armv6
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_armv6, path: go2rtc }
|
with: { name: go2rtc_linux_armv6, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_linux_mipsel
|
- name: Build go2rtc_linux_mipsel
|
||||||
env: { GOOS: linux, GOARCH: mipsle }
|
env: { GOOS: linux, GOARCH: mipsle }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_linux_mipsel
|
- name: Upload go2rtc_linux_mipsel
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_mac_amd64
|
- name: Build go2rtc_mac_amd64
|
||||||
env: { GOOS: darwin, GOARCH: amd64 }
|
env: { GOOS: darwin, GOARCH: amd64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_mac_amd64
|
- name: Upload go2rtc_mac_amd64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_mac_amd64, path: go2rtc }
|
with: { name: go2rtc_mac_amd64, path: go2rtc }
|
||||||
|
|
||||||
- name: Build go2rtc_mac_arm64
|
- name: Build go2rtc_mac_arm64
|
||||||
env: { GOOS: darwin, GOARCH: arm64 }
|
env: { GOOS: darwin, GOARCH: arm64 }
|
||||||
run: go build -ldflags "-s -w" -trimpath
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
- name: Upload go2rtc_mac_arm64
|
- name: Upload go2rtc_mac_arm64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with: { name: go2rtc_mac_arm64, path: go2rtc }
|
with: { name: go2rtc_mac_arm64, path: go2rtc }
|
||||||
|
|
||||||
docker-master:
|
docker-master:
|
||||||
@@ -103,11 +103,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: ${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -116,20 +116,20 @@ jobs:
|
|||||||
type=match,pattern=v(.*),group=1
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: |
|
platforms: |
|
||||||
@@ -148,11 +148,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-hw
|
id: meta-hw
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ github.repository }}
|
images: ${{ github.repository }}
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -164,20 +164,20 @@ jobs:
|
|||||||
type=match,pattern=v(.*),group=1
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: hardware.Dockerfile
|
file: hardware.Dockerfile
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v3
|
uses: actions/configure-pages@v4
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: './website'
|
path: './website'
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v2
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ jobs:
|
|||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
|
|
||||||
@@ -70,13 +70,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/${{ matrix.platform }}
|
platforms: linux/${{ matrix.platform }}
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Hardware
|
- name: Build and push Hardware
|
||||||
if: matrix.platform == 'amd64'
|
if: matrix.platform == 'amd64'
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: hardware.Dockerfile
|
file: hardware.Dockerfile
|
||||||
|
|||||||
@@ -146,13 +146,13 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
|
|||||||
|
|
||||||
Latest, but maybe unstable version:
|
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
|
- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions
|
||||||
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
|
- Hass Add-on: `go2rtc master` or `go2rtc master hardware` versions
|
||||||
|
|
||||||
## Configuration
|
## 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)
|
- `api` server will start on default **1984 port** (TCP)
|
||||||
- `rtsp` server will start on default **8554 port** (TCP)
|
- `rtsp` server will start on default **8554 port** (TCP)
|
||||||
- `webrtc` will use port **8555** (TCP/UDP) for connections
|
- `webrtc` will use port **8555** (TCP/UDP) for connections
|
||||||
@@ -230,7 +230,7 @@ streams:
|
|||||||
amcrest_doorbell:
|
amcrest_doorbell:
|
||||||
- rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
- 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
|
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**
|
**Recommendations**
|
||||||
@@ -265,7 +265,7 @@ streams:
|
|||||||
|
|
||||||
#### Source: RTMP
|
#### 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
|
```yaml
|
||||||
streams:
|
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.
|
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
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
@@ -610,7 +610,7 @@ streams:
|
|||||||
|
|
||||||
*[New in v1.6.0](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)*
|
*[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.
|
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**
|
**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**
|
**go2rtc**
|
||||||
|
|
||||||
@@ -1352,6 +1352,7 @@ streams:
|
|||||||
**Distributions**
|
**Distributions**
|
||||||
|
|
||||||
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)
|
- [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)
|
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
|
||||||
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
|
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
|
||||||
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
|
||||||
|
|||||||
+2
-1
@@ -53,7 +53,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t
|
|||||||
python3 curl jq \
|
python3 curl jq \
|
||||||
intel-media-va-driver-non-free \
|
intel-media-va-driver-non-free \
|
||||||
mesa-va-drivers \
|
mesa-va-drivers \
|
||||||
libasound2-plugins
|
libasound2-plugins && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --link --from=rootfs / /
|
COPY --link --from=rootfs / /
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
@@ -96,7 +97,10 @@ func listen(network, address string) {
|
|||||||
Port = ln.Addr().(*net.TCPAddr).Port
|
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 {
|
if err = server.Serve(ln); err != nil {
|
||||||
log.Fatal().Err(err).Msg("[api] serve")
|
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")
|
log.Info().Str("addr", address).Msg("[api] tls listen")
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Handler: Handler,
|
Handler: Handler,
|
||||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
if err = server.ServeTLS(ln, "", ""); err != nil {
|
if err = server.ServeTLS(ln, "", ""); err != nil {
|
||||||
log.Fatal().Err(err).Msg("[api] tls serve")
|
log.Fatal().Err(err).Msg("[api] tls serve")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
waitersMu.Unlock()
|
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()
|
ts := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func ServiceCameraRTPStreamManagement() *hap.Service {
|
|||||||
VideoAttrs: []VideoAttrs{
|
VideoAttrs: []VideoAttrs{
|
||||||
{Width: 1920, Height: 1080, Framerate: 30},
|
{Width: 1920, Height: 1080, Framerate: 30},
|
||||||
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
|
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
|
||||||
|
{Width: 320, Height: 240, Framerate: 15}, // apple watch
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ func (c *Client) Handle() error {
|
|||||||
case "stream-init":
|
case "stream-init":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
case "metadata":
|
||||||
|
continue
|
||||||
|
|
||||||
case "fragment":
|
case "fragment":
|
||||||
_, data, err = c.conn.ReadMessage()
|
_, data, err = c.conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -183,6 +186,9 @@ func (c *Client) getTracks() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
|
case "metadata":
|
||||||
|
continue
|
||||||
|
|
||||||
case "stream-init":
|
case "stream-init":
|
||||||
s := msg.CodecString
|
s := msg.CodecString
|
||||||
i := strings.IndexByte(s, '.')
|
i := strings.IndexByte(s, '.')
|
||||||
|
|||||||
+84
-1
@@ -14,6 +14,13 @@ import (
|
|||||||
type API struct {
|
type API struct {
|
||||||
Token string
|
Token string
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
|
|
||||||
|
StreamProjectID string
|
||||||
|
StreamDeviceID string
|
||||||
|
StreamSessionID string
|
||||||
|
StreamExpiresAt time.Time
|
||||||
|
|
||||||
|
extendTimer *time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
@@ -159,7 +166,7 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) {
|
|||||||
Results struct {
|
Results struct {
|
||||||
Answer string `json:"answerSdp"`
|
Answer string `json:"answerSdp"`
|
||||||
ExpiresAt time.Time `json:"expiresAt"`
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
MediaSessionId string `json:"mediaSessionId"`
|
MediaSessionID string `json:"mediaSessionId"`
|
||||||
} `json:"results"`
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +174,65 @@ func (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.StreamProjectID = projectID
|
||||||
|
a.StreamDeviceID = deviceID
|
||||||
|
a.StreamSessionID = resv.Results.MediaSessionID
|
||||||
|
a.StreamExpiresAt = resv.Results.ExpiresAt
|
||||||
|
|
||||||
return resv.Results.Answer, nil
|
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 {
|
type Device struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -203,3 +266,23 @@ type Device struct {
|
|||||||
// DisplayName string `json:"displayName"`
|
// DisplayName string `json:"displayName"`
|
||||||
//} `json:"parentRelations"`
|
//} `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()
|
||||||
|
}
|
||||||
|
|||||||
+4
-1
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
conn *webrtc.Conn
|
conn *webrtc.Conn
|
||||||
|
api *API
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(rawURL string) (*Client, error) {
|
func NewClient(rawURL string) (*Client, error) {
|
||||||
@@ -74,7 +75,7 @@ func NewClient(rawURL string) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{conn: conn}, nil
|
return &Client{conn: conn, api: nestAPI}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetMedias() []*core.Media {
|
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 {
|
func (c *Client) Start() error {
|
||||||
|
c.api.StartExtendStreamTimer()
|
||||||
return c.conn.Start()
|
return c.conn.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Stop() error {
|
func (c *Client) Stop() error {
|
||||||
|
c.api.StopExtendStreamTimer()
|
||||||
return c.conn.Stop()
|
return c.conn.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -281,7 +281,7 @@ func dial(req *http.Request) (net.Conn, *http.Response, error) {
|
|||||||
auth := res.Header.Get("WWW-Authenticate")
|
auth := res.Header.Get("WWW-Authenticate")
|
||||||
|
|
||||||
if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
|
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 == "" {
|
if password == "" {
|
||||||
|
|||||||
@@ -5,11 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<style>
|
<style>
|
||||||
body {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -27,33 +22,11 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
|
||||||
background-color: white;
|
|
||||||
text-align: left;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
table td, table th {
|
|
||||||
border: 1px solid black;
|
|
||||||
padding: 5px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table tbody td {
|
table tbody td {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
<title>File Editor</title>
|
<title>File Editor</title>
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<script src="https://unpkg.com/ace-builds@1.28.0/src-min/ace.js"></script>
|
<script src="https://unpkg.com/ace-builds@1.33.0/src-min/ace.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<script>
|
<script>
|
||||||
let dump;
|
let dump;
|
||||||
|
|
||||||
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.28.0/src-min/');
|
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.33.0/src-min/');
|
||||||
const editor = ace.edit('config', {
|
const editor = ace.edit('config', {
|
||||||
mode: 'ace/mode/yaml',
|
mode: 'ace/mode/yaml',
|
||||||
});
|
});
|
||||||
|
|||||||
+34
-34
@@ -8,39 +8,18 @@
|
|||||||
<title>go2rtc</title>
|
<title>go2rtc</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
margin: 0;
|
||||||
background-color: white;
|
padding: 0;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
table {
|
|
||||||
background-color: white;
|
|
||||||
text-align: left;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
table td, table th {
|
|
||||||
border: 1px solid black;
|
|
||||||
padding: 5px 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table tbody td {
|
table tbody td {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -121,28 +100,49 @@
|
|||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
const url = new URL('api/streams', location.href);
|
const url = new URL('api/streams', location.href);
|
||||||
|
const checkboxStates = {};
|
||||||
|
tbody.querySelectorAll('input[type="checkbox"][name]').forEach(checkbox => {
|
||||||
|
checkboxStates[checkbox.name] = checkbox.checked;
|
||||||
|
});
|
||||||
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||||
tbody.innerHTML = '';
|
const existingIds = Array.from(tbody.querySelectorAll('tr')).map(tr => tr.dataset['id']);
|
||||||
|
const fetchedIds = [];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
const name = key.replace(/[<">]/g, ''); // sanitize
|
const name = key.replace(/[<">]/g, ''); // sanitize
|
||||||
|
fetchedIds.push(name);
|
||||||
|
|
||||||
|
let tr = tbody.querySelector(`tr[data-id="${name}"]`);
|
||||||
const online = value && value.consumers ? value.consumers.length : 0;
|
const online = value && value.consumers ? value.consumers.length : 0;
|
||||||
const src = encodeURIComponent(name);
|
const src = encodeURIComponent(name);
|
||||||
const links = templates.map(link => {
|
const links = templates.map(link => link.replace('{name}', src)).join(' ');
|
||||||
return link.replace('{name}', src);
|
|
||||||
}).join(' ');
|
|
||||||
|
|
||||||
const tr = document.createElement('tr');
|
if (!tr) {
|
||||||
tr.dataset['id'] = name;
|
tr = document.createElement('tr');
|
||||||
|
tr.dataset['id'] = name;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChecked = checkboxStates[name] ? 'checked' : '';
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
|
`<td><label><input type="checkbox" name="${name}" ${isChecked}>${name}</label></td>` +
|
||||||
`<td><a href="api/streams?src=${src}">${online} / info</a></td>` +
|
`<td><a href="api/streams?src=${src}">${online} / info</a></td>` +
|
||||||
`<td>${links}</td>`;
|
`<td>${links}</td>`;
|
||||||
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
|
||||||
|
setInterval(reload, 1000);
|
||||||
|
|
||||||
const url = new URL('api', location.href);
|
const url = new URL('api', location.href);
|
||||||
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||||
const info = document.querySelector('.info');
|
const info = document.querySelector('.info');
|
||||||
|
|||||||
@@ -5,10 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<style>
|
<style>
|
||||||
body {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
+26
-25
@@ -19,34 +19,17 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
|
||||||
background-color: white;
|
|
||||||
text-align: left;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
table td, table th {
|
|
||||||
border: 1px solid black;
|
|
||||||
padding: 5px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table tbody td {
|
table tbody td {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -54,6 +37,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<button id="clean">Clean</button>
|
<button id="clean">Clean</button>
|
||||||
<button id="update">Auto Update: ON</button>
|
<button id="update">Auto Update: ON</button>
|
||||||
|
<button id="reverse">Reverse Log Order: OFF</button>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<table>
|
<table>
|
||||||
@@ -85,9 +69,21 @@
|
|||||||
.replace(/\n/g, '<br>');
|
.replace(/\n/g, '<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function applyLogStyling(jsonlines) {
|
||||||
const KEYS = ['time', 'level', 'message'];
|
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();
|
||||||
|
}
|
||||||
return lines.map(line => {
|
return lines.map(line => {
|
||||||
const ts = new Date(line['time']);
|
const ts = new Date(line['time']);
|
||||||
const msg = Object.keys(line).reduce((msg, key) => {
|
const msg = Object.keys(line).reduce((msg, key) => {
|
||||||
@@ -112,15 +108,20 @@
|
|||||||
|
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
// Handle auto-update switch
|
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
|
||||||
let autoUpdateEnabled = true;
|
|
||||||
|
|
||||||
const update = document.getElementById('update');
|
|
||||||
update.addEventListener('click', () => {
|
update.addEventListener('click', () => {
|
||||||
autoUpdateEnabled = !autoUpdateEnabled;
|
autoUpdateEnabled = !autoUpdateEnabled;
|
||||||
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
|
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toggle log order
|
||||||
|
reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`;
|
||||||
|
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
|
// Reload the logs every 5 seconds
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (autoUpdateEnabled) reload();
|
if (autoUpdateEnabled) reload();
|
||||||
|
|||||||
+146
-1
@@ -18,7 +18,6 @@ i {
|
|||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: block;
|
display: block;
|
||||||
/*width: 660px;*/
|
|
||||||
margin: 0 auto 10px;
|
margin: 0 auto 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +40,97 @@ nav a:hover {
|
|||||||
nav li {
|
nav li {
|
||||||
display: inline;
|
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 a {
|
||||||
|
background: rgba(45, 45, 45, .8);
|
||||||
|
border-right: 1px solid #2c2c2c;
|
||||||
|
color: #c7c7c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode a:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode a:visited {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode input,
|
||||||
|
body.dark-mode select,
|
||||||
|
body.dark-mode textarea {
|
||||||
|
background-color: #333;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode input::placeholder,
|
||||||
|
body.dark-mode textarea::placeholder {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode hr {
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -48,6 +138,61 @@ nav li {
|
|||||||
<li><a href="add.html">Add</a></li>
|
<li><a href="add.html">Add</a></li>
|
||||||
<li><a href="editor.html">Config</a></li>
|
<li><a href="editor.html">Config</a></li>
|
||||||
<li><a href="log.html">Log</a></li>
|
<li><a href="log.html">Log</a></li>
|
||||||
|
<li><a href="#" id="darkModeToggle">
|
||||||
|
🌙
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
` + document.body.innerHTML;
|
` + document.body.innerHTML;
|
||||||
|
|
||||||
|
const sunIcon = '☀️';
|
||||||
|
const moonIcon = '🌕';
|
||||||
|
|
||||||
|
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;
|
||||||
|
darkModeToggle.setAttribute('aria-label', 'Enable light mode');
|
||||||
|
} else {
|
||||||
|
darkModeToggle.innerHTML = moonIcon;
|
||||||
|
darkModeToggle.setAttribute('aria-label', 'Enable dark mode');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDarkMode = () => {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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', () => {
|
||||||
|
const enabled = document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', enabled ? 'enabled' : 'disabled');
|
||||||
|
updateToggleButton(); // Update the button after toggling
|
||||||
|
updateEditorTheme();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user