7 Commits

Author SHA1 Message Date
4817089056 Update public/logo.png
All checks were successful
Code review / review (pull_request) Successful in 35s
2026-09-12 22:40:40 -04:00
169ffad863 Upload files to "public" 2026-09-12 22:40:19 -04:00
4bfecdeacc Add public/index.html 2026-09-12 22:39:16 -04:00
c84008cf5c Add src/server.mjs 2026-09-12 22:38:26 -04:00
e24ad685fe Add package.json 2026-09-12 22:36:53 -04:00
e08bc14634 Update README.md 2026-09-12 22:34:58 -04:00
063a228134 Merge pull request 'Update README.md' (#1) from ztimson-patch-1 into master
Reviewed-on: #1
2026-09-12 22:32:41 -04:00
5 changed files with 194 additions and 0 deletions

View File

@@ -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. 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: 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 - Remote desktop into the pi via the browser on any device
- Mirrors to the TV - Mirrors to the TV
- Install the PWA via the browser to get the remote control app - Install the PWA via the browser to get the remote control app

17
package.json Normal file
View File

@@ -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"
}
}

46
public/index.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Remote Desktop</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #000;
}
#screen {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="screen"></div>
<script type="module">
import RFB from '/novnc/core/rfb.js';
const screen = document.querySelector('#screen');
const rfb = new RFB(
screen,
`wss://${location.host}/websockify`
);
rfb.addEventListener('connect', () => console.log('RFB connected'));
rfb.addEventListener('disconnect', event => console.log('RFB disconnected', event.detail));
rfb.scaleViewport = true;
rfb.resizeSession = false;
rfb.showDotCursor = true;
rfb.addEventListener('connect', () => console.log('Connected'));
rfb.addEventListener('disconnect', event => console.log('Disconnected', event.detail));
</script>
</body>
</html>

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

129
src/server.mjs Normal file
View File

@@ -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}`);
})