227 行
8.1 KiB
JavaScript
227 行
8.1 KiB
JavaScript
const t0 = Date.now() / 1000;
|
|
const playbackTimeout = 16.0; // seconds until playback should have begun
|
|
|
|
const segmentDuration = parseFloat(document.getElementById("hls-time").value); // seconds per segment
|
|
const latencyThreshold = 180; // notify the viewer once they cross this threshold
|
|
const segmentThreshold = latencyThreshold / segmentDuration;
|
|
|
|
const heartbeatPeriod = 20.0; // seconds between heartbeats
|
|
|
|
const video = document.querySelector("video");
|
|
const videojsEnabled = parseInt(document.getElementById("videojs-enabled").value);
|
|
let firstSegment;
|
|
|
|
if ( !videojsEnabled ) {
|
|
try {
|
|
firstSegment = parseInt(/segment=(\d+)/.exec(video.src)[1]);
|
|
} catch ( error ) {
|
|
firstSegment = null; // happens when the stream is offline
|
|
}
|
|
}
|
|
|
|
let token, streamTitle, viewerCount, streamStatus, streamLight, refreshButton, radialLoader;
|
|
let streamAbsoluteStart, streamRelativeStart, streamTimer, streamTimerLastUpdated;
|
|
|
|
// ensure only one heartbeat is sent at a time
|
|
let heartIsBeating = false;
|
|
|
|
let nextHeartbeat;
|
|
|
|
let streamInfoFrame = window.frames["stream-info"];
|
|
streamInfoFrame.addEventListener("load", function() {
|
|
console.log("stream info iframe loaded");
|
|
|
|
const streamInfo = streamInfoFrame.contentDocument
|
|
streamTitle = streamInfo.getElementById("stream-title");
|
|
viewerCount = streamInfo.getElementById("viewer-count");
|
|
|
|
streamStatus = streamInfo.getElementById("stream-status");
|
|
streamLight = streamInfo.getElementById("stream-light");
|
|
refreshStreamButton = streamInfo.getElementById("refresh-stream-button");
|
|
refreshPageButton = streamInfo.getElementById("refresh-page-button");
|
|
|
|
refreshStreamButton.onclick = function() { refreshStreamButton.style.display = "none"; return video.load(); };
|
|
refreshPageButton.onclick = function() { return window.location.reload(true); };
|
|
|
|
streamTimer = streamInfo.getElementById("uptime");
|
|
streamAbsoluteStart = streamInfoFrame.contentWindow.streamAbsoluteStart;
|
|
streamRelativeStart = streamInfoFrame.contentWindow.streamRelativeStart;
|
|
streamTimerLastUpdated = Date.now() / 1000;
|
|
|
|
radialLoader = streamInfo.getElementById("radial-loader");
|
|
|
|
// this viewer's token
|
|
token = document.getElementById("token").value;
|
|
|
|
// get stream info every heartbeatPeriod seconds
|
|
setInterval(heartbeat, heartbeatPeriod * 1000);
|
|
|
|
// update stream timer every second
|
|
setInterval(updateStreamTimer, 1000);
|
|
});
|
|
|
|
function currentSegment() {
|
|
if ( videojsEnabled ) {
|
|
try {
|
|
let player = videojs.players.vjs_video_3;
|
|
if ( player == null ) {
|
|
player = videojs.players.videojs;
|
|
}
|
|
const tracks = player.textTracks();
|
|
const cues = tracks[0].cues;
|
|
const uri = cues[cues.length - 1].value.uri;
|
|
return parseInt(uri.split("/")[3].slice(6));
|
|
} catch ( error ) {
|
|
return null;
|
|
}
|
|
} else {
|
|
if ( video.readyState != video.HAVE_ENOUGH_DATA || video.networkState == video.NETWORK_IDLE ) {
|
|
return null;
|
|
}
|
|
return firstSegment + Math.floor(video.duration / segmentDuration);
|
|
}
|
|
}
|
|
|
|
function updateStreamStatus(msg, backgroundColor, refreshStream, refreshPage) {
|
|
// TODO: figure out why when the colour is yellow the stream light moves down a few pixels
|
|
streamStatus.innerHTML = msg;
|
|
streamLight.style.backgroundColor = backgroundColor;
|
|
|
|
// doesn't work with videojs: there are errors in the console; probably there is a workaround
|
|
// could work with html5 video but we'd need to find what segment to start at; too complicated for now
|
|
refreshPage = refreshPage | refreshStream;
|
|
refreshStream = false;
|
|
|
|
if ( refreshStream ) {
|
|
refreshStreamButton.style.display = null;
|
|
} else {
|
|
refreshStreamButton.style.display = "none";
|
|
}
|
|
if ( refreshPage ) {
|
|
refreshPageButton.style.display = null;
|
|
} else {
|
|
refreshPageButton.style.display = "none";
|
|
}
|
|
}
|
|
|
|
function updateStreamTimer() {
|
|
if ( streamTimer == null ) {
|
|
return;
|
|
}
|
|
let diff = streamRelativeStart;
|
|
if ( !Number.isInteger(diff) ) {
|
|
streamTimer.innerHTML = "";
|
|
} else {
|
|
diff += Math.floor(Date.now() / 1000 - streamTimerLastUpdated);
|
|
|
|
const hours = Math.floor(diff / 3600);
|
|
const minutes = Math.floor((diff % 3600) / 60);
|
|
const seconds = diff % 60;
|
|
|
|
const mm = ("0" + minutes).slice(-2);
|
|
const ss = ("0" + seconds).slice(-2);
|
|
|
|
if ( hours == 0 ) {
|
|
streamTimer.innerHTML = `${mm}:${ss}`;
|
|
} else if ( hours < 1000 ) {
|
|
streamTimer.innerHTML = `${hours}:${mm}:${ss}`;
|
|
} else {
|
|
streamTimer.innerHTML = "1000+ hours";
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetRadialLoader(animationDuration) {
|
|
const element = radialLoader;
|
|
const newElement = element.cloneNode(true);
|
|
newElement.children[0].style.animationDuration = animationDuration + "s";
|
|
element.parentNode.replaceChild(newElement, element);
|
|
radialLoader = newElement;
|
|
}
|
|
|
|
// TODO: this
|
|
function fitFrame(frame) {
|
|
}
|
|
|
|
// get stream info from the server (viewer count, current segment, if stream is online, etc.)
|
|
function heartbeat() {
|
|
nextHeartbeat = Date.now() / 1000 + heartbeatPeriod;
|
|
|
|
if ( heartIsBeating ) {
|
|
return;
|
|
} else {
|
|
heartIsBeating = true;
|
|
}
|
|
|
|
try {
|
|
// prepare a request to /heartbeat
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("GET", `/heartbeat?token=${token}`);
|
|
|
|
xhr.timeout = 18000; // timeout in ms, 18 seconds
|
|
xhr.onerror = function(e) {
|
|
heartIsBeating = false;
|
|
console.log(e);
|
|
updateStreamStatus("The stream was unreachable. Try refreshing the page.", "yellow", false, true);
|
|
}
|
|
xhr.ontimeout = xhr.onerror;
|
|
xhr.onload = function(e) {
|
|
heartIsBeating = false;
|
|
if ( xhr.status != 200 ) {
|
|
return xhr.onerror(xhr);
|
|
}
|
|
response = JSON.parse(xhr.responseText)
|
|
|
|
// reset radial loader
|
|
resetRadialLoader(nextHeartbeat - Date.now() / 1000);
|
|
|
|
// update viewer count
|
|
viewerCount.innerHTML = response.viewers;
|
|
|
|
// update stream title
|
|
streamTitle.innerHTML = response.title;
|
|
|
|
// update stream start time (for the timer)
|
|
const oldStreamAbsoluteStart = streamAbsoluteStart;
|
|
streamAbsoluteStart = response.start_abs;
|
|
streamRelativeStart = response.start_rel;
|
|
streamTimerLastUpdated = Date.now() / 1000;
|
|
|
|
// update stream status
|
|
if ( !response.online ) {
|
|
return updateStreamStatus("The stream has ended.", "red", false, false);
|
|
}
|
|
|
|
const serverSegment = response.current_segment;
|
|
if ( !Number.isInteger(serverSegment) ) {
|
|
return updateStreamStatus("The stream restarted. Reload the stream.", "yellow", true, false);
|
|
}
|
|
|
|
if ( oldStreamAbsoluteStart != response.start_abs ) {
|
|
return updateStreamStatus("The stream restarted. Reload the stream.", "yellow", true, false);
|
|
}
|
|
|
|
// when the page is first loaded clientSegment may be null
|
|
const clientSegment = currentSegment();
|
|
if ( Number.isInteger(clientSegment) ) {
|
|
const diff = serverSegment - clientSegment;
|
|
if ( diff >= segmentThreshold ) {
|
|
return updateStreamStatus(`You're more than ${latencyThreshold} seconds behind the stream. Reload the stream.`, "yellow", true, false);
|
|
} else if ( diff < 0 ) {
|
|
return updateStreamStatus("The stream restarted. Reload the stream.", "yellow", true, false);
|
|
}
|
|
} else if (Date.now() / 1000 - t0 >= playbackTimeout) {
|
|
return updateStreamStatus("The stream is online but you're not receiving it. Try refreshing the page.", "yellow", false, true);
|
|
}
|
|
|
|
// otherwise
|
|
return updateStreamStatus("The stream is online.", "green", false, false);
|
|
}
|
|
|
|
xhr.send();
|
|
} catch ( error ) {
|
|
heartIsBeating = false;
|
|
throw error;
|
|
}
|
|
}
|