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.

Working — describes ArdaPlayer v1.2.3

A camera in the page

app/index.html excerpt
<div id="door" style="width: 640px; height: 360px; background: #000"></div>
app/cameras.js
// 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.
  • el may 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 maxLag seconds (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 path fps (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) and Arda.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 user and password, 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. A password with no user anywhere is refused.
  • Keep cameras in a rows setting with the password as a secret field, per device if devices see different cameras:
app/manifest.json settings
{
  "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?

app/cameras.js
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.

app/wall.js
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}]. draw is gpu, direct or cpu; via: 'rtmp' when a Reolink is played over RTMP. From v1.1.17 also behind (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) and why. 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:// and rtmp:// 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 no cameraScreen feature. 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" in arda.dev.json stands in for the second monitor: show() draws the camera into that window.

Finding cameras: ONVIF

app/setup.js
// 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 (timeout in 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 into onvif(). On a device the player opens its own firewall to the answers while it listens. On a desk whose firewall drops them, pass sweep: true.
  • onvif({address, user, password}): address may 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 has token, name, video {codec, width, height, fps, bitrate, gop, profile}, audio or null, rtsp and snapshot.
  • 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 call onvif() 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 a secret field, which is where a camera password lives. So either the operator enters the camera's address and login as settings and the app calls onvif() at start (about a second on a LAN), or the app's setup screen shows the chosen profile's rtsp for 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=256 under [all] in /boot/firmware/config.txt and 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.