Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
1f2ab07de9 | |||
2ee893c369 | |||
1da32f9161 | |||
fecb39b9d0 | |||
dbc75f7eb6 | |||
e1280479cb | |||
cac08f29e3 | |||
933aa3d9df | |||
84ebc30b3f | |||
e39daec704 | |||
bb9bdfb829 | |||
7b724f6ceb | |||
6108d44117 | |||
6c7c049025 | |||
fe8804afcc | |||
ffa19ebb8b | |||
d553b7406f | |||
c0608568b9 | |||
a30fd2424e | |||
2df4e24d04 | |||
b4abb629b7 | |||
27a8dd9466 | |||
bc6093b45d | |||
ad6490b9a8 | |||
ae4331a886 | |||
34c8eaa8c2 | |||
1678e107b3 | |||
75658e249f | |||
3c6c94e866 | |||
69e8ba0154 | |||
9b4124cc40 | |||
2cf710dbed | |||
07ece0891f | |||
d3a98182f8 | |||
c2e1e274b2 | |||
8d4aa1f60e | |||
0d6b5763b0 | |||
bfc7956f8c | |||
e9e798ebae | |||
8e67b6cb32 | |||
42d6db95ea |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
metadata
|
metadata
|
||||||
videos
|
videos
|
||||||
|
channels
|
337
index.js
337
index.js
@ -1,101 +1,278 @@
|
|||||||
var express = require("express");
|
import express from "express";
|
||||||
require("express-async-errors");
|
import "express-async-errors";
|
||||||
var morgan = require("morgan");
|
import { WebSocket, WebSocketServer } from "ws";
|
||||||
var fs = require("fs/promises");
|
import { access, readdir } from "fs/promises";
|
||||||
var getVideoLength = v => require('video-length')(v, {bin: "mediainfo"});
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
var app = module.exports.app = express();
|
var app = express();
|
||||||
app.set("env", process.env.NODE_ENV || "production");
|
app.set("env", process.env.NODE_ENV || "production");
|
||||||
app.set("trust proxy", ["loopback","linklocal","uniquelocal","173.245.48.0/20","103.21.244.0/22","103.22.200.0/22","103.31.4.0/22","141.101.64.0/18","108.162.192.0/18","190.93.240.0/20","188.114.96.0/20","197.234.240.0/22","198.41.128.0/17","162.158.0.0/15","104.16.0.0/13","104.24.0.0/14","172.64.0.0/13","131.0.72.0/22","2400:cb00::/32","2606:4700::/32","2803:f800::/32","2405:b500::/32","2405:8100::/32","2a06:98c0::/29","2c0f:f248::/32"]);
|
app.set("trust proxy", "loopback");
|
||||||
module.exports.server = app.listen(process.env.PORT || 8080, process.env.ADDR);
|
var server = app.listen(process.env.PORT || 9245, process.env.ADDR);
|
||||||
|
|
||||||
app.use(morgan(`:date[iso] :remote-addr :method :url ":req[user-agent]" :referrer`));
|
app.get("/channels", async (req, res) => {
|
||||||
|
res.send(await getChannels());
|
||||||
app.use("/videos/", express.static("videos"));
|
|
||||||
|
|
||||||
app.get("/playlist", async (req, res) => {
|
|
||||||
res.send(await getPlaylist());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/newchannel", async (req, res, next) => {
|
||||||
|
|
||||||
|
switch (req.query.from) {
|
||||||
|
case "search":
|
||||||
|
var {query, number} = req.query;
|
||||||
|
if (!number) number = 500;
|
||||||
|
if (!query || !(number >= 1 && number <= 1000)) return res.sendStatus(400);
|
||||||
|
var url = `ytsearch${number}:${query}`;
|
||||||
|
var channel = `${query.replaceAll(' ','-')}~${number}`;
|
||||||
|
break;
|
||||||
|
case "channel":
|
||||||
|
if (!/^[\w\-.]{3,30}$/.test(req.query.handle)) return res.sendStatus(400);
|
||||||
|
var url = `https://www.youtube.com/@${req.query.handle}/videos`;
|
||||||
|
var channel = `@${req.query.handle}`;
|
||||||
|
break;
|
||||||
|
case "playlist":
|
||||||
|
if (!req.query.playlist) return res.sendStatus(400);
|
||||||
|
var url = `https://www.youtube.com/playlist?list=${req.query.playlist}`;
|
||||||
|
var channel = `playlist:${req.query.playlist}`; //todo get playlist name
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return res.sendStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[.\/]/.test(channel)) return res.sendStatus(400);
|
||||||
|
|
||||||
|
var cp = spawn("python3", ["makechannel.py", url, channel]);
|
||||||
|
cp.stdout.pipe(process.stdout);
|
||||||
|
cp.stderr.pipe(process.stderr);
|
||||||
|
cp.on("close", code => {
|
||||||
|
console.log("python exit code", code);
|
||||||
|
broadcastChannels();
|
||||||
|
res.status(200).type("text").send(channel);
|
||||||
|
});
|
||||||
|
cp.on("error", next);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/:channel", (req, res, next) => {
|
||||||
|
var channel = req.params.channel;
|
||||||
|
access(`channels/${channel}.json`).then(() => {
|
||||||
|
//req.url = "/index.html"; //??
|
||||||
|
res.sendFile("index.html", {root: "public"});
|
||||||
|
}).catch(() => {
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/channels/", express.static("channels"));
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
|
|
||||||
require("./mousie-server");
|
|
||||||
|
|
||||||
|
|
||||||
|
async function getChannels() {
|
||||||
|
var channels = await readdir("channels");
|
||||||
|
channels = channels.map(c => {
|
||||||
var currentPlaylist;
|
c = c.replace(/\.json$/,'');
|
||||||
|
var n = [...wss.clients].reduce((num, ws) => ws.channel == c ? num + 1 : num, 0);
|
||||||
async function getPlaylist() {
|
return [c, n];
|
||||||
if (!currentPlaylist || Date.now() > currentPlaylist.timestamp + currentPlaylist.totalDuration*1000) {
|
});
|
||||||
currentPlaylist = await generatePlaylist();
|
return channels;
|
||||||
}
|
|
||||||
return currentPlaylist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generatePlaylist() {
|
|
||||||
var videofiles = await fs.readdir("videos");
|
|
||||||
|
|
||||||
var videos = [];
|
|
||||||
|
|
||||||
for (let name of videofiles) {
|
|
||||||
let metadata = await getMetadata(name);
|
|
||||||
if (!metadata.duration) {
|
|
||||||
console.warn("missing duration:", name);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
videos.push({
|
|
||||||
name,
|
|
||||||
duration: metadata.duration,
|
|
||||||
source: metadata.source || inferSource(name)
|
var wss = new WebSocketServer({ server, clientTracking: true });
|
||||||
|
|
||||||
|
|
||||||
|
var drawStates = {};
|
||||||
|
var txtLogs = {};
|
||||||
|
|
||||||
|
function clearLines(channel, owner) {
|
||||||
|
if (!drawStates[channel]) return;
|
||||||
|
var newLines = [];
|
||||||
|
for (var line of drawStates[channel]) {
|
||||||
|
if (line.owner != owner) newLines.push(line);
|
||||||
|
}
|
||||||
|
drawStates[channel] = newLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastGlobal(data) {
|
||||||
|
for (var client of wss.clients) {
|
||||||
|
if (client.readyState == WebSocket.OPEN) {
|
||||||
|
client.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastChannels() {
|
||||||
|
getChannels().then(channels => broadcastGlobal(JSON.stringify(["channels", channels])));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
wss.on('connection', ws => {
|
||||||
|
ws.send(new BigUint64Array([BigInt(Date.now())]));
|
||||||
|
|
||||||
|
ws.id = (function newId(){
|
||||||
|
var used_ids = [...wss.clients].map(ws => ws.id);
|
||||||
|
for (var id = 0; id < 256; id++) {
|
||||||
|
if (!used_ids.includes(id)) return id;
|
||||||
|
}
|
||||||
|
ws.close();
|
||||||
|
})();
|
||||||
|
|
||||||
|
function broadcast(data) {
|
||||||
|
if (!ws.channel) return;
|
||||||
|
for (var client of wss.clients) {
|
||||||
|
if (client != ws && client.channel == ws.channel && client.readyState == WebSocket.OPEN) {
|
||||||
|
client.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.on('message', (data, isBinary) => {
|
||||||
|
try {
|
||||||
|
if (isBinary) {
|
||||||
|
switch (data.length) {
|
||||||
|
case 4:
|
||||||
|
var x = data.readInt16BE(0);
|
||||||
|
var y = data.readInt16BE(2);
|
||||||
|
var b = Buffer.alloc(5);
|
||||||
|
b.writeUInt8(ws.id, 0);
|
||||||
|
b.writeInt16BE(x, 1);
|
||||||
|
b.writeInt16BE(y, 3);
|
||||||
|
broadcast(b);
|
||||||
|
if (ws.depressed && ws.m) {
|
||||||
|
drawStates[ws.channel] ||= [];
|
||||||
|
drawStates[ws.channel].push({
|
||||||
|
x1: ws.m.x,
|
||||||
|
y1: ws.m.y,
|
||||||
|
x2: x,
|
||||||
|
y2: y,
|
||||||
|
color: ws.color,
|
||||||
|
owner: ws.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
ws.m = {x, y};
|
||||||
shuffle(videos);
|
break;
|
||||||
|
case 1:
|
||||||
return {
|
var c = data.readUint8(0);
|
||||||
videos,
|
broadcast(Buffer.from([ws.id, c]));
|
||||||
timestamp: Date.now(),
|
switch (c){
|
||||||
totalDuration: videos.reduce((td, v) => td + v.duration, 0)
|
case 1: // click
|
||||||
|
ws.depressed = true;
|
||||||
|
break;
|
||||||
|
case 0: // unclick
|
||||||
|
ws.depressed = false;
|
||||||
|
break;
|
||||||
|
case 2: // right click
|
||||||
|
clearLines(ws.channel, ws.id);
|
||||||
|
break;
|
||||||
|
case 5: // blur
|
||||||
|
ws.away = true;
|
||||||
|
break;
|
||||||
|
case 6: // focus
|
||||||
|
ws.away = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
var color = [data.readUInt8(0), data.readUInt8(1), data.readUInt8(2)];
|
||||||
|
broadcast(new Uint8Array([ws.id, ...color]));
|
||||||
|
ws.color = color;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var p = JSON.parse(data.toString());
|
||||||
|
switch (p[0]) {
|
||||||
|
case "txt":
|
||||||
|
broadcast(JSON.stringify(["txt", ws.id, p[1]]));
|
||||||
|
ws.txt ||= "";
|
||||||
|
txtLogs[ws.channel] ||= [];
|
||||||
|
switch (p[1]) {
|
||||||
|
case "":
|
||||||
|
ws.txt = "";
|
||||||
|
txtLogs[ws.channel].push(["␛", ws.color]);
|
||||||
|
break;
|
||||||
|
case -1:
|
||||||
|
ws.txt = ws.txt.slice(0, -1);
|
||||||
|
txtLogs[ws.channel].push(["⌫", ws.color]);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
ws.txt = ws.txt.slice(1);
|
||||||
|
txtLogs[ws.channel].push(["⌦", ws.color]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ws.txt += p[1];
|
||||||
|
txtLogs[ws.channel].push([p[1] === '\n' ? "⮒" : p[1], ws.color]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "channel":
|
||||||
|
if (ws.channel) broadcast(new Uint8Array([ws.id, 4]));
|
||||||
|
clearLines(ws.channel, ws.id);
|
||||||
|
ws.channel = p[1];
|
||||||
|
broadcastChannels();
|
||||||
|
broadcast(new Uint8Array([ws.id, 3]));
|
||||||
|
if (ws.color) broadcast(new Uint8Array([ws.id, ...ws.color]));
|
||||||
|
if (ws.x && ws.y) broadcast(new Uint8Array([ws.id, ws.x, ws.y]));
|
||||||
|
ws.send(JSON.stringify(["channel", ws.channel]));
|
||||||
|
for (let w of wss.clients) {
|
||||||
|
if (w.id == null || w.id == ws.id || w.channel != ws.channel) continue;
|
||||||
|
if (w.m) {
|
||||||
|
let b = Buffer.alloc(5);
|
||||||
|
b.writeUInt8(w.id, 0);
|
||||||
|
b.writeInt16BE(w.m.x, 1);
|
||||||
|
b.writeInt16BE(w.m.y, 3);
|
||||||
|
ws.send(b);
|
||||||
|
}
|
||||||
|
if (w.txt) {
|
||||||
|
ws.send(JSON.stringify(["txt", w.id, w.txt]));
|
||||||
|
}
|
||||||
|
if (w.color) {
|
||||||
|
ws.send(new Uint8Array([w.id, ...w.color]));
|
||||||
|
}
|
||||||
|
if (w.away) {
|
||||||
|
ws.send(new Uint8Array([w.id, 5]));
|
||||||
|
} else if (w.away === false) {
|
||||||
|
ws.send(new Uint8Array([w.id, 6]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var lines = drawStates[ws.channel];
|
||||||
|
if (lines) {
|
||||||
|
var b = Buffer.alloc(12*lines.length);
|
||||||
|
for (var i = 0, o = 0; i < lines.length; i++, o = i*12) {
|
||||||
|
var line = lines[i];
|
||||||
|
b.writeInt16BE(line.x1, o);
|
||||||
|
b.writeInt16BE(line.y1, o+2);
|
||||||
|
b.writeInt16BE(line.x2, o+4);
|
||||||
|
b.writeInt16BE(line.y2 ,o+6);
|
||||||
|
b.writeUint8(line.owner, o+8);
|
||||||
|
b.writeUint8(line.color[0], o+9);
|
||||||
|
b.writeUint8(line.color[1], o+10);
|
||||||
|
b.writeUint8(line.color[2], o+11);
|
||||||
|
}
|
||||||
|
ws.send(b);
|
||||||
|
}
|
||||||
|
if (txtLogs[ws.channel]) {
|
||||||
|
ws.send(JSON.stringify(["txtlog", txtLogs[ws.channel]]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMetadata(video) {
|
|
||||||
var videopath = `videos/${video}`;
|
|
||||||
var metadatapath = `metadata/${video}.json`;
|
|
||||||
try {
|
|
||||||
var metadata = JSON.parse(await fs.readFile(metadatapath, "utf8"));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
var metadata = {
|
console.error(error.stack);
|
||||||
duration: null
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
metadata.duration = await getVideoLength(videopath);
|
|
||||||
} catch(error) {
|
|
||||||
console.error("get video length:", error.message);
|
|
||||||
};
|
|
||||||
await fs.writeFile(metadatapath, JSON.stringify(metadata));
|
|
||||||
}
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function inferSource(name) {
|
ws.on("close", () => {
|
||||||
var youtube_id = name.match(/\[([A-Za-z0-9_-]{11})\]/);
|
broadcast(new Uint8Array([ws.id, 4]));
|
||||||
if (youtube_id)
|
broadcastChannels();
|
||||||
return `https://www.youtube.com/watch?v=${youtube_id[1]}`;
|
clearLines(ws.channel, ws.id);
|
||||||
|
});
|
||||||
|
|
||||||
var niconico_id = name.match(/\[(sm\d+)\]/);
|
ws.on('error', console.error);
|
||||||
if (niconico_id)
|
});
|
||||||
return `https://www.nicovideo.jp/watch/${niconico_id[1]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function shuffle(array) {
|
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
34
makechannel.py
Normal file
34
makechannel.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from yt_dlp import YoutubeDL
|
||||||
|
from simplejson import dump
|
||||||
|
from sys import argv
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
url = argv[1]
|
||||||
|
channelname = argv[2]
|
||||||
|
filepath = f"channels/{channelname}.json"
|
||||||
|
|
||||||
|
json = {
|
||||||
|
"name": channelname,
|
||||||
|
"url": url,
|
||||||
|
"videos": []
|
||||||
|
}
|
||||||
|
|
||||||
|
with YoutubeDL() as ydl:
|
||||||
|
info = ydl.extract_info(url, download=False, process=False)
|
||||||
|
for video in info["entries"]:
|
||||||
|
if not video.get("duration"):
|
||||||
|
# the videos with missing duration seem to all be youtube shorts
|
||||||
|
print(f"{video['id']} {video['title']} missing duration")
|
||||||
|
continue
|
||||||
|
json["videos"].append({
|
||||||
|
"id": video["id"],
|
||||||
|
"title": video["title"],
|
||||||
|
"duration": video["duration"]
|
||||||
|
})
|
||||||
|
|
||||||
|
json["timestamp"] = int(time() * 1000)
|
||||||
|
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
dump(json, f)
|
||||||
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
|||||||
var {WebSocket, WebSocketServer} = require("ws");
|
|
||||||
var {server} = require("./index.js");
|
|
||||||
|
|
||||||
var wss = new WebSocketServer({ server, clientTracking: true });
|
|
||||||
|
|
||||||
wss.on('connection', ws => {
|
|
||||||
for (let w of wss.clients) {
|
|
||||||
if (w.id == null) continue;
|
|
||||||
if (w.m) {
|
|
||||||
let b = Buffer.alloc(5);
|
|
||||||
b.writeUInt8(w.id, 0);
|
|
||||||
b.writeInt16BE(w.m.x, 1);
|
|
||||||
b.writeInt16BE(w.m.y, 3);
|
|
||||||
ws.send(b);
|
|
||||||
}
|
|
||||||
if (w.txt) {
|
|
||||||
ws.send(JSON.stringify(["type", w.id, w.txt]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.id = (function newId(){
|
|
||||||
var used_ids = [...wss.clients].map(ws => ws.id);
|
|
||||||
for (var id = 0; id < 256; id++) {
|
|
||||||
if (!used_ids.includes(id)) return id;
|
|
||||||
}
|
|
||||||
ws.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
function broadcast(data) {
|
|
||||||
if (typeof data == "object" && !Buffer.isBuffer(data)) data = JSON.stringify(data);
|
|
||||||
wss.clients.forEach(client => {
|
|
||||||
if (client != ws && client.readyState == WebSocket.OPEN) {
|
|
||||||
client.send(data, { binary: typeof data != "string" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast(["join", ws.id]);
|
|
||||||
ws.on("close", () => {
|
|
||||||
broadcast(["gone", ws.id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on('message', (data, isBinary) => {
|
|
||||||
try {
|
|
||||||
if (isBinary) {
|
|
||||||
if (data.length == 1) { // click
|
|
||||||
broadcast(Buffer.from([ws.id, data.readUint8(0)]));
|
|
||||||
} else { // move
|
|
||||||
var x = data.readInt16BE(0);
|
|
||||||
var y = data.readInt16BE(2);
|
|
||||||
var b = Buffer.alloc(5);
|
|
||||||
b.writeUInt8(ws.id, 0);
|
|
||||||
b.writeInt16BE(x, 1);
|
|
||||||
b.writeInt16BE(y, 3);
|
|
||||||
broadcast(b);
|
|
||||||
ws.m = {x, y};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var txt = data.toString();
|
|
||||||
broadcast(["type", ws.id, txt]);
|
|
||||||
if (txt == "") {
|
|
||||||
ws.txt = "";
|
|
||||||
} else if (txt == "Backspace") {
|
|
||||||
ws.txt = ws.txt.slice(0, -1);
|
|
||||||
} else if (txt == "Delete") {
|
|
||||||
ws.txt = ws.txt.slice(1);
|
|
||||||
} else {
|
|
||||||
ws.txt ||= "";
|
|
||||||
ws.txt += txt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ws.on('error', console.error);
|
|
||||||
});
|
|
104
package-lock.json
generated
104
package-lock.json
generated
@ -7,8 +7,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"video-length": "^2.0.6",
|
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -29,22 +27,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
"node_modules/basic-auth": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "5.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/basic-auth/node_modules/safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
|
||||||
},
|
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||||
@ -392,32 +374,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/morgan": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"basic-auth": "~2.0.1",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "~2.0.0",
|
|
||||||
"on-finished": "~2.3.0",
|
|
||||||
"on-headers": "~1.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/morgan/node_modules/on-finished": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
|
|
||||||
"dependencies": {
|
|
||||||
"ee-first": "1.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
@ -450,14 +406,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/on-headers": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@ -655,11 +603,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/video-length": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/video-length/-/video-length-2.0.6.tgz",
|
|
||||||
"integrity": "sha512-vPh0n39/3q2Gdzv93w745bjfNg+DFvhpKCr0GH2ZVYuPCXXYUGmy0fgmJOpk9btiZmAZsBrX/xhwb2tmyEoyBw=="
|
|
||||||
},
|
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.14.2",
|
"version": "8.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
||||||
@ -696,21 +639,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
"basic-auth": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
|
||||||
"requires": {
|
|
||||||
"safe-buffer": "5.1.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||||
@ -967,28 +895,6 @@
|
|||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"morgan": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
|
|
||||||
"requires": {
|
|
||||||
"basic-auth": "~2.0.1",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "~2.0.0",
|
|
||||||
"on-finished": "~2.3.0",
|
|
||||||
"on-headers": "~1.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"on-finished": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
|
|
||||||
"requires": {
|
|
||||||
"ee-first": "1.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
@ -1012,11 +918,6 @@
|
|||||||
"ee-first": "1.1.1"
|
"ee-first": "1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"on-headers": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
|
|
||||||
},
|
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@ -1157,11 +1058,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
||||||
},
|
},
|
||||||
"video-length": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/video-length/-/video-length-2.0.6.tgz",
|
|
||||||
"integrity": "sha512-vPh0n39/3q2Gdzv93w745bjfNg+DFvhpKCr0GH2ZVYuPCXXYUGmy0fgmJOpk9btiZmAZsBrX/xhwb2tmyEoyBw=="
|
|
||||||
},
|
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.14.2",
|
"version": "8.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"video-length": "^2.0.6",
|
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
}
|
},
|
||||||
|
"type": "module"
|
||||||
}
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 B |
@ -1,30 +1,54 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html onclick="videoplayer.muted = false">
|
<html onclick="player.unMute()">
|
||||||
<head>
|
<head>
|
||||||
<title>Non-Stop MMD</title>
|
<title>Non-stop MMD</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Symbols+2&display=swap" rel="stylesheet" />
|
<meta property="og:title" content="Non-stop MMD" />
|
||||||
|
<meta name="description" property="og:description" content="Real-time synchronized live multiplayer video entertainment system" />
|
||||||
|
<meta name="theme-color" content="#00FFFF" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+Symbols+2|Bevan&display=block" rel="stylesheet" />
|
||||||
|
<script src="https://www.youtube.com/iframe_api"></script>
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
cursor: none;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
#videoplayer {
|
body {
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
max-height: 100%;
|
height: 100%;
|
||||||
transform: translate(-50%, -50%);
|
display: grid;
|
||||||
position: absolute;
|
place-items: center;
|
||||||
top: 50%;
|
align-content: center;
|
||||||
left: 50%;
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#ytplayer {
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#mousie_canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: none;
|
||||||
}
|
}
|
||||||
.mousie {
|
.mousie {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: gray;
|
color: gray;
|
||||||
text-shadow: 1px 1px 1px black, -1px -1px 1px black, 1px -1px 1px black, -1px 1px 1px black;
|
text-shadow: 1px 1px 1px black, -1px -1px 1px black, 1px -1px 1px black, -1px 1px 1px black;
|
||||||
|
cursor: none;
|
||||||
}
|
}
|
||||||
.mousie_pointer {
|
.mousie_pointer {
|
||||||
font-family: "Noto Sans Symbols 2";
|
font-family: "Noto Sans Symbols 2";
|
||||||
}
|
}
|
||||||
|
.blur {
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
.mousie_talk {
|
.mousie_talk {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
@ -34,111 +58,482 @@
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
.depressed {
|
.depressed {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
#logo {
|
||||||
|
position: fixed;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 0px;
|
||||||
|
font-size: 24pt;
|
||||||
|
font-family: Bevan;
|
||||||
|
color: rgba(127, 127, 127, 0.5)
|
||||||
|
}
|
||||||
|
.modalbg {
|
||||||
|
position: fixed;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(127,127,127,0.33);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background-image: linear-gradient(0deg, #001f4b, #004fc0);
|
||||||
|
color: white;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-color: black;
|
||||||
|
border-width: 3px;
|
||||||
|
border-style: solid;
|
||||||
|
box-shadow: 0px 8px 20px 0px black;
|
||||||
|
font-family: Verdana, sans-serif;
|
||||||
|
text-shadow: 2px 2px 8px black;
|
||||||
|
}
|
||||||
|
.modal > :first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.modal > :last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
.gone {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: visibility 0.1s linear, opacity 0.1s linear;
|
||||||
|
}
|
||||||
|
#channel_select {
|
||||||
|
position: fixed;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#volume_slider {
|
||||||
|
position: fixed;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
appearance: slider-vertical;
|
||||||
|
width: 10px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
#nowplaying_buttom {
|
||||||
|
position: fixed;
|
||||||
|
left: 5px;
|
||||||
|
top: 5px;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.33;
|
||||||
|
}
|
||||||
|
#nowplaying_button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.radiocontent {
|
||||||
|
display: none;
|
||||||
|
text-indent: 37px;
|
||||||
|
}
|
||||||
|
input[type="radio"]:checked ~ .radiocontent {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ncform div {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#txtlog_panel {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 5px 2px #0000009c;
|
||||||
|
background-color: #0000009c;
|
||||||
|
border-color: white;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
|
||||||
|
bottom: 0px;
|
||||||
|
right: 18vw;
|
||||||
|
width: 403px;
|
||||||
|
height: 280px;
|
||||||
|
|
||||||
|
transition: bottom 1s cubic-bezier(0.02, 0.99, 0.58, 1);
|
||||||
|
}
|
||||||
|
.txtlog_closed {
|
||||||
|
bottom: -285px !important;
|
||||||
|
transition: bottom 1s cubic-bezier(0.02, 0.99, 0.58, 1);
|
||||||
|
}
|
||||||
|
#txtlog_handle {
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: -25px;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.5;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
border: 1px white solid;
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding-right: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
|
||||||
|
}
|
||||||
|
#txtlog {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: break-spaces;
|
||||||
|
font-family: monospace;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
height: 255px;
|
||||||
|
overflow: auto;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
#txtlog span {
|
||||||
|
color: white;
|
||||||
|
text-shadow: 1px 1px 1px black, -1px -1px 1px black, 1px -1px 1px black, -1px 1px 1px black;
|
||||||
|
overflow-anchor: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<video id="videoplayer"></video>
|
<div id="ytplayer"></div>
|
||||||
|
<div id="logo">NonstopMMD.com</div>
|
||||||
|
<canvas id="mousie_canvas"></canvas>
|
||||||
<template id="mousie_template">
|
<template id="mousie_template">
|
||||||
<div class="mousie">
|
<div class="mousie">
|
||||||
<div class="mousie_pointer">🮰</div>
|
<div class="mousie_pointer">🮰</div>
|
||||||
<div class="mousie_talk"></div>
|
<div class="mousie_talk"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<button id="nowplaying_buttom" onclick="nowPlaying().then(v => open('https://www.youtube.com/watch?v='+v.video.id+'&t='+Math.floor(v.position), '_blank', 'noopener'))">↗</button>
|
||||||
|
<input type="range" id="volume_slider" min="0" max="100" value="100" oninput="player.setVolume(this.value)" onmouseover="mousie_self.style.visibility='hidden'" onmouseout="mousie_self.style.visibility='visible'" orient="vertical" />
|
||||||
|
<select id="channel_select" onchange="if (this.value == 'newchannel') {channel_select.value = gChannel; newChannelModal()} else changeChannel(this.value)">
|
||||||
|
<option value="newchannel" id="optionnewchannel">+ New Channel</option>
|
||||||
|
</select>
|
||||||
|
<div id="txtlog_panel" class="txtlog_closed" onclick="event.stopPropagation()" onmousedown="event.stopPropagation()" oncontextmenu="event.stopPropagation()"><!--TODO put event handlers on canvas so dont need stopPropagation; why don't they work???!-->
|
||||||
|
<div id="txtlog_handle" onclick="txtlog_panel.classList[txtlog_panel.classList.contains('txtlog_closed') ? 'remove' : 'add']('txtlog_closed')">
|
||||||
|
<span>log</span>
|
||||||
|
</div>
|
||||||
|
<div id="txtlog" onwheel="event.stopPropagation()"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modalbg" id="welcome_modal_bg" onclick="this.classList.add('gone')">
|
||||||
|
<div class="modal" id="welcome_modal">
|
||||||
|
<h1 style="text-align: center">Welcome to Non-stop MMD!</h1>
|
||||||
|
<ul>
|
||||||
|
<li><b>Scroll</b> to adjust <b>volume</b></li>
|
||||||
|
<li><b>Type</b> to <b>talk</b></li>
|
||||||
|
<li><b>Esc</b> to <b>clear text</b></li>
|
||||||
|
<li><b>Click</b> to <b>dance</b></li>
|
||||||
|
<li><b>Drag</b> to <b>draw</b></li>
|
||||||
|
<li><b>Right-click</b> to <b>clear lines</b></li>
|
||||||
|
<li><b>Middle-click</b> to <b>sync</b></li>
|
||||||
|
</ul>
|
||||||
|
<p>Don't like what you see? <b>Change</b> the <b>channel</b> in the upper right corner.</p>
|
||||||
|
<p style="text-align: center">Click anywhere to unmute and continue</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modalbg gone" id="newchannel_modalbg" onclick="newchannel_modalbg.classList.add('gone');">
|
||||||
|
<div class="modal" id="newchannel_modal" onclick="event.stopPropagation()">
|
||||||
|
<h1>Create new channel</h1>
|
||||||
|
<div class="ncform">
|
||||||
|
<!--<div>
|
||||||
|
<label for="ncname">Channel name:</label>
|
||||||
|
<input type="text" placeholder="foo bar channel" />
|
||||||
|
</div>-->
|
||||||
|
<div>
|
||||||
|
<input type="radio" name="ncradio" id="ncradio1" value="search" />
|
||||||
|
<label for="ncr1">From YouTube search</label>
|
||||||
|
<div class="radiocontent">
|
||||||
|
<div>
|
||||||
|
<label>Query:</label>
|
||||||
|
<input type="text" id="ncsq" placeholder="funny cat videos" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Number of videos:</label>
|
||||||
|
<input type="number" id="ncsn" min="1" max="1000" defaultValue="500" placeHolder="500" value="500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" name="ncradio" id="ncradio2" value="channel" />
|
||||||
|
<label for="ncr2">From YouTube channel</label>
|
||||||
|
<div class="radiocontent">
|
||||||
|
Channel handle: @<input type="text" id="nchandle" placeholder="googoo888" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" name="ncradio" id="ncradio3" value="playlist" />
|
||||||
|
<label for="ncr2">From YouTube playlist</label>
|
||||||
|
<div class="radiocontent">
|
||||||
|
Playlist URL: <input type="text" id="ncplaylist" placeholder="https://www.youtube.com/playlist?list=PLZof6GKTlRzPBbRFSIRveZsRWIreTzalP" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" id="ncsubmit" />
|
||||||
|
<div id="ncerror" style="color: red"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
(onload = onresize = () => {
|
||||||
|
ytplayer.style.width = Math.min(innerWidth, 16/9 * innerHeight) + "px";
|
||||||
|
ytplayer.style.height = Math.min(innerHeight, 9/16 * innerWidth) + "px";
|
||||||
|
mousie_canvas.width = innerWidth * devicePixelRatio;
|
||||||
|
mousie_canvas.height = innerHeight * devicePixelRatio;
|
||||||
|
})();
|
||||||
|
|
||||||
var playlist;
|
var gChannel = decodeURIComponent(location.pathname.replace('/','')) || "mmd~500";
|
||||||
|
|
||||||
async function nowPlaying() {
|
|
||||||
if (!playlist || Date.now() > playlist.timestamp + playlist.totalDuration*1000) {
|
var serverTimeOffset = 0; // negative = client > server
|
||||||
playlist = await fetch("playlist").then(res => res.json());
|
function now() {
|
||||||
|
return Date.now() + serverTimeOffset;
|
||||||
}
|
}
|
||||||
for (var i = 0, pastDurations = 0; i < playlist.videos.length; i++) {
|
|
||||||
var video = playlist.videos[i];
|
|
||||||
var videoDurationMs = video.duration * 1000;
|
var player;
|
||||||
if (Date.now() < playlist.timestamp + pastDurations + videoDurationMs) {
|
|
||||||
break;
|
function initializePlayer(id, position) {
|
||||||
|
initializePlayer = () => console.warn("player already initialized");
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
player = new YT.Player('ytplayer', {
|
||||||
|
videoId: id,
|
||||||
|
width: "1920",
|
||||||
|
height: "1080",
|
||||||
|
playerVars: {
|
||||||
|
'playsinline': 1,
|
||||||
|
"autoplay": 1,
|
||||||
|
"controls": 0,
|
||||||
|
"disablekb": 1,
|
||||||
|
"start": position
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
'onReady': () => {
|
||||||
|
console.log("player ready");
|
||||||
|
player.mute();
|
||||||
|
player.seekTo(position);
|
||||||
|
player.playVideo();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
'onStateChange': event => {
|
||||||
|
console.debug("ytplayerstate", Object.entries(YT.PlayerState).find(x=>x[1]==event.data)?.[0] || event.data);
|
||||||
|
if (event.data == YT.PlayerState.ENDED) {
|
||||||
|
sync();
|
||||||
|
// race condition
|
||||||
|
setTimeout(sync, 1000);
|
||||||
}
|
}
|
||||||
pastDurations += videoDurationMs;
|
|
||||||
}
|
}
|
||||||
return {
|
}
|
||||||
name: video.name,
|
});
|
||||||
position: (Date.now() - pastDurations - playlist.timestamp) / 1000,
|
});
|
||||||
source: video.source
|
}
|
||||||
};
|
|
||||||
|
function onYouTubeIframeAPIReady() {
|
||||||
|
sync();
|
||||||
|
syncInterval = setInterval(sync, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sync(force) {
|
async function sync(force) {
|
||||||
var {name, position} = await nowPlaying();
|
var {video, position} = await nowPlaying();
|
||||||
var src = `videos/${encodeURIComponent(name)}`;
|
|
||||||
|
|
||||||
if (!videoplayer.src.endsWith(src)) {
|
if (!player) {
|
||||||
console.debug("change src", videoplayer.src, "to", src);
|
await initializePlayer(video.id, position);
|
||||||
videoplayer.src = src;
|
|
||||||
videoplayer.load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("desync", (videoplayer.currentTime - position) * 1000, "ms");
|
var currentId = player.getVideoData().video_id;
|
||||||
|
if (currentId != video.id) {
|
||||||
if (force || Math.abs(position - videoplayer.currentTime) >= 1) {
|
console.debug("change video", currentId, video.id);
|
||||||
console.debug("change pos", videoplayer.currentTime, "to", position);
|
player.loadVideoById(video.id, position);
|
||||||
videoplayer.currentTime = position;
|
player.playVideo();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoplayer.paused) try {
|
if ([YT.PlayerState.PAUSED, YT.PlayerState.UNSTARTED].includes(player.getPlayerState())) {
|
||||||
await videoplayer.play();
|
console.debug("play!");
|
||||||
} catch (error) {
|
player.playVideo();
|
||||||
videoplayer.muted = true;
|
|
||||||
await videoplayer.play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sync();
|
var currentPos = player.getCurrentTime();
|
||||||
setInterval(sync, 5000);
|
|
||||||
|
console.log("desync", currentPos, position, (currentPos - position) * 1000 + "ms");
|
||||||
|
|
||||||
|
if (force || Math.abs(currentPos - position) > 1) {
|
||||||
|
console.debug("change position", currentPos, position);
|
||||||
|
player.seekTo(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var playlist;
|
||||||
|
|
||||||
|
async function loadPlaylist() {
|
||||||
|
playlist = await fetch(`channels/${gChannel}.json`).then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nowPlaying() {
|
||||||
|
if (!playlist) await loadPlaylist();
|
||||||
|
|
||||||
|
var totalDurationMs = playlist.videos.reduce((d, v) => d + v.duration, 0) * 1000;
|
||||||
|
var elapsedMs = now() - playlist.timestamp;
|
||||||
|
var repetition = Math.floor(elapsedMs / totalDurationMs);
|
||||||
|
var playlistPosMs = elapsedMs % totalDurationMs;
|
||||||
|
var seed = playlist.timestamp + repetition;
|
||||||
|
|
||||||
|
var videos = seededShuffle(playlist.videos, seed);
|
||||||
|
console.debug("shuffle:", videos.map(v => playlist.videos.indexOf(v)));
|
||||||
|
|
||||||
|
for (var i = 0, pastVideoDurations = 0; i < videos.length; i++) {
|
||||||
|
var video = videos[i];
|
||||||
|
if (!video) continue;
|
||||||
|
var videoDurationMs = video.duration * 1000;
|
||||||
|
if (playlistPosMs < pastVideoDurations + videoDurationMs) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pastVideoDurations += videoDurationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
var np = {
|
||||||
|
video,
|
||||||
|
position: (playlistPosMs - pastVideoDurations) / 1000
|
||||||
|
};
|
||||||
|
console.debug("nowPlaying", np, video);
|
||||||
|
return np;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var gColor;
|
||||||
|
if (localStorage.color) {
|
||||||
|
gColor = JSON.parse(localStorage.color);
|
||||||
|
} else {
|
||||||
|
gColor = [Math.floor(Math.random()*256), Math.floor(Math.random()*256), Math.floor(Math.random()*256)];
|
||||||
|
localStorage.color = JSON.stringify(gColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeColor(r, g, b) {
|
||||||
|
gColor = [r, g, b];
|
||||||
|
var ab = new ArrayBuffer(3);
|
||||||
|
var dv = new DataView(ab);
|
||||||
|
dv.setUint8(0, r);
|
||||||
|
dv.setUint8(1, g);
|
||||||
|
dv.setUint8(2, b);
|
||||||
|
ws.send(ab);
|
||||||
|
updateMousie({color: `rgb(${gColor})`});
|
||||||
|
localStorage.color = JSON.stringify(gColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
videoplayer.addEventListener("ended", () => {
|
|
||||||
console.debug("ended");
|
|
||||||
sync();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script> // mousies
|
|
||||||
var ws;
|
var ws;
|
||||||
(function createWs() {
|
(function createWs() {
|
||||||
ws = new WebSocket(location.href.replace('http','ws'));
|
ws = new WebSocket(location.origin.replace('http','ws'));
|
||||||
ws.binaryType = "arraybuffer";
|
ws.binaryType = "arraybuffer";
|
||||||
|
ws.onopen = () => {
|
||||||
|
ws.send(JSON.stringify(["channel", gChannel]));
|
||||||
|
var b = new ArrayBuffer(3);
|
||||||
|
var dv = new DataView(b);
|
||||||
|
dv.setUint8(0, gColor[0]);
|
||||||
|
dv.setUint8(1, gColor[1]);
|
||||||
|
dv.setUint8(2, gColor[2]);
|
||||||
|
ws.send(b);
|
||||||
|
};
|
||||||
ws.onmessage = evt => {
|
ws.onmessage = evt => {
|
||||||
if (typeof evt.data == "string") {
|
if (typeof evt.data == "string") {
|
||||||
var j = JSON.parse(evt.data);
|
var j = JSON.parse(evt.data);
|
||||||
switch (j[0]) {
|
switch (j[0]) {
|
||||||
case "join":
|
case "txt":
|
||||||
break;
|
|
||||||
case "gone":
|
|
||||||
document.getElementById("mousie"+j[1])?.remove();
|
|
||||||
break;
|
|
||||||
case "type":
|
|
||||||
updateMousie({id: j[1], txt: j[2]});
|
updateMousie({id: j[1], txt: j[2]});
|
||||||
break;
|
break;
|
||||||
|
case "channel":
|
||||||
|
gChannel = j[1];
|
||||||
|
history.pushState({}, "", `/${gChannel}`);
|
||||||
|
playlist = undefined;
|
||||||
|
sync();
|
||||||
|
[...document.getElementsByClassName('mousie')].forEach(m => m.remove());
|
||||||
|
lines = [];
|
||||||
|
renderCanvas();
|
||||||
|
txtlog.innerHTML = "";
|
||||||
|
break;
|
||||||
|
case "channels":
|
||||||
|
setChannels(j[1]);
|
||||||
|
break;
|
||||||
|
case "txtlog":
|
||||||
|
txtlog.innerHTML = "";
|
||||||
|
for (var [txt, color] of j[1]) {
|
||||||
|
appendTxtLog(txt, color);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var dv = new DataView(evt.data);
|
var dv = new DataView(evt.data);
|
||||||
|
switch (dv.byteLength) {
|
||||||
|
case 5: // user move
|
||||||
var id = dv.getUint8(0);
|
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 x = dv.getInt16(1, false);
|
||||||
var y = dv.getInt16(3, false);
|
var y = dv.getInt16(3, false);
|
||||||
updateMousie({id, x, y});
|
updateMousie({id, x, y});
|
||||||
} else {
|
break;
|
||||||
|
case 2: // user events
|
||||||
|
var id = dv.getUint8(0);
|
||||||
|
switch (dv.getUint8(1)) {
|
||||||
|
case 1: // click
|
||||||
|
updateMousie({id, depressed: true});
|
||||||
|
break;
|
||||||
|
case 0: // unclick
|
||||||
|
updateMousie({id, depressed: false});
|
||||||
|
break;
|
||||||
|
case 2: // right click
|
||||||
|
clearLines(id);
|
||||||
|
break;
|
||||||
|
case 3: // join
|
||||||
|
break;
|
||||||
|
case 4: // leave
|
||||||
|
document.getElementById("mousie"+id)?.remove();
|
||||||
|
clearLines(id);
|
||||||
|
break;
|
||||||
|
case 5: // blur
|
||||||
|
updateMousie({id, blur: true});
|
||||||
|
break;
|
||||||
|
case 6: // focus
|
||||||
|
updateMousie({id, blur: false});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4: // user color
|
||||||
|
var id = dv.getUint8(0);
|
||||||
|
var rgb = [dv.getUint8(1), dv.getUint8(2), dv.getUint8(3)];
|
||||||
|
updateMousie({id, color: `rgb(${rgb})`});
|
||||||
|
break;
|
||||||
|
case 8: // server time
|
||||||
|
var serverTime = Number(dv.getBigUint64(0, true));
|
||||||
|
serverTimeOffset = serverTime - Date.now();
|
||||||
|
console.log("server time", serverTime, "client time", Date.now(), "offset", serverTimeOffset);
|
||||||
|
default: // canvas state
|
||||||
|
for (var o = 0; o + 12 < dv.byteLength; o += 12) {
|
||||||
|
mkline(
|
||||||
|
dv.getInt16(o),
|
||||||
|
dv.getInt16(o+2),
|
||||||
|
dv.getInt16(o+4),
|
||||||
|
dv.getInt16(o+6),
|
||||||
|
dv.getUint8(o+8),
|
||||||
|
`rgb(${[
|
||||||
|
dv.getUint8(o+9),
|
||||||
|
dv.getUint8(o+10),
|
||||||
|
dv.getUint8(o+11)
|
||||||
|
]})`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -150,51 +545,105 @@
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
onmousemove = evt => {
|
onmousemove = evt => {
|
||||||
if (ws.readyState != WebSocket.OPEN) return;
|
var {left, top, width, height} = ytplayer.getBoundingClientRect();
|
||||||
var {left, top, width, height} = videoplayer.getBoundingClientRect();
|
|
||||||
var x = (4096 * evt.pageX / width) - (4096 * left / width);
|
var x = (4096 * evt.pageX / width) - (4096 * left / width);
|
||||||
var y = (4096 * evt.pageY / height) - (4096 * top / height);
|
var y = (4096 * evt.pageY / height) - (4096 * top / height);
|
||||||
|
updateMousie({x, y});
|
||||||
var b = new ArrayBuffer(4);
|
var b = new ArrayBuffer(4);
|
||||||
var dv = new DataView(b);
|
var dv = new DataView(b);
|
||||||
dv.setInt16(0, x, false);
|
dv.setInt16(0, x, false);
|
||||||
dv.setInt16(2, y, false);
|
dv.setInt16(2, y, false);
|
||||||
ws.send(b);
|
if (ws.readyState == WebSocket.OPEN) ws.send(b);
|
||||||
updateMousie({x, y});
|
|
||||||
};
|
};
|
||||||
|
window.addEventListener("touchmove", evt => {
|
||||||
|
evt.preventDefault()
|
||||||
|
onmousemove(evt);
|
||||||
|
}, {passive: false});
|
||||||
|
|
||||||
function updateMousie({id = "_self", x, y, txt, depressed}) {
|
window.addEventListener("resize", () => {
|
||||||
|
document.querySelectorAll(".mousie").forEach(m => {
|
||||||
|
updateMousie({id: m.id.substring(6), x: m.x, y: m.y});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateMousie({id = "_self", x, y, txt, depressed, blur, color}) {
|
||||||
var mousie_element = document.getElementById("mousie"+id);
|
var mousie_element = document.getElementById("mousie"+id);
|
||||||
if (!mousie_element) {
|
if (!mousie_element) {
|
||||||
mousie_element = mousie_template.content.firstElementChild.cloneNode(true);
|
mousie_element = mousie_template.content.firstElementChild.cloneNode(true);
|
||||||
mousie_element.id = "mousie"+id;
|
mousie_element.id = "mousie"+id;
|
||||||
document.body.appendChild(mousie_element);
|
if (id == "_self") mousie_element.style.color = `rgb(${gColor})`;
|
||||||
|
document.body.insertBefore(mousie_element, mousie_template);
|
||||||
|
}
|
||||||
|
var previous = {
|
||||||
|
x: mousie_element.x,
|
||||||
|
y: mousie_element.y,
|
||||||
|
depressed: mousie_element.querySelector(".mousie_pointer").classList.contains("depressed")
|
||||||
|
};
|
||||||
|
var {left, top, width, height} = ytplayer.getBoundingClientRect();
|
||||||
|
if (x) {
|
||||||
|
mousie_element.x = x;
|
||||||
|
mousie_element.style.left = (x * width / 4096 + left) + "px";
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
mousie_element.y = y;
|
||||||
|
mousie_element.style.top = (y * height / 4096 + top - 3) + "px";
|
||||||
}
|
}
|
||||||
var {left, top, width, height} = videoplayer.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) {
|
if (txt != null) {
|
||||||
var talk = mousie_element.querySelector(".mousie_talk");
|
var talk = mousie_element.querySelector(".mousie_talk");
|
||||||
if (txt === "") talk.innerText = "";
|
switch (txt) {
|
||||||
else if (txt == "Backspace") talk.innerText = talk.innerText.slice(0,-1);
|
case "":
|
||||||
else if (txt == "Delete") talk.innerText = talk.innerText.slice(1);
|
talk.innerText = "";
|
||||||
else talk.innerText += txt;
|
appendTxtLog("␛", mousie_element.style.color);
|
||||||
|
break;
|
||||||
|
case -1:
|
||||||
|
talk.innerText = talk.innerText.slice(0,-1);
|
||||||
|
appendTxtLog("⌫", mousie_element.style.color);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
talk.innerText = talk.innerText.slice(1);
|
||||||
|
appendTxtLog("⌦", mousie_element.style.color);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
talk.innerText += txt;
|
||||||
|
appendTxtLog(txt === '\n' ? "⮒" : txt, mousie_element.style.color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (depressed) {
|
if (depressed) {
|
||||||
mousie_element.querySelector(".mousie_pointer").classList.add("depressed");
|
mousie_element.querySelector(".mousie_pointer").classList.add("depressed");
|
||||||
} else if (depressed === false) {
|
} else if (depressed === false) {
|
||||||
mousie_element.querySelector(".mousie_pointer").classList.remove("depressed");
|
mousie_element.querySelector(".mousie_pointer").classList.remove("depressed");
|
||||||
}
|
}
|
||||||
|
if ((x || y) && previous.depressed && depressed !== false /*&& mousie_element.style.visibility != "hidden"*/) {
|
||||||
|
mkline(previous.x, previous.y, x, y, id, mousie_element.style.color);
|
||||||
|
}
|
||||||
|
if (blur) {
|
||||||
|
mousie_element.querySelector(".mousie_pointer").classList.add("blur");
|
||||||
|
} else if (blur === false) {
|
||||||
|
mousie_element.querySelector(".mousie_pointer").classList.remove("blur");
|
||||||
|
}
|
||||||
|
if (color) {
|
||||||
|
mousie_element.style.color = color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onkeypress = evt => {
|
onkeypress = evt => {
|
||||||
if (evt.key == "Escape") {
|
switch (evt.key) {
|
||||||
|
case "Escape":
|
||||||
var txt = "";
|
var txt = "";
|
||||||
} else if (evt.key == "Enter") {
|
break;
|
||||||
|
case "Enter":
|
||||||
var txt = '\n';
|
var txt = '\n';
|
||||||
} else {
|
break;
|
||||||
|
case "Backspace":
|
||||||
|
var txt = -1;
|
||||||
|
break;
|
||||||
|
case "Delete":
|
||||||
|
var txt = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
var txt = evt.key;
|
var txt = evt.key;
|
||||||
}
|
}
|
||||||
ws.send(txt);
|
ws.send(JSON.stringify(["txt", txt]));
|
||||||
updateMousie({txt});
|
updateMousie({txt});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -205,40 +654,232 @@
|
|||||||
document.documentElement.onpaste = evt => {
|
document.documentElement.onpaste = evt => {
|
||||||
var txt = evt.clipboardData.getData("Text");
|
var txt = evt.clipboardData.getData("Text");
|
||||||
if (txt) {
|
if (txt) {
|
||||||
ws.send(txt);
|
ws.send(JSON.stringify(["txt", txt]));
|
||||||
updateMousie({txt});
|
updateMousie({txt});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
oncontextmenu = evt => {
|
oncontextmenu = evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
nowPlaying().then(v => {
|
|
||||||
if (v.source) open(v.source, "_blank");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onauxclick = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
sync(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onwheel = evt => {
|
onwheel = evt => {
|
||||||
|
var volume = player.getVolume()
|
||||||
if (evt.deltaY > 0) {
|
if (evt.deltaY > 0) {
|
||||||
videoplayer.volume = videoplayer.volume + 0.1 > 1 ? 1 : videoplayer.volume + 0.1;
|
volume += 10;
|
||||||
} else if (evt.deltaY < 0) {
|
} else if (evt.deltaY < 0) {
|
||||||
videoplayer.volume = videoplayer.volume - 0.1 < 0 ? 0 : videoplayer.volume - 0.1;
|
volume -= 10;
|
||||||
}
|
}
|
||||||
|
player.setVolume(volume);
|
||||||
|
volume_slider.value = volume;
|
||||||
};
|
};
|
||||||
|
|
||||||
onmousedown = evt => {
|
onmousedown = ontouchstart = evt => {
|
||||||
|
if (!evt.button || evt.button == 0) {
|
||||||
ws.send(new Uint8Array([1]));
|
ws.send(new Uint8Array([1]));
|
||||||
updateMousie({depressed: true});
|
updateMousie({depressed: true});
|
||||||
|
} else if (evt.button == 1) {
|
||||||
|
evt.preventDefault();
|
||||||
|
sync(true);
|
||||||
|
} else if (evt.button == 2) {
|
||||||
|
ws.send(new Uint8Array([2]));
|
||||||
|
clearLines("_self");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
onmouseup = evt => {
|
onmouseup = ontouchend = evt => {
|
||||||
|
if (evt.button > 0) return;
|
||||||
ws.send(new Uint8Array([0]));
|
ws.send(new Uint8Array([0]));
|
||||||
updateMousie({depressed: false});
|
updateMousie({depressed: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onblur = () => {
|
||||||
|
ws.send(new Uint8Array([5]));
|
||||||
|
updateMousie({blur: true});
|
||||||
|
};
|
||||||
|
onfocus = () => {
|
||||||
|
ws.send(new Uint8Array([6]));
|
||||||
|
updateMousie({blur: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var lines = [];
|
||||||
|
|
||||||
|
function mkline(x1, y1, x2, y2, owner, color) {
|
||||||
|
lines.push({x1, y1, x2, y2, owner, color});;
|
||||||
|
drawline(x1, y1, x2, y2, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawline(x1, y1, x2, y2, color) {
|
||||||
|
var {left, top, width, height} = ytplayer.getBoundingClientRect();
|
||||||
|
x1 = x1 * width / 4096 + left;
|
||||||
|
x2 = x2 * width / 4096 + left;
|
||||||
|
y1 = y1 * height / 4096 + top + 3;
|
||||||
|
y2 = y2 * height / 4096 + top + 3;
|
||||||
|
x1 *= devicePixelRatio;
|
||||||
|
x2 *= devicePixelRatio;
|
||||||
|
y1 *= devicePixelRatio;
|
||||||
|
y2 *= devicePixelRatio;
|
||||||
|
var ctx = mousie_canvas.getContext("2d");
|
||||||
|
ctx.lineWidth = devicePixelRatio;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.lineTo(x2, y2);
|
||||||
|
ctx.strokeStyle = color || "gray";
|
||||||
|
ctx.shadowColor = "black";
|
||||||
|
ctx.shadowBlur = 2;
|
||||||
|
ctx.shadowOffsetX = 1;
|
||||||
|
ctx.shadowOffsetY = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCanvas() {
|
||||||
|
var ctx = mousie_canvas.getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, mousie_canvas.width, mousie_canvas.height);
|
||||||
|
for (var {x1, y1, x2, y2, color} of lines) {
|
||||||
|
drawline(x1, y1, x2, y2, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", renderCanvas);
|
||||||
|
|
||||||
|
function clearLines(owner) {
|
||||||
|
var newLines = [];
|
||||||
|
for (var line of lines) {
|
||||||
|
if (line.owner != owner) newLines.push(line);
|
||||||
|
}
|
||||||
|
if (newLines.length == lines.length) return;
|
||||||
|
lines = newLines;
|
||||||
|
renderCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function changeChannel(newChannel) {
|
||||||
|
ws.send(JSON.stringify(["channel", newChannel]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setChannels(channels) {
|
||||||
|
channels = Object.fromEntries(channels);
|
||||||
|
for (var channel in channels) {
|
||||||
|
var option = channel_select.querySelector("[value=\""+channel+"\"]");
|
||||||
|
if (!option) {
|
||||||
|
option = document.createElement("option");
|
||||||
|
option.value = channel;
|
||||||
|
channel_select.insertBefore(option, optionnewchannel);
|
||||||
|
}
|
||||||
|
option.innerText = `(${channels[channel]}) ${channel}`;
|
||||||
|
if (channel == gChannel) {
|
||||||
|
document.title = `(${channels[channel]}) Non-stop MMD`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var toRemove = [];
|
||||||
|
for (var option of channel_select.children) {
|
||||||
|
if (option.value == "newchannel") continue;
|
||||||
|
if (!(option.value in channels)) toRemove.push(option);
|
||||||
|
}
|
||||||
|
for (var option of toRemove) {
|
||||||
|
option.remove();
|
||||||
|
}
|
||||||
|
channel_select.value = gChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("channels").then(res => res.json()).then(setChannels);
|
||||||
|
|
||||||
|
function newChannelModal() {
|
||||||
|
newchannel_modalbg.classList.remove("gone");
|
||||||
|
}
|
||||||
|
|
||||||
|
ncsubmit.onclick = () => {
|
||||||
|
var selection = document.querySelector(`input[type="radio"]:checked`)?.value;
|
||||||
|
if (!selection) return;
|
||||||
|
|
||||||
|
ncsubmit.disabled = true;
|
||||||
|
ncerror.innerText = "";
|
||||||
|
|
||||||
|
if (selection == "playlist") {
|
||||||
|
var playlistid = ncplaylist.value.match(/playlist\?list=([a-zA-Z0-9-]+)/)?.[1] || ncplaylist.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`newchannel?from=${selection}&query=${encodeURIComponent(ncsq.value)}&number=${ncsn.value}&handle=${encodeURIComponent(nchandle.value)}&playlist=${encodeURIComponent(playlistid)}`, {
|
||||||
|
method: "POST"
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status == 200) {
|
||||||
|
res.text().then(channel => {
|
||||||
|
changeChannel(channel);
|
||||||
|
newchannel_modalbg.classList.add("gone");
|
||||||
|
ncsq.value = "";
|
||||||
|
ncsn.value = "";
|
||||||
|
nchandle.value = "";
|
||||||
|
ncplaylist.value = "";
|
||||||
|
ncsubmit.disabled = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ncerror.innerText = "an error occured";
|
||||||
|
ncsubmit.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function appendTxtLog(txt, color) {
|
||||||
|
if (color instanceof Array) color = `rgb(${color})`;
|
||||||
|
var span = document.createElement("span");
|
||||||
|
span.style.backgroundColor = color;
|
||||||
|
span.style.backgroundColor = span.style.backgroundColor.replace(')', ',0.5)');
|
||||||
|
span.innerText = txt;
|
||||||
|
txtlog.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function seededShuffle(array, seed) {
|
||||||
|
array = [...array];
|
||||||
|
var random = splitmix32(seed);
|
||||||
|
for (var i = array.length - 1; i > 0; i--) {
|
||||||
|
var j = Math.floor(random(seed) * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitmix32(a) {
|
||||||
|
return function() {
|
||||||
|
a |= 0;
|
||||||
|
a = a + 0x9e3779b9 | 0;
|
||||||
|
var t = a ^ a >>> 16;
|
||||||
|
t = Math.imul(t, 0x21f0aaad);
|
||||||
|
t = t ^ t >>> 15;
|
||||||
|
t = Math.imul(t, 0x735a2d97);
|
||||||
|
t = t ^ t >>> 15;
|
||||||
|
return (t >>> 0) / 4294967296;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user