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
This commit is contained in:
eduard256 2026-04-08 11:00:32 +00:00
parent ce4b777e98
commit 0fb7356a5e
4 changed files with 118 additions and 8 deletions

104
pkg/tester/source_onvif.go Normal file
View file

@ -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,
})
}

View file

@ -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

View file

@ -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);

View file

@ -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';
}