mirror of
https://git.sr.ht/~cadence/cloudtube
synced 2024-11-24 16:37:30 +00:00
243 lines
5.3 KiB
JavaScript
243 lines
5.3 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
|
|
for (const f of audioFormats.values()) {
|
|
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) 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)
|
|
})
|