diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6015efa0..89ba42ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - 'master' + - 'beta' tags: - 'v*' @@ -19,7 +19,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 - with: { go-version: '1.25' } + with: { go-version: '1.26' } - name: Build go2rtc_win64 env: { GOOS: windows, GOARCH: amd64 } @@ -123,9 +123,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: | - name=${{ github.repository }},enable=${{ github.event.repository.fork == false }} - ghcr.io/${{ github.repository }} + images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}},enable=false @@ -137,15 +135,8 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - if: github.event_name == 'push' && github.event.repository.fork == false - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Login to GitHub Container Registry - if: github.event_name == 'push' + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io @@ -180,9 +171,7 @@ jobs: id: meta-hw uses: docker/metadata-action@v5 with: - images: | - name=${{ github.repository }},enable=${{ github.event.repository.fork == false }} - ghcr.io/${{ github.repository }} + images: ghcr.io/${{ github.repository }} flavor: | suffix=-hardware,onlatest=true latest=auto @@ -197,15 +186,8 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - if: github.event_name == 'push' && github.event.repository.fork == false - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Login to GitHub Container Registry - if: github.event_name == 'push' + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io @@ -235,9 +217,7 @@ jobs: id: meta-rk uses: docker/metadata-action@v5 with: - images: | - name=${{ github.repository }},enable=${{ github.event.repository.fork == false }} - ghcr.io/${{ github.repository }} + images: ghcr.io/${{ github.repository }} flavor: | suffix=-rockchip,onlatest=true latest=auto @@ -252,15 +232,8 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - if: github.event_name == 'push' && github.event.repository.fork == false - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Login to GitHub Container Registry - if: github.event_name == 'push' + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d9e7e25..b81245f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: '1.26' - name: Build Go binary run: go build -ldflags "-s -w" -trimpath -o ./go2rtc diff --git a/.gitignore b/.gitignore index 5d539075..201329c3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,9 @@ website/.vitepress/dist node_modules package-lock.json -CLAUDE.md \ No newline at end of file +CLAUDE.md +*/**/CLAUDE.md +.claude* +.ruff* + +.omc diff --git a/README.md b/README.md index b15d57ab..6f7c1316 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,86 @@ Ultimate camera streaming application with support for dozens formats and protocols. +--- + +> ### 🔀 Fork: [skrashevich/go2rtc](https://github.com/skrashevich/go2rtc) +> +> This is a fork of [AlexxIT/go2rtc](https://github.com/AlexxIT/go2rtc) with the following additions: +> +> **Features** +> - **HomeKit Secure Video (HKSV)** — full recording support with motion detection (P-frame analysis, ONVIF events, API) +> - **ONVIF motion detection** — automatic motion events from ONVIF cameras for HomeKit +> - **WebP streaming** — native WebP encoding (snapshot & multipart stream) without FFmpeg +> - **System resource monitoring** — CPU/memory usage in API (`/api/system`) and WebUI ASCII graphs +> - **Read-only mode** — disable all write operations in API/WebUI for production security +> - **Offline WebUI in Docker** — CDN JS dependencies bundled into Docker images +> +> **WebUI improvements** +> - **Redesigned interface** — dark/light theme toggle, unified color scheme, improved layout +> - **Stream Info & Probe pages** — detailed stream analysis with producers/consumers data +> - **Stream Links page** — direct URLs for all supported formats (RTSP, WebRTC, MSE, HLS, etc.) +> - **Mobile-responsive** — improved header, tables, and word wrapping on mobile devices +> +> **Bug fixes & maintenance** +> - **YAML config merge fix** — corrected recursive merge behavior preserving comments +> - **Streams race condition fix** — fixed race condition in stream schema handling +> - **Go 1.26** — updated to the latest Go runtime +> +> #### Download +> +> **Docker images** (GHCR): +> ```bash +> # Standard +> docker pull ghcr.io/skrashevich/go2rtc:beta +> +> # With hardware acceleration (Intel/AMD) +> docker pull ghcr.io/skrashevich/go2rtc:beta-hardware +> +> # Rockchip +> docker pull ghcr.io/skrashevich/go2rtc:beta-rockchip +> ``` +> +> **Binaries**: download from [GitHub Actions](https://github.com/skrashevich/go2rtc/actions/workflows/build.yml) artifacts (select the latest successful run on the `beta` branch). +> Available for: Windows (amd64, i386, arm64), Linux (amd64, i386, arm, arm64), macOS (amd64, arm64), FreeBSD (amd64, arm64). + +--- + +## Screenshots + +| Streams Dashboard | Add Stream | +|:-:|:-:| +| ![Streams](website/images/screenshots/01-streams-dashboard-dark.png) | ![Add Stream](website/images/screenshots/02-add-stream-dark.png) | +| Stream list with status, actions, and system monitoring | Quick setup for dozens of protocols and integrations | + +| Stream Info | Stream Links | +|:-:|:-:| +| ![Info](website/images/screenshots/03-stream-info-dark.png) | ![Links](website/images/screenshots/04-stream-links-dark.png) | +| Producers and consumers details | Direct URLs for all supported formats | + +| Config Editor | Logs | +|:-:|:-:| +| ![Config](website/images/screenshots/05-config-editor-dark.png) | ![Logs](website/images/screenshots/06-logs-dark.png) | +| YAML configuration with syntax highlighting | Real-time log viewer with auto-update | + +
+Light theme + +| Streams Dashboard | Add Stream | +|:-:|:-:| +| ![Streams](website/images/screenshots/01-streams-dashboard-light.png) | ![Add Stream](website/images/screenshots/02-add-stream-light.png) | + +| Stream Info | Stream Links | +|:-:|:-:| +| ![Info](website/images/screenshots/03-stream-info-light.png) | ![Links](website/images/screenshots/04-stream-links-light.png) | + +| Config Editor | Logs | +|:-:|:-:| +| ![Config](website/images/screenshots/05-config-editor-light.png) | ![Logs](website/images/screenshots/06-logs-light.png) | + +
+ +--- + - zero-dependency [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, FreeBSD) - zero-delay for many [supported protocols](#codecs-madness) (lowest possible streaming latency) - [streaming input](#streaming-input) from dozens formats and protocols @@ -135,7 +215,7 @@ It comes preinstalled with [FFmpeg](internal/ffmpeg/README.md) and [Python](inte Latest, but maybe unstable version: - Binary: [latest master build](https://nightly.link/AlexxIT/go2rtc/workflows/build/master) -- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions +- Docker: `ghcr.io/skrashevich/go2rtc:beta` or `ghcr.io/skrashevich/go2rtc:beta-hardware` versions - Home Assistant add-on: `go2rtc master` or `go2rtc master hardware` versions ## Configuration @@ -144,7 +224,7 @@ This is the `go2rtc.yaml` file in [YAML-format](https://en.wikipedia.org/wiki/YA The configuration can be changed in the [WebUI](www/README.md) at `http://localhost:1984`. The editor provides syntax highlighting and checking. -![go2rtc webui config](website/images/webui-config.png) +![go2rtc webui config](website/images/screenshots/05-config-editor-dark.png) The simplest config looks like this: @@ -311,7 +391,7 @@ You can preload any stream on go2rtc start. This is useful for cameras that take [WebUI](www/README.md) provides detailed information about all active connections, including IP-addresses, formats, protocols, number of packets and bytes transferred. Via the [HTTP API](internal/api/README.md) in [`json`](https://en.wikipedia.org/wiki/JSON) or [`dot`](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) format on an interactive connection map. -![go2rtc webui net](website/images/webui-net.png) +![go2rtc webui net](website/images/screenshots/05-network-topology-dark.png) ## Codecs @@ -462,6 +542,8 @@ api: allow_paths: [/api, /api/streams, /api/webrtc, /api/frame.jpeg] # enable auth for localhost (used together with username and password) local_auth: true + # disable write actions in WebUI/API + read_only: true exec: # use only allowed exec paths diff --git a/docker/Dockerfile b/docker/Dockerfile index 9efded4b..1afb774b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,7 +2,7 @@ # 0. Prepare images ARG PYTHON_VERSION="3.13" -ARG GO_VERSION="1.25" +ARG GO_VERSION="1.26" # 1. Build go2rtc binary @@ -26,7 +26,15 @@ COPY . . RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -# 2. Final image +# 2. Download CDN dependencies for offline web UI +FROM alpine AS download-cdn +RUN apk add --no-cache wget +COPY www/ /web/ +COPY docker/download_cdn.sh /tmp/ +RUN sh /tmp/download_cdn.sh /web + + +# 3. Final image FROM python:${PYTHON_VERSION}-alpine AS base # Install ffmpeg, tini (for signal handling), @@ -46,10 +54,11 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver # RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total) COPY --from=build /build/go2rtc /usr/local/bin/ +COPY --from=download-cdn /web /var/www/go2rtc +COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/ EXPOSE 1984 8554 8555 8555/udp -ENTRYPOINT ["/sbin/tini", "--"] +ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"] VOLUME /config WORKDIR /config -CMD ["go2rtc", "-config", "/config/go2rtc.yaml"] diff --git a/docker/README.md b/docker/README.md index 41069baf..5537eba8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -7,9 +7,9 @@ Images are built automatically via [GitHub actions](https://github.com/AlexxIT/g - `alexxit/go2rtc:latest` - latest release based on `alpine` (`amd64`, `386`, `arm/v6`, `arm/v7`, `arm64`) with support for hardware transcoding for Intel iGPU and Raspberry - `alexxit/go2rtc:latest-hardware` - latest release based on `debian 13` (`amd64`) with support for hardware transcoding for Intel iGPU, AMD GPU and NVidia GPU - `alexxit/go2rtc:latest-rockchip` - latest release based on `debian 12` (`arm64`) with support for hardware transcoding for Rockchip RK35xx -- `alexxit/go2rtc:master` - latest unstable version based on `alpine` -- `alexxit/go2rtc:master-hardware` - latest unstable version based on `debian 13` (`amd64`) -- `alexxit/go2rtc:master-rockchip` - latest unstable version based on `debian 12` (`arm64`) +- `ghcr.io/skrashevich/go2rtc:beta` - latest unstable version based on `alpine` +- `ghcr.io/skrashevich/go2rtc:beta-hardware` - latest unstable version based on `debian 13` (`amd64`) +- `ghcr.io/skrashevich/go2rtc:beta-rockchip` - latest unstable version based on `debian 12` (`arm64`) ## Docker compose diff --git a/docker/cdn_test.go b/docker/cdn_test.go new file mode 100644 index 00000000..cbcec59b --- /dev/null +++ b/docker/cdn_test.go @@ -0,0 +1,407 @@ +package docker_test + +import ( + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" +) + +// cdnURLPattern is the same regex used by download_cdn.sh to extract CDN URLs. +var cdnURLPattern = regexp.MustCompile(`https://cdn\.jsdelivr\.net/npm/[^"' )\x60]*`) + +// HTML fixtures that mirror the real www/*.html files. +var htmlFixtures = map[string]string{ + "hls.html": ` + + + + + +`, + + "config.html": ` + + + + + + +`, + + "net.html": ` + + + + + +`, + + "links.html": ` + + + + +`, +} + +func parseCDNURL(rawURL string) (pkgName, filePath, localURL string) { + npmPath := strings.TrimPrefix(rawURL, "https://cdn.jsdelivr.net/npm/") + + parts := strings.SplitN(npmPath, "/", 2) + pkgVer := parts[0] + + if len(parts) > 1 { + filePath = parts[1] + } + + // Remove @version suffix to get package name + if idx := strings.LastIndex(pkgVer, "@"); idx > 0 { + pkgName = pkgVer[:idx] + } else { + pkgName = pkgVer + } + + if filePath != "" { + localURL = "cdn/" + pkgName + "/" + filePath + } else { + localURL = "cdn/" + pkgName + "/index.js" + } + return +} + +// extractURLs finds all CDN URLs in the given HTML content. +func extractURLs(htmlFiles map[string]string) []string { + seen := map[string]bool{} + for _, content := range htmlFiles { + for _, match := range cdnURLPattern.FindAllString(content, -1) { + seen[match] = true + } + } + urls := make([]string, 0, len(seen)) + for u := range seen { + urls = append(urls, u) + } + sort.Strings(urls) + return urls +} + +// patchHTML replaces all CDN URLs in content with local paths. +func patchHTML(content string, urls []string) string { + for _, u := range urls { + _, _, localURL := parseCDNURL(u) + content = strings.ReplaceAll(content, u, localURL) + } + return content +} + +func TestExtractURLs(t *testing.T) { + urls := extractURLs(htmlFixtures) + + expected := []string{ + "https://cdn.jsdelivr.net/npm/hls.js@1", + "https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js", + "https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min", + "https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs/loader.js", + "https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js", + "https://cdn.jsdelivr.net/npm/vis-network@10.0.2/standalone/umd/vis-network.min.js", + } + + if len(urls) != len(expected) { + t.Fatalf("expected %d URLs, got %d: %v", len(expected), len(urls), urls) + } + for i, u := range urls { + if u != expected[i] { + t.Errorf("URL[%d]: expected %q, got %q", i, expected[i], u) + } + } +} + +func TestParseCDNURL(t *testing.T) { + tests := []struct { + url string + pkgName string + filePath string + localURL string + }{ + { + url: "https://cdn.jsdelivr.net/npm/hls.js@1", + pkgName: "hls.js", + filePath: "", + localURL: "cdn/hls.js/index.js", + }, + { + url: "https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js", + pkgName: "js-yaml", + filePath: "dist/js-yaml.min.js", + localURL: "cdn/js-yaml/dist/js-yaml.min.js", + }, + { + url: "https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs/loader.js", + pkgName: "monaco-editor", + filePath: "min/vs/loader.js", + localURL: "cdn/monaco-editor/min/vs/loader.js", + }, + { + url: "https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min", + pkgName: "monaco-editor", + filePath: "min", + localURL: "cdn/monaco-editor/min", + }, + { + url: "https://cdn.jsdelivr.net/npm/vis-network@10.0.2/standalone/umd/vis-network.min.js", + pkgName: "vis-network", + filePath: "standalone/umd/vis-network.min.js", + localURL: "cdn/vis-network/standalone/umd/vis-network.min.js", + }, + { + url: "https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js", + pkgName: "qrcodejs", + filePath: "qrcode.min.js", + localURL: "cdn/qrcodejs/qrcode.min.js", + }, + } + + for _, tt := range tests { + t.Run(tt.pkgName, func(t *testing.T) { + pkgName, filePath, localURL := parseCDNURL(tt.url) + if pkgName != tt.pkgName { + t.Errorf("pkgName: expected %q, got %q", tt.pkgName, pkgName) + } + if filePath != tt.filePath { + t.Errorf("filePath: expected %q, got %q", tt.filePath, filePath) + } + if localURL != tt.localURL { + t.Errorf("localURL: expected %q, got %q", tt.localURL, localURL) + } + }) + } +} + +func TestPatchHTML(t *testing.T) { + urls := extractURLs(htmlFixtures) + + t.Run("hls", func(t *testing.T) { + patched := patchHTML(htmlFixtures["hls.html"], urls) + if !strings.Contains(patched, `src="cdn/hls.js/index.js"`) { + t.Error("hls.js src not patched") + } + if strings.Contains(patched, "cdn.jsdelivr.net") { + t.Error("CDN URL still present") + } + if !strings.Contains(patched, `