diff --git a/README.md b/README.md
index d32b309..03476ba 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,8 @@ Homecast is a homebrew, Chromecast-style streaming appliance built around a Rasp
The goal is to provide a simple, self-hosted casting and media experience while retaining complete control over the underlying hardware and software.
Features include:
+- Installs ontop of any Rasbian + Wayland OS
+ - Allows you to fully customize and use as your own media center
- Remote desktop into the pi via the browser on any device
- Mirrors to the TV
- Install the PWA via the browser to get the remote control app
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d26aff1
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "homecast",
+ "version": "0.1.0",
+ "description": "The Homebrew Chromecast",
+ "main": "src/server.mjs",
+ "scripts": {
+ "start": "node server.mjs"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "module",
+ "dependencies": {
+ "@novnc/novnc": "^1.7.0",
+ "ws": "^8.21.3"
+ }
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..1210801
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ Remote Desktop
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/logo.png b/public/logo.png
new file mode 100644
index 0000000..ac3c3d6
Binary files /dev/null and b/public/logo.png differ
diff --git a/src/server.mjs b/src/server.mjs
new file mode 100644
index 0000000..ac0c513
--- /dev/null
+++ b/src/server.mjs
@@ -0,0 +1,129 @@
+import http from 'node:http';
+import https from 'node:https';
+import net from 'node:net';
+import fs from 'node:fs';
+import {readFile} from 'node:fs/promises';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+import {WebSocketServer} from 'ws';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const NOVNC = path.join(__dirname, 'node_modules/@novnc/novnc');
+const PORT = process.env['PORT'] || 3000;
+const VNC_HOST = '127.0.0.1';
+const VNC_PORT = 5900;
+
+const MIME_TYPES = {
+ '.html': 'text/html; charset=utf-8',
+ '.js': 'text/javascript',
+ '.css': 'text/css',
+ '.json': 'application/json',
+ '.svg': 'image/svg+xml',
+ '.png': 'image/png',
+ '.ico': 'image/x-icon',
+ '.wasm': 'application/wasm'
+};
+
+const server = https.createServer({
+ key: fs.readFileSync('./certs/key.pem'),
+ cert: fs.readFileSync('./certs/cert.pem')
+}, async (req, res) => {
+ let file;
+
+ if (req.url === '/') file = path.join(__dirname, '../public/index.html');
+ else if (req.url.startsWith('/novnc/')) file = path.join(NOVNC, req.url.slice(7));
+ else {
+ res.writeHead(404);
+ res.end('Not found');
+ return;
+ }
+
+ try {
+ const data = await readFile(file);
+ const type = MIME_TYPES[path.extname(file)] || 'application/octet-stream';
+
+ res.writeHead(200, {'Content-Type': type});
+ res.end(data);
+ } catch (error) {
+ console.error('HTTP error:', error.message);
+ res.writeHead(404);
+ res.end('Not found');
+ }
+});
+
+const wss = new WebSocketServer({
+ server,
+ path: '/websockify',
+ handleProtocols: protocols => {
+ console.log('WebSocket protocols:', [...protocols]);
+
+ if (protocols.has('binary')) return 'binary';
+ return false;
+ }
+});
+
+wss.on('connection', ws => {
+ console.log('VNC client connected');
+
+ const socket = net.createConnection(VNC_PORT, VNC_HOST);
+
+ socket.on('connect', () => {
+ console.log(`Connected to VNC ${VNC_HOST}:${VNC_PORT}`);
+ });
+
+ socket.on('data', data => {
+ console.log(`VNC -> browser: ${data.length} bytes`);
+
+ if (data.length <= 64) {
+ console.log(` hex: ${data.toString('hex')}`);
+ console.log(` text: ${JSON.stringify(data.toString())}`);
+ }
+
+ if (ws.readyState === ws.OPEN) {
+ ws.send(data, {binary: true});
+ }
+ });
+
+ ws.on('message', (data, isBinary) => {
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
+
+ console.log(`Browser -> VNC: ${buffer.length} bytes (${isBinary ? 'binary' : 'text'})`);
+
+ if (buffer.length <= 64) {
+ console.log(` hex: ${buffer.toString('hex')}`);
+ console.log(` text: ${JSON.stringify(buffer.toString())}`);
+ }
+
+ if (socket.writable) {
+ socket.write(buffer);
+ }
+ });
+
+ ws.on('error', error => {
+ console.error('WebSocket error:', error.message);
+ socket.destroy();
+ });
+
+ ws.on('close', (code, reason) => {
+ console.log(`Browser disconnected: ${code}${reason?.length ? ` ${reason}` : ''}`);
+ socket.destroy();
+ });
+
+ socket.on('error', error => {
+ console.error('VNC socket error:', error.message);
+ if (ws.readyState === ws.OPEN) ws.close();
+ });
+
+ socket.on('close', () => {
+ console.log('VNC socket closed');
+ if (ws.readyState === ws.OPEN) ws.close();
+ });
+});
+
+server.on('error', error => {
+ console.error('Server error:', error);
+});
+
+server.listen(PORT, '0.0.0.0', () => {
+ console.log(`https://0.0.0.0:${PORT}`);
+})
\ No newline at end of file