skip broken video on tintok
This commit is contained in:
@@ -146,6 +146,9 @@ App.feed = App.feed || {};
|
|||||||
video._hlsPlayer.destroy();
|
video._hlsPlayer.destroy();
|
||||||
video._hlsPlayer = null;
|
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.pause();
|
||||||
video.removeAttribute('src');
|
video.removeAttribute('src');
|
||||||
video.load();
|
video.load();
|
||||||
@@ -244,12 +247,18 @@ App.feed = App.feed || {};
|
|||||||
slide.classList.add('is-loaded');
|
slide.classList.add('is-loaded');
|
||||||
|
|
||||||
const resolved = App.videos.resolveStreamSource(videoData);
|
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 streamUrl = App.videos.buildStreamUrlFromSource(resolved);
|
||||||
const isHls = resolved.isLive ? true : /\.m3u8($|\?)/i.test(resolved.url);
|
const isHls = resolved.isLive ? true : /\.m3u8($|\?)/i.test(resolved.url);
|
||||||
|
|
||||||
video.muted = state.feedMuted;
|
video.muted = state.feedMuted;
|
||||||
video.preload = 'auto';
|
video.preload = 'auto';
|
||||||
|
video._tearingDown = false;
|
||||||
applyResume(video, videoData && videoData.id, resolved.isLive);
|
applyResume(video, videoData && videoData.id, resolved.isLive);
|
||||||
|
|
||||||
const startPlay = () => {
|
const startPlay = () => {
|
||||||
@@ -267,6 +276,8 @@ App.feed = App.feed || {};
|
|||||||
if (data && data.fatal && video._hlsPlayer === hls) {
|
if (data && data.fatal && video._hlsPlayer === hls) {
|
||||||
hls.destroy();
|
hls.destroy();
|
||||||
video._hlsPlayer = null;
|
video._hlsPlayer = null;
|
||||||
|
// A fatal HLS error means the stream won't play: drop it.
|
||||||
|
markSlideFailed(slide);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
startPlay();
|
startPlay();
|
||||||
@@ -337,6 +348,12 @@ App.feed = App.feed || {};
|
|||||||
const slideVideo = slide.querySelector('.feed-video');
|
const slideVideo = slide.querySelector('.feed-video');
|
||||||
bindTimeline(slide, slideVideo);
|
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`
|
// 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
|
// 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.
|
// active slide so a preloaded neighbour ending early can't hijack focus.
|
||||||
@@ -379,6 +396,79 @@ App.feed = App.feed || {};
|
|||||||
slidesByIndex.delete(index);
|
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
|
// Brings the rendered window in line with the active index: drops slides
|
||||||
// that fell outside [active - HISTORY_COUNT, active + RENDER_AHEAD], builds
|
// 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
|
// any missing ones inside it, and sizes the top spacer to stand in for the
|
||||||
|
|||||||
Reference in New Issue
Block a user