initial commit

This commit is contained in:
2025-12-28 01:52:52 +09:00
commit 6deb4254b7
20 changed files with 1229 additions and 0 deletions

163
.gitignore vendored Normal file
View File

@@ -0,0 +1,163 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,rust
# Edit at https://www.toptal.com/developers/gitignore?templates=node,rust
.direnv
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# End of https://www.toptal.com/developers/gitignore/api/node,rust

1
backend/.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

1
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

10
backend/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2024"
[dependencies]
ffmpeg-next = "8.0.0"
futures-util = "0.3.31"
tokio = { version = "1.48.0", features = ["full"] }
tokio-tungstenite = "0.28.0"

27
backend/flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766651565,
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

27
backend/flake.nix Normal file
View File

@@ -0,0 +1,27 @@
{
description = "Dev shell";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }: {
devShells.x86_64-linux.default =
let
pkgs = import nixpkgs {
system = "x86_64-linux";
};
in pkgs.mkShell {
nativeBuildInputs = with pkgs; [
pkg-config
rustPlatform.bindgenHook
];
buildInputs = with pkgs; [
ffmpeg.dev
];
shellHook = ''
'';
};
};
}

184
backend/src/main.rs Normal file
View File

@@ -0,0 +1,184 @@
use ffmpeg_next as ffmpeg;
use futures_util::{SinkExt, StreamExt};
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::accept_async;
use tokio_tungstenite::tungstenite::Message;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
ffmpeg::init()?;
let addr = "0.0.0.0:8080";
let listener = TcpListener::bind(&addr).await?;
println!("Listening on: {}", addr);
while let Ok((stream, _)) = listener.accept().await {
tokio::spawn(accept_connection(stream));
}
Ok(())
}
async fn accept_connection(stream: TcpStream) {
let addr = stream
.peer_addr()
.expect("connected streams should have a peer addr");
println!("Peer address: {}", addr);
let ws_stream = match accept_async(stream).await {
Ok(ws) => ws,
Err(e) => {
eprintln!("Error during the websocket handshake occurred: {}", e);
return;
}
};
println!("New WebSocket connection: {}", addr);
let (mut write, mut read) = ws_stream.split();
// Channel to communicate between the ffmpeg thread and the websocket task
// We send (data, is_key)
let (tx, mut rx) = tokio::sync::mpsc::channel::<(Vec<u8>, bool)>(100);
// Spawn a blocking thread for FFmpeg processing
std::thread::spawn(move || {
if let Err(e) = process_video(tx) {
eprintln!("FFmpeg processing error: {}", e);
}
});
// Handle incoming messages (mostly to keep connection alive or handle close)
let mut read_task = tokio::spawn(async move {
while let Some(msg) = read.next().await {
match msg {
Ok(m) => {
if m.is_close() {
break;
}
}
Err(_) => break,
}
}
});
// Write loop
let mut write_task = tokio::spawn(async move {
while let Some((data, is_key)) = rx.recv().await {
// Prefix: 0x1 for key frame, 0x0 for delta frame
let mut payload = Vec::with_capacity(data.len() + 1);
payload.push(if is_key { 1 } else { 0 });
payload.extend_from_slice(&data);
let msg = Message::Binary(payload.into());
if let Err(e) = write.send(msg).await {
eprintln!("Error sending message: {}", e);
break;
}
}
// Connection closed or processing done
let _ = write.close().await;
});
tokio::select! {
_ = (&mut read_task) => {
write_task.abort();
},
_ = (&mut write_task) => {
read_task.abort();
}
};
println!("Connection closed: {}", addr);
}
fn process_video(
tx: tokio::sync::mpsc::Sender<(Vec<u8>, bool)>,
) -> Result<(), Box<dyn std::error::Error>> {
let input_file = "video.mp4";
let mut ictx = ffmpeg::format::input(&input_file)?;
let input_stream = ictx
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or(ffmpeg::Error::StreamNotFound)?;
let input_stream_index = input_stream.index();
let decoder_ctx = ffmpeg::codec::context::Context::from_parameters(input_stream.parameters())?;
let mut decoder = decoder_ctx.decoder().video()?;
// Setup Encoder
// Try to find libsvtav1 or fallback to AV1 generic
let codec = ffmpeg::codec::encoder::find_by_name("libsvtav1")
.or_else(|| ffmpeg::codec::encoder::find(ffmpeg::codec::Id::AV1))
.ok_or(ffmpeg::Error::EncoderNotFound)?;
let output_ctx = ffmpeg::codec::context::Context::new();
let mut encoder_builder = output_ctx.encoder().video()?;
// We will scale to YUV420P because it's widely supported and good for streaming
encoder_builder.set_format(ffmpeg::format::Pixel::YUV420P);
encoder_builder.set_width(decoder.width());
encoder_builder.set_height(decoder.height());
encoder_builder.set_time_base(input_stream.time_base());
encoder_builder.set_frame_rate(Some(input_stream.rate()));
let mut encoder = encoder_builder.open_as(codec)?;
// Scaler to convert whatever input to YUV420P
let mut scaler = ffmpeg::software::scaling::context::Context::get(
decoder.format(),
decoder.width(),
decoder.height(),
ffmpeg::format::Pixel::YUV420P,
decoder.width(),
decoder.height(),
ffmpeg::software::scaling::flag::Flags::BILINEAR,
)?;
// Send packet function closure not easy due to ownership, doing inline
for (stream, packet) in ictx.packets() {
if stream.index() == input_stream_index {
decoder.send_packet(&packet)?;
let mut decoded = ffmpeg::util::frame::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
// Scale frame
let mut scaled = ffmpeg::util::frame::Video::empty();
scaler.run(&decoded, &mut scaled)?;
// Set pts for the scaled frame to match decoded
scaled.set_pts(decoded.pts());
// Send to encoder
encoder.send_frame(&scaled)?;
// Receive encoded packets
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
let is_key = encoded.is_key();
let data = encoded.data().ok_or("Empty packet data")?.to_vec();
// Blocking send to the tokio channel
if tx.blocking_send((data, is_key)).is_err() {
return Ok(()); // Receiver dropped
}
}
}
}
}
// Flush encoder
encoder.send_eof()?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
let is_key = encoded.is_key();
let data = encoded.data().ok_or("Empty packet data")?.to_vec();
if tx.blocking_send((data, is_key)).is_err() {
return Ok(());
}
}
Ok(())
}

BIN
backend/video.mp4 Normal file

Binary file not shown.

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

14
frontend/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
Hi
<canvas id="renderer">This browser does not support canvas</canvas>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

23
frontend/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.2.5"
},
"pnpm": {
"overrides": {
"vite": "npm:rolldown-vite@7.2.5"
}
},
"dependencies": {
"@discord/embedded-app-sdk": "^2.4.0"
}
}

555
frontend/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,555 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
vite: npm:rolldown-vite@7.2.5
importers:
.:
dependencies:
'@discord/embedded-app-sdk':
specifier: ^2.4.0
version: 2.4.0
devDependencies:
typescript:
specifier: ~5.9.3
version: 5.9.3
vite:
specifier: npm:rolldown-vite@7.2.5
version: rolldown-vite@7.2.5
packages:
'@discord/embedded-app-sdk@2.4.0':
resolution: {integrity: sha512-kIIS79tuVKvu9YC6GIuvBSfUqNa6511UqafD4i3qGjWSRqVulioYuRzZ+M9D9/KZ2wuu0nQ5IWIYlnh1bsy2tg==}
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
'@napi-rs/wasm-runtime@1.1.0':
resolution: {integrity: sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==}
'@oxc-project/runtime@0.97.0':
resolution: {integrity: sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==}
engines: {node: ^20.19.0 || >=22.12.0}
'@oxc-project/types@0.97.0':
resolution: {integrity: sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==}
'@rolldown/binding-android-arm64@1.0.0-beta.50':
resolution: {integrity: sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-beta.50':
resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-beta.50':
resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-beta.50':
resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-beta.50':
resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/lodash.transform@4.6.9':
resolution: {integrity: sha512-1iIn+l7Vrj8hsr2iZLtxRkcV9AtjTafIyxKO9DX2EEcdOgz3Op5dhwKQFhMJgdfIRbYHBUF+SU97Y6P+zyLXNg==}
'@types/lodash@4.17.21':
resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==}
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.30.2:
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.30.2:
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.30.2:
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.30.2:
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.30.2:
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.30.2:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'}
lodash.transform@4.6.0:
resolution: {integrity: sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
rolldown-vite@7.2.5:
resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
esbuild: ^0.25.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
rolldown@1.0.0-beta.50:
resolution: {integrity: sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
snapshots:
'@discord/embedded-app-sdk@2.4.0':
dependencies:
'@types/lodash.transform': 4.6.9
'@types/uuid': 10.0.0
big-integer: 1.6.52
decimal.js-light: 2.5.1
eventemitter3: 5.0.1
lodash.transform: 4.6.0
uuid: 11.1.0
zod: 3.25.76
'@emnapi/core@1.7.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.7.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
optional: true
'@napi-rs/wasm-runtime@1.1.0':
dependencies:
'@emnapi/core': 1.7.1
'@emnapi/runtime': 1.7.1
'@tybys/wasm-util': 0.10.1
optional: true
'@oxc-project/runtime@0.97.0': {}
'@oxc-project/types@0.97.0': {}
'@rolldown/binding-android-arm64@1.0.0-beta.50':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.50':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.50':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.50':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.50':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.50':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-beta.50':
dependencies:
'@napi-rs/wasm-runtime': 1.1.0
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50':
optional: true
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.50':
optional: true
'@rolldown/pluginutils@1.0.0-beta.50': {}
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
optional: true
'@types/lodash.transform@4.6.9':
dependencies:
'@types/lodash': 4.17.21
'@types/lodash@4.17.21': {}
'@types/uuid@10.0.0': {}
big-integer@1.6.52: {}
decimal.js-light@2.5.1: {}
detect-libc@2.1.2: {}
eventemitter3@5.0.1: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fsevents@2.3.3:
optional: true
lightningcss-android-arm64@1.30.2:
optional: true
lightningcss-darwin-arm64@1.30.2:
optional: true
lightningcss-darwin-x64@1.30.2:
optional: true
lightningcss-freebsd-x64@1.30.2:
optional: true
lightningcss-linux-arm-gnueabihf@1.30.2:
optional: true
lightningcss-linux-arm64-gnu@1.30.2:
optional: true
lightningcss-linux-arm64-musl@1.30.2:
optional: true
lightningcss-linux-x64-gnu@1.30.2:
optional: true
lightningcss-linux-x64-musl@1.30.2:
optional: true
lightningcss-win32-arm64-msvc@1.30.2:
optional: true
lightningcss-win32-x64-msvc@1.30.2:
optional: true
lightningcss@1.30.2:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.30.2
lightningcss-darwin-arm64: 1.30.2
lightningcss-darwin-x64: 1.30.2
lightningcss-freebsd-x64: 1.30.2
lightningcss-linux-arm-gnueabihf: 1.30.2
lightningcss-linux-arm64-gnu: 1.30.2
lightningcss-linux-arm64-musl: 1.30.2
lightningcss-linux-x64-gnu: 1.30.2
lightningcss-linux-x64-musl: 1.30.2
lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2
lodash.transform@4.6.0: {}
nanoid@3.3.11: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rolldown-vite@7.2.5:
dependencies:
'@oxc-project/runtime': 0.97.0
fdir: 6.5.0(picomatch@4.0.3)
lightningcss: 1.30.2
picomatch: 4.0.3
postcss: 8.5.6
rolldown: 1.0.0-beta.50
tinyglobby: 0.2.15
optionalDependencies:
fsevents: 2.3.3
rolldown@1.0.0-beta.50:
dependencies:
'@oxc-project/types': 0.97.0
'@rolldown/pluginutils': 1.0.0-beta.50
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-beta.50
'@rolldown/binding-darwin-arm64': 1.0.0-beta.50
'@rolldown/binding-darwin-x64': 1.0.0-beta.50
'@rolldown/binding-freebsd-x64': 1.0.0-beta.50
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.50
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.50
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.50
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.50
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.50
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.50
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.50
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.50
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50
source-map-js@1.2.1: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tslib@2.8.1:
optional: true
typescript@5.9.3: {}
uuid@11.1.0: {}
zod@3.25.76: {}

1
frontend/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

10
frontend/src/discord.ts Normal file
View File

@@ -0,0 +1,10 @@
import { DiscordSDK } from "@discord/embedded-app-sdk";
export async function setupDiscord() {
const discordClientId = import.meta.env.VITE_DISCORD_CLIENT_ID;
console.log("Discord Client ID:", discordClientId);
const discordSdk = new DiscordSDK(discordClientId);
await discordSdk.ready();
}

21
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,21 @@
import { setupDiscord } from './discord';
import './style.css'
import { createDecoder, createSocket } from './transport';
setupDiscord()
const canvas = document.querySelector<HTMLCanvasElement>('#renderer')!;
const ctx = canvas.getContext('2d')!;
canvas.width = 1920;
canvas.height = 916;
const decoder = createDecoder((frame) => {
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
frame.close();
})
const discordClientId = import.meta.env.VITE_DISCORD_CLIENT_ID;
const addr = `wss://${discordClientId}.discordsays.com/ws`
createSocket(addr, decoder);

96
frontend/src/style.css Normal file
View File

@@ -0,0 +1,96 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #3178c6aa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

40
frontend/src/transport.ts Normal file
View File

@@ -0,0 +1,40 @@
export function createDecoder(f: (frame: VideoFrame) => void): VideoDecoder {
const decoder = new VideoDecoder({
output: f,
error: (e) => { console.error(e); }
})
decoder.configure({
codec: 'av01.0.04M.08',
optimizeForLatency: true
});
return decoder;
}
export function createSocket(addr: string, decoder: VideoDecoder) {
const socket = new WebSocket(addr);
socket.binaryType = 'arraybuffer';
socket.onopen = () => {
console.log("WebSocket connection established");
}
socket.onerror = (error) => {
console.error("WebSocket error:", error);
}
socket.onmessage = (event) => {
const data = new Uint8Array(event.data);
const typeByte = data[0];
const payload = data.slice(1);
const chunk = new EncodedVideoChunk({
type: typeByte === 1 ? "key" : "delta",
timestamp: performance.now(),
data: payload
})
decoder.decode(chunk);
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

26
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

5
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,5 @@
export default {
server: {
allowedHosts: true
}
}