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;
+}