Add support for recording servers
This commit is contained in:
parent
e7e7d159ae
commit
c87c82702d
57
app/ui.js
57
app/ui.js
|
|
@ -43,6 +43,7 @@ const UI = {
|
||||||
recordingFileHandle: null,
|
recordingFileHandle: null,
|
||||||
recordingWritable: null,
|
recordingWritable: null,
|
||||||
recordingWriteQueue: Promise.resolve(), // Chain writes to ensure order
|
recordingWriteQueue: Promise.resolve(), // Chain writes to ensure order
|
||||||
|
recordingWebSocket: null, // WebSocket for streaming to external server
|
||||||
|
|
||||||
controlbarGrabbed: false,
|
controlbarGrabbed: false,
|
||||||
controlbarDrag: false,
|
controlbarDrag: false,
|
||||||
|
|
@ -209,6 +210,7 @@ const UI = {
|
||||||
UI.initSetting('reconnect', false);
|
UI.initSetting('reconnect', false);
|
||||||
UI.initSetting('reconnect_delay', 5000);
|
UI.initSetting('reconnect_delay', 5000);
|
||||||
UI.initSetting('autorecord', false);
|
UI.initSetting('autorecord', false);
|
||||||
|
UI.initSetting('record_url', ''); // WebSocket URL to stream recording to
|
||||||
},
|
},
|
||||||
// Adds a link to the label elements on the corresponding input elements
|
// Adds a link to the label elements on the corresponding input elements
|
||||||
setupSettingLabels() {
|
setupSettingLabels() {
|
||||||
|
|
@ -1183,6 +1185,16 @@ const UI = {
|
||||||
UI.recording = false;
|
UI.recording = false;
|
||||||
UI.recordingPending = false;
|
UI.recordingPending = false;
|
||||||
|
|
||||||
|
// Close recording WebSocket if streaming to external server
|
||||||
|
if (UI.recordingWebSocket) {
|
||||||
|
try {
|
||||||
|
UI.recordingWebSocket.close();
|
||||||
|
} catch (e) {
|
||||||
|
Log.Error("Error closing recording WebSocket: " + e);
|
||||||
|
}
|
||||||
|
UI.recordingWebSocket = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for pending writes and close the OPFS file
|
// Wait for pending writes and close the OPFS file
|
||||||
if (UI.recordingWritable) {
|
if (UI.recordingWritable) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1395,15 +1407,36 @@ const UI = {
|
||||||
url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
|
url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
|
||||||
}
|
}
|
||||||
|
|
||||||
// If recording is pending, set up OPFS file and wrap WebSocket
|
// If recording is pending, set up recording destination and wrap WebSocket
|
||||||
let OriginalWebSocket = null;
|
let OriginalWebSocket = null;
|
||||||
if (UI.recordingPending) {
|
if (UI.recordingPending) {
|
||||||
try {
|
try {
|
||||||
// Set up OPFS file for recording
|
const recordUrl = UI.getSetting('record_url');
|
||||||
|
|
||||||
|
if (recordUrl) {
|
||||||
|
// Stream recording to external WebSocket server
|
||||||
|
Log.Info("Setting up recording to external server: " + recordUrl);
|
||||||
|
UI.recordingWebSocket = new WebSocket(recordUrl);
|
||||||
|
UI.recordingWebSocket.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
UI.recordingWebSocket.onopen = () => {
|
||||||
|
Log.Info("Recording WebSocket connected");
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
UI.recordingWebSocket.onerror = (e) => {
|
||||||
|
reject(new Error("Failed to connect to recording server"));
|
||||||
|
};
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => reject(new Error("Recording server connection timeout")), 5000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Set up OPFS file for local recording
|
||||||
const root = await navigator.storage.getDirectory();
|
const root = await navigator.storage.getDirectory();
|
||||||
UI.recordingFileHandle = await root.getFileHandle('vnc-recording.bin', { create: true });
|
UI.recordingFileHandle = await root.getFileHandle('vnc-recording.bin', { create: true });
|
||||||
UI.recordingWritable = await UI.recordingFileHandle.createWritable();
|
UI.recordingWritable = await UI.recordingFileHandle.createWritable();
|
||||||
UI.recordingWriteQueue = Promise.resolve();
|
UI.recordingWriteQueue = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
UI.recording = true;
|
UI.recording = true;
|
||||||
UI.recordingPending = false;
|
UI.recordingPending = false;
|
||||||
|
|
@ -1411,10 +1444,10 @@ const UI = {
|
||||||
UI.recordingFrameCount = 0;
|
UI.recordingFrameCount = 0;
|
||||||
UI.recordingBytesWritten = 0;
|
UI.recordingBytesWritten = 0;
|
||||||
|
|
||||||
// Helper to write a frame to OPFS (binary format)
|
// Helper to write a frame (binary format)
|
||||||
// Format: fromClient(1) + timestamp(4) + dataLen(4) + data(dataLen)
|
// Format: fromClient(1) + timestamp(4) + dataLen(4) + data(dataLen)
|
||||||
const writeFrame = (fromClient, timestamp, data) => {
|
const writeFrame = (fromClient, timestamp, data) => {
|
||||||
if (!UI.recording || !UI.recordingWritable) return;
|
if (!UI.recording) return;
|
||||||
|
|
||||||
const header = new Uint8Array(9);
|
const header = new Uint8Array(9);
|
||||||
header[0] = fromClient ? 1 : 0;
|
header[0] = fromClient ? 1 : 0;
|
||||||
|
|
@ -1427,7 +1460,16 @@ const UI = {
|
||||||
header[7] = (data.length >> 8) & 0xff;
|
header[7] = (data.length >> 8) & 0xff;
|
||||||
header[8] = data.length & 0xff;
|
header[8] = data.length & 0xff;
|
||||||
|
|
||||||
// Chain writes to ensure order
|
if (UI.recordingWebSocket && UI.recordingWebSocket.readyState === WebSocket.OPEN) {
|
||||||
|
// Stream to external server
|
||||||
|
const frame = new Uint8Array(9 + data.length);
|
||||||
|
frame.set(header, 0);
|
||||||
|
frame.set(data, 9);
|
||||||
|
UI.recordingWebSocket.send(frame);
|
||||||
|
UI.recordingFrameCount++;
|
||||||
|
UI.recordingBytesWritten += frame.length;
|
||||||
|
} else if (UI.recordingWritable) {
|
||||||
|
// Write to OPFS
|
||||||
UI.recordingWriteQueue = UI.recordingWriteQueue.then(async () => {
|
UI.recordingWriteQueue = UI.recordingWriteQueue.then(async () => {
|
||||||
try {
|
try {
|
||||||
await UI.recordingWritable.write(header);
|
await UI.recordingWritable.write(header);
|
||||||
|
|
@ -1438,6 +1480,7 @@ const UI = {
|
||||||
Log.Error("Error writing frame: " + e);
|
Log.Error("Error writing frame: " + e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap WebSocket constructor temporarily
|
// Wrap WebSocket constructor temporarily
|
||||||
|
|
@ -1484,6 +1527,10 @@ const UI = {
|
||||||
Log.Error("Failed to set up recording: " + e);
|
Log.Error("Failed to set up recording: " + e);
|
||||||
UI.showStatus(_("Recording setup failed: ") + e.message, 'error');
|
UI.showStatus(_("Recording setup failed: ") + e.message, 'error');
|
||||||
UI.recordingPending = false;
|
UI.recordingPending = false;
|
||||||
|
if (UI.recordingWebSocket) {
|
||||||
|
UI.recordingWebSocket.close();
|
||||||
|
UI.recordingWebSocket = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,12 @@ usage() {
|
||||||
echo " "
|
echo " "
|
||||||
echo " --password PASS VNC password (passed to vnc.html URL)"
|
echo " --password PASS VNC password (passed to vnc.html URL)"
|
||||||
echo " --autoconnect auto-connect on page load"
|
echo " --autoconnect auto-connect on page load"
|
||||||
echo " --autorecord auto-record on page load"
|
echo " --autorecord auto-record on page load (local OPFS storage)"
|
||||||
|
echo " "
|
||||||
|
echo " --record-server start recording server for external recording"
|
||||||
|
echo " --record-port PORT recording server port (default: 6090)"
|
||||||
|
echo " --record-output FILE recording output file (default: recording.bin)"
|
||||||
|
echo " --record-js also convert recording to JS format"
|
||||||
echo " "
|
echo " "
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +76,11 @@ FILEONLY_ARG=""
|
||||||
VNC_PASSWORD=""
|
VNC_PASSWORD=""
|
||||||
AUTOCONNECT=""
|
AUTOCONNECT=""
|
||||||
AUTORECORD=""
|
AUTORECORD=""
|
||||||
|
RECORD_SERVER=""
|
||||||
|
RECORD_PORT="6090"
|
||||||
|
RECORD_OUTPUT="recording.bin"
|
||||||
|
RECORD_JS=""
|
||||||
|
recording_server_pid=""
|
||||||
|
|
||||||
|
|
||||||
die() {
|
die() {
|
||||||
|
|
@ -82,6 +92,10 @@ cleanup() {
|
||||||
trap - TERM QUIT INT EXIT
|
trap - TERM QUIT INT EXIT
|
||||||
trap "true" CHLD # Ignore cleanup messages
|
trap "true" CHLD # Ignore cleanup messages
|
||||||
echo
|
echo
|
||||||
|
if [ -n "${recording_server_pid}" ]; then
|
||||||
|
echo "Terminating recording server (${recording_server_pid})"
|
||||||
|
kill ${recording_server_pid} 2>/dev/null
|
||||||
|
fi
|
||||||
if [ -n "${proxy_pid}" ]; then
|
if [ -n "${proxy_pid}" ]; then
|
||||||
echo "Terminating WebSockets proxy (${proxy_pid})"
|
echo "Terminating WebSockets proxy (${proxy_pid})"
|
||||||
kill ${proxy_pid}
|
kill ${proxy_pid}
|
||||||
|
|
@ -112,6 +126,10 @@ while [ "$*" ]; do
|
||||||
--password) VNC_PASSWORD="${OPTARG}"; shift ;;
|
--password) VNC_PASSWORD="${OPTARG}"; shift ;;
|
||||||
--autoconnect) AUTOCONNECT="1" ;;
|
--autoconnect) AUTOCONNECT="1" ;;
|
||||||
--autorecord) AUTORECORD="1" ;;
|
--autorecord) AUTORECORD="1" ;;
|
||||||
|
--record-server) RECORD_SERVER="1" ;;
|
||||||
|
--record-port) RECORD_PORT="${OPTARG}"; shift ;;
|
||||||
|
--record-output) RECORD_OUTPUT="${OPTARG}"; shift ;;
|
||||||
|
--record-js) RECORD_JS="1" ;;
|
||||||
-h|--help) usage ;;
|
-h|--help) usage ;;
|
||||||
-*) usage "Unknown chrooter option: ${param}" ;;
|
-*) usage "Unknown chrooter option: ${param}" ;;
|
||||||
*) break ;;
|
*) break ;;
|
||||||
|
|
@ -216,6 +234,28 @@ WEB=`realpath "${WEB}"`
|
||||||
[ -n "${CERT}" ] && CERT=`realpath "${CERT}"`
|
[ -n "${CERT}" ] && CERT=`realpath "${CERT}"`
|
||||||
[ -n "${KEY}" ] && KEY=`realpath "${KEY}"`
|
[ -n "${KEY}" ] && KEY=`realpath "${KEY}"`
|
||||||
[ -n "${RECORD}" ] && RECORD=`realpath "${RECORD}"`
|
[ -n "${RECORD}" ] && RECORD=`realpath "${RECORD}"`
|
||||||
|
[ -n "${RECORD_OUTPUT}" ] && RECORD_OUTPUT=`realpath "${RECORD_OUTPUT}"`
|
||||||
|
|
||||||
|
# Start recording server if requested
|
||||||
|
if [ -n "${RECORD_SERVER}" ]; then
|
||||||
|
RECORD_SCRIPT="${HERE}/recording_server.py"
|
||||||
|
if [ ! -f "${RECORD_SCRIPT}" ]; then
|
||||||
|
die "Recording server script not found: ${RECORD_SCRIPT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting recording server on port ${RECORD_PORT}..."
|
||||||
|
RECORD_JS_ARG=""
|
||||||
|
[ -n "${RECORD_JS}" ] && RECORD_JS_ARG="--js"
|
||||||
|
python3 "${RECORD_SCRIPT}" --port "${RECORD_PORT}" --output "${RECORD_OUTPUT}" ${RECORD_JS_ARG} &
|
||||||
|
recording_server_pid="$!"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if [ -z "$recording_server_pid" ] || ! ps -eo pid= | grep -w "$recording_server_pid" > /dev/null; then
|
||||||
|
recording_server_pid=
|
||||||
|
die "Failed to start recording server"
|
||||||
|
fi
|
||||||
|
echo "Recording server started (PID: ${recording_server_pid})"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Starting webserver and WebSockets proxy on${HOST:+ host ${HOST}} port ${PORT}"
|
echo "Starting webserver and WebSockets proxy on${HOST:+ host ${HOST}} port ${PORT}"
|
||||||
${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD:+--record ${RECORD}} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} &
|
${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD:+--record ${RECORD}} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} &
|
||||||
|
|
@ -236,6 +276,11 @@ URL_PARAMS="host=${HOST}&port=${PORT}"
|
||||||
[ -n "$VNC_PASSWORD" ] && URL_PARAMS="${URL_PARAMS}&password=${VNC_PASSWORD}"
|
[ -n "$VNC_PASSWORD" ] && URL_PARAMS="${URL_PARAMS}&password=${VNC_PASSWORD}"
|
||||||
[ -n "$AUTOCONNECT" ] && URL_PARAMS="${URL_PARAMS}&autoconnect=1"
|
[ -n "$AUTOCONNECT" ] && URL_PARAMS="${URL_PARAMS}&autoconnect=1"
|
||||||
[ -n "$AUTORECORD" ] && URL_PARAMS="${URL_PARAMS}&autorecord=1"
|
[ -n "$AUTORECORD" ] && URL_PARAMS="${URL_PARAMS}&autorecord=1"
|
||||||
|
# If recording server is running, add record_url (autorecord must also be set for streaming)
|
||||||
|
if [ -n "$RECORD_SERVER" ]; then
|
||||||
|
RECORD_URL="ws://${HOST}:${RECORD_PORT}"
|
||||||
|
URL_PARAMS="${URL_PARAMS}&record_url=${RECORD_URL}&autorecord=1"
|
||||||
|
fi
|
||||||
if [ "x$SSLONLY" == "x" ]; then
|
if [ "x$SSLONLY" == "x" ]; then
|
||||||
echo -e " http://${HOST}:${PORT}/vnc.html?${URL_PARAMS}\n"
|
echo -e " http://${HOST}:${PORT}/vnc.html?${URL_PARAMS}\n"
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
VNC Recording Server
|
||||||
|
|
||||||
|
A WebSocket server that receives VNC recording frames from noVNC
|
||||||
|
and saves them to a file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python recording_server.py [--port PORT] [--output FILE]
|
||||||
|
|
||||||
|
--port PORT Port to listen on (default: 6090)
|
||||||
|
--output FILE Output file path (default: recording.bin)
|
||||||
|
--js Also convert to JS playback format when done
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python recording_server.py --port 6090 --output demo.bin --js
|
||||||
|
|
||||||
|
Then in noVNC, use:
|
||||||
|
vnc.html?host=...&autoconnect=1&autorecord=1&record_url=ws://localhost:6090
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import websockets
|
||||||
|
except ImportError:
|
||||||
|
print("Error: websockets library not found. Install with: pip install websockets")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingServer:
|
||||||
|
def __init__(self, output_path: str, convert_to_js: bool = False):
|
||||||
|
self.output_path = Path(output_path)
|
||||||
|
self.convert_to_js = convert_to_js
|
||||||
|
self.file = None
|
||||||
|
self.frame_count = 0
|
||||||
|
self.bytes_written = 0
|
||||||
|
self.client_connected = False
|
||||||
|
self.start_time = None
|
||||||
|
|
||||||
|
async def handle_client(self, websocket):
|
||||||
|
"""Handle a single WebSocket client connection."""
|
||||||
|
if self.client_connected:
|
||||||
|
print("Warning: Another client is already connected, rejecting new connection")
|
||||||
|
await websocket.close(1008, "Another client already connected")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.client_connected = True
|
||||||
|
self.frame_count = 0
|
||||||
|
self.bytes_written = 0
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
|
||||||
|
# Open output file
|
||||||
|
self.file = open(self.output_path, 'wb')
|
||||||
|
|
||||||
|
client_addr = websocket.remote_address
|
||||||
|
print(f"[{self.start_time.strftime('%H:%M:%S')}] Client connected from {client_addr}")
|
||||||
|
print(f"Recording to: {self.output_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for message in websocket:
|
||||||
|
if isinstance(message, bytes):
|
||||||
|
self.file.write(message)
|
||||||
|
self.frame_count += 1
|
||||||
|
self.bytes_written += len(message)
|
||||||
|
|
||||||
|
# Progress update every 1000 frames
|
||||||
|
if self.frame_count % 1000 == 0:
|
||||||
|
mb = self.bytes_written / (1024 * 1024)
|
||||||
|
print(f" Received {self.frame_count} frames ({mb:.2f} MB)")
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
|
print(f"Connection closed: {e.code} {e.reason}")
|
||||||
|
finally:
|
||||||
|
self.file.close()
|
||||||
|
self.client_connected = False
|
||||||
|
|
||||||
|
duration = (datetime.now() - self.start_time).total_seconds()
|
||||||
|
mb = self.bytes_written / (1024 * 1024)
|
||||||
|
print(f"Recording complete: {self.frame_count} frames, {mb:.2f} MB in {duration:.1f}s")
|
||||||
|
|
||||||
|
if self.convert_to_js:
|
||||||
|
await self.convert_to_js_format()
|
||||||
|
|
||||||
|
async def convert_to_js_format(self):
|
||||||
|
"""Convert binary recording to JS playback format."""
|
||||||
|
js_path = self.output_path.with_suffix('.js')
|
||||||
|
print(f"Converting to JS format: {js_path}")
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
with open(self.output_path, 'rb') as f:
|
||||||
|
while True:
|
||||||
|
header = f.read(9)
|
||||||
|
if len(header) < 9:
|
||||||
|
break
|
||||||
|
|
||||||
|
from_client = header[0] == 1
|
||||||
|
timestamp = struct.unpack('>I', header[1:5])[0] # big endian
|
||||||
|
data_len = struct.unpack('>I', header[5:9])[0] # big endian
|
||||||
|
|
||||||
|
data = f.read(data_len)
|
||||||
|
if len(data) < data_len:
|
||||||
|
print(f"Warning: Truncated frame, expected {data_len} bytes, got {len(data)}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Convert to base64
|
||||||
|
b64_data = base64.b64encode(data).decode('ascii')
|
||||||
|
|
||||||
|
# Format: "{timestamp{base64data" for server, "}timestamp{base64data" for client
|
||||||
|
prefix = '}' if from_client else '{'
|
||||||
|
frame_str = f'{prefix}{timestamp}{{{b64_data}'
|
||||||
|
frames.append(f'"{frame_str}"')
|
||||||
|
|
||||||
|
frames.append('"EOF"')
|
||||||
|
|
||||||
|
# Write JS file
|
||||||
|
with open(js_path, 'w') as f:
|
||||||
|
f.write(f'/* Recorded VNC session - {datetime.now().isoformat()} */\n')
|
||||||
|
f.write(f'/* {len(frames) - 1} frames */\n')
|
||||||
|
f.write('var VNC_frame_data = [\n')
|
||||||
|
f.write(',\n'.join(frames))
|
||||||
|
f.write('\n];\n')
|
||||||
|
|
||||||
|
print(f"JS conversion complete: {js_path}")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser(description='VNC Recording Server')
|
||||||
|
parser.add_argument('--port', type=int, default=6090, help='Port to listen on (default: 6090)')
|
||||||
|
parser.add_argument('--output', type=str, default='recording.bin', help='Output file path')
|
||||||
|
parser.add_argument('--js', action='store_true', help='Convert to JS format when recording ends')
|
||||||
|
parser.add_argument('--host', type=str, default='0.0.0.0', help='Host to bind to (default: 0.0.0.0)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
server = RecordingServer(args.output, convert_to_js=args.js)
|
||||||
|
|
||||||
|
# Handle graceful shutdown
|
||||||
|
stop_event = asyncio.Event()
|
||||||
|
|
||||||
|
def signal_handler():
|
||||||
|
print("\nShutting down...")
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
loop.add_signal_handler(sig, signal_handler)
|
||||||
|
|
||||||
|
print(f"VNC Recording Server")
|
||||||
|
print(f"====================")
|
||||||
|
print(f"Listening on ws://{args.host}:{args.port}")
|
||||||
|
print(f"Output file: {args.output}")
|
||||||
|
print(f"JS conversion: {'enabled' if args.js else 'disabled'}")
|
||||||
|
print()
|
||||||
|
print("Use in noVNC with URL parameter:")
|
||||||
|
print(f" record_url=ws://localhost:{args.port}")
|
||||||
|
print()
|
||||||
|
print("Press Ctrl+C to stop")
|
||||||
|
print()
|
||||||
|
|
||||||
|
async with websockets.serve(server.handle_client, args.host, args.port):
|
||||||
|
await stop_event.wait()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
Loading…
Reference in New Issue