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 }