skip broken video on tintok

This commit is contained in:
Simon
2026-06-24 20:39:03 +00:00
parent 48a15759fc
commit e3eeaacc53

View File

@@ -146,6 +146,9 @@ App.feed = App.feed || {};
video._hlsPlayer.destroy();
video._hlsPlayer = null;
}
// Clearing the src below makes the element fire a spurious `error` event;
// flag the teardown so the failure handler ignores it (see markSlideFailed).
video._tearingDown = true;
video.pause();
video.removeAttribute('src');
video.load();
@@ -244,12 +247,18 @@ App.feed = App.feed || {};
slide.classList.add('is-loaded');
const resolved = App.videos.resolveStreamSource(videoData);
if (!resolved.url) return;
if (!resolved.url) {
// No playable source -- treat exactly like a load failure so the
// clip is dropped from the queue and the next one takes its place.
markSlideFailed(slide);
return;
}
const streamUrl = App.videos.buildStreamUrlFromSource(resolved);
const isHls = resolved.isLive ? true : /\.m3u8($|\?)/i.test(resolved.url);
video.muted = state.feedMuted;
video.preload = 'auto';
video._tearingDown = false;
applyResume(video, videoData && videoData.id, resolved.isLive);
const startPlay = () => {
@@ -267,6 +276,8 @@ App.feed = App.feed || {};
if (data && data.fatal && video._hlsPlayer === hls) {
hls.destroy();
video._hlsPlayer = null;
// A fatal HLS error means the stream won't play: drop it.
markSlideFailed(slide);
}
});
startPlay();
@@ -337,6 +348,12 @@ App.feed = App.feed || {};
const slideVideo = slide.querySelector('.feed-video');
bindTimeline(slide, slideVideo);
// A media error (bad/expired source, network failure, unsupported codec)
// means this clip can't play -- drop it from the queue. Errors fired by
// our own teardown (src cleared) carry the _tearingDown flag and are
// ignored inside markSlideFailed.
slideVideo.addEventListener('error', () => markSlideFailed(slide));
// On video end, either loop (handled by the `loop` flag, so `ended`
// never fires) or auto-scroll to the next clip. We only advance for the
// active slide so a preloaded neighbour ending early can't hijack focus.
@@ -379,6 +396,79 @@ App.feed = App.feed || {};
slidesByIndex.delete(index);
};
// Re-keys every rendered slide after `removedIndex` was spliced out of
// state.loadedVideos: indices past the hole shift down by one so
// slidesByIndex (and each slide's _index) stays aligned with the queue.
const reindexAfterRemoval = function(removedIndex) {
const entries = [];
slidesByIndex.forEach((slide, i) => entries.push([i, slide]));
slidesByIndex.clear();
entries.forEach(([i, slide]) => {
const ni = i > removedIndex ? i - 1 : i;
slide._index = ni;
slide.dataset.index = String(ni);
slidesByIndex.set(ni, slide);
});
};
// Drops a video that failed to load/resolve from the queue and pulls the
// next clip into its place. A failed *preload* neighbour leaves the active
// video playing untouched; a failed *active* clip is replaced in-place by
// the next one (the broken frame is removed and the next clip slides into
// the same scroll position, so playback advances without a visible jump).
const removeVideoFromQueue = function(videoId) {
const videos = state.loadedVideos || [];
const r = videos.findIndex((v) => String(v.id) === String(videoId));
if (r < 0) return;
const prevActive = state.feedActiveIndex;
removeSlide(r);
videos.splice(r, 1);
reindexAfterRemoval(r);
// Keep the grid's parallel index-based virtualization in sync with the
// now-shorter queue.
if (App.virtualGrid && typeof App.virtualGrid.relayout === 'function') {
App.virtualGrid.relayout();
}
if (videos.length === 0) {
App.feed.close();
return;
}
// The active slot only moves when the removed clip was the active one
// (r === prevActive) or, defensively, sat before it.
let newActive = prevActive;
if (r < prevActive) newActive -= 1;
newActive = clampIndex(newActive);
state.feedActiveIndex = -1; // force setActive to re-promote the slot
setActive(newActive);
if (r <= prevActive) {
// Active clip failed: re-anchor scroll onto the clip that slid into
// its slot so the snap container stays pinned to the new active.
const scroller = getScroller();
if (scroller) scroller.scrollTop = newActive * slideHeight();
}
};
// Flags a slide whose video failed and schedules its removal from the queue.
// Deferred to a macrotask so we never mutate slidesByIndex while setActive /
// syncWindow is mid-iteration over it. Teardown-induced errors (src cleared)
// are ignored via the video's _tearingDown flag, and we only act while the
// feed is open so late errors after close are harmless.
const markSlideFailed = function(slide) {
if (!slide || slide._failed || !state.feedOpen) return;
const video = slide.querySelector('.feed-video');
if (video && video._tearingDown) return;
const id = slideVideoId(slide);
if (id == null) return;
slide._failed = true;
setTimeout(() => removeVideoFromQueue(id), 0);
};
// Brings the rendered window in line with the active index: drops slides
// that fell outside [active - HISTORY_COUNT, active + RENDER_AHEAD], builds
// any missing ones inside it, and sizes the top spacer to stand in for the