1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
// Signing a browser out after a stretch with nobody at it.
//
// The hub cannot measure this. It hears a token renewal every few hours from
// any open tab, attended or not, and nothing at all while a film plays over
// WebRTC. So the page decides, and the hub only says how long
// (`browser_idle_hours` in /v1/hub/info).
//
// Activity is input, or any <video>/<audio> on the page that is playing: a film
// nobody touches for two hours is somebody watching it. The last-active time
// lives in localStorage so every tab of this browser shares it — one idle tab
// must not sign out the tab in use — and a browser closed without signing out
// is caught the moment it is opened again.
//
// Never started in the desktop application (app.js): that is its owner's own
// machine, and it signs back in with its device key.
const LAST_ACTIVE_KEY = 'mb_last_active';
const CHECK_MS = 60000;
// Input fires constantly; the timestamp only has to be minutes-accurate.
const WRITE_EVERY_MS = 30000;
const INPUT_EVENTS = ['pointerdown', 'keydown', 'wheel', 'touchstart'];
let lastWrite = 0;
function readLastActive() {
try {
const v = Number(localStorage.getItem(LAST_ACTIVE_KEY));
return Number.isFinite(v) && v > 0 ? v : null;
} catch {
return null;
}
}
/** Record activity now. `force` skips the write throttle — a sign-in uses it. */
function markActive(force = false) {
const now = Date.now();
if (!force && now - lastWrite < WRITE_EVERY_MS) return;
lastWrite = now;
try { localStorage.setItem(LAST_ACTIVE_KEY, String(now)); } catch { /* private mode */ }
}
function mediaPlaying() {
return [...document.querySelectorAll('video, audio')]
.some((m) => !m.paused && !m.ended);
}
/**
* Watch for `idleMs` without activity, then call `onIdle` once.
* Returns the function that stops watching.
*/
function startIdleWatch(idleMs, onIdle) {
let fired = false;
const onInput = () => markActive();
const check = () => {
if (fired) return;
if (mediaPlaying()) { markActive(); return; }
const last = readLastActive();
// No record at all is a browser that predates this, not an idle one.
if (last === null) { markActive(true); return; }
if (Date.now() - last > idleMs) { fired = true; onIdle(); }
};
INPUT_EVENTS.forEach((e) =>
window.addEventListener(e, onInput, { capture: true, passive: true }));
document.addEventListener('visibilitychange', check);
const timer = setInterval(check, CHECK_MS);
check();
return () => {
INPUT_EVENTS.forEach((e) =>
window.removeEventListener(e, onInput, { capture: true }));
document.removeEventListener('visibilitychange', check);
clearInterval(timer);
};
}
export { startIdleWatch, markActive, LAST_ACTIVE_KEY };
|