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:
absidue 2023-03-23 01:22:20 +01:00 committed by GitHub
parent f0ba90e766
commit a53e7f0598
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 167 additions and 98 deletions

View File

@ -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) {

View File

@ -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]
}
}

View File

@ -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 = []

View File

@ -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 => {