/* global alert, XMLHttpRequest, STREAM_SERVER, STREAM_NAME */ import Hls from 'hls.js' // Elements const player = document.querySelector('.livecnt') const vid = player.querySelector('#stream') const overlay = player.querySelector('.overlay') const btn = overlay.querySelector('#playbtn') const time = overlay.querySelector('#duration') const fullscreenbtn = overlay.querySelector('#fullscrbtn') const subbtn = overlay.querySelector('#subbtn') const playbtn = overlay.querySelector('#playbtn') const mutebtn = overlay.querySelector('#mutebtn') const lstat = overlay.querySelector('.live') const opts = overlay.querySelector('.controls') const bigbtn = overlay.querySelector('.bigplaybtn') const volumebar = overlay.querySelector('#volume_seek') const volumeseek = volumebar.querySelector('.seeker') const volumeseekInner = volumeseek.querySelector('.seekbar') const viewers = overlay.querySelector('.viewers') let links let linksList // Variables let hls let hideTimeout let retryTimeout let vidReady = false let shouldHide = true let inFullscreen = false let errored = false let live = false let ws function clampAddition (val) { let volume = vid.volume if (volume + val > 1) { volume = 1 } else if (volume + val < 0) { volume = 0 } else { volume += val } return volume.toFixed(2) } function showBigBtn (show) { if (show) { bigbtn.className = 'bigplaybtn' } else { bigbtn.className = 'bigplaybtn hidden' } } function updateVolume () { volumeseekInner.style.width = vid.volume * 100 + '%' } function viewersCount (res) { viewers.style.display = 'block' viewers.innerHTML = res.length + ' watching' } function handleWebSocket () { if (!live && ws) { ws.onerror = ws.onopen = ws.onclose = null ws.close() ws = null return } if (!live || ws) return ws = new WebSocket(`ws${location.protocol.indexOf('s') !== -1 ? 's' : ''}://${location.host}`) ws.onerror = function (e) { console.error('Socket errored, retrying..', e) ws.close() ws = null setTimeout(() => handleWebSocket(), 5000) } ws.onopen = function () { console.log('Upstream socket connection established') if (!vid.paused) ws.send('watch ' + STREAM_NAME) ws.onmessage = function (event) { if (!event) return const message = event.data if (message.indexOf('viewlist ') === 0) { const str = message.substring(9) let list = str.split(',') if (str === '') list = [] viewersCount(list) } } } ws.onclose = function () { console.error('Socket died, retrying..') ws = null setTimeout(() => handleWebSocket(), 5000) } } function liveStatus (status) { live = status if (status) { lstat.innerHTML = 'live now' lstat.className = 'badge live' clearTimeout(retryTimeout) if (vid.paused) showBigBtn(true) } else { lstat.innerHTML = 'offline' lstat.className = 'badge live offline' viewers.style.display = 'none' } handleWebSocket() } function hide () { if (vid.paused || !shouldHide) { overlay.className = 'overlay' return } overlay.className = 'overlay hidden' } function resetHide () { overlay.className = 'overlay' clearTimeout(hideTimeout) if (vid.paused) return if (!shouldHide) return hideTimeout = setTimeout(() => { hide() }, 5000) } function updateTime () { const minutes = Math.floor(vid.currentTime / 60) const seconds = Math.floor(vid.currentTime - minutes * 60) time.innerHTML = minutes + ':' + (seconds < 10 ? '0' + seconds : seconds) } function toggleStream () { if (!vid) return if (!vidReady) return if (vid.paused) { hls.startLoad(-1) vid.play() } else { hls.stopLoad() vid.pause() } } function toggleSound () { const muteicon = mutebtn.querySelector('.fa') if (vid.muted) { vid.muted = false muteicon.className = 'fa fa-volume-up fa-fw' } else { vid.muted = true muteicon.className = 'fa fa-volume-off fa-fw' } } function exitHandler () { if (!(document.fullScreen || document.webkitIsFullScreen || document.mozFullScreen)) { inFullscreen = false } if (inFullscreen) { fullscreenbtn.innerHTML = '' } else { fullscreenbtn.innerHTML = '' } } function toggleFullscreen () { if (vid.enterFullscreen) { if (!document.fullScreen) { player.requestFullScreen() inFullscreen = true } else { document.cancelFullScreen() } } else if (vid.webkitEnterFullscreen) { if (!document.webkitIsFullScreen) { player.webkitRequestFullScreen() inFullscreen = true } else { document.webkitCancelFullScreen() } } else if (vid.mozRequestFullScreen) { if (!document.mozFullScreen) { player.mozRequestFullScreen() inFullscreen = true } else { document.mozCancelFullScreen() } } else { alert('Your browser doesn\'t support fullscreen!') } } function handlePlay () { if (ws) ws.send('watch ' + STREAM_NAME) btn.innerHTML = '' showBigBtn(false) } function handlePause () { if (ws) ws.send('stop ' + STREAM_NAME) btn.innerHTML = '' showBigBtn(true) } function askSubscription () { const email = prompt('Enter your email address to get notifications for when this channel goes live') if (!email) { return } fetch('/api/email/' + STREAM_NAME, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, csrf: CSRF_TOKEN }) }) .then(e => e.json()) .then(b => alert(b.message || 'Something went wrong!')) } window.addEventListener('onblur', () => { shouldHide = true }, false) window.addEventListener('onfocus', () => { shouldHide = true }, false) player.addEventListener('mousemove', resetHide) opts.addEventListener('mouseenter', () => { shouldHide = false }) opts.addEventListener('mouseleave', () => { shouldHide = true }) opts.addEventListener('mousemove', () => { shouldHide = false }) bigbtn.addEventListener('click', () => { toggleStream() }) volumeseek.addEventListener('click', (e) => { vid.volume = ((e.pageX - volumeseek.offsetLeft) / volumeseek.clientWidth) updateVolume() }) const mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? 'DOMMouseScroll' : 'mousewheel' volumebar.addEventListener(mousewheelevt, (e) => { e.preventDefault() const scrollAmnt = (e.wheelDelta == null ? e.detail * -40 : e.wheelDelta) if (scrollAmnt < 0) { vid.volume = clampAddition(-0.1) } else { vid.volume = clampAddition(0.1) } updateVolume() }, false) function loadSource () { if (!hls) return hls.loadSource(STREAM_SERVER + STREAM_NAME + '.m3u8') } if (Hls.isSupported()) { hls = new Hls() hls.attachMedia(vid) loadSource() hls.on(Hls.Events.MANIFEST_PARSED, () => { vidReady = true hls.stopLoad() clearTimeout(retryTimeout) }) hls.on(Hls.Events.ERROR, (e, d) => { if (!d.fatal) return // Don't attempt to recover the stream when a non-fatal error occurs vidReady = false retryTimeout = setTimeout(() => { if (vidReady) return loadSource() }, 5000) if (!vid.paused) { toggleStream() resetHide() } }) } else { alert('Your browser does not support HLS streaming!') } // helper function to get an element's exact position function getPosition (el) { let xPosition = 0 let yPosition = 0 while (el) { if (el.tagName == 'BODY') { // deal with browser quirks with body/window/document and page scroll const xScrollPos = el.scrollLeft || document.documentElement.scrollLeft const yScrollPos = el.scrollTop || document.documentElement.scrollTop xPosition += (el.offsetLeft - xScrollPos + el.clientLeft) yPosition += (el.offsetTop - yScrollPos + el.clientTop) } else { xPosition += (el.offsetLeft - el.scrollLeft + el.clientLeft) yPosition += (el.offsetTop - el.scrollTop + el.clientTop) } el = el.offsetParent } return { x: xPosition, y: yPosition } } function hideOnClickOutside (element) { const isVisible = (element) => { return element.style.display === 'block' } const outsideClickListener = event => { if ((!element.contains(event.target) && isVisible(element)) && (event.target !== links && !links.contains(event.target))) { element.style.display = 'none' removeClickListener() } } const removeClickListener = () => { document.removeEventListener('click', outsideClickListener) } document.addEventListener('click', outsideClickListener) } function updateLinks (srcs) { if (srcs.length === 0) { if (links) links.style.display = 'none' return } if (!links) { links = document.createElement('div') links.className = 'right button' links.setAttribute('title', 'Links') links.id = 'links' links.innerHTML = '' overlay.getElementsByClassName('controls')[0].insertBefore(links, fullscreenbtn) linksList = document.createElement('div') linksList.className = 'links overlay-list' linksList.style = 'display: none;' overlay.appendChild(linksList) links.addEventListener('click', function (e) { e.preventDefault() if (linksList.style.display === 'block') { linksList.style = 'display: none;' return } hideOnClickOutside(linksList) const pos = getPosition(links) linksList.style = 'display: block;' pos.x -= linksList.offsetWidth - links.offsetWidth / 2 pos.y -= linksList.offsetHeight linksList.style = 'display: block; left: ' + pos.x + 'px; top: ' + pos.y + 'px;' }) } links.style.display = 'block' linksList.innerHTML = '' for (const i in srcs) { const link = srcs[i] const el = document.createElement('a') el.href = link.url el.innerText = link.name el.target = '_blank' linksList.appendChild(el) } } function getStreamStatus () { fetch('/api/channel/' + STREAM_NAME) .then(data => data.json()) .then((jd) => { if (jd.error) { errored = true return alert(jd.error) } if (jd.links) updateLinks(jd.links) if (ws) ws.send('viewers ' + STREAM_NAME) if (jd.live && !vidReady) loadSource() liveStatus(jd.live) }, (e) => { errored = true liveStatus(false) }) if (!errored) setTimeout(getStreamStatus, 8000) } playbtn.addEventListener('click', toggleStream) mutebtn.addEventListener('click', toggleSound) fullscreenbtn.addEventListener('click', toggleFullscreen) subbtn.addEventListener('click', askSubscription) if (EMAIL_ENABLED === 'false') { subbtn.style.display = 'none' } document.addEventListener('webkitfullscreenchange', exitHandler, false) document.addEventListener('mozfullscreenchange', exitHandler, false) document.addEventListener('fullscreenchange', exitHandler, false) document.addEventListener('MSFullscreenChange', exitHandler, false) window.addEventListener('resize', function () { if (!linksList || linksList.style.display !== 'block') return linksList.style = 'display: none;' }) vid.addEventListener('timeupdate', updateTime, false) vid.addEventListener('play', handlePlay, false) vid.addEventListener('pause', handlePause, false) getStreamStatus()