This commit is contained in:
2026-06-21 03:53:06 +03:00
commit f1dd557c5d
147 changed files with 34363 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
<body>
<audio controls autoplay>
<source src="http://212.47.251.184:5902" type="audio/ogg" />
</audio>
</body>
<script>
/*
const videoRec = document.createElement('audio');
videoRec.autoplay = true;
videoRec.controls = true;
videoRec.id = 'audio';
videoRec.type = 'audio/ogg; codec=opus';
videoRec.src = 'http://212.47.251.184:5902';
document.body.appendChild(videoRec);
*/
/*
const ms = new MediaSource();
let queue = [];
let buffSize = 0;
let first = true;
ms.onsourceopen = () => {
console.log('onsourceopen');
ms.addSourceBuffer('audio/mp4; codecs=opus');
const sourceBuffer = ms.sourceBuffers[0];
const updateNext = () => {
if (queue.length) {
console.log('premerge', queue.length, buffSize);
const merged = mergeBuffers();
console.log('merged', merged.map(num => num.toString(16).padStart(2, '0')));
// debugger;
try {
sourceBuffer.appendBuffer(merged);
} catch (e) {
console.log(document.getElementById('audio').error);
}
}
};
sourceBuffer.addEventListener('updateend', updateNext);
const socket = new WebSocket('ws://212.47.251.184:5001');
socket.binaryType = 'arraybuffer';
socket.onmessage = (event) => {
const buffer = new Uint8Array(event.data);
queue.push(buffer);
buffSize += buffer.length;
if (!sourceBuffer.updating) {
updateNext();
}
}
};
ms.onsourceclose = () => {
console.log('onsourceclose');
}
videoRec.src = window.URL.createObjectURL(ms);
function mergeBuffers() {
var merged;
if (queue.length == 1) {
merged = queue[0];
} else {
merged = new Uint8Array(buffSize);
var length = queue.length;
var offset = 0;
for (var i = 0; i < length; i++) {
var curr = queue[i];
if (curr.length <= 0) {
continue;
}
merged.set(curr, offset);
offset += curr.length;
}
}
queue = [];
buffSize = 0;
return merged;
}
*/
//options
// mp4mux with mediasource and websockify
// ogg with TCP->HTTP forwarder and audio src
</script>
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
sudo docker exec -it --user postgres postgres sh -c "pg_dump postgres" > postgres.bak
sudo docker exec -it redis sh -c "cat dump.rdb" > dump.rdb
# on local: scp root@ip:dump.rdb dump.rdb
sudo docker exec -it --user postgres postgres sh -c "psql postgres" < postgres.bak
sudo docker exec -it redis sh -c "cat > dump.rdb" < dump.rdb
+7
View File
@@ -0,0 +1,7 @@
FROM siomiz/chrome
RUN apt-get update
RUN apt-get install -y libasound2 alsa-utils alsa-oss
RUN apt-get install -y pulseaudio
+35
View File
@@ -0,0 +1,35 @@
FROM ubuntu:xenial
RUN apt-get update; apt-get clean
# Add a user for running applications.
RUN useradd apps
RUN mkdir -p /home/apps && chown apps:apps /home/apps
# Install x11vnc.
RUN apt-get install -y x11vnc
# Install xvfb.
RUN apt-get install -y xvfb
# Install fluxbox.
RUN apt-get install -y fluxbox
# Install wget.
RUN apt-get install -y wget
# Install wmctrl.
RUN apt-get install -y wmctrl
RUN apt-get install -y gnupg2
# Set the Chrome repo.
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list
# Install Chrome.
RUN apt-get update && apt-get -y install google-chrome-stable
COPY bootstrap.sh /
CMD '/bootstrap.sh'
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# Based on: http://www.richud.com/wiki/Ubuntu_Fluxbox_GUI_with_x11vnc_and_Xvfb
readonly G_LOG_I='[INFO]'
readonly G_LOG_W='[WARN]'
readonly G_LOG_E='[ERROR]'
main() {
launch_xvfb
launch_window_manager
run_vnc_server
}
launch_xvfb() {
# Set defaults if the user did not specify envs.
export DISPLAY=${XVFB_DISPLAY:-:1}
local screen=${XVFB_SCREEN:-0}
local resolution=${XVFB_RESOLUTION:-1280x1024x24}
local timeout=${XVFB_TIMEOUT:-5}
# Start and wait for either Xvfb to be fully up or we hit the timeout.
Xvfb ${DISPLAY} -screen ${screen} ${resolution} &
local loopCount=0
until xdpyinfo -display ${DISPLAY} > /dev/null 2>&1
do
loopCount=$((loopCount+1))
sleep 1
if [ ${loopCount} -gt ${timeout} ]
then
echo "${G_LOG_E} xvfb failed to start."
exit 1
fi
done
}
launch_window_manager() {
local timeout=${XVFB_TIMEOUT:-5}
# Start and wait for either fluxbox to be fully up or we hit the timeout.
fluxbox &
local loopCount=0
until wmctrl -m > /dev/null 2>&1
do
loopCount=$((loopCount+1))
sleep 1
if [ ${loopCount} -gt ${timeout} ]
then
echo "${G_LOG_E} fluxbox failed to start."
exit 1
fi
done
}
run_vnc_server() {
local passwordArgument='-nopw'
if [ -n "${VNC_SERVER_PASSWORD}" ]
then
local passwordFilePath="${HOME}/x11vnc.pass"
if ! x11vnc -storepasswd "${VNC_SERVER_PASSWORD}" "${passwordFilePath}"
then
echo "${G_LOG_E} Failed to store x11vnc password."
exit 1
fi
passwordArgument=-"-rfbauth ${passwordFilePath}"
echo "${G_LOG_I} The VNC server will ask for a password."
else
echo "${G_LOG_W} The VNC server will NOT ask for a password."
fi
x11vnc -display ${DISPLAY} -forever ${passwordArgument} &
wait $!
}
control_c() {
echo ""
exit
}
#trap control_c SIGINT SIGTERM SIGHUP
main
sleep 1
google-chrome --start-maximized --no-first-run --display ${DISPLAY}
exit
+16
View File
@@ -0,0 +1,16 @@
import http from "node:http";
import net from "node:net";
const port = 5902;
const client = new net.Socket();
const server = http.createServer((req, res) => {
console.log(req.url);
res.setHeader("Content-Type", "audio/ogg");
client.connect(5901, "localhost");
client.pipe(res);
});
server.listen(port, () => {
console.log(`server is listening on ${port}`);
});
+29
View File
@@ -0,0 +1,29 @@
# Enable kernel modules on the host
modprobe snd-aloop
# Install gstreamer
apt-get install -y libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio
# Inside container: ALSA to view sound devices with aplay -l
apt-get install -y libasound2 alsa-utils alsa-oss
apt-get install -y pulseaudio
# Rebuild our container
docker build . -t browseparty/chrome
# Run our services
# Start chrome + VNC, sharing audio device
docker run -d -p 5900:5900 --rm --name chrome --device /dev/snd -e VNC_PASSWORD="password" -e VNC_SCREEN_SIZE="1280x720" -e CHROME_OPTS_OVERRIDE=" --user-data-dir --no-sandbox --no-first-run --window-position=0,0 --force-device-scale-factor=1 --disable-dev-shm-usage" browseparty/chrome
# Proxy the VNC from TCP to WebSocket
docker run -d -it --rm --net=host --name websockify efrecon/websockify 5000 localhost:5900
# Audio test
speaker-test -D hw:0,0,0 --channels 2 --rate 48000
# Stream the audio to a TCP sink
gst-launch-1.0 -v alsasrc device=hw:0,1,0 ! audio/x-raw, channels=2, rate=48000 ! opusenc complexity=0 ! oggmux ! tcpserversink port=5901 host=0.0.0.0
gst-launch-1.0 -v alsasrc device=hw:0,1,0 ! audio/x-raw, channels=2, rate=48000 ! audioconvert ! lamemp3enc ! tcpserversink port=5901 host=0.0.0.0
# Proxy the audio from TCP to WebSocket
docker run -d -it --rm --net=host --name websockify-audio efrecon/websockify 5001 localhost:5901
+11
View File
@@ -0,0 +1,11 @@
{
"CORSRules": [
{
"AllowedOrigins": ["*"],
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE"],
"MaxAgeSeconds": 3000,
"ExposeHeaders": ["Etag"]
}
]
}
+9
View File
@@ -0,0 +1,9 @@
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<ExposeHeader>ETag</ExposeHeader>
</CORSRule>
</CORSConfiguration>
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# install docker
curl -fsSL https://get.docker.com | sh
# coturn
docker run -d --network=host --name=coturn coturn/coturn \
-n --log-file=stdout \
--lt-cred-mech --fingerprint \
--no-multicast-peers --no-cli \
--no-tlsv1 --no-tlsv1_1 \
--realm=watchparty \
# doesn't actually work, manually docker exec bash and run the add
docker exec -it --user nobody coturn "bash -c 'turnadmin --add -u username -p password -r watchparty'"
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
apt-get update
apt-get install -y curl
# install docker
curl -fsSL https://get.docker.com | sh
# pull vbrowser image
docker pull howardc93/vbrowser
# install certbot
DEBIAN_FRONTEND=noninteractive apt-get install -y certbot
certbot certonly --standalone -n --email howardzchung@gmail.com --agree-tos -d docker1.watchparty.me
chmod -R 755 /etc/letsencrypt/live/
chmod -R 755 /etc/letsencrypt/archive/
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
apt update
DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-dns-cloudflare
certbot certonly --standalone -n --email howardzchung@gmail.com --agree-tos -d $(hostname).watchparty.me
#certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/cloudflare.ini -d *.watchparty.me --preferred-challenges dns-01
apt install -y nginx
apt install -y bind9
echo 'events {}
http {
server {
listen 443 ssl;
server_name HOSTNAME_PLACEHOLDER.watchparty.me;
ssl_certificate /etc/letsencrypt/live/HOSTNAME_PLACEHOLDER.watchparty.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/HOSTNAME_PLACEHOLDER.watchparty.me/privkey.pem;
location / {
resolver 127.0.0.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://$arg_ip;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}' | sed "s/HOSTNAME_PLACEHOLDER/$(hostname)/g" > /etc/nginx/nginx.conf
/etc/init.d/nginx reload
# Update /etc/letsencrypt/cli.ini with renew hook
echo '
deploy-hook = systemctl reload nginx' >> /etc/letsencrypt/cli.ini
+8
View File
@@ -0,0 +1,8 @@
import { Redis } from "ioredis";
const redis = new Redis("localhost:6379");
const redis2 = new Redis("localhost:6379");
redis.blpop("ioRedisTest", 0);
redis.disconnect();
await redis2.lpush("ioRedisTest", "val");
console.log(await redis2.lrange("ioRedisTest", 0, -1));
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker run --name watchparty-redis --rm --net=host -d redis
+21
View File
@@ -0,0 +1,21 @@
import fs from "node:fs";
import xml2js from "xml2js";
const xml = ``;
const subPath = "/var/www/html/subtitles/";
const subs = fs.readdirSync(subPath);
let vids = fs.readdirSync("/var/www/html/");
const data = await xml2js.parseStringPromise(xml);
// vids = [...data.ListBucketResult.Contents.filter(file => file.Key[0].startsWith('')).map(file => file.Key[0])];
console.log(subs);
console.log(vids);
vids.forEach((vid) => {
const match = subs.find((sub) =>
vid.toLowerCase().startsWith(sub.slice(0, -4).toLowerCase()),
);
console.log(vid, match);
if (vid && match) {
fs.copyFileSync(subPath + match, subPath + vid + ".srt");
}
});
+29
View File
@@ -0,0 +1,29 @@
import requests
url = "https://discord.com/api/v10/applications/1071394728513380372/commands"
# This is an example CHAT_INPUT or Slash Command, with a type of 1
json = {
"name": "watch",
"type": 1,
"description": "Start a new WatchParty",
"options": [
{
"name": "video",
"description": "The URL to start watching",
"type": 3
}
]
}
# For authorization, you can use either your bot token
headers = {
"Authorization": "Bot <TOKEN>"
}
# or a client credentials token for your app with the applications.commands.update scope
# headers = {
# "Authorization": "Bearer <my_credentials_token>"
# }
r = requests.post(url, headers=headers, json=json)
+96
View File
@@ -0,0 +1,96 @@
<video id="video" autoplay="true"></video>
<canvas id="canvas"></canvas>
<script>
var video = document.getElementById("video");
if (navigator.mediaDevices.getUserMedia) {
var successCallback = function (stream) {
video.srcObject = stream;
var canvas = document.querySelector("canvas");
// Optional frames per second argument.
var stream2 = canvas.captureStream(25);
var recordedChunks = [];
console.log(stream);
var options = { mimeType: "video/webm; codecs=vp9" };
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = handleDataAvailable;
mediaRecorder.start(1000);
async function handleDataAvailable(event) {
console.log(event.data, await event.data.arrayBuffer());
if (event.data.size > 0) {
// recordedChunks.push(event.data);
addVideoChunk({ data: event.data });
}
}
};
var errorCallback = function (error) {
console.log(error);
};
navigator.mediaDevices
.getUserMedia({
audio: false,
video: { facingMode: { ideal: "environment" } }, // prefer rear-facing camera
})
.then(successCallback, errorCallback);
}
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
requestAnimationFrame(renderFrame);
function renderFrame() {
// re-register callback
requestAnimationFrame(renderFrame);
// set internal canvas size to match HTML element size
canvas.width = canvas.scrollWidth;
canvas.height = canvas.scrollHeight;
if (video.readyState === video.HAVE_ENOUGH_DATA) {
// scale and horizontally center the camera image
var videoSize = { width: video.videoWidth, height: video.videoHeight };
var canvasSize = { width: canvas.width, height: canvas.height };
var renderSize = calculateSize(videoSize, canvasSize);
var xOffset = (canvasSize.width - renderSize.width) / 2;
context.drawImage(video, xOffset, 0, renderSize.width, renderSize.height);
}
}
function calculateSize(srcSize, dstSize) {
var srcRatio = srcSize.width / srcSize.height;
var dstRatio = dstSize.width / dstSize.height;
if (dstRatio > srcRatio) {
return {
width: dstSize.height * srcRatio,
height: dstSize.height,
};
} else {
return {
width: dstSize.width,
height: dstSize.width / srcRatio,
};
}
}
const videoRec = document.createElement("video");
const ms = new MediaSource();
videoRec.autoplay = true;
videoRec.width = 320;
videoRec.height = 240;
// console.log(MediaSource.isTypeSupported('video/webm; codecs=vp9'));
ms.onsourceopen = () => {
console.log("onsourceopen");
ms.addSourceBuffer("video/webm; codecs=vp9");
};
ms.onsourceclose = () => {
console.log("onsourceclose");
};
videoRec.src = window.URL.createObjectURL(ms);
document.body.appendChild(videoRec);
async function addVideoChunk(data) {
ms.sourceBuffers[0].appendBuffer(await data.data.arrayBuffer());
}
</script>
+12
View File
@@ -0,0 +1,12 @@
import { resolveShard } from "../server/utils/resolveShard";
import { makeName } from "../server/utils/moniker";
for (let i = 0; i < 100; i++) {
const shard = (i % 2) + 1;
const name = makeName(shard);
const nameShard = resolveShard(name);
console.log(name, shard, nameShard);
if (shard !== nameShard) {
throw new Error("shard mismatch");
}
}
+51
View File
@@ -0,0 +1,51 @@
import axios from "axios";
import fs from "node:fs";
import s3 from "s3-client";
var client = s3.createClient({
maxAsyncS3: 20, // this is the default
s3RetryCount: 3, // this is the default
s3RetryDelay: 1000, // this is the default
multipartUploadThreshold: 2097152000,
multipartUploadSize: 15728640, // this is the default (15 MB)
s3Options: {
accessKeyId: process.env.SCW_ACCESS_KEY,
secretAccessKey: process.env.SCW_SECRET_KEY,
endpoint: "s3.fr-par.scw.cloud",
// endpoint: 's3.yourdomain.com',
// sslEnabled: false
// any other options are passed to new AWS.S3()
// See: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#constructor-property
},
});
var params = {
localDir: "/var/www/html",
deleteRemoved: false, // default false, whether to remove s3 objects that have no corresponding local file.
s3Params: {
Bucket: "hcmedia",
Prefix: "",
},
getS3Params: (localFile, stat, callback) => {
// call callback like this:
var s3Params = {
// if there is no error
Key: localFile.replace(/ /g, "."),
ACL: "public-read",
// other options supported by putObject, except Body and ContentLength.
// See: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property
};
// pass `null` for `s3Params` if you want to skip uploading this file.
callback(null, s3Params);
},
};
var uploader = client.uploadDir(params);
uploader.on("error", function (err) {
console.error("unable to sync:", err.stack);
});
uploader.on("progress", function () {
console.log("progress", uploader.progressAmount, uploader.progressTotal);
});
uploader.on("end", function () {
console.log("done uploading");
});
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# apt-get update
# apt-get install -y curl
# install docker (if not already installed)
# curl -fsSL https://get.docker.com | sh
# install dnsutils (iptables?)
apt-get install -y dnsutils
# disable unattended-upgrades
# apt-get remove -y unattended-upgrades
# determine if this is a large or not via nproc
# This systemd config starts the vbrowser on reboot (or instances created from a snapshot of this init vm)
echo '
#!/bin/bash
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 5000
PASSWORD=$(uuidgen)
#RESOLUTION=$(if [ "$(nproc)" -le "2" ]; then echo "1280x720@30"; else echo "1920x1080@30"; fi)
RESOLUTION="1280x720@30"
ADMIN_KEY={VBROWSER_ADMIN_KEY}
# docker pull howardc93/vbrowser
docker run -d --rm --name=vbrowser --log-opt max-size=1g --net=host --shm-size=1g --cap-add="SYS_ADMIN" -e DISPLAY=":99.0" -e NEKO_SCREEN=$RESOLUTION -e NEKO_PASSWORD=$PASSWORD -e NEKO_PASSWORD_ADMIN=$PASSWORD -e NEKO_ADMIN_KEY=$ADMIN_KEY -e NEKO_BIND=":5000" -e NEKO_EPR=":59000-59100" -e NEKO_H264="1" howardc93/vbrowser
' > /etc/systemd/system/vbrowser.sh
chmod u+x /etc/systemd/system/vbrowser.sh
echo '[Unit]
Description=Start VBrowser
[Service]
Type=oneshot
ExecStart=bash /etc/systemd/system/vbrowser.sh
[Install]
WantedBy=multi-user.target' > /etc/systemd/system/vbrowser.service
systemctl enable vbrowser.service
# This ensures the image is pre-pulled and also creates an endpoint for updateSnapshot to check
docker run -d --rm --name=test --net=host --shm-size=1g --cap-add="SYS_ADMIN" -e DISPLAY=":99.0" -e NEKO_PASSWORD=neko -e NEKO_PASSWORD_ADMIN=admin -e NEKO_BIND=":5000" -e NEKO_EPR=":59000-59100" howardc93/vbrowser
+98
View File
@@ -0,0 +1,98 @@
# Allow ssh, http, https
ufw allow http
ufw allow https
ufw allow ssh
ufw allow 3000
# Enable ufw
ufw enable
# Install nginx/bind9
apt install -y nginx
apt install -y bind9
echo 'worker_rlimit_nofile 4096;
events {
worker_connections 4096;
}
http {
upstream rr {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
# server 127.0.0.1:3003;
}
upstream 1 {
server 127.0.0.1:3001;
}
upstream 2 {
server 127.0.0.1:3002;
}
upstream 3 {
server 127.0.0.1:3003;
}
map $arg_shard $pool {
default "rr";
1 "1";
2 "2";
# 3 "3";
}
server {
listen 80;
location / {
proxy_pass http://$pool;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
# enable WebSockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}' > /etc/nginx/nginx.conf
/etc/init.d/nginx reload
# Install git
apt update
apt install -y git
# Clone application code
git clone https://github.com/howardchung/watchparty
# Install docker
curl -sSL https://get.docker.com/ | sh
# Start Redis
sudo docker run --log-opt max-size=1g -d --name redis --restart=always --net=host redis
# Start Postgres
sudo docker run --log-opt max-size=1g -d --name postgres --restart=always -e POSTGRES_PASSWORD=password --net=host -v $PWD/sql/:/docker-entrypoint-initdb.d/ postgres
# Install NodeJS
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs
# Set up certbot or Cloudflare HTTPS
# Build watchparty
npm run build
# Set .env config
echo '
DATABASE_URL=postgresql://postgres@localhost:5432/postgres?sslmode=disable
REDIS_URL=localhost:6379
' > .env
# Install PM2 globally
npm install -g pm2
# PM2 start
npm run pm2
# Run on startup
./node_modules/pm2/bin/pm2 startup
./node_modules/pm2/bin/pm2 save