mirror of https://github.com/FreeTubeApp/FreeTube
Fix DASH default quality and quality selection (#3278)
* Fix DASH default quality and quality selection * Add comments and multiple bandwidth by 10 Co-authored-by: PikachuEXE <pikachuexe@gmail.com> * Fix typo Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> * ! Fix default quality `auto` handling * ! Fix quality selection for short/vertical videos * ! Fix default quality selection for Invidious API * * Update videojs bandwidth option value to max * ~ Add comment about video format sorting logic when heights from 2 formats equal * * Revert format preference back to higher bitrate with comments * * Revert inflated initial bandwidth option value to fix #595 --------- Co-authored-by: PikachuEXE <pikachuexe@gmail.com> Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
This commit is contained in:
parent
f0ba90e766
commit
a53e7f0598
|
@ -28,6 +28,11 @@ videojs.Vhs.xhr.beforeRequest = (options) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtVideoPlayer',
|
||||
props: {
|
||||
|
@ -150,7 +155,10 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
defaultQuality: function () {
|
||||
return parseInt(this.$store.getters.getDefaultQuality)
|
||||
const valueFromStore = this.$store.getters.getDefaultQuality
|
||||
if (valueFromStore === 'auto') { return valueFromStore }
|
||||
|
||||
return parseInt(valueFromStore)
|
||||
},
|
||||
|
||||
defaultCaptionSettings: function () {
|
||||
|
@ -320,6 +328,10 @@ export default defineComponent({
|
|||
|
||||
this.dataSetup.playbackRates = this.playbackRates
|
||||
|
||||
if (this.format === 'dash') {
|
||||
this.determineDefaultQualityDash()
|
||||
}
|
||||
|
||||
this.createFullWindowButton()
|
||||
this.createLoopButton()
|
||||
this.createToggleTheatreModeButton()
|
||||
|
@ -369,6 +381,18 @@ export default defineComponent({
|
|||
controlBarItems.splice(index, 1)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
this.player = videojs(this.$refs.video, {
|
||||
html5: {
|
||||
preloadTextTracks: false,
|
||||
|
@ -376,7 +400,8 @@ export default defineComponent({
|
|||
limitRenditionByPlayerDimensions: false,
|
||||
smoothQualityChange: false,
|
||||
allowSeeksWithinUnsafeLiveWindow: true,
|
||||
handlePartialData: true
|
||||
handlePartialData: true,
|
||||
...playerBandwidthOption
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -397,6 +422,15 @@ export default defineComponent({
|
|||
}
|
||||
})
|
||||
|
||||
// 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') {
|
||||
const qualityLevels = this.player.qualityLevels()
|
||||
qualityLevels.on('addqualitylevel', ({ qualityLevel }) => {
|
||||
qualityLevel.enabled = qualityLevel.bitrate === this.selectedBitrate
|
||||
})
|
||||
}
|
||||
|
||||
this.player.volume(this.volume)
|
||||
this.player.muted(this.muted)
|
||||
this.player.playbackRate(this.defaultPlayback)
|
||||
|
@ -907,92 +941,67 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
determineDefaultQualityDash: function () {
|
||||
// TODO add settings and filtering for 60fps and HDR
|
||||
|
||||
if (this.defaultQuality === 'auto') {
|
||||
this.setDashQualityLevel('auto')
|
||||
}
|
||||
|
||||
let formatsToTest
|
||||
|
||||
if (typeof this.activeAdaptiveFormats !== 'undefined' && this.activeAdaptiveFormats.length > 0) {
|
||||
formatsToTest = this.activeAdaptiveFormats.filter((format) => {
|
||||
return format.height === this.defaultQuality
|
||||
})
|
||||
|
||||
if (formatsToTest.length === 0) {
|
||||
formatsToTest = this.activeAdaptiveFormats.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
|
||||
}
|
||||
})
|
||||
this.selectedResolution = 'auto'
|
||||
this.selectedFPS = 'auto'
|
||||
this.selectedBitrate = 'auto'
|
||||
this.selectedMimeType = 'auto'
|
||||
} else {
|
||||
formatsToTest = this.player.qualityLevels().levels_.filter((format) => {
|
||||
const videoFormats = this.adaptiveFormats.filter(format => {
|
||||
return (format.mimeType || format.type).startsWith('video') &&
|
||||
typeof format.height === 'number'
|
||||
})
|
||||
|
||||
// Select the quality that is identical to the users chosen default quality if it's available
|
||||
// otherwise select the next lowest quality
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return format.height === this.defaultQuality
|
||||
})
|
||||
|
||||
if (formatsToTest.length === 0) {
|
||||
formatsToTest = this.player.qualityLevels().levels_.filter((format) => {
|
||||
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
|
||||
}
|
||||
|
||||
return format.height < this.defaultQuality
|
||||
})
|
||||
}
|
||||
|
||||
formatsToTest = formatsToTest.sort((a, b) => {
|
||||
formatsToTest.sort((a, b) => {
|
||||
if (a.height === b.height) {
|
||||
// Higher bitrate for video formats with HDR qualities
|
||||
// `height` and `fps` are the same but `bitrate` would be higher
|
||||
return b.bitrate - a.bitrate
|
||||
} else {
|
||||
return b.height - a.height
|
||||
}
|
||||
})
|
||||
|
||||
const selectedFormat = formatsToTest[0]
|
||||
this.selectedBitrate = selectedFormat.bitrate
|
||||
this.selectedResolution = `${selectedFormat.width}x${selectedFormat.height}`
|
||||
this.selectedFPS = selectedFormat.fps
|
||||
this.selectedMimeType = selectedFormat.mimeType || selectedFormat.type
|
||||
}
|
||||
|
||||
// TODO: Test formats to determine if HDR / 60 FPS and skip them based on
|
||||
// User settings
|
||||
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
|
||||
}
|
||||
}).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)
|
||||
} 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)
|
||||
}
|
||||
} else if (index === 0 && quality > this.defaultQuality) {
|
||||
this.setDashQualityLevel(height)
|
||||
} else if (index === (arr.length - 1) && quality < this.defaultQuality) {
|
||||
this.setDashQualityLevel(height)
|
||||
}
|
||||
}) */
|
||||
},
|
||||
|
||||
setDashQualityLevel: function (bitrate) {
|
||||
if (bitrate === this.selectedBitrate) {
|
||||
return
|
||||
}
|
||||
|
||||
let adaptiveFormat = null
|
||||
|
||||
if (bitrate !== 'auto') {
|
||||
|
@ -1003,24 +1012,39 @@ export default defineComponent({
|
|||
|
||||
let qualityLabel = adaptiveFormat ? adaptiveFormat.qualityLabel : ''
|
||||
|
||||
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) {
|
||||
const qualityLevels = Array.from(this.player.qualityLevels())
|
||||
if (bitrate === 'auto') {
|
||||
qualityLevels.forEach(ql => {
|
||||
ql.enabled = true
|
||||
ql.enabled_(true)
|
||||
if (bitrate !== 'auto' && qualityLabel === '') {
|
||||
qualityLabel = ql.height + 'p'
|
||||
})
|
||||
} 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'
|
||||
}
|
||||
|
||||
qualityLevels.find(ql => previousBitrate === ql.bitrate).enabled = false
|
||||
} else {
|
||||
ql.enabled = false
|
||||
ql.enabled_(false)
|
||||
qualityLevels.forEach(ql => {
|
||||
if (bitrate === ql.bitrate) {
|
||||
ql.enabled = true
|
||||
if (qualityLabel === '') {
|
||||
qualityLabel = ql.height + 'p'
|
||||
}
|
||||
} else {
|
||||
ql.enabled = false
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const selectedQuality = bitrate === 'auto' ? 'auto' : qualityLabel
|
||||
|
||||
|
@ -1526,6 +1550,8 @@ export default defineComponent({
|
|||
const adaptiveFormats = this.adaptiveFormats
|
||||
const activeAdaptiveFormats = this.activeAdaptiveFormats
|
||||
const setDashQualityLevel = this.setDashQualityLevel
|
||||
const defaultQuality = this.defaultQuality
|
||||
const defaultBitrate = this.selectedBitrate
|
||||
|
||||
const VjsButton = videojs.getComponent('Button')
|
||||
class dashQualitySelector extends VjsButton {
|
||||
|
@ -1543,12 +1569,16 @@ export default defineComponent({
|
|||
<ul class="vjs-menu-content" role="menu">`
|
||||
const endingHtml = '</ul></div>'
|
||||
|
||||
let qualityHtml = `<li class="vjs-menu-item quality-item" role="menuitemradio" tabindex="-1" aria-checked="false" aria-disabled="false">
|
||||
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">
|
||||
<span class="vjs-menu-item-text">Auto</span>
|
||||
<span class="vjs-control-text" aria-live="polite"></span>
|
||||
</li>`
|
||||
|
||||
levels.levels_.sort((a, b) => {
|
||||
let currentQualityLabel
|
||||
|
||||
Array.from(levels).sort((a, b) => {
|
||||
if (b.height === a.height) {
|
||||
return b.bitrate - a.bitrate
|
||||
} else {
|
||||
|
@ -1579,7 +1609,13 @@ export default defineComponent({
|
|||
bitrate = quality.bitrate
|
||||
}
|
||||
|
||||
qualityHtml += `<li class="vjs-menu-item quality-item" role="menuitemradio" tabindex="-1" aria-checked="false" aria-disabled="false" fps="${fps}" bitrate="${bitrate}">
|
||||
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}">
|
||||
<span class="vjs-menu-item-text" fps="${fps}" bitrate="${bitrate}">${qualityLabel}</span>
|
||||
<span class="vjs-control-text" aria-live="polite"></span>
|
||||
</li>`
|
||||
|
@ -1602,13 +1638,14 @@ export default defineComponent({
|
|||
button.title = 'Select Quality'
|
||||
button.innerHTML = beginningHtml + qualityHtml + endingHtml
|
||||
|
||||
button.querySelector('#vjs-current-quality').innerText = defaultIsAuto ? 'auto' : currentQualityLabel
|
||||
|
||||
return button.children[0]
|
||||
}
|
||||
}
|
||||
|
||||
videojs.registerComponent('dashQualitySelector', dashQualitySelector)
|
||||
this.player.controlBar.addChild('dashQualitySelector', {}, this.player.controlBar.children_.length - 1)
|
||||
this.determineDefaultQualityDash()
|
||||
},
|
||||
|
||||
sortCaptions: function (captionList) {
|
||||
|
|
|
@ -232,3 +232,33 @@ function parseInvidiousCommunityAttachments(data) {
|
|||
|
||||
console.error('New Invidious Community Post Type: ' + data.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* video.js only supports MP4 DASH not WebM DASH
|
||||
* so we filter out the WebM DASH formats
|
||||
* @param {any[]} formats
|
||||
* @param {boolean} allowAv1 Use the AV1 formats if they are available
|
||||
*/
|
||||
export function filterInvidiousFormats(formats, allowAv1 = false) {
|
||||
const audioFormats = []
|
||||
const h264Formats = []
|
||||
const av1Formats = []
|
||||
|
||||
formats.forEach(format => {
|
||||
const mimeType = format.type
|
||||
|
||||
if (mimeType.startsWith('audio/mp4')) {
|
||||
audioFormats.push(format)
|
||||
} else if (allowAv1 && mimeType.startsWith('video/mp4; codecs="av01')) {
|
||||
av1Formats.push(format)
|
||||
} else if (mimeType.startsWith('video/mp4; codecs="avc')) {
|
||||
h264Formats.push(format)
|
||||
}
|
||||
})
|
||||
|
||||
if (allowAv1 && av1Formats.length > 0) {
|
||||
return [...audioFormats, ...av1Formats]
|
||||
} else {
|
||||
return [...audioFormats, ...h264Formats]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -576,6 +576,7 @@ export function mapLocalFormat(format) {
|
|||
bitrate: format.bitrate,
|
||||
mimeType: format.mime_type,
|
||||
height: format.height,
|
||||
width: format.width,
|
||||
url: format.url
|
||||
}
|
||||
}
|
||||
|
@ -620,7 +621,7 @@ export function parseLocalComment(comment, commentThread = undefined) {
|
|||
* @param {Format[]} formats
|
||||
* @param {boolean} allowAv1 Use the AV1 formats if they are available
|
||||
*/
|
||||
export function filterFormats(formats, allowAv1 = false) {
|
||||
export function filterLocalFormats(formats, allowAv1 = false) {
|
||||
const audioFormats = []
|
||||
const h264Formats = []
|
||||
const av1Formats = []
|
||||
|
|
|
@ -22,14 +22,14 @@ import {
|
|||
showToast
|
||||
} from '../../helpers/utils'
|
||||
import {
|
||||
filterFormats,
|
||||
filterLocalFormats,
|
||||
getLocalVideoInfo,
|
||||
mapLocalFormat,
|
||||
parseLocalSubscriberCount,
|
||||
parseLocalTextRuns,
|
||||
parseLocalWatchNextVideo
|
||||
} from '../../helpers/api/local'
|
||||
import { invidiousGetVideoInformation, youtubeImageUrlToInvidious } from '../../helpers/api/invidious'
|
||||
import { filterInvidiousFormats, invidiousGetVideoInformation, youtubeImageUrlToInvidious } from '../../helpers/api/invidious'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Watch',
|
||||
|
@ -486,7 +486,7 @@ export default defineComponent({
|
|||
if (result.streaming_data.formats.length > 0) {
|
||||
this.videoSourceList = result.streaming_data.formats.map(mapLocalFormat).reverse()
|
||||
} else {
|
||||
this.videoSourceList = filterFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats).map(mapLocalFormat).reverse()
|
||||
this.videoSourceList = filterLocalFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats).map(mapLocalFormat).reverse()
|
||||
}
|
||||
this.adaptiveFormats = this.videoSourceList
|
||||
|
||||
|
@ -584,7 +584,7 @@ export default defineComponent({
|
|||
}).reverse()
|
||||
|
||||
// we need to alter the result object so the toDash function uses the filtered formats too
|
||||
result.streaming_data.adaptive_formats = filterFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats)
|
||||
result.streaming_data.adaptive_formats = filterLocalFormats(result.streaming_data.adaptive_formats, this.allowDashAv1Formats)
|
||||
|
||||
this.adaptiveFormats = result.streaming_data.adaptive_formats.map(mapLocalFormat)
|
||||
if (this.proxyVideos) {
|
||||
|
@ -668,13 +668,14 @@ export default defineComponent({
|
|||
this.videoPublished = result.published * 1000
|
||||
this.videoDescriptionHtml = result.descriptionHtml
|
||||
this.recommendedVideos = result.recommendedVideos
|
||||
this.adaptiveFormats = result.adaptiveFormats.map((format) => {
|
||||
format.bitrate = parseInt(format.bitrate)
|
||||
if (typeof format.resolution !== 'undefined') {
|
||||
format.height = parseInt(format.resolution.replace('p', ''))
|
||||
}
|
||||
return format
|
||||
})
|
||||
this.adaptiveFormats = filterInvidiousFormats(result.adaptiveFormats, this.allowDashAv1Formats)
|
||||
.map((format) => {
|
||||
format.bitrate = parseInt(format.bitrate)
|
||||
if (typeof format.resolution !== 'undefined') {
|
||||
format.height = parseInt(format.resolution.replace('p', ''))
|
||||
}
|
||||
return format
|
||||
})
|
||||
this.isLive = result.liveNow
|
||||
this.isFamilyFriendly = result.isFamilyFriendly
|
||||
this.captionHybridList = result.captions.map(caption => {
|
||||
|
|
Loading…
Reference in New Issue