Cameras.
A browser cannot play rtsp://. The player does it for you: give it a camera's address and an element, and it keeps the live picture there. On a PC-type device it can also put a camera full-screen on a second monitor while the app runs on the first.
A camera in the page
<div id="door" style="width: 640px; height: 360px; background: #000"></div>
// The camera's address and login come from a setting (see below), never from your code.
var cam = (Arda.config.cameras ?? [])[0];
if (cam) {
Arda.camera.set('door', { url: cam.url, user: cam.user, password: cam.password })
.then(function () {
var stop = Arda.camera.play('door', document.getElementById('door'), { fit: 'cover' });
// call stop() when the tile goes away
}, function (e) { showError('Camera: ' + e.message); });
}
set(name, spec)registers a camera under a name of yours and resolves its status. Setting the same name again with a different spec restarts it; viewers stay attached.forget(name)stops and drops it.play(name, el, {maxLag, fit})shows it the best way this device has and keeps it showing. It returns a function that stops it.- With feature
cameraVideo: the camera's own H.264/H.265 in a<video>— no re-encoding, the hardware decoder where there is one. - Otherwise (feature
camera): motion JPEG in an<img>. - It falls back from the first to the second by itself when the engine cannot play the stream.
- With feature
elmay be a<video>, an<img>or any element. One of the wrong kind is replaced by the right kind with the same id, class and style; any other element gets a child that fills it (fit:'contain'by default).- It never falls behind live: past
maxLagseconds (2.5 by default) it starts again at the live edge. It also restarts after the camera changes format or reboots, and after nine seconds of a frozen picture. set()spec:url,user,password, and for the motion-JPEG pathfps(up to 30, default 12),width(default 1920),quality(2 best … 31, default 6),hw('auto'or'off').- A plain HTTP JPEG snapshot or MJPEG stream needs none of this: put its URL in an
<img>. (An HTTPS address with a self-signed certificate is refused by an<img>; use the camera's rtsp:// stream.) Arda.camera.url(name)andArda.camera.video(name)are the bare motion-JPEG and MP4 addresses, for a page that wants its own element.
The camera's login
- Give the login in
userandpassword, never in the link (since v1.1.14). The player applies it on every path.status()then shows the link exactly as you gave it, with no password in it. - A link with
rtsp://user:pass@…still works; the fields win over it. Apasswordwith no user anywhere is refused. - Keep cameras in a
rowssetting with the password as asecretfield, per device if devices see different cameras:
{
"settings": [
{ "key": "cameras", "caption": "Cameras", "type": "rows", "perDevice": true, "max": 6, "item": "Camera",
"fields": [
{ "key": "url", "caption": "Stream address", "type": "text", "required": true, "help": "rtsp://192.168.1.50:554/stream2" },
{ "key": "user", "caption": "User", "type": "text" },
{ "key": "password", "caption": "Password", "type": "text", "secret": true },
{ "key": "fit", "caption": "Fit", "type": "select", "options": ["contain", "cover"], "default": "contain" }
] }
]
}
Never print the password, log it or show it. Arda.camera.onvif() results carry no login either.
Is it working?
setInterval(function () {
Arda.camera.status().then(function (list) {
// [{name, url, running, clients, frames, fps, lastFrame, decoder, restarts, error}]
list.forEach(function (c) {
// running is false while nobody is watching, so judge by the last error
setBadge(c.name, c.error ? 'Camera not answering: ' + c.error : '');
});
});
}, 5000);
When a camera stops answering, the player dials it again by itself: after 2, 4 and 8 seconds, then — once four attempts in a row have produced no picture — every two minutes for as long as the app asks for it. Cheap cameras serve only a few connections, and a device knocking every few seconds can lock other devices out. When the device's network comes back every camera is dialled at once. The two-minute pace is from v1.1.17; earlier players retry more often.
A camera on a second monitor
On a device with two monitors, the app is on one (the Primary screen) and every other monitor is a video screen: the player can show a camera on it, full-screen, drawn by the graphics chip. There is no page on that screen.
function videoScreen() {
return Arda.screens.list().filter(function (s) { return s.kind === 'video'; })[0];
}
function showWall() {
var cam = (Arda.config.cameras ?? [])[0], scr = videoScreen();
if (!cam || !scr) return;
Arda.camera.set('wall', { url: cam.url, user: cam.user, password: cam.password })
.then(function () { return Arda.camera.show('wall', { screen: scr.name }); })
.catch(function (e) { console.warn('wall: ' + e.message); });
}
Arda.device().then(function (d) {
if ((d.features || []).indexOf('cameraScreen') < 0) return; // this device cannot
showWall();
Arda.on('screens', showWall); // a monitor switched on or plugged in later (list() is current here)
});
show(name, {screen, delay})puts a camera on a video screen;show(null, {screen})clears it. Showing the same camera on the same screen again is harmless.shows()→[{screen, camera, running, restarts, delay, auto, draw, via, error}].drawisgpu,directorcpu;via: 'rtmp'when a Reolink is played over RTMP. From v1.1.17 alsobehind(ms behind live),lagJumps(how often the player brought it back to live),stream: 'sub'(a Reolink moved to its sub-stream because the main kept falling behind) andwhy. The player does this by itself; from v1.2.1 it skips ahead instead of restarting the picture.- The delay. The picture plays on the camera's own clock, a little behind live, so it is smooth. Left out, the player chooses: 0.5 s for RTSP, 0.3 s for RTMP, stepping back to 1, 2, at most 2.5 s only when frames keep arriving late.
delay: 1500(ms, 0–5000) fixes it. rtsp://andrtmp://sources. A Reolink's rtsp:// link is played over the camera's RTMP by itself (its RTSP server freezes every few seconds); if RTMP is off on the camera the link is used as given.Arda.camera.forget(name)also takes it off any screen.- It needs a PC-type (x86) device installed from the v1.1.12 images or later. A device that only received a newer player, and the Raspberry Pi image, have one screen: no
'video'screen in the list and nocameraScreenfeature. Test for either and the same app runs everywhere. - Camera settings that keep the lightest path: 15 or 30 fps, not 20; a resolution that fits the monitor at less than twice its size (2304×1296 or 1920×1080 for a 1080p monitor).
- On your desk, a screen with
"kind": "video"inarda.dev.jsonstands in for the second monitor:show()draws the camera into that window.
Finding cameras: ONVIF
// A setup screen: find cameras, ask one what it streams, pick its smallest H.264 stream.
function findCamera(user, password) {
return Arda.camera.discover().then(function (cams) {
// [{address, endpoint, xaddrs, scopes: {name, hardware, location}}]
if (!cams.length) throw new Error('no camera answered');
return Arda.camera.onvif({ address: cams[0].xaddrs[0] || cams[0].address, user: user, password: password });
}).then(function (r) {
if (r.auth === 'refused') throw new Error('the camera refused the user or password');
if (r.auth === 'required') throw new Error('the camera needs a user and password');
var p = r.profiles.filter(function (p) { return p.video && p.video.codec === 'H264' && p.rtsp; })
.sort(function (a, b) { return a.video.width - b.video.width; })[0];
if (!p) throw new Error('no H.264 stream: ' + JSON.stringify(r.errors));
return { url: p.rtsp, width: p.video.width, height: p.video.height, model: r.device.model };
});
}
discover({timeout, sweep})listens about 3 s (timeoutin ms, up to 10000) and resolves an array — empty when nothing answered, never a rejection for that. It looks on the device's own networks; a camera on another subnet is reached by typing its address intoonvif(). On a device the player opens its own firewall to the answers while it listens. On a desk whose firewall drops them, passsweep: true.onvif({address, user, password}):addressmay be an IP, IP:port or the whole service URL; with no port, 80, 8000, 8080, 8899 and 2020 are tried. It resolves{address, device: {manufacturer, model, firmware, serial, hardware}, auth, clockOffset, media, ptz, profiles, errors, ms}; each profile hastoken,name,video {codec, width, height, fps, bitrate, gop, profile},audioornull,rtspandsnapshot.- It rejects only when nothing ONVIF answers at the address. Steps that failed after that are in
errors. auth:none,wsse,digest,required(no login given),refused(wrong login). A refused login is tried once: cameras lock an account after a few failures, so never callonvif()in a loop with a wrong password.- Up to v1.1.16 an app cannot write its settings, and from v1.1.17 it can write only a setting whose manifest says
"writable": {"device": true}— never one with asecretfield, which is where a camera password lives. So either the operator enters the camera's address and login as settings and the app callsonvif()at start (about a second on a LAN), or the app's setup screen shows the chosen profile'srtspfor the operator to copy into a setting. - Feature
cameraOnvif(v1.1.14 and later). The device itself (127.0.0.1) is refused on a device.
On a Raspberry Pi
- Prefer H.264 on the cameras. The engine cannot use the Pi's H.265 decoder, so H.265 decodes in software (usable for a sub-stream, about 1 frame a second at 4K).
- Six cameras in hardware. A Pi 4's firmware has decoder memory for six H.264 streams at the image's stock setting. A seventh is shown anyway, decoded in software (its CPU cost has not been measured; expect more the bigger the stream), while the first six stay in hardware. For more in hardware, add
gpu_mem=256under[all]in/boot/firmware/config.txtand reboot; the image does not do this for everyone. A seven-camera wall at that setting measured about 30 % CPU on a Pi 4. - Use sub-streams for a wall. The hardware decodes about 1080p60 in total however it is split; a 4K H.265 main stream is beyond it.
- No second monitor yet: the Pi image has one screen.
Checked: set() resolving a status whose url carries no login, status(), url(), video() and the refusal of a password with no user ("a password needs a user to go with it") were observed in a local ardaplayer run; every name and option is from ArdaJS(). The manifest passes ArdaForm's pack-time parser. The calls are shaped like those of apps running on devices today. Playback, the second monitor and ONVIF were not verified against a device here; the Raspberry Pi figures are the player team's measurements.