icytv/src/player.js

460 lines
11 KiB
JavaScript

/* 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 = '<i class="fa fa-compress fa-fw"></i>'
} else {
fullscreenbtn.innerHTML = '<i class="fa fa-expand fa-fw"></i>'
}
}
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 = '<i class="fa fa-pause fa-fw"></i>'
showBigBtn(false)
}
function handlePause () {
if (ws) ws.send('stop ' + STREAM_NAME)
btn.innerHTML = '<i class="fa fa-play fa-fw"></i>'
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 = '<i class="fa fa-link" aria-hidden="true"></i>'
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()