mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
ROOT CAUSE (hypothesis A, pre-existing — NOT a beta7 regression; server/player/index.html is untouched since v1.9.2-beta6): handlePlaylistUpdate's "Playlist unchanged" branch blindly returned. The media re-attach (renderContent) lives ONLY in the content-changed branch, so if the <video> surface was lost (element detached from the DOM while still decoding — video gone, audio still playing) a no-new-content refresh never re-attached it. New-content refreshes were fine because they re-render. FIX (make the refresh idempotent for the media surface, no flicker on the healthy path): - server/lib/player-media-health.js (new, UMD + unit-testable, mirrors schedule-eval.js): needsReattach(state) — re-attach ONLY when playback should be happening but the current item's surface is actually lost (video null / detached / ended / errored; non-video: no mounted surface). A healthy attached+live video returns false, so a routine poll stays a no-op (no re-render, no flicker). Served at /player/player-media-health.js from the single source; loaded by the player. - index.html no-change branch: extract the current item's DOM facts and, iff PlayerMediaHealth.needsReattach, call playCurrentItem() to re-render the current item. Wrapped so the health check can never break a refresh. - teardownCurrentMedia: also release currentVideoEl even when it was DETACHED from the container — a detached-but-playing <video> keeps emitting audio and the container-scoped querySelectorAll can't find it. This kills the "ghost audio" on re-attach. - sw.js cache bumped v9 -> v10 so players pick up the new index.html + module. Tests: test/player-media-health.test.js (6) exercises the branch selection — healthy video -> no re-attach; detached/null/ended/errored -> re-attach; idle -> never; non-video by surface presence. Inline player JS syntax-checked; module served + referenced verified on a booted server. Suite 316/316. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
1.8 KiB
JavaScript
53 lines
1.8 KiB
JavaScript
const CACHE_NAME = 'rd-player-v10';
|
|
|
|
// Install: skip waiting to activate immediately
|
|
self.addEventListener('install', (event) => {
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate: clean old caches (including old content cache), claim clients
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then(keys => Promise.all(
|
|
keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
|
|
)).then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// Fetch handler — ONLY cache player page and static assets.
|
|
// Content files (/uploads/content/) are NOT intercepted — the server sets
|
|
// Cache-Control: public, max-age=2592000, immutable which lets the browser
|
|
// cache them natively without SW complications (range requests, opaque
|
|
// responses, video seeking, etc.)
|
|
self.addEventListener('fetch', (event) => {
|
|
// Only handle GET requests
|
|
if (event.request.method !== 'GET') return;
|
|
|
|
const url = new URL(event.request.url);
|
|
|
|
// Player page and static assets: network-first, fall back to cache
|
|
if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') {
|
|
event.respondWith(
|
|
fetch(event.request).then(response => {
|
|
if (response.ok && response.type !== 'opaque') {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
|
|
}
|
|
return response;
|
|
}).catch(() =>
|
|
caches.match(event.request, { ignoreSearch: true }).then(cached =>
|
|
cached || new Response('Offline', {
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
})
|
|
)
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Everything else (content files, API calls, etc.): don't intercept.
|
|
// Returning without event.respondWith lets the browser handle it natively.
|
|
});
|