1
0
mirror of https://git.sr.ht/~cadence/cloudtube synced 2024-11-22 15:47:30 +00:00

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:
Cadence Ember 2021-04-04 16:51:39 +12:00
parent fd854ec222
commit ac3de4b4e6
No known key found for this signature in database
GPG Key ID: BC1C2C61CF521B17
6 changed files with 85 additions and 19 deletions

View File

@ -37,6 +37,53 @@ function formatOrder(format) {
return -total 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) { function rewriteVideoDescription(descriptionHtml, id) {
// replace timestamps to clickable links and rewrite youtube links to stay on the instance instead of pointing to YouTube // replace timestamps to clickable links and rewrite youtube links to stay on the instance instead of pointing to YouTube
// test cases // test cases
@ -71,23 +118,24 @@ function rewriteVideoDescription(descriptionHtml, id) {
return descriptionHtml return descriptionHtml
} }
async function renderVideo(videoPromise, {user, id, instanceOrigin}, locals = {}) { async function renderVideo(videoPromise, {user, settings, id, instanceOrigin}, locals = {}) {
try { try {
// resolve video // resolve video
const video = await videoPromise const video = await videoPromise
if (!video) throw new Error("The instance returned null.") if (!video) throw new Error("The instance returned null.")
if (video.error) throw new InstanceError(video.error, video.identifier) if (video.error) throw new InstanceError(video.error, video.identifier)
// process stream list ordering // process stream list ordering
for (const format of video.formatStreams.concat(video.adaptiveFormats)) { const formats = sortFormats(video, settings.quality)
if (!format.second__height && format.resolution) format.second__height = +format.resolution.slice(0, -1)
if (!format.second__order) format.second__order = formatOrder(format)
}
// process length text and view count // process length text and view count
for (const rec of video.recommendedVideos) { for (const rec of video.recommendedVideos) {
converters.normaliseVideoInfo(rec) converters.normaliseVideoInfo(rec)
} }
// get subscription data // get subscription data
const subscribed = user.isSubscribed(video.authorId) const subscribed = user.isSubscribed(video.authorId)
// process watched videos // process watched videos
user.addWatchedVideoMaybe(video.videoId) user.addWatchedVideoMaybe(video.videoId)
const watchedVideos = user.getWatchedVideos() const watchedVideos = user.getWatchedVideos()
@ -96,12 +144,16 @@ async function renderVideo(videoPromise, {user, id, instanceOrigin}, locals = {}
rec.watched = watchedVideos.includes(rec.videoId) rec.watched = watchedVideos.includes(rec.videoId)
} }
} }
// normalise view count // normalise view count
if (!video.second__viewCountText && video.viewCount) { if (!video.second__viewCountText && video.viewCount) {
video.second__viewCountText = converters.viewCountToText(video.viewCount) video.second__viewCountText = converters.viewCountToText(video.viewCount)
} }
// rewrite description
video.descriptionHtml = rewriteVideoDescription(video.descriptionHtml, id) 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) { } catch (e) {
// show an appropriate error message // show an appropriate error message
// these should probably be split out to their own files // these should probably be split out to their own files
@ -168,7 +220,7 @@ module.exports = [
const instanceOrigin = settings.instance const instanceOrigin = settings.instance
const outURL = `${instanceOrigin}/api/v1/videos/${id}` const outURL = `${instanceOrigin}/api/v1/videos/${id}`
const videoPromise = request(outURL).then(res => res.json()) const videoPromise = request(outURL).then(res => res.json())
return renderVideo(videoPromise, {user, id, instanceOrigin}, {mediaFragment}) return renderVideo(videoPromise, {user, settings, id, instanceOrigin}, {mediaFragment})
} else { } else {
return render(200, "pug/local-video.pug", {id}) 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 video = JSON.parse(new URLSearchParams(body.toString()).get("video"))
const videoPromise = Promise.resolve(video) const videoPromise = Promise.resolve(video)
const instanceOrigin = "http://localhost:3000" const instanceOrigin = "http://localhost:3000"
return renderVideo(videoPromise, {user, id, instanceOrigin}, {mediaFragment}) return renderVideo(videoPromise, {user, settings, id, instanceOrigin}, {mediaFragment})
} }
} }
} }

View File

@ -87,10 +87,11 @@ const playManagers = {
class QualitySelect extends ElemJS { class QualitySelect extends ElemJS {
constructor() { constructor() {
super(q("#quality-select")) 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 const itag = this.element.value
formatLoader.play(itag) formatLoader.play(itag)
video.focus() video.focus()

View File

@ -32,6 +32,14 @@ block content
+input("instance", "Instance", "url", constants.user_settings.instance.default, false, instances) +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, [ +select("save_history", "Watched videos history", false, [
{value: "0", text: "Don't store"}, {value: "0", text: "Don't store"},
{value: "1", text: "Store on server"} {value: "1", text: "Store on server"}

View File

@ -14,12 +14,9 @@ block head
block content block content
unless error unless error
main.video-page 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 .main-video-section
.video-container .video-container
- const format = sortedFormatStreams[0] - const format = formats[0]
if format if format
video(controls preload="auto" width=format.second__width height=format.second__height data-itag=format.itag)#video.video 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) 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 +subscribe_button(video.authorId, subscribed, `/watch?v=${video.videoId}`).border-look
//- button.border-look#theatre Theatre //- button.border-look#theatre Theatre
select(autocomplete="off").border-look#quality-select select(autocomplete="off").border-look#quality-select
each f in sortedFormatStreams each f in formats
option(value=f.itag)= `${f.qualityLabel} ${f.container}` option(value=f.itag)= f.cloudtube__label
each f in sortedVideoAdaptiveFormats
option(value=f.itag)= `${f.qualityLabel} ${f.container} *`
//- //-
a(href="/subscriptions").border-look a(href="/subscriptions").border-look
img(src="/static/images/search.svg" width=17 height=17 alt="").button-icon img(src="/static/images/search.svg" width=17 height=17 alt="").button-icon

View File

@ -16,6 +16,10 @@ let constants = {
local: { local: {
type: "boolean", type: "boolean",
default: false default: false
},
quality: {
type: "integer",
default: 0
} }
}, },
@ -56,7 +60,8 @@ try {
const overrides = require("../config/config.js") const overrides = require("../config/config.js")
constants = mixin(constants, overrides) constants = mixin(constants, overrides)
} catch (e) { } 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 module.exports = constants

View File

@ -38,6 +38,11 @@ const deltas = [
function() { function() {
db.prepare("UPDATE Settings SET instance = REPLACE(REPLACE(instance, '/', ''), ':', '://') WHERE instance LIKE '%/'") db.prepare("UPDATE Settings SET instance = REPLACE(REPLACE(instance, '/', ''), ':', '://') WHERE instance LIKE '%/'")
.run() .run()
},
// 5: Settings +quality
function() {
db.prepare("ALTER TABLE Settings ADD COLUMN quality INTEGER DEFAULT 0")
.run()
} }
] ]