mirror of
https://git.sr.ht/~cadence/cloudtube
synced 2024-12-22 13:07:00 +00:00
be33a66e8c
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>
251 lines
5.5 KiB
JavaScript
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)
|
|
})
|