2023-01-18 08:50:02 +01:00
|
|
|
import { defineComponent } from 'vue'
|
2021-01-13 04:56:31 +01:00
|
|
|
import { mapActions } from 'vuex'
|
2020-02-16 19:30:00 +01:00
|
|
|
|
|
|
|
import videojs from 'video.js'
|
2020-02-18 21:59:01 +01:00
|
|
|
import qualitySelector from '@silvermine/videojs-quality-selector'
|
2022-12-29 02:19:48 +01:00
|
|
|
import fs from 'fs/promises'
|
2022-05-30 15:24:34 +02:00
|
|
|
import path from 'path'
|
2020-11-15 20:13:18 +01:00
|
|
|
import 'videojs-overlay/dist/videojs-overlay'
|
|
|
|
import 'videojs-overlay/dist/videojs-overlay.css'
|
2020-09-01 12:04:54 +02:00
|
|
|
import 'videojs-vtt-thumbnails-freetube'
|
2020-02-18 21:59:01 +01:00
|
|
|
import 'videojs-contrib-quality-levels'
|
|
|
|
import 'videojs-http-source-selector'
|
2022-10-24 19:49:52 +02:00
|
|
|
import 'videojs-mobile-ui'
|
|
|
|
import 'videojs-mobile-ui/dist/videojs-mobile-ui.css'
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
import { IpcChannels } from '../../../constants'
|
2022-10-08 22:23:14 +02:00
|
|
|
import { sponsorBlockSkipSegments } from '../../helpers/sponsorblock'
|
2022-11-09 06:57:48 +01:00
|
|
|
import { calculateColorLuminance, colors } from '../../helpers/colors'
|
2022-12-29 02:19:48 +01:00
|
|
|
import { pathExists } from '../../helpers/filesystem'
|
2023-07-17 14:32:39 +02:00
|
|
|
import {
|
|
|
|
copyToClipboard,
|
|
|
|
getPicturesPath,
|
|
|
|
showSaveDialog,
|
|
|
|
showToast,
|
|
|
|
} from '../../helpers/utils'
|
2023-06-12 17:06:08 +02:00
|
|
|
import { getProxyUrl } from '../../helpers/api/invidious'
|
|
|
|
import store from '../../store'
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
|
2023-08-03 15:01:08 +02:00
|
|
|
const EXPECTED_PLAY_RELATED_ERROR_MESSAGES = [
|
|
|
|
// This is thrown when `play()` called but user already viewing another page
|
|
|
|
'The play() request was interrupted by a new load request.',
|
|
|
|
// This is thrown when `pause()` called before video started playing on load
|
|
|
|
'The play() request was interrupted by a call to pause()',
|
|
|
|
]
|
|
|
|
|
2023-03-01 02:38:13 +01:00
|
|
|
// YouTube now throttles if you use the `Range` header for the DASH formats, instead of the range query parameter
|
|
|
|
// videojs-http-streaming calls this hook everytime it makes a request,
|
|
|
|
// so we can use it to convert the Range header into the range query parameter for the streaming URLs
|
|
|
|
videojs.Vhs.xhr.beforeRequest = (options) => {
|
2023-09-25 16:55:47 +02:00
|
|
|
if (store.getters.getProxyVideos && !options.uri.startsWith('data:application/dash+xml')) {
|
2023-06-12 17:06:08 +02:00
|
|
|
const { uri } = options
|
|
|
|
options.uri = getProxyUrl(uri)
|
|
|
|
}
|
2023-05-10 14:00:27 +02:00
|
|
|
// pass in the optional base so it doesn't error for `dashFiles/videoId.xml` (DASH manifest in dev mode)
|
|
|
|
if (new URL(options.uri, window.location.origin).hostname.endsWith('.googlevideo.com')) {
|
|
|
|
// The official clients use POST requests with this body for the DASH requests, so we should do that too
|
|
|
|
options.method = 'POST'
|
|
|
|
options.body = 'x\x00' // protobuf: { 15: 0 } (no idea what it means but this is what YouTube uses)
|
|
|
|
|
|
|
|
if (options.headers?.Range) {
|
|
|
|
options.uri += `&range=${options.headers.Range.split('=')[1]}`
|
|
|
|
delete options.headers.Range
|
|
|
|
}
|
2023-03-01 02:38:13 +01:00
|
|
|
}
|
|
|
|
}
|
2023-03-23 01:22:20 +01:00
|
|
|
// videojs-http-streaming spits out a warning every time you access videojs.Vhs.BANDWIDTH_VARIANCE
|
|
|
|
// so we'll get the value once here, to stop it spamming the console
|
|
|
|
// https://github.com/videojs/http-streaming/blob/main/src/config.js#L8-L10
|
|
|
|
const VHS_BANDWIDTH_VARIANCE = videojs.Vhs.BANDWIDTH_VARIANCE
|
|
|
|
|
2023-01-18 08:50:02 +01:00
|
|
|
export default defineComponent({
|
2020-02-16 19:30:00 +01:00
|
|
|
name: 'FtVideoPlayer',
|
|
|
|
props: {
|
2020-02-19 04:31:10 +01:00
|
|
|
format: {
|
|
|
|
type: String,
|
|
|
|
required: true
|
|
|
|
},
|
2020-02-18 21:59:01 +01:00
|
|
|
sourceList: {
|
|
|
|
type: Array,
|
2020-04-21 22:22:41 +02:00
|
|
|
default: () => { return [] }
|
2020-02-18 21:59:01 +01:00
|
|
|
},
|
2021-05-15 21:08:41 +02:00
|
|
|
adaptiveFormats: {
|
|
|
|
type: Array,
|
|
|
|
default: () => { return [] }
|
|
|
|
},
|
2020-02-18 21:59:01 +01:00
|
|
|
dashSrc: {
|
2020-02-19 04:31:10 +01:00
|
|
|
type: Array,
|
2020-02-18 21:59:01 +01:00
|
|
|
default: null
|
|
|
|
},
|
2021-03-17 02:28:25 +01:00
|
|
|
captionHybridList: {
|
2020-02-16 19:30:00 +01:00
|
|
|
type: Array,
|
|
|
|
default: () => { return [] }
|
|
|
|
},
|
2020-02-18 21:59:01 +01:00
|
|
|
storyboardSrc: {
|
2020-02-16 19:30:00 +01:00
|
|
|
type: String,
|
2020-02-18 21:59:01 +01:00
|
|
|
default: ''
|
2020-06-01 04:47:22 +02:00
|
|
|
},
|
|
|
|
thumbnail: {
|
|
|
|
type: String,
|
|
|
|
default: ''
|
2021-05-16 22:01:24 +02:00
|
|
|
},
|
|
|
|
videoId: {
|
|
|
|
type: String,
|
|
|
|
required: true
|
2022-08-20 14:49:41 +02:00
|
|
|
},
|
|
|
|
lengthSeconds: {
|
|
|
|
type: Number,
|
|
|
|
required: true
|
2022-09-29 22:01:54 +02:00
|
|
|
},
|
|
|
|
chapters: {
|
|
|
|
type: Array,
|
|
|
|
default: () => { return [] }
|
2023-05-20 14:48:15 +02:00
|
|
|
},
|
2023-08-03 14:57:09 +02:00
|
|
|
currentChapterIndex: {
|
|
|
|
type: Number,
|
|
|
|
default: 0
|
|
|
|
},
|
2023-05-20 14:48:15 +02:00
|
|
|
audioTracks: {
|
|
|
|
type: Array,
|
|
|
|
default: () => ([])
|
2023-08-03 14:57:09 +02:00
|
|
|
},
|
|
|
|
theatrePossible: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
|
|
|
},
|
|
|
|
useTheatreMode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
data: function () {
|
|
|
|
return {
|
2021-01-11 21:45:46 +01:00
|
|
|
powerSaveBlocker: null,
|
2020-03-01 04:37:02 +01:00
|
|
|
volume: 1,
|
2023-03-08 19:22:45 +01:00
|
|
|
muted: false,
|
2023-05-20 14:48:15 +02:00
|
|
|
/** @type {(import('video.js').VideoJsPlayer|null)} */
|
2020-02-16 19:30:00 +01:00
|
|
|
player: null,
|
2020-02-18 21:59:01 +01:00
|
|
|
useDash: false,
|
|
|
|
useHls: false,
|
2020-09-25 04:35:13 +02:00
|
|
|
selectedDefaultQuality: '',
|
2021-03-12 22:09:08 +01:00
|
|
|
selectedQuality: '',
|
2022-02-19 23:17:58 +01:00
|
|
|
selectedResolution: '',
|
|
|
|
selectedBitrate: '',
|
|
|
|
selectedMimeType: '',
|
|
|
|
selectedFPS: 0,
|
2023-08-26 18:20:55 +02:00
|
|
|
currentAdaptiveFormat: null,
|
|
|
|
autoQuality: '',
|
|
|
|
autoResolution: '',
|
|
|
|
autoBitrate: '',
|
|
|
|
autoMimeType: '',
|
|
|
|
autoFPS: 0,
|
2021-04-30 23:18:45 +02:00
|
|
|
using60Fps: false,
|
2020-02-18 21:59:01 +01:00
|
|
|
activeSourceList: [],
|
2021-05-15 21:08:41 +02:00
|
|
|
activeAdaptiveFormats: [],
|
2022-02-19 23:17:58 +01:00
|
|
|
playerStats: null,
|
|
|
|
statsModal: null,
|
|
|
|
showStatsModal: false,
|
|
|
|
statsModalEventName: 'updateStats',
|
2022-10-24 19:49:52 +02:00
|
|
|
usingTouch: false,
|
2023-02-05 20:47:02 +01:00
|
|
|
// whether or not sponsor segments should be skipped
|
|
|
|
skipSponsors: true,
|
|
|
|
// countdown before actually skipping sponsor segments
|
|
|
|
skipCountdown: 1,
|
2020-02-16 19:30:00 +01:00
|
|
|
dataSetup: {
|
2021-01-15 05:20:42 +01:00
|
|
|
fluid: true,
|
2020-02-18 21:59:01 +01:00
|
|
|
nativeTextTracks: false,
|
|
|
|
plugins: {},
|
|
|
|
controlBar: {
|
|
|
|
children: [
|
|
|
|
'playToggle',
|
|
|
|
'volumePanel',
|
|
|
|
'currentTimeDisplay',
|
|
|
|
'timeDivider',
|
|
|
|
'durationDisplay',
|
|
|
|
'progressControl',
|
|
|
|
'liveDisplay',
|
|
|
|
'seekToLive',
|
|
|
|
'remainingTimeDisplay',
|
|
|
|
'customControlSpacer',
|
2022-05-30 15:24:34 +02:00
|
|
|
'screenshotButton',
|
2020-02-18 21:59:01 +01:00
|
|
|
'playbackRateMenuButton',
|
2021-01-13 04:56:31 +01:00
|
|
|
'loopButton',
|
2023-05-20 14:48:15 +02:00
|
|
|
'audioTrackButton',
|
2020-02-18 21:59:01 +01:00
|
|
|
'chaptersButton',
|
|
|
|
'descriptionsButton',
|
|
|
|
'subsCapsButton',
|
2020-12-15 00:25:51 +01:00
|
|
|
'pictureInPictureToggle',
|
2021-08-24 09:36:10 +02:00
|
|
|
'toggleTheatreModeButton',
|
2020-12-15 00:25:51 +01:00
|
|
|
'fullWindowButton',
|
2020-12-14 23:37:58 +01:00
|
|
|
'qualitySelector',
|
2020-12-15 00:25:51 +01:00
|
|
|
'fullscreenToggle'
|
2020-02-18 21:59:01 +01:00
|
|
|
]
|
2022-04-11 00:00:47 +02:00
|
|
|
}
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
2021-09-20 04:12:14 +02:00
|
|
|
currentLocale: function () {
|
2023-08-15 20:17:10 +02:00
|
|
|
return this.$i18n.locale.replace('_', '-')
|
2021-09-20 04:12:14 +02:00
|
|
|
},
|
|
|
|
|
2020-03-01 04:37:02 +01:00
|
|
|
defaultPlayback: function () {
|
|
|
|
return this.$store.getters.getDefaultPlayback
|
2020-02-20 21:58:21 +01:00
|
|
|
},
|
|
|
|
|
2021-08-05 22:17:01 +02:00
|
|
|
defaultSkipInterval: function () {
|
|
|
|
return this.$store.getters.getDefaultSkipInterval
|
|
|
|
},
|
|
|
|
|
2020-05-17 22:12:58 +02:00
|
|
|
defaultQuality: function () {
|
2023-03-23 01:22:20 +01:00
|
|
|
const valueFromStore = this.$store.getters.getDefaultQuality
|
|
|
|
if (valueFromStore === 'auto') { return valueFromStore }
|
|
|
|
|
|
|
|
return parseInt(valueFromStore)
|
2020-05-17 22:12:58 +02:00
|
|
|
},
|
|
|
|
|
2021-06-10 21:35:00 +02:00
|
|
|
defaultCaptionSettings: function () {
|
|
|
|
try {
|
|
|
|
return JSON.parse(this.$store.getters.getDefaultCaptionSettings)
|
|
|
|
} catch (e) {
|
2022-09-23 03:04:10 +02:00
|
|
|
console.error(e)
|
2021-06-10 21:35:00 +02:00
|
|
|
return {}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-03-01 04:37:02 +01:00
|
|
|
autoplayVideos: function () {
|
|
|
|
return this.$store.getters.getAutoplayVideos
|
2021-05-16 22:01:24 +02:00
|
|
|
},
|
|
|
|
|
2021-05-26 17:55:11 +02:00
|
|
|
videoVolumeMouseScroll: function () {
|
|
|
|
return this.$store.getters.getVideoVolumeMouseScroll
|
|
|
|
},
|
|
|
|
|
2021-11-24 22:52:56 +01:00
|
|
|
videoPlaybackRateMouseScroll: function () {
|
|
|
|
return this.$store.getters.getVideoPlaybackRateMouseScroll
|
|
|
|
},
|
|
|
|
|
2022-12-22 16:21:32 +01:00
|
|
|
videoSkipMouseScroll: function () {
|
|
|
|
return this.$store.getters.getVideoSkipMouseScroll
|
|
|
|
},
|
|
|
|
|
2021-05-16 22:01:24 +02:00
|
|
|
useSponsorBlock: function () {
|
|
|
|
return this.$store.getters.getUseSponsorBlock
|
|
|
|
},
|
|
|
|
|
|
|
|
sponsorBlockShowSkippedToast: function () {
|
|
|
|
return this.$store.getters.getSponsorBlockShowSkippedToast
|
2021-05-29 20:35:28 +02:00
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
displayVideoPlayButton: function () {
|
2021-05-29 20:35:28 +02:00
|
|
|
return this.$store.getters.getDisplayVideoPlayButton
|
2022-04-11 00:00:47 +02:00
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
enterFullscreenOnDisplayRotate: function () {
|
2022-11-04 02:11:22 +01:00
|
|
|
return this.$store.getters.getEnterFullscreenOnDisplayRotate
|
|
|
|
},
|
|
|
|
|
2022-05-29 22:36:59 +02:00
|
|
|
sponsorSkips: function () {
|
|
|
|
const sponsorCats = ['sponsor',
|
|
|
|
'selfpromo',
|
|
|
|
'interaction',
|
|
|
|
'intro',
|
|
|
|
'outro',
|
|
|
|
'preview',
|
|
|
|
'music_offtopic',
|
|
|
|
'filler'
|
|
|
|
]
|
|
|
|
const autoSkip = {}
|
|
|
|
const seekBar = []
|
|
|
|
const promptSkip = {}
|
|
|
|
const categoryData = {}
|
|
|
|
sponsorCats.forEach(x => {
|
|
|
|
let sponsorVal = {}
|
|
|
|
switch (x) {
|
|
|
|
case 'sponsor':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockSponsor
|
|
|
|
break
|
|
|
|
case 'selfpromo':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockSelfPromo
|
|
|
|
break
|
|
|
|
case 'interaction':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockInteraction
|
|
|
|
break
|
|
|
|
case 'intro':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockIntro
|
|
|
|
break
|
|
|
|
case 'outro':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockOutro
|
|
|
|
break
|
|
|
|
case 'preview':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockRecap
|
|
|
|
break
|
|
|
|
case 'music_offtopic':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockMusicOffTopic
|
|
|
|
break
|
|
|
|
case 'filler':
|
|
|
|
sponsorVal = this.$store.getters.getSponsorBlockFiller
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if (sponsorVal.skip !== 'doNothing') {
|
|
|
|
seekBar.push(x)
|
|
|
|
}
|
|
|
|
if (sponsorVal.skip === 'autoSkip') {
|
|
|
|
autoSkip[x] = true
|
|
|
|
}
|
|
|
|
if (sponsorVal.skip === 'promptToSkip') {
|
|
|
|
promptSkip[x] = true
|
|
|
|
}
|
|
|
|
categoryData[x] = sponsorVal
|
|
|
|
})
|
|
|
|
return { autoSkip, seekBar, promptSkip, categoryData }
|
|
|
|
},
|
|
|
|
|
2022-04-11 00:00:47 +02:00
|
|
|
maxVideoPlaybackRate: function () {
|
|
|
|
return parseInt(this.$store.getters.getMaxVideoPlaybackRate)
|
|
|
|
},
|
|
|
|
|
|
|
|
videoPlaybackRateInterval: function () {
|
|
|
|
return parseFloat(this.$store.getters.getVideoPlaybackRateInterval)
|
|
|
|
},
|
|
|
|
|
|
|
|
playbackRates: function () {
|
|
|
|
const playbackRates = []
|
|
|
|
let i = this.videoPlaybackRateInterval
|
|
|
|
|
|
|
|
while (i <= this.maxVideoPlaybackRate) {
|
|
|
|
playbackRates.push(i)
|
|
|
|
i = i + this.videoPlaybackRateInterval
|
|
|
|
i = parseFloat(i.toFixed(2))
|
|
|
|
}
|
|
|
|
|
|
|
|
return playbackRates
|
2022-05-30 15:24:34 +02:00
|
|
|
},
|
|
|
|
|
2024-01-03 01:28:13 +01:00
|
|
|
enableSubtitlesByDefault: function () {
|
|
|
|
return this.$store.getters.getEnableSubtitlesByDefault
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
enableScreenshot: function () {
|
2022-05-30 15:24:34 +02:00
|
|
|
return this.$store.getters.getEnableScreenshot
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
screenshotFormat: function () {
|
2022-05-30 15:24:34 +02:00
|
|
|
return this.$store.getters.getScreenshotFormat
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
screenshotQuality: function () {
|
2022-05-30 15:24:34 +02:00
|
|
|
return this.$store.getters.getScreenshotQuality
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
screenshotAskPath: function () {
|
2022-05-30 15:24:34 +02:00
|
|
|
return this.$store.getters.getScreenshotAskPath
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
screenshotFolder: function () {
|
2022-05-30 15:24:34 +02:00
|
|
|
return this.$store.getters.getScreenshotFolderPath
|
2023-06-12 17:06:08 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
proxyVideos: function () {
|
|
|
|
return this.$store.getters.getProxyVideos
|
Stats for nerds (#1867)
* transition duration of 0.5s added to watched videos
* small code reformating
* extra white spaces deleted
* typo in the word transition corrected
* original whitespaces respected
* transition added when hovering end
* video stat components started and properties chosen
* ft-video-stats integraded into the video player for dev and debugging
* using a timer to get video stats and a method to update the statistic every second
* getting statistic from vhs and adaptativeFormat
* frame drop capture
* stats capture in the form of event
* useless comment deleted
* stats render with a for loop in the template
* stats correctly displayed
* overlay stats added
* video stats component deleted
* video stats component deleted inside template video player
* video stats component fully deleted
* modal solution working need more styling and code messy
* lint
* modal working with stats
* keyboard shortcut for stats
* lint fix
* network state is now a string
* new line deleted
* useless whitespace deleted
* package-lock.json remove and ignore
* keyboard shortcut restricted to up arrow
* stats overlay made larger
* align to left corner
* useless formatting of string deleted
* renaming of variable formatedStrats for formattedStats
* keyboard shortcut made into a variable
* lint-fix
* key change for i
* label translated
* whitespace added for gitignore
* lock file not ignored
* videoId stat deleted
* ft-video-player.js, en-US.yaml, fr-FR.yaml: changing percentage stats display
changing the display for percentage stats for the format 'x%' instead of 'xx.xx'
* ft-video-player.js, en-US.yaml, fr-FR.yaml: network state video statistic deleted
* ft-video-player.js: made stats modal background color darker
* ft-video-player.js, en-US.yaml, fr-FR.yaml: video id are now related to the one of youtube
* ft-video-player.js, en-US.yaml, fr-FR.yaml: stats displayed made closet to the youtube implementation
the name are capitalized, the order of display is changed and fps is combined with viewport
* lint-fix
* en-US.yaml, fr-FR.yaml: network state possibilities deleted because not used
* package.json.lock: deleted
* ft-video-player.js: formated_stats renamed for formatted_stats
* lock file deleted
* index.js, ft-video-player.js: handling of right click context menu
via electon ipc bus an event is send to tell the vue component to show the stats modal
* ft-video-player.js, index.js: renaming of video stats display event and definition of it as a variable
* index.js, en-US.yaml: inconsistant capitalization of video statistics label solved
* index.js: pluralized video stats
* ft-video-player.js: fix right click undefined this.player
change the arrow function inside the closure for a function with a bind to this
* ft-video-player.js: handling of the case when this.player is not defined
the property this.stats.display.activated as been added and manage when the to show the stats. In this way in the runtime (it is still refered in the run time but it is capture in an event loop) with dont have to refer to this.player so when it is not defined it doesnt affect the behavior.
* lint fix
* src/renderer/components/ft-video-player/ft-video-player.js: modal.close move into the display event of the statistic context
* lint fix
* src/renderer/components/ft-video-player/ft-video-player.js, static/locales/en-US.yaml, static/locales/fr-FR.yaml: better capitalization of the stats labels
* static/locales/en-US.yaml: fps capitalized
* static/locales/fr-FR.yaml, static/locales/en-US.yaml: capitalized label
2021-11-23 12:34:04 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
2023-08-26 18:20:55 +02:00
|
|
|
showStatsModal: function () {
|
2022-02-19 23:17:58 +01:00
|
|
|
this.player.trigger(this.statsModalEventName)
|
2022-05-30 15:24:34 +02:00
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
enableScreenshot: function () {
|
2022-05-30 15:24:34 +02:00
|
|
|
this.toggleScreenshotButton()
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
|
|
|
},
|
2023-05-20 14:48:15 +02:00
|
|
|
created: function () {
|
|
|
|
this.dataSetup.playbackRates = this.playbackRates
|
|
|
|
|
|
|
|
if (this.format === 'audio') {
|
|
|
|
// hide the PIP button for the audio formats
|
|
|
|
const controlBarItems = this.dataSetup.controlBar.children
|
|
|
|
const index = controlBarItems.indexOf('pictureInPictureToggle')
|
|
|
|
controlBarItems.splice(index, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.format === 'legacy' || this.audioTracks.length === 0) {
|
|
|
|
// hide the audio track selector for the legacy formats
|
|
|
|
// and Invidious(it doesn't give us the information for multiple audio tracks yet)
|
|
|
|
const controlBarItems = this.dataSetup.controlBar.children
|
|
|
|
const index = controlBarItems.indexOf('audioTrackButton')
|
|
|
|
controlBarItems.splice(index, 1)
|
|
|
|
}
|
|
|
|
},
|
2020-02-16 19:30:00 +01:00
|
|
|
mounted: function () {
|
2020-03-01 04:37:02 +01:00
|
|
|
const volume = sessionStorage.getItem('volume')
|
2023-03-08 19:22:45 +01:00
|
|
|
const muted = sessionStorage.getItem('muted')
|
2020-03-01 04:37:02 +01:00
|
|
|
|
|
|
|
if (volume !== null) {
|
|
|
|
this.volume = volume
|
|
|
|
}
|
|
|
|
|
2023-03-08 19:22:45 +01:00
|
|
|
if (muted !== null) {
|
|
|
|
// as sessionStorage stores string values which are truthy by default so we must check with 'true'
|
|
|
|
// otherwise 'false' will be returned as true as well
|
|
|
|
this.muted = (muted === 'true')
|
|
|
|
}
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
if (this.format === 'dash') {
|
|
|
|
this.determineDefaultQualityDash()
|
|
|
|
}
|
|
|
|
|
2020-12-15 00:25:51 +01:00
|
|
|
this.createFullWindowButton()
|
2021-01-13 04:56:31 +01:00
|
|
|
this.createLoopButton()
|
2021-08-24 09:36:10 +02:00
|
|
|
this.createToggleTheatreModeButton()
|
2022-05-30 15:24:34 +02:00
|
|
|
this.createScreenshotButton()
|
2020-02-18 21:59:01 +01:00
|
|
|
this.determineFormatType()
|
2022-05-23 03:48:49 +02:00
|
|
|
|
|
|
|
if ('mediaSession' in navigator) {
|
2023-07-17 14:32:39 +02:00
|
|
|
navigator.mediaSession.setActionHandler('play', () => this.playVideo())
|
2022-05-23 03:48:49 +02:00
|
|
|
navigator.mediaSession.setActionHandler('pause', () => this.player.pause())
|
|
|
|
}
|
2023-05-23 13:37:44 +02:00
|
|
|
|
|
|
|
window.addEventListener('beforeunload', this.stopPowerSaveBlocker)
|
2020-02-16 19:30:00 +01:00
|
|
|
},
|
2020-02-20 21:58:21 +01:00
|
|
|
beforeDestroy: function () {
|
2023-02-27 02:03:26 +01:00
|
|
|
document.removeEventListener('keydown', this.keyboardShortcutHandler)
|
2020-12-22 19:58:17 +01:00
|
|
|
if (this.player !== null) {
|
|
|
|
this.exitFullWindow()
|
|
|
|
|
|
|
|
if (!this.player.isInPictureInPicture()) {
|
|
|
|
this.player.dispose()
|
|
|
|
this.player = null
|
|
|
|
}
|
2020-02-20 21:58:21 +01:00
|
|
|
}
|
2021-01-13 21:56:25 +01:00
|
|
|
|
2022-05-23 03:48:49 +02:00
|
|
|
if ('mediaSession' in navigator) {
|
|
|
|
navigator.mediaSession.setActionHandler('play', null)
|
|
|
|
navigator.mediaSession.setActionHandler('pause', null)
|
|
|
|
navigator.mediaSession.playbackState = 'none'
|
|
|
|
}
|
|
|
|
|
2023-05-23 13:37:44 +02:00
|
|
|
this.stopPowerSaveBlocker()
|
|
|
|
window.removeEventListener('beforeunload', this.stopPowerSaveBlocker)
|
2020-02-20 21:58:21 +01:00
|
|
|
},
|
2020-02-16 19:30:00 +01:00
|
|
|
methods: {
|
2020-09-25 04:35:13 +02:00
|
|
|
initializePlayer: async function () {
|
2022-10-05 10:25:50 +02:00
|
|
|
if (typeof this.$refs.video !== 'undefined') {
|
2020-05-24 23:32:02 +02:00
|
|
|
if (!this.useDash) {
|
2020-02-18 21:59:01 +01:00
|
|
|
qualitySelector(videojs, { showQualitySelectionLabelInControlBar: true })
|
2020-09-26 23:03:42 +02:00
|
|
|
await this.determineDefaultQualityLegacy()
|
2020-02-18 21:59:01 +01:00
|
|
|
}
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
// regardless of what DASH qualities you enable or disable in the qualityLevels plugin
|
|
|
|
// the first segments videojs-http-streaming requests are chosen based on the available bandwidth, which is set to 0.5MB/s by default
|
|
|
|
// overriding that to be the same as the quality we requested, makes videojs-http-streamming pick the correct quality
|
|
|
|
const playerBandwidthOption = {}
|
|
|
|
|
|
|
|
if (this.useDash && this.defaultQuality !== 'auto') {
|
|
|
|
// https://github.com/videojs/http-streaming#bandwidth
|
|
|
|
// Cannot be too high to fix https://github.com/FreeTubeApp/FreeTube/issues/595
|
|
|
|
// (when default quality is low like 240p)
|
|
|
|
playerBandwidthOption.bandwidth = this.selectedBitrate * VHS_BANDWIDTH_VARIANCE + 1
|
|
|
|
}
|
|
|
|
|
2022-10-05 10:25:50 +02:00
|
|
|
this.player = videojs(this.$refs.video, {
|
2020-10-28 22:20:07 +01:00
|
|
|
html5: {
|
2021-03-17 02:28:25 +01:00
|
|
|
preloadTextTracks: false,
|
2020-10-28 22:20:07 +01:00
|
|
|
vhs: {
|
2020-12-27 04:21:06 +01:00
|
|
|
limitRenditionByPlayerDimensions: false,
|
|
|
|
smoothQualityChange: false,
|
2021-03-12 22:09:08 +01:00
|
|
|
allowSeeksWithinUnsafeLiveWindow: true,
|
2023-03-23 01:22:20 +01:00
|
|
|
handlePartialData: true,
|
|
|
|
...playerBandwidthOption
|
2020-10-28 22:20:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2022-10-24 19:49:52 +02:00
|
|
|
this.player.mobileUi({
|
|
|
|
fullscreen: {
|
2022-11-04 02:11:22 +01:00
|
|
|
enterOnRotate: this.enterFullscreenOnDisplayRotate,
|
|
|
|
exitOnRotate: this.enterFullscreenOnDisplayRotate,
|
2022-10-24 19:49:52 +02:00
|
|
|
lockOnRotate: false
|
|
|
|
},
|
|
|
|
// Without this flag, the mobile UI will only activate
|
|
|
|
// if videojs detects it is in Android or iOS
|
|
|
|
// With this flag, the mobile UI could theoretically
|
|
|
|
// work on any device that has a touch input
|
|
|
|
forceForTesting: true,
|
|
|
|
touchControls: {
|
|
|
|
seekSeconds: this.defaultSkipInterval,
|
|
|
|
tapTimeout: 300
|
|
|
|
}
|
|
|
|
})
|
2020-12-15 00:25:51 +01:00
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
const qualityLevels = this.player.qualityLevels()
|
|
|
|
|
|
|
|
// Catch quality changes and update auto labels
|
|
|
|
// Event will not fire if new auto resolution is same as previous manual
|
|
|
|
// eg. 1080p30 -> auto 1080p30
|
|
|
|
qualityLevels.on('change', ({ selectedIndex }) => {
|
|
|
|
if (this.selectedQuality === 'auto' || (this.selectedQuality === '' && this.defaultQuality === 'auto')) {
|
|
|
|
const newQualityLevel = qualityLevels[selectedIndex]
|
|
|
|
this.autoBitrate = newQualityLevel.bitrate
|
|
|
|
this.autoFPS = newQualityLevel.frameRate
|
|
|
|
this.autoResolution = `${newQualityLevel.width}x${newQualityLevel.height}`
|
|
|
|
|
|
|
|
let qualityLabel = ''
|
|
|
|
const adaptiveFormat = this.activeAdaptiveFormats.find((format) => {
|
|
|
|
return format.bitrate === newQualityLevel.bitrate
|
|
|
|
})
|
|
|
|
if (adaptiveFormat) {
|
|
|
|
this.autoMimeType = adaptiveFormat.mimeType
|
|
|
|
this.currentAdaptiveFormat = adaptiveFormat
|
|
|
|
qualityLabel = `auto ${adaptiveFormat.qualityLabel}`
|
|
|
|
} else {
|
|
|
|
qualityLabel = `auto ${newQualityLevel.height}p`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Can be run before createDashQualitySelector is called
|
|
|
|
const qualityElement = document.getElementById('vjs-current-quality')
|
|
|
|
if (qualityElement !== null) {
|
|
|
|
qualityElement.innerText = qualityLabel
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
// disable any quality the isn't the default one, as soon as it gets added
|
|
|
|
// we don't need to disable any qualities for auto
|
|
|
|
if (this.useDash && this.defaultQuality !== 'auto') {
|
|
|
|
qualityLevels.on('addqualitylevel', ({ qualityLevel }) => {
|
|
|
|
qualityLevel.enabled = qualityLevel.bitrate === this.selectedBitrate
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-05-20 14:48:15 +02:00
|
|
|
// for the DASH formats, videojs-http-streaming takes care of the audio track management for us,
|
|
|
|
// thanks to the values in the DASH manifest
|
|
|
|
// so we only need a custom implementation for the audio only formats
|
|
|
|
if (this.format === 'audio' && this.audioTracks.length > 0) {
|
|
|
|
/** @type {import('../../views/Watch/Watch.js').AudioTrack[]} */
|
|
|
|
const audioTracks = this.audioTracks
|
|
|
|
|
|
|
|
const audioTrackList = this.player.audioTracks()
|
|
|
|
audioTracks.forEach(({ id, kind, label, language, isDefault: enabled }) => {
|
|
|
|
audioTrackList.addTrack(new videojs.AudioTrack({
|
|
|
|
id, kind, label, language, enabled,
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
|
|
|
|
audioTrackList.on('change', () => {
|
|
|
|
let trackId
|
|
|
|
// doesn't support foreach so we need to use an indexed for loop here
|
|
|
|
for (let i = 0; i < audioTrackList.length; i++) {
|
|
|
|
const track = audioTrackList[i]
|
|
|
|
|
|
|
|
if (track.enabled) {
|
|
|
|
trackId = track.id
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.changeAudioTrack(trackId)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-03-01 04:37:02 +01:00
|
|
|
this.player.volume(this.volume)
|
2023-03-08 19:22:45 +01:00
|
|
|
this.player.muted(this.muted)
|
2020-03-01 04:37:02 +01:00
|
|
|
this.player.playbackRate(this.defaultPlayback)
|
2021-06-10 21:35:00 +02:00
|
|
|
this.player.textTrackSettings.setValues(this.defaultCaptionSettings)
|
2021-05-28 22:29:35 +02:00
|
|
|
// Remove big play button
|
|
|
|
// https://github.com/videojs/video.js/blob/v7.12.1/docs/guides/components.md#basic-example
|
2021-05-29 20:35:28 +02:00
|
|
|
if (!this.displayVideoPlayButton) {
|
|
|
|
this.player.removeChild('BigPlayButton')
|
|
|
|
}
|
2020-03-01 04:37:02 +01:00
|
|
|
|
2021-11-30 23:08:33 +01:00
|
|
|
// Makes the playback rate menu focus the current item on mouse hover
|
|
|
|
// or the closest item if the playback rate is between two items
|
|
|
|
// which is likely to be the case when the playback rate is changed by scrolling
|
|
|
|
const playbackRateMenuButton = this.player.controlBar.getChild('playbackRateMenuButton')
|
|
|
|
playbackRateMenuButton.on(playbackRateMenuButton.menuButton_, 'mouseenter', () => {
|
|
|
|
const playbackRate = this.player.playbackRate()
|
|
|
|
const rates = this.player.playbackRates()
|
|
|
|
|
|
|
|
// iterate through the items in reverse order as the highest is displayed first
|
|
|
|
// `slice` must be used as `reverse` does reversing in place
|
|
|
|
const targetPlaybackRateMenuItemIndex = rates.slice().reverse().findIndex((rate) => {
|
|
|
|
return rate === playbackRate || rate < playbackRate
|
|
|
|
})
|
|
|
|
|
|
|
|
// center the selected item in the middle of the visible area
|
|
|
|
// the first and last items will never be in the center so it can be skipped for them
|
|
|
|
if (targetPlaybackRateMenuItemIndex !== 0 && targetPlaybackRateMenuItemIndex !== rates.length - 1) {
|
|
|
|
const playbackRateMenu = playbackRateMenuButton.menu
|
|
|
|
const menuElement = playbackRateMenu.contentEl()
|
|
|
|
|
|
|
|
const itemHeight = playbackRateMenu.children()[targetPlaybackRateMenuItemIndex].contentEl().clientHeight
|
|
|
|
|
|
|
|
// clientHeight is the height of the visible part of an element
|
|
|
|
const centerOfVisibleArea = (menuElement.clientHeight - itemHeight) / 2
|
|
|
|
const menuScrollOffset = (itemHeight * targetPlaybackRateMenuItemIndex) - centerOfVisibleArea
|
|
|
|
|
|
|
|
menuElement.scrollTo({ top: menuScrollOffset })
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2020-09-25 04:35:13 +02:00
|
|
|
if (this.storyboardSrc !== '') {
|
|
|
|
this.player.vttThumbnails({
|
|
|
|
src: this.storyboardSrc,
|
|
|
|
showTimestamp: true
|
|
|
|
})
|
|
|
|
}
|
2020-02-18 21:59:01 +01:00
|
|
|
|
2020-03-01 04:37:02 +01:00
|
|
|
if (this.autoplayVideos) {
|
2020-02-20 21:58:21 +01:00
|
|
|
// Calling play() won't happen right away, so a quick timeout will make it function properly.
|
|
|
|
setTimeout(() => {
|
2023-07-17 14:32:39 +02:00
|
|
|
// `this.player` can be destroyed before this runs
|
|
|
|
this.playVideo()
|
2020-05-24 23:32:02 +02:00
|
|
|
}, 200)
|
2020-02-20 21:58:21 +01:00
|
|
|
}
|
|
|
|
|
2021-08-25 00:46:42 +02:00
|
|
|
// Remove built-in progress bar mouse over current time display
|
|
|
|
// `MouseTimeDisplay` in
|
|
|
|
// https://github.com/videojs/video.js/blob/v7.13.3/docs/guides/components.md#default-component-tree
|
|
|
|
this.player.controlBar.progressControl.seekBar.playProgressBar.removeChild('timeTooltip')
|
|
|
|
|
2022-09-29 22:01:54 +02:00
|
|
|
if (this.chapters.length > 0) {
|
|
|
|
this.chapters.forEach(this.addChapterMarker)
|
|
|
|
}
|
|
|
|
|
2021-05-20 05:00:39 +02:00
|
|
|
if (this.useSponsorBlock) {
|
|
|
|
this.initializeSponsorBlock()
|
|
|
|
}
|
|
|
|
|
2022-09-25 02:44:16 +02:00
|
|
|
document.removeEventListener('keydown', this.keyboardShortcutHandler)
|
|
|
|
document.addEventListener('keydown', this.keyboardShortcutHandler)
|
2020-02-21 19:31:32 +01:00
|
|
|
|
2020-03-01 04:37:02 +01:00
|
|
|
this.player.on('volumechange', this.updateVolume)
|
2021-05-26 17:55:11 +02:00
|
|
|
if (this.videoVolumeMouseScroll) {
|
|
|
|
this.player.on('wheel', this.mouseScrollVolume)
|
|
|
|
} else {
|
|
|
|
this.player.controlBar.getChild('volumePanel').on('wheel', this.mouseScrollVolume)
|
|
|
|
}
|
2020-02-21 19:31:32 +01:00
|
|
|
|
2021-11-24 22:52:56 +01:00
|
|
|
if (this.videoPlaybackRateMouseScroll) {
|
|
|
|
this.player.on('wheel', this.mouseScrollPlaybackRate)
|
|
|
|
// Removes the 'out-of-the-box' click event and adds a custom click event so that a user can
|
|
|
|
// ctrl-click (or command+click on a mac) without toggling play/pause
|
|
|
|
this.player.el_.firstChild.style.pointerEvents = 'none'
|
|
|
|
this.player.on('click', this.handlePlayerClick)
|
|
|
|
}
|
2022-12-22 16:21:32 +01:00
|
|
|
if (this.videoSkipMouseScroll) {
|
|
|
|
this.player.on('wheel', this.mouseScrollSkip)
|
|
|
|
}
|
2021-11-24 22:52:56 +01:00
|
|
|
|
2023-02-17 02:34:43 +01:00
|
|
|
this.player.on('fullscreenchange', () => {
|
|
|
|
this.fullscreenOverlay()
|
|
|
|
this.toggleFullscreenClass()
|
|
|
|
})
|
2020-11-15 20:13:18 +01:00
|
|
|
|
2021-05-22 01:52:11 +02:00
|
|
|
this.player.on('ready', () => {
|
|
|
|
this.$emit('ready')
|
2022-02-19 23:17:58 +01:00
|
|
|
this.createStatsModal()
|
2021-05-22 01:52:11 +02:00
|
|
|
if (this.captionHybridList.length !== 0) {
|
|
|
|
this.transformAndInsertCaptions()
|
2021-03-17 02:28:25 +01:00
|
|
|
}
|
2022-05-30 15:24:34 +02:00
|
|
|
this.toggleScreenshotButton()
|
2020-08-20 04:39:44 +02:00
|
|
|
})
|
|
|
|
|
2023-02-05 19:40:25 +01:00
|
|
|
this.player.one('loadedmetadata', () => {
|
2023-02-17 02:34:43 +01:00
|
|
|
if (this.useDash) {
|
|
|
|
// Reserved for switching back to videojs-http-quality-selector if needed
|
|
|
|
// this.dataSetup.plugins.httpSourceSelector = {
|
|
|
|
// default: 'auto'
|
|
|
|
// }
|
|
|
|
|
|
|
|
// this.player.httpSourceSelector()
|
|
|
|
this.createDashQualitySelector(this.player.qualityLevels())
|
|
|
|
}
|
|
|
|
|
2023-02-05 19:40:25 +01:00
|
|
|
this.checkAspectRatio()
|
|
|
|
})
|
|
|
|
|
2021-05-22 01:52:11 +02:00
|
|
|
this.player.on('ended', () => {
|
|
|
|
this.$emit('ended')
|
2022-05-23 03:48:49 +02:00
|
|
|
|
|
|
|
if ('mediaSession' in navigator) {
|
|
|
|
navigator.mediaSession.playbackState = 'none'
|
|
|
|
}
|
2023-03-14 07:17:44 +01:00
|
|
|
|
2023-05-23 13:37:44 +02:00
|
|
|
this.stopPowerSaveBlocker()
|
2020-05-17 22:12:58 +02:00
|
|
|
})
|
|
|
|
|
2021-05-22 01:52:11 +02:00
|
|
|
this.player.on('error', (error, message) => {
|
|
|
|
this.$emit('error', error.target.player.error_)
|
2022-05-23 03:48:49 +02:00
|
|
|
|
|
|
|
if ('mediaSession' in navigator) {
|
|
|
|
navigator.mediaSession.playbackState = 'none'
|
|
|
|
}
|
2023-03-14 07:17:44 +01:00
|
|
|
|
2023-05-23 13:37:44 +02:00
|
|
|
this.stopPowerSaveBlocker()
|
2020-02-21 19:31:32 +01:00
|
|
|
})
|
2021-01-11 21:45:46 +01:00
|
|
|
|
2023-03-14 07:17:44 +01:00
|
|
|
this.player.on('play', async () => {
|
2022-05-23 03:48:49 +02:00
|
|
|
if ('mediaSession' in navigator) {
|
|
|
|
navigator.mediaSession.playbackState = 'playing'
|
|
|
|
}
|
|
|
|
|
2022-09-15 10:59:09 +02:00
|
|
|
if (process.env.IS_ELECTRON) {
|
2021-05-22 01:49:48 +02:00
|
|
|
const { ipcRenderer } = require('electron')
|
|
|
|
this.powerSaveBlocker =
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
await ipcRenderer.invoke(IpcChannels.START_POWER_SAVE_BLOCKER)
|
2021-01-11 21:45:46 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-03-14 07:17:44 +01:00
|
|
|
this.player.on('pause', () => {
|
2022-05-23 03:48:49 +02:00
|
|
|
if ('mediaSession' in navigator) {
|
|
|
|
navigator.mediaSession.playbackState = 'paused'
|
|
|
|
}
|
|
|
|
|
2023-05-23 13:37:44 +02:00
|
|
|
this.stopPowerSaveBlocker()
|
2021-01-11 21:45:46 +01:00
|
|
|
})
|
2021-06-10 21:35:00 +02:00
|
|
|
|
2022-02-19 23:17:58 +01:00
|
|
|
this.player.on(this.statsModalEventName, () => {
|
|
|
|
if (this.showStatsModal) {
|
|
|
|
this.statsModal.open()
|
|
|
|
this.player.controls(true)
|
|
|
|
this.statsModal.contentEl().innerHTML = this.getFormattedStats()
|
|
|
|
} else {
|
|
|
|
this.statsModal.close()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.player.on('timeupdate', () => {
|
|
|
|
if (this.format === 'dash') {
|
|
|
|
this.playerStats = this.player.tech({ IWillNotUseThisInPlugins: true }).vhs.stats
|
|
|
|
this.updateStatsContent()
|
|
|
|
}
|
2022-09-29 22:01:54 +02:00
|
|
|
this.$emit('timeupdate')
|
2022-02-19 23:17:58 +01:00
|
|
|
})
|
|
|
|
|
2021-06-10 21:35:00 +02:00
|
|
|
this.player.textTrackSettings.on('modalclose', (_) => {
|
|
|
|
const settings = this.player.textTrackSettings.getValues()
|
|
|
|
this.updateDefaultCaptionSettings(JSON.stringify(settings))
|
|
|
|
})
|
2022-02-19 23:17:58 +01:00
|
|
|
|
|
|
|
// right click menu
|
2022-09-15 10:59:09 +02:00
|
|
|
if (process.env.IS_ELECTRON) {
|
2022-02-19 23:17:58 +01:00
|
|
|
const { ipcRenderer } = require('electron')
|
|
|
|
ipcRenderer.removeAllListeners('showVideoStatistics')
|
|
|
|
ipcRenderer.on('showVideoStatistics', (event) => {
|
|
|
|
this.toggleShowStatsModal()
|
|
|
|
})
|
|
|
|
}
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
2021-05-16 22:01:24 +02:00
|
|
|
},
|
|
|
|
|
2021-05-20 05:00:39 +02:00
|
|
|
initializeSponsorBlock() {
|
2022-10-08 22:23:14 +02:00
|
|
|
sponsorBlockSkipSegments(this.videoId, this.sponsorSkips.seekBar)
|
|
|
|
.then((skipSegments) => {
|
|
|
|
if (skipSegments.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
2021-05-20 05:00:39 +02:00
|
|
|
|
2022-10-08 22:23:14 +02:00
|
|
|
this.player.ready(() => {
|
|
|
|
this.player.on('timeupdate', () => {
|
|
|
|
this.skipSponsorBlocks(skipSegments)
|
|
|
|
})
|
2021-05-20 05:00:39 +02:00
|
|
|
|
2023-02-05 20:47:02 +01:00
|
|
|
this.player.on('seeking', () => {
|
|
|
|
// disabling sponsors auto skipping when the user manually seeks
|
|
|
|
this.skipSponsors = false
|
|
|
|
})
|
|
|
|
|
2022-10-08 22:23:14 +02:00
|
|
|
skipSegments.forEach(({
|
|
|
|
category,
|
|
|
|
segment: [startTime, endTime]
|
|
|
|
}) => {
|
|
|
|
this.addSponsorBlockMarker({
|
|
|
|
time: startTime,
|
|
|
|
duration: endTime - startTime,
|
|
|
|
color: 'var(--primary-color)',
|
|
|
|
category: category
|
|
|
|
})
|
2021-05-16 22:01:24 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
skipSponsorBlocks(skipSegments) {
|
|
|
|
const currentTime = this.player.currentTime()
|
2021-05-20 05:00:39 +02:00
|
|
|
const duration = this.player.duration()
|
2021-05-16 22:01:24 +02:00
|
|
|
let newTime = null
|
|
|
|
let skippedCategory = null
|
|
|
|
skipSegments.forEach(({ category, segment: [startTime, endTime] }) => {
|
|
|
|
if (startTime <= currentTime && currentTime < endTime) {
|
|
|
|
newTime = endTime
|
|
|
|
skippedCategory = category
|
|
|
|
}
|
|
|
|
})
|
2023-02-05 20:47:02 +01:00
|
|
|
if (this.skipSponsors && newTime !== null && Math.abs(duration - currentTime) > 0.500) {
|
2022-05-29 22:36:59 +02:00
|
|
|
if (this.sponsorSkips.autoSkip[skippedCategory]) {
|
2023-02-05 20:47:02 +01:00
|
|
|
if (this.skipCountdown === 0) {
|
|
|
|
if (this.sponsorBlockShowSkippedToast) {
|
|
|
|
this.showSkippedSponsorSegmentInformation(skippedCategory)
|
|
|
|
}
|
|
|
|
this.player.currentTime(newTime)
|
|
|
|
} else {
|
|
|
|
this.skipCountdown--
|
2022-05-29 22:36:59 +02:00
|
|
|
}
|
2021-05-16 22:01:24 +02:00
|
|
|
}
|
|
|
|
}
|
2023-02-05 20:47:02 +01:00
|
|
|
// restoring sponsors skipping default values
|
|
|
|
if (newTime === null && !this.skipSponsors) {
|
|
|
|
this.skipSponsors = true
|
|
|
|
this.skipCountdown = 1
|
|
|
|
}
|
2021-05-16 22:01:24 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
showSkippedSponsorSegmentInformation(category) {
|
|
|
|
const translatedCategory = this.sponsorBlockTranslatedCategory(category)
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(`${this.$t('Video.Skipped segment')} ${translatedCategory}`)
|
2021-05-16 22:01:24 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
sponsorBlockTranslatedCategory(category) {
|
|
|
|
switch (category) {
|
|
|
|
case 'sponsor':
|
|
|
|
return this.$t('Video.Sponsor Block category.sponsor')
|
|
|
|
case 'intro':
|
|
|
|
return this.$t('Video.Sponsor Block category.intro')
|
|
|
|
case 'outro':
|
|
|
|
return this.$t('Video.Sponsor Block category.outro')
|
|
|
|
case 'selfpromo':
|
|
|
|
return this.$t('Video.Sponsor Block category.self-promotion')
|
|
|
|
case 'interaction':
|
|
|
|
return this.$t('Video.Sponsor Block category.interaction')
|
|
|
|
case 'music_offtopic':
|
|
|
|
return this.$t('Video.Sponsor Block category.music offtopic')
|
2022-05-29 22:36:59 +02:00
|
|
|
case 'filler':
|
|
|
|
return this.$t('Video.Sponsor Block category.filler')
|
2021-05-16 22:01:24 +02:00
|
|
|
default:
|
|
|
|
console.error(`Unknown translation for SponsorBlock category ${category}`)
|
|
|
|
return category
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
addSponsorBlockMarker(marker) {
|
2022-09-29 22:01:54 +02:00
|
|
|
const markerDiv = videojs.dom.createEl('div')
|
2021-05-16 22:01:24 +02:00
|
|
|
|
2022-09-29 22:01:54 +02:00
|
|
|
markerDiv.title = this.sponsorBlockTranslatedCategory(marker.category)
|
2022-05-29 22:36:59 +02:00
|
|
|
markerDiv.className = `sponsorBlockMarker main${this.sponsorSkips.categoryData[marker.category].color}`
|
2022-08-20 14:49:41 +02:00
|
|
|
markerDiv.style.width = (marker.duration / this.lengthSeconds) * 100 + '%'
|
|
|
|
markerDiv.style.marginLeft = (marker.time / this.lengthSeconds) * 100 + '%'
|
2022-09-29 22:01:54 +02:00
|
|
|
|
|
|
|
this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv)
|
|
|
|
},
|
|
|
|
|
|
|
|
addChapterMarker(chapter) {
|
|
|
|
const markerDiv = videojs.dom.createEl('div')
|
|
|
|
|
|
|
|
markerDiv.title = chapter.title
|
|
|
|
markerDiv.className = 'chapterMarker'
|
2022-11-20 22:46:34 +01:00
|
|
|
markerDiv.style.marginLeft = `calc(${(chapter.startSeconds / this.lengthSeconds) * 100}% - 1px)`
|
2021-05-16 22:01:24 +02:00
|
|
|
|
|
|
|
this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv)
|
2020-02-18 21:59:01 +01:00
|
|
|
},
|
|
|
|
|
2021-01-15 05:20:42 +01:00
|
|
|
checkAspectRatio() {
|
|
|
|
const videoWidth = this.player.videoWidth()
|
|
|
|
const videoHeight = this.player.videoHeight()
|
|
|
|
|
2021-01-21 16:28:45 +01:00
|
|
|
if ((videoWidth - videoHeight) <= 240) {
|
2021-01-15 05:20:42 +01:00
|
|
|
this.player.fluid(false)
|
|
|
|
this.player.aspectRatio('16:9')
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-06-14 19:15:46 +02:00
|
|
|
updateVolume: function (_event) {
|
|
|
|
// https://docs.videojs.com/html5#volume
|
2023-03-08 19:22:45 +01:00
|
|
|
if (sessionStorage.getItem('muted') === 'false' && this.player.volume() === 0) {
|
|
|
|
// If video is muted by dragging volume slider, it doesn't change 'muted' in sessionStorage to true
|
|
|
|
// hence compare it with 'false' and set volume to defaultVolume.
|
|
|
|
const volume = parseFloat(sessionStorage.getItem('defaultVolume'))
|
|
|
|
const muted = true
|
|
|
|
sessionStorage.setItem('volume', volume)
|
|
|
|
sessionStorage.setItem('muted', muted)
|
|
|
|
} else {
|
|
|
|
// If volume isn't muted by dragging the slider, muted and volume values are carried over to next video.
|
|
|
|
const volume = this.player.volume()
|
|
|
|
const muted = this.player.muted()
|
|
|
|
sessionStorage.setItem('volume', volume)
|
|
|
|
sessionStorage.setItem('muted', muted)
|
|
|
|
}
|
2020-03-01 04:37:02 +01:00
|
|
|
},
|
|
|
|
|
2020-10-27 20:20:41 +01:00
|
|
|
mouseScrollVolume: function (event) {
|
2021-09-05 21:39:44 +02:00
|
|
|
if (event.target && !event.currentTarget.querySelector('.vjs-menu:hover')) {
|
2020-10-27 20:20:41 +01:00
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
if (this.player.muted() && event.wheelDelta > 0) {
|
|
|
|
this.player.muted(false)
|
|
|
|
this.player.volume(0)
|
|
|
|
}
|
|
|
|
|
2021-11-24 22:52:56 +01:00
|
|
|
if (!event.ctrlKey && !event.metaKey) {
|
|
|
|
if (!this.player.muted()) {
|
|
|
|
if (event.wheelDelta > 0) {
|
|
|
|
this.changeVolume(0.05)
|
|
|
|
} else if (event.wheelDelta < 0) {
|
|
|
|
this.changeVolume(-0.05)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-08-25 17:41:56 +02:00
|
|
|
/**
|
|
|
|
* @param {WheelEvent} event
|
|
|
|
*/
|
2021-11-24 22:52:56 +01:00
|
|
|
mouseScrollPlaybackRate: function (event) {
|
|
|
|
if (event.target && !event.currentTarget.querySelector('.vjs-menu:hover')) {
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
2023-08-25 17:41:56 +02:00
|
|
|
// Only stop page scrolling when Cmd/Ctrl pressed
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
if (event.deltaY > 0) {
|
2021-11-24 22:52:56 +01:00
|
|
|
this.changePlayBackRate(0.05)
|
2023-08-25 17:41:56 +02:00
|
|
|
} else if (event.deltaY < 0) {
|
2021-11-24 22:52:56 +01:00
|
|
|
this.changePlayBackRate(-0.05)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-12-22 16:21:32 +01:00
|
|
|
mouseScrollSkip: function (event) {
|
2022-12-25 13:06:18 +01:00
|
|
|
// Avoid doing both
|
|
|
|
if ((event.ctrlKey || event.metaKey) && this.videoPlaybackRateMouseScroll) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-12-22 16:21:32 +01:00
|
|
|
// ensure that the mouse is over the player
|
|
|
|
if (event.target && (event.target.matches('.vjs-tech') || event.target.matches('.ftVideoPlayer'))) {
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
if (event.wheelDelta > 0) {
|
|
|
|
this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate())
|
|
|
|
}
|
|
|
|
if (event.wheelDelta < 0) {
|
|
|
|
this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-11-24 22:52:56 +01:00
|
|
|
handlePlayerClick: function (event) {
|
|
|
|
if (event.target.matches('.ftVideoPlayer')) {
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
|
|
|
this.player.playbackRate(this.defaultPlayback)
|
|
|
|
} else {
|
|
|
|
if (this.player.paused() || !this.player.hasStarted()) {
|
2023-07-17 14:32:39 +02:00
|
|
|
this.playVideo()
|
2021-11-24 22:52:56 +01:00
|
|
|
} else {
|
|
|
|
this.player.pause()
|
2020-10-27 20:20:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-05-20 14:48:15 +02:00
|
|
|
/**
|
|
|
|
* @param {string} trackId
|
|
|
|
*/
|
|
|
|
changeAudioTrack: function (trackId) {
|
|
|
|
// changing the player sources resets it, so we need to store the values that get reset,
|
|
|
|
// before we change the sources and restore them afterwards
|
|
|
|
const isPaused = this.player.paused()
|
|
|
|
const currentTime = this.player.currentTime()
|
|
|
|
const playbackRate = this.player.playbackRate()
|
|
|
|
const selectedQualityIndex = this.player.currentSources().findIndex(quality => quality.selected)
|
|
|
|
|
|
|
|
const newSourceList = this.audioTracks.find(audioTrack => audioTrack.id === trackId).sourceList
|
|
|
|
|
|
|
|
// video.js doesn't pick up changes to the sources in the HTML after it's initial load
|
|
|
|
// updating the sources of an existing player requires calling player.src() instead
|
|
|
|
// which accepts a different object that what use for the html sources
|
|
|
|
|
|
|
|
const newSources = newSourceList.map((source, index) => {
|
|
|
|
return {
|
|
|
|
src: source.url,
|
|
|
|
type: source.type,
|
|
|
|
label: source.qualityLabel,
|
|
|
|
selected: index === selectedQualityIndex
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.player.one('canplay', () => {
|
|
|
|
this.player.currentTime(currentTime)
|
|
|
|
this.player.playbackRate(playbackRate)
|
|
|
|
|
|
|
|
// need to call play to restore the player state, even if we want to pause afterwards
|
2023-07-17 14:32:39 +02:00
|
|
|
this.playVideo(() => {
|
2023-05-20 14:48:15 +02:00
|
|
|
if (isPaused) { this.player.pause() }
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
this.player.src(newSources)
|
|
|
|
},
|
|
|
|
|
2020-02-18 21:59:01 +01:00
|
|
|
determineFormatType: function () {
|
2020-02-19 04:31:10 +01:00
|
|
|
if (this.format === 'dash') {
|
2020-02-18 21:59:01 +01:00
|
|
|
this.enableDashFormat()
|
|
|
|
} else {
|
|
|
|
this.enableLegacyFormat()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-09-26 23:03:42 +02:00
|
|
|
determineDefaultQualityLegacy: function () {
|
2020-05-24 23:46:26 +02:00
|
|
|
if (this.useDash) {
|
2020-09-25 04:35:13 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.sourceList.length === 0) {
|
|
|
|
return ''
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof (this.sourceList[0].qualityLabel) === 'number') {
|
|
|
|
return ''
|
2020-05-24 23:46:26 +02:00
|
|
|
}
|
|
|
|
|
2020-10-05 03:20:30 +02:00
|
|
|
if (this.sourceList[this.sourceList.length - 1].qualityLabel === this.$t('Video.Audio.Low')) {
|
|
|
|
this.selectedDefaultQuality = this.sourceList[0].qualityLabel
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-26 23:03:42 +02:00
|
|
|
let defaultQuality = this.defaultQuality
|
|
|
|
|
|
|
|
if (defaultQuality === 'auto') {
|
|
|
|
defaultQuality = 720
|
|
|
|
}
|
|
|
|
|
2020-09-25 04:35:13 +02:00
|
|
|
let maxAvailableQuality = parseInt(this.sourceList[this.sourceList.length - 1].qualityLabel.replace(/p|k/, ''))
|
|
|
|
|
|
|
|
if (maxAvailableQuality === 4) {
|
|
|
|
maxAvailableQuality = 2160
|
|
|
|
}
|
|
|
|
|
|
|
|
if (maxAvailableQuality === 8) {
|
|
|
|
maxAvailableQuality = 4320
|
|
|
|
}
|
|
|
|
|
2020-09-26 23:03:42 +02:00
|
|
|
if (maxAvailableQuality < defaultQuality) {
|
2020-09-25 04:35:13 +02:00
|
|
|
this.selectedDefaultQuality = this.sourceList[this.sourceList.length - 1].qualityLabel
|
|
|
|
}
|
|
|
|
|
|
|
|
const reversedList = [].concat(this.sourceList).reverse()
|
|
|
|
|
|
|
|
reversedList.forEach((source, index) => {
|
|
|
|
let qualityNumber = parseInt(source.qualityLabel.replace(/p|k/, ''))
|
|
|
|
if (qualityNumber === 4) {
|
|
|
|
qualityNumber = 2160
|
2020-05-17 22:12:58 +02:00
|
|
|
}
|
2020-09-25 04:35:13 +02:00
|
|
|
if (qualityNumber === 8) {
|
|
|
|
qualityNumber = 4320
|
|
|
|
}
|
|
|
|
|
2020-09-26 23:03:42 +02:00
|
|
|
if (defaultQuality === qualityNumber) {
|
2020-09-26 18:10:56 +02:00
|
|
|
this.selectedDefaultQuality = source.qualityLabel
|
|
|
|
}
|
|
|
|
|
2020-09-25 04:35:13 +02:00
|
|
|
if (index < (this.sourceList.length - 1)) {
|
|
|
|
let upperQualityNumber = parseInt(reversedList[index + 1].qualityLabel.replace(/p|k/, ''))
|
|
|
|
if (upperQualityNumber === 4) {
|
|
|
|
upperQualityNumber = 2160
|
|
|
|
}
|
|
|
|
if (upperQualityNumber === 8) {
|
|
|
|
upperQualityNumber = 4320
|
|
|
|
}
|
2020-09-26 23:03:42 +02:00
|
|
|
if (defaultQuality >= qualityNumber && defaultQuality < upperQualityNumber) {
|
2020-09-25 04:35:13 +02:00
|
|
|
this.selectedDefaultQuality = source.qualityLabel
|
|
|
|
}
|
2020-09-26 23:03:42 +02:00
|
|
|
} else if (qualityNumber <= defaultQuality) {
|
2020-09-25 04:35:13 +02:00
|
|
|
this.selectedDefaultQuality = source.qualityLabel
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (this.selectedDefaultQuality === '') {
|
|
|
|
this.selectedDefaultQuality = this.sourceList[this.sourceList.length - 1].qualityLabel
|
2020-05-17 22:12:58 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-09-26 23:03:42 +02:00
|
|
|
determineDefaultQualityDash: function () {
|
2023-03-23 01:22:20 +01:00
|
|
|
// TODO add settings and filtering for 60fps and HDR
|
2020-09-26 23:03:42 +02:00
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
if (this.defaultQuality === 'auto') {
|
|
|
|
this.selectedResolution = 'auto'
|
|
|
|
this.selectedFPS = 'auto'
|
|
|
|
this.selectedBitrate = 'auto'
|
|
|
|
this.selectedMimeType = 'auto'
|
|
|
|
} else {
|
|
|
|
const videoFormats = this.adaptiveFormats.filter(format => {
|
|
|
|
return (format.mimeType || format.type).startsWith('video') &&
|
|
|
|
typeof format.height === 'number'
|
2021-05-15 21:08:41 +02:00
|
|
|
})
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
// Select the quality that is identical to the users chosen default quality if it's available
|
|
|
|
// otherwise select the next lowest quality
|
2021-05-17 03:40:34 +02:00
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
let formatsToTest = videoFormats.filter(format => {
|
|
|
|
// For short videos (or vertical videos?)
|
|
|
|
// Height > width (e.g. H: 1280, W: 720)
|
|
|
|
if (typeof format.width === 'number' && format.height > format.width) {
|
|
|
|
return format.width === this.defaultQuality
|
2021-05-17 03:40:34 +02:00
|
|
|
}
|
2023-03-23 01:22:20 +01:00
|
|
|
|
2021-05-17 03:40:34 +02:00
|
|
|
return format.height === this.defaultQuality
|
|
|
|
})
|
|
|
|
|
|
|
|
if (formatsToTest.length === 0) {
|
2023-03-23 01:22:20 +01:00
|
|
|
formatsToTest = videoFormats.filter(format => {
|
|
|
|
// For short videos (or vertical videos?)
|
|
|
|
// Height > width (e.g. H: 1280, W: 720)
|
|
|
|
if (typeof format.width === 'number' && format.height > format.width) {
|
|
|
|
return format.width < this.defaultQuality
|
|
|
|
}
|
|
|
|
|
2021-05-17 03:40:34 +02:00
|
|
|
return format.height < this.defaultQuality
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
formatsToTest.sort((a, b) => {
|
2021-05-17 03:40:34 +02:00
|
|
|
if (a.height === b.height) {
|
2023-03-23 01:22:20 +01:00
|
|
|
// Higher bitrate for video formats with HDR qualities
|
|
|
|
// `height` and `fps` are the same but `bitrate` would be higher
|
2021-05-17 03:40:34 +02:00
|
|
|
return b.bitrate - a.bitrate
|
|
|
|
} else {
|
|
|
|
return b.height - a.height
|
|
|
|
}
|
|
|
|
})
|
2020-09-26 23:03:42 +02:00
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
const selectedFormat = formatsToTest[0]
|
2023-08-26 18:20:55 +02:00
|
|
|
this.currentAdaptiveFormat = selectedFormat
|
2023-03-23 01:22:20 +01:00
|
|
|
this.selectedBitrate = selectedFormat.bitrate
|
|
|
|
this.selectedResolution = `${selectedFormat.width}x${selectedFormat.height}`
|
|
|
|
this.selectedFPS = selectedFormat.fps
|
|
|
|
this.selectedMimeType = selectedFormat.mimeType || selectedFormat.type
|
|
|
|
}
|
2021-03-12 22:09:08 +01:00
|
|
|
},
|
|
|
|
|
2021-05-15 21:08:41 +02:00
|
|
|
setDashQualityLevel: function (bitrate) {
|
2023-03-23 01:22:20 +01:00
|
|
|
if (bitrate === this.selectedBitrate) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-15 21:08:41 +02:00
|
|
|
let adaptiveFormat = null
|
|
|
|
|
|
|
|
if (bitrate !== 'auto') {
|
|
|
|
adaptiveFormat = this.activeAdaptiveFormats.find((format) => {
|
|
|
|
return format.bitrate === bitrate
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-17 03:40:34 +02:00
|
|
|
let qualityLabel = adaptiveFormat ? adaptiveFormat.qualityLabel : ''
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
const qualityLevels = Array.from(this.player.qualityLevels())
|
|
|
|
if (bitrate === 'auto') {
|
|
|
|
qualityLevels.forEach(ql => {
|
2021-05-15 21:08:41 +02:00
|
|
|
ql.enabled = true
|
2023-03-23 01:22:20 +01:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
const previousBitrate = this.selectedBitrate
|
|
|
|
|
|
|
|
// if it was previously set to a specific quality we can disable just that and enable just the new one
|
|
|
|
// if it was previously set to auto, it means all qualitylevels were enabled, so we need to disable them
|
|
|
|
|
|
|
|
if (previousBitrate !== 'auto') {
|
|
|
|
const qualityLevel = qualityLevels.find(ql => bitrate === ql.bitrate)
|
|
|
|
qualityLevel.enabled = true
|
|
|
|
|
|
|
|
if (qualityLabel === '') {
|
|
|
|
qualityLabel = qualityLevel.height + 'p'
|
2021-05-17 03:40:34 +02:00
|
|
|
}
|
2023-03-23 01:22:20 +01:00
|
|
|
|
|
|
|
qualityLevels.find(ql => previousBitrate === ql.bitrate).enabled = false
|
2021-05-15 21:08:41 +02:00
|
|
|
} else {
|
2023-03-23 01:22:20 +01:00
|
|
|
qualityLevels.forEach(ql => {
|
|
|
|
if (bitrate === ql.bitrate) {
|
|
|
|
ql.enabled = true
|
|
|
|
if (qualityLabel === '') {
|
|
|
|
qualityLabel = ql.height + 'p'
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ql.enabled = false
|
|
|
|
}
|
|
|
|
})
|
2021-05-15 21:08:41 +02:00
|
|
|
}
|
2023-03-23 01:22:20 +01:00
|
|
|
}
|
2021-05-15 21:08:41 +02:00
|
|
|
|
2021-05-17 03:40:34 +02:00
|
|
|
const selectedQuality = bitrate === 'auto' ? 'auto' : qualityLabel
|
2021-05-15 21:08:41 +02:00
|
|
|
|
|
|
|
const qualityElement = document.getElementById('vjs-current-quality')
|
2023-08-26 18:20:55 +02:00
|
|
|
// Include resolution of previous selection
|
|
|
|
// If new auto resolution is the same as the previous resolution
|
|
|
|
// the qualityLevels on 'change' event will not be called
|
|
|
|
qualityElement.innerText = (selectedQuality === 'auto') ? `auto ${this.currentAdaptiveFormat.qualityLabel}` : selectedQuality
|
2021-05-15 21:08:41 +02:00
|
|
|
this.selectedQuality = selectedQuality
|
|
|
|
|
2022-02-19 23:17:58 +01:00
|
|
|
if (selectedQuality !== 'auto') {
|
|
|
|
this.selectedResolution = `${adaptiveFormat.width}x${adaptiveFormat.height}`
|
|
|
|
this.selectedFPS = adaptiveFormat.fps
|
|
|
|
this.selectedBitrate = adaptiveFormat.bitrate
|
|
|
|
this.selectedMimeType = adaptiveFormat.mimeType
|
2023-08-26 18:20:55 +02:00
|
|
|
this.currentAdaptiveFormat = adaptiveFormat
|
2022-02-19 23:17:58 +01:00
|
|
|
} else {
|
2023-08-26 18:20:55 +02:00
|
|
|
// Default auto values to use previous selected values in case the adaptive format doesn't change
|
|
|
|
this.autoResolution = this.selectedResolution
|
|
|
|
this.autoFPS = this.selectedFPS
|
|
|
|
this.autoBitrate = this.selectedBitrate
|
|
|
|
this.autoMimeType = this.selectedMimeType
|
|
|
|
|
2022-02-19 23:17:58 +01:00
|
|
|
this.selectedResolution = 'auto'
|
|
|
|
this.selectedFPS = 'auto'
|
|
|
|
this.selectedBitrate = 'auto'
|
|
|
|
this.selectedMimeType = 'auto'
|
|
|
|
}
|
|
|
|
|
2022-10-05 10:25:50 +02:00
|
|
|
const qualityItems = document.querySelectorAll('.quality-item')
|
2021-05-15 21:08:41 +02:00
|
|
|
|
|
|
|
qualityItems.forEach((item) => {
|
2022-10-05 10:25:50 +02:00
|
|
|
item.classList.remove('quality-selected')
|
|
|
|
const qualityText = item.querySelector('.vjs-menu-item-text')
|
2021-05-15 21:08:41 +02:00
|
|
|
if (qualityText.innerText === selectedQuality.toLowerCase()) {
|
2022-10-05 10:25:50 +02:00
|
|
|
item.classList.add('quality-selected')
|
2021-05-15 21:08:41 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
/* if (this.selectedQuality === qualityLevel && this.using60Fps === is60Fps) {
|
2021-03-12 22:09:08 +01:00
|
|
|
return
|
|
|
|
}
|
2021-04-30 23:18:45 +02:00
|
|
|
let foundSelectedQuality = false
|
|
|
|
this.using60Fps = is60Fps
|
2021-03-12 22:09:08 +01:00
|
|
|
this.player.qualityLevels().levels_.sort((a, b) => {
|
2021-04-30 23:18:45 +02:00
|
|
|
if (a.height === b.height) {
|
|
|
|
return a.bitrate - b.bitrate
|
|
|
|
} else {
|
|
|
|
return a.height - b.height
|
|
|
|
}
|
2021-03-12 22:09:08 +01:00
|
|
|
}).forEach((ql, index, arr) => {
|
2021-04-30 23:18:45 +02:00
|
|
|
if (foundSelectedQuality) {
|
|
|
|
ql.enabled = false
|
|
|
|
ql.enabled_(false)
|
|
|
|
} else if (qualityLevel === 'auto') {
|
|
|
|
ql.enabled = true
|
|
|
|
ql.enabled_(true)
|
|
|
|
} else if (ql.height === qualityLevel) {
|
2020-09-26 23:03:42 +02:00
|
|
|
ql.enabled = true
|
2021-03-12 22:09:08 +01:00
|
|
|
ql.enabled_(true)
|
2021-04-30 23:18:45 +02:00
|
|
|
foundSelectedQuality = true
|
|
|
|
|
|
|
|
let lowerQuality
|
|
|
|
let higherQuality
|
|
|
|
|
|
|
|
if ((index - 1) !== -1) {
|
|
|
|
lowerQuality = arr[index - 1]
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((index + 1) < arr.length) {
|
|
|
|
higherQuality = arr[index + 1]
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof (lowerQuality) !== 'undefined' && lowerQuality.height === ql.height && lowerQuality.bitrate < ql.bitrate && !is60Fps) {
|
|
|
|
ql.enabled = false
|
|
|
|
ql.enabled_(false)
|
|
|
|
foundSelectedQuality = false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof (higherQuality) !== 'undefined' && higherQuality.height === ql.height && higherQuality.bitrate > ql.bitrate && is60Fps) {
|
|
|
|
ql.enabled = false
|
|
|
|
ql.enabled_(false)
|
|
|
|
foundSelectedQuality = false
|
|
|
|
}
|
2020-09-26 23:03:42 +02:00
|
|
|
} else {
|
|
|
|
ql.enabled = false
|
2021-03-12 22:09:08 +01:00
|
|
|
ql.enabled_(false)
|
2020-09-26 23:03:42 +02:00
|
|
|
}
|
|
|
|
})
|
2021-03-12 22:09:08 +01:00
|
|
|
|
2021-04-30 23:18:45 +02:00
|
|
|
let selectedQuality = qualityLevel
|
|
|
|
|
|
|
|
if (selectedQuality !== 'auto' && is60Fps) {
|
|
|
|
selectedQuality = selectedQuality + 'p60'
|
|
|
|
} else if (selectedQuality !== 'auto') {
|
|
|
|
selectedQuality = selectedQuality + 'p'
|
|
|
|
}
|
2021-03-12 22:09:08 +01:00
|
|
|
|
|
|
|
const qualityElement = document.getElementById('vjs-current-quality')
|
|
|
|
qualityElement.innerText = selectedQuality
|
|
|
|
this.selectedQuality = qualityLevel
|
|
|
|
|
|
|
|
const qualityItems = $('.quality-item').get()
|
|
|
|
|
|
|
|
$('.quality-item').removeClass('quality-selected')
|
|
|
|
|
|
|
|
qualityItems.forEach((item) => {
|
|
|
|
const qualityText = $(item).find('.vjs-menu-item-text').get(0)
|
|
|
|
if (qualityText.innerText === selectedQuality) {
|
|
|
|
$(item).addClass('quality-selected')
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// const currentTime = this.player.currentTime()
|
|
|
|
|
|
|
|
// this.player.currentTime(0)
|
2021-05-15 21:08:41 +02:00
|
|
|
// this.player.currentTime(currentTime) */
|
2020-09-26 23:03:42 +02:00
|
|
|
},
|
|
|
|
|
2020-02-18 21:59:01 +01:00
|
|
|
enableDashFormat: function () {
|
|
|
|
if (this.dashSrc === null) {
|
2022-09-23 03:04:10 +02:00
|
|
|
console.warn('No dash format available.')
|
2020-02-18 21:59:01 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.useDash = true
|
|
|
|
this.useHls = false
|
|
|
|
this.activeSourceList = this.dashSrc
|
|
|
|
|
2020-04-22 04:59:09 +02:00
|
|
|
setTimeout(this.initializePlayer, 100)
|
2020-02-18 21:59:01 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
enableLegacyFormat: function () {
|
|
|
|
if (this.sourceList.length === 0) {
|
2022-09-23 03:04:10 +02:00
|
|
|
console.error('No sources available')
|
2020-02-18 21:59:01 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.useDash = false
|
|
|
|
this.useHls = false
|
2023-06-12 17:06:08 +02:00
|
|
|
this.activeSourceList = (this.proxyVideos || !process.env.IS_ELECTRON)
|
|
|
|
// use map here to return slightly different list without modifying original
|
|
|
|
? this.sourceList.map((source) => {
|
|
|
|
return {
|
|
|
|
...source,
|
|
|
|
url: getProxyUrl(source.url)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
: this.sourceList
|
2020-02-18 21:59:01 +01:00
|
|
|
|
|
|
|
setTimeout(this.initializePlayer, 100)
|
2020-02-20 21:58:21 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
togglePlayPause: function () {
|
|
|
|
if (this.player.paused()) {
|
2023-07-17 14:32:39 +02:00
|
|
|
this.playVideo()
|
2020-02-20 21:58:21 +01:00
|
|
|
} else {
|
|
|
|
this.player.pause()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
changeDurationBySeconds: function (seconds) {
|
|
|
|
const currentTime = this.player.currentTime()
|
|
|
|
const newTime = currentTime + seconds
|
|
|
|
|
|
|
|
if (newTime < 0) {
|
|
|
|
this.player.currentTime(0)
|
|
|
|
} else if (newTime > this.player.duration) {
|
|
|
|
this.player.currentTime(this.player.duration)
|
|
|
|
} else {
|
|
|
|
this.player.currentTime(newTime)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
changePlayBackRate: function (rate) {
|
2021-11-24 22:52:56 +01:00
|
|
|
const newPlaybackRate = (this.player.playbackRate() + rate).toFixed(2)
|
2020-02-20 21:58:21 +01:00
|
|
|
|
2022-04-11 00:00:47 +02:00
|
|
|
if (newPlaybackRate >= this.videoPlaybackRateInterval && newPlaybackRate <= this.maxVideoPlaybackRate) {
|
2020-02-20 21:58:21 +01:00
|
|
|
this.player.playbackRate(newPlaybackRate)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-11-13 23:29:41 +01:00
|
|
|
framebyframe: function (step) {
|
2023-04-17 08:51:47 +02:00
|
|
|
if (this.format === 'audio') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-13 23:29:41 +01:00
|
|
|
this.player.pause()
|
2023-04-17 08:51:47 +02:00
|
|
|
|
|
|
|
let fps
|
|
|
|
if (this.useDash) {
|
|
|
|
const qualityLevels = this.player.qualityLevels()
|
|
|
|
fps = qualityLevels[qualityLevels.selectedIndex].frameRate
|
|
|
|
} else {
|
|
|
|
// legacy formats
|
|
|
|
const currentPlayerSourceUrl = this.player.currentSource().src
|
|
|
|
fps = this.sourceList.find(source => source.url === currentPlayerSourceUrl)?.fps
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isNaN(fps)) {
|
|
|
|
fps = 30
|
2020-11-13 23:29:41 +01:00
|
|
|
}
|
2021-10-07 13:44:39 +02:00
|
|
|
|
2020-11-13 23:29:41 +01:00
|
|
|
// The 3 lines below were taken from the videojs-framebyframe node module by Helena Rasche
|
|
|
|
const frameTime = 1 / fps
|
|
|
|
const dist = frameTime * step
|
|
|
|
this.player.currentTime(this.player.currentTime() + dist)
|
|
|
|
},
|
|
|
|
|
2020-02-20 21:58:21 +01:00
|
|
|
changeVolume: function (volume) {
|
|
|
|
const currentVolume = this.player.volume()
|
|
|
|
const newVolume = currentVolume + volume
|
|
|
|
|
|
|
|
if (newVolume < 0) {
|
|
|
|
this.player.volume(0)
|
|
|
|
} else if (newVolume > 1) {
|
|
|
|
this.player.volume(1)
|
|
|
|
} else {
|
|
|
|
this.player.volume(newVolume)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
toggleMute: function () {
|
|
|
|
if (this.player.muted()) {
|
|
|
|
this.player.muted(false)
|
|
|
|
} else {
|
|
|
|
this.player.muted(true)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
toggleCaptions: function () {
|
2023-05-23 14:28:01 +02:00
|
|
|
// skip videojs-http-streaming's segment-metadata track
|
|
|
|
// https://github.com/videojs/http-streaming#segment-metadata
|
|
|
|
const trackIndex = this.useDash ? 1 : 0
|
|
|
|
|
|
|
|
const tracks = this.player.textTracks()
|
2024-01-03 01:28:13 +01:00
|
|
|
|
|
|
|
// visually and semantically disable any other enabled tracks
|
|
|
|
for (let i = 0; i < tracks.length; ++i) {
|
|
|
|
if (i !== trackIndex && tracks[i].mode === 'showing') {
|
|
|
|
tracks[i].mode = 'disabled'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-23 14:28:01 +02:00
|
|
|
if (tracks.length > trackIndex) {
|
|
|
|
if (tracks[trackIndex].mode === 'showing') {
|
|
|
|
tracks[trackIndex].mode = 'disabled'
|
2020-02-20 21:58:21 +01:00
|
|
|
} else {
|
2023-05-23 14:28:01 +02:00
|
|
|
tracks[trackIndex].mode = 'showing'
|
2020-02-20 21:58:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-01-13 04:56:31 +01:00
|
|
|
createLoopButton: function () {
|
2023-01-14 16:01:27 +01:00
|
|
|
const toggleVideoLoop = this.toggleVideoLoop
|
|
|
|
|
2021-01-13 04:56:31 +01:00
|
|
|
const VjsButton = videojs.getComponent('Button')
|
2023-01-14 16:01:27 +01:00
|
|
|
class loopButton extends VjsButton {
|
|
|
|
handleClick() {
|
|
|
|
toggleVideoLoop()
|
|
|
|
}
|
|
|
|
|
|
|
|
createControlTextEl(button) {
|
2022-09-29 15:15:30 +02:00
|
|
|
button.title = 'Toggle Loop'
|
|
|
|
|
|
|
|
const div = document.createElement('div')
|
|
|
|
|
|
|
|
div.id = 'loopButton'
|
2023-01-14 16:01:27 +01:00
|
|
|
div.className = 'vjs-icon-loop loop-white vjs-button'
|
2022-09-29 15:15:30 +02:00
|
|
|
|
|
|
|
button.appendChild(div)
|
|
|
|
|
|
|
|
return div
|
2021-01-13 04:56:31 +01:00
|
|
|
}
|
2023-01-14 16:01:27 +01:00
|
|
|
}
|
|
|
|
|
2021-01-13 04:56:31 +01:00
|
|
|
videojs.registerComponent('loopButton', loopButton)
|
|
|
|
},
|
|
|
|
|
2022-10-10 09:45:18 +02:00
|
|
|
toggleVideoLoop: function () {
|
2022-10-05 10:25:50 +02:00
|
|
|
const loopButton = document.getElementById('loopButton')
|
|
|
|
|
2021-01-13 04:56:31 +01:00
|
|
|
if (!this.player.loop()) {
|
2022-06-19 20:39:29 +02:00
|
|
|
const currentTheme = this.$store.state.settings.mainColor
|
2021-01-13 04:56:31 +01:00
|
|
|
|
2022-10-12 08:49:12 +02:00
|
|
|
const colorValue = colors.find(color => color.name === currentTheme).value
|
2021-01-13 04:56:31 +01:00
|
|
|
|
2022-10-12 08:49:12 +02:00
|
|
|
const themeTextColor = calculateColorLuminance(colorValue)
|
2021-01-13 04:56:31 +01:00
|
|
|
|
2022-10-05 10:25:50 +02:00
|
|
|
loopButton.classList.add('vjs-icon-loop-active')
|
2021-01-13 04:56:31 +01:00
|
|
|
|
|
|
|
if (themeTextColor === '#000000') {
|
2022-10-05 10:25:50 +02:00
|
|
|
loopButton.classList.add('loop-black')
|
|
|
|
loopButton.classList.remove('loop-white')
|
2021-01-13 04:56:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
this.player.loop(true)
|
|
|
|
} else {
|
2022-10-05 10:25:50 +02:00
|
|
|
loopButton.classList.remove('vjs-icon-loop-active')
|
|
|
|
loopButton.classList.remove('loop-black')
|
|
|
|
loopButton.classList.add('loop-white')
|
2021-01-13 04:56:31 +01:00
|
|
|
this.player.loop(false)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
createFullWindowButton: function () {
|
2023-01-14 16:01:27 +01:00
|
|
|
const toggleFullWindow = this.toggleFullWindow
|
|
|
|
|
2020-12-14 23:37:58 +01:00
|
|
|
const VjsButton = videojs.getComponent('Button')
|
2023-01-14 16:01:27 +01:00
|
|
|
class fullWindowButton extends VjsButton {
|
|
|
|
handleClick() {
|
|
|
|
toggleFullWindow()
|
|
|
|
}
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
createControlTextEl(button) {
|
2021-04-22 20:41:50 +02:00
|
|
|
// Add class name to button to be able to target it with CSS selector
|
2022-09-29 15:15:30 +02:00
|
|
|
button.classList.add('vjs-button-fullwindow')
|
|
|
|
button.title = 'Full Window'
|
|
|
|
|
|
|
|
const div = document.createElement('div')
|
|
|
|
div.id = 'fullwindow'
|
|
|
|
div.className = 'vjs-icon-fullwindow-enter vjs-button'
|
|
|
|
|
|
|
|
button.appendChild(div)
|
|
|
|
|
|
|
|
return div
|
2020-12-14 23:37:58 +01:00
|
|
|
}
|
2023-01-14 16:01:27 +01:00
|
|
|
}
|
|
|
|
|
2020-12-14 23:37:58 +01:00
|
|
|
videojs.registerComponent('fullWindowButton', fullWindowButton)
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
createToggleTheatreModeButton: function () {
|
2023-08-03 14:57:09 +02:00
|
|
|
if (!this.theatrePossible) {
|
2021-08-24 09:36:10 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-03 14:57:09 +02:00
|
|
|
const theatreModeActive = this.useTheatreMode ? ' vjs-icon-theatre-active' : ''
|
2021-08-24 09:36:10 +02:00
|
|
|
|
2023-01-14 16:01:27 +01:00
|
|
|
const toggleTheatreMode = this.toggleTheatreMode
|
|
|
|
|
2021-08-24 09:36:10 +02:00
|
|
|
const VjsButton = videojs.getComponent('Button')
|
2023-01-14 16:01:27 +01:00
|
|
|
class toggleTheatreModeButton extends VjsButton {
|
|
|
|
handleClick() {
|
|
|
|
toggleTheatreMode()
|
|
|
|
}
|
|
|
|
|
|
|
|
createControlTextEl(button) {
|
2022-09-29 15:15:30 +02:00
|
|
|
button.classList.add('vjs-button-theatre')
|
|
|
|
button.title = 'Toggle Theatre Mode'
|
|
|
|
|
|
|
|
const div = document.createElement('div')
|
|
|
|
div.id = 'toggleTheatreModeButton'
|
|
|
|
div.className = `vjs-icon-theatre-inactive${theatreModeActive} vjs-button`
|
|
|
|
|
|
|
|
button.appendChild(div)
|
|
|
|
|
|
|
|
return button
|
2021-08-24 09:36:10 +02:00
|
|
|
}
|
2023-01-14 16:01:27 +01:00
|
|
|
}
|
2021-08-24 09:36:10 +02:00
|
|
|
|
|
|
|
videojs.registerComponent('toggleTheatreModeButton', toggleTheatreModeButton)
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
toggleTheatreMode: function () {
|
2021-08-24 09:36:10 +02:00
|
|
|
if (!this.player.isFullscreen_) {
|
2022-10-05 10:25:50 +02:00
|
|
|
const toggleTheatreModeButton = document.getElementById('toggleTheatreModeButton')
|
2023-08-03 14:57:09 +02:00
|
|
|
if (!this.useTheatreMode) {
|
2022-10-05 10:25:50 +02:00
|
|
|
toggleTheatreModeButton.classList.add('vjs-icon-theatre-active')
|
2021-08-24 09:36:10 +02:00
|
|
|
} else {
|
2022-10-05 10:25:50 +02:00
|
|
|
toggleTheatreModeButton.classList.remove('vjs-icon-theatre-active')
|
2021-08-24 09:36:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-03 14:57:09 +02:00
|
|
|
this.$emit('toggle-theatre-mode')
|
2021-08-24 09:36:10 +02:00
|
|
|
},
|
|
|
|
|
2023-01-14 16:01:27 +01:00
|
|
|
createScreenshotButton: function () {
|
|
|
|
const takeScreenshot = this.takeScreenshot
|
|
|
|
|
2022-05-30 15:24:34 +02:00
|
|
|
const VjsButton = videojs.getComponent('Button')
|
2023-01-14 16:01:27 +01:00
|
|
|
class screenshotButton extends VjsButton {
|
|
|
|
handleClick() {
|
|
|
|
takeScreenshot()
|
2022-05-30 15:24:34 +02:00
|
|
|
const video = document.getElementsByTagName('video')[0]
|
|
|
|
video.focus()
|
|
|
|
video.blur()
|
2023-01-14 16:01:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
createControlTextEl(button) {
|
2022-09-29 15:15:30 +02:00
|
|
|
button.classList.add('vjs-hidden')
|
|
|
|
button.title = 'Screenshot'
|
|
|
|
|
|
|
|
const div = document.createElement('div')
|
|
|
|
div.id = 'screenshotButton'
|
|
|
|
div.className = 'vjs-icon-screenshot vjs-button'
|
|
|
|
|
|
|
|
button.appendChild(div)
|
|
|
|
|
|
|
|
return div
|
2022-05-30 15:24:34 +02:00
|
|
|
}
|
2023-01-14 16:01:27 +01:00
|
|
|
}
|
2022-05-30 15:24:34 +02:00
|
|
|
|
|
|
|
videojs.registerComponent('screenshotButton', screenshotButton)
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
toggleScreenshotButton: function () {
|
2022-09-29 15:15:30 +02:00
|
|
|
const button = document.getElementById('screenshotButton').parentNode
|
2022-05-30 15:24:34 +02:00
|
|
|
if (this.enableScreenshot && this.format !== 'audio') {
|
|
|
|
button.classList.remove('vjs-hidden')
|
|
|
|
} else {
|
|
|
|
button.classList.add('vjs-hidden')
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
takeScreenshot: async function () {
|
2022-05-30 15:24:34 +02:00
|
|
|
if (!this.enableScreenshot || this.format === 'audio') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const width = this.player.videoWidth()
|
|
|
|
const height = this.player.videoHeight()
|
|
|
|
if (width <= 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Need to set crossorigin="anonymous" for LegacyFormat on Invidious
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
|
|
|
|
const video = document.querySelector('video')
|
|
|
|
const canvas = document.createElement('canvas')
|
|
|
|
canvas.width = width
|
|
|
|
canvas.height = height
|
|
|
|
canvas.getContext('2d').drawImage(video, 0, 0)
|
|
|
|
|
|
|
|
const format = this.screenshotFormat
|
|
|
|
const mimeType = `image/${format === 'jpg' ? 'jpeg' : format}`
|
|
|
|
const imageQuality = format === 'jpg' ? this.screenshotQuality / 100 : 1
|
|
|
|
|
|
|
|
let filename
|
|
|
|
try {
|
|
|
|
filename = await this.parseScreenshotCustomFileName({
|
|
|
|
date: new Date(Date.now()),
|
|
|
|
playerTime: this.player.currentTime(),
|
|
|
|
videoId: this.videoId
|
|
|
|
})
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Parse failed: ${err.message}`)
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Screenshot Error', { error: err.message }))
|
2022-05-30 15:24:34 +02:00
|
|
|
canvas.remove()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let subDir = ''
|
2022-12-27 03:15:38 +01:00
|
|
|
if (filename.indexOf(path.sep) !== -1) {
|
|
|
|
const lastIndex = filename.lastIndexOf(path.sep)
|
2022-05-30 15:24:34 +02:00
|
|
|
subDir = filename.substring(0, lastIndex)
|
|
|
|
filename = filename.substring(lastIndex + 1)
|
|
|
|
}
|
|
|
|
const filenameWithExtension = `${filename}.${format}`
|
|
|
|
|
|
|
|
let dirPath
|
|
|
|
let filePath
|
|
|
|
if (this.screenshotAskPath) {
|
|
|
|
const wasPlaying = !this.player.paused()
|
|
|
|
if (wasPlaying) {
|
|
|
|
this.player.pause()
|
|
|
|
}
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
if (this.screenshotFolder === '' || !(await pathExists(this.screenshotFolder))) {
|
2022-12-06 12:37:44 +01:00
|
|
|
dirPath = await getPicturesPath()
|
2022-05-30 15:24:34 +02:00
|
|
|
} else {
|
|
|
|
dirPath = this.screenshotFolder
|
|
|
|
}
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: path.join(dirPath, filenameWithExtension),
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: format.toUpperCase(),
|
|
|
|
extensions: [format]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2022-10-25 16:44:18 +02:00
|
|
|
const response = await showSaveDialog(options)
|
2022-05-30 15:24:34 +02:00
|
|
|
if (wasPlaying) {
|
2023-07-17 14:32:39 +02:00
|
|
|
this.playVideo()
|
2022-05-30 15:24:34 +02:00
|
|
|
}
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
canvas.remove()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
filePath = response.filePath
|
|
|
|
if (!filePath.endsWith(`.${format}`)) {
|
|
|
|
filePath = `${filePath}.${format}`
|
|
|
|
}
|
|
|
|
|
|
|
|
dirPath = path.dirname(filePath)
|
|
|
|
this.updateScreenshotFolderPath(dirPath)
|
|
|
|
} else {
|
|
|
|
if (this.screenshotFolder === '') {
|
2022-12-06 12:37:44 +01:00
|
|
|
dirPath = path.join(await getPicturesPath(), 'Freetube', subDir)
|
2022-05-30 15:24:34 +02:00
|
|
|
} else {
|
|
|
|
dirPath = path.join(this.screenshotFolder, subDir)
|
|
|
|
}
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
if (!(await pathExists(dirPath))) {
|
2022-05-30 15:24:34 +02:00
|
|
|
try {
|
2022-12-29 02:19:48 +01:00
|
|
|
fs.mkdir(dirPath, { recursive: true })
|
2022-05-30 15:24:34 +02:00
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Screenshot Error', { error: err }))
|
2022-05-30 15:24:34 +02:00
|
|
|
canvas.remove()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
filePath = path.join(dirPath, filenameWithExtension)
|
|
|
|
}
|
|
|
|
|
|
|
|
canvas.toBlob((result) => {
|
|
|
|
result.arrayBuffer().then(ab => {
|
|
|
|
const arr = new Uint8Array(ab)
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
fs.writeFile(filePath, arr)
|
|
|
|
.then(() => {
|
|
|
|
showToast(this.$t('Screenshot Success', { filePath }))
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
2022-05-30 15:24:34 +02:00
|
|
|
console.error(err)
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Screenshot Error', { error: err }))
|
2022-12-29 02:19:48 +01:00
|
|
|
})
|
2022-05-30 15:24:34 +02:00
|
|
|
})
|
|
|
|
}, mimeType, imageQuality)
|
|
|
|
canvas.remove()
|
|
|
|
},
|
|
|
|
|
2021-03-12 22:09:08 +01:00
|
|
|
createDashQualitySelector: function (levels) {
|
2023-08-26 18:20:55 +02:00
|
|
|
const currentAdaptiveFormat = this.currentAdaptiveFormat
|
2023-01-14 16:01:27 +01:00
|
|
|
const adaptiveFormats = this.adaptiveFormats
|
|
|
|
const activeAdaptiveFormats = this.activeAdaptiveFormats
|
|
|
|
const setDashQualityLevel = this.setDashQualityLevel
|
2023-03-23 01:22:20 +01:00
|
|
|
const defaultQuality = this.defaultQuality
|
|
|
|
const defaultBitrate = this.selectedBitrate
|
2023-08-26 18:20:55 +02:00
|
|
|
const autoResolution = this.autoResolution
|
2023-01-14 16:01:27 +01:00
|
|
|
|
2021-03-12 22:09:08 +01:00
|
|
|
const VjsButton = videojs.getComponent('Button')
|
2023-01-14 16:01:27 +01:00
|
|
|
class dashQualitySelector extends VjsButton {
|
|
|
|
handleClick(event) {
|
2023-10-11 03:31:04 +02:00
|
|
|
if (!event.target.classList.contains('quality-item') && !event.target.classList.contains('vjs-menu-item-text')) {
|
|
|
|
return
|
|
|
|
}
|
2021-03-12 22:09:08 +01:00
|
|
|
const selectedQuality = event.target.innerText
|
2021-05-15 21:08:41 +02:00
|
|
|
const bitrate = selectedQuality === 'auto' ? 'auto' : parseInt(event.target.attributes.bitrate.value)
|
2023-01-14 16:01:27 +01:00
|
|
|
setDashQualityLevel(bitrate)
|
|
|
|
}
|
|
|
|
|
|
|
|
createControlTextEl(button) {
|
2021-03-12 22:09:08 +01:00
|
|
|
const beginningHtml = `<div class="vjs-quality-level-value">
|
|
|
|
<span id="vjs-current-quality">1080p</span>
|
|
|
|
</div>
|
|
|
|
<div class="vjs-quality-level-menu vjs-menu">
|
|
|
|
<ul class="vjs-menu-content" role="menu">`
|
|
|
|
const endingHtml = '</ul></div>'
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
const defaultIsAuto = defaultQuality === 'auto'
|
|
|
|
|
|
|
|
let qualityHtml = `<li class="vjs-menu-item quality-item ${defaultIsAuto ? 'quality-selected' : ''}" role="menuitemradio" tabindex="-1" aria-checked="false" aria-disabled="false">
|
2021-03-12 22:09:08 +01:00
|
|
|
<span class="vjs-menu-item-text">Auto</span>
|
|
|
|
<span class="vjs-control-text" aria-live="polite"></span>
|
|
|
|
</li>`
|
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
let currentQualityLabel
|
|
|
|
|
|
|
|
Array.from(levels).sort((a, b) => {
|
2021-04-30 23:18:45 +02:00
|
|
|
if (b.height === a.height) {
|
|
|
|
return b.bitrate - a.bitrate
|
|
|
|
} else {
|
|
|
|
return b.height - a.height
|
|
|
|
}
|
|
|
|
}).forEach((quality, index, array) => {
|
2021-05-17 03:40:34 +02:00
|
|
|
let fps
|
|
|
|
let qualityLabel
|
|
|
|
let bitrate
|
2021-05-15 21:08:41 +02:00
|
|
|
|
2023-01-14 16:01:27 +01:00
|
|
|
if (typeof adaptiveFormats !== 'undefined' && adaptiveFormats.length > 0) {
|
|
|
|
const adaptiveFormat = adaptiveFormats.find((format) => {
|
2021-05-17 03:40:34 +02:00
|
|
|
return format.bitrate === quality.bitrate
|
|
|
|
})
|
2021-05-15 21:08:41 +02:00
|
|
|
|
2022-02-25 23:51:02 +01:00
|
|
|
if (typeof adaptiveFormat === 'undefined') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-14 16:01:27 +01:00
|
|
|
activeAdaptiveFormats.push(adaptiveFormat)
|
2021-05-17 03:40:34 +02:00
|
|
|
|
2021-10-12 23:03:22 +02:00
|
|
|
fps = adaptiveFormat.fps ? adaptiveFormat.fps : 30
|
2021-05-17 03:40:34 +02:00
|
|
|
qualityLabel = adaptiveFormat.qualityLabel ? adaptiveFormat.qualityLabel : quality.height + 'p'
|
|
|
|
bitrate = quality.bitrate
|
|
|
|
} else {
|
|
|
|
fps = 30
|
|
|
|
qualityLabel = quality.height + 'p'
|
|
|
|
bitrate = quality.bitrate
|
|
|
|
}
|
2021-05-15 21:08:41 +02:00
|
|
|
|
2023-03-23 01:22:20 +01:00
|
|
|
const isSelected = !defaultIsAuto && bitrate === defaultBitrate
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
currentQualityLabel = qualityLabel
|
|
|
|
}
|
|
|
|
|
|
|
|
qualityHtml += `<li class="vjs-menu-item quality-item ${isSelected ? 'quality-selected' : ''}" role="menuitemradio" tabindex="-1" aria-checked="false" aria-disabled="false" fps="${fps}" bitrate="${bitrate}">
|
2021-05-15 21:08:41 +02:00
|
|
|
<span class="vjs-menu-item-text" fps="${fps}" bitrate="${bitrate}">${qualityLabel}</span>
|
|
|
|
<span class="vjs-control-text" aria-live="polite"></span>
|
|
|
|
</li>`
|
|
|
|
|
|
|
|
// Old logic, revert if needed.
|
|
|
|
/* let is60Fps = false
|
2021-04-30 23:18:45 +02:00
|
|
|
if (index < array.length - 1 && array[index + 1].height === quality.height) {
|
|
|
|
if (array[index + 1].bitrate < quality.bitrate) {
|
|
|
|
is60Fps = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const qualityText = is60Fps ? quality.height + 'p60' : quality.height + 'p'
|
2021-03-12 22:09:08 +01:00
|
|
|
qualityHtml = qualityHtml + `<li class="vjs-menu-item quality-item" role="menuitemradio" tabindex="-1" aria-checked="false aria-disabled="false">
|
2021-04-30 23:18:45 +02:00
|
|
|
<span class="vjs-menu-item-text">${qualityText}</span>
|
2021-03-12 22:09:08 +01:00
|
|
|
<span class="vjs-control-text" aria-live="polite"></span>
|
2021-05-15 21:08:41 +02:00
|
|
|
</li>` */
|
2021-03-12 22:09:08 +01:00
|
|
|
})
|
2022-09-10 16:14:18 +02:00
|
|
|
// the default width is 3em which is too narrow for qualitly labels with fps e.g. 1080p60
|
|
|
|
button.style.width = '4em'
|
2022-09-29 15:15:30 +02:00
|
|
|
button.title = 'Select Quality'
|
|
|
|
button.innerHTML = beginningHtml + qualityHtml + endingHtml
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
let autoQualityLabel = 'auto'
|
|
|
|
if (currentAdaptiveFormat) {
|
|
|
|
autoQualityLabel += ` ${currentAdaptiveFormat.qualityLabel}`
|
|
|
|
} else if (autoResolution !== '') {
|
|
|
|
autoQualityLabel += ` ${autoResolution.split('x')[1]}p`
|
|
|
|
}
|
|
|
|
|
|
|
|
// For default auto, it may select a resolution before generating the quality buttons
|
|
|
|
button.querySelector('#vjs-current-quality').innerText = defaultIsAuto ? autoQualityLabel : currentQualityLabel
|
2024-03-10 23:00:42 +01:00
|
|
|
const vjsMenu = button.querySelector('.vjs-menu')
|
|
|
|
let isTapping = false
|
|
|
|
button.addEventListener('touchstart', () => {
|
|
|
|
isTapping = true
|
|
|
|
})
|
|
|
|
button.addEventListener('touchmove', () => {
|
|
|
|
// if they are moving, they cannot be tapping
|
|
|
|
isTapping = false
|
|
|
|
})
|
|
|
|
button.addEventListener('touchend', (e) => {
|
|
|
|
if (isTapping) {
|
|
|
|
button.focus()
|
|
|
|
// make it easier to toggle the vjs-menu on touch (hover css is inconsistent w/ touch)
|
|
|
|
if (!e.target.classList.contains('quality-item') && !e.target.classList.contains('vjs-menu-item-text')) {
|
|
|
|
vjsMenu.classList.toggle('vjs-lock-showing')
|
|
|
|
} else {
|
|
|
|
// hide the quality selector on select (just like the other quality selectors do on mobile)
|
|
|
|
vjsMenu.classList.remove('vjs-lock-showing')
|
|
|
|
}
|
|
|
|
this.handleClick(e)
|
|
|
|
isTapping = false
|
|
|
|
}
|
|
|
|
})
|
|
|
|
button.addEventListener('focusout', () => {
|
|
|
|
// remove class which shows the selector
|
|
|
|
vjsMenu.classList.remove('vjs-lock-showing')
|
|
|
|
})
|
|
|
|
button.classList.add('dash-selector')
|
2023-03-23 01:22:20 +01:00
|
|
|
|
2022-09-29 15:15:30 +02:00
|
|
|
return button.children[0]
|
2021-03-12 22:09:08 +01:00
|
|
|
}
|
2023-01-14 16:01:27 +01:00
|
|
|
}
|
|
|
|
|
2021-03-12 22:09:08 +01:00
|
|
|
videojs.registerComponent('dashQualitySelector', dashQualitySelector)
|
|
|
|
this.player.controlBar.addChild('dashQualitySelector', {}, this.player.controlBar.children_.length - 1)
|
|
|
|
},
|
|
|
|
|
2021-09-20 04:12:14 +02:00
|
|
|
sortCaptions: function (captionList) {
|
|
|
|
return captionList.sort((captionA, captionB) => {
|
2023-01-07 02:52:59 +01:00
|
|
|
const aCode = captionA.language_code.split('-') // ex. [en,US]
|
|
|
|
const bCode = captionB.language_code.split('-')
|
|
|
|
const aName = (captionA.label) // ex: english (auto-generated)
|
|
|
|
const bName = (captionB.label)
|
2024-01-06 11:25:16 +01:00
|
|
|
const aIsAutotranslated = captionA.is_autotranslated
|
|
|
|
const bIsAutotranslated = captionB.is_autotranslated
|
2023-08-15 20:17:10 +02:00
|
|
|
const userLocale = this.currentLocale.split('-') // ex. [en,US]
|
2021-09-20 04:12:14 +02:00
|
|
|
if (aCode[0] === userLocale[0]) { // caption a has same language as user's locale
|
|
|
|
if (bCode[0] === userLocale[0]) { // caption b has same language as user's locale
|
|
|
|
if (bName.search('auto') !== -1) {
|
|
|
|
// prefer caption a: b is auto-generated captions
|
|
|
|
return -1
|
|
|
|
} else if (aName.search('auto') !== -1) {
|
|
|
|
// prefer caption b: a is auto-generated captions
|
|
|
|
return 1
|
2024-01-06 11:25:16 +01:00
|
|
|
} else if (bIsAutotranslated) {
|
|
|
|
// prefer caption a: b is auto-translated captions
|
|
|
|
return -1
|
|
|
|
} else if (aIsAutotranslated) {
|
|
|
|
// prefer caption b: a is auto-translated captions
|
|
|
|
return 1
|
2021-09-20 04:12:14 +02:00
|
|
|
} else if (aCode[1] === userLocale[1]) {
|
|
|
|
// prefer caption a: caption a has same county code as user's locale
|
|
|
|
return -1
|
|
|
|
} else if (bCode[1] === userLocale[1]) {
|
|
|
|
// prefer caption b: caption b has same county code as user's locale
|
|
|
|
return 1
|
|
|
|
} else if (aCode[1] === undefined) {
|
|
|
|
// prefer caption a: no country code is better than wrong country code
|
|
|
|
return -1
|
|
|
|
} else if (bCode[1] === undefined) {
|
|
|
|
// prefer caption b: no country code is better than wrong country code
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// prefer caption a: b does not match user's language
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
} else if (bCode[0] === userLocale[0]) {
|
|
|
|
// prefer caption b: a does not match user's language
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
// sort alphabetically
|
2023-08-15 20:17:10 +02:00
|
|
|
return aName.localeCompare(bName, this.currentLocale)
|
2021-09-20 04:12:14 +02:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
transformAndInsertCaptions: async function () {
|
2021-03-17 02:28:25 +01:00
|
|
|
let captionList
|
|
|
|
if (this.captionHybridList[0] instanceof Promise) {
|
|
|
|
captionList = await Promise.all(this.captionHybridList)
|
|
|
|
this.$emit('store-caption-list', captionList)
|
|
|
|
} else {
|
|
|
|
captionList = this.captionHybridList
|
|
|
|
}
|
|
|
|
|
2024-01-03 01:28:13 +01:00
|
|
|
this.sortCaptions(captionList).forEach((caption, i) =>
|
2021-03-17 02:28:25 +01:00
|
|
|
this.player.addRemoteTextTrack({
|
|
|
|
kind: 'subtitles',
|
2023-01-07 02:52:59 +01:00
|
|
|
src: caption.url,
|
|
|
|
srclang: caption.language_code,
|
|
|
|
label: caption.label,
|
2024-01-03 01:28:13 +01:00
|
|
|
type: caption.type,
|
|
|
|
default: i === 0 && this.enableSubtitlesByDefault
|
2021-03-17 02:28:25 +01:00
|
|
|
}, true)
|
2024-01-03 01:28:13 +01:00
|
|
|
)
|
2021-03-17 02:28:25 +01:00
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
toggleFullWindow: function () {
|
2020-12-14 23:37:58 +01:00
|
|
|
if (!this.player.isFullscreen_) {
|
|
|
|
if (this.player.isFullWindow) {
|
|
|
|
this.player.removeClass('vjs-full-screen')
|
|
|
|
this.player.isFullWindow = false
|
|
|
|
document.documentElement.style.overflow = this.player.docOrigOverflow
|
2022-10-05 10:25:50 +02:00
|
|
|
document.body.classList.remove('vjs-full-window')
|
|
|
|
document.getElementById('fullwindow').classList.remove('vjs-icon-fullwindow-exit')
|
2020-12-14 23:37:58 +01:00
|
|
|
this.player.trigger('exitFullWindow')
|
|
|
|
} else {
|
|
|
|
this.player.addClass('vjs-full-screen')
|
|
|
|
this.player.isFullscreen_ = false
|
|
|
|
this.player.isFullWindow = true
|
|
|
|
this.player.docOrigOverflow = document.documentElement.style.overflow
|
|
|
|
document.documentElement.style.overflow = 'hidden'
|
2022-10-05 10:25:50 +02:00
|
|
|
document.body.classList.add('vjs-full-window')
|
|
|
|
document.getElementById('fullwindow').classList.add('vjs-icon-fullwindow-exit')
|
2020-12-14 23:37:58 +01:00
|
|
|
this.player.trigger('enterFullWindow')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
exitFullWindow: function () {
|
2020-12-14 23:37:58 +01:00
|
|
|
if (this.player.isFullWindow) {
|
|
|
|
this.player.isFullWindow = false
|
|
|
|
document.documentElement.style.overflow = this.player.docOrigOverflow
|
|
|
|
this.player.removeClass('vjs-full-screen')
|
2022-10-05 10:25:50 +02:00
|
|
|
document.body.classList.remove('vjs-full-window')
|
|
|
|
document.getElementById('fullwindow').classList.remove('vjs-icon-fullwindow-exit')
|
2020-12-14 23:37:58 +01:00
|
|
|
this.player.trigger('exitFullWindow')
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
toggleFullscreen: function () {
|
|
|
|
if (this.player.isFullscreen()) {
|
|
|
|
this.player.exitFullscreen()
|
|
|
|
} else {
|
|
|
|
this.player.requestFullscreen()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-11-15 20:13:18 +01:00
|
|
|
fullscreenOverlay: function () {
|
|
|
|
const title = document.title.replace('- FreeTube', '')
|
|
|
|
|
|
|
|
if (this.player.isFullscreen()) {
|
2021-05-22 01:52:11 +02:00
|
|
|
this.player.ready(() => {
|
|
|
|
this.player.overlay({
|
2020-11-15 20:13:18 +01:00
|
|
|
overlays: [{
|
|
|
|
showBackground: false,
|
|
|
|
content: title,
|
|
|
|
start: 'mousemove',
|
|
|
|
end: 'userinactive'
|
|
|
|
}]
|
|
|
|
})
|
|
|
|
})
|
|
|
|
} else {
|
2021-05-22 01:52:11 +02:00
|
|
|
this.player.ready(() => {
|
|
|
|
this.player.overlay({
|
2020-11-15 20:13:18 +01:00
|
|
|
overlays: [{
|
|
|
|
showBackground: false,
|
|
|
|
content: ' ',
|
|
|
|
start: 'play',
|
|
|
|
end: 'loadstart'
|
|
|
|
}]
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-04-22 20:41:50 +02:00
|
|
|
toggleFullscreenClass: function () {
|
|
|
|
if (this.player.isFullscreen()) {
|
2022-10-05 10:25:50 +02:00
|
|
|
document.body.classList.add('vjs--full-screen-enabled')
|
2021-04-22 20:41:50 +02:00
|
|
|
} else {
|
2022-10-05 10:25:50 +02:00
|
|
|
document.body.classList.remove('vjs--full-screen-enabled')
|
2021-04-22 20:41:50 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-10-24 19:49:52 +02:00
|
|
|
handleTouchStart: function () {
|
|
|
|
this.usingTouch = true
|
2021-01-16 04:34:49 +01:00
|
|
|
},
|
|
|
|
|
2022-10-24 19:49:52 +02:00
|
|
|
handleMouseOver: function () {
|
|
|
|
// This addresses a discrepancy that only seems to occur in the mobile version of firefox
|
|
|
|
if (navigator.userAgent.search('Firefox') !== -1 && (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS)) { return }
|
|
|
|
this.usingTouch = false
|
2021-01-16 04:34:49 +01:00
|
|
|
},
|
2022-10-24 19:49:52 +02:00
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
toggleShowStatsModal: function () {
|
2022-02-19 23:17:58 +01:00
|
|
|
if (this.format !== 'dash') {
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Video.Stats.Video statistics are not available for legacy videos'))
|
2022-02-19 23:17:58 +01:00
|
|
|
} else {
|
|
|
|
this.showStatsModal = !this.showStatsModal
|
|
|
|
}
|
|
|
|
},
|
2023-08-26 18:20:55 +02:00
|
|
|
createStatsModal: function () {
|
2022-02-19 23:17:58 +01:00
|
|
|
const ModalDialog = videojs.getComponent('ModalDialog')
|
|
|
|
this.statsModal = new ModalDialog(this.player, {
|
|
|
|
temporary: false,
|
|
|
|
pauseOnOpen: false
|
|
|
|
})
|
2022-05-22 13:59:49 +02:00
|
|
|
this.statsModal.handleKeyDown_ = (event) => {
|
|
|
|
// the default handler prevents keyboard events propagating beyond the modal
|
|
|
|
// the modal should only handle the escape and tab key, all others should be handled by the player
|
|
|
|
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
|
|
this.statsModal.handleKeyDown(event)
|
|
|
|
}
|
|
|
|
}
|
2022-02-19 23:17:58 +01:00
|
|
|
this.player.addChild(this.statsModal)
|
|
|
|
this.statsModal.el_.classList.add('statsModal')
|
|
|
|
this.statsModal.on('modalclose', () => {
|
|
|
|
this.showStatsModal = false
|
|
|
|
})
|
|
|
|
},
|
2023-08-26 18:20:55 +02:00
|
|
|
updateStatsContent: function () {
|
2022-02-19 23:17:58 +01:00
|
|
|
if (this.showStatsModal) {
|
|
|
|
this.statsModal.contentEl().innerHTML = this.getFormattedStats()
|
|
|
|
}
|
|
|
|
},
|
2023-08-26 18:20:55 +02:00
|
|
|
getFormattedStats: function () {
|
2022-02-19 23:17:58 +01:00
|
|
|
const currentVolume = this.player.muted() ? 0 : this.player.volume()
|
|
|
|
const volume = `${(currentVolume * 100).toFixed(0)}%`
|
|
|
|
const bandwidth = `${(this.playerStats.bandwidth / 1000).toFixed(2)}kbps`
|
|
|
|
const buffered = `${(this.player.bufferedPercent() * 100).toFixed(0)}%`
|
|
|
|
const droppedFrames = this.playerStats.videoPlaybackQuality.droppedVideoFrames
|
|
|
|
const totalFrames = this.playerStats.videoPlaybackQuality.totalVideoFrames
|
|
|
|
const frames = `${droppedFrames} / ${totalFrames}`
|
2023-08-26 18:20:55 +02:00
|
|
|
const resolution = this.selectedResolution === 'auto' ? `${this.autoResolution}@${this.autoFPS}fps (auto)` : `${this.selectedResolution}@${this.selectedFPS}fps`
|
2022-02-19 23:17:58 +01:00
|
|
|
const playerDimensions = `${this.playerStats.playerDimensions.width}x${this.playerStats.playerDimensions.height}`
|
2023-08-26 18:20:55 +02:00
|
|
|
const bitrate = this.selectedBitrate === 'auto' ? `${this.autoBitrate} (auto)` : this.selectedBitrate
|
|
|
|
const mimeType = this.selectedMimeType === 'auto' ? `${this.autoMimeType} (auto)` : this.selectedMimeType
|
2022-02-19 23:17:58 +01:00
|
|
|
const statsArray = [
|
|
|
|
[this.$t('Video.Stats.Video ID'), this.videoId],
|
|
|
|
[this.$t('Video.Stats.Resolution'), resolution],
|
|
|
|
[this.$t('Video.Stats.Player Dimensions'), playerDimensions],
|
2023-08-26 18:20:55 +02:00
|
|
|
[this.$t('Video.Stats.Bitrate'), bitrate],
|
2022-02-19 23:17:58 +01:00
|
|
|
[this.$t('Video.Stats.Volume'), volume],
|
|
|
|
[this.$t('Video.Stats.Bandwidth'), bandwidth],
|
|
|
|
[this.$t('Video.Stats.Buffered'), buffered],
|
|
|
|
[this.$t('Video.Stats.Dropped / Total Frames'), frames],
|
2023-08-26 18:20:55 +02:00
|
|
|
[this.$t('Video.Stats.Mimetype'), mimeType]
|
2022-02-19 23:17:58 +01:00
|
|
|
]
|
|
|
|
let listContentHTML = ''
|
|
|
|
|
|
|
|
statsArray.forEach((stat) => {
|
|
|
|
const content = `<p>${stat[0]}: ${stat[1]}</p>`
|
|
|
|
listContentHTML += content
|
|
|
|
})
|
|
|
|
return listContentHTML
|
|
|
|
},
|
2021-01-16 04:34:49 +01:00
|
|
|
|
2022-12-12 07:03:01 +01:00
|
|
|
/**
|
|
|
|
* determines whether the jump to the previous or next chapter
|
|
|
|
* with the the keyboard shortcuts, should be done
|
|
|
|
* first it checks whether there are any chapters (the array is also empty if chapters are hidden)
|
|
|
|
* it also checks that the approprate combination was used ALT/OPTION on macOS and CTRL everywhere else
|
|
|
|
* @param {KeyboardEvent} event the keyboard event
|
|
|
|
* @param {string} direction the direction of the jump either previous or next
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
canChapterJump: function (event, direction) {
|
2023-08-03 14:57:09 +02:00
|
|
|
const currentChapter = this.currentChapterIndex
|
2022-12-12 07:03:01 +01:00
|
|
|
return this.chapters.length > 0 &&
|
|
|
|
(direction === 'previous' ? currentChapter > 0 : this.chapters.length - 1 !== currentChapter) &&
|
|
|
|
((process.platform !== 'darwin' && event.ctrlKey) ||
|
|
|
|
(process.platform === 'darwin' && event.metaKey))
|
|
|
|
},
|
|
|
|
|
2023-07-17 14:32:39 +02:00
|
|
|
playVideo(thenFunc = null) {
|
|
|
|
// It can be called in `setTimeout` & user can navigate to other pages before it runs
|
|
|
|
// Which makes `this.player` become `null`
|
|
|
|
if (this.player == null) { return }
|
|
|
|
|
|
|
|
let promise = this.player.play()
|
|
|
|
if (typeof thenFunc === 'function') {
|
|
|
|
promise = promise.then(thenFunc)
|
|
|
|
}
|
|
|
|
promise
|
|
|
|
.catch(err => {
|
2023-08-03 15:01:08 +02:00
|
|
|
if (EXPECTED_PLAY_RELATED_ERROR_MESSAGES.some(msg => err.message.includes(msg))) {
|
2023-07-17 14:32:39 +02:00
|
|
|
// Ignoring expected exception
|
|
|
|
// console.debug('Ignoring expected error')
|
|
|
|
// console.debug(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unexpected errors should be reported
|
|
|
|
console.error(err)
|
|
|
|
const errorMessage = this.$t('play() request Error (Click to copy)')
|
|
|
|
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
|
|
|
copyToClipboard(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2022-02-19 23:17:58 +01:00
|
|
|
// This function should always be at the bottom of this file
|
2023-02-10 18:44:17 +01:00
|
|
|
/**
|
|
|
|
* @param {KeyboardEvent} event
|
|
|
|
*/
|
2020-02-20 21:58:21 +01:00
|
|
|
keyboardShortcutHandler: function (event) {
|
2023-02-10 18:44:17 +01:00
|
|
|
if (document.activeElement.classList.contains('ft-input') || event.altKey) {
|
2022-12-12 07:03:01 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// allow chapter jump keyboard shortcuts
|
|
|
|
if (event.ctrlKey && (process.platform === 'darwin' || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight'))) {
|
2020-12-21 20:43:50 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-03 19:20:16 +01:00
|
|
|
// allow copying text
|
2023-01-14 16:01:12 +01:00
|
|
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c') {
|
2023-01-03 19:20:16 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-02-20 21:58:21 +01:00
|
|
|
if (this.player !== null) {
|
2022-09-25 02:44:16 +02:00
|
|
|
switch (event.key) {
|
|
|
|
case ' ':
|
|
|
|
case 'Spacebar': // older browsers might return spacebar instead of a space character
|
2020-02-20 21:58:21 +01:00
|
|
|
// Toggle Play/Pause
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2020-02-20 21:58:21 +01:00
|
|
|
this.togglePlayPause()
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'J':
|
|
|
|
case 'j':
|
2021-08-05 22:17:01 +02:00
|
|
|
// Rewind by 2x the time-skip interval (in seconds)
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2022-03-30 04:06:54 +02:00
|
|
|
this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate() * 2)
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'K':
|
|
|
|
case 'k':
|
2020-02-20 21:58:21 +01:00
|
|
|
// Toggle Play/Pause
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2020-02-20 21:58:21 +01:00
|
|
|
this.togglePlayPause()
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'L':
|
|
|
|
case 'l':
|
2021-08-05 22:17:01 +02:00
|
|
|
// Fast-Forward by 2x the time-skip interval (in seconds)
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2022-03-30 04:06:54 +02:00
|
|
|
this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate() * 2)
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'O':
|
|
|
|
case 'o':
|
2020-02-20 21:58:21 +01:00
|
|
|
// Decrease playback rate by 0.25x
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2022-04-11 00:00:47 +02:00
|
|
|
this.changePlayBackRate(-this.videoPlaybackRateInterval)
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'P':
|
|
|
|
case 'p':
|
2020-02-20 21:58:21 +01:00
|
|
|
// Increase playback rate by 0.25x
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2022-04-11 00:00:47 +02:00
|
|
|
this.changePlayBackRate(this.videoPlaybackRateInterval)
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'F':
|
|
|
|
case 'f':
|
2020-02-20 21:58:21 +01:00
|
|
|
// Toggle Fullscreen Playback
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2020-02-20 21:58:21 +01:00
|
|
|
this.toggleFullscreen()
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'M':
|
|
|
|
case 'm':
|
2020-02-20 21:58:21 +01:00
|
|
|
// Toggle Mute
|
2022-08-16 12:57:07 +02:00
|
|
|
if (!event.metaKey) {
|
|
|
|
event.preventDefault()
|
|
|
|
this.toggleMute()
|
|
|
|
}
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'C':
|
|
|
|
case 'c':
|
2020-02-20 21:58:21 +01:00
|
|
|
// Toggle Captions
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2020-02-20 21:58:21 +01:00
|
|
|
this.toggleCaptions()
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'ArrowUp':
|
2020-02-20 21:58:21 +01:00
|
|
|
// Increase volume
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2020-02-20 21:58:21 +01:00
|
|
|
this.changeVolume(0.05)
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'ArrowDown':
|
2020-11-15 20:13:18 +01:00
|
|
|
// Decrease Volume
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2020-02-20 21:58:21 +01:00
|
|
|
this.changeVolume(-0.05)
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'ArrowLeft':
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2022-12-12 07:03:01 +01:00
|
|
|
if (this.canChapterJump(event, 'previous')) {
|
|
|
|
// Jump to the previous chapter
|
2023-08-03 14:57:09 +02:00
|
|
|
this.player.currentTime(this.chapters[this.currentChapterIndex - 1].startSeconds)
|
2022-12-12 07:03:01 +01:00
|
|
|
} else {
|
|
|
|
// Rewind by the time-skip interval (in seconds)
|
|
|
|
this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate())
|
|
|
|
}
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'ArrowRight':
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2022-12-12 07:03:01 +01:00
|
|
|
if (this.canChapterJump(event, 'next')) {
|
|
|
|
// Jump to the next chapter
|
2023-08-03 14:57:09 +02:00
|
|
|
this.player.currentTime(this.chapters[this.currentChapterIndex + 1].startSeconds)
|
2022-12-12 07:03:01 +01:00
|
|
|
} else {
|
|
|
|
// Fast-Forward by the time-skip interval (in seconds)
|
|
|
|
this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate())
|
|
|
|
}
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'I':
|
|
|
|
case 'i':
|
2022-02-19 23:17:58 +01:00
|
|
|
event.preventDefault()
|
2022-04-08 04:40:29 +02:00
|
|
|
// Toggle Picture in Picture Mode
|
2022-05-02 13:50:23 +02:00
|
|
|
if (this.format !== 'audio' && !this.player.isInPictureInPicture()) {
|
2022-04-08 04:40:29 +02:00
|
|
|
this.player.requestPictureInPicture()
|
|
|
|
} else if (this.player.isInPictureInPicture()) {
|
|
|
|
this.player.exitPictureInPicture()
|
|
|
|
}
|
2022-02-19 23:17:58 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case '0':
|
|
|
|
case '1':
|
|
|
|
case '2':
|
|
|
|
case '3':
|
|
|
|
case '4':
|
|
|
|
case '5':
|
|
|
|
case '6':
|
|
|
|
case '7':
|
|
|
|
case '8':
|
|
|
|
case '9': {
|
|
|
|
// Jump to percentage in the video
|
2020-09-26 17:57:57 +02:00
|
|
|
event.preventDefault()
|
2022-09-25 02:44:16 +02:00
|
|
|
|
|
|
|
const percentage = parseInt(event.key) / 10
|
|
|
|
const duration = this.player.duration()
|
|
|
|
const newTime = duration * percentage
|
|
|
|
|
|
|
|
this.player.currentTime(newTime)
|
2020-02-20 21:58:21 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
}
|
|
|
|
case ',':
|
2020-11-13 23:29:41 +01:00
|
|
|
// Return to previous frame
|
|
|
|
this.framebyframe(-1)
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case '.':
|
2020-11-13 23:29:41 +01:00
|
|
|
// Advance to next frame
|
|
|
|
this.framebyframe(1)
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'D':
|
|
|
|
case 'd':
|
2022-04-08 04:40:29 +02:00
|
|
|
event.preventDefault()
|
|
|
|
this.toggleShowStatsModal()
|
2020-12-15 20:07:40 +01:00
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'Escape':
|
2020-12-14 23:37:58 +01:00
|
|
|
// Exit full window
|
|
|
|
event.preventDefault()
|
|
|
|
this.exitFullWindow()
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'S':
|
|
|
|
case 's':
|
2020-12-15 20:07:40 +01:00
|
|
|
// Toggle Full Window Mode
|
|
|
|
this.toggleFullWindow()
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'T':
|
|
|
|
case 't':
|
2021-09-15 04:00:34 +02:00
|
|
|
// Toggle Theatre Mode
|
|
|
|
this.toggleTheatreMode()
|
|
|
|
break
|
2022-09-25 02:44:16 +02:00
|
|
|
case 'U':
|
|
|
|
case 'u':
|
2022-05-30 15:24:34 +02:00
|
|
|
// Take screenshot
|
|
|
|
this.takeScreenshot()
|
|
|
|
break
|
2020-02-20 21:58:21 +01:00
|
|
|
}
|
|
|
|
}
|
2021-01-13 04:56:31 +01:00
|
|
|
},
|
|
|
|
|
2023-08-26 18:20:55 +02:00
|
|
|
stopPowerSaveBlocker: function () {
|
2023-05-23 13:37:44 +02:00
|
|
|
if (process.env.IS_ELECTRON && this.powerSaveBlocker !== null) {
|
|
|
|
const { ipcRenderer } = require('electron')
|
|
|
|
ipcRenderer.send(IpcChannels.STOP_POWER_SAVE_BLOCKER, this.powerSaveBlocker)
|
|
|
|
this.powerSaveBlocker = null
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-01-11 03:16:50 +01:00
|
|
|
...mapActions([
|
|
|
|
'updateDefaultCaptionSettings',
|
2022-05-30 15:24:34 +02:00
|
|
|
'parseScreenshotCustomFileName',
|
|
|
|
'updateScreenshotFolderPath',
|
2022-01-11 03:16:50 +01:00
|
|
|
])
|
2020-02-16 19:30:00 +01:00
|
|
|
}
|
|
|
|
})
|