diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js
index 664bc63e446fc..2d98325def716 100644
--- a/_scripts/webpack.web.config.js
+++ b/_scripts/webpack.web.config.js
@@ -31,7 +31,8 @@ const config = {
filename: '[name].js',
},
externals: {
- 'youtubei.js': '{}'
+ 'youtubei.js': '{}',
+ googlevideo: '{}'
},
module: {
rules: [
@@ -131,7 +132,7 @@ const config = {
'process.env.SWIPER_VERSION': `'${swiperVersion}'`
}),
new webpack.ProvidePlugin({
- process: 'process/browser'
+ process: 'process/browser.js'
}),
new HtmlWebpackPlugin({
excludeChunks: ['processTaskWorker'],
diff --git a/package.json b/package.json
index 7911307430d09..ed76d99b9eb26 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"autolinker": "^4.1.5",
"bgutils-js": "^3.2.0",
"electron-context-menu": "^4.1.1",
+ "googlevideo": "^4.0.4",
"marked": "^16.3.0",
"portal-vue": "^2.1.7",
"process": "^0.11.10",
diff --git a/src/renderer/components/ExperimentalSettings/ExperimentalSettings.vue b/src/renderer/components/ExperimentalSettings/ExperimentalSettings.vue
index 1e6a2bfd73db2..82a98945bf139 100644
--- a/src/renderer/components/ExperimentalSettings/ExperimentalSettings.vue
+++ b/src/renderer/components/ExperimentalSettings/ExperimentalSettings.vue
@@ -16,6 +16,16 @@
@change="handleRestartPrompt"
/>
+
+
+
diff --git a/src/renderer/components/FtToast/FtToast.vue b/src/renderer/components/FtToast/FtToast.vue
index dd207232d2724..070a915a1810e 100644
--- a/src/renderer/components/FtToast/FtToast.vue
+++ b/src/renderer/components/FtToast/FtToast.vue
@@ -30,6 +30,7 @@ let idCounter = 0
* @property {Function | null} action
* @property {boolean} isOpen
* @property {NodeJS.Timeout | number} timeout
+ * @property {NodeJS.Timeout | number} interval
* @property {number} id
*/
@@ -37,7 +38,7 @@ let idCounter = 0
const toasts = shallowReactive([])
/**
- * @param {CustomEvent<{ message: string, time: number | null, action: Function | null, abortSignal: AbortSignal | null }>} event
+ * @param {CustomEvent<{ message: string | (({elapsedMs: number, remainingMs: number}) => string), time: number | null, action: Function | null, abortSignal: AbortSignal | null }>} event
*/
function open({ detail: { message, time, action, abortSignal } }) {
/** @type {Toast} */
@@ -46,10 +47,24 @@ function open({ detail: { message, time, action, abortSignal } }) {
action,
isOpen: false,
timeout: 0,
+ interval: 0,
id: idCounter++
}
+ time = time || 3000
+ let elapsed = 0
+ const updateDelay = 1000
+
+ if (typeof message === 'function') {
+ toast.message = message({ elapsedMs: elapsed, remainingMs: time - elapsed })
+ toast.interval = setInterval(() => {
+ elapsed += updateDelay
+ // Skip last update
+ if (elapsed >= time) { return }
+ toast.message = message({ elapsedMs: elapsed, remainingMs: time - elapsed })
+ }, updateDelay)
+ }
- toast.timeout = setTimeout(close, time || 3000, toast)
+ toast.timeout = setTimeout(close, time, toast)
if (abortSignal != null) {
abortSignal.addEventListener('abort', () => {
close(toast)
@@ -92,6 +107,7 @@ function remove(toast) {
if (index !== -1) {
toasts.splice(index, 1)
clearTimeout(toast.timeout)
+ clearInterval(toast.interval)
}
}
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 78f076b732ba4..b34e6c36b8632 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
@@ -1,4 +1,4 @@
-import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue'
+import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
import shaka from 'shaka-player'
import { useI18n } from '../../composables/use-i18n-polyfill'
@@ -17,7 +17,6 @@ import {
getSponsorBlockSegments,
logShakaError,
repairInvidiousManifest,
- sortCaptions,
translateSponsorBlockCategory
} from '../../helpers/player/utils'
import {
@@ -25,8 +24,11 @@ import {
showToast,
writeFileWithPicker,
throttle,
+ debounce,
removeFromArrayIfExists,
} from '../../helpers/utils'
+import { MANIFEST_TYPE_SABR } from '../../helpers/player/SabrManifestParser'
+import { setupSabrScheme } from '../../helpers/player/SabrSchemePlugin'
/** @typedef {import('../../helpers/sponsorblock').SponsorBlockCategory} SponsorBlockCategory */
@@ -38,6 +40,7 @@ const USE_OVERFLOW_MENU_WIDTH_THRESHOLD = 634
const RequestType = shaka.net.NetworkingEngine.RequestType
const AdvancedRequestType = shaka.net.NetworkingEngine.AdvancedRequestType
const TrackLabelFormat = shaka.ui.Overlay.TrackLabelFormat
+const { Severity: ErrorSeverity, Category: ErrorCategory, Code: ErrorCode } = shaka.util.Error
/*
Mapping of Shaka localization keys for control labels to FreeTube shortcuts.
@@ -74,6 +77,10 @@ export default defineComponent({
type: String,
required: true
},
+ sabrData: {
+ type: Object,
+ default: null
+ },
legacyFormats: {
type: Array,
default: () => ([])
@@ -146,6 +153,10 @@ export default defineComponent({
type: Number,
default: 1
},
+ delayLoadUntilUnix: {
+ type: Number,
+ default: 0
+ },
},
emits: [
'error',
@@ -157,6 +168,7 @@ export default defineComponent({
'playback-rate-updated',
'skip-to-next',
'skip-to-prev',
+ 'player-reload-requested',
],
setup: function (props, { emit, expose }) {
const { locale, t } = useI18n()
@@ -193,31 +205,10 @@ export default defineComponent({
let startInFullscreen = props.startInFullscreen
let startInPip = props.startInPip
- /**
- * @type {{
- * url: string,
- * label: string,
- * language: string,
- * mimeType: string,
- * isAutotranslated?: boolean
- * }[]}
- */
- let sortedCaptions
-
- // we don't need to sort if we only have one caption or don't have any
- if (props.captions.length > 1) {
- // theoretically we would resort when the language changes, but we can't remove captions that we already added to the player
- sortedCaptions = sortCaptions(props.captions)
- } else if (props.captions.length === 1) {
- sortedCaptions = props.captions
- } else {
- sortedCaptions = []
- }
-
/** @type {number|null} */
let restoreCaptionIndex = null
- if (store.getters.getEnableSubtitlesByDefault && sortedCaptions.length > 0) {
+ if (store.getters.getEnableSubtitlesByDefault && props.captions.length > 0) {
restoreCaptionIndex = 0
}
@@ -228,10 +219,6 @@ export default defineComponent({
height: 0,
frameRate: 0
},
- playerDimensions: {
- width: 0,
- height: 0
- },
bitrate: '0',
volume: '100',
bandwidth: '0',
@@ -248,6 +235,11 @@ export default defineComponent({
}
})
+ const playerDimensions = computed(() => ({
+ width: playerWidth.value,
+ height: playerHeight.value
+ }))
+
// #region settings
/** @type {import('vue').ComputedRef} */
@@ -802,7 +794,7 @@ export default defineComponent({
trackLabelFormat: hasMultipleAudioTracks.value ? TrackLabelFormat.LABEL : TrackLabelFormat.LANGUAGE_ROLE,
// Only set it to label if we added the captions ourselves,
// some live streams come with subtitles in the DASH manifest, but without labels
- textTrackLabelFormat: sortedCaptions.length > 0 ? TrackLabelFormat.LABEL : TrackLabelFormat.LANGUAGE,
+ textTrackLabelFormat: props.captions.length > 0 ? TrackLabelFormat.LABEL : TrackLabelFormat.LANGUAGE,
displayInVrMode: useVrMode.value
}
@@ -1049,7 +1041,7 @@ export default defineComponent({
})
/** @type {ResizeObserver|null} */
- let resizeObserver = null
+ let containerResizeObserver = null
/** @type {ResizeObserverCallback} */
function resized(entries) {
@@ -1205,8 +1197,88 @@ export default defineComponent({
}
}
+ const videoElementWidth = ref(0)
+ const videoElementHeight = ref(0)
+
+ /** @type {ResizeObserver} */
+ const videoResizeObserver = new ResizeObserver(() => {
+ if (video.value) {
+ const devicePixelRatio = window.devicePixelRatio > 1 ? window.devicePixelRatio : 1
+ const video_ = video.value
+
+ videoElementWidth.value = video_.clientWidth * devicePixelRatio
+ videoElementHeight.value = video_.clientHeight * devicePixelRatio
+ }
+ })
+
+ /** @type {PictureInPictureWindow | null} */
+ let pipWindow = null
+ const pipWindowWidth = ref(null)
+ const pipWindowHeight = ref(null)
+
+ /**
+ * @param {PictureInPictureEvent} event
+ */
+ function handleEnterPictureInPicture(event) {
+ pipWindow = event.pictureInPictureWindow
+ handlePictureInPictureResize()
+ pipWindow.addEventListener('resize', handlePictureInPictureResize)
+ }
+
+ function handleLeavePictureInPicture() {
+ pipWindow.removeEventListener('resize', handlePictureInPictureResize)
+
+ pipWindow = null
+ pipWindowWidth.value = null
+ pipWindowHeight.value = null
+ }
+
+ function handlePictureInPictureResize() {
+ const devicePixelRatio = window.devicePixelRatio > 1 ? window.devicePixelRatio : 1
+
+ pipWindowWidth.value = pipWindow.width * devicePixelRatio
+ pipWindowHeight.value = pipWindow.height * devicePixelRatio
+ }
+
+ const playerWidth = computed(() => Math.round(pipWindowWidth.value ?? videoElementWidth.value))
+ const playerHeight = computed(() => Math.round(pipWindowHeight.value ?? videoElementHeight.value))
+
// #endregion video event handlers
+ // #region SABR
+
+ /** @type {shaka.extern.Manifest | undefined} */
+ let sabrManifest
+
+ /** @type {import('../../helpers/player/SabrSchemePlugin').SabrStream | undefined} */
+ let sabrStream
+ /** @type {AbortController | undefined} */
+ let sabrAbortController
+
+ if (process.env.SUPPORTS_LOCAL_API && props.sabrData) {
+ sabrStream = /** @__NOINLINE__ */ setupSabrScheme(props.sabrData, () => player, () => sabrManifest, playerWidth, playerHeight)
+ sabrAbortController = new AbortController()
+ // Since there can be 2 requests at the same time (video + audio), we debounce the listener to only show the message once
+ sabrStream.onBackoffRequested(debounce(({ backoffMs }) => {
+ showToast(
+ ({ remainingMs }) => {
+ // `+value` converts string back to float
+ return `Remaining SABR backoff time: ${+(remainingMs / 1000).toFixed(1)}s`
+ },
+ // So that we don't see last countdown text like 0/N
+ backoffMs,
+ null,
+ sabrAbortController.signal,
+ )
+ }, 1000))
+ sabrStream.onReloadOnce(() => {
+ sabrAbortController.abort()
+ emit('player-reload-requested')
+ })
+ }
+
+ // #endregion SABR
+
// #region request/response filters
/** @type {shaka.extern.RequestFilter} */
@@ -1216,7 +1288,7 @@ export default defineComponent({
// only when we aren't proxying through Invidious,
// it doesn't like the range param and makes get requests to youtube anyway
- if (url.hostname.endsWith('.googlevideo.com') && url.pathname === '/videoplayback') {
+ if (url.protocol !== 'sabr:' && url.hostname.endsWith('.googlevideo.com') && url.pathname === '/videoplayback') {
request.method = 'POST'
request.body = new Uint8Array([0x78, 0]) // protobuf: { 15: 0 } (no idea what it means but this is what YouTube uses)
@@ -1230,13 +1302,15 @@ export default defineComponent({
}
}
- /**
- * Handles Application Level Redirects
- * Based on the example in the YouTube.js repository
- * @type {shaka.extern.ResponseFilter}
- */
+ /** @type {shaka.extern.ResponseFilter} */
async function responseFilter(type, response, context) {
if (type === RequestType.SEGMENT) {
+ const url = new URL(response.uri)
+
+ if (url.protocol === 'sabr:') {
+ return
+ }
+
if (response.data && response.data.byteLength > 4 &&
new DataView(response.data).getUint32(0) === HTTP_IN_HEX) {
// Interpret the response data as a URL string.
@@ -1256,8 +1330,6 @@ export default defineComponent({
response.headers = redirectResponse.headers
response.uri = redirectResponse.uri
} else {
- const url = new URL(response.uri)
-
// Fix positioning for auto-generated subtitles
if (url.hostname.endsWith('.youtube.com') && url.pathname === '/api/timedtext' &&
url.searchParams.get('caps') === 'asr' && url.searchParams.get('kind') === 'asr' && url.searchParams.get('fmt') === 'vtt') {
@@ -1436,12 +1508,6 @@ export default defineComponent({
updateLegacyQualityStats(activeLegacyFormat.value)
}
- const playerDimensions = video_.getBoundingClientRect()
- stats.playerDimensions = {
- width: Math.floor(playerDimensions.width),
- height: Math.floor(playerDimensions.height)
- }
-
if (!hasLoaded.value) {
player.addEventListener('loaded', () => {
if (showStats.value) {
@@ -1497,14 +1563,13 @@ export default defineComponent({
stats.resolution.width = newTrack.width
stats.resolution.height = newTrack.height
} else {
- // for videos with multiple audio tracks, youtube.js appends the track id to the itag, to make it unique
- stats.codecs.audioItag = newTrack.originalAudioId.split('-')[0]
+ stats.codecs.audioItag = newTrack.originalAudioId.split('-', 1)[0]
stats.codecs.audioCodec = newTrack.audioCodec
if (props.format === 'dash') {
stats.resolution.frameRate = newTrack.frameRate
- stats.codecs.videoItag = newTrack.originalVideoId
+ stats.codecs.videoItag = newTrack.originalVideoId.split('-', 1)[0]
stats.codecs.videoCodec = newTrack.videoCodec
stats.resolution.width = newTrack.width
@@ -1537,12 +1602,6 @@ export default defineComponent({
}
function updateStats() {
- const playerDimensions = video.value.getBoundingClientRect()
- stats.playerDimensions = {
- width: Math.floor(playerDimensions.width),
- height: Math.floor(playerDimensions.height)
- }
-
const playerStats = player.getStats()
if (props.format !== 'audio') {
@@ -2375,10 +2434,27 @@ export default defineComponent({
function handleError(error, context, details) {
// These two errors are just wrappers around another error, so use the original error instead
// As they can be nested (e.g. multiple googlevideo redirects because the Invidious server was far away from the user) we should pick the inner most one
- while (error.code === shaka.util.Error.Code.REQUEST_FILTER_ERROR || error.code === shaka.util.Error.Code.RESPONSE_FILTER_ERROR) {
+ while (error.code === ErrorCode.REQUEST_FILTER_ERROR || error.code === ErrorCode.RESPONSE_FILTER_ERROR) {
error = error.data[0]
}
+ // Allow shaka-player to retry on potentially recoverable network errors
+ if (error.severity === ErrorSeverity.RECOVERABLE && error.category === ErrorCategory.NETWORK) {
+ /** @type {keyof ErrorCategory} */
+ const categoryText = Object.keys(ErrorCategory).find((/** @type {keyof ErrorCategory} */ key) => ErrorCategory[key] === error.category)
+
+ /** @type {keyof ErrorCode} */
+ const codeText = Object.keys(ErrorCode).find((/** @type {keyof ErrorCode} */ key) => ErrorCode[key] === error.code)
+
+ console.warn(
+ 'Recoverable network error retrying...\n' +
+ `Category: ${categoryText} (${error.category})\n` +
+ `Code: ${codeText} (${error.code})\n` +
+ 'Data', error.data
+ )
+ return
+ }
+
logShakaError(error, context, props.videoId, details)
// text related errors aren't serious (captions and seek bar thumbnails), so we should just log them
@@ -2502,6 +2578,7 @@ export default defineComponent({
// #endregion offline message
// #region setup
+ const initLoadWaitTimeToastAC = new AbortController()
onMounted(async () => {
watch(() => props.currentPlaybackRate,
@@ -2572,6 +2649,8 @@ export default defineComponent({
return
}
+ videoResizeObserver.observe(videoElement)
+
registerScreenshotButton()
registerAudioTrackSelection()
registerAutoplayToggle()
@@ -2586,8 +2665,8 @@ export default defineComponent({
} else {
onlyUseOverFlowMenu.value = container.value.getBoundingClientRect().width <= USE_OVERFLOW_MENU_WIDTH_THRESHOLD
- resizeObserver = new ResizeObserver(resized)
- resizeObserver.observe(container.value)
+ containerResizeObserver = new ResizeObserver(resized)
+ containerResizeObserver.observe(container.value)
}
controls.addEventListener('uiupdated', addUICustomizations)
@@ -2630,13 +2709,52 @@ export default defineComponent({
container.value.classList.add('no-cursor')
await performFirstLoad()
+ // Whatever runs after `performFirstLoad` might be after switching to another page due to SABR backoff
- player.addEventListener('ratechange', () => {
+ player?.addEventListener('ratechange', () => {
emit('playback-rate-updated', player.getPlaybackRate())
})
})
+ onUnmounted(() => {
+ initLoadWaitTimeToastAC.abort()
+ })
async function performFirstLoad() {
+ if (process.env.SUPPORTS_LOCAL_API && sabrStream) {
+ // Longer timeout for receiving larger responses
+ player.configure({
+ streaming: {
+ retryParameters: {
+ timeout: 30 * 1000 * 2,
+ }
+ }
+ })
+ } else {
+ // Reset to default value
+ player.configure({
+ streaming: {
+ retryParameters: {
+ timeout: 30 * 1000,
+ }
+ }
+ })
+ }
+
+ const delayLoadUntilUnix = props.delayLoadUntilUnix
+ const initialLoadDelayMs = delayLoadUntilUnix - Date.now()
+ if (initialLoadDelayMs > 0) {
+ showToast(
+ ({ remainingMs }) => {
+ return `Remaining preroll-ad time time: ${(remainingMs / 1000).toFixed(1)}s`
+ },
+ // So that we don't see last countdown text like 0/N
+ initialLoadDelayMs,
+ null,
+ initLoadWaitTimeToastAC.signal,
+ )
+ await new Promise((resolve) => setTimeout(resolve, initialLoadDelayMs))
+ }
+
if (props.format === 'dash' || props.format === 'audio') {
try {
await player.load(props.manifestSrc, props.startTime, props.manifestMimeType)
@@ -2679,40 +2797,59 @@ export default defineComponent({
// getAudioTracks() returns an empty array when no variant is active, so we can't do this in the `streaming` event
hasMultipleAudioTracks.value = deduplicateAudioTracks(player.getAudioTracks()).size > 1
- const promises = []
-
- for (const caption of sortedCaptions) {
- if (props.format === 'legacy') {
- const url = new URL(caption.url)
-
- if (url.hostname.endsWith('.youtube.com') && url.pathname === '/api/timedtext' &&
- url.searchParams.get('caps') === 'asr' && url.searchParams.get('kind') === 'asr' && url.searchParams.get('fmt') === 'vtt') {
- promises.push((async () => {
- try {
- const response = await fetch(caption.url)
- let text = await response.text()
-
- // position:0% for LTR text and position:100% for RTL text
- text = text.replaceAll(/ align:start position:(?:10)?0%$/gm, '')
-
- const url = `data:${caption.mimeType};charset=utf-8,${encodeURIComponent(text)}`
-
- await player.addTextTrackAsync(
- url,
+ if (process.env.SUPPORTS_LOCAL_API && props.format !== 'legacy' && props.manifestMimeType === MANIFEST_TYPE_SABR) {
+ sabrManifest = player.getManifest()
+ }
+
+ // For SABR we include the thumbnails and subtitles in the manifest
+ if (!process.env.SUPPORTS_LOCAL_API || props.format === 'legacy' || props.manifestMimeType !== MANIFEST_TYPE_SABR) {
+ const promises = []
+
+ for (const caption of props.captions) {
+ if (props.format === 'legacy') {
+ const url = new URL(caption.url)
+
+ if (url.hostname.endsWith('.youtube.com') && url.pathname === '/api/timedtext' &&
+ url.searchParams.get('caps') === 'asr' && url.searchParams.get('kind') === 'asr' && url.searchParams.get('fmt') === 'vtt') {
+ promises.push((async () => {
+ try {
+ const response = await fetch(caption.url)
+ let text = await response.text()
+
+ // position:0% for LTR text and position:100% for RTL text
+ text = text.replaceAll(/ align:start position:(?:10)?0%$/gm, '')
+
+ const url = `data:${caption.mimeType};charset=utf-8,${encodeURIComponent(text)}`
+
+ await player.addTextTrackAsync(
+ url,
+ caption.language,
+ 'captions',
+ caption.mimeType,
+ undefined, // codec, only needed if the captions are inside a container (e.g. mp4)
+ caption.label
+ )
+ } catch (error) {
+ if (error instanceof shaka.util.Error) {
+ handleError(error, 'addTextTrackAsync', caption)
+ } else {
+ console.error(error)
+ }
+ }
+ })())
+ } else {
+ promises.push(
+ player.addTextTrackAsync(
+ caption.url,
caption.language,
'captions',
caption.mimeType,
undefined, // codec, only needed if the captions are inside a container (e.g. mp4)
caption.label
)
- } catch (error) {
- if (error instanceof shaka.util.Error) {
- handleError(error, 'addTextTrackAsync', caption)
- } else {
- console.error(error)
- }
- }
- })())
+ .catch(error => handleError(error, 'addTextTrackAsync', caption))
+ )
+ }
} else {
promises.push(
player.addTextTrackAsync(
@@ -2726,32 +2863,20 @@ export default defineComponent({
.catch(error => handleError(error, 'addTextTrackAsync', caption))
)
}
- } else {
+ }
+
+ if (!isLive.value && props.storyboardSrc) {
promises.push(
- player.addTextTrackAsync(
- caption.url,
- caption.language,
- 'captions',
- caption.mimeType,
- undefined, // codec, only needed if the captions are inside a container (e.g. mp4)
- caption.label
- )
- .catch(error => handleError(error, 'addTextTrackAsync', caption))
+ // Only log the error, as the thumbnails are a nice to have
+ // If an error occurs with them, it's not critical
+ player.addThumbnailsTrack(props.storyboardSrc, 'text/vtt')
+ .catch(error => logShakaError(error, 'addThumbnailsTrack', props.videoId, props.storyboardSrc))
)
}
- }
- if (!isLive.value && props.storyboardSrc) {
- promises.push(
- // Only log the error, as the thumbnails are a nice to have
- // If an error occurs with them, it's not critical
- player.addThumbnailsTrack(props.storyboardSrc, 'text/vtt')
- .catch(error => logShakaError(error, 'addThumbnailsTrack', props.videoId, props.storyboardSrc))
- )
+ await Promise.all(promises)
}
- await Promise.all(promises)
-
if (restoreCaptionIndex !== null) {
const index = restoreCaptionIndex
restoreCaptionIndex = null
@@ -2938,9 +3063,13 @@ export default defineComponent({
document.removeEventListener('keydown', keyboardShortcutHandler)
document.removeEventListener('fullscreenchange', fullscreenChangeHandler)
- if (resizeObserver) {
- resizeObserver.disconnect()
- resizeObserver = null
+ if (containerResizeObserver) {
+ containerResizeObserver.disconnect()
+ containerResizeObserver = null
+ }
+
+ if (videoResizeObserver) {
+ videoResizeObserver.disconnect()
}
cleanUpCustomPlayerControls()
@@ -3013,6 +3142,11 @@ export default defineComponent({
player = null
}
+ if (process.env.SUPPORTS_LOCAL_API && sabrStream) {
+ sabrStream.cleanup()
+ sabrAbortController?.abort()
+ }
+
// shaka-player doesn't clear these itself, which prevents shaka.ui.Overlay from being garbage collected
// Should really be fixed in shaka-player but it's easier just to do it ourselves
if (container.value) {
@@ -3081,6 +3215,7 @@ export default defineComponent({
showStats,
stats,
+ playerDimensions,
autoplayVideos,
sponsorBlockShowSkippedToast,
@@ -3095,6 +3230,8 @@ export default defineComponent({
handleEnded,
updateVolume,
handleTimeupdate,
+ handleEnterPictureInPicture,
+ handleLeavePictureInPicture,
valueChangeMessage,
valueChangeIcon,
diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue
index 198d8b98ad7d1..869d43e5ec23e 100644
--- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue
+++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue
@@ -22,6 +22,8 @@
@canplay="handleCanPlay"
@volumechange="updateVolume"
@timeupdate="handleTimeupdate"
+ @enterpictureinpicture="handleEnterPictureInPicture"
+ @leavepictureinpicture="handleLeavePictureInPicture"
/>