feat: close STREAMING.md production readiness gaps
- Added run ID + unique segment naming (r-{ISOTIME}/seg-{seq}.m4s)
- Added fsnotify watcher for real-time R2 segment upload (~7s)
- Added atomic manifest publication (temp key → CopyObject)
- Fixed manifest Cache-Control: public, max-age=1, s-maxage=2, must-revalidate
- Added hls.js error recovery: bounded retries (5x), exponential backoff, reconnecting UI
- Enriched /health endpoint: run_id, active_item, last_segment_seq, last_r2_pub, consecutive_failures, unhealthy detection
- Updated tests: HealthHandler (new fields + unhealthy), player error recovery test
- Updated TODO.md, BACKLOG.md, DONE.md, MEMORY.md
This commit is contained in:
1 parent
f6ca89747e
commit
18459a9811
12 files changed
+699
-48
No files matched your search
+75
-1
@@ -18,6 +18,11 @@ let username = "viewer" + Math.floor(Math.random() * 1000);
|
||||
let chatVisible = true;
|
||||
const onlineUsers = new Set<string>();
|
||||
|
||||
// Player error recovery state (STREAMING.md contract)
|
||||
let playerErrorCount = 0;
|
||||
const PLAYER_MAX_RETRIES = 5;
|
||||
const PLAYER_BASE_DELAY_MS = 2000;
|
||||
|
||||
export function resetViewerStateForTests() {
|
||||
hls = null;
|
||||
ws = null;
|
||||
@@ -25,6 +30,7 @@ export function resetViewerStateForTests() {
|
||||
username = "viewer";
|
||||
chatVisible = true;
|
||||
onlineUsers.clear();
|
||||
playerErrorCount = 0;
|
||||
}
|
||||
|
||||
export function resolveWebSocketBaseUrl() {
|
||||
@@ -159,10 +165,60 @@ export async function initPlayerAndChat(loadHlsModule: () => Promise<{ default:
|
||||
try {
|
||||
const { default: Hls } = await loadHlsModule();
|
||||
if (Hls.isSupported()) {
|
||||
hls = new Hls();
|
||||
hls = new Hls({
|
||||
liveDurationInfinity: true,
|
||||
liveSyncDurationCount: 3,
|
||||
});
|
||||
hls.loadSource(manifestUrl);
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play());
|
||||
|
||||
// Player error recovery (STREAMING.md contract)
|
||||
hls.on(Hls.Events.ERROR, (_event, data) => {
|
||||
if (!data.fatal) return;
|
||||
|
||||
playerErrorCount++;
|
||||
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
if (playerErrorCount <= PLAYER_MAX_RETRIES) {
|
||||
const delay = PLAYER_BASE_DELAY_MS * Math.pow(2, playerErrorCount - 1);
|
||||
console.warn(`HLS network error (attempt ${playerErrorCount}/${PLAYER_MAX_RETRIES}), retrying in ${delay}ms...`);
|
||||
showReconnecting("Reconnecting...");
|
||||
setTimeout(() => {
|
||||
hls?.startLoad();
|
||||
}, delay);
|
||||
} else {
|
||||
console.error("HLS network error: max retries exhausted");
|
||||
showReconnecting("Stream temporarily unavailable");
|
||||
}
|
||||
break;
|
||||
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
if (playerErrorCount <= 1) {
|
||||
console.warn("HLS media error, attempting recovery...");
|
||||
showReconnecting("Recovering...");
|
||||
hls.recoverMediaError();
|
||||
} else {
|
||||
console.error("HLS media error: recovery failed");
|
||||
hls.destroy();
|
||||
showReconnecting("Stream temporarily unavailable");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error("HLS fatal error:", data.type, data.details);
|
||||
hls.destroy();
|
||||
showReconnecting("Stream temporarily unavailable");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset error count on successful manifest parse
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
playerErrorCount = 0;
|
||||
hideReconnecting();
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load HLS player", error);
|
||||
@@ -179,6 +235,24 @@ export async function initPlayerAndChat(loadHlsModule: () => Promise<{ default:
|
||||
console.log("Linear Void Frontend initialized - HLS Player + Chat Sidebar ready");
|
||||
}
|
||||
|
||||
export function showReconnecting(message = "Reconnecting...") {
|
||||
const overlay = document.getElementById("reconnecting-overlay");
|
||||
const text = document.getElementById("reconnecting-text");
|
||||
if (overlay) {
|
||||
overlay.classList.remove("hidden");
|
||||
}
|
||||
if (text) {
|
||||
text.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
export function hideReconnecting() {
|
||||
const overlay = document.getElementById("reconnecting-overlay");
|
||||
if (overlay) {
|
||||
overlay.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChatPanel() {
|
||||
const shell = document.querySelector(".viewer-shell");
|
||||
const toggle = document.getElementById("chat-toggle");
|
||||
|
||||
Reference in new issue
Block a user