From 0aa3e3dff32872194a47c2018e85d7d1439d1fcb Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Sat, 2 May 2026 16:55:24 +0530 Subject: [PATCH 1/3] Use Shaka chapter tracks for seek preview --- .../ft-shaka-video-player.css | 8 +- .../ft-shaka-video-player.js | 106 ++++++++++++------ 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css index e1f3d79055cfd..04586ed83739e 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css @@ -160,13 +160,7 @@ background-color: var(--primary-color); } -:deep(.chapterMarker) { - width: 2px; - background-color: #000; -} - -:deep(.sponsorBlockMarker), -:deep(.chapterMarker) { +:deep(.sponsorBlockMarker) { position: absolute; opacity: 0.6; height: 100%; diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 02ee2146bf928..2d70142724df4 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -903,7 +903,8 @@ export default defineComponent({ contextMenuElements: ['ft_stats'], enableTooltips: true, seekBarColors: { - played: 'var(--primary-color)' + played: 'var(--primary-color)', + chapters: 'rgba(0, 0, 0, 0.6)' }, showAudioCodec: false, showVideoCodec: false, @@ -1021,10 +1022,6 @@ export default defineComponent({ fullscreenTitleOverlay.dir = 'auto' controlsContainer.appendChild(fullscreenTitleOverlay) - if (hasLoaded.value && props.chapters.length > 0) { - createChapterMarkers() - } - if (useSponsorBlock.value && sponsorBlockSegments.length > 0) { let duration if (hasLoaded.value) { @@ -2582,34 +2579,6 @@ export default defineComponent({ ) } - function createChapterMarkers() { - const { start, end } = player.seekRange() - const duration = end - start - - /** - * @type {{ - * title: string, - * timestamp: string, - * startSeconds: number, - * endSeconds: number, - * thumbnail?: string - * }[]} - */ - const chapters = props.chapters - - addMarkers( - chapters.map(chapter => { - const markerDiv = document.createElement('div') - - markerDiv.title = chapter.title - markerDiv.className = 'chapterMarker' - markerDiv.style.left = `calc(${(chapter.startSeconds / duration) * 100}% - 1px)` - - return markerDiv - }) - ) - } - /** * @param {HTMLDivElement[]} markers */ @@ -2633,6 +2602,64 @@ export default defineComponent({ // #endregion seek bar markers + // #region chapters + + /** + * @param {number} seconds + * @returns {string} + */ + function formatSecondsForVtt(seconds) { + const milliseconds = Math.round(seconds * 1000) + const hours = Math.floor(milliseconds / 3_600_000) + const minutes = Math.floor((milliseconds % 3_600_000) / 60_000) + const wholeSeconds = Math.floor((milliseconds % 60_000) / 1000) + const millisecondsRemainder = milliseconds % 1000 + + return [ + hours.toString().padStart(2, '0'), + minutes.toString().padStart(2, '0'), + wholeSeconds.toString().padStart(2, '0') + ].join(':') + `.${millisecondsRemainder.toString().padStart(3, '0')}` + } + + /** + * @returns {string|null} + */ + function createChaptersVttUrl() { + let vtt = 'WEBVTT\n\n' + let hasValidChapter = false + + for (const chapter of props.chapters) { + if ( + !Number.isFinite(chapter.startSeconds) || + !Number.isFinite(chapter.endSeconds) || + chapter.endSeconds <= chapter.startSeconds + ) { + continue + } + + const title = String(chapter.title ?? '') + .replaceAll(/\s+/g, ' ') + .trim() + + if (title.length === 0) { + continue + } + + hasValidChapter = true + + vtt += [ + `${formatSecondsForVtt(chapter.startSeconds)} --> ${formatSecondsForVtt(chapter.endSeconds)}`, + title, + '' + ].join('\n') + '\n' + } + + return hasValidChapter ? `data:text/vtt;charset=utf-8,${encodeURIComponent(vtt)}` : null + } + + // #endregion chapters + // #region offline message const isOffline = ref(!navigator.onLine) @@ -2955,6 +2982,15 @@ export default defineComponent({ await Promise.all(promises) } + if (!isLive.value && props.chapters.length > 0) { + const chaptersVttUrl = createChaptersVttUrl() + + if (chaptersVttUrl !== null) { + await player.addChaptersTrack(chaptersVttUrl, 'und', 'text/vtt') + .catch(error => logShakaError(error, 'addChaptersTrack', props.videoId, props.chapters)) + } + } + if (restoreCaptionIndex !== null) { const index = restoreCaptionIndex restoreCaptionIndex = null @@ -2966,10 +3002,6 @@ export default defineComponent({ } } - if (props.chapters.length > 0) { - createChapterMarkers() - } - if (startInFullscreen && process.env.IS_ELECTRON) { startInFullscreen = false window.ftElectron.requestFullscreen() From 491263a38d28f67d3241479b020689faa006c522 Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Sat, 2 May 2026 18:24:33 +0530 Subject: [PATCH 2/3] Keep chapter markers crisp with Shaka chapters --- .../ft-shaka-video-player.css | 8 +- .../ft-shaka-video-player.js | 76 ++++++++++++++++--- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css index 04586ed83739e..e1f3d79055cfd 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css @@ -160,7 +160,13 @@ background-color: var(--primary-color); } -:deep(.sponsorBlockMarker) { +:deep(.chapterMarker) { + width: 2px; + background-color: #000; +} + +:deep(.sponsorBlockMarker), +:deep(.chapterMarker) { position: absolute; opacity: 0.6; height: 100%; diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 2d70142724df4..a7d0fae511724 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -904,7 +904,9 @@ export default defineComponent({ enableTooltips: true, seekBarColors: { played: 'var(--primary-color)', - chapters: 'rgba(0, 0, 0, 0.6)' + // Shaka's native chapter marker gradient renders blurry on + // Chromium, so FreeTube keeps drawing the visual markers itself. + chapters: 'transparent' }, showAudioCodec: false, showVideoCodec: false, @@ -1022,6 +1024,10 @@ export default defineComponent({ fullscreenTitleOverlay.dir = 'auto' controlsContainer.appendChild(fullscreenTitleOverlay) + if (hasLoaded.value && props.chapters.length > 0) { + createChapterMarkers() + } + if (useSponsorBlock.value && sponsorBlockSegments.length > 0) { let duration if (hasLoaded.value) { @@ -2579,25 +2585,73 @@ export default defineComponent({ ) } + function createChapterMarkers() { + const { start, end } = player.seekRange() + const duration = end - start + + if (!Number.isFinite(duration) || duration <= 0) { + return + } + + /** + * @type {{ + * title: string, + * timestamp: string, + * startSeconds: number, + * endSeconds: number, + * thumbnail?: string + * }[]} + */ + const chapters = props.chapters + + addMarkers( + chapters.flatMap(chapter => { + if (!Number.isFinite(chapter.startSeconds)) { + return [] + } + + const startFraction = (chapter.startSeconds - start) / duration + + if (startFraction < 0 || startFraction > 1) { + return [] + } + + const markerDiv = document.createElement('div') + + markerDiv.title = chapter.title + markerDiv.className = 'chapterMarker' + markerDiv.style.left = `calc(${startFraction * 100}% - 1px)` + + return [markerDiv] + }), + 'chapterMarker' + ) + } + /** * @param {HTMLDivElement[]} markers + * @param {string} [markerClassName] */ - function addMarkers(markers) { + function addMarkers(markers, markerClassName) { const seekBarContainer = container.value.querySelector('.shaka-seek-bar-container') - if (seekBarContainer.firstElementChild?.classList.contains('markerContainer')) { - /** @type {HTMLDivElement} */ - const markerBar = seekBarContainer.firstElementChild + /** @type {HTMLDivElement} */ + let markerBar - markers.forEach(marker => markerBar.appendChild(marker)) + if (seekBarContainer.firstElementChild?.classList.contains('markerContainer')) { + markerBar = seekBarContainer.firstElementChild } else { - const markerBar = document.createElement('div') + markerBar = document.createElement('div') markerBar.className = 'markerContainer' - markers.forEach(marker => markerBar.appendChild(marker)) - seekBarContainer.insertBefore(markerBar, seekBarContainer.firstElementChild) } + + if (markerClassName) { + markerBar.querySelectorAll(`.${markerClassName}`).forEach(marker => marker.remove()) + } + + markers.forEach(marker => markerBar.appendChild(marker)) } // #endregion seek bar markers @@ -2991,6 +3045,10 @@ export default defineComponent({ } } + if (props.chapters.length > 0) { + createChapterMarkers() + } + if (restoreCaptionIndex !== null) { const index = restoreCaptionIndex restoreCaptionIndex = null From b11852abb468cbeae55cac1de0f617299b23cfec Mon Sep 17 00:00:00 2001 From: Aditya Mishra Date: Sat, 2 May 2026 21:09:25 +0530 Subject: [PATCH 3/3] Improve chapter error handling and live stream guard Replace await .catch() with try/catch for addChaptersTrack for consistency with other error handling patterns in the file. Add !isLive.value guard to the visual chapter markers call in handleLoaded() for symmetry with the VTT chapter track guard. --- .../ft-shaka-video-player/ft-shaka-video-player.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index a7d0fae511724..cf9927fa8acdb 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -3040,12 +3040,15 @@ export default defineComponent({ const chaptersVttUrl = createChaptersVttUrl() if (chaptersVttUrl !== null) { - await player.addChaptersTrack(chaptersVttUrl, 'und', 'text/vtt') - .catch(error => logShakaError(error, 'addChaptersTrack', props.videoId, props.chapters)) + try { + await player.addChaptersTrack(chaptersVttUrl, 'und', 'text/vtt') + } catch (error) { + logShakaError(error, 'addChaptersTrack', props.videoId, props.chapters) + } } } - if (props.chapters.length > 0) { + if (!isLive.value && props.chapters.length > 0) { createChapterMarkers() }