Files
Behavision/shared/cameraMakes.js
Suriyakumarvijayanayagam c6a2c392d9 Paste the camera's RTSP address instead of taking it apart by hand
Asked directly: "our cameras have an rtsp url, we can use that to connect them
to this software right". Yes - and that has always been the mechanism, which
is the point. CameraConfig.source() builds exactly that URL from the parts,
and CameraConfig.url has always accepted a whole one and taken priority over
them. No form ever offered it.

So an operator holding the address their camera's own app shows had to split
it into five fields by eye. That is where a password containing @ or / goes
wrong, and this repository has already been bitten once by unencoded @ in RTSP
credentials.

parseRtspUrl lives in shared/cameraMakes.js and is imported by BOTH forms, for
the same reason the make picker is: two copies would be worse than not
offering it, because an operator trusts a filled-in field. A test asserts both
import it.

Decisions worth keeping:

- Split into fields, not stored whole. Everything else on the form - Test, the
  make picker, editing later, and the rule that a password is never returned
  to the browser - works on the parts. A URL kept intact would carry the
  password back out to every screen that reads a camera.
- WHATWG splits user info at the LAST @, which is what makes an unencoded @
  inside a password parse the way a person means it. An operator doing it by
  eye would put "p" in the password box and "ssw0rd@192.168.1.121" in the
  address box.
- Percent-encoded credentials are DECODED, because source() encodes again
  when it rebuilds the URL. Keeping them encoded would double-encode and the
  camera would refuse a password that is correct.
- The scheme is optional, structure is not. Without requiring a slash,
  "nonsense" parses as a perfectly good hostname and silently fills the
  Address field with it - a wrong answer that looks like it worked. A bare
  address is refused too: the Address field already takes one.
- A query string stays with the path. Some cameras carry the channel there,
  and dropping it opens the wrong channel - which looks like a camera pointed
  somewhere unexpected.
- A URL carrying no credentials does not wipe a password already typed.

Tested through node from pytest, the same pattern test_dashboard.py uses, and
skipped when node is absent so the suite stays dependency-light.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 18:29:59 +05:30

127 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The stream path for the common camera makes.
//
// Imported by BOTH user interfaces - the head-office web app and the shop PC's
// desktop app - and kept here rather than copied into each because a make that
// is right in one and stale in the other is worse than not offering the list at
// all: an installer trusts a filled-in field.
//
// This is the single biggest obstacle for somebody setting up their first
// camera: the address and the password are on a label or in the installer's
// notes, but the RTSP *path* is not written anywhere a shop owner would look.
// It is model-specific, undiscoverable, and getting it wrong produces "could
// not open stream", which reads like a password problem and is not.
//
// Picking a make fills it in. The field stays editable, because these are
// conventions rather than guarantees and an installer who knows better must be
// able to overrule us.
export const MAKES = [
{
id: 'hikvision',
label: 'Hikvision',
path: '/Streaming/Channels/101',
note: 'Channel 1, main stream. Use /Streaming/Channels/102 for the lower-quality sub stream.',
},
{
id: 'dahua',
label: 'Dahua',
path: '/cam/realmonitor?channel=1&subtype=0',
note: 'Channel 1, main stream. subtype=1 is the sub stream.',
},
{
// Dahua hardware under another name, and very common in Indian retail.
id: 'cpplus',
label: 'CP Plus',
path: '/cam/realmonitor?channel=1&subtype=0',
note: 'CP Plus cameras use the Dahua stream path.',
},
{
id: 'uniview',
label: 'Uniview',
path: '/media/video1',
note: 'Some older Uniview models use /video1 instead.',
},
{
id: 'tplink',
label: 'TP-Link / Tapo',
path: '/stream1',
note: 'Tapo cameras need a separate camera account created in the Tapo app — your Tapo login will not work.',
},
{
id: 'reolink',
label: 'Reolink',
path: '/h264Preview_01_main',
note: 'Use /h264Preview_01_sub for the lower-quality stream.',
},
{
id: 'amcrest',
label: 'Amcrest',
path: '/cam/realmonitor?channel=1&subtype=0',
note: 'Amcrest cameras use the Dahua stream path.',
},
{
id: 'axis',
label: 'Axis',
path: '/axis-media/media.amp',
note: '',
},
{
id: 'onvif',
label: 'Other (ONVIF)',
path: '/onvif1',
note: 'Many generic cameras answer here. If it does not work, look for “RTSP” in the camera’s own app.',
},
{
id: 'manual',
label: 'I know the path',
path: '',
note: '',
},
]
export const makeById = (id) => MAKES.find(m => m.id === id) || MAKES[MAKES.length - 1]
// Paste the camera's RTSP URL, rather than taking it apart by hand.
//
// The engine has always accepted a whole URL (`CameraConfig.url` wins over the
// parts) and no form has ever offered one - the same gap as the webcam option,
// and it costs more here. A URL is how people actually HAVE this information:
// it is what the camera's own app shows, what an installer writes down and
// what gets pasted into a message. Splitting it into five fields by eye is
// where a password containing `@` or `/` goes wrong, and this repository
// already records a whole class of bug from unencoded `@` in RTSP credentials.
//
// Split into fields rather than stored whole, deliberately: everything else on
// the form - Test, the make picker, editing later, and the rule that a
// password is never returned to the browser - works on the parts. A URL kept
// intact would carry the password back out to every screen that reads a
// camera.
export function parseRtspUrl(raw) {
const text = String(raw || '').trim()
if (!text) return null
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(text)
// A bare `host/path` is a reasonable thing to paste, so the scheme is
// optional - but something has to mark this as a URL rather than a word.
// Without the slash test, `nonsense` parses as a perfectly good hostname
// and silently fills the Address field with it: a wrong answer that looks
// like it worked, which is worse than refusing.
if (!hasScheme && !text.includes('/')) return null
const withScheme = hasScheme ? text : 'rtsp://' + text
let u
try {
u = new URL(withScheme)
} catch {
return null
}
if (!u.hostname) return null
// WHATWG splits user info at the LAST `@`, which is what makes an unencoded
// `@` inside a password parse the way a person means it.
const out = {
host: u.hostname,
port: Number(u.port) || 554,
path: (u.pathname || '') + (u.search || '') || '/',
username: decodeURIComponent(u.username || ''),
password: decodeURIComponent(u.password || ''),
}
return out
}