icytv/src/player.js

443 lines
11 KiB
JavaScript

/* global alert, XMLHttpRequest, STREAM_SERVER, STREAM_NAME */
import Hls from 'hls.js'
// Elements
let player = document.querySelector('.livecnt')
let vid = player.querySelector('#stream')
let overlay = player.querySelector('.overlay')
let btn = overlay.querySelector('#playbtn')
let time = overlay.querySelector('#duration')
let fullscreenbtn = overlay.querySelector('#fullscrbtn')
let playbtn = overlay.querySelector('#playbtn')
let mutebtn = overlay.querySelector('#mutebtn')
let lstat = overlay.querySelector('.live')
let opts = overlay.querySelector('.controls')
let bigbtn = overlay.querySelector('.bigplaybtn')
let volumebar = overlay.querySelector('#volume_seek')
let volumeseek = volumebar.querySelector('.seeker')
let volumeseekInner = volumeseek.querySelector('.seekbar')
let 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 ws
function GET (url, istext) {
return new Promise((resolve, reject) => {
var xmlHttp = new XMLHttpRequest()
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
resolve(xmlHttp.responseText)
} else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) {
let err = new Error(xmlHttp.status)
err.request = xmlHttp
reject(err)
}
}
xmlHttp.open('GET', url, true)
istext && (xmlHttp.responseType = 'text')
xmlHttp.send(null)
})
}
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 (live) {
if (!live && ws) {
ws.onerror = ws.onopen = ws.onclose = null
ws.close()
ws = null
return
}
if (ws) return
ws = new WebSocket(`ws://${location.host}`)
ws.onerror = function(e) {
console.error('Socket errored, retrying..', e)
handleWebSocket(false)
setTimeout(() => handleWebSocket(vidReady), 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
let message = event.data
if (message.indexOf('viewlist ') === 0) {
let 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(vidReady), 5000)
}
}
function liveStatus (status) {
if (status) {
lstat.innerHTML = 'live now'
lstat.className = 'badge live'
clearTimeout(retryTimeout)
if (vid.paused) {
showBigBtn(true)
}
handleWebSocket(true)
} else {
lstat.innerHTML = 'offline'
lstat.className = 'badge live offline'
viewers.style.display = 'none'
handleWebSocket(false)
retryTimeout = setTimeout(() => {
if (vidReady) return
loadSource()
}, 10000)
}
}
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 () {
let minutes = Math.floor(vid.currentTime / 60)
let 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) {
if (ws) ws.send('watch ' + STREAM_NAME)
vid.play()
btn.innerHTML = '<i class="fa fa-pause fa-fw"></i>'
showBigBtn(false)
} else {
if (ws) ws.send('stop ' + STREAM_NAME)
vid.pause()
btn.innerHTML = '<i class="fa fa-play fa-fw"></i>'
showBigBtn(true)
}
}
function toggleSound () {
let 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!')
}
}
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()
})
let mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? 'DOMMouseScroll' : 'mousewheel'
volumebar.addEventListener(mousewheelevt, (e) => {
e.preventDefault()
let 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.on(Hls.Events.ERROR, (e) => {
vidReady = false
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
let xScrollPos = el.scrollLeft || document.documentElement.scrollLeft
let 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.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)
let 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 (let i in srcs) {
let link = srcs[i]
let el = document.createElement('a')
el.href = link.url
el.innerText = link.name
el.target = '_blank'
linksList.appendChild(el)
}
}
function getStreamStatus () {
GET('/api/channel/' + STREAM_NAME).then((data) => {
let jd = JSON.parse(data)
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)
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)
getStreamStatus()