skip broken video on tintok
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user