require('../../scss/video.scss');
const $ = require('jquery');
const utils = require('.');
const screenfull = require('screenfull');
const noScroll = require('no-scroll');
const query_string = require('query-string');
const bs = require('binary-search');
const api = require('./api');
const DOMPurify = require('dompurify');
const storage = require('../utils/storage');
let websocket;
let our_user_id;
let video_id;
let video;
let $video_overlay;
// timestamp in ms
let curr_subtitle_index = -1;
let subtitles;
let ready = false;
let selected_state = "Here";
let hide_controls_timeout;
function sendViewerState(state) {
//logger.log("send new state:", ready, state || selected_state);
websocket.send({
command: "update-viewer",
data: {
video_id: video_id,
viewer: {
ready: ready,
state: state || selected_state
}
}
});
}
function togglePaused() {
let $button = $("#play-pause-button");
if (!$button.hasClass("disabled-button")) {
$button.text("Wait...");
logger.log("Current time when paused:", video.currentTime);
if (video.paused) {
selected_state = "Here";
} else {
selected_state = "Paused";
}
sendViewerState();
websocket.send({
command: "set-server-video-state",
data: {
video_id: video_id,
state: {
paused: !video.paused
}
}
});
}
}
function toggleFullscreen() {
if (screenfull.isEnabled) {
if (screenfull.isFullscreen) {
$("#fullscreen-button").text("Enter Fullscreen");
if (!video.paused) {
toggleFullscreenVideo();
clearTimeout(hide_controls_timeout);
$(".video-controls").removeClass("fullscreen");
$(".subtitle-wrapper").removeClass("fullscreen-controls");
}
} else {
$("#fullscreen-button").text("Exit Fullscreen");
if (!video.paused) {
toggleFullscreenVideo();
}
}
screenfull.toggle($video_overlay[0]);
}
}
function toggleFullscreenVideo() {
$(".video-wrapper").toggleClass("fullscreen");
$(".video-controls").toggleClass("fullscreen");
$(".video-sidebar").animate({
width: 'toggle'
}, 350);
}
module.exports = {
video_id: function() {
return video_id;
},
open: function(data, new_websocket) {
websocket = new_websocket;
video_id = data.video_id;
our_user_id = data.user_id;
video = document.createElement("video");
$video_overlay = $(`<div class="video-overlay"></div>`).append(
$(`<div class="video-wrapper"></div>`).on("mousemove", function() {
if (screenfull.isFullscreen && !video.paused) {
$(".video-controls").removeClass("fullscreen");
$(".subtitle-wrapper").addClass("fullscreen-controls");
clearTimeout(hide_controls_timeout);
hide_controls_timeout = setTimeout(function() {
if (!video.paused) {
$(".video-controls").addClass("fullscreen");
$(".subtitle-wrapper").removeClass("fullscreen-controls");
}
}, 3000);
}
}).append(video).append(
`<div class="subtitle-wrapper"></div>`
),
`<div class="video-sidebar">
<div class="field-header sidebar">Viewers</div>
<div class="viewer-list"></div>
</div>`,
$(`<div class="video-controls"></div>`).append($(
`<div class="seek-wrapper"></div>`
).append($(
`<input type="range" id="seek" class="seek-slider" min="0" max="1" value="0">`
).on("change", function() {
$(this).trigger("blur");
logger.log(document.getElementById("seek").value);
let $button = $("#play-pause-button");
if (video.paused && !$button.hasClass("disabled-button")) {
$button.text("Wait...");
websocket.send({
command: "set-server-video-state",
data: {
video_id: data.video_id,
state: {
currentTime: document.getElementById("seek").value / 1000
}
}
});
} else {
document.getElementById("seek").value = video.currentTime * 1000;
}
}))).append(
$(`<button class="video-control-button" id="subtitles-button" style="display: none;">Enable Subtitles</button>`).on("click", function() {
let $this = $(this).trigger("blur");
let $subtitle_wrapper = $(".subtitle-wrapper");
if ($subtitle_wrapper.is(":visible")) {
$subtitle_wrapper.hide();
$this.text("Enable Subtitles");
} else {
$subtitle_wrapper.show();
$this.text("Disable Subtitles");
}
}),
$(`<button class="video-control-button" id="play-pause-button">Play</button>`).on("click", function() {
$(this).trigger("blur");
togglePaused();
}),
$(`<div class="volume-wrapper"></div>`).append(
`<label for="volume">Volume</label>`,
$(`<input type="range" id="volume" class="seek-slider volume" min="1" max="100" value="${storage.get('volume') || 50}">`).on("change", function() {
$(this).trigger("blur");
let volume = document.getElementById("volume").value * 1;
logger.log("new volume:", volume);
video.volume = volume / 100;
storage.set('volume', volume);
})
),
$(`<button class="video-control-button" id="fullscreen-button">Enter Fullscreen</button>`).on("click", function() {
$(this).trigger("blur");
toggleFullscreen();
}),
$(`<button class="video-control-button">Leave Room</button>`).on("click", function() {
$(this).trigger("blur");
if (screenfull.isEnabled && screenfull.isFullscreen) {
screenfull.exit();
}
$(document.body).off("keydown");
websocket.send({
command: "stop-watching",
data: data.video_id
});
video_id = undefined;
$video_overlay.remove();
noScroll.off();
window.history.replaceState({}, "", location.pathname);
})
)
);
for (const viewer of Object.values(data.viewers)) {
logger.log(viewer);
update_viewer(viewer);
}
const source = document.createElement("source");
source.setAttribute("src", "/api/video/" + data.video_id);
video.ondurationchange = function() {
logger.log("duration set to", video.duration);
let seek = document.getElementById("seek");
if (!seek) {
return;
}
seek.max = video.duration * 1000;
};
video.ontimeupdate = function() {
//logger.log("Time updated to", video.currentTime);
let time_ms = video.currentTime * 1000;
if (subtitles) {
if (video.paused) {
logger.log("Current time", time_ms);
// binary search for correct subtitle index
curr_subtitle_index = bs(subtitles, time_ms, function(element, needle) {
return element.start - needle;
});
if (curr_subtitle_index < 0) {
// If the value is not in the array, then -(index + 1) is returned, where
// index is where the value should be inserted into the array to maintain sorted order
curr_subtitle_index = Math.max((-curr_subtitle_index) - 2, -1);
logger.log("curr index", subtitles[curr_subtitle_index]);
if (curr_subtitle_index >= 0) {
if (time_ms >= subtitles[curr_subtitle_index].start && time_ms < subtitles[curr_subtitle_index].end) {
set_subtitles(subtitles[curr_subtitle_index].text);
} else {
set_subtitles("");
}
}
} else {
// Amazing, time_ms is the EXACT start time of a subtitle
set_subtitles(subtitles[curr_subtitle_index].text);
}
} else {
if (curr_subtitle_index >= 0 &&
time_ms >= subtitles[curr_subtitle_index].end
) {
set_subtitles("");
}
if (curr_subtitle_index + 1 < subtitles.length &&
time_ms >= subtitles[curr_subtitle_index + 1].start
) {
curr_subtitle_index++;
set_subtitles(subtitles[curr_subtitle_index].text);
}
}
}
let seek = document.getElementById("seek");
if (!seek) {
return;
}
seek.value = time_ms;
};
video.oncanplay = function() {
logger.log("Video can play");
};
video.oncanplaythrough = function() {
logger.log("Video can play without buffering");
ready = true;
sendViewerState();
};
video.onstalled = function() {
logger.log("Video stopped loading");
};
video.onprogress = function() {
logger.log("Video loading");
};
video.onwaiting = function() {
logger.log("Video is waiting for more data.");
//ready = false;
//sendViewerState("Buffering...");
};
video.onplaying = function() {
logger.log("Video playing after waiting");
//ready = true;
//sendViewerState("Here");
};
video.appendChild(source);
$(document.body).append($video_overlay);
noScroll.on();
window.history.replaceState({}, "", location.pathname + "?" + query_string.stringify({
watchroom: video_id
}));
$(document.body).on("keydown", function(event) {
if (event.key === " ") {
togglePaused();
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
} else if (event.key === "Escape" && screenfull.isEnabled && screenfull.isFullscreen) {
toggleFullscreen();
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
});
api.post("/video/subtitle", {
video_id: video_id
}).then(function(data) {
logger.log("Got back subtitles:", data);
if (data.subtitles) {
$("#subtitles-button").show();
subtitles = data.subtitles;
}
}).catch(api.handle_error_message);
if (video.readyState > 3 && ready === false) {
logger.log("canplaythrough event fired before listener was attached");
ready = true;
sendViewerState();
} else {
logger.log("Video state", video.readyState, "immediately after load");
}
},
set_state: function(state) {
logger.log("set state to", state);
if (Object.prototype.hasOwnProperty.call(state, "paused") && state.paused !== video.paused) {
if (state.paused) {
video.pause();
if (state.invoked_by !== our_user_id || state.invoked_by === "server") {
$("#play-pause-button").addClass("disabled-button");
}
$("#rewind-button").removeClass("disabled-button");
clearTimeout(hide_controls_timeout);
} else {
video.play();
$("#play-pause-button").removeClass("disabled-button").text("Pause");
$("#rewind-button").addClass("disabled-button");
}
if (screenfull.isFullscreen) {
toggleFullscreenVideo();
if (video.paused) {
clearTimeout(hide_controls_timeout);
$(".video-controls").removeClass("fullscreen");
$(".subtitle-wrapper").removeClass("fullscreen-controls");
}
}
}
if (Object.prototype.hasOwnProperty.call(state, "currentTime")) {
video.currentTime = state.currentTime;
}
// The play button is set to "Wait..." for multiple actions
// such as seek. Reset text here after they are executed.
if (video.paused) {
$("#play-pause-button").text("Play");
}
},
update_viewer: update_viewer
};
function set_subtitles(data) {
$(".subtitle-wrapper").text(DOMPurify.sanitize(data, {
RETURN_DOM: true
}).innerText);
}
function update_viewer(viewer) {
let $user_status = $(".viewer-list").find(`#${viewer.user_id}`);
if (!$user_status.length) {
$user_status = $(
`<div id="${viewer.user_id}" class="viewer">
<div class="viewer-row">
<span class="viewer-row-left">
<span class="viewer-name"></span>
</span>
<span class="viewer-state"></span>
</div>
<div class="viewer-row">
<span class="viewer-row-left">
Latency: <span class="viewer-latency"></span>ms
</span>
<span>‌</span>
</div>
</div>`
);
if (viewer.user_id === our_user_id) {
let $viewer_state_menu = $user_status.find(".viewer-state").addClass("self").on("click", function() {
let $this = $(this);
if (!$this.hasClass("viewer-state-menu-open")) {
$this.addClass("viewer-state-menu-open").hide().empty().append(
$(`<div class="viewer-state-option">Here</div>`).on("click", update_viewer_state($viewer_state_menu)),
$(`<div class="viewer-state-option">AFK - Wait</div>`).on("click", update_viewer_state($viewer_state_menu)),
$(`<div class="viewer-state-option">AFK - Play</div>`).on("click", update_viewer_state($viewer_state_menu))
).show();
}
});
$(".viewer-list").prepend($user_status);
} else {
$(".viewer-list").append($user_status);
}
}
$user_status.find(".viewer-name").text(viewer.display_name || "null");
$user_status.find(".viewer-latency").text(utils.round(viewer.latency));
$user_status.find(".viewer-state").not(".viewer-state-menu-open").text(viewer.state);
}
function update_viewer_state($viewer_state_menu) {
return function(event) {
event.stopPropagation();
selected_state = event.target.innerText;
sendViewerState();
$viewer_state_menu.removeClass("viewer-state-menu-open");
};
}