ovenemprex/assets/player.js
Trysdyn Black c1aaac4923 Only sort frames when it's safe and pertinent
- Don't sort if a player is fullscreened. Fixes #12
- Force a sort when exiting fullscreen
- Only sort when the player list changes, not any resize
2025-03-18 23:24:45 -07:00

249 lines
7.2 KiB
JavaScript

// Get our initial stream list and create streams
xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
processStreamList(JSON.parse(this.responseText));
}
}
disabledPlayers = [];
playerVolumeSettings = {};
function webcallFrameSort() {
// If we're full screening a player this will break it, so don't
if (document.fullscreenElement) { return }
// Sort the players alphabetically by ID
Array.from(document.body.querySelectorAll(".frame"))
.sort((a, b) => a.id > b.id ? 1 : -1)
.forEach(node => document.body.appendChild(node))
}
// Auto-resize frames in a webcall interface
function webcallFrameResize() {
// Figure out how many Frames are visible
div_count = 0;
document.querySelectorAll(".frame").forEach(
function(element) {
div_count += 1;
}
)
// If none are visible, show placeholder text and bail
if (div_count < 1) {
document.querySelector("#placeholder").style.display = "block";
return;
}
// Hide placeholder if any players are visible
document.querySelector("#placeholder").style.display = "none";
// Get player frame aspect ratio for fitting purposes
const player_ar = 16 / 9;
// Try arrangements until the best fit is found
// Take the first column count that doesn't overflow height
cols = 0
for (let i = 1; i <= div_count; i++) {
const frame_width = window.innerWidth / i;
const frame_height = frame_width / player_ar;
if (frame_height * Math.ceil(div_count / i) <= window.innerHeight) {
cols = i;
break;
}
}
// Set frames to the appropriate width
if (cols) {
w = `${Math.floor(100 / cols)}%`;
} else {
w = `${Math.floor(window.innerHeight * player_ar)}px`;
}
document.querySelectorAll(".frame").forEach(
function(element) {
element.style.width = w;
}
)
}
function createPlayer(stream, muted, volume) {
// Create frame
var outer_div = document.createElement("div");
outer_div.classList.add("frame");
outer_div.id = `frame_${stream}`;
// If we're putting name frames around players, make them
// Also provide close buttons to nuke this player
if (named_frames) {
var tab_div = document.createElement("div");
tab_div.classList.add("frame_tabs");
outer_div.appendChild(tab_div);
var name_div = document.createElement("div");
name_div.classList.add("frame_name");
name_div.innerHTML = `<a href="${stream}"> ${stream} </a>`;
tab_div.appendChild(name_div);
var button_div = document.createElement("div");
button_div.classList.add("frame_buttons");
button_div.innerHTML = `<a href="#close" onclick="closePlayer('${stream}'); return false"> &#10006; </a>`;
tab_div.appendChild(button_div);
}
// Put the player div in the container
var player_div = document.createElement("div");
player_div.classList.add("player");
player_div.id = stream;
outer_div.appendChild(player_div);
// Create a throbber for dead streams
// We had this in the player div before, but it blinks every time the player reconnects
// This hides under a backgroundless player so it's only visible when nothing's up
throbber_div = document.createElement("div");
throbber_div.id = "throbber";
outer_div.appendChild(throbber_div);
// Put container in document
document.body.appendChild(outer_div);
// Initialize OvenPlayer
// We want as little interface stuff as possible, but we need to keep
// volume controls around so that can't be hidden.
player = OvenPlayer.create(stream, {
currentProtocolOnly: true,
showBigPlayButton: false,
aspect: "16:9",
autoStart: true,
mute: muted,
volume: volume,
sources: [
{
label: stream,
type: 'webrtc',
file: `wss://${domain}:3334/${app_name}/${stream}`
}
]
});
// Set player up to auto-restart if it stops
// If a streamer goes offline, the player will be reaped
player.stateManager = manageState;
player.on("stateChanged", player.stateManager);
// Run a resize and sort
if (named_frames) {
webcallFrameResize();
webcallFrameSort();
}
}
function manageState(data) {
// This serves one purpose: keep attempting to start a live player if it stops
if (data.newstate == "error") {
setTimeout(this.setCurrentSource, 3000, 0);
}
}
function closePlayer(containerId) {
// Close the player
destroyPlayerById(containerId);
// Add this ID to the closed player list so we don't reopen it
if (!disabledPlayers.includes(containerId)) {
disabledPlayers.push(containerId);
}
}
function destroyPlayerById(containerId) {
player = OvenPlayer.getPlayerByContainerId(containerId);
// Get our volume settings to save for re-use if this player comes back
playerVolumeSettings[containerId] = [player.getMute(), player.getVolume()];
// Tear down player
player.remove();
// Delete frame
document.getElementById(`frame_${containerId}`).remove();
// Run our resize and sort
if (named_frames) {
webcallFrameResize();
webcallFrameSort();
}
}
function processStreamList(streams) {
// Remove any closed player from the list
disabledPlayers.forEach((i, index) => {
var removeIndex = streams.indexOf(i);
if (removeIndex !== -1) {
streams.splice(removeIndex, 1);
}
})
// Create any player in the list that doesn't have one
streams.forEach((i, index) => {
if (OvenPlayer.getPlayerByContainerId(i) == null) {
// Check if we have volume settings for this player
var muted = true;
var volume = 100;
if (i in playerVolumeSettings) {
muted = playerVolumeSettings[i][0];
volume = playerVolumeSettings[i][1];
}
// Create the player with noted settings or defaults
createPlayer(i, muted, volume);
}
})
// Destroy any player not in the list
var players = OvenPlayer.getPlayerList();
players.forEach((p, index) => {
if (!streams.includes(p.getContainerId())) {
destroyPlayerById(p.getContainerId());
}
})
}
function requestStreamList() {
xhr.open("GET", `https://${domain}/status/default/${app_name}/`)
xhr.send()
}
// Set up each embed
function EmprexSetup() {
// Pre-populate our disabled list if we have a param for it
window.location.search.substr(1).split("&").forEach((s, index) => {
tmp = s.split("=");
if (tmp[0] == "disabled") {
disabledPlayers = decodeURIComponent(tmp[1]).split(",");
}
})
// Placeholder if nothing is live
placeholder = document.createElement("div");
placeholder.id = "placeholder";
placeholder.innerHTML = "<img src='/assets/errorlogo.gif'><br />Waiting for a stream to start...";
document.body.appendChild(placeholder);
// Also resize elements to fill the frame
// We try to debounce this so we're not editing the DOM 100x a second on resize
resize_timeout = false;
addEventListener("resize", function() {
clearTimeout(resize_timeout);
resize_timeout = setTimeout(webcallFrameResize, 250);
})
// If we enter or exit full screen, call the webcall frame sort
// The sort will refuse to run if we're in full screen. So we need to sort once we leave it
addEventListener("fullscreenchange", webcallFrameSort);
// Update streams every 5 seconds, and immediately
requestStreamList();
setInterval(requestStreamList, 5000);
}