openvibers/openvibe.live Commits

Live player: one clock (no buffer-length time beside the uptime at the live edge; rewound it reads how far behind live), a calmer LIVE / Go live button and plain viewer and uptime stats (#285)

OP OpenVibers committed 24 days ago · 403fb096df · 5 files, +109 −34

Changes

ddiff --git a/public/css/features/channel.css b/public/css/features/channel.css
iindex 3a487212..b2173787 100644
---- a/public/css/features/channel.css
++++ b/public/css/features/channel.css
@@@ -512,10 +512,10 @@
}
}
.viewer-badge, .uptime-badge {
-- font-size: 0.75rem; color: rgba(255,255,255,0.85);
-- display: flex; align-items: center; gap: 4px;
-- padding: 2px 8px; border-radius: var(--radius-sm);
-- background: rgba(255,255,255,0.08);
++ font-size: 0.78rem; color: rgba(255,255,255,0.9);
++ display: flex; align-items: center; gap: 5px;
++ padding: 0 6px;
++ font-variant-numeric: tabular-nums;
}
.dvr-progress-wrap:hover .dvr-progress-bar {
height: 6px;
ddiff --git a/public/css/style.css b/public/css/style.css
iindex 424f0e15..8d4b358d 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -2844,9 +2844,8 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
--/* Badges */
--.viewer-badge i { color: var(--live-red); }
--.uptime-badge i { color: var(--accent); }
++/* Viewers and uptime: plain stats on the bar, not coloured pills (owner, 2026-10-20: "this gui should be improved"). */
++.viewer-badge i, .uptime-badge i { color: rgba(255,255,255,0.65); }
/* ── DVR Progress Bar (Live Stream Seeking) ─────────────────── */
.dvr-progress-wrap {
@@@ -2881,25 +2880,28 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
/* DVR in non-live (rewound) mode: use accent color instead of red */
.dvr-time-display {
-- font-size: 0.75rem; color: rgba(255,255,255,0.85);
-- font-family: var(--font-mono);
++ font-size: 0.78rem; color: rgba(255,255,255,0.9);
++ font-variant-numeric: tabular-nums;
white-space: nowrap; padding: 0 4px;
-- min-width: 82px; text-align: center;
}
++.dvr-time-display[hidden] { display: none !important; }
++/* LIVE: a red dot while at the live edge; behind it, a grey dot and "Go live" (a click jumps back). */
.dvr-live-btn {
-- background: #e53e3e !important; color: #fff !important;
-- font-size: 0.7rem !important; font-weight: 700;
-- padding: 3px 10px !important; border-radius: 4px !important;
-- gap: 4px; letter-spacing: 0.5px;
-- animation: livePulse 1.5s infinite;
--}
--.dvr-live-btn:hover { background: #c53030 !important; }
--.dvr-live-btn.dvr-behind {
-- background: rgba(255,255,255,0.15) !important;
-- border: 1px solid rgba(255,255,255,0.3);
-- animation: none;
++ color: #fff !important;
++ font-size: 0.72rem !important; font-weight: 700;
++ letter-spacing: 0.06em; text-transform: uppercase;
++ padding: 4px 10px !important; border-radius: 999px !important;
++ gap: 7px;
++}
++.dvr-live-btn .live-dot {
++ display: inline-block; flex-shrink: 0;
++ width: 8px; height: 8px; border-radius: 50%;
++ background: var(--live-red, #e53e3e);
++ box-shadow: 0 0 0 3px rgba(229,62,62,0.28);
}
++.dvr-live-btn.dvr-behind { background: rgba(255,255,255,0.14) !important; }
++.dvr-live-btn.dvr-behind .live-dot { background: rgba(255,255,255,0.6); box-shadow: none; animation: none; }
/* Progress bar (for VOD/clip players) */
.vc-progress-wrap {
ddiff --git a/public/fragments/channel.html b/public/fragments/channel.html
iindex af548c5c..b1073bae 100644
---- a/public/fragments/channel.html
++++ b/public/fragments/channel.html
@@@ -40,11 +40,9 @@
</button>
<input type="range" id="volume-slider" min="0" max="100" value="75" class="volume-slider" aria-label="Volume">
</div>
-- <span class="dvr-time-display" id="dvr-time-display" style="display:none">0:00 / 0:00</span>
++ <span class="dvr-time-display" id="dvr-time-display" style="display:none" hidden></span>
<div class="vc-spacer"></div>
-- <button class="vc-btn dvr-live-btn" id="dvr-live-btn" title="Jump to Live" style="display:none">
-- <i class="fa-solid fa-tower-broadcast"></i> LIVE
-- </button>
++ <button class="vc-btn dvr-live-btn" id="dvr-live-btn" title="Jump to live" style="display:none"><span class="live-dot"></span>LIVE</button>
<span class="viewer-badge" id="viewer-count-badge">
<i class="fa-solid fa-eye"></i> <span id="vc-viewers">0</span>
</span>
ddiff --git a/public/js/stream-player.js b/public/js/stream-player.js
iindex 8720f388..438fb1b6 100644
---- a/public/js/stream-player.js
++++ b/public/js/stream-player.js
@@@ -2391,20 +2391,22 @@ function updateDVRUI() {
const container = document.getElementById('video-container');
if (dvrState.isLive) {
-- // At live edge — progress bar is full (100%)
++ // At the live edge the bar shows no time of its own: the uptime beside it is the one clock. This read the DVR
++ // buffer's length, which trails the stream by a segment or more ("1:30:00 [LIVE]" next to "1:30:17").
if (fill) fill.style.width = '100%';
-- const elapsed = dvrState.duration > 0
-- ? dvrState.duration
-- : Math.max(0, Math.floor((Date.now() - dvrState.streamStartTime) / 1000));
-- if (timeEl) timeEl.textContent = `${_dvrFmtTime(elapsed)} [LIVE]`;
++ if (timeEl) { timeEl.hidden = true; timeEl.textContent = ''; }
if (container) container.classList.remove('dvr-rewound');
} else {
-- // Rewound — show position within the DVR buffer
++ // Rewound: how far behind live, with the position in the buffer as its title
const video = document.getElementById('video-element');
if (video && dvrState.duration > 0 && !dvrState.seeking) {
const pct = (video.currentTime / dvrState.duration) * 100;
if (fill) fill.style.width = Math.min(pct, 100) + '%';
-- if (timeEl) timeEl.textContent = `${_dvrFmtTime(video.currentTime)} / ${_dvrFmtTime(dvrState.duration)}`;
++ if (timeEl) {
++ timeEl.hidden = false;
++ timeEl.textContent = `\u2212${_dvrFmtTime(dvrState.duration - video.currentTime)}`;
++ timeEl.title = `${_dvrFmtTime(video.currentTime)} of ${_dvrFmtTime(dvrState.duration)} recorded; behind live`;
++ }
}
if (container) container.classList.add('dvr-rewound');
}
@@@ -2531,7 +2533,7 @@ function switchToDVR(targetTime) {
// Show "Jump to Live" button in alert style
if (liveBtn) {
liveBtn.classList.add('dvr-behind');
-- liveBtn.innerHTML = '<i class="fa-solid fa-forward"></i> Back to LIVE';
++ liveBtn.innerHTML = '<span class="live-dot"></span>Go live';
}
// Ensure the video element is visible (JSMPEG normally hides it)
@@@ -2615,7 +2617,7 @@ function jumpToLive() {
// Reset live button
if (liveBtn) {
liveBtn.classList.remove('dvr-behind');
-- liveBtn.innerHTML = '<i class="fa-solid fa-tower-broadcast"></i> LIVE';
++ liveBtn.innerHTML = '<span class="live-dot"></span>LIVE';
}
updateDVRUI();
ddiff --git a/test/player-live-clock.test.js b/test/player-live-clock.test.js
nnew file mode 100644
iindex 00000000..41ad3405
---- /dev/null
++++ b/test/player-live-clock.test.js
@@@ -0,0 +1,73 @@
++/**
++ * The live player's bar has one clock (owner, 2026-10-20: "why is there two spots for this and why is one inaccurate").
++ * At the live edge the DVR time showed the recorded buffer's length ("1:30:00 [LIVE]"), which trails the stream by a
++ * segment or more, next to the uptime ("1:30:17"). Now the bar shows no time of its own at the live edge; rewound, it
++ * shows how far behind live you are. The LIVE button is a dot and a word, grey with "Go live" when behind.
++ *
++ * node test/player-live-clock.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const root = path.join(__dirname, '..');
++const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
++const src = read('public/js/stream-player.js');
++
++/** The source of a top-level function declaration, braces matched (after its parameter list). */
++function extract(name) {
++ const m = new RegExp(`(?:async )?function ${name}\\(`).exec(src);
++ assert.ok(m, `${name}() must exist`);
++ let i = m.index + m[0].length;
++ for (let parens = 1; parens; i++) { if (src[i] === '(') parens++; else if (src[i] === ')') parens--; }
++ i = src.indexOf('{', i);
++ for (let depth = 0; i < src.length; i++) {
++ if (src[i] === '{') depth++;
++ else if (src[i] === '}' && --depth === 0) return src.slice(m.index, i + 1);
++ }
++ throw new Error(`unbalanced ${name}`);
++}
++
++const els = {
++ 'dvr-progress-fill': { style: {} },
++ 'dvr-time-display': { hidden: false, textContent: '0:00 / 0:00', title: '' },
++ 'video-container': { classList: { set: new Set(), add(c) { this.set.add(c); }, remove(c) { this.set.delete(c); } } },
++ 'video-element': { currentTime: 0 },
++};
++const ctx = { document: { getElementById: (id) => els[id] || null }, Date, Math, isNaN, isFinite, String };
++vm.createContext(ctx);
++vm.runInContext(`${extract('_dvrFmtTime')}\n${extract('updateDVRUI')}\nthis.run = updateDVRUI;`, ctx);
++
++// At the live edge: no time of its own (the buffer is 1:30:00 while the stream is 1:30:17 old).
++ctx.dvrState = { active: true, isLive: true, duration: 5400, streamStartTime: Date.now() - 5417000, seeking: false };
++vm.runInContext('var dvrState = this.dvrState;', ctx);
++ctx.run();
++const time = els['dvr-time-display'];
++assert.strictEqual(time.hidden, true, 'no second clock at the live edge');
++assert.strictEqual(time.textContent, '');
++assert.ok(!src.includes('} [LIVE]`'), 'the "[LIVE]" time is gone');
++
++// Rewound 42 s: how far behind live, with the position as the title.
++vm.runInContext('dvrState.isLive = false;', ctx);
++els['video-element'].currentTime = 5358;
++ctx.run();
++assert.strictEqual(time.hidden, false);
++assert.strictEqual(time.textContent, '−0:42');
++assert.match(time.title, /^1:29:18 of 1:30:00 recorded; behind live$/);
++assert.ok(els['video-container'].classList.set.has('dvr-rewound'));
++
++// The LIVE button: a dot and a word; grey "Go live" when behind.
++assert.ok(src.includes(`liveBtn.innerHTML = '<span class="live-dot"></span>Go live';`));
++assert.ok(src.includes(`liveBtn.innerHTML = '<span class="live-dot"></span>LIVE';`));
++const fragment = read('public/fragments/channel.html');
++assert.ok(fragment.includes('<button class="vc-btn dvr-live-btn" id="dvr-live-btn" title="Jump to live" style="display:none"><span class="live-dot"></span>LIVE</button>'));
++assert.ok(fragment.includes('<span class="dvr-time-display" id="dvr-time-display" style="display:none" hidden></span>'));
++const css = read('public/css/style.css');
++assert.match(css, /\.dvr-time-display\[hidden\] \{ display: none !important; \}/);
++assert.match(css, /\.dvr-live-btn\.dvr-behind \.live-dot \{[^}]*animation: none;/);
++assert.ok(!/\.dvr-live-btn \{[^}]*livePulse/.test(css), 'no pulsing red box');
++assert.match(read('public/css/features/channel.css'), /\.viewer-badge, \.uptime-badge \{[^}]*font-variant-numeric: tabular-nums;/);
++
++console.log('player live clock: one clock at the live edge, "behind live" when rewound — all checks passed');