Implement preferred quality selection
The list is subject to change as I collect more feedback. I just want to get this initial change out for everyone to use and appreciate.
This commit is contained in:
parent
fd854ec222
commit
ac3de4b4e6
68
api/video.js
68
api/video.js
|
@ -37,6 +37,53 @@ function formatOrder(format) {
|
|||
return -total
|
||||
}
|
||||
|
||||
function sortFormats(video, preference) {
|
||||
const standard = video.formatStreams.slice().sort((a, b) => b.second__height - a.second__height)
|
||||
const adaptive = video.adaptiveFormats.filter(f => f.type.startsWith("video") && f.qualityLabel).sort((a, b) => a.second__order - b.second__order)
|
||||
let formats = standard.concat(adaptive)
|
||||
|
||||
for (const format of formats) {
|
||||
if (!format.second__height && format.resolution) format.second__height = +format.resolution.slice(0, -1)
|
||||
if (!format.second__order) format.second__order = formatOrder(format)
|
||||
format.cloudtube__label = `${format.qualityLabel} ${format.container}`
|
||||
}
|
||||
for (const format of adaptive) {
|
||||
format.cloudtube__label += " *"
|
||||
}
|
||||
|
||||
if (preference === 1) { // best dash
|
||||
formats.sort((a, b) => (b.second__height - a.second__height))
|
||||
} else if (preference === 2) { // best <=1080p
|
||||
formats.sort((a, b) => {
|
||||
if (b.second__height > 1080) {
|
||||
if (a.second__height > 1080) return b.second__height - a.second__height
|
||||
return -1
|
||||
}
|
||||
if (a.second__height > 1080) return 1
|
||||
return b.second__height - a.second__height
|
||||
})
|
||||
} else if (preference === 3) { // best low-fps
|
||||
formats.sort((a, b) => {
|
||||
if (b.fps > 30) {
|
||||
if (a.fps < 30) return b.second__height - a.second__height
|
||||
return -1
|
||||
}
|
||||
if (a.fps > 30) return 1
|
||||
return b.second__height - a.second__height
|
||||
})
|
||||
} else if (preference === 4) { // 360p only
|
||||
formats.sort((a, b) => {
|
||||
if (a.itag == 18) return -1
|
||||
if (b.itag == 18) return 1
|
||||
return 0
|
||||
})
|
||||
} else { // preference === 0, best combined
|
||||
// should already be correct
|
||||
}
|
||||
|
||||
return formats
|
||||
}
|
||||
|
||||
function rewriteVideoDescription(descriptionHtml, id) {
|
||||
// replace timestamps to clickable links and rewrite youtube links to stay on the instance instead of pointing to YouTube
|
||||
// test cases
|
||||
|
@ -71,23 +118,24 @@ function rewriteVideoDescription(descriptionHtml, id) {
|
|||
return descriptionHtml
|
||||
}
|
||||
|
||||
async function renderVideo(videoPromise, {user, id, instanceOrigin}, locals = {}) {
|
||||
async function renderVideo(videoPromise, {user, settings, id, instanceOrigin}, locals = {}) {
|
||||
try {
|
||||
// resolve video
|
||||
const video = await videoPromise
|
||||
if (!video) throw new Error("The instance returned null.")
|
||||
if (video.error) throw new InstanceError(video.error, video.identifier)
|
||||
|
||||
// process stream list ordering
|
||||
for (const format of video.formatStreams.concat(video.adaptiveFormats)) {
|
||||
if (!format.second__height && format.resolution) format.second__height = +format.resolution.slice(0, -1)
|
||||
if (!format.second__order) format.second__order = formatOrder(format)
|
||||
}
|
||||
const formats = sortFormats(video, settings.quality)
|
||||
|
||||
// process length text and view count
|
||||
for (const rec of video.recommendedVideos) {
|
||||
converters.normaliseVideoInfo(rec)
|
||||
}
|
||||
|
||||
// get subscription data
|
||||
const subscribed = user.isSubscribed(video.authorId)
|
||||
|
||||
// process watched videos
|
||||
user.addWatchedVideoMaybe(video.videoId)
|
||||
const watchedVideos = user.getWatchedVideos()
|
||||
|
@ -96,12 +144,16 @@ async function renderVideo(videoPromise, {user, id, instanceOrigin}, locals = {}
|
|||
rec.watched = watchedVideos.includes(rec.videoId)
|
||||
}
|
||||
}
|
||||
|
||||
// normalise view count
|
||||
if (!video.second__viewCountText && video.viewCount) {
|
||||
video.second__viewCountText = converters.viewCountToText(video.viewCount)
|
||||
}
|
||||
|
||||
// rewrite description
|
||||
video.descriptionHtml = rewriteVideoDescription(video.descriptionHtml, id)
|
||||
return render(200, "pug/video.pug", Object.assign(locals, {video, subscribed, instanceOrigin}))
|
||||
|
||||
return render(200, "pug/video.pug", Object.assign(locals, {video, formats, subscribed, instanceOrigin}))
|
||||
} catch (e) {
|
||||
// show an appropriate error message
|
||||
// these should probably be split out to their own files
|
||||
|
@ -168,7 +220,7 @@ module.exports = [
|
|||
const instanceOrigin = settings.instance
|
||||
const outURL = `${instanceOrigin}/api/v1/videos/${id}`
|
||||
const videoPromise = request(outURL).then(res => res.json())
|
||||
return renderVideo(videoPromise, {user, id, instanceOrigin}, {mediaFragment})
|
||||
return renderVideo(videoPromise, {user, settings, id, instanceOrigin}, {mediaFragment})
|
||||
} else {
|
||||
return render(200, "pug/local-video.pug", {id})
|
||||
}
|
||||
|
@ -176,7 +228,7 @@ module.exports = [
|
|||
const video = JSON.parse(new URLSearchParams(body.toString()).get("video"))
|
||||
const videoPromise = Promise.resolve(video)
|
||||
const instanceOrigin = "http://localhost:3000"
|
||||
return renderVideo(videoPromise, {user, id, instanceOrigin}, {mediaFragment})
|
||||
return renderVideo(videoPromise, {user, settings, id, instanceOrigin}, {mediaFragment})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,10 +87,11 @@ const playManagers = {
|
|||
class QualitySelect extends ElemJS {
|
||||
constructor() {
|
||||
super(q("#quality-select"))
|
||||
this.on("input", this.onInput.bind(this))
|
||||
this.on("input", this.setFormat.bind(this))
|
||||
this.setFormat()
|
||||
}
|
||||
|
||||
onInput() {
|
||||
setFormat() {
|
||||
const itag = this.element.value
|
||||
formatLoader.play(itag)
|
||||
video.focus()
|
||||
|
|
|
@ -32,6 +32,14 @@ block content
|
|||
|
||||
+input("instance", "Instance", "url", constants.user_settings.instance.default, false, instances)
|
||||
|
||||
+select("quality", "Preferred qualities", false, [
|
||||
{value: "0", text: "Best combined"},
|
||||
{value: "1", text: "Best DASH"},
|
||||
{value: "2", text: "Best <=1080p"},
|
||||
{value: "3", text: "Best low-fps"},
|
||||
{value: "4", text: "360p"}
|
||||
])
|
||||
|
||||
+select("save_history", "Watched videos history", false, [
|
||||
{value: "0", text: "Don't store"},
|
||||
{value: "1", text: "Store on server"}
|
||||
|
|
|
@ -14,12 +14,9 @@ block head
|
|||
block content
|
||||
unless error
|
||||
main.video-page
|
||||
- const sortedFormatStreams = video.formatStreams.slice().sort((a, b) => b.second__height - a.second__height)
|
||||
- const sortedVideoAdaptiveFormats = video.adaptiveFormats.filter(f => f.type.startsWith("video") && f.qualityLabel).sort((a, b) => a.second__order - b.second__order)
|
||||
|
||||
.main-video-section
|
||||
.video-container
|
||||
- const format = sortedFormatStreams[0]
|
||||
- const format = formats[0]
|
||||
if format
|
||||
video(controls preload="auto" width=format.second__width height=format.second__height data-itag=format.itag)#video.video
|
||||
source(src=format.url+mediaFragment type=format.type)
|
||||
|
@ -49,10 +46,8 @@ block content
|
|||
+subscribe_button(video.authorId, subscribed, `/watch?v=${video.videoId}`).border-look
|
||||
//- button.border-look#theatre Theatre
|
||||
select(autocomplete="off").border-look#quality-select
|
||||
each f in sortedFormatStreams
|
||||
option(value=f.itag)= `${f.qualityLabel} ${f.container}`
|
||||
each f in sortedVideoAdaptiveFormats
|
||||
option(value=f.itag)= `${f.qualityLabel} ${f.container} *`
|
||||
each f in formats
|
||||
option(value=f.itag)= f.cloudtube__label
|
||||
//-
|
||||
a(href="/subscriptions").border-look
|
||||
img(src="/static/images/search.svg" width=17 height=17 alt="").button-icon
|
||||
|
|
|
@ -16,6 +16,10 @@ let constants = {
|
|||
local: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
quality: {
|
||||
type: "integer",
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -56,7 +60,8 @@ try {
|
|||
const overrides = require("../config/config.js")
|
||||
constants = mixin(constants, overrides)
|
||||
} catch (e) {
|
||||
console.log("Note: overrides file `config/config.js` ignored, file not found.")
|
||||
console.error("Missing config file /config/config.js\nDocumentation: https://git.sr.ht/~cadence/tube-docs/tree/main/item/docs")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
module.exports = constants
|
||||
|
|
|
@ -38,6 +38,11 @@ const deltas = [
|
|||
function() {
|
||||
db.prepare("UPDATE Settings SET instance = REPLACE(REPLACE(instance, '/', ''), ':', '://') WHERE instance LIKE '%/'")
|
||||
.run()
|
||||
},
|
||||
// 5: Settings +quality
|
||||
function() {
|
||||
db.prepare("ALTER TABLE Settings ADD COLUMN quality INTEGER DEFAULT 0")
|
||||
.run()
|
||||
}
|
||||
]
|
||||
|
||||
|
|
Loading…
Reference in New Issue