add video backend server, fix styling and more #54
+48
-7
@@ -63,6 +63,7 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
|||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
let lastSavedAt = 0;
|
let lastSavedAt = 0;
|
||||||
let lastSavedTime = -1;
|
let lastSavedTime = -1;
|
||||||
|
let lastSaveStatus = '—';
|
||||||
|
|
||||||
function getSnapshot() {
|
function getSnapshot() {
|
||||||
const t = Number(videoEl.currentTime);
|
const t = Number(videoEl.currentTime);
|
||||||
@@ -96,13 +97,18 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
|||||||
const clamped = Math.min(Math.max(targetTime, 0), Math.max(0, dur - 3));
|
const clamped = Math.min(Math.max(targetTime, 0), Math.max(0, dur - 3));
|
||||||
if (clamped > 1) {
|
if (clamped > 1) {
|
||||||
try { videoEl.currentTime = clamped; } catch (_) {}
|
try { videoEl.currentTime = clamped; } catch (_) {}
|
||||||
|
lastSaveStatus = `Reprise à ${formatClock(clamped)}.`;
|
||||||
|
setProgressStatus(lastSaveStatus);
|
||||||
|
} else {
|
||||||
|
lastSaveStatus = 'Lecture depuis le début.';
|
||||||
|
setProgressStatus(lastSaveStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persist({ keepalive } = {}) {
|
async function persist({ keepalive, force } = {}) {
|
||||||
// Throttle to avoid spamming the API
|
// Throttle to avoid spamming the API
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!keepalive && now - lastSavedAt < 2000) return;
|
if (!force && !keepalive && now - lastSavedAt < 3000) return;
|
||||||
|
|
||||||
const snap = getSnapshot();
|
const snap = getSnapshot();
|
||||||
if (!Number.isFinite(snap.time)) return;
|
if (!Number.isFinite(snap.time)) return;
|
||||||
@@ -117,18 +123,28 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
|||||||
localStorage.setItem(key, JSON.stringify({ time: snap.time, duration: snap.duration, at: now }));
|
localStorage.setItem(key, JSON.stringify({ time: snap.time, duration: snap.duration, at: now }));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
setProgressStatus('Sauvegarde…');
|
||||||
const ok = await savePlaybackProgress(movieId, token, { ...snap, keepalive: !!keepalive });
|
const ok = await savePlaybackProgress(movieId, token, { ...snap, keepalive: !!keepalive });
|
||||||
if (ok === false) {
|
if (ok === false) {
|
||||||
// Token expired mid-playback; stop spamming requests and prompt user.
|
// Token expired mid-playback; stop spamming requests and prompt user.
|
||||||
stopInterval();
|
stopInterval();
|
||||||
try { localStorage.removeItem('auth_token'); } catch (_) {}
|
try { localStorage.removeItem('auth_token'); } catch (_) {}
|
||||||
|
lastSaveStatus = 'Connexion requise pour sauvegarder.';
|
||||||
|
setProgressStatus(lastSaveStatus);
|
||||||
showAlertVariant('Session expirée. Reconnecte-toi pour continuer à sauvegarder la progression.', 'warning');
|
showAlertVariant('Session expirée. Reconnecte-toi pour continuer à sauvegarder la progression.', 'warning');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
lastSaveStatus = `Sauvegardé à ${formatClock(snap.time)}.`;
|
||||||
|
setProgressStatus(lastSaveStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startInterval() {
|
function startInterval() {
|
||||||
if (intervalId) return;
|
if (intervalId) return;
|
||||||
intervalId = window.setInterval(() => { void persist(); }, 10000);
|
// Safety-net; primary saving happens via timeupdate throttling.
|
||||||
|
intervalId = window.setInterval(() => {
|
||||||
|
if (videoEl.paused || videoEl.seeking) return;
|
||||||
|
void persist();
|
||||||
|
}, 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopInterval() {
|
function stopInterval() {
|
||||||
@@ -138,13 +154,17 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPlay = () => startInterval();
|
const onPlay = () => startInterval();
|
||||||
const onPause = () => { stopInterval(); void persist(); };
|
const onPause = () => { stopInterval(); void persist({ force: true }); };
|
||||||
const onSeeked = () => { void persist(); };
|
const onSeeked = () => { void persist({ force: true }); };
|
||||||
const onEnded = () => { stopInterval(); void persist(); };
|
const onEnded = () => { stopInterval(); void persist(); };
|
||||||
const onError = () => { stopInterval(); };
|
const onError = () => { stopInterval(); };
|
||||||
const onPageHide = () => { void persist({ keepalive: true }); };
|
const onTimeUpdate = () => {
|
||||||
|
if (videoEl.paused || videoEl.seeking) return;
|
||||||
|
void persist();
|
||||||
|
};
|
||||||
|
const onPageHide = () => { void persist({ keepalive: true, force: true }); };
|
||||||
const onVisibility = () => {
|
const onVisibility = () => {
|
||||||
if (document.visibilityState === 'hidden') void persist({ keepalive: true });
|
if (document.visibilityState === 'hidden') void persist({ keepalive: true, force: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
videoEl.addEventListener('loadedmetadata', restoreProgressOnce, { once: true });
|
videoEl.addEventListener('loadedmetadata', restoreProgressOnce, { once: true });
|
||||||
@@ -153,6 +173,7 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
|||||||
videoEl.addEventListener('seeked', onSeeked);
|
videoEl.addEventListener('seeked', onSeeked);
|
||||||
videoEl.addEventListener('ended', onEnded);
|
videoEl.addEventListener('ended', onEnded);
|
||||||
videoEl.addEventListener('error', onError);
|
videoEl.addEventListener('error', onError);
|
||||||
|
videoEl.addEventListener('timeupdate', onTimeUpdate);
|
||||||
|
|
||||||
window.addEventListener('pagehide', onPageHide);
|
window.addEventListener('pagehide', onPageHide);
|
||||||
document.addEventListener('visibilitychange', onVisibility);
|
document.addEventListener('visibilitychange', onVisibility);
|
||||||
@@ -167,8 +188,10 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
|||||||
videoEl.removeEventListener('seeked', onSeeked);
|
videoEl.removeEventListener('seeked', onSeeked);
|
||||||
videoEl.removeEventListener('ended', onEnded);
|
videoEl.removeEventListener('ended', onEnded);
|
||||||
videoEl.removeEventListener('error', onError);
|
videoEl.removeEventListener('error', onError);
|
||||||
|
videoEl.removeEventListener('timeupdate', onTimeUpdate);
|
||||||
window.removeEventListener('pagehide', onPageHide);
|
window.removeEventListener('pagehide', onPageHide);
|
||||||
document.removeEventListener('visibilitychange', onVisibility);
|
document.removeEventListener('visibilitychange', onVisibility);
|
||||||
|
if (lastSaveStatus) setProgressStatus(lastSaveStatus);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +229,21 @@ function setSourceLabel(text) {
|
|||||||
el.title = text || '';
|
el.title = text || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setProgressStatus(text) {
|
||||||
|
const el = document.getElementById('progress-status');
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = text || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatClock(seconds) {
|
||||||
|
const s = Math.max(0, Math.floor(Number(seconds) || 0));
|
||||||
|
const h = Math.floor(s / 3600);
|
||||||
|
const m = Math.floor((s % 3600) / 60);
|
||||||
|
const sec = s % 60;
|
||||||
|
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||||
|
return `${m}:${String(sec).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function setHeader(movie) {
|
function setHeader(movie) {
|
||||||
const titleEl = document.getElementById('movie-title');
|
const titleEl = document.getElementById('movie-title');
|
||||||
const categoryEl = document.getElementById('movie-category');
|
const categoryEl = document.getElementById('movie-category');
|
||||||
@@ -614,7 +652,10 @@ window.onload = async function () {
|
|||||||
// Only the <video> player supports reliable progress tracking (mp4/hls).
|
// Only the <video> player supports reliable progress tracking (mp4/hls).
|
||||||
const videoEl = document.getElementById('video');
|
const videoEl = document.getElementById('video');
|
||||||
if (videoEl && !videoEl.classList.contains('d-none')) {
|
if (videoEl && !videoEl.classList.contains('d-none')) {
|
||||||
|
setProgressStatus('Prépare la reprise…');
|
||||||
setupVideoProgress({ videoEl, movieId: id, token, userId: decoded?.id });
|
setupVideoProgress({ videoEl, movieId: id, token, userId: decoded?.id });
|
||||||
|
} else {
|
||||||
|
setProgressStatus('—');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert(err.message || 'Network error.');
|
showAlert(err.message || 'Network error.');
|
||||||
|
|||||||
@@ -115,6 +115,11 @@
|
|||||||
<span class="badge bg-secondary">Source</span>
|
<span class="badge bg-secondary">Source</span>
|
||||||
<span id="source-label" class="badge bg-light text-dark badge-src">—</span>
|
<span id="source-label" class="badge bg-light text-dark badge-src">—</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 d-flex flex-wrap gap-2 align-items-center">
|
||||||
|
<span class="badge bg-secondary">Progress</span>
|
||||||
|
<span id="progress-status" class="text-muted small">—</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user