openvibers/openvibe.cam Commits

The camera works after an in-place move; openvibe-shared 4.102.0 (#57)

OP OpenVibers committed 11 days ago · 2feebb2785 · 4 files, +141 −129

* The camera app is wired when boost swaps its page in, and the camera turns off when the page is left in place

* openvibe-shared v4.102.0 (was v4.100.1): boost lets each parsed page go

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex ab41142..089c695 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -10,11 +10,11 @@
"domain": "openvibe.cam",
"contracts": "openvibe-contracts v0.237.0",
"sdk": "openvibe-sdk v0.81.0",
-- "shared": "openvibe-shared v4.100.1",
++ "shared": "openvibe-shared v4.102.0",
"packages": [
"openvibe-contracts v0.237.0",
"openvibe-sdk v0.81.0",
-- "openvibe-shared v4.100.1"
++ "openvibe-shared v4.102.0"
],
"summary": "OpenVibe.Cam is the camera app, the private wall for cameras a person owns, the API Live's cam button calls, and F1's event timeline. The camera preview, timer, burst, grid and filter run in the browser; a photo is posted to Cam and forwarded to OpenVibe.Pics with the person's own Network token. A camera is private: its owner sees it, and a viewer sees it only while a grant naming them is unrevoked and unexpired, checked on every read, so a revocation is refused at once. Familiar faces proxy the owner's own token to OpenVibe.Vision's library; Cam keeps only the Vision library id and no template or crop. A detection becomes one event with a 10 s/20 s clip reference, a still, away-mode priority, a Network notification, a private link and a police export.",
"works": [
ddiff --git a/package-lock.json b/package-lock.json
iindex ae6ad28..213b7b6 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.237.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.100.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.102.0",
"pg": "^8.23.0"
},
"devDependencies": {
@@@ -1194,9 +1194,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.100.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.100.1",
-- "integrity": "sha512-99cgDZNFPj/+EsDJRfjbS5exWMTFA55w2Ixu95apRw/07L7aiQTuE00ZkA6jCTJ6rZxz4U7xgbgZvc2KSM+vyA==",
++ "version": "4.102.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.102.0",
++ "integrity": "sha512-9giJmk+LwIp6skYUejaBtkHfCRHTQlbGM3PIfGZVXo7rUdwLeFC89CLtQehEnPw2Q9inlffsgVblJjALq88EQg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex 423e4f2..5e330b1 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.237.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.100.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.102.0",
"pg": "^8.23.0"
},
"engines": {
ddiff --git a/public/js/cam.js b/public/js/cam.js
iindex 02da2b4..8c6403d 100644
---- a/public/js/cam.js
++++ b/public/js/cam.js
@@@ -4,24 +4,6 @@
(function () {
'use strict';
-- var root = document.getElementById('cm-camera');
-- if (!root) return;
-- var video = root.querySelector('[data-cm-preview]');
-- var canvas = root.querySelector('[data-cm-canvas]');
-- var statusEl = root.querySelector('[data-cm-status]');
-- var startBtn = root.querySelector('[data-cm-start]');
-- var shootBtn = root.querySelector('[data-cm-shoot]');
-- var timerSel = root.querySelector('[data-cm-timer]');
-- var burstBtn = root.querySelector('[data-cm-burst]');
-- var filterSel = root.querySelector('[data-cm-filter]');
-- var captionEl = root.querySelector('[data-cm-caption]');
-- var gridBtn = root.querySelector('[data-cm-grid-toggle]');
-- var gridEl = root.querySelector('[data-cm-grid]');
-- var fileInput = root.querySelector('[data-cm-file]');
-- var gallery = document.getElementById('cm-gallery');
-- var stream = null;
-- var busy = false;
--
var FILTERS = {
none: 'none',
mono: 'grayscale(1) contrast(1.08)',
@@@ -32,119 +14,149 @@
blur: 'blur(1.5px)'
};
-- function say(text) { if (statusEl) statusEl.textContent = text; }
++ // The camera on the page now. Leaving the page, for good or for another one swapped in place (boost), turns it off.
++ var stopCamera = null;
++ function off() { if (stopCamera) { stopCamera(); stopCamera = null; } }
++ window.addEventListener('pagehide', off);
++ document.addEventListener('ov:boost:before', off);
++
++ // The camera app is wired when its page is shown: on load, and when boost swaps it in (a link from the home or the
++ // navbar). It used to be wired only on load, so a visit from another page left a camera that did nothing.
++ function wire() {
++ var root = document.getElementById('cm-camera');
++ if (!root || root.dataset.cmReady) return;
++ root.dataset.cmReady = '1';
++ var video = root.querySelector('[data-cm-preview]');
++ var canvas = root.querySelector('[data-cm-canvas]');
++ var statusEl = root.querySelector('[data-cm-status]');
++ var startBtn = root.querySelector('[data-cm-start]');
++ var shootBtn = root.querySelector('[data-cm-shoot]');
++ var timerSel = root.querySelector('[data-cm-timer]');
++ var burstBtn = root.querySelector('[data-cm-burst]');
++ var filterSel = root.querySelector('[data-cm-filter]');
++ var captionEl = root.querySelector('[data-cm-caption]');
++ var gridBtn = root.querySelector('[data-cm-grid-toggle]');
++ var gridEl = root.querySelector('[data-cm-grid]');
++ var fileInput = root.querySelector('[data-cm-file]');
++ var gallery = document.getElementById('cm-gallery');
++ var stream = null;
++ var busy = false;
++
-- function start() {
-- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
-- say('This browser has no camera input. Pick a picture below instead.');
-- return;
++ function say(text) { if (statusEl) statusEl.textContent = text; }
++
++ function start() {
++ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
++ say('This browser has no camera input. Pick a picture below instead.');
++ return;
++ }
++ navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: false }).then(function (s) {
++ stream = s;
++ video.srcObject = s;
++ return video.play();
++ }).then(function () {
++ shootBtn.disabled = false;
++ startBtn.textContent = 'Camera on';
++ say('The camera is on. Nothing is recorded.');
++ }).catch(function () {
++ say('The camera was not allowed. Allow it in the browser, or pick a picture below.');
++ });
}
-- navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: false }).then(function (s) {
-- stream = s;
-- video.srcObject = s;
-- return video.play();
-- }).then(function () {
-- shootBtn.disabled = false;
-- startBtn.textContent = 'Camera on';
-- say('The camera is on. Nothing is recorded.');
-- }).catch(function () {
-- say('The camera was not allowed. Allow it in the browser, or pick a picture below.');
-- });
-- }
-- function drawFiltered() {
-- var w = video.videoWidth || 1280;
-- var h = video.videoHeight || 720;
-- canvas.width = w;
-- canvas.height = h;
-- var ctx = canvas.getContext('2d');
-- ctx.filter = FILTERS[filterSel.value] || 'none';
-- ctx.drawImage(video, 0, 0, w, h);
-- }
++ function drawFiltered() {
++ var w = video.videoWidth || 1280;
++ var h = video.videoHeight || 720;
++ canvas.width = w;
++ canvas.height = h;
++ var ctx = canvas.getContext('2d');
++ ctx.filter = FILTERS[filterSel.value] || 'none';
++ ctx.drawImage(video, 0, 0, w, h);
++ }
-- function addToGallery(p) {
-- if (!gallery || !p || !p.pics_direct) return;
-- var list = gallery.querySelector('.cm-photos');
-- if (!list) {
-- list = document.createElement('ul');
-- list.className = 'cm-photos';
-- gallery.appendChild(list);
++ function addToGallery(p) {
++ if (!gallery || !p || !p.pics_direct) return;
++ var list = gallery.querySelector('.cm-photos');
++ if (!list) {
++ list = document.createElement('ul');
++ list.className = 'cm-photos';
++ gallery.appendChild(list);
++ }
++ var li = document.createElement('li');
++ li.className = 'cm-photo';
++ var a = document.createElement('a');
++ a.className = 'cm-photo-link';
++ a.href = p.pics_url;
++ a.rel = 'noopener';
++ var img = document.createElement('img');
++ img.src = p.pics_direct;
++ img.alt = p.caption || 'A photo on OpenVibe.Cam';
++ img.loading = 'lazy';
++ a.appendChild(img);
++ li.appendChild(a);
++ list.insertBefore(li, list.firstChild);
}
-- var li = document.createElement('li');
-- li.className = 'cm-photo';
-- var a = document.createElement('a');
-- a.className = 'cm-photo-link';
-- a.href = p.pics_url;
-- a.rel = 'noopener';
-- var img = document.createElement('img');
-- img.src = p.pics_direct;
-- img.alt = p.caption || 'A photo on OpenVibe.Cam';
-- img.loading = 'lazy';
-- a.appendChild(img);
-- li.appendChild(a);
-- list.insertBefore(li, list.firstChild);
-- }
-- function upload(blob, type, filterName) {
-- if (busy) return Promise.resolve(null);
-- busy = true;
-- var headers = { 'Content-Type': type, 'X-Filter': filterName || 'none', 'X-Filename': encodeURIComponent('cam-' + Date.now() + '.jpg') };
-- var caption = captionEl && captionEl.value ? captionEl.value : '';
-- if (caption) headers['X-Caption'] = encodeURIComponent(caption);
-- return fetch('/api/v1/photos', { method: 'POST', credentials: 'same-origin', headers: headers, body: blob })
-- .then(function (res) {
-- return res.json().catch(function () { return {}; }).then(function (body) {
-- if (res.status === 401 || res.status === 403) { say('Sign in to save this photo to OpenVibe.Pics.'); return null; }
-- if (!res.ok) { say((body && body.detail) || 'The photo was not saved.'); return null; }
-- say('Saved to OpenVibe.Pics.');
-- addToGallery(body);
-- return body;
-- });
-- })
-- .catch(function () { say('The photo was not saved: the network did not answer.'); return null; })
-- .then(function (out) { busy = false; return out; });
-- }
++ function upload(blob, type, filterName) {
++ if (busy) return Promise.resolve(null);
++ busy = true;
++ var headers = { 'Content-Type': type, 'X-Filter': filterName || 'none', 'X-Filename': encodeURIComponent('cam-' + Date.now() + '.jpg') };
++ var caption = captionEl && captionEl.value ? captionEl.value : '';
++ if (caption) headers['X-Caption'] = encodeURIComponent(caption);
++ return fetch('/api/v1/photos', { method: 'POST', credentials: 'same-origin', headers: headers, body: blob })
++ .then(function (res) {
++ return res.json().catch(function () { return {}; }).then(function (body) {
++ if (res.status === 401 || res.status === 403) { say('Sign in to save this photo to OpenVibe.Pics.'); return null; }
++ if (!res.ok) { say((body && body.detail) || 'The photo was not saved.'); return null; }
++ say('Saved to OpenVibe.Pics.');
++ addToGallery(body);
++ return body;
++ });
++ })
++ .catch(function () { say('The photo was not saved: the network did not answer.'); return null; })
++ .then(function (out) { busy = false; return out; });
++ }
-- function snap() {
-- if (!stream) { say('Turn the camera on first.'); return; }
-- drawFiltered();
-- canvas.toBlob(function (blob) { if (blob) upload(blob, 'image/jpeg', filterSel.value); }, 'image/jpeg', 0.92);
-- }
++ function snap() {
++ if (!stream) { say('Turn the camera on first.'); return; }
++ drawFiltered();
++ canvas.toBlob(function (blob) { if (blob) upload(blob, 'image/jpeg', filterSel.value); }, 'image/jpeg', 0.92);
++ }
-- function countdown(n) {
-- if (n <= 0) { snap(); return; }
-- say(n + '…');
-- setTimeout(function () { countdown(n - 1); }, 1000);
-- }
++ function countdown(n) {
++ if (n <= 0) { snap(); return; }
++ say(n + '…');
++ setTimeout(function () { countdown(n - 1); }, 1000);
++ }
-- function shoot() {
-- var n = parseInt(timerSel.value, 10) || 0;
-- if (n > 0) countdown(n); else snap();
-- }
++ function shoot() {
++ var n = parseInt(timerSel.value, 10) || 0;
++ if (n > 0) countdown(n); else snap();
++ }
-- function burst() {
-- if (!stream) { say('Turn the camera on first.'); return; }
-- say('Burst of three…');
-- for (var i = 0; i < 3; i++) setTimeout(snap, i * 450);
-- }
++ function burst() {
++ if (!stream) { say('Turn the camera on first.'); return; }
++ say('Burst of three…');
++ for (var i = 0; i < 3; i++) setTimeout(snap, i * 450);
++ }
-- startBtn.addEventListener('click', start);
-- shootBtn.addEventListener('click', shoot);
-- burstBtn.addEventListener('click', burst);
-- if (gridBtn) gridBtn.addEventListener('click', function () {
-- if (!gridEl) return;
-- var show = gridEl.hidden;
-- gridEl.hidden = !show;
-- gridBtn.setAttribute('aria-pressed', show ? 'true' : 'false');
-- });
-- if (fileInput) fileInput.addEventListener('change', function () {
-- var f = fileInput.files && fileInput.files[0];
-- if (!f) return;
-- if (!/^image\//.test(f.type)) { say('That is not a picture.'); return; }
-- upload(f, f.type, 'none');
-- fileInput.value = '';
-- });
-- window.addEventListener('pagehide', function () {
-- if (stream) stream.getTracks().forEach(function (t) { t.stop(); });
-- });
++ startBtn.addEventListener('click', start);
++ shootBtn.addEventListener('click', shoot);
++ burstBtn.addEventListener('click', burst);
++ if (gridBtn) gridBtn.addEventListener('click', function () {
++ if (!gridEl) return;
++ var show = gridEl.hidden;
++ gridEl.hidden = !show;
++ gridBtn.setAttribute('aria-pressed', show ? 'true' : 'false');
++ });
++ if (fileInput) fileInput.addEventListener('change', function () {
++ var f = fileInput.files && fileInput.files[0];
++ if (!f) return;
++ if (!/^image\//.test(f.type)) { say('That is not a picture.'); return; }
++ upload(f, f.type, 'none');
++ fileInput.value = '';
++ });
++ stopCamera = function () { if (stream) stream.getTracks().forEach(function (t) { t.stop(); }); stream = null; };
++ }
++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire); else wire();
++ document.addEventListener('ov:boost:load', wire);
})();