From 0fb7356a5eec92a70311d28f100234c379040d0e Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 11:00:32 +0000 Subject: [PATCH] Add ONVIF stream handler for tester - Add testOnvif(): resolves all profiles via ONVIF client, tests each RTSP stream, returns two Results per profile (onvif + rtsp) with shared screenshot - Route onvif:// URLs in worker.go alongside homekit:// - Classify onvif:// streams as recommended in test.html - Harden create.html against undefined/null URL values --- pkg/tester/source_onvif.go | 104 +++++++++++++++++++++++++++++++++++++ pkg/tester/worker.go | 5 ++ www/create.html | 11 ++-- www/test.html | 6 +-- 4 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 pkg/tester/source_onvif.go diff --git a/pkg/tester/source_onvif.go b/pkg/tester/source_onvif.go new file mode 100644 index 0000000..99c04b6 --- /dev/null +++ b/pkg/tester/source_onvif.go @@ -0,0 +1,104 @@ +package tester + +import ( + "fmt" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/onvif" +) + +// testOnvif resolves all ONVIF profiles, tests each via RTSP, +// and adds two Results per profile (onvif:// + rtsp://). +// ex. "onvif://admin:pass@10.0.20.111" or "onvif://admin:pass@10.0.20.119:2020" +func testOnvif(s *Session, rawURL string) { + client, err := onvif.NewClient(rawURL) + if err != nil { + return + } + + tokens, err := client.GetProfilesTokens() + if err != nil { + return + } + + for _, token := range tokens { + profileURL := rawURL + "?subtype=" + token + + pc, err := onvif.NewClient(profileURL) + if err != nil { + continue + } + + rtspURI, err := pc.GetURI() + if err != nil { + continue + } + + testOnvifProfile(s, profileURL, rtspURI) + } +} + +// testOnvifProfile tests a single RTSP stream and adds two Results (onvif + rtsp) +func testOnvifProfile(s *Session, onvifURL, rtspURL string) { + start := time.Now() + + prod, err := rtspHandler(rtspURL) + if err != nil { + return + } + defer func() { _ = prod.Stop() }() + + latency := time.Since(start).Milliseconds() + + var codecs []string + for _, media := range prod.GetMedias() { + if media.Direction != core.DirectionRecvonly { + continue + } + for _, codec := range media.Codecs { + codecs = append(codecs, codec.Name) + } + } + + // capture screenshot + var screenshotPath string + var width, height int + + if raw, codecName := getScreenshot(prod); raw != nil { + var jpeg []byte + + switch codecName { + case core.CodecH264, core.CodecH265: + jpeg = toJPEG(raw) + default: + jpeg = raw + } + + if jpeg != nil { + idx := s.AddScreenshot(jpeg) + screenshotPath = fmt.Sprintf("api/test/screenshot?id=%s&i=%d", s.ID, idx) + width, height = jpegSize(jpeg) + } + } + + // add onvif:// result + s.AddResult(&Result{ + Source: onvifURL, + Screenshot: screenshotPath, + Codecs: codecs, + Width: width, + Height: height, + LatencyMs: latency, + }) + + // add rtsp:// result (same screenshot, same codecs) + s.AddResult(&Result{ + Source: rtspURL, + Screenshot: screenshotPath, + Codecs: codecs, + Width: width, + Height: height, + LatencyMs: latency, + }) +} diff --git a/pkg/tester/worker.go b/pkg/tester/worker.go index 0512486..49a6069 100644 --- a/pkg/tester/worker.go +++ b/pkg/tester/worker.go @@ -56,6 +56,11 @@ func testURL(s *Session, rawURL string) { return } + if strings.HasPrefix(rawURL, "onvif://") { + testOnvif(s, rawURL) + return + } + handler := GetHandler(rawURL) if handler == nil { return diff --git a/www/create.html b/www/create.html index 5ba7e4a..c80c3d9 100644 --- a/www/create.html +++ b/www/create.html @@ -328,8 +328,9 @@ // Pre-populate custom streams from "url" query parameter (supports multiple) params.getAll('url').forEach(function(u) { + if (!u || typeof u !== 'string') return; u = u.trim(); - if (u && u.indexOf('://') !== -1 && customStreams.indexOf(u) === -1) { + if (u && u !== 'undefined' && u !== 'null' && u.indexOf('://') !== -1 && customStreams.indexOf(u) === -1) { customStreams.push(u); } }); @@ -395,7 +396,7 @@ addInput.type = 'text'; addInput.placeholder = 'rtsp://user:pass@host/path or bubble://...'; addInput.spellcheck = false; - addInput.value = pendingInput; + addInput.value = pendingInput || ''; var addBtn = document.createElement('button'); addBtn.className = 'btn-add'; @@ -404,7 +405,7 @@ function addCustom() { var v = addInput.value.trim(); - if (!v) return; + if (!v || v === 'undefined' || v === 'null') return; if (v.indexOf('://') === -1) { showToast('URL must include protocol (rtsp://, http://, bubble://, ...)'); return; @@ -592,7 +593,7 @@ addInput.type = 'text'; addInput.placeholder = 'rtsp://user:pass@host/path or bubble://...'; addInput.spellcheck = false; - addInput.value = pendingInput; + addInput.value = pendingInput || ''; var addBtn = document.createElement('button'); addBtn.className = 'btn-add'; addBtn.type = 'button'; @@ -600,7 +601,7 @@ function addCustom() { var v = addInput.value.trim(); - if (!v) return; + if (!v || v === 'undefined' || v === 'null') return; if (v.indexOf('://') === -1) { showToast('URL must include protocol (rtsp://, http://, bubble://, ...)'); return; } if (customStreams.indexOf(v) !== -1 || dbStreams.indexOf(v) !== -1) { showToast('This URL is already in the list'); return; } customStreams.push(v); diff --git a/www/test.html b/www/test.html index da2bbd6..5d65c81 100644 --- a/www/test.html +++ b/www/test.html @@ -423,11 +423,11 @@ function classifyResult(r) { var scheme = r.source.split('://')[0] || ''; - var isRtsp = scheme === 'rtsp' || scheme === 'rtsps'; + var isRecommended = scheme === 'rtsp' || scheme === 'rtsps' || scheme === 'onvif'; var isHD = r.width >= 1280; - if (isRtsp && isHD) return 'rec-main'; - if (isRtsp) return 'rec-sub'; + if (isRecommended && isHD) return 'rec-main'; + if (isRecommended) return 'rec-sub'; return 'alt'; }