generated from ztimson/template
Compare commits
11 Commits
ztimson-pa
...
ztimson-pa
| Author | SHA1 | Date | |
|---|---|---|---|
| d54d318166 | |||
| 4e70c78a7a | |||
| fd6e2b07dc | |||
| 240119ee5f | |||
| 4817089056 | |||
| 169ffad863 | |||
| 4bfecdeacc | |||
| c84008cf5c | |||
| e24ad685fe | |||
| e08bc14634 | |||
| 063a228134 |
@@ -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 on-top of any Raspberry Pi OS + Wayland
|
||||||
|
- 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
|
||||||
@@ -68,8 +70,6 @@ Features include:
|
|||||||
[](https://www.w3.org/Style/CSS/Overview.en.html)
|
[](https://www.w3.org/Style/CSS/Overview.en.html)
|
||||||
[](https://www.raspberrypi.com/)
|
[](https://www.raspberrypi.com/)
|
||||||
[](https://wayland.freedesktop.org/)
|
[](https://wayland.freedesktop.org/)
|
||||||
[](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
|
||||||
[](https://novnc.com/)
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|||||||
28
install.sh
Normal file
28
install.sh
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Fallback AP
|
||||||
|
curl -sL https://git.zakscode.com/ztimson/fallback-ap/raw/branch/master/install.sh | sudo bash
|
||||||
|
|
||||||
|
# VNC
|
||||||
|
sudo raspi-config nonint do_vnc 0
|
||||||
|
|
||||||
|
# Node
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash
|
||||||
|
source ~/.bashrc
|
||||||
|
nvm install stable && nvm use stable
|
||||||
|
|
||||||
|
# Install Homecast
|
||||||
|
cd /tbd
|
||||||
|
git clone https://git.zakscode.com/ztimson/homecast.git .
|
||||||
|
npm i
|
||||||
|
mkdir -p certs
|
||||||
|
openssl req -x509 -newkey rsa:2048 \
|
||||||
|
-keyout certs/key.pem \
|
||||||
|
-out certs/cert.pem \
|
||||||
|
-days 3650 \
|
||||||
|
-nodes \
|
||||||
|
-subj "/CN=homecast"
|
||||||
|
|
||||||
|
# TODO: Create service
|
||||||
17
package.json
Normal file
17
package.json
Normal 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
46
public/index.html
Normal 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
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
129
src/server.mjs
Normal file
129
src/server.mjs
Normal 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}`);
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user