commit 547d9a777c3c46a7ec50b2775541a53aec7d561c Author: Erik Thuning <boooink@gmail.com> Date: Tue Nov 10 15:50:27 2020 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf34796 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*~ +*.mp4 diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..3b1d850 Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..63d125e --- /dev/null +++ b/index.html @@ -0,0 +1,167 @@ +<!doctype html> +<html lang="se"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <link rel="shortcut icon" href="favicon.ico"> + <link rel="stylesheet" href="./style.css?a"> + <title>DSVPlayer</title> + <script src="player.js" defer></script> + </head> + <body> + <div id="wrapper"> + <div class="main"> + <video></video> + <svg> + <use href="#play-icon"></use> + </svg> + </div> + <div class="about"> + <button id="playlist-show"> + <svg> + <use href="#playlist-icon"></use> + </svg> + </button> + <h1>TITLE</h1> + </div> + <div id="controls"> + <div id="progress-container"> + <div id="buffer"></div> + <div id="progress"></div> + </div> + <div class="left-controls"> + <button title="Spela" id="play-button"> + <svg> + <use href="#play-icon"></use> + </svg> + </button> + <div id="speed-select"> + <button id="speed-current">1</button> + <ul id="speed-list"> + <li> + <button>1</button> + </li> + <li> + <button>1.5</button> + </li> + <li> + <button>2</button> + </li> + <li> + <button>4</button> + </li> + </ul> + </div> + <div id="volume-controls"> + <button id="volume-button" title="Stäng av ljud"> + <svg> + <use href="#volume-icon"></use> + <use href="#mute-icon" class="hidden"></use> + </svg> + </button> + <input type="range" id="volume" + min="0" max="1" step="0.01" value="1"> + </div> + <div class="time"> + <time id="time-elapsed">00:00</time> + <span> / </span> + <time id="duration">00:00</time> + </div> + </div> + <div class="right-controls"> + <button title="Undertexter är av" id="subtitles-button"> + <svg> + <use href="#subtitles-off-icon"></use> + <use href="#subtitles-on-icon" class="hidden"></use> + </svg> + </button> + <button title="Helskärm" id="fullscreen-button"> + <svg> + <use href="#fullscreen-enter-icon"></use> + <use href="#fullscreen-exit-icon" class="hidden"></use> + </svg> + </button> + </div> + </div> + <div id="playlist" class="hidden"> + <div class="about"> + <button id="playlist-hide"> + <svg> + <use href="#close-icon"></use> + </svg> + </button> + <h1>TITLE</h1> + </div> + <ul> + <li> + ITEM 1 + </li> + <li> + ITEM 2 + </li> + <li> + ITEM 3 + </li> + </ul> + </div> + </div> + <template id="stream-template"> + <div class="secondary"> + <video></video> + <svg> + <use href="#switch-icon"></use> + </svg> + </div> + </template> + <svg class="hidden"> + <defs> + <symbol id="pause-icon" viewBox="0 0 24 24"> + <path d="M14.016 5.016h3.984v13.969h-3.984v-13.969zM6 18.984v-13.969h3.984v13.969h-3.984z"></path> + </symbol> + + <symbol id="play-icon" viewBox="0 0 24 24"> + <path d="M8.016 5.016l10.969 6.984-10.969 6.984v-13.969z"></path> + </symbol> + + <symbol id="playlist-icon" viewBox="0 0 24 24"> + <path d="M0 0h24v24H0z" fill="none"/> + <path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/> + </symbol> + + <symbol id="close-icon" viewBox="0 0 24 24"> + <path d="M0 0h24v24H0z" fill="none"/> + <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> + </symbol> + <symbol id="switch-icon" viewBox="0 0 24 24"> + <path d="M12 6v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.2.2-.51 0-.71l-2.79-2.79c-.31-.31-.85-.09-.85.36V4c-4.42 0-8 3.58-8 8 0 1.04.2 2.04.57 2.95.27.67 1.13.85 1.64.34.27-.27.38-.68.23-1.04C6.15 13.56 6 12.79 6 12c0-3.31 2.69-6 6-6zm5.79 2.71c-.27.27-.38.69-.23 1.04.28.7.44 1.46.44 2.25 0 3.31-2.69 6-6 6v-1.79c0-.45-.54-.67-.85-.35l-2.79 2.79c-.2.2-.2.51 0 .71l2.79 2.79c.31.31.85.09.85-.35V20c4.42 0 8-3.58 8-8 0-1.04-.2-2.04-.57-2.95-.27-.67-1.13-.85-1.64-.34z"/> + </symbol> + + <symbol id="volume-icon" viewBox="0 0 24 24"> + <path d="M14.016 3.234q3.047 0.656 5.016 3.117t1.969 5.648-1.969 5.648-5.016 3.117v-2.063q2.203-0.656 3.586-2.484t1.383-4.219-1.383-4.219-3.586-2.484v-2.063zM16.5 12q0 2.813-2.484 4.031v-8.063q1.031 0.516 1.758 1.688t0.727 2.344zM3 9h3.984l5.016-5.016v16.031l-5.016-5.016h-3.984v-6z"></path> + </symbol> + + <symbol id="mute-icon" viewBox="0 0 24 24"> + <path d="M12 3.984v4.219l-2.109-2.109zM4.266 3l16.734 16.734-1.266 1.266-2.063-2.063q-1.547 1.313-3.656 1.828v-2.063q1.172-0.328 2.25-1.172l-4.266-4.266v6.75l-5.016-5.016h-3.984v-6h4.734l-4.734-4.734zM18.984 12q0-2.391-1.383-4.219t-3.586-2.484v-2.063q3.047 0.656 5.016 3.117t1.969 5.648q0 2.203-1.031 4.172l-1.5-1.547q0.516-1.266 0.516-2.625zM16.5 12q0 0.422-0.047 0.609l-2.438-2.438v-2.203q1.031 0.516 1.758 1.688t0.727 2.344z"></path> + </symbol> + + <symbol id="subtitles-off-icon" viewBox="0 0 24 24"> + <path d="M20,4H6.83l8,8H19c0.55,0,1,0.45,1,1c0,0.55-0.45,1-1,1h-2.17l4.93,4.93C21.91,18.65,22,18.34,22,18V6C22,4.9,21.1,4,20,4 z"/><path d="M20,20l-6-6l-1.71-1.71L12,12L3.16,3.16c-0.39-0.39-1.02-0.39-1.41,0c-0.39,0.39-0.39,1.02,0,1.41l0.49,0.49 C2.09,5.35,2,5.66,2,6v12c0,1.1,0.9,2,2,2h13.17l2.25,2.25c0.39,0.39,1.02,0.39,1.41,0c0.39-0.39,0.39-1.02,0-1.41L20,20z M8,13 c0,0.55-0.45,1-1,1H5c-0.55,0-1-0.45-1-1c0-0.55,0.45-1,1-1h2C7.55,12,8,12.45,8,13z M14,17c0,0.55-0.45,1-1,1H5 c-0.55,0-1-0.45-1-1c0-0.55,0.45-1,1-1h8c0.08,0,0.14,0.03,0.21,0.04l0.74,0.74C13.97,16.86,14,16.92,14,17z"/> + </symbol> + + <symbol id="subtitles-on-icon" viewBox="0 0 24 24"> + <path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM5 12h2c.55 0 1 .45 1 1s-.45 1-1 1H5c-.55 0-1-.45-1-1s.45-1 1-1zm8 6H5c-.55 0-1-.45-1-1s.45-1 1-1h8c.55 0 1 .45 1 1s-.45 1-1 1zm6 0h-2c-.55 0-1-.45-1-1s.45-1 1-1h2c.55 0 1 .45 1 1s-.45 1-1 1zm0-4h-8c-.55 0-1-.45-1-1s.45-1 1-1h8c.55 0 1 .45 1 1s-.45 1-1 1z"/> + </symbol> + + <symbol id="fullscreen-enter-icon" viewBox="0 0 24 24"> + <path d="M14.016 5.016h4.969v4.969h-1.969v-3h-3v-1.969zM17.016 17.016v-3h1.969v4.969h-4.969v-1.969h3zM5.016 9.984v-4.969h4.969v1.969h-3v3h-1.969zM6.984 14.016v3h3v1.969h-4.969v-4.969h1.969z"></path> + </symbol> + + <symbol id="fullscreen-exit-icon" viewBox="0 0 24 24"> + <path d="M15.984 8.016h3v1.969h-4.969v-4.969h1.969v3zM14.016 18.984v-4.969h4.969v1.969h-3v3h-1.969zM8.016 8.016v-3h1.969v4.969h-4.969v-1.969h3zM5.016 15.984v-1.969h4.969v4.969h-1.969v-3h-3z"></path> + </symbol> + + </defs> + </svg> + </body> +</html> diff --git a/player.js b/player.js new file mode 100644 index 0000000..2636f54 --- /dev/null +++ b/player.js @@ -0,0 +1,235 @@ +document.addEventListener('DOMContentLoaded', init) + +function init() { + loadStreams(getStreams()) + setupHiding() + setupPlayback() + setupSwitching() + setupProgress() + setupSpeed() + setupVolume() + setupSubs() + setupFullscreen() + setupPlaylist() +} + +function getStreams() { + return window.location.search.substring(1).split('&') +} + +function loadStreams(streamlist) { + var mainstream = document.querySelector('.main > video') + var mainparent = mainstream.parentNode + var template = document.getElementById('stream-template') + + mainstream.src = streamlist[0] + mainstream.load() + for (var i = 1; i < streamlist.length; i++) { + var newstream = template.content.cloneNode(true) + var video = newstream.querySelector('video') + video.src = streamlist[i] + video.load() + video.muted = true + mainparent.parentNode.insertBefore(newstream, mainparent.nextSibling) + } +} + +function setupHiding() { + const selector = 'nocursor' + var timer = null + var body = document.querySelector('body') + var controls = document.querySelector('#controls') + var about = document.querySelector('#wrapper > .about') + + function hide() { + if(!body.classList.contains(selector)) { + body.classList.add(selector) + } + } + function mouseMove(event) { + if(timer) { + window.clearTimeout(timer) + } + if(body.classList.contains(selector)) { + body.classList.remove(selector) + } + if(!controls.contains(event.target) && !about.contains(event.target)) { + timer = window.setTimeout(hide, 1500) + } + } + + body.addEventListener('mousemove', mouseMove) +} + +function setupPlayback() { + var playing = false + var videos = document.querySelectorAll('video') + + function togglePlayback(event) { + videos.forEach(function(video) { + if(!playing) { + video.play() + } else { + video.pause() + } + }) + playing = !playing; + } + + document.querySelectorAll('.main, #play-button') + .forEach(function(button) { + button.addEventListener('click', togglePlayback) + }) +} + +function setupSwitching() { + var main = document.querySelector('.main') + + function switchStreams(event) { + var target = event.currentTarget + var stream = target.querySelector('video') + var other = main.querySelector('video') + main.replaceChild(stream, other) + target.insertBefore(other, target.firstElementChild) + } + + document.querySelectorAll('.secondary') + .forEach(function(div) { + div.addEventListener('click', switchStreams) + }) +} + +function setupProgress() { + var body = document.querySelector('body') + var backdrop = document.querySelector('#progress-container') + var pb = document.querySelector('#progress') + var dragging = false + + function startDrag(event) { + dragging = true + } + function stopDrag(event) { + dragging = false + } + function update(event) { + if(dragging) { + pb.style.width = event.clientX + 'px' + } + } + function set(event) { + pb.style.width = event.clientX + 'px' + } + + backdrop.addEventListener('mousedown', startDrag) + backdrop.addEventListener('click', set) + body.addEventListener('mousemove', update) + body.addEventListener('mouseup', stopDrag) +} + +function setupSpeed() { + var videos = document.querySelectorAll('video') + var current = document.querySelector('#speed-current') + + function setSpeed(event) { + var speed = event.currentTarget.textContent + if(event.currentTarget.id == 'speed-current') { + speed = 1 + } + current.textContent = speed + videos.forEach(function(video) { + video.playbackRate = speed + }) + } + + var buttons = document.querySelectorAll('#speed-select button') + buttons.forEach(function(button) { + button.addEventListener('click', setSpeed) + }) +} + +function setupVolume() { + var soundStream = document.querySelector('.main > video') + var icons = document.querySelectorAll('#volume-button > svg > use') + var volume = document.querySelector('#volume') + var muted = false + var mutedVol = 0 + + // There may be a cached setting to apply + soundStream.volume = volume.value + + function toggleVolume(event) { + if(!muted) { + mutedVol = volume.value + volume.value = 0 + soundStream.volume = 0 + muted = true + } else { + if(mutedVol < 0.1) { + mutedVol = 0.1 + } + volume.value = mutedVol + soundStream.volume = mutedVol + muted = false; + } + icons.forEach(function(icon) { + icon.classList.toggle('hidden') + }) + } + function slideVolume(event) { + soundStream.volume = event.currentTarget.value + } + + var button = document.querySelector('#volume-button') + button.addEventListener('click', toggleVolume) + + var slider = document.querySelector('#volume-controls > input[type="range"]') + slider.addEventListener('input', slideVolume) +} + +function setupSubs() { + var icons = document.querySelectorAll('#subtitles-button > svg > use') + + function toggleSubs(event) { + icons.forEach(function(icon) { + icon.classList.toggle('hidden') + }) + } + + var button = document.querySelector('#subtitles-button') + button.addEventListener('click', toggleSubs) +} + +function setupFullscreen() { + var body = document.querySelector('body') + var icons = document.querySelectorAll('#fullscreen-button > svg > use') + + function toggleFullscreen(event) { + if(document.fullscreenElement) { + document.exitFullscreen() + } else { + body.requestFullscreen() + } + icons.forEach(function(icon) { + icon.classList.toggle('hidden') + }) + } + + var button = document.querySelector('#fullscreen-button') + button.addEventListener('click', toggleFullscreen) +} + +function setupPlaylist() { + function togglePlaylist(event) { + const h = 'hidden' + var about = document.querySelector('#wrapper > .about') + var playlist = document.querySelector('#playlist') + about.classList.toggle(h) + playlist.classList.toggle(h) + } + + document.querySelectorAll('#playlist-show, #playlist-hide') + .forEach(function(button) { + button.addEventListener('click', togglePlaylist) + }) +} + diff --git a/style.css b/style.css new file mode 100644 index 0000000..2a189a7 --- /dev/null +++ b/style.css @@ -0,0 +1,215 @@ +:root { + --main-width: 75vw; + --main-height: 100vh; + --secondary-width: 25vw; + --secondary-height: 33vh; + + --background: black; + --foreground: white; + --highlight: #9BB2CE; + + --controls-height: 28px; +} + +* { + position: relative; +} +body { + margin: 0; + padding: 0; + display: grid; + align-items: center; + justify-items: center; + background-color: var(--background); + color: var(--foreground); +} +svg { + height: 24px; + width: 24px; + fill: var(--foreground); +} +button { + background: none; + border: none; + color: var(--foreground); +} +.nocursor { + cursor: none; +} +.nocursor .about, +.nocursor #controls, +.nocursor .main > svg, +.nocursor .secondary > svg { + display: none !important; +} +#wrapper { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + grid-auto-columns: 1fr; + grid-auto-rows: 1fr; +} +.about { + margin-top: 1em; + margin-left: 1em; + display: flex; +} +#wrapper > .about { + position: absolute; + top: 0; + left: 0; +} +h1 { + margin: 0; + padding: 0; + font-size: 120%; +} +video { + object-fit: contain; + width: 100%; + height: 100%; +} +.main, .secondary { + display: grid; + align-items: center; + justify-items: center; +} +.main { + grid-row: 1 / 4; + grid-column: 1 / 4; +} +.secondary { + grid-column-start: 4; +} +.main > video { + grid-row: 1 / 2; + grid-column: 1 / 2; + max-height: var(--main-height); + max-width: var(--main-width); +} +.secondary > video { + grid-row: 1 / 2; + grid-column: 1 / 2; + max-height: var(--secondary-height); + max-width: var(--secondary-width); +} +.main > svg, +.secondary > svg { + grid-row: 1 / 2; + grid-column: 1 / 2; + display: none; +} +.main:hover > svg, +.secondary:hover > svg { + display: block; + opacity: 60%; +} +.main > svg { + width: calc(minmax(720px, var(--main-height)/2)); + height: calc(minmax(720px, var(--main-height)/2)); +} +.secondary > svg { + width: calc(var(--secondary-height)/2); + height: calc(var(--secondary-height)/2); +} +#controls { + position: absolute; + width: 100%; + bottom: 0; + left: 0; + display: grid; + grid-template-areas: "progress progress" "left right"; + background-color: rgba(0, 0, 0, 0.7); +} +#progress-container { + grid-area: progress; + height: 20px; + display: grid; + grid-template-areas: "bar"; + justify-items: start; + align-items: end; +} +#progress, #buffer { + grid-area: bar; +} +#buffer { + /* DEBUG */ + width: 50%; + /* END DEBUG */ + height: 8px; + background-color: gray; +} +#progress { + height: 8px; + background-color: var(--highlight); +} +.left-controls { + grid-area: left; + display: flex; + height: var(--controls-height); +} +#speed-list { + display: none; + position: absolute; + left: 0; + bottom: 0; + padding: 0; + margin-top: 0; + margin-bottom: var(--controls-height); + transform: translate(-25%); + list-style: none; +} +#speed-list { + background-color: darkgray; +} +#speed-list button { + width: 100%; + text-align: right; +} +#speed-select button::after { + content: "x"; +} +#speed-list button:hover, +#speed-current:hover, +#speed-select:focus-within #speed-current { + background-color: var(--highlight); +} +#speed-select:focus-within #speed-list, +#speed-select:hover #speed-list { + display: block; +} +.right-controls { + grid-area: right; + display: flex; + justify-content: flex-end; + height: var(--controls-height); +} +#playlist { + position: absolute; + top: 0; + left: 0; + height: 100%; + display: flex; + flex-direction: column; +} + +/* small screens */ + +@media screen and (aspect-ratio < 16/9) { + :root { + --main-width: 100vw; + --main-height: 75vh; + --secondary-width: 33vw; + --secondary-height: 25vh; + } + .secondary { + grid-column-start: initial; + grid-row-start: 4; + } +} + +/* playlist hiding */ + +.hidden { + display: none !important; +}