2024-12-19 18:49:09 -06:00

494 lines
17 KiB
JavaScript
Executable File

export default (modules, q, qa) => {
let self = {
volumePopout: q(".videoControls .volumePopout"),
volumeBar: q(".videoControls .volumePopout .volumeBar"),
volumeText: q(".videoControls .volumePopout .volumeText"),
fullscreenBtn: q(".videoControls .fullscreen")
}
const vidContainer = modules.player.vidContainer
const vidCtrls = q(".videoControls")
const playBtn = q(".videoControls .playBtn")
const seek = q(".videoControls .timeline .seek")
const seekThumb = q(".videoControls .timeline .fakeThumb")
const hoverTime = q(".videoControls .timeline .hoverTimeContainer")
const hoverTimeVideo = q(".videoControls .timeline .hoverTimeContainer .hoverTimeVideo")
const hoverTimeText = q(".videoControls .timeline .hoverTimeContainer .hoverTimeText")
const timecode = q(".videoControls .timecode")
const volumeBtn = q(".videoControls .volumeBtn")
// const volumePopout = q(".videoControls .volumePopout")
// const volumeBar = q(".videoControls .volumePopout .volumeBar")
// const volumeText = q(".videoControls .volumePopout .volumeText")
const settingsBtn = q(".videoControls .settingsBtn")
const settingsPopout = q(".videoControls .settingsPopout")
// const fullscreenBtn = q(".videoControls .fullscreen")
let cursorOverVideo = false
modules.globals.video.addEventListener("mouseover", () => cursorOverVideo = true)
modules.globals.video.addEventListener("mouseout", () => cursorOverVideo = false)
// Auto hide
let doAutoHide = true
let hoveringSubmenu = false
let controlHideTimeout
let hideHoverTextTimeout
let hideVolumeTimeout
// Play button
function updatePlayBtn() {
if (!modules.globals.video.paused) {
seek.classList.remove("paused")
playBtn.classList.add("pause")
} else {
seek.classList.add("paused")
playBtn.classList.remove("pause")
}
}
modules.globals.video.addEventListener("playing", updatePlayBtn)
modules.globals.video.addEventListener("pause", updatePlayBtn)
playBtn.addEventListener("click", () => {
modules.player.togglePlaying()
modules.globals.video.focus()
})
// Seeking and timeline
let seeking = false
let totalLengthFormatted
function updateTotalLengthFormatted() {
totalLengthFormatted = new Date(1000 * modules.player.videoLength).toISOString().slice(11, 19)
}
updateTotalLengthFormatted()
modules.globals.video.addEventListener("canplaythrough", () => {
modules.player.videoLength = modules.globals.video.seekable.end(modules.globals.video.seekable.length - 1)
updateTotalLengthFormatted()
})
self = {
...self,
updateVolume: newVol => {
newVol = newVol || modules.volume.currentVolume
self.volumeBar.value = newVol
self.volumeText.innerHTML = `${Math.round(newVol * 100)}%`
},
seekFakeThumbUpdate: () => {
seekThumb.style.left = `${(seek.value / modules.player.videoLength) * 100}%`
},
}
// Volume change
volumeBtn.addEventListener("click", function() {
modules.globals.video.muted = !modules.globals.video.muted
modules.globals.video.focus()
})
modules.globals.video.addEventListener("volumechange", function() {
// Also show video controls and volume bar
self.hideControls(true)
let className = "volumeBtn videoControlBtn"
if (modules.globals.video.muted || modules.volume.currentVolume <= 0)
className += " mute"
else {
if (modules.volume.currentVolume < 0.15)
className += " off"
else if (modules.volume.currentVolume < 0.55)
className += " low"
}
volumeBtn.className = className
})
// Volume popout displaying
volumeBtn.addEventListener("mouseover", () => {
self.hideVolume(false)
})
volumeBtn.addEventListener("mouseout", () => self.hideVolume(true))
self.volumePopout.addEventListener("mouseover", function() {
self.hideVolume(false)
})
self.volumePopout.addEventListener("mouseout", () => self.hideVolume(true))
modules.globals.video.addEventListener("volumechange", () => self.updateVolume())
self.volumeBar.addEventListener("input", function() {
modules.volume.setVolume(self.volumeBar.value)
modules.globals.video.muted = false
})
self.volumeBar.addEventListener("mouseup", () => {
setTimeout(() => modules.globals.video.focus(), 1)
})
self.volumeBar.addEventListener("mousedown", () => self.hideVolume(false))
// Settings popout...
settingsBtn.addEventListener("click", () => {
settingsBtn.classList.toggle("active")
settingsPopout.classList.toggle("hidden")
// If freshly opened, go to main page
if (settingsBtn.classList.contains("active"))
settingsPopout.setAttribute("data-page", "main")
modules.globals.video.focus()
})
// "Back" buttons
const backBtns = qa(".videoControls .settingsPopout .settingsPage .header.goBack")
for (const backBtn of backBtns)
backBtn.addEventListener("click", () => settingsPopout.setAttribute("data-page", "main"))
// Settings page
const settingAutoHide = q(".videoControls .settingsPopout .settingsPage[data-name='main'] .setting.autoHide")
const settingLoop = q(".videoControls .settingsPopout .settingsPage[data-name='main'] .setting.loop")
const settingSpeed = q(".videoControls .settingsPopout .settingsPage[data-name='main'] .setting.speed")
const settingTricks = q(".videoControls .settingsPopout .settingsPage[data-name='main'] .setting.tricks")
const settingCC = q(".videoControls .settingsPopout .settingsPage[data-name='main'] .setting.cc")
const settingQuality = q(".videoControls .settingsPopout .settingsPage[data-name='main'] .setting.quality")
settingAutoHide.addEventListener("click", () => {
doAutoHide = settingAutoHide.classList.contains("active")
settingAutoHide.classList.toggle("active")
settingAutoHide.querySelector(".text").innerText = `Pin controls (${!doAutoHide ? "on" : "off"})`
})
settingLoop.addEventListener("click", () => {
modules.globals.video.loop = !modules.globals.video.loop
settingLoop.classList.toggle("active")
settingLoop.querySelector(".text").innerText = `Loop (${modules.globals.video.loop ? "on" : "off"})`
})
settingSpeed.addEventListener("click", () => settingsPopout.setAttribute("data-page", "speed"))
settingTricks.addEventListener("click", () => settingsPopout.setAttribute("data-page", "tricks"))
if (settingCC)
settingCC.addEventListener("click", () => settingsPopout.setAttribute("data-page", "cc"))
settingQuality.addEventListener("click", () => settingsPopout.setAttribute("data-page", "quality"))
// Speed bar
const speedBar = q(".videoControls .settingsPopout .settingsPage[data-name='speed'] .speedBar")
const speedText = q(".videoControls .settingsPopout .settingsPage[data-name='speed'] .speedText")
speedBar.addEventListener("input", () => {
modules.globals.video.playbackRate = speedBar.value
speedText.innerText = `${speedBar.value}x`
settingSpeed.querySelector(".text").innerText = `Speed (${speedBar.value}x)`
})
speedBar.addEventListener("mouseup", () => {
setTimeout(() => modules.globals.video.focus(), 1)
})
// Tricks page
const settingForceStereo = q(".videoControls .settingsPopout .settingsPage[data-name='tricks'] .forceStereo")
// Needs to run after all modules are initialized
modules.globals.video.addEventListener("canplaythrough", () => {
if (modules.volume.getNumChannels) {
settingForceStereo.addEventListener("click", () => {
const currentNumChannels = modules.volume.getNumChannels()
modules.volume.setNumChannels(3 - modules.volume.getNumChannels())
settingForceStereo.querySelector(".text").innerText = `Force mono audio (${currentNumChannels == 1 ? "off" : "on"})`
})
} else {
settingForceStereo.setAttribute("disabled", true)
}
})
// Subtitles page
const captionButtons = qa(".videoControls .settingsPopout .settingsPage[data-name='cc'] .setting.caption")
const captionRedownloadButtons = qa(".videoControls .settingsPopout .settingsPage[data-name='cc'] .setting.caption .redownloadBtn")
if (captionButtons && captionButtons.length > 0) {
// Caption switching
for (const captionBtn of captionButtons)
captionBtn.addEventListener("click", () => self.clickCaptionBtn(captionBtn))
// Redownload button
for (const redownloadBtn of captionRedownloadButtons) {
redownloadBtn.addEventListener("click", () => {
for (const rdlb of captionRedownloadButtons)
rdlb.setAttribute("disabled", true)
const captionLabel = redownloadBtn.previousElementSibling.innerText
let linkedTrack
for (const track of modules.globals.video.textTracks) {
if (track.label == captionLabel) {
linkedTrack = track
break
}
}
newToastWhenReady("yellow", "loading", `Redownloading captions (${captionLabel})...`)
modules.globals.video.focus()
// fetch
fetch(`/getCaption?url=${linkedTrack.url}&redownload=1`)
.then(r => {
if (r.status == 404 || r.status == 403 || r.status == 429) {
if (r.status == 429) // Ratelimited
newToastWhenReady("red", "x", `Too many requests. You may download captions \"${captionLabel}\" in ${r.headers.get("retry-after")} seconds...`)
else
newToastWhenReady("red", "x", `Failed to fetch captions (${r.status}: ${r.statusText})`)
console.error(r)
for (const rdlb of captionRedownloadButtons)
rdlb.removeAttribute("disabled")
} else {
self.clickCaptionBtn(captionButtons[0])
const previousSrc = linkedTrack.src
linkedTrack.src = previousSrc
self.clickCaptionBtn(redownloadBtn.parentNode)
newToastWhenReady("green", "check", "Done!")
}
})
})
}
}
// Quality page
self = {
...self,
qualityButtons: qa(".videoControls .settingsPopout .settingsPage[data-name='quality'] .setting.quality"),
qualityRedownloadButtons: qa(".videoControls .settingsPopout .settingsPage[data-name='quality'] .setting.quality .redownloadBtn")
}
const setQualityText = quality => settingQuality.querySelector(".text").innerText = quality
function switchQualityBtn(qualityBtn) {
setQualityText(qualityBtn.getAttribute("data-label"))
modules.quality.qualitySelected(qualityBtn.getAttribute("data-label"))
modules.globals.video.focus()
}
// Quality switching
for (const btn of self.qualityButtons)
btn.addEventListener("click", () => {
if (!btn.getAttribute("disabled"))
switchQualityBtn(btn)
})
// Redownload buttons
for (const redownloadBtn of self.qualityRedownloadButtons) {
redownloadBtn.addEventListener("click", () => {
if (!redownloadBtn.parentNode.getAttribute("disabled")) {
modules.quality.setQualityButtons(false)
setQualityButtonActive(null)
const quality = redownloadBtn.parentNode.getAttribute("data-label")
// Switch to a known safe quality if possible
delete modules.quality.safeQualities[quality]
if (Object.keys(modules.quality.safeQualities).length > 0) {
const q = Object.keys(modules.quality.safeQualities)[0]
modules.quality.setVideoSource(`/getVideo?v=${data.videoId}&q=${q}`)
// Resize video
for (const btn of self.qualityButtons) {
if (btn.getAttribute("data-label") == q) {
if (btn.getAttribute("data-w")) {
modules.globals.video.setAttribute("width", btn.getAttribute("data-w"))
modules.globals.video.setAttribute("height", btn.getAttribute("data-h"))
}
break
}
}
}
newToastWhenReady("yellow", "loading", `Requesting redownload for stream (${quality})...`)
video.focus()
// fetch
let doFetch
doFetch = () => {
fetch(`/redownloadVideo?videoID=${data.videoId}&quality=${quality}`)
.then(r => {
if (r.status == 404 || r.status == 500) {
newToastWhenReady("red", "x", `Failed to redownload video (${r.status}: ${r.statusText})`)
console.error(r)
modules.quality.setQualityButtons(true)
} else if (r.status == 429) {
const rateLimitToast = newToast("red", "x", `Too many requests. Downloading ${quality} in ${r.headers.get("retry-after")} seconds...`)
setTimeout(() => {
if (rateLimitToast)
setToastRemove(rateLimitToast.container)
doFetch()
}, 1000 * Number(r.headers.get("retry-after")))
} else {
modules.quality.lastToast = newToast("yellow", "loading", `Redownloading ${quality}...`, true)
const checkInt = setInterval(() => {
fetch(`/cacheInfo?videoName=${data.videoId}-${quality}`)
.then(r => r.json().then(r => {
const status = r.status
if (status == "found") {
videoDownloaded(`/getVideo?v=${data.videoId}&q=${quality}`, quality)
clearInterval(checkInt)
}
}))
}, 1000 * 3)
}
})
}
doFetch()
}
})
}
// Fullscreen
self.fullscreenBtn.addEventListener("click", () => {
modules.player.toggleFullScreen()
modules.globals.video.focus()
})
self = {
...self,
hideControls: doHide => {
self.updateTime()
// Restores mouse trail on Eir theme
if (modules.globals.forceCursor && vidContainer.classList.contains("hideCursor"))
modules.globals.forceCursor("default")
vidContainer.classList.remove("hideCursor")
vidCtrls.classList.remove("hidden")
clearTimeout(controlHideTimeout)
if (doHide && doAutoHide && !hoveringSubmenu)
controlHideTimeout = setTimeout(() => {
// Hides mouse trail on Eir theme
if (modules.globals.forceCursor && cursorOverVideo)
modules.globals.forceCursor(null)
vidContainer.classList.add("hideCursor")
vidCtrls.classList.add("hidden")
}, 1000)
},
hideHoverText: doHide => {
hoverTime.classList.remove("hidden")
clearTimeout(hideHoverTextTimeout)
if (doHide)
hideHoverTextTimeout = setTimeout(() => hoverTime.classList.add("hidden"), 50)
},
hideVolume: doHide => {
self.volumePopout.classList.remove("hidden")
clearTimeout(hideVolumeTimeout)
if (doHide)
hideVolumeTimeout = setTimeout(() => self.volumePopout.classList.add("hidden"), 750)
},
setQualityButtonActive: qualityLabel => {
for (const btn of self.qualityButtons)
if (btn.getAttribute("data-label") != qualityLabel)
btn.classList.remove("active")
else
btn.classList.add("active")
},
onVideoDownloaded: quality => {
for (const btn of self.qualityButtons) {
if (btn.getAttribute("data-label") == quality) {
modules.quality.downloadMp4Btn.innerHTML = `Download ${btn.querySelector(".text").innerText.replace(" *", "")} (${btn.getAttribute("data-size")})`
if (btn.getAttribute("data-w")) {
modules.globals.video.setAttribute("width", btn.getAttribute("data-w"))
modules.globals.video.setAttribute("height", btn.getAttribute("data-h"))
}
}
btn.removeAttribute("disabled")
}
self.setQualityButtonActive(quality)
setQualityText(quality)
},
updateTime: () => {
if (!seeking) {
seek.value = modules.globals.video.currentTime
self.seekFakeThumbUpdate()
}
timecode.innerText = `${new Date(1000 * modules.globals.video.currentTime).toISOString().slice(11, 19)} / ${totalLengthFormatted}`
},
seekUpdate: () => {
modules.globals.video.currentTime = seek.value
modules.globals.video.focus()
},
clickCaptionBtn: captionBtn => {
function onDone(track) {
if (track) {
if (track.eirWasHere)
modules.captions.registerCueChange(track, true)
else
track.mode = "hidden"
modules.player.lastTrack = track
}
settingCC.querySelector(".text").innerText = captionBtn.getAttribute("data-label")
for (const btn of captionButtons)
btn.classList.remove("active")
captionBtn.classList.add("active")
modules.globals.video.focus()
}
modules.globals.video.focus()
let found = false
for (let i = 0; i < modules.globals.video.textTracks.length; i++) {
const track = modules.globals.video.textTracks[i]
if (track.label == captionBtn.getAttribute("data-label")) {
found = true
// Format if needed
if (!track.eirWasHere)
modules.captions.replaceTrack(i).then(onDone)
else
onDone(track)
} else
if (track.eirWasHere)
modules.captions.registerCueChange(track, false)
else
track.mode = "hidden"
}
if (!found)
onDone()
}
}
// Auto hide
vidContainer.parentNode.addEventListener("mousemove", () => {
self.hideControls(true)
})
vidCtrls.addEventListener("mouseover", () => {
hoveringSubmenu = true
})
vidCtrls.addEventListener("mouseout", () => {
hoveringSubmenu = false
self.hideControls(true)
})
// Seeking and timeline
modules.globals.video.addEventListener("timeupdate", function() {
if (vidCtrls.classList.contains("hidden"))
return
self.updateTime()
}, false)
seek.addEventListener("input", self.seekFakeThumbUpdate)
seek.addEventListener("change", () => {
self.seekFakeThumbUpdate()
self.seekUpdate()
})
seek.addEventListener("mousedown", () => {
seeking = true
self.hideControls(true)
})
seek.addEventListener("mouseup", () => {
seeking = false
self.seekUpdate()
})
seek.addEventListener("mouseover", () => {
self.hideHoverText(false)
})
seek.addEventListener("mousemove", event => {
self.hideHoverText(false)
// Move hover text to match current mouse position over play bar
const seekRect = seek.getBoundingClientRect()
const progress = (event.clientX - seekRect.left) / seekRect.width
// const progress = Math.max(event.clientX - (seekRect.left + 8), 0) / (seekRect.width - 16)
hoverTime.style.left = `${progress * 100}%`
hoverTimeText.innerText = new Date(1000 * (progress * modules.player.videoLength)).toISOString().slice(11, 19)
hoverTimeVideo.currentTime = progress * modules.player.videoLength
})
seek.addEventListener("mouseout", () => {
self.hideHoverText(true)
})
self.hideControls(true)
return self
}