From 73c43dbf8b253f97f5edaae3e921ac81fbd6f1ab Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Mon, 16 Feb 2026 04:50:24 +0300 Subject: [PATCH 1/2] Add CDN dependency download script and update Dockerfiles for offline web UI --- docker/Dockerfile | 15 ++++-- docker/download_cdn.sh | 105 +++++++++++++++++++++++++++++++++++++ docker/entrypoint.sh | 12 +++++ docker/hardware.Dockerfile | 15 ++++-- docker/rockchip.Dockerfile | 16 ++++-- 5 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 docker/download_cdn.sh create mode 100644 docker/entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 9efded4b..2c095d48 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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/download_cdn.sh b/docker/download_cdn.sh new file mode 100644 index 00000000..16b5e2d2 --- /dev/null +++ b/docker/download_cdn.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# Downloads CDN dependencies from jsdelivr for offline web UI. +# Automatically parses CDN URLs from HTML files, so updating +# a library version in HTML is all that's needed. +# +# Usage: download_cdn.sh +set -e + +WEB_DIR="${1:?Usage: download_cdn.sh }" +CDN_DIR="$WEB_DIR/cdn" +mkdir -p "$CDN_DIR" + +# Step 1: Extract all jsdelivr CDN URLs from HTML files +URLS=$(grep -roh 'https://cdn\.jsdelivr\.net/npm/[^"'"'"' )`]*' "$WEB_DIR"/*.html | sort -u) + +echo "=== Found CDN URLs ===" +echo "$URLS" +echo "" + +# Step 2: Process each URL +MONACO_VER="" +for url in $URLS; do + # Remove CDN prefix to get npm path + npm_path="${url#https://cdn.jsdelivr.net/npm/}" + + # Extract package@version and file path + pkg_ver=$(echo "$npm_path" | cut -d/ -f1) + remaining=$(echo "$npm_path" | cut -d/ -f2-) + if [ "$remaining" = "$npm_path" ]; then + file_path="" + else + file_path="$remaining" + fi + + pkg_name=$(echo "$pkg_ver" | sed 's/@[^@]*$//') + + # Monaco editor: remember version, download as tarball later + case "$pkg_name" in + monaco-editor) + MONACO_VER=$(echo "$pkg_ver" | sed 's/.*@//') + echo "Monaco editor v$MONACO_VER (will download tarball)" + continue + ;; + esac + + # Determine local file path + if [ -n "$file_path" ]; then + local_file="$CDN_DIR/$pkg_name/$file_path" + else + local_file="$CDN_DIR/$pkg_name/index.js" + fi + + mkdir -p "$(dirname "$local_file")" + echo "Downloading $pkg_ver -> $local_file" + wget -q -O "$local_file" "$url" +done + +# Step 3: Download monaco-editor tarball and extract min/ directory +# The AMD loader dynamically loads modules, so we need the entire min/vs/ tree +if [ -n "$MONACO_VER" ]; then + echo "" + echo "=== Downloading monaco-editor@$MONACO_VER tarball ===" + + TARBALL_URL=$(wget -q -O - "https://registry.npmjs.org/monaco-editor/$MONACO_VER" | \ + grep -o '"tarball":"[^"]*"' | head -1 | cut -d'"' -f4) + + mkdir -p /tmp/monaco "$CDN_DIR/monaco-editor" + wget -q -O /tmp/monaco.tgz "$TARBALL_URL" + tar xzf /tmp/monaco.tgz -C /tmp/monaco + + cp -r /tmp/monaco/package/min "$CDN_DIR/monaco-editor/" + rm -rf /tmp/monaco /tmp/monaco.tgz + + echo " Extracted min/ directory ($(du -sh "$CDN_DIR/monaco-editor/min" | cut -f1))" +fi + +# Step 4: Patch HTML files to use local paths instead of CDN URLs +echo "" +echo "=== Patching HTML files ===" +for url in $URLS; do + npm_path="${url#https://cdn.jsdelivr.net/npm/}" + + pkg_ver=$(echo "$npm_path" | cut -d/ -f1) + remaining=$(echo "$npm_path" | cut -d/ -f2-) + if [ "$remaining" = "$npm_path" ]; then + file_path="" + else + file_path="$remaining" + fi + + pkg_name=$(echo "$pkg_ver" | sed 's/@[^@]*$//') + + if [ -n "$file_path" ]; then + local_url="cdn/$pkg_name/$file_path" + else + local_url="cdn/$pkg_name/index.js" + fi + + echo " $url -> $local_url" + sed -i "s|$url|$local_url|g" "$WEB_DIR"/*.html +done + +echo "" +echo "=== Done ===" +du -sh "$CDN_DIR" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..0e0bb3cd --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Entrypoint wrapper for go2rtc Docker container. +# If /var/www/go2rtc exists (CDN files bundled), automatically +# configures static_dir to serve web UI without internet access. +if [ -d /var/www/go2rtc ]; then + exec go2rtc \ + -config '{"api":{"static_dir":"/var/www/go2rtc"}}' \ + -config /config/go2rtc.yaml \ + "$@" +else + exec go2rtc -config /config/go2rtc.yaml "$@" +fi diff --git a/docker/hardware.Dockerfile b/docker/hardware.Dockerfile index 563843b5..e972d59b 100644 --- a/docker/hardware.Dockerfile +++ b/docker/hardware.Dockerfile @@ -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 debian:${DEBIAN_VERSION} # Prepare apt for buildkit cache @@ -48,13 +56,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t apt-get clean && rm -rf /var/lib/apt/lists/* 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 ["/usr/bin/tini", "--"] +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] VOLUME /config WORKDIR /config # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) ENV NVIDIA_VISIBLE_DEVICES all ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility -CMD ["go2rtc", "-config", "/config/go2rtc.yaml"] diff --git a/docker/rockchip.Dockerfile b/docker/rockchip.Dockerfile index 6ab924ee..12bfd002 100644 --- a/docker/rockchip.Dockerfile +++ b/docker/rockchip.Dockerfile @@ -24,7 +24,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} # Prepare apt for buildkit cache @@ -42,10 +50,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,t COPY --from=build /build/go2rtc /usr/local/bin/ ADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /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 ["/usr/bin/tini", "--"] +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] VOLUME /config WORKDIR /config - -CMD ["go2rtc", "-config", "/config/go2rtc.yaml"] From a0a36f87bd7a96d1fe21fc8a3e68155c6a295bfb Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Mon, 16 Feb 2026 05:20:25 +0300 Subject: [PATCH 2/2] feat(tests): add CDN URL extraction and patching tests - implement tests for extracting CDN URLs from HTML files - add tests for parsing CDN URLs and patching HTML content - ensure functionality works with real HTML files and CDN URLs --- docker/cdn_test.go | 407 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 docker/cdn_test.go 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, `