FreeTube/src/renderer/components/ft-video-player/ft-video-player.js

1678 lines
52 KiB
JavaScript
Raw Normal View History

2020-02-16 19:30:00 +01:00
import Vue from 'vue'
import { mapActions } from 'vuex'
2020-02-16 19:30:00 +01:00
import FtCard from '../ft-card/ft-card.vue'
import $ from 'jquery'
2020-02-16 19:30:00 +01:00
import videojs from 'video.js'
import qualitySelector from '@silvermine/videojs-quality-selector'
import fs from 'fs'
import 'videojs-overlay/dist/videojs-overlay'
import 'videojs-overlay/dist/videojs-overlay.css'
import 'videojs-vtt-thumbnails-freetube'
import 'videojs-contrib-quality-levels'
import 'videojs-http-source-selector'
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
import { ipcRenderer } from 'electron'
2020-02-16 19:30:00 +01:00
export default Vue.extend({
name: 'FtVideoPlayer',
components: {
'ft-card': FtCard
},
beforeRouteLeave: function () {
if (this.player !== null) {
this.exitFullWindow()
}
if (this.player !== null && !this.player.isInPictureInPicture()) {
this.player.dispose()
this.player = null
clearTimeout(this.mouseTimeout)
} else if (this.player.isInPictureInPicture()) {
this.player.play()
}
2021-01-13 21:56:25 +01:00
if (this.usingElectron && this.powerSaveBlocker !== null) {
const { ipcRenderer } = require('electron')
ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker)
2021-01-13 21:56:25 +01:00
}
},
2020-02-16 19:30:00 +01:00
props: {
format: {
type: String,
required: true
},
sourceList: {
type: Array,
default: () => { return [] }
},
2021-05-15 21:08:41 +02:00
adaptiveFormats: {
type: Array,
default: () => { return [] }
},
dashSrc: {
type: Array,
default: null
},
hlsSrc: {
type: Array,
default: null
},
2021-03-17 02:28:25 +01:00
captionHybridList: {
2020-02-16 19:30:00 +01:00
type: Array,
default: () => { return [] }
},
storyboardSrc: {
2020-02-16 19:30:00 +01:00
type: String,
default: ''
},
thumbnail: {
type: String,
default: ''
},
videoId: {
type: String,
required: true
2020-02-16 19:30:00 +01:00
}
},
data: function () {
return {
id: '',
powerSaveBlocker: null,
volume: 1,
2020-02-16 19:30:00 +01:00
player: null,
useDash: false,
useHls: false,
2020-09-25 04:35:13 +02:00
selectedDefaultQuality: '',
selectedQuality: '',
using60Fps: false,
maxFramerate: 0,
activeSourceList: [],
2021-05-15 21:08:41 +02:00
activeAdaptiveFormats: [],
2020-02-21 19:31:32 +01:00
mouseTimeout: null,
touchTimeout: null,
lastTouchTime: null,
2020-02-16 19:30:00 +01:00
dataSetup: {
fluid: true,
nativeTextTracks: false,
plugins: {},
controlBar: {
children: [
'playToggle',
'volumePanel',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'progressControl',
'liveDisplay',
'seekToLive',
'remainingTimeDisplay',
'customControlSpacer',
'playbackRateMenuButton',
'loopButton',
'chaptersButton',
'descriptionsButton',
'subsCapsButton',
'audioTrackButton',
2020-12-15 00:25:51 +01:00
'pictureInPictureToggle',
'toggleTheatreModeButton',
2020-12-15 00:25:51 +01:00
'fullWindowButton',
'qualitySelector',
2020-12-15 00:25:51 +01:00
'fullscreenToggle'
]
},
2020-02-16 19:30:00 +01:00
playbackRates: [
0.25,
0.5,
0.75,
1,
1.25,
1.5,
1.75,
2,
2.25,
2.5,
2.75,
3,
3.25,
3.5,
3.75,
4,
4.25,
4.5,
4.75,
5,
5.25,
5.5,
5.75,
6,
6.25,
6.5,
6.75,
7,
7.25,
7.5,
7.75,
8
2020-02-16 19:30:00 +01:00
]
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
},
stats: {
videoId: '',
playerResolution: null,
frameInfo: null,
volume: 0,
bandwidth: null,
bufferPercent: 0,
fps: null,
display: {
modal: null,
event: 'statsUpdated',
keyboardShortcut: 'KeyI',
rightClickEvent: 'showVideoStatistics',
activated: false
}
2020-02-16 19:30:00 +01:00
}
}
},
computed: {
usingElectron: function () {
return this.$store.getters.getUsingElectron
},
currentLocale: function () {
return this.$store.getters.getCurrentLocale
},
defaultPlayback: function () {
return this.$store.getters.getDefaultPlayback
},
defaultSkipInterval: function () {
return this.$store.getters.getDefaultSkipInterval
},
defaultQuality: function () {
2020-09-26 18:10:56 +02:00
return parseInt(this.$store.getters.getDefaultQuality)
},
defaultCaptionSettings: function () {
try {
return JSON.parse(this.$store.getters.getDefaultCaptionSettings)
} catch (e) {
console.log(e)
return {}
}
},
defaultVideoFormat: function () {
return this.$store.getters.getDefaultVideoFormat
},
autoplayVideos: function () {
return this.$store.getters.getAutoplayVideos
},
videoVolumeMouseScroll: function () {
return this.$store.getters.getVideoVolumeMouseScroll
},
videoPlaybackRateMouseScroll: function () {
return this.$store.getters.getVideoPlaybackRateMouseScroll
},
useSponsorBlock: function () {
return this.$store.getters.getUseSponsorBlock
},
sponsorBlockShowSkippedToast: function () {
return this.$store.getters.getSponsorBlockShowSkippedToast
},
displayVideoPlayButton: function() {
return this.$store.getters.getDisplayVideoPlayButton
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
},
formatted_stats: function() {
let resolution = ''
let dropFrame = ''
if (this.stats.playerResolution != null) {
resolution = `(${this.stats.playerResolution.height}X${this.stats.playerResolution.width}) @ ${this.stats.fps} ${this.$t('Video.Stats.fps')}`
}
if (this.stats.frameInfo != null) {
dropFrame = `${this.stats.frameInfo.droppedVideoFrames} ${this.$t('Video.Stats.out of')} ${this.stats.frameInfo.totalVideoFrames}`
}
const stats = [
[this.$t('Video.Stats.video id'), this.stats.videoId],
[this.$t('Video.Stats.frame drop'), dropFrame],
[this.$t('Video.Stats.player resolution'), resolution],
[this.$t('Video.Stats.volume'), `${(this.stats.volume * 100).toFixed(0)} %`],
[this.$t('Video.Stats.bandwidth'), `${(this.stats.bandwidth / 1000).toFixed(2)} Kbps`],
[this.$t('Video.Stats.buffered'), `${(this.stats.bufferPercent * 100).toFixed(0)} %`]
]
let formattedStats = '<ul style="list-style-type: none;text-align:left; padding-left:0px";>'
for (const stat of stats) {
formattedStats += `<li style="font-size: 75%">${stat[0]}: ${stat[1]}</li>`
}
formattedStats += '</ul>'
return formattedStats
}
},
watch: {
selectedQuality: function() {
this.currentFps()
2020-02-16 19:30:00 +01:00
}
},
mounted: function () {
this.id = this._uid
const volume = sessionStorage.getItem('volume')
if (volume !== null) {
this.volume = volume
}
2020-12-15 00:25:51 +01:00
this.createFullWindowButton()
this.createLoopButton()
this.createToggleTheatreModeButton()
this.determineFormatType()
this.determineMaxFramerate()
2020-02-16 19:30:00 +01:00
},
beforeDestroy: function () {
if (this.player !== null) {
this.exitFullWindow()
if (!this.player.isInPictureInPicture()) {
this.player.dispose()
this.player = null
clearTimeout(this.mouseTimeout)
}
}
2021-01-13 21:56:25 +01:00
if (this.usingElectron && this.powerSaveBlocker !== null) {
const { ipcRenderer } = require('electron')
ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker)
2021-01-13 21:56:25 +01:00
}
},
2020-02-16 19:30:00 +01:00
methods: {
2020-09-25 04:35:13 +02:00
initializePlayer: async function () {
2021-05-15 21:08:41 +02:00
console.log(this.adaptiveFormats)
2020-02-16 19:30:00 +01:00
const videoPlayer = document.getElementById(this.id)
if (videoPlayer !== null) {
2020-05-24 23:32:02 +02:00
if (!this.useDash) {
qualitySelector(videojs, { showQualitySelectionLabelInControlBar: true })
2020-09-26 23:03:42 +02:00
await this.determineDefaultQualityLegacy()
}
this.player = videojs(videoPlayer, {
html5: {
2021-03-17 02:28:25 +01:00
preloadTextTracks: false,
vhs: {
limitRenditionByPlayerDimensions: false,
smoothQualityChange: false,
allowSeeksWithinUnsafeLiveWindow: true,
handlePartialData: true
}
}
})
2020-12-15 00:25:51 +01:00
this.player.volume(this.volume)
this.player.playbackRate(this.defaultPlayback)
this.player.textTrackSettings.setValues(this.defaultCaptionSettings)
// Remove big play button
// https://github.com/videojs/video.js/blob/v7.12.1/docs/guides/components.md#basic-example
if (!this.displayVideoPlayButton) {
this.player.removeChild('BigPlayButton')
}
2020-09-25 04:35:13 +02:00
if (this.storyboardSrc !== '') {
this.player.vttThumbnails({
src: this.storyboardSrc,
showTimestamp: true
})
}
2020-05-24 23:32:02 +02:00
if (this.useDash) {
// this.dataSetup.plugins.httpSourceSelector = {
// default: 'auto'
// }
// this.player.httpSourceSelector()
this.createDashQualitySelector(this.player.qualityLevels())
}
if (this.autoplayVideos) {
// Calling play() won't happen right away, so a quick timeout will make it function properly.
setTimeout(() => {
this.player.play()
2020-05-24 23:32:02 +02:00
}, 200)
}
// 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')
if (this.useSponsorBlock) {
this.initializeSponsorBlock()
}
2020-12-15 00:25:51 +01:00
$(document).on('keydown', this.keyboardShortcutHandler)
2020-02-21 19:31:32 +01:00
this.player.on('mousemove', this.hideMouseTimeout)
this.player.on('mouseleave', this.removeMouseTimeout)
this.player.on('volumechange', this.updateVolume)
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
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)
}
this.player.on('fullscreenchange', this.fullscreenOverlay)
this.player.on('fullscreenchange', this.toggleFullscreenClass)
this.player.on('ready', () => {
this.$emit('ready')
this.checkAspectRatio()
if (this.captionHybridList.length !== 0) {
this.transformAndInsertCaptions()
2021-03-17 02:28:25 +01:00
}
})
this.player.on('ended', () => {
this.$emit('ended')
})
this.player.on('error', (error, message) => {
this.$emit('error', error.target.player.error_)
2020-02-21 19:31:32 +01:00
})
this.player.on('play', async function () {
if (this.usingElectron) {
const { ipcRenderer } = require('electron')
this.powerSaveBlocker =
await ipcRenderer.invoke('startPowerSaveBlocker', 'prevent-display-sleep')
}
})
this.player.on('pause', function () {
if (this.usingElectron && this.powerSaveBlocker !== null) {
const { ipcRenderer } = require('electron')
ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker)
this.powerSaveBlocker = null
}
})
this.player.textTrackSettings.on('modalclose', (_) => {
const settings = this.player.textTrackSettings.getValues()
this.updateDefaultCaptionSettings(JSON.stringify(settings))
})
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
this.addPlayerStatsEvent()
2020-02-16 19:30:00 +01:00
}
},
initializeSponsorBlock() {
this.sponsorBlockSkipSegments({
videoId: this.videoId,
categories: ['sponsor']
}).then((skipSegments) => {
if (skipSegments.length === 0) {
return
}
this.player.ready(() => {
this.player.on('timeupdate', () => {
this.skipSponsorBlocks(skipSegments)
})
skipSegments.forEach(({
category,
segment: [startTime, endTime]
}) => {
this.addSponsorBlockMarker({
time: startTime,
duration: endTime - startTime,
color: this.sponsorBlockCategoryColor(category)
})
})
})
})
},
skipSponsorBlocks(skipSegments) {
const currentTime = this.player.currentTime()
const duration = this.player.duration()
let newTime = null
let skippedCategory = null
skipSegments.forEach(({ category, segment: [startTime, endTime] }) => {
if (startTime <= currentTime && currentTime < endTime) {
newTime = endTime
skippedCategory = category
}
})
if (newTime !== null && Math.abs(duration - currentTime) > 0.500) {
if (this.sponsorBlockShowSkippedToast) {
this.showSkippedSponsorSegmentInformation(skippedCategory)
}
this.player.currentTime(newTime)
}
},
showSkippedSponsorSegmentInformation(category) {
const translatedCategory = this.sponsorBlockTranslatedCategory(category)
this.showToast({
message: `${this.$t('Video.Skipped segment')} ${translatedCategory}`
})
},
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')
default:
console.error(`Unknown translation for SponsorBlock category ${category}`)
return category
}
},
sponsorBlockCategoryColor(category) {
// TODO: allow to set these colors in settings
switch (category) {
case 'sponsor':
return 'var(--accent-color)'
case 'intro':
return 'var(--accent-color)'
case 'outro':
return 'var(--accent-color)'
case 'selfpromo':
return 'var(--accent-color)'
case 'interaction':
return 'var(--accent-color)'
case 'music_offtopic':
return 'var(--accent-color)'
default:
console.error(`Unknown SponsorBlock category ${category}`)
2021-05-17 21:06:10 +02:00
return 'var(--accent-color)'
}
},
addSponsorBlockMarker(marker) {
const markerDiv = videojs.dom.createEl('div', {}, {})
markerDiv.className = 'sponsorBlockMarker'
markerDiv.style.height = '100%'
markerDiv.style.position = 'absolute'
markerDiv.style['background-color'] = marker.color
markerDiv.style.width = (marker.duration / this.player.duration()) * 100 + '%'
markerDiv.style.marginLeft = (marker.time / this.player.duration()) * 100 + '%'
this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv)
},
checkAspectRatio() {
const videoWidth = this.player.videoWidth()
const videoHeight = this.player.videoHeight()
if (videoWidth === 0 || videoHeight === 0) {
setTimeout(() => {
this.checkAspectRatio()
}, 200)
return
}
if ((videoWidth - videoHeight) <= 240) {
this.player.fluid(false)
this.player.aspectRatio('16:9')
}
},
updateVolume: function (_event) {
// 0 means muted
// https://docs.videojs.com/html5#volume
const volume = this.player.muted() ? 0 : this.player.volume()
sessionStorage.setItem('volume', volume)
},
mouseScrollVolume: function (event) {
if (event.target && !event.currentTarget.querySelector('.vjs-menu:hover')) {
event.preventDefault()
if (this.player.muted() && event.wheelDelta > 0) {
this.player.muted(false)
this.player.volume(0)
}
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)
}
}
}
}
},
mouseScrollPlaybackRate: function (event) {
if (event.target && !event.currentTarget.querySelector('.vjs-menu:hover')) {
event.preventDefault()
if (event.ctrlKey || event.metaKey) {
if (event.wheelDelta > 0) {
this.changePlayBackRate(0.05)
} else if (event.wheelDelta < 0) {
this.changePlayBackRate(-0.05)
}
}
}
},
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()) {
this.player.play()
} else {
this.player.pause()
}
}
}
},
determineFormatType: function () {
if (this.format === 'dash') {
this.enableDashFormat()
} else {
this.enableLegacyFormat()
}
},
determineMaxFramerate: function() {
if (this.dashSrc.length === 0) {
this.maxFramerate = 60
return
}
fs.readFile(this.dashSrc[0].url, (err, data) => {
if (err) {
this.maxFramerate = 60
return
}
if (data.includes('frameRate="60"')) {
this.maxFramerate = 60
} else {
this.maxFramerate = 30
}
})
},
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-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-09-26 23:03:42 +02:00
determineDefaultQualityDash: function () {
if (this.defaultQuality === 'auto') {
this.setDashQualityLevel('auto')
2020-09-26 23:03:42 +02:00
}
let formatsToTest
2021-05-15 21:08:41 +02:00
if (typeof this.activeAdaptiveFormats !== 'undefined' && this.activeAdaptiveFormats.length > 0) {
2021-05-15 21:08:41 +02:00
formatsToTest = this.activeAdaptiveFormats.filter((format) => {
return format.height === this.defaultQuality
2021-05-15 21:08:41 +02:00
})
if (formatsToTest.length === 0) {
formatsToTest = this.activeAdaptiveFormats.filter((format) => {
return format.height < this.defaultQuality
})
2021-05-15 21:08:41 +02:00
}
formatsToTest = formatsToTest.sort((a, b) => {
if (a.height === b.height) {
return b.bitrate - a.bitrate
} else {
return b.height - a.height
}
})
} else {
formatsToTest = this.player.qualityLevels().levels_.filter((format) => {
return format.height === this.defaultQuality
})
if (formatsToTest.length === 0) {
formatsToTest = this.player.qualityLevels().levels_.filter((format) => {
return format.height < this.defaultQuality
})
}
formatsToTest = formatsToTest.sort((a, b) => {
if (a.height === b.height) {
return b.bitrate - a.bitrate
} else {
return b.height - a.height
}
})
}
2021-05-15 21:08:41 +02:00
2021-05-15 21:13:17 +02:00
// TODO: Test formats to determine if HDR / 60 FPS and skip them based on
// User settings
2021-05-15 21:08:41 +02:00
this.setDashQualityLevel(formatsToTest[0].bitrate)
// Old logic. Revert if needed
/* this.player.qualityLevels().levels_.sort((a, b) => {
if (a.height === b.height) {
return a.bitrate - b.bitrate
} else {
return a.height - b.height
}
2020-09-26 23:03:42 +02:00
}).forEach((ql, index, arr) => {
const height = ql.height
const width = ql.width
const quality = width < height ? width : height
let upperLevel = null
if (index < arr.length - 1) {
upperLevel = arr[index + 1]
}
if (this.defaultQuality === quality && upperLevel === null) {
this.setDashQualityLevel(height, true)
2020-09-26 23:03:42 +02:00
} else if (upperLevel !== null) {
const upperHeight = upperLevel.height
const upperWidth = upperLevel.width
const upperQuality = upperWidth < upperHeight ? upperWidth : upperHeight
if (this.defaultQuality >= quality && this.defaultQuality === upperQuality) {
this.setDashQualityLevel(height, true)
} else if (this.defaultQuality >= quality && this.defaultQuality < upperQuality) {
this.setDashQualityLevel(height)
2020-09-26 23:03:42 +02:00
}
} else if (index === 0 && quality > this.defaultQuality) {
this.setDashQualityLevel(height)
2020-09-26 23:03:42 +02:00
} else if (index === (arr.length - 1) && quality < this.defaultQuality) {
this.setDashQualityLevel(height)
}
2021-05-15 21:08:41 +02:00
}) */
},
2021-05-15 21:08:41 +02:00
setDashQualityLevel: function (bitrate) {
let adaptiveFormat = null
if (bitrate !== 'auto') {
adaptiveFormat = this.activeAdaptiveFormats.find((format) => {
return format.bitrate === bitrate
})
}
let qualityLabel = adaptiveFormat ? adaptiveFormat.qualityLabel : ''
2021-05-15 21:08:41 +02:00
this.player.qualityLevels().levels_.sort((a, b) => {
if (a.height === b.height) {
return a.bitrate - b.bitrate
} else {
return a.height - b.height
}
}).forEach((ql, index, arr) => {
if (bitrate === 'auto' || bitrate === ql.bitrate) {
ql.enabled = true
ql.enabled_(true)
if (bitrate !== 'auto' && qualityLabel === '') {
qualityLabel = ql.height + 'p'
}
2021-05-15 21:08:41 +02:00
} else {
ql.enabled = false
ql.enabled_(false)
}
})
const selectedQuality = bitrate === 'auto' ? 'auto' : qualityLabel
2021-05-15 21:08:41 +02:00
const qualityElement = document.getElementById('vjs-current-quality')
qualityElement.innerText = selectedQuality
this.selectedQuality = selectedQuality
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.toLowerCase()) {
$(item).addClass('quality-selected')
}
})
/* if (this.selectedQuality === qualityLevel && this.using60Fps === is60Fps) {
return
}
let foundSelectedQuality = false
this.using60Fps = is60Fps
this.player.qualityLevels().levels_.sort((a, b) => {
if (a.height === b.height) {
return a.bitrate - b.bitrate
} else {
return a.height - b.height
}
}).forEach((ql, index, arr) => {
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
ql.enabled_(true)
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
ql.enabled_(false)
2020-09-26 23:03:42 +02:00
}
})
let selectedQuality = qualityLevel
if (selectedQuality !== 'auto' && is60Fps) {
selectedQuality = selectedQuality + 'p60'
} else if (selectedQuality !== 'auto') {
selectedQuality = selectedQuality + 'p'
}
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
},
enableDashFormat: function () {
if (this.dashSrc === null) {
console.log('No dash format available.')
return
}
this.useDash = true
this.useHls = false
this.activeSourceList = this.dashSrc
setTimeout(this.initializePlayer, 100)
},
enableLegacyFormat: function () {
if (this.sourceList.length === 0) {
console.log('No sources available')
return
}
this.useDash = false
this.useHls = false
this.activeSourceList = this.sourceList
setTimeout(this.initializePlayer, 100)
},
togglePlayPause: function () {
if (this.player.paused()) {
this.player.play()
} 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)
}
},
changeDurationByPercentage: function (percentage) {
const duration = this.player.duration()
const newTime = duration * percentage
this.player.currentTime(newTime)
},
changePlayBackRate: function (rate) {
const newPlaybackRate = (this.player.playbackRate() + rate).toFixed(2)
if (newPlaybackRate >= 0.25 && newPlaybackRate <= 8) {
this.player.playbackRate(newPlaybackRate)
}
},
framebyframe: function (step) {
this.player.pause()
const quality = this.useDash ? this.player.qualityLevels()[this.player.qualityLevels().selectedIndex] : {}
let fps = 30
// Non-Dash formats are 30fps only
if (this.maxFramerate === 60 && quality.height >= 480) {
for (let i = 0; i < this.adaptiveFormats.length; i++) {
if (this.adaptiveFormats[i].bitrate === quality.bitrate) {
fps = this.adaptiveFormats[i].fps
break
}
}
}
// 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)
console.log(fps)
},
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 () {
const tracks = this.player.textTracks().tracks_
if (tracks.length > 1) {
if (tracks[1].mode === 'showing') {
tracks[1].mode = 'disabled'
} else {
tracks[1].mode = 'showing'
}
}
},
createLoopButton: function () {
const VjsButton = videojs.getComponent('Button')
const loopButton = videojs.extend(VjsButton, {
constructor: function(player, options) {
VjsButton.call(this, player, options)
},
handleClick: () => {
this.toggleVideoLoop()
},
createControlTextEl: function (button) {
return $(button).html($('<div id="loopButton" class="vjs-icon-loop loop-white vjs-button loopWhite"></div>')
.attr('title', 'Toggle Loop'))
}
})
videojs.registerComponent('loopButton', loopButton)
},
toggleVideoLoop: async function () {
if (!this.player.loop()) {
const currentTheme = localStorage.getItem('mainColor')
const colorNames = this.$store.state.utils.colorClasses
const colorValues = this.$store.state.utils.colorValues
const nameIndex = colorNames.findIndex((color) => {
return color === currentTheme
})
const themeTextColor = await this.calculateColorLuminance(colorValues[nameIndex])
$('#loopButton').addClass('vjs-icon-loop-active')
if (themeTextColor === '#000000') {
$('#loopButton').addClass('loop-black')
$('#loopButton').removeClass('loop-white')
}
this.player.loop(true)
} else {
$('#loopButton').removeClass('vjs-icon-loop-active')
$('#loopButton').removeClass('loop-black')
$('#loopButton').addClass('loop-white')
this.player.loop(false)
}
},
createFullWindowButton: function () {
const VjsButton = videojs.getComponent('Button')
const fullWindowButton = videojs.extend(VjsButton, {
constructor: function(player, options) {
VjsButton.call(this, player, options)
},
handleClick: () => {
this.toggleFullWindow()
},
createControlTextEl: function (button) {
// Add class name to button to be able to target it with CSS selector
return $(button)
.addClass('vjs-button-fullwindow')
.html($('<div id="fullwindow" class="vjs-icon-fullwindow-enter vjs-button"></div>')
.attr('title', 'Full Window'))
}
})
videojs.registerComponent('fullWindowButton', fullWindowButton)
},
createToggleTheatreModeButton: function() {
if (!this.$parent.theatrePossible) {
return
}
const theatreModeActive = this.$parent.useTheatreMode ? ' vjs-icon-theatre-active' : ''
const VjsButton = videojs.getComponent('Button')
const toggleTheatreModeButton = videojs.extend(VjsButton, {
constructor: function(player, options) {
VjsButton.call(this, player, options)
},
handleClick: () => {
this.toggleTheatreMode()
},
createControlTextEl: function (button) {
return $(button)
.addClass('vjs-button-theatre')
.html($(`<div id="toggleTheatreModeButton" class="vjs-icon-theatre-inactive${theatreModeActive} vjs-button"></div>`))
.attr('title', 'Toggle Theatre Mode')
}
})
videojs.registerComponent('toggleTheatreModeButton', toggleTheatreModeButton)
},
toggleTheatreMode: function() {
if (!this.player.isFullscreen_) {
const toggleTheatreModeButton = $('#toggleTheatreModeButton')
if (!this.$parent.useTheatreMode) {
toggleTheatreModeButton.addClass('vjs-icon-theatre-active')
} else {
toggleTheatreModeButton.removeClass('vjs-icon-theatre-active')
}
}
this.$parent.toggleTheatreMode()
},
createDashQualitySelector: function (levels) {
if (levels.levels_.length === 0) {
setTimeout(() => {
this.createDashQualitySelector(this.player.qualityLevels())
}, 200)
return
}
const VjsButton = videojs.getComponent('Button')
const dashQualitySelector = videojs.extend(VjsButton, {
constructor: function(player, options) {
VjsButton.call(this, player, options)
},
handleClick: (event) => {
2021-05-15 21:08:41 +02:00
console.log(event)
const selectedQuality = event.target.innerText
2021-05-15 21:08:41 +02:00
const bitrate = selectedQuality === 'auto' ? 'auto' : parseInt(event.target.attributes.bitrate.value)
this.setDashQualityLevel(bitrate)
},
createControlTextEl: (button) => {
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>'
2021-05-15 21:08:41 +02:00
let qualityHtml = `<li class="vjs-menu-item quality-item" role="menuitemradio" tabindex="-1" aria-checked="false" aria-disabled="false">
<span class="vjs-menu-item-text">Auto</span>
<span class="vjs-control-text" aria-live="polite"></span>
</li>`
levels.levels_.sort((a, b) => {
if (b.height === a.height) {
return b.bitrate - a.bitrate
} else {
return b.height - a.height
}
}).forEach((quality, index, array) => {
let fps
let qualityLabel
let bitrate
2021-05-15 21:08:41 +02:00
if (typeof this.adaptiveFormats !== 'undefined' && this.adaptiveFormats.length > 0) {
const adaptiveFormat = this.adaptiveFormats.find((format) => {
return format.bitrate === quality.bitrate
})
2021-05-15 21:08:41 +02:00
this.activeAdaptiveFormats.push(adaptiveFormat)
fps = adaptiveFormat.fps ? adaptiveFormat.fps : 30
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
qualityHtml = qualityHtml + `<li class="vjs-menu-item quality-item" role="menuitemradio" tabindex="-1" aria-checked="false" aria-disabled="false" fps="${fps}" bitrate="${bitrate}">
<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
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'
qualityHtml = qualityHtml + `<li class="vjs-menu-item quality-item" role="menuitemradio" tabindex="-1" aria-checked="false aria-disabled="false">
<span class="vjs-menu-item-text">${qualityText}</span>
<span class="vjs-control-text" aria-live="polite"></span>
2021-05-15 21:08:41 +02:00
</li>` */
})
return $(button).html(
$(beginningHtml + qualityHtml + endingHtml).attr(
'title',
'Select Quality'
))
}
})
videojs.registerComponent('dashQualitySelector', dashQualitySelector)
this.player.controlBar.addChild('dashQualitySelector', {}, this.player.controlBar.children_.length - 1)
this.determineDefaultQualityDash()
},
sortCaptions: function (captionList) {
return captionList.sort((captionA, captionB) => {
const aCode = captionA.languageCode.split('-') // ex. [en,US]
const bCode = captionB.languageCode.split('-')
const aName = (captionA.label || captionA.name.simpleText) // ex: english (auto-generated)
const bName = (captionB.label || captionB.name.simpleText)
const userLocale = this.currentLocale.split(/-|_/) // ex. [en,US]
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
} 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
return aName.localeCompare(bName)
})
},
2021-03-17 02:28:25 +01:00
transformAndInsertCaptions: async function() {
let captionList
if (this.captionHybridList[0] instanceof Promise) {
captionList = await Promise.all(this.captionHybridList)
this.$emit('store-caption-list', captionList)
} else {
captionList = this.captionHybridList
}
for (const caption of this.sortCaptions(captionList)) {
2021-03-17 02:28:25 +01:00
this.player.addRemoteTextTrack({
kind: 'subtitles',
src: caption.baseUrl || caption.url,
srclang: caption.languageCode,
label: caption.label || caption.name.simpleText,
type: caption.type
}, true)
}
},
toggleFullWindow: function() {
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
$('body').removeClass('vjs-full-window')
$('#fullwindow').removeClass('vjs-icon-fullwindow-exit')
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'
$('body').addClass('vjs-full-window')
$('#fullwindow').addClass('vjs-icon-fullwindow-exit')
this.player.trigger('enterFullWindow')
}
}
},
exitFullWindow: function() {
if (this.player.isFullWindow) {
this.player.isFullWindow = false
document.documentElement.style.overflow = this.player.docOrigOverflow
this.player.removeClass('vjs-full-screen')
$('body').removeClass('vjs-full-window')
$('#fullwindow').removeClass('vjs-icon-fullwindow-exit')
this.player.trigger('exitFullWindow')
}
},
toggleFullscreen: function () {
if (this.player.isFullscreen()) {
this.player.exitFullscreen()
} else {
this.player.requestFullscreen()
}
},
2020-02-21 19:31:32 +01:00
hideMouseTimeout: function () {
if (this.id === '') {
return
}
const videoPlayer = $(`#${this.id} video`).get(0)
if (typeof (videoPlayer) !== 'undefined') {
videoPlayer.style.cursor = 'default'
clearTimeout(this.mouseTimeout)
this.mouseTimeout = window.setTimeout(function () {
videoPlayer.style.cursor = 'none'
}, 2650)
}
},
removeMouseTimeout: function () {
if (this.mouseTimeout !== null) {
clearTimeout(this.mouseTimeout)
}
},
fullscreenOverlay: function () {
const title = document.title.replace('- FreeTube', '')
if (this.player.isFullscreen()) {
this.player.ready(() => {
this.player.overlay({
overlays: [{
showBackground: false,
content: title,
start: 'mousemove',
end: 'userinactive'
}]
})
})
} else {
this.player.ready(() => {
this.player.overlay({
overlays: [{
showBackground: false,
content: ' ',
start: 'play',
end: 'loadstart'
}]
})
})
}
},
toggleFullscreenClass: function () {
if (this.player.isFullscreen()) {
$('body').addClass('vjs--full-screen-enabled')
} else {
$('body').removeClass('vjs--full-screen-enabled')
}
},
handleTouchStart: function (event) {
this.touchPauseTimeout = setTimeout(() => {
this.togglePlayPause()
}, 1000)
const touchTime = new Date()
if (this.lastTouchTime !== null && (touchTime.getTime() - this.lastTouchTime.getTime()) < 250) {
this.toggleFullscreen()
}
this.lastTouchTime = touchTime
},
handleTouchEnd: function (event) {
clearTimeout(this.touchPauseTimeout)
},
keyboardShortcutHandler: function (event) {
const activeInputs = $('.ft-input')
for (let i = 0; i < activeInputs.length; i++) {
if (activeInputs[i] === document.activeElement) {
return
}
}
if (event.ctrlKey) {
return
}
if (this.player !== null) {
switch (event.which) {
case 32:
// Space Bar
// Toggle Play/Pause
event.preventDefault()
this.togglePlayPause()
break
case 74:
// J Key
// Rewind by 2x the time-skip interval (in seconds)
event.preventDefault()
this.changeDurationBySeconds(-this.defaultSkipInterval * 2)
break
case 75:
// K Key
// Toggle Play/Pause
event.preventDefault()
this.togglePlayPause()
break
case 76:
// L Key
// Fast-Forward by 2x the time-skip interval (in seconds)
event.preventDefault()
this.changeDurationBySeconds(this.defaultSkipInterval * 2)
break
case 79:
// O Key
// Decrease playback rate by 0.25x
event.preventDefault()
this.changePlayBackRate(-0.25)
break
case 80:
// P Key
// Increase playback rate by 0.25x
event.preventDefault()
this.changePlayBackRate(0.25)
break
case 70:
// F Key
// Toggle Fullscreen Playback
event.preventDefault()
this.toggleFullscreen()
break
case 77:
// M Key
// Toggle Mute
event.preventDefault()
this.toggleMute()
break
case 67:
// C Key
// Toggle Captions
event.preventDefault()
this.toggleCaptions()
break
case 38:
// Up Arrow Key
// Increase volume
event.preventDefault()
this.changeVolume(0.05)
break
case 40:
// Down Arrow Key
// Decrease Volume
event.preventDefault()
this.changeVolume(-0.05)
break
case 37:
// Left Arrow Key
// Rewind by the time-skip interval (in seconds)
event.preventDefault()
this.changeDurationBySeconds(-this.defaultSkipInterval * 1)
break
case 39:
// Right Arrow Key
// Fast-Forward by the time-skip interval (in seconds)
event.preventDefault()
this.changeDurationBySeconds(this.defaultSkipInterval * 1)
break
case 49:
// 1 Key
// Jump to 10% in the video
event.preventDefault()
this.changeDurationByPercentage(0.1)
break
case 50:
// 2 Key
// Jump to 20% in the video
event.preventDefault()
this.changeDurationByPercentage(0.2)
break
case 51:
// 3 Key
// Jump to 30% in the video
event.preventDefault()
this.changeDurationByPercentage(0.3)
break
case 52:
// 4 Key
// Jump to 40% in the video
event.preventDefault()
this.changeDurationByPercentage(0.4)
break
case 53:
// 5 Key
// Jump to 50% in the video
event.preventDefault()
this.changeDurationByPercentage(0.5)
break
case 54:
// 6 Key
// Jump to 60% in the video
event.preventDefault()
this.changeDurationByPercentage(0.6)
break
case 55:
// 7 Key
// Jump to 70% in the video
event.preventDefault()
this.changeDurationByPercentage(0.7)
break
case 56:
// 8 Key
// Jump to 80% in the video
event.preventDefault()
this.changeDurationByPercentage(0.8)
break
case 57:
// 9 Key
// Jump to 90% in the video
event.preventDefault()
this.changeDurationByPercentage(0.9)
break
case 48:
// 0 Key
// Jump to 0% in the video (The beginning)
event.preventDefault()
this.changeDurationByPercentage(0)
break
case 188:
// , Key
// Return to previous frame
this.framebyframe(-1)
break
case 190:
// . Key
// Advance to next frame
this.framebyframe(1)
break
case 68:
// D Key
// Toggle Picture in Picture Mode
if (!this.player.isInPictureInPicture()) {
this.player.requestPictureInPicture()
} else if (this.player.isInPictureInPicture()) {
this.player.exitPictureInPicture()
}
break
case 27:
// esc Key
// Exit full window
event.preventDefault()
this.exitFullWindow()
break
case 83:
// S Key
// Toggle Full Window Mode
this.toggleFullWindow()
break
2021-09-15 04:00:34 +02:00
case 84:
// T Key
// Toggle Theatre Mode
this.toggleTheatreMode()
break
}
}
},
...mapActions([
'calculateColorLuminance',
'updateDefaultCaptionSettings',
'showToast',
'sponsorBlockSkipSegments'
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
]),
addPlayerStatsEvent: function() {
this.stats.videoId = this.videoId
this.player.on('volumechange', () => {
this.stats.volume = this.player.volume()
this.player.trigger(this.stats.display.event)
})
this.player.on('timeupdate', () => {
const stats = this.player.tech({ IWillNotUseThisInPlugins: true }).vhs.stats
this.stats.frameInfo = stats.videoPlaybackQuality
this.player.trigger(this.stats.display.event)
})
this.player.on('progress', () => {
const stats = this.player.tech({ IWillNotUseThisInPlugins: true }).vhs.stats
this.stats.bandwidth = stats.bandwidth
this.stats.bufferPercent = this.player.bufferedPercent()
})
this.player.on('playerresize', () => {
this.stats.playerResolution = this.player.currentDimensions()
this.player.trigger(this.stats.display.event)
})
this.createStatsModal()
this.player.on(this.stats.display.event, () => {
if (this.stats.display.activated) {
this.stats.display.modal.open()
this.player.controls(true)
this.stats.display.modal.contentEl().innerHTML = this.formatted_stats
} else {
this.stats.display.modal.close()
}
})
// keyboard shortcut
window.addEventListener('keyup', (event) => {
if (event.code === this.stats.display.keyboardShortcut) {
if (this.stats.display.activated) {
this.deactivateStatsDisplay()
} else {
this.activateStatsDisplay()
}
}
}, true)
// right click menu
ipcRenderer.on(this.stats.display.rightClickEvent, () => {
this.activateStatsDisplay()
})
},
createStatsModal: function() {
const ModalDialog = videojs.getComponent('ModalDialog')
this.stats.display.modal = new ModalDialog(this.player, {
temporary: false,
pauseOnOpen: false
})
this.player.addChild(this.stats.display.modal)
this.stats.display.modal.height('35%')
this.stats.display.modal.width('50%')
this.stats.display.modal.contentEl().style.backgroundColor = 'rgba(0, 0, 0, 0.55)'
this.stats.display.modal.on('modalclose', () => {
this.deactivateStatsDisplay()
})
},
activateStatsDisplay: function() {
this.stats.display.activated = true
},
deactivateStatsDisplay: function() {
this.stats.display.activated = false
},
currentFps: function() {
for (const el of this.activeAdaptiveFormats) {
if (el.qualityLabel === this.selectedQuality) {
this.stats.fps = el.fps
break
}
}
}
2020-02-16 19:30:00 +01:00
}
})