summaryrefslogtreecommitdiff
path: root/pages/together.php
diff options
context:
space:
mode:
authorMinteck <contact@minteck.org>2022-08-21 17:31:56 +0200
committerMinteck <contact@minteck.org>2022-08-21 17:31:56 +0200
commita2df9a69dcc14cb70118cda2ded499055e7ee358 (patch)
tree6dd283e4e9452d38bce81ddaaae49b5335755842 /pages/together.php
parent84dd0735820b16b60f600284d35183d76547a71f (diff)
downloadpluralconnect-a2df9a69dcc14cb70118cda2ded499055e7ee358.tar.gz
pluralconnect-a2df9a69dcc14cb70118cda2ded499055e7ee358.tar.bz2
pluralconnect-a2df9a69dcc14cb70118cda2ded499055e7ee358.zip
m. update
Diffstat (limited to 'pages/together.php')
-rw-r--r--pages/together.php941
1 files changed, 941 insertions, 0 deletions
diff --git a/pages/together.php b/pages/together.php
new file mode 100644
index 0000000..644a6c0
--- /dev/null
+++ b/pages/together.php
@@ -0,0 +1,941 @@
+<?php
+
+require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn;
+if (!$isLoggedIn) header("Location: /login") and die();
+
+$title = "Watch Together"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php';
+
+global $WebSocketAddress;
+
+if (!isset($WebSocketAddress)) {
+ $WebSocketAddress = "wss://ponies.equestria.horse/_WatchTogether-WebSocket-EntryPoint/socket";
+}
+
+?>
+
+<style>
+ .list-group-item {
+ color: #fff;
+ background-color: #222;
+ border: 1px solid rgba(255, 255, 255, .125);
+ }
+
+ .list-group-item.disabled {
+ color: #fff;
+ background-color: #222;
+ border-color: rgba(255, 255, 255, .125);
+ opacity: .75;
+ }
+
+ .list-group-item-action:hover {
+ background-color: #252525;
+ color: #ddd;
+ }
+
+ .list-group-item-action:active, .list-group-item-action:focus {
+ background-color: #272727;
+ color: #bbb;
+ }
+
+ .video-queue-item {
+ display: grid;
+ grid-template-columns: 1fr max-content;
+ grid-gap: 10px;
+ cursor: pointer;
+ }
+
+ .video-queue-item-status {
+ filter: invert(1);
+ display: block;
+ height: 32px;
+ width: 32px;
+ }
+
+ .video-queue-item-part {
+ display: flex;
+ align-items: center;
+ }
+
+ .video-queue-item-metadata {
+ width: 100%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ .video-queue-item-title-outer, .video-queue-item-author-outer {
+ display: flex;
+ align-items: center;
+ }
+
+ .video-queue-item-title, .video-queue-item-title-outer, .video-queue-item-author, .video-queue-item-author-outer {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ width: 100%;
+ }
+
+ .modal-header {
+ border-bottom: 1px solid #353738;
+ }
+
+ .modal-content {
+ border: 1px solid rgba(255, 255, 255, .2);
+ background-color: #111;
+ }
+
+ .btn-close {
+ filter: invert(1);
+ }
+
+ .user-ping {
+ float: right;
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ border-radius: 999px;
+ position: relative;
+ top: 5px;
+ }
+
+ .control-item {
+ display: inline-block;
+ cursor: pointer;
+ border-radius: 999px;
+ background-color: rgba(0, 0, 0, 0);
+ border: 1px solid rgba(255, 255, 255, 0);
+ transition: background-color 200ms, border-color 200ms;
+ }
+
+ .control-item:hover {
+ background-color: rgba(0, 0, 0, .25);
+ border-color: rgba(255, 255, 255, .1);
+ }
+
+ .control-item:active {
+ background-color: rgba(0, 0, 0, .5);
+ border-color: rgba(255, 255, 255, .25);
+ }
+
+ .control-icon {
+ filter: invert(1);
+ width: 24px;
+ height: 24px;
+ margin: 12px;
+ pointer-events: none;
+ }
+
+ body.hide-controls * {
+ cursor: none;
+ }
+
+ #controls-outer {
+ opacity: 1;
+ transition: opacity 500ms;
+ }
+
+ .navbar, #sidebar {
+ opacity: 1;
+ transition: opacity 500ms;
+ }
+
+ body.hide-controls #controls-outer {
+ opacity: 0;
+ }
+
+ body.hide-controls .navbar, body.hide-controls #sidebar {
+ opacity: .25;
+ }
+
+ body.fullscreen #app-container {
+ grid-template-columns: 1fr !important;
+ }
+
+ body.fullscreen .navbar, body.fullscreen #sidebar {
+ display: none;
+ }
+
+ body.fullscreen #app-container div, body.fullscreen #app-container video {
+ height: 100vh !important;
+ }
+
+ body.fullscreen #controls-outer, body.fullscreen #notification {
+ inset: 0 !important;
+ }
+
+ #notification {
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 200ms;
+ }
+
+ #video-title {
+ opacity: 1;
+ transition: opacity 200ms;
+ }
+
+ body.notification #notification {
+ opacity: 1;
+ }
+
+ body.notification #video-title {
+ opacity: 0;
+ }
+
+ body.skipper #skipper {
+ opacity: 1 !important;
+ pointer-events: initial !important;
+ }
+
+ #skipper {
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 200ms;
+ position: fixed;
+ bottom: 79px;
+ z-index: 999;
+ background: rgba(0, 0, 0, .5);
+ border-radius: 10px;
+ padding: 10px 20px;
+ left: 20px;
+ border: 1px solid rgba(255, 255, 255, .25);
+ cursor: pointer;
+ }
+
+ #skipper:hover {
+ background: rgba(0, 0, 0, .75) !important;
+ }
+
+ #skipper:active {
+ background: rgba(0, 0, 0, 1) !important;
+ }
+</style>
+
+<script src="/assets/editor/thing.js"></script>
+
+<div class="modal fade" id="error" data-bs-backdrop="static" data-bs-keyboard="false" style="z-index: 99999;">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title">An error occurred</h4>
+ </div>
+
+ <div class="modal-body">
+ <div class="alert alert-danger" id="error-message"></div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="modal fade" id="add" data-bs-backdrop="static" data-bs-keyboard="false">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title">Add new video</h4>
+ </div>
+
+ <div class="modal-body">
+ <p>Enter a YouTube video ID to add to the queue:</p>
+ <input type="text" class="form-control" id="add-id" placeholder="e.g. K9tUKbOFots" style="color:white;background:#111;border-color:#222;">
+ <p style="margin-top:10px;margin-bottom: 0;">
+ <div class="btn-group">
+ <span onclick="doAdd();" id="add-btn-yes" class="btn btn-primary">Add</span>
+ <span onclick="closeAdd();" id="add-btn-no" class="btn btn-secondary">Cancel</span>
+ </div>
+ <a href="#" onclick="document.getElementById('add-id').value = 'K9tUKbOFots';">test</a>
+ </p>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="modal fade" id="welcome" data-bs-backdrop="static" data-bs-keyboard="false">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title">Welcome to Watch Together!</h4>
+ </div>
+
+ <div class="modal-body">
+ <h5>Host a new session</h5>
+ <p>You will be given an invite code to share with your partner·s.</p>
+ <span class="btn btn-primary" id="host-start" onclick="hostSession();">Start</span>
+
+ <hr>
+
+ <h5>Join an existing session</h5>
+ <p>Enter the invite code your partner has given you:</p>
+ <div style="display:grid;grid-template-columns: repeat(8, 1fr);grid-gap:10px;">
+ <input type="text" class="form-control" id="invite-input-1" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ <input type="text" class="form-control" id="invite-input-2" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ <input type="text" class="form-control" id="invite-input-3" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ <input type="text" class="form-control" id="invite-input-4" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ <input type="text" class="form-control" id="invite-input-5" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ <input type="text" class="form-control" id="invite-input-6" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ <input type="text" class="form-control" id="invite-input-7" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ <input type="text" class="form-control" id="invite-input-8" style="text-align: center;color:white;background:#111;border-color:#222;" onchange="processInviteCode(event);" onkeydown="processInviteCode(event);" onkeyup="processInviteCode(event);" maxlength="1">
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<script>
+ document.getElementById("invite-input-1").disabled = true;
+ document.getElementById("invite-input-2").disabled = true;
+ document.getElementById("invite-input-3").disabled = true;
+ document.getElementById("invite-input-4").disabled = true;
+ document.getElementById("invite-input-5").disabled = true;
+ document.getElementById("invite-input-6").disabled = true;
+ document.getElementById("invite-input-7").disabled = true;
+ document.getElementById("invite-input-8").disabled = true;
+ document.getElementById("host-start").classList.add("disabled");
+
+ let modal = new bootstrap.Modal(document.getElementById('welcome'));
+ modal.show();
+ document.getElementById("invite-input-1").focus();
+
+ window.modalAdd = new bootstrap.Modal(document.getElementById('add'));
+
+ function openAdd() {
+ modalAdd.show();
+ }
+
+ function closeAdd() {
+ document.getElementById("add-id").value = "";
+ modalAdd.hide();
+ }
+
+ function doAdd() {
+ document.getElementById("add-id").disabled = true;
+ document.getElementById("add-btn-yes").classList.add("disabled");
+ document.getElementById("add-btn-no").classList.add("disabled");
+
+ socket.send(JSON.stringify({
+ task: "UPDATE_QUEUE",
+ payload: {
+ operation: "+",
+ video: document.getElementById("add-id").value
+ }
+ }))
+ }
+
+ function toPrettyTime(seconds) {
+ let parts = new Date(seconds * 1000).toUTCString().split(" ")[4].split(":");
+ parts[0] = parseInt(parts[0]).toString();
+
+ if (parts[0] === "0") {
+ parts[1] = parseInt(parts[1]).toString();
+ parts.shift();
+ }
+
+ return parts.join(":");
+ }
+
+ function processInviteCode(event) {
+ let i1 = document.getElementById("invite-input-1").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+ let i2 = document.getElementById("invite-input-2").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+ let i3 = document.getElementById("invite-input-3").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+ let i4 = document.getElementById("invite-input-4").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+ let i5 = document.getElementById("invite-input-5").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+ let i6 = document.getElementById("invite-input-6").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+ let i7 = document.getElementById("invite-input-7").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+ let i8 = document.getElementById("invite-input-8").value.trim().toLowerCase().replace(/[^\da-z]+/gm, "");
+
+ document.getElementById("invite-input-1").value = i1;
+ document.getElementById("invite-input-2").value = i2;
+ document.getElementById("invite-input-3").value = i3;
+ document.getElementById("invite-input-4").value = i4;
+ document.getElementById("invite-input-5").value = i5;
+ document.getElementById("invite-input-6").value = i6;
+ document.getElementById("invite-input-7").value = i7;
+ document.getElementById("invite-input-8").value = i8;
+
+ if (i8 === "") {
+ document.getElementById("invite-input-8").focus();
+ }
+ if (i7 === "") {
+ document.getElementById("invite-input-7").focus();
+ }
+ if (i6 === "") {
+ document.getElementById("invite-input-6").focus();
+ }
+ if (i5 === "") {
+ document.getElementById("invite-input-5").focus();
+ }
+ if (i4 === "") {
+ document.getElementById("invite-input-4").focus();
+ }
+ if (i3 === "") {
+ document.getElementById("invite-input-3").focus();
+ }
+ if (i2 === "") {
+ document.getElementById("invite-input-2").focus();
+ }
+ if (i1 === "") {
+ document.getElementById("invite-input-1").focus();
+ }
+
+ if (event instanceof KeyboardEvent) {
+ if (event.code === "Backspace" && event.type === "keydown") {
+ let el = event.target;
+ let ep = document.getElementById("invite-input-" + (el['id'].split("-")[2] - 1));
+
+ if (ep !== null) {
+ if (el.value.trim() === "") {
+ ep.value = "";
+ ep.focus();
+ } else {
+ el.value = "";
+ ep.focus();
+ }
+ }
+ }
+ }
+
+ if (i1.length === 1 && i2.length === 1 && i3.length === 1 && i4.length === 1 && i5.length === 1 && i6.length === 1 && i7.length === 1 && i8.length === 1 && ((event.type && event.type === "keyup") || !event.type)) {
+ joinSession(i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8);
+ }
+ }
+
+ async function joinSession(code) {
+ document.getElementById("invite-input-1").disabled = true;
+ document.getElementById("invite-input-2").disabled = true;
+ document.getElementById("invite-input-3").disabled = true;
+ document.getElementById("invite-input-4").disabled = true;
+ document.getElementById("invite-input-5").disabled = true;
+ document.getElementById("invite-input-6").disabled = true;
+ document.getElementById("invite-input-7").disabled = true;
+ document.getElementById("invite-input-8").disabled = true;
+ document.getElementById("host-start").classList.add("disabled");
+
+ window.inviteCode = code;
+
+ startSession();
+ }
+
+ async function hostSession() {
+ document.getElementById("invite-input-1").disabled = true;
+ document.getElementById("invite-input-2").disabled = true;
+ document.getElementById("invite-input-3").disabled = true;
+ document.getElementById("invite-input-4").disabled = true;
+ document.getElementById("invite-input-5").disabled = true;
+ document.getElementById("invite-input-6").disabled = true;
+ document.getElementById("invite-input-7").disabled = true;
+ document.getElementById("invite-input-8").disabled = true;
+ document.getElementById("host-start").classList.add("disabled");
+
+ window.inviteCode = null;
+
+ startSession();
+ }
+
+ window.terminated = false;
+ window.users = null;
+ window.queue = null;
+ window.segments = [];
+ window.originalTitle = document.title;
+
+ async function connect() {
+ window.identity = JSON.parse(await (await window.fetch("/api/me")).text());
+ window.sessionToken = await (await window.fetch("/api/token")).text();
+
+ window.socket = new WebSocket("<?= $WebSocketAddress ?>");
+
+ socket.onclose = (event) => {
+ console.log("[ws:close] ",event);
+
+ if (window.terminated) {
+ modal.hide();
+ return;
+ }
+
+ if (event.wasClean) {
+ document.getElementById('error-message').innerText = "Connection closed";
+ } else {
+ if (event.reason) {
+ document.getElementById('error-message').innerText = "Connection closed unexpectedly with code " + event.code + ": " + event.reason;
+ } else {
+ document.getElementById('error-message').innerText = "Connection closed unexpectedly with code " + event.code;
+ }
+ }
+
+ let errorModal = new bootstrap.Modal(document.getElementById('error'));
+ errorModal.show();
+ modal.hide();
+ }
+
+ socket.onmessage = async (event) => {
+ let data = JSON.parse(event.data);
+ if (data.task !== "HEARTBEAT_ACK") console.log("[ws:message]", data);
+
+ if (data.task === "CONFIG") {
+ document.getElementById("invite-input-1").disabled = false;
+ document.getElementById("invite-input-2").disabled = false;
+ document.getElementById("invite-input-3").disabled = false;
+ document.getElementById("invite-input-4").disabled = false;
+ document.getElementById("invite-input-5").disabled = false;
+ document.getElementById("invite-input-6").disabled = false;
+ document.getElementById("invite-input-7").disabled = false;
+ document.getElementById("invite-input-8").disabled = false;
+ document.getElementById("host-start").classList.remove("disabled");
+
+ setInterval(() => {
+ socket.send(JSON.stringify({
+ task: "HEARTBEAT",
+ payload: {
+ videoPositon: document.getElementById("video").currentTime
+ }
+ }));
+ }, data.payload.heartbeatInterval);
+
+ return;
+ }
+
+ if (data.task === "SESSION") {
+ window.users = data.payload.users;
+ window.queue = data.payload.queue;
+
+ updateQueue();
+
+ document.getElementById("participants").innerHTML = "";
+ for (let user of data.payload.users) {
+ document.getElementById("participants").innerHTML += "<li class='list-group-item'>" + user.id + "<span id='user-" + user.id + "-ping' class='bg-primary user-ping'></span></li>";
+ }
+
+ document.getElementById("invite-code").innerText = data.payload.code;
+ }
+
+ if (data.task === "VIDEO_UPDATE") {
+ if (data.payload.id !== null) {
+ try {
+ window.segments = JSON.parse(await (await window.fetch("https://sponsor.ajay.app/api/skipSegments/" + data.payload.sha.substring(0, 32) + '?categories=["sponsor","intro","outro","music_offtopic"]')).text()).filter(i => i['videoID'] === data.payload.id)[0].segments;
+ notification("This video is enhanced by smart playback", "success");
+ } catch (e) {
+ notification("This video is not compatible with smart playback", "warning");
+ window.segments = [];
+ }
+ }
+
+ if (data.payload.url) document.getElementById("video").src = data.payload.url;
+
+ if (data.payload.state === 1) {
+ document.getElementById("video").play();
+ } else if (!!data.payload.state) {
+ document.getElementById("video").pause();
+ }
+
+ if (data.payload.title && data.payload.title.trim() !== "") {
+ document.getElementById("video-title").innerText = data.payload.title;
+ document.title = data.payload.title + " · " + window.originalTitle;
+ } else {
+ if (data.payload.title) {
+ document.getElementById("video-title").innerText = "";
+ document.title = window.originalTitle;
+ }
+ }
+
+ document.getElementById("video").currentTime = data.payload.position;
+ }
+
+ if (data.task === "UPDATE_USERS") {
+ window.users = data.payload.users;
+
+ document.getElementById("participants").innerHTML = "";
+ for (let user of data.payload.users) {
+ document.getElementById("participants").innerHTML += "<li class='list-group-item'>" + user.id + "<span id='user-" + user.id + "-ping' class='bg-primary user-ping'></span></li>";
+ }
+ }
+
+ if (data.task === "UPDATE_QUEUE") {
+ window.queue = data.payload.queue;
+
+ if (data.payload.poster === identity.id) {
+ document.getElementById("add-id").disabled = false;
+ document.getElementById("add-id").value = "";
+ document.getElementById("add-btn-yes").classList.remove("disabled");
+ document.getElementById("add-btn-no").classList.remove("disabled");
+
+ modalAdd.hide();
+ }
+
+ updateQueue();
+ }
+
+ if (data.task === "HEARTBEAT_ACK") {
+ for (let user of Object.keys(data.payload.delays)) {
+ let abs = Math.abs(data.payload.delays[user]);
+ let color = "primary";
+
+ if (abs >= 500) color = "warning";
+ if (abs >= 1000) color = "danger";
+ if (abs < 500) color = "success";
+
+ let sign = "±";
+ if (data.payload.delays[user] < 0) sign = "-";
+ if (data.payload.delays[user] > 0) sign = "+";
+
+ document.getElementById("user-" + user + "-ping").title = sign + abs + " ms";
+ document.getElementById("user-" + user + "-ping").classList.remove("bg-primary");
+ document.getElementById("user-" + user + "-ping").classList.remove("bg-danger");
+ document.getElementById("user-" + user + "-ping").classList.remove("bg-warning");
+ document.getElementById("user-" + user + "-ping").classList.remove("bg-green");
+ document.getElementById("user-" + user + "-ping").classList.add("bg-" + color);
+ }
+ }
+
+ if (data.task === "TERMINATE" || data.task === "FAILURE") {
+ window.terminated = true;
+
+ if (data.payload.code) {
+ if (data.payload.reason) {
+ document.getElementById('error-message').innerText = data.payload.code + ": " + data.payload.reason;
+ let errorModal = new bootstrap.Modal(document.getElementById('error'));
+ errorModal.show();
+ } else {
+ document.getElementById('error-message').innerText = data.payload.code;
+ let errorModal = new bootstrap.Modal(document.getElementById('error'));
+ errorModal.show();
+ }
+ } else {
+ document.getElementById('error-message').innerText = "Error";
+ let errorModal = new bootstrap.Modal(document.getElementById('error'));
+ errorModal.show();
+ }
+
+ if (data.task === "FAILURE") socket.close();
+ }
+ }
+
+ socket.onopen = (event) => {
+ console.log("[ws:open] ", event);
+
+ socket.send(JSON.stringify({
+ task: "IDENTIFY",
+ payload: {
+ token: window.sessionToken
+ }
+ }))
+ }
+ }
+
+ function updateQueue() {
+ document.getElementById("queue").innerHTML = "";
+
+ for (let video of window.queue) {
+ document.getElementById("queue").innerHTML += `
+<li class="list-group-item list-group-item-action video-queue-item">
+ <div class="video-queue-item-part video-queue-item-metadata">
+ <div>
+ <div class="video-queue-item-title-outer">
+ <span class="video-queue-item-title">${video.title}</span>
+ </div>
+ <div class="video-queue-item-author-outer">
+ <span class="video-queue-item-author text-muted">${video.author}</span>
+ </div>
+ </div>
+ </div>
+ <div class="video-queue-item-part text-muted">${video['duration_pretty'] ?? toPrettyTime(video.duration)}</div>
+</li>
+`;
+ }
+ }
+
+ function startSession() {
+ socket.send(JSON.stringify({
+ task: "SESSION",
+ payload: {
+ id: window.inviteCode
+ }
+ }))
+
+ modal.hide();
+ }
+
+ connect();
+</script>
+
+<div style="height:calc(100vh - 60px);display:grid;grid-template-columns: 3fr 1.5fr;" id="app-container">
+ <div style="height:calc(100vh - 60px);">
+ <video id="video" style="width:100%;height:calc(100vh - 60px);"></video>
+ </div>
+ <script>
+
+ document.getElementById("video").onplay = () => {
+ console.log("play");
+
+ if (socket) socket.send(JSON.stringify({
+ task: "VIDEO_UPDATE",
+ payload: {
+ state: 1,
+ position: document.getElementById("video").currentTime
+ }
+ }));
+ }
+
+ document.getElementById("video").onpause = () => {
+ console.log("pause");
+
+ if (socket) socket.send(JSON.stringify({
+ task: "VIDEO_UPDATE",
+ payload: {
+ state: 0,
+ position: document.getElementById("video").currentTime
+ }
+ }));
+ }
+
+ let controlsFadeInterval;
+ document.body.classList.remove("hide-controls");
+
+ document.body.onmousemove = document.body.onmouseup = () => {
+ document.body.classList.remove("hide-controls");
+ try { clearTimeout(controlsFadeInterval) } catch (e) {}
+ controlsFadeInterval = setTimeout(() => {
+ if (!document.getElementById("video").paused) document.body.classList.add("hide-controls");
+ }, 5000);
+ }
+
+ </script>
+ <div class="container" id="sidebar" style="border-left:1px solid rgba(255, 255, 255, .25);">
+ <br>
+ <h4>Participants</h4>
+ <ul class="list-group" id="participants"></ul>
+ <p style="margin-top:10px;margin-bottom:0;">Invite new participants with this invite code: <code id="invite-code">--------</code></p>
+
+ <br>
+ <h4>Queue</h4>
+ <ul class="list-group" id="queue"></ul>
+ <p style="margin-top:10px;margin-bottom:0;">
+ <span class="btn btn-primary" onclick="openAdd()">Add video</span>
+ </p>
+ </div>
+</div>
+
+<div id="notification" style="z-index:999;padding: 15px 20px;font-size: 20px;position: fixed;top: 60px;left:0;text-shadow: 0 0 10px black;">
+ Notification.
+</div>
+<a onclick="controls_skipIntroOutro();" id="skipper">Skip</a>
+<div id="controls-outer" style="position:fixed;background: linear-gradient(180deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 35%, rgba(0,0,0,0) 65%, rgba(0,0,0,0.5) 100%);top: 60px;left: 0;right: 0;bottom: 0;">
+ <div id="video-title" style="padding: 15px 20px;font-size: 20px;"></div>
+ <div id="controls" style="position: fixed;bottom: 20px;height: 50px;left: 15px;display:grid;grid-template-columns: max-content 1fr max-content;">
+ <div>
+ <a id="control-play" onclick="controls_playPause();" class="control-item">
+ <img alt="" src="/assets/icons/together/play.svg" id="control-play-icon" class="control-icon">
+ </a>
+ <a id="control-next" onclick="controls_next();" class="control-item">
+ <img alt="" src="/assets/icons/together/next.svg" id="control-next-icon" class="control-icon">
+ </a>
+ </div>
+ <div style="display: flex;align-items: center;padding-left:15px;padding-right:15px;">
+ <div style="padding: 22px 0;width:100%;" id="seek-bar">
+ <div style="height:6px;width:100%;background:rgba(255, 255, 255, .1);border-radius:999px;pointer-events: none;">
+ <div style="height:6px;width:0;background:rgba(255, 255, 255, .75);border-radius:999px;" id="control-progress"></div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div style="display: inline-flex;align-items: center;">
+ <span id="time-remaining" style="font-family:monospace;">-0:00</span>
+ </div>
+ <a id="control-full" onclick="controls_fullscreen();" class="control-item">
+ <img alt="" src="/assets/icons/together/full-on.svg" id="control-full-icon" class="control-icon">
+ </a>
+ </div>
+ </div>
+ <div id="seek-bar-dot" style="position: fixed;bottom: 38px;width: 12px;height: 12px;background: white;border-radius: 999px;pointer-events: none;display:none;"></div>
+</div>
+<script>
+ document.getElementById("controls").style.width = (document.getElementById("video").clientWidth - 30) + "px";
+ document.getElementById("controls-outer").style.right = (window.innerWidth - document.getElementById("video").clientWidth) + "px";
+ document.getElementById("notification").style.right = (window.innerWidth - document.getElementById("video").clientWidth) + "px";
+
+ let notificationDecayTimeout;
+
+ function notification(text, color) {
+ document.getElementById("notification").innerText = text;
+ document.getElementById("notification").classList.add("text-" + color);
+ document.body.classList.add("notification");
+
+ try { clearTimeout(notificationDecayTimeout) } catch (e) {}
+
+ notificationDecayTimeout = setTimeout(() => {
+ document.body.classList.remove("notification");
+ setTimeout(() => {
+ document.getElementById("notification").innerText = "Notification.";
+ document.getElementById("notification").classList.remove("text-" + color);
+ }, 200);
+ }, 5000)
+ }
+
+ window.onresize = () => {
+ document.getElementById("controls").style.width = (document.getElementById("video").clientWidth - 30) + "px";
+ document.getElementById("controls-outer").style.right = (window.innerWidth - document.getElementById("video").clientWidth) + "px";
+ document.getElementById("notification").style.right = (window.innerWidth - document.getElementById("video").clientWidth) + "px";
+ }
+
+ function controls_playPause() {
+ if (document.getElementById("video").src.trim() === "") return;
+
+ if (document.getElementById("video").paused) {
+ document.getElementById("video").play();
+ } else {
+ document.getElementById("video").pause();
+ }
+ }
+
+ function controls_fullscreen() {
+ if (document.fullscreen) {
+ document.exitFullscreen();
+ } else {
+ document.documentElement.requestFullscreen();
+ }
+ }
+
+ function controls_next() {
+ document.getElementById("video").play();
+ document.getElementById("video").currentTime = document.getElementById("video").duration;
+ }
+
+ window.seeking = false;
+ window.seekPosition = 0;
+
+ document.getElementById("controls-outer").onclick = (event) => {
+ if (event.target === document.getElementById("controls-outer")) controls_playPause();
+ }
+
+ document.getElementById("controls-outer").ondblclick = (event) => {
+ if (event.target === document.getElementById("controls-outer")) controls_fullscreen();
+ }
+
+ document.getElementById("seek-bar").onmouseenter = () => {
+ document.getElementById("seek-bar-dot").style.display = "";
+ console.log("> ENTER <");
+ window.seeking = true;
+ }
+
+ document.getElementById("seek-bar").onclick = (event) => {
+ let percentage = (event.offsetX / document.getElementById("seek-bar").clientWidth) * 100;
+ let multiplier = event.offsetX / document.getElementById("seek-bar").clientWidth;
+
+ document.getElementById("seek-bar-dot").style.left = event.clientX + "px";
+
+ console.log(" CLICK: ", event.offsetX, "(" + percentage.toFixed(3) + "%, " + toPrettyTime(document.getElementById("video").duration * multiplier) + ")");
+ window.seekPosition = document.getElementById("video").duration * multiplier;
+
+ document.getElementById("video").pause();
+ document.getElementById("video").currentTime = window.seekPosition;
+ document.getElementById("video").play();
+ }
+
+ document.getElementById("seek-bar").onmouseleave = () => {
+ document.getElementById("seek-bar-dot").style.display = "none";
+ console.log("< LEAVE >");
+ window.seeking = false;
+ }
+
+ function controls_skipIntroOutro() {
+ let skipper = null;
+
+ for (let segment of window.segments.filter(i => i['category'] === "intro")) {
+ if (segment.segment) {
+ if (document.getElementById("video").currentTime >= segment.segment[0] && document.getElementById("video").currentTime < segment.segment[1]) {
+ skipper = segment.segment[1];
+ }
+ }
+ }
+ for (let segment of window.segments.filter(i => i['category'] === "outro")) {
+ if (segment.segment) {
+ if (document.getElementById("video").currentTime >= segment.segment[0] && document.getElementById("video").currentTime < segment.segment[1]) {
+ skipper = segment.segment[1];
+ }
+ }
+ }
+
+ if (skipper) {
+ document.getElementById("video").pause();
+ document.getElementById("video").currentTime = skipper;
+ document.getElementById("video").play();
+ }
+ }
+
+ document.getElementById("seek-bar").onmousemove = (event) => {
+ let percentage = (event.offsetX / document.getElementById("seek-bar").clientWidth) * 100;
+ let multiplier = event.offsetX / document.getElementById("seek-bar").clientWidth;
+
+ document.getElementById("seek-bar-dot").style.left = event.clientX + "px";
+
+ console.log(" POS: ", event.offsetX, "(" + percentage.toFixed(3) + "%, " + toPrettyTime(document.getElementById("video").duration * multiplier) + ")");
+ window.seekPosition = document.getElementById("video").duration * multiplier;
+ }
+
+ setInterval(() => {
+ if (document.getElementById("video").src.trim() === "") {
+ document.getElementById("controls").style.display = "none";
+ } else {
+ document.getElementById("controls").style.display = "grid";
+ }
+
+ document.getElementById("control-play-icon").src = document.getElementById("video").paused ? "/assets/icons/together/play.svg" : "/assets/icons/together/pause.svg";
+ document.getElementById("control-full-icon").src = document.fullscreen ? "/assets/icons/together/full-off.svg" : "/assets/icons/together/full-on.svg";
+ document.getElementById("control-progress").style.width = ((document.getElementById("video").currentTime / document.getElementById("video").duration) * 100) + "%";
+
+ if (document.fullscreen) {
+ document.body.classList.add("fullscreen");
+ } else {
+ document.body.classList.remove("fullscreen");
+ }
+
+ if (window.seeking) {
+ document.getElementById("time-remaining").classList.add("text-warning");
+ document.getElementById("time-remaining").innerText = toPrettyTime(window.seekPosition);
+ } else {
+ document.getElementById("time-remaining").classList.remove("text-warning");
+
+ if (!isNaN(document.getElementById("video").duration)) {
+ document.getElementById("time-remaining").innerText = "-" + toPrettyTime(Math.round(document.getElementById("video").duration) - Math.round(document.getElementById("video").currentTime));
+ } else {
+ document.getElementById("time-remaining").innerText = "-0:00";
+ }
+ }
+ })
+
+ setInterval(() => {
+ if (!document.getElementById("video").paused) {
+ for (let segment of window.segments.filter(i => i['category'] === "sponsor")) {
+ if (segment.segment) {
+ if (document.getElementById("video").currentTime >= segment.segment[0] && document.getElementById("video").currentTime < segment.segment[1]) {
+ document.getElementById("video").pause();
+ document.getElementById("video").currentTime = segment.segment[1];
+ document.getElementById("video").play();
+ notification("Advert skipped by smart playback", "primary");
+ }
+ }
+ }
+
+ let skipper = false;
+
+ for (let segment of window.segments.filter(i => i['category'] === "intro")) {
+ if (segment.segment) {
+ if (document.getElementById("video").currentTime >= segment.segment[0] && document.getElementById("video").currentTime < segment.segment[1]) {
+ skipper = true;
+ document.getElementById("skipper").innerText = "Skip intro";
+ document.body.classList.add("skipper");
+ }
+ }
+ }
+ for (let segment of window.segments.filter(i => i['category'] === "outro")) {
+ if (segment.segment) {
+ if (document.getElementById("video").currentTime >= segment.segment[0] && document.getElementById("video").currentTime < segment.segment[1]) {
+ skipper = true;
+ document.getElementById("skipper").innerText = "Skip to the end";
+ document.body.classList.add("skipper");
+ }
+ }
+ }
+
+ if (!skipper) {
+ document.body.classList.remove("skipper");
+ }
+ }
+ })
+</script> \ No newline at end of file