diff --git a/package.json b/package.json index e1bdd08..c5f5e49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minpluto", - "version": "2024.07.13", + "version": "2024.07.26", "description": "An open source frontend alternative to YouTube.", "repository": "https://sudovanilla.org/MinPluto/MinPluto", "author": "Korbs ", diff --git a/src/components/global/Head.astro b/src/components/global/Head.astro index e179951..d9e8816 100644 --- a/src/components/global/Head.astro +++ b/src/components/global/Head.astro @@ -91,9 +91,5 @@ if (Astro.url.href.match('watch')) { : null } - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/video-player/Controls.astro b/src/components/video-player/Controls.astro new file mode 100644 index 0000000..a23ad31 --- /dev/null +++ b/src/components/video-player/Controls.astro @@ -0,0 +1,37 @@ +--- +import { + Backward15Seconds, + Enlarge, + Forward15Seconds, + PlaySolid, + Settings, +} from "@iconoir/vue"; +--- +
+
+

DJI: Landscape Footage of Cities

+
+
+
+ + + +
+
+
+ + +
+

+ 00:00 + 00:00 + 00:00 +

+
+
+ + + +
+
+
\ No newline at end of file diff --git a/src/components/video-player/Player.astro b/src/components/video-player/Player.astro new file mode 100644 index 0000000..df6fcbe --- /dev/null +++ b/src/components/video-player/Player.astro @@ -0,0 +1,21 @@ +--- +// Properties +const { Poster, Source } = Astro.props + +// Components +import Controls from '@components/video-player/Controls.astro' + +// Styles +import '@styles/player.scss' +--- + +
+ + + +
+ + + + + \ No newline at end of file diff --git a/src/pages/vp.astro b/src/pages/vp.astro new file mode 100644 index 0000000..0f91b92 --- /dev/null +++ b/src/pages/vp.astro @@ -0,0 +1,8 @@ +--- +import Base from "@layouts/Default.astro"; +import Player from "@components/video-player/Player.astro" +--- + + + + \ No newline at end of file diff --git a/src/pages/watch.astro b/src/pages/watch.astro index aaf6a1a..2aefd45 100644 --- a/src/pages/watch.astro +++ b/src/pages/watch.astro @@ -9,6 +9,7 @@ import { Donate, Download, ShareIos, ThumbsUp, MediaVideo } from "@iconoir/vue"; // Components import Dialog from '@components/Dialog.astro' import Video from '@components/VideoItem.astro' +import Player from "@components/video-player/Player.astro"; // Fetch const SWV = Astro.url.href.split("watch?v=").pop(); @@ -71,17 +72,10 @@ if (EightK === true) { // 571
- -
diff --git a/src/public/demo-purposes.mp4 b/src/public/demo-purposes.mp4 new file mode 100644 index 0000000..3a34fe2 Binary files /dev/null and b/src/public/demo-purposes.mp4 differ diff --git a/src/public/player/controller.js b/src/public/player/controller.js new file mode 100644 index 0000000..f5fac46 --- /dev/null +++ b/src/public/player/controller.js @@ -0,0 +1,193 @@ +var VideoContainer = document.querySelector('.video-container') +var VideoControls = document.querySelector('.video-controls') +var Player = document.querySelector('video') + +// Icons +var play_solid_default = ''; +var pause_solid_default = ''; +var PlayIcon = play_solid_default; +var PauseIcon = pause_solid_default; + +// Fullscreen +function Fullscreen() { + const Button_Fullscreen = document.getElementById("vc-fullscreen"); + function Toggle_Fullscreen() { + if (document.fullscreenElement) { + document.querySelector('.vc-top').style.opacity = '0' + document.exitFullscreen(); + } else if (document.webkitFullscreenElement) { + document.querySelector('.vc-top').style.opacity = '0' + document.webkitExitFullscreen(); + } else if (VideoContainer.webkitRequestFullscreen) { + document.querySelector('.vc-top').style.opacity = '1' + VideoContainer.webkitRequestFullscreen(); + } else { + document.querySelector('.vc-top').style.opacity = '1' + VideoContainer.requestFullscreen(); + } + } + Button_Fullscreen.onclick = Toggle_Fullscreen; + function Update_FullscreenButton() { + if (document.fullscreenElement) { + Button_Fullscreen.setAttribute("data-title", "Exit full screen (f)"); + } else { + Button_Fullscreen.setAttribute("data-title", "Full screen (f)"); + } + } + Player.addEventListener("dblclick", () => { + Toggle_Fullscreen() + Update_FullscreenButton() + }); +} + +// Play/Pause +function PlayPause() { + const Button_PlayPause = document.querySelector(".video-controls #vc-playpause"); + Button_PlayPause.addEventListener("click", Toggle_PlayPause); + Player.addEventListener("click", Toggle_PlayPause); + Player.addEventListener("play", Update_PlayPauseButton); + Player.addEventListener("pause", Update_PlayPauseButton); + function Toggle_PlayPause() { + if (Player.paused || Player.ended) { + Player.play(); + } else { + Player.pause(); + } + } + function Update_PlayPauseButton() { + if (Player.paused) { + Button_PlayPause.setAttribute("data-title", "Play (K)"); + Button_PlayPause.innerHTML = `${PlayIcon}`; + } else { + Button_PlayPause.setAttribute("data-title", "Pause (K)"); + Button_PlayPause.innerHTML = `${PauseIcon}`; + } + } +} + +// Skip Around +function SkipAround() { + const Button_SkipBack = document.querySelector(".video-controls #vc-backwards"); + const Button_SkipForth = document.querySelector(".video-controls #vc-forwards"); + Button_SkipBack.addEventListener("click", Toggle_SkipBack); + Button_SkipForth.addEventListener("click", Toggle_SkipForth); + function Toggle_SkipBack() { + Skip(-10); + } + function Toggle_SkipForth() { + Skip(10); + } + function Skip(value) { + Player.currentTime += value; + } +} + +// Hide Controls +function AutoToggleControls() { + function Hide_Controls2() { + if (Player.paused) { + return; + } else { + document.querySelector(".video-controls").classList.add("hide"); + } + } + function Show_Controls2() { + document.querySelector(".video-controls").classList.remove("hide"); + } + VideoControls.addEventListener("mouseenter", Show_Controls2); + VideoControls.addEventListener("mouseleave", Hide_Controls2); + var mouseTimer = null, cursorVisible = true; + function Hide_Cursor() { + mouseTimer = null; + VideoContainer.style.cursor = "none"; + cursorVisible = false; + Hide_Controls2(); + } + document.onmousemove = function () { + if (mouseTimer) { + window.clearTimeout(mouseTimer); + Show_Controls2(); + } + if (!cursorVisible) { + VideoContainer.style.cursor = "default"; + cursorVisible = true; + } + mouseTimer = window.setTimeout(Hide_Cursor, 3200); + }; +} + +// Keyboard Shortcuts +function KeyboardShortcuts(events) { + if (Player.hasAttribute("keyboard-shortcut-fullscreen")) { + var Fullscreen_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-fullscreen"); + } else { + var Fullscreen_KeyboardShortcut = "f"; + } + if (Player.hasAttribute("keyboard-shortcut-mute")) { + var Mute_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-mute"); + } else { + var Mute_KeyboardShortcut = "m"; + } + if (Player.hasAttribute("keyboard-shortcut-playpause")) { + var PlayPause_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-playpause"); + } else { + var PlayPause_KeyboardShortcut = "k"; + } + if (Player.hasAttribute("keyboard-shortcut-skipback")) { + var SkipBack_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-skipback"); + } else { + var SkipBack_KeyboardShortcut = "j"; + } + if (Player.hasAttribute("keyboard-shortcut-skipforth")) { + var SkipForth_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-skipforth"); + } else { + var SkipForth_KeyboardShortcut = "l"; + } + function keyboardShortcuts(event) { + const { key } = event; + if (key === PlayPause_KeyboardShortcut) { + if (Player.paused || Player.ended) { + Player.play(); + } else { + Player.pause(); + } + if (Player.paused) { + Show_Controls(); + } else { + setTimeout(() => { + Hide_Controls(); + }, 1200); + } + } else if (key === Mute_KeyboardShortcut) { + Player.muted = !Player.muted; + if (Player.muted) { + volume.setAttribute("data-volume", volume.value); + volume.value = 0; + } else { + volume.value = volume.dataset.volume; + } + } else if (key === Fullscreen_KeyboardShortcut) { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else if (document.webkitFullscreenElement) { + document.webkitExitFullscreen(); + } else if (VideoContainer.webkitRequestFullscreen) { + VideoContainer.webkitRequestFullscreen(); + } else { + VideoContainer.requestFullscreen(); + } + } else if (key === SkipBack_KeyboardShortcut) { + Player.currentTime += -10; + } else if (key === SkipForth_KeyboardShortcut) { + Player.currentTime += 10; + } + } + document.addEventListener("keyup", keyboardShortcuts); +} + +// Init All Functions +AutoToggleControls() +Fullscreen() +KeyboardShortcuts() +PlayPause() +SkipAround() \ No newline at end of file diff --git a/src/public/player/seek.js b/src/public/player/seek.js new file mode 100644 index 0000000..a879732 --- /dev/null +++ b/src/public/player/seek.js @@ -0,0 +1,81 @@ +function Seek() { + var Player = document.querySelector('video') + const timeElapsed = document.getElementById("current"); + const duration = document.getElementById("duration"); + function formatTime(timeInSeconds) { + const result = new Date(timeInSeconds * 1e3) + .toISOString() + .substr(11, 8); + return { + minutes: result.substr(3, 2), + seconds: result.substr(6, 2), + }; + } + function initializeVideo() { + const videoDuration = Math.round(Player.duration); + const time = formatTime(videoDuration); + duration.innerText = `${time.minutes}:${time.seconds}`; + duration.setAttribute( + "datetime", + `${time.minutes}m ${time.seconds}s`, + ); + } + Player.addEventListener("loadedmetadata", initializeVideo); + function updateTimeElapsed() { + const time = formatTime(Math.round(Player.currentTime)); + timeElapsed.innerText = `${time.minutes}:${time.seconds}`; + timeElapsed.setAttribute( + "datetime", + `${time.minutes}m ${time.seconds}s`, + ); + } + Player.addEventListener("timeupdate", updateTimeElapsed); + const progressBar = document.querySelector(".vc-progress-bar"); + const seek = document.getElementById("seek"); + function initializeVideo() { + const videoDuration = Math.round(Player.duration); + seek.setAttribute("max", videoDuration); + progressBar.setAttribute("max", videoDuration); + const time = formatTime(videoDuration); + duration.innerText = `${time.minutes}:${time.seconds}`; + duration.setAttribute( + "datetime", + `${time.minutes}m ${time.seconds}s`, + ); + } + function updateProgress() { + seek.value = Math.floor(Player.currentTime); + document.querySelector('.vc-progress-bar').style.width = Player.currentTime / Player.duration * 100 + '%' + } + Player.addEventListener("timeupdate", updateProgress); + const seekTooltip = document.getElementById("seek-tooltip"); + function updateSeekTooltip(event) { + const skipTo = Math.round( + (event.offsetX / event.target.clientWidth) * + parseInt(event.target.getAttribute("max"), 10), + ); + seek.setAttribute("data-seek", skipTo); + const t = formatTime(skipTo); + seekTooltip.textContent = `${t.minutes}:${t.seconds}`; + const rect = Player.getBoundingClientRect(); + seekTooltip.style.left = `${event.pageX - rect.left}px`; + seekTooltip.style.opacity = '1' + document.querySelector('.vc-progress-bar').style.width = Player.currentTime / Player.duration * 100 + '%' + seek.addEventListener('mouseleave', () => { + seekTooltip.style.opacity = '0' + }) + } + seek.addEventListener("mousemove", updateSeekTooltip); + function skipAhead(event) { + const skipTo = event.target.dataset.seek + ? event.target.dataset.seek + : event.target.value; + Player.currentTime = skipTo; + progressBar.value = skipTo; + seek.value = skipTo; + } + seek.addEventListener("input", skipAhead); + + initializeVideo(); +} +Seek(); \ No newline at end of file diff --git a/src/public/player/sync.js b/src/public/player/sync.js new file mode 100644 index 0000000..abaf72d --- /dev/null +++ b/src/public/player/sync.js @@ -0,0 +1,64 @@ +// https://gist.github.com/michancio/59b9f3dc54b3ff4f6a84 +// Find elements +var SyncVideo = document.querySelector(".main-video"); +var SyncAudio = document.querySelector(".main-audio"); + +// Object for synchronization of multiple media/sources +if (typeof window.MediaController === "function") { + var controller = new MediaController(); + SyncVideo.controller = controller; + SyncAudio.controller = controller; +} else { + controller = null; +} + +// Run SyncAudio and SyncVideo simultaneously +SyncVideo.addEventListener( + "play", + function () { + if (!controller && SyncAudio.paused) { + SyncAudio.play(); + } + }, + false, +); + +// Pause/Play and Buffering +SyncVideo.addEventListener("waiting", () => { + // If SyncVideo is buffering + SyncAudio.pause(); +}); +SyncVideo.addEventListener("playing", () => { + // If SyncVideo is done buffering + SyncAudio.play(); + SyncTimestamp(); +}); + +SyncVideo.addEventListener( + "pause", + function () { + if (!controller && !SyncAudio.paused) { + SyncAudio.pause(); + } + }, + false, +); + +// When Media Ends +SyncVideo.addEventListener( + "ended", + function () { + if (controller) { + controller.pause(); + } else { + SyncVideo.pause(); + SyncAudio.pause(); + } + }, + false, +); + +// Seekbar +function SyncTimestamp() { + SyncAudio.currentTime = SyncVideo.currentTime; +} \ No newline at end of file diff --git a/src/styles/player.scss b/src/styles/player.scss new file mode 100644 index 0000000..770164c --- /dev/null +++ b/src/styles/player.scss @@ -0,0 +1,163 @@ +.video-container { + position: relative; + *:focus { + border: 2px white solid; + transition: 1s border; + } + video { + width: 100%; + z-index: 1; + } + .video-controls { + background: linear-gradient(0deg, rgba(0,0,0,0.7523460067620799) 0%, rgba(0,0,0,0) 15%, rgba(0,0,0,0) 94%, rgba(0,0,0,0.7495448863139005) 100%);; + position: absolute; + bottom: 0px; + left: 0px; + width: calc(100% - 24px); + padding: 12px; + z-index: 5; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + transition: 0.3s opacity; + button { + color: white; + border-radius: 3rem; + aspect-ratio: 1; + border: none; + background: transparent; + display: flex; + align-items: center; + padding: 6px; + cursor: pointer; + &:hover { + background: rgba(255, 255, 255, 0.1); + } + } + .vc-top { + margin-top: 12px; + pointer-events: none; + opacity: 0; + transition: 0.3s opacity; + } + .vc-bottom { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + .vc-start, + .vc-end { + display: flex; + align-items: center; + gap: 6px; + } + .vc-center { + width: calc(100% - 220px); + display: flex; + align-items: center; + gap: 12px; + p { + width: max-content; + } + } + .vc-seek { + background: rgb(255 255 255 / 10%); + width: 100%; + height: 6px; + position: relative; + display: block; + border-radius: 3rem; + .vc-progress-bar { + width: 1%; + background: #ff274d; + position: absolute; + left: 0px; + height: 100%; + border-radius: 3rem; + } + input#seek { + position: absolute; + top: -8px; + left: 0px; + width: 100%; + + } + #seek[type="range"] { + -webkit-appearance: none; + appearance: none; + background: transparent; + cursor: pointer; + width: 100%; + } + #seek[type="range"]:focus { + outline: none; + } + #seek[type="range"]::-webkit-slider-runnable-track { + background-color: transparent; + border-radius: 3rem; + height: 1rem; + } + #seek[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + margin-top: -4px; + background-color: #ffffff; + border-radius: 3rem; + height: 1.5rem; + width: 1.5rem; + } + #seek[type="range"]:focus::-webkit-slider-thumb { + outline: 3px solid #ffffff; + outline-offset: 0.125rem; + } + #seek[type="range"]::-moz-range-track { + background-color: transparent; + border-radius: 3rem; + height: 1rem; + } + #seek[type="range"]::-moz-range-thumb { + background-color: #ffffff; + border: none; + border-radius: 3rem; + height: 1.5rem; + width: 1.5rem; + } + #seek[type="range"]:focus::-moz-range-thumb{ + outline: 3px solid transparent; + outline-offset: 0.125rem; + } + } + .timestamp { + display: flex; + background: rgb(255 255 255 / 10%); + border-radius: 3rem; + align-items: center; + position: relative; + pointer-events: none; + #seek-tooltip { + z-index: 10; + background: #464646; + padding: 6px 12px; + border-radius: 3rem 0px 0px 3rem; + margin-right: -64px; + opacity: 0; + transition: 0.3s opacity; + } + #current { + background: rgb(255 255 255 / 15%); + padding: 6px 12px; + border-radius: 3rem 0px 0px 3rem; + } + #duration { + padding: 6px 12px; + } + } + } + } +} + +.video-controls.hide { + opacity: 0; + transition: 0.3s opacity; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d9588f5..83aa2f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "@components/*": ["src/components/*"], "@layouts/*": ["src/layouts/*"], "@library/*": ["src/library/*"], + "@player/*": ["src/components/video-player/*"], "@root/*": ["*"], "@styles/*": ["src/styles/*"], "@utils/*": ["src/utilities/*"]