1
0
mirror of https://git.sr.ht/~cadence/cloudtube synced 2024-12-22 13:07:00 +00:00
cloudtube/html/static/js/player.js
Martyn Ranyard be33a66e8c If multiple languages found, look for default
Fix for Google rolling out AI dubbed translations and cloudtube picking
the first audio stream, not the default.

Chooses the best audio format by filtering for default.

If none of the audio streams are marked as default, then use old
codepath.

Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2024-12-14 19:35:34 +13:00

251 lines
5.5 KiB
JavaScript

import {q, qa, ElemJS} from "/static/js/elemjs/elemjs.js"
import {SubscribeButton} from "/static/js/modules/SubscribeButton.js"
const video = q("#video")
const audio = q("#audio")
const videoFormats = new Map()
const audioFormats = new Map()
for (const f of [].concat(
data.formatStreams.map(f => (f.isAdaptive = false, f)),
data.adaptiveFormats.map(f => (f.isAdaptive = true, f))
)) {
if (f.type.startsWith("video")) {
videoFormats.set(f.itag, f)
} else {
audioFormats.set(f.itag, f)
}
}
function getBestAudioFormat() {
let best = null
let aidub = false
for (const f of audioFormats.values()) {
if (f.resolution.includes("default")) {
aidub = true
}
}
for (const f of audioFormats.values()) {
if (!aidub || f.resolution.includes("default")) {
if (best === null || f.bitrate > best.bitrate) {
best = f
}
}
}
return best
}
class FormatLoader {
constructor() {
this.npv = videoFormats.get(q("#video").getAttribute("data-itag"))
this.npa = null
}
play(itag) {
this.npv = videoFormats.get(itag)
if (this.npv.isAdaptive) {
this.npa = getBestAudioFormat()
} else {
this.npa = null
}
this.update()
}
update() {
const lastTime = video.currentTime
video.src = this.npv.url
video.currentTime = lastTime
if (this.npa) {
audio.src = this.npa.url
audio.pause()
audio.currentTime = lastTime
} else {
audio.pause()
audio.removeAttribute("src")
}
}
}
const formatLoader = new FormatLoader()
class PlayManager {
constructor(media, isAudio) {
this.media = media
this.isAudio = isAudio
}
isActive() {
return !this.isAudio || formatLoader.npa
}
play() {
if (this.isActive()) this.media.play()
}
pause() {
if (this.isActive()) this.media.pause()
}
}
const playManagers = {
video: new PlayManager(video, false),
audio: new PlayManager(audio, true)
}
class QualitySelect extends ElemJS {
constructor() {
super(q("#quality-select"))
this.on("input", this.setFormat.bind(this))
this.setFormat()
}
setFormat() {
const itag = this.element.value
formatLoader.play(itag)
video.focus()
}
}
const qualitySelect = new QualitySelect()
const ignoreNext = {
play: 0
}
function playbackIntervention(event) {
console.log(event.target.tagName.toLowerCase(), event.type)
if (audio.src) {
let target = event.target
let other = (event.target === video ? audio : video)
let targetPlayManager = playManagers[target.tagName.toLowerCase()]
let otherPlayManager = playManagers[other.tagName.toLowerCase()]
if (ignoreNext[event.type] > 0) {
ignoreNext[event.type]--
return
}
switch (event.type) {
case "durationchange":
target.ready = false;
break;
case "seeked":
target.ready = false;
target.pause();
other.currentTime = target.currentTime;
break;
case "play":
other.currentTime = target.currentTime;
otherPlayManager.play();
break;
case "pause":
other.currentTime = target.currentTime;
other.pause();
case "playing":
other.currentTime = target.currentTime;
break;
case "ratechange":
other.playbackRate = target.playbackRate;
break;
// case "stalled":
// case "waiting":
// target.pause();
// break;
}
} else {
// @ts-ignore this does exist
// if (event.type == "canplaythrough" && !video.manualPaused) video.play();
}
}
for (let eventName of ["pause", "play", "seeked"]) {
video.addEventListener(eventName, playbackIntervention)
}
for (let eventName of ["canplaythrough", "waiting", "stalled", "ratechange"]) {
video.addEventListener(eventName, playbackIntervention)
audio.addEventListener(eventName, playbackIntervention)
}
function relativeSeek(seconds) {
video.currentTime += seconds
}
function playVideo() {
audio.currentTime = video.currentTime
let lastTime = video.currentTime
ignoreNext.play++
video.play().then(() => {
const interval = setInterval(() => {
console.log("checking video", video.currentTime, lastTime)
if (video.currentTime !== lastTime) {
clearInterval(interval)
playManagers.audio.play()
return
}
}, 15)
})
}
function togglePlaying() {
if (video.paused) playVideo()
else video.pause()
}
function toggleFullScreen() {
if (document.fullscreen) document.exitFullscreen()
else video.requestFullscreen()
}
video.addEventListener("click", event => {
event.preventDefault()
togglePlaying()
})
video.addEventListener("dblclick", event => {
event.preventDefault()
toggleFullScreen()
})
document.addEventListener("keydown", event => {
if (["INPUT", "SELECT", "BUTTON"].includes(event.target.tagName)) return
if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return
let caught = true
if (event.key === "j" || event.key === "n") {
relativeSeek(-10)
} else if (["k", "p", " ", "e"].includes(event.key)) {
togglePlaying()
} else if (event.key === "l" || event.key === "o") {
relativeSeek(10)
} else if (event.key === "ArrowLeft") {
relativeSeek(-5)
} else if (event.key === "ArrowRight") {
relativeSeek(5)
} else if (event.key >= "0" && event.key <= "9") {
video.currentTime = video.duration * (+event.key) / 10
} else if (event.key === "f") {
toggleFullScreen()
} else {
caught = false
}
if (caught) event.preventDefault()
})
new SubscribeButton(q("#subscribe"))
const timestamps = qa("[data-clickable-timestamp]")
class Timestamp extends ElemJS {
constructor(element) {
super(element)
this.on("click", this.onClick.bind(this))
}
onClick(event) {
event.preventDefault()
video.currentTime = event.target.getAttribute("data-clickable-timestamp")
window.history.replaceState(null, "", event.target.href)
}
}
timestamps.forEach(el => {
new Timestamp(el)
})