Improve frame stall recovery
This commit is contained in:
102
public/app.js
102
public/app.js
@@ -74,6 +74,7 @@ const state = {
|
|||||||
lastFramePacketAt: 0,
|
lastFramePacketAt: 0,
|
||||||
lastFramePaintedAt: 0,
|
lastFramePaintedAt: 0,
|
||||||
lastPlaybackRestartAt: 0,
|
lastPlaybackRestartAt: 0,
|
||||||
|
lastRenderErrorAt: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
void loadEntryLists();
|
void loadEntryLists();
|
||||||
@@ -286,8 +287,18 @@ elements.audio.addEventListener('error', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
if (state.session && !elements.audio.paused && isFrameStreamStale(FRAME_STALL_RESET_MS)) {
|
const stallReason = getFrameStallReason();
|
||||||
restartPlaybackStreams();
|
|
||||||
|
if (state.session && !elements.audio.paused && stallReason) {
|
||||||
|
restartPlaybackStreams(`network_recovered_${stallReason}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
const stallReason = getFrameStallReason();
|
||||||
|
|
||||||
|
if (!document.hidden && state.session && !elements.audio.paused && stallReason) {
|
||||||
|
restartPlaybackStreams(`visible_${stallReason}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -402,26 +413,40 @@ function checkFrameWatchdog() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetAfterMs = state.lastFramePaintedAt > 0 ? FRAME_STALL_RESET_MS : FRAME_STARTUP_STALL_RESET_MS;
|
const stallReason = getFrameStallReason();
|
||||||
|
|
||||||
if (!isFrameStreamStale(resetAfterMs)) {
|
if (!stallReason) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
restartPlaybackStreams();
|
restartPlaybackStreams(stallReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFrameStreamStale(resetAfterMs) {
|
function getFrameStallReason() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastFrameAt = state.lastFramePacketAt || state.lastPlaybackRestartAt || now;
|
|
||||||
return now - lastFrameAt >= resetAfterMs && now - state.lastPlaybackRestartAt >= PLAYBACK_RESTART_COOLDOWN_MS;
|
if (!state.session || state.isSeeking || elements.audio.paused || elements.audio.ended || elements.audio.readyState === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - state.lastPlaybackRestartAt < PLAYBACK_RESTART_COOLDOWN_MS) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.lastFramePaintedAt < state.lastPlaybackRestartAt) {
|
||||||
|
return now - state.lastPlaybackRestartAt >= FRAME_STARTUP_STALL_RESET_MS ? 'frame_startup_stalled' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return now - state.lastFramePaintedAt >= FRAME_STALL_RESET_MS ? 'frame_paint_stalled' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartPlaybackStreams() {
|
function restartPlaybackStreams(reason = 'frame_stalled') {
|
||||||
if (!state.session) {
|
if (!state.session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn(`Restarting playback streams: ${reason}`);
|
||||||
|
|
||||||
if (state.seekable && Number.isFinite(state.duration) && state.duration > 0) {
|
if (state.seekable && Number.isFinite(state.duration) && state.duration > 0) {
|
||||||
state.lastPlaybackRestartAt = Date.now();
|
state.lastPlaybackRestartAt = Date.now();
|
||||||
void seekTo(getVisiblePlaybackTime());
|
void seekTo(getVisiblePlaybackTime());
|
||||||
@@ -462,7 +487,7 @@ function handleFramePacket(packet, streamGeneration) {
|
|||||||
|
|
||||||
const timestamp = new DataView(packet, 0, 8).getFloat64(0, true);
|
const timestamp = new DataView(packet, 0, 8).getFloat64(0, true);
|
||||||
|
|
||||||
if (streamGeneration !== state.streamGeneration || isLateFrame(timestamp)) {
|
if (streamGeneration !== state.streamGeneration) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,9 +527,18 @@ function startRenderLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
drawReadyFrames();
|
try {
|
||||||
syncPlayhead();
|
drawReadyFrames();
|
||||||
state.raf = requestAnimationFrame(render);
|
syncPlayhead();
|
||||||
|
} catch (error) {
|
||||||
|
logRenderError(error);
|
||||||
|
} finally {
|
||||||
|
if (state.session) {
|
||||||
|
state.raf = requestAnimationFrame(render);
|
||||||
|
} else {
|
||||||
|
state.raf = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
state.raf = requestAnimationFrame(render);
|
state.raf = requestAnimationFrame(render);
|
||||||
@@ -515,7 +549,7 @@ function drawReadyFrames() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dropLateDecodedFrames();
|
dropLateDecodedFrames({ keepNewest: true });
|
||||||
|
|
||||||
const frameLeadSeconds = 1 / Math.max(1, state.session.options.fps);
|
const frameLeadSeconds = 1 / Math.max(1, state.session.options.fps);
|
||||||
const targetTime = elements.audio.currentTime + frameLeadSeconds;
|
const targetTime = elements.audio.currentTime + frameLeadSeconds;
|
||||||
@@ -567,7 +601,7 @@ async function pumpFrameDecodeQueue() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while (state.pendingFrames.length > 0) {
|
while (state.pendingFrames.length > 0) {
|
||||||
dropLatePendingFrames();
|
dropLatePendingFrames({ keepNewest: true });
|
||||||
|
|
||||||
const frame = state.pendingFrames.shift();
|
const frame = state.pendingFrames.shift();
|
||||||
|
|
||||||
@@ -575,7 +609,11 @@ async function pumpFrameDecodeQueue() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame.streamGeneration !== state.streamGeneration || isLateFrame(frame.timestamp)) {
|
if (frame.streamGeneration !== state.streamGeneration) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLateFrame(frame.timestamp) && state.pendingFrames.length > 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,7 +625,12 @@ async function pumpFrameDecodeQueue() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame.streamGeneration !== state.streamGeneration || isLateFrame(frame.timestamp)) {
|
if (frame.streamGeneration !== state.streamGeneration) {
|
||||||
|
releaseImage(bitmap);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLateFrame(frame.timestamp) && (state.pendingFrames.length > 0 || state.frames.length > 0)) {
|
||||||
releaseImage(bitmap);
|
releaseImage(bitmap);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -608,7 +651,7 @@ async function pumpFrameDecodeQueue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function trimPendingFrameQueue() {
|
function trimPendingFrameQueue() {
|
||||||
dropLatePendingFrames();
|
dropLatePendingFrames({ keepNewest: true });
|
||||||
|
|
||||||
const maxQueuedFrames = getFrameQueueLimit(MAX_PENDING_FRAME_QUEUE_SECONDS, MIN_PENDING_FRAME_QUEUE);
|
const maxQueuedFrames = getFrameQueueLimit(MAX_PENDING_FRAME_QUEUE_SECONDS, MIN_PENDING_FRAME_QUEUE);
|
||||||
const overflow = state.pendingFrames.length - maxQueuedFrames;
|
const overflow = state.pendingFrames.length - maxQueuedFrames;
|
||||||
@@ -619,7 +662,7 @@ function trimPendingFrameQueue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function trimFrameQueue() {
|
function trimFrameQueue() {
|
||||||
dropLateDecodedFrames();
|
dropLateDecodedFrames({ keepNewest: true });
|
||||||
|
|
||||||
const maxQueuedFrames = getFrameQueueLimit(MAX_DECODED_FRAME_QUEUE_SECONDS, MIN_DECODED_FRAME_QUEUE);
|
const maxQueuedFrames = getFrameQueueLimit(MAX_DECODED_FRAME_QUEUE_SECONDS, MIN_DECODED_FRAME_QUEUE);
|
||||||
const overflow = state.frames.length - maxQueuedFrames;
|
const overflow = state.frames.length - maxQueuedFrames;
|
||||||
@@ -635,10 +678,11 @@ function trimFrameQueue() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropLatePendingFrames() {
|
function dropLatePendingFrames({ keepNewest = false } = {}) {
|
||||||
let removeCount = 0;
|
let removeCount = 0;
|
||||||
|
const removableFrames = keepNewest ? Math.max(0, state.pendingFrames.length - 1) : state.pendingFrames.length;
|
||||||
|
|
||||||
while (removeCount < state.pendingFrames.length && isLateFrame(state.pendingFrames[removeCount].timestamp)) {
|
while (removeCount < removableFrames && isLateFrame(state.pendingFrames[removeCount].timestamp)) {
|
||||||
removeCount += 1;
|
removeCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,10 +691,11 @@ function dropLatePendingFrames() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropLateDecodedFrames() {
|
function dropLateDecodedFrames({ keepNewest = false } = {}) {
|
||||||
let removeCount = 0;
|
let removeCount = 0;
|
||||||
|
const removableFrames = keepNewest ? Math.max(0, state.frames.length - 1) : state.frames.length;
|
||||||
|
|
||||||
while (removeCount < state.frames.length && isLateFrame(state.frames[removeCount].timestamp)) {
|
while (removeCount < removableFrames && isLateFrame(state.frames[removeCount].timestamp)) {
|
||||||
removeCount += 1;
|
removeCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,6 +838,17 @@ function releaseImage(image) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logRenderError(error) {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now - state.lastRenderErrorAt < 5000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.lastRenderErrorAt = now;
|
||||||
|
console.warn('Frame render loop error', error);
|
||||||
|
}
|
||||||
|
|
||||||
async function seekTo(value) {
|
async function seekTo(value) {
|
||||||
if (!state.session || !state.seekable) {
|
if (!state.session || !state.seekable) {
|
||||||
state.isSeeking = false;
|
state.isSeeking = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user