noVNC/utils/recording_server.py

173 lines
5.9 KiB
Python
Executable File

#!/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())