494 lines
17 KiB
JavaScript
Executable File
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
|
|
}
|