2023-10-14 14:36:57 -07:00

287 lines
7.1 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Non-Stop MMD</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Symbols+2&display=swap" rel="stylesheet" />
<script src="https://www.youtube.com/iframe_api"></script>
<style>
html {
cursor: none;
background-color: black;
}
#ytplayer {
display: block;
transform: translate(-50%, -50%);
position: absolute;
top: 50%;
left: 50%;
pointer-events: none;
}
.mousie {
position: fixed;
user-select: none;
color: gray;
text-shadow: 1px 1px 1px black, -1px -1px 1px black, 1px -1px 1px black, -1px 1px 1px black;
}
.mousie_pointer {
font-family: "Noto Sans Symbols 2";
}
.mousie_talk {
position: absolute;
width: 300px;
bottom: 30px;
left: -150px;
text-align: center;
word-wrap: break-word;
white-space: break-spaces;
font-family: monospace;
}
.depressed {
position: relative;
top: 3px;
}
</style>
</head>
<body>
<div id="ytplayer"></div>
<template id="mousie_template">
<div class="mousie">
<div class="mousie_pointer">🮰</div>
<div class="mousie_talk"></div>
</div>
</template>
<script>
// css is retarded
onload = onresize = () => {
ytplayer.width = Math.min(innerWidth, 16/9 * innerHeight);
ytplayer.height = Math.min(innerHeight, 9/16 * innerWidth);
};
var player;
function setVideo(ytid, position) {
if (!player) {
return new Promise(function (resolve, reject) {
console.debug("new player");
player = new YT.Player('ytplayer', {
videoId: ytid,
width: ytplayer.width,
height: ytplayer.height,
playerVars: {
'playsinline': 1,
"autoplay": 1,
"controls": 0,
"disablekb": 1,
"start": position
},
events: {
'onReady': function onPlayerReady(event) {
event.target.playVideo();
player.seekTo(position);
resolve();
},
'onStateChange': function onStateChange(event) {
console.debug("ytstate", event.data);
if (event.data == YT.PlayerState.ENDED) {
sync();
}
}
}
});
});
} else {
console.debug("change video");
player.loadVideoById(ytid, position);
player.playVideo();
}
}
async function onYouTubeIframeAPIReady() {
sync();
setInterval(sync, 5000);
}
async function sync(force) {
var {ytid, position} = await nowPlaying();
if (!player || !player.getVideoUrl || !player.getVideoUrl().includes(ytid)) {
console.debug("setVideo", ytid, position);
await setVideo(ytid, position);
}
if (force || Math.abs(player.getCurrentTime() - position) > 0.5) {
player.seekTo(position);
}
console.log("desync", (player.getCurrentTime() - position) * 1000, "ms");
}
var playlist;
async function nowPlaying() {
if (!playlist || Date.now() > playlist.timestamp + playlist.totalDuration*1000) {
playlist = await fetch("playlist").then(res => res.json());
}
playlist.videos = playlist.videos.filter(v => v.source.includes('youtube'));//tmp
for (var i = 0, pastDurations = 0; i < playlist.videos.length; i++) {
var video = playlist.videos[i];
var videoDurationMs = video.duration * 1000;
if (Date.now() < playlist.timestamp + pastDurations + videoDurationMs) {
break;
}
pastDurations += videoDurationMs;
}
return {
position: (Date.now() - pastDurations - playlist.timestamp) / 1000,
source: video.source,
ytid: video.source.split('?v=')[1]
};
}
</script>
<script> // mousies
var ws;
(function createWs() {
ws = new WebSocket(location.href.replace('http','ws'));
ws.binaryType = "arraybuffer";
ws.onmessage = evt => {
if (typeof evt.data == "string") {
var j = JSON.parse(evt.data);
switch (j[0]) {
case "join":
break;
case "gone":
document.getElementById("mousie"+j[1])?.remove();
break;
case "type":
updateMousie({id: j[1], txt: j[2]});
break;
}
} else {
var dv = new DataView(evt.data);
var id = dv.getUint8(0);
if (dv.byteLength == 2) {
if (dv.getUint8(1)) {
updateMousie({id, depressed: true});
} else {
updateMousie({id, depressed: false});
}
} else if (dv.byteLength == 5) {
var x = dv.getInt16(1, false);
var y = dv.getInt16(3, false);
updateMousie({id, x, y});
} else {
}
}
};
ws.onclose = () => {
setTimeout(createWs, 5000);
playlist = undefined;
[...document.getElementsByClassName('mousie')].forEach(m => m.remove());
};
})();
onmousemove = evt => {
if (ws.readyState != WebSocket.OPEN) return;
var {left, top, width, height} = ytplayer.getBoundingClientRect();
var x = (4096 * evt.pageX / width) - (4096 * left / width);
var y = (4096 * evt.pageY / height) - (4096 * top / height);
var b = new ArrayBuffer(4);
var dv = new DataView(b);
dv.setInt16(0, x, false);
dv.setInt16(2, y, false);
ws.send(b);
updateMousie({x, y});
};
function updateMousie({id = "_self", x, y, txt, depressed}) {
var mousie_element = document.getElementById("mousie"+id);
if (!mousie_element) {
mousie_element = mousie_template.content.firstElementChild.cloneNode(true);
mousie_element.id = "mousie"+id;
document.body.appendChild(mousie_element);
}
var {left, top, width, height} = ytplayer.getBoundingClientRect();
if (x) mousie_element.style.left = (x * width / 4096 + left) + "px";
if (y) mousie_element.style.top = (y * height / 4096 + top - 3) + "px";
if (txt != null) {
var talk = mousie_element.querySelector(".mousie_talk");
if (txt === "") talk.innerText = "";
else if (txt == "Backspace") talk.innerText = talk.innerText.slice(0,-1);
else if (txt == "Delete") talk.innerText = talk.innerText.slice(1);
else talk.innerText += txt;
}
if (depressed) {
mousie_element.querySelector(".mousie_pointer").classList.add("depressed");
} else if (depressed === false) {
mousie_element.querySelector(".mousie_pointer").classList.remove("depressed");
}
}
onkeypress = evt => {
if (evt.key == "Escape") {
var txt = "";
} else if (evt.key == "Enter") {
var txt = '\n';
} else {
var txt = evt.key;
}
ws.send(txt);
updateMousie({txt});
};
onkeydown = evt => {
if (["Backspace", "Delete", "Escape"].includes(evt.key)) onkeypress(evt);
};
document.documentElement.onpaste = evt => {
var txt = evt.clipboardData.getData("Text");
if (txt) {
ws.send(txt);
updateMousie({txt});
}
};
oncontextmenu = evt => {
evt.preventDefault();
nowPlaying().then(v => {
if (v.source) open(v.source, "_blank");
});
};
onauxclick = evt => {
evt.preventDefault();
sync(true);
};
onwheel = evt => {
if (evt.deltaY > 0) {
player.setVolume(player.getVolume() + 10);
} else if (evt.deltaY < 0) {
player.setVolume(player.getVolume() - 10);
}
};
onmousedown = evt => {
ws.send(new Uint8Array([1]));
updateMousie({depressed: true});
};
onmouseup = evt => {
ws.send(new Uint8Array([0]));
updateMousie({depressed: false});
};
</script>
</body>
</html>