Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 110 additions & 17 deletions src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,10 @@ export default defineComponent({
contextMenuElements: ['ft_stats'],
enableTooltips: true,
seekBarColors: {
played: 'var(--primary-color)'
played: 'var(--primary-color)',
// Shaka's native chapter marker gradient renders blurry on
// Chromium, so FreeTube keeps drawing the visual markers itself.
chapters: 'transparent'
},
showAudioCodec: false,
showVideoCodec: false,
Expand Down Expand Up @@ -2586,6 +2589,10 @@ export default defineComponent({
const { start, end } = player.seekRange()
const duration = end - start

if (!Number.isFinite(duration) || duration <= 0) {
return
}

/**
* @type {{
* title: string,
Expand All @@ -2598,41 +2605,115 @@ export default defineComponent({
const chapters = props.chapters

addMarkers(
chapters.map(chapter => {
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(${(chapter.startSeconds / duration) * 100}% - 1px)`
markerDiv.style.left = `calc(${startFraction * 100}% - 1px)`

return markerDiv
})
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

// #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)
Expand Down Expand Up @@ -2955,6 +3036,22 @@ export default defineComponent({
await Promise.all(promises)
}

if (!isLive.value && props.chapters.length > 0) {
const chaptersVttUrl = createChaptersVttUrl()

if (chaptersVttUrl !== null) {
try {
await player.addChaptersTrack(chaptersVttUrl, 'und', 'text/vtt')
} catch (error) {
logShakaError(error, 'addChaptersTrack', props.videoId, props.chapters)
}
}
}

if (!isLive.value && props.chapters.length > 0) {
createChapterMarkers()
}

if (restoreCaptionIndex !== null) {
const index = restoreCaptionIndex
restoreCaptionIndex = null
Expand All @@ -2966,10 +3063,6 @@ export default defineComponent({
}
}

if (props.chapters.length > 0) {
createChapterMarkers()
}

if (startInFullscreen && process.env.IS_ELECTRON) {
startInFullscreen = false
window.ftElectron.requestFullscreen()
Expand Down
Loading