From 6eb4b3ca95c1139be4ec8907cce458818c89e594 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 9 Feb 2026 13:27:22 -0500 Subject: [PATCH] Refresh presigned media URL every 7 minutes to prevent TTL expiry Long videos (>15 min) would die mid-playback when the 15-minute presigned URL expired. Now a timer fetches a fresh URL from the watch JSON endpoint every 7 minutes, swaps the src, and seeks back to the saved position. Timer resets on every product transition. --- make_post_sell/static/js/watch.js | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/make_post_sell/static/js/watch.js b/make_post_sell/static/js/watch.js index f67ca11..74a9383 100644 --- a/make_post_sell/static/js/watch.js +++ b/make_post_sell/static/js/watch.js @@ -18,6 +18,9 @@ var djCrossfadeActive = false; var djCrossfadeData = null; var preloadingId = null; + var currentProductId = null; + var urlRefreshTimer = null; + var URL_REFRESH_MS = 7 * 60 * 1000; // 7 minutes // --- Ring state (persisted in localStorage) --- var ringProductIds = []; @@ -161,6 +164,7 @@ var initialProductEl = document.querySelector('[data-watch-product-id]'); if (initialProductEl) { var initialId = initialProductEl.getAttribute('data-watch-product-id'); + currentProductId = initialId; markWatched(initialId); syncRingPosition(initialId); if (initialProductEl.getAttribute('data-watch-is-mod') === '1') { @@ -514,8 +518,10 @@ ringProductIds = data.ring; } if (data.is_mod) isMod = true; + currentProductId = data.product_id; markWatched(data.product_id); syncRingPosition(data.product_id); + scheduleUrlRefresh(); document.title = data.title; @@ -1056,6 +1062,48 @@ standbyMedia.load(); } + // --- Presigned URL refresh: swap src every 7 min to prevent TTL expiry --- + function scheduleUrlRefresh() { + if (urlRefreshTimer) clearTimeout(urlRefreshTimer); + urlRefreshTimer = setTimeout(refreshMediaUrl, URL_REFRESH_MS); + } + + function refreshMediaUrl() { + urlRefreshTimer = null; + if (!activeMedia || !currentProductId) return; + // Skip refresh during crossfade — the new media already has a fresh URL + if (djCrossfadeActive) { scheduleUrlRefresh(); return; } + + fetchWatchData(currentProductId).then(function(data) { + if (!data || !data.media_url) { scheduleUrlRefresh(); return; } + // Only refresh if still on the same product + if (data.product_id !== currentProductId) { scheduleUrlRefresh(); return; } + + var wasPlaying = !activeMedia.paused; + var savedTime = activeMedia.currentTime; + var savedVolume = activeMedia.volume; + var savedMuted = activeMedia.muted; + + activeMedia.src = data.media_url; + + activeMedia.addEventListener('loadeddata', function onLoaded() { + activeMedia.removeEventListener('loadeddata', onLoaded); + activeMedia.currentTime = savedTime; + activeMedia.volume = savedVolume; + activeMedia.muted = savedMuted; + if (wasPlaying) activeMedia.play().catch(function() {}); + scheduleUrlRefresh(); + }); + + // Fallback: if loadeddata never fires, reschedule anyway + setTimeout(function() { + if (!urlRefreshTimer) scheduleUrlRefresh(); + }, 10000); + }).catch(function() { + scheduleUrlRefresh(); + }); + } + // --- Named event handlers (must be named so removeEventListener works) --- function onMediaEnded() { // Fallback: only start countdown if DJ crossfade didn't handle it @@ -1113,6 +1161,7 @@ // Initial setup if (activeMedia) { setupMediaEvents(); + scheduleUrlRefresh(); } else { // Static content (PDF, image, etc.) — start 42-second timer // Reset on scroll — scrolling means the user is still reading