Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix DASH default quality and quality selection #3278

Merged
187 changes: 100 additions & 87 deletions src/renderer/components/ft-video-player/ft-video-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -320,6 +325,10 @@ export default defineComponent({

this.dataSetup.playbackRates = this.playbackRates

if (this.format === 'dash') {
this.determineDefaultQualityDash()
}

this.createFullWindowButton()
this.createLoopButton()
this.createToggleTheatreModeButton()
Expand Down Expand Up @@ -369,14 +378,25 @@ export default defineComponent({
controlBarItems.splice(index, 1)
}

// regardless of what DASH qualities you enable or disable in the qualitLevels plugin
absidue marked this conversation as resolved.
Show resolved Hide resolved
// 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
playerBandwidthOption.bandwidth = this.selectedBitrate * VHS_BANDWIDTH_VARIANCE * 10 + 1
}

this.player = videojs(this.$refs.video, {
html5: {
preloadTextTracks: false,
vhs: {
limitRenditionByPlayerDimensions: false,
smoothQualityChange: false,
allowSeeksWithinUnsafeLiveWindow: true,
handlePartialData: true
handlePartialData: true,
...playerBandwidthOption
}
}
})
Expand All @@ -397,6 +417,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)
Expand Down Expand Up @@ -895,92 +924,48 @@ export default defineComponent({
},

determineDefaultQualityDash: function () {
if (this.defaultQuality === 'auto') {
this.setDashQualityLevel('auto')
}

let formatsToTest
// TODO add settings and filtering for 60fps and HDR

if (typeof this.activeAdaptiveFormats !== 'undefined' && this.activeAdaptiveFormats.length > 0) {
formatsToTest = this.activeAdaptiveFormats.filter((format) => {
return format.height === this.defaultQuality
if (this.defaultQuality === 'auto') {
this.selectedResolution = 'auto'
this.selectedFPS = 'auto'
this.selectedBitrate = 'auto'
this.selectedMimeType = 'auto'
} else {
const videoFormats = this.adaptiveFormats.filter(format => {
return (format.mimeType || format.type).startsWith('video') && typeof format.height !== 'undefined'
})

if (formatsToTest.length === 0) {
formatsToTest = this.activeAdaptiveFormats.filter((format) => {
return format.height < this.defaultQuality
})
}
// Select the quality that is identical to the users chosen default quality if it's available
// otherwise select the next lowest quality

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
})
let formatsToTest = videoFormats.filter(format => format.height === this.defaultQuality)

if (formatsToTest.length === 0) {
formatsToTest = this.player.qualityLevels().levels_.filter((format) => {
return format.height < this.defaultQuality
})
formatsToTest = videoFormats.filter(format => format.height < this.defaultQuality)
}

formatsToTest = formatsToTest.sort((a, b) => {
formatsToTest.sort((a, b) => {
if (a.height === b.height) {
return b.bitrate - a.bitrate
} else {
return b.height - a.height
}
})
}

// 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)
}
}) */
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
}
},

setDashQualityLevel: function (bitrate) {
if (bitrate === this.selectedBitrate) {
return
}

let adaptiveFormat = null

if (bitrate !== 'auto') {
Expand All @@ -991,24 +976,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

Expand Down Expand Up @@ -1514,6 +1514,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 {
Expand All @@ -1531,12 +1533,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 {
Expand Down Expand Up @@ -1567,7 +1573,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>`
Expand All @@ -1590,13 +1602,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) {
Expand Down
30 changes: 30 additions & 0 deletions src/renderer/helpers/api/invidious.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,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]
}
}
3 changes: 2 additions & 1 deletion src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ export function mapLocalFormat(format) {
bitrate: format.bitrate,
mimeType: format.mime_type,
height: format.height,
width: format.width,
url: format.url
}
}
Expand Down Expand Up @@ -606,7 +607,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 = []
Expand Down
Loading