WebP / app.py
Alvin3y1's picture
Update app.py
e3a745f verified
import asyncio
import json
import os
import shutil
import subprocess
import time
import logging
from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceServer, RTCConfiguration
# --- Configuration ---
HOST = "0.0.0.0"
PORT = 7860
DISPLAY_NUM = ":99"
VNC_PORT = 5900
# Initial Resolution
DEFAULT_WIDTH = 1280
DEFAULT_HEIGHT = 720
# Cloudflare TURN Credentials (Optional but recommended for remote access)
TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6"
TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d"
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("VNC-RTC-Bridge")
# --- System Process Management ---
def start_system():
"""Initializes the virtual display environment and VNC server."""
os.environ["DISPLAY"] = DISPLAY_NUM
# 1. Start Xvfb (Virtual Framebuffer)
logger.info(f"Starting Xvfb on {DISPLAY_NUM}...")
subprocess.Popen([
"Xvfb", DISPLAY_NUM,
"-screen", "0", f"{DEFAULT_WIDTH}x{DEFAULT_HEIGHT}x24",
"-ac", "-noreset"
])
time.sleep(2) # Wait for X to initialize
# 2. Start x11vnc (The VNC Server)
# -forever: keep running after client disconnects
# -shared: allow multiple connections
# -nopw: no password for this internal bridge
# -rfbport: VNC standard port
logger.info(f"Starting x11vnc on port {VNC_PORT}...")
subprocess.Popen([
"x11vnc", "-display", DISPLAY_NUM,
"-rfbport", str(VNC_PORT),
"-forever", "-shared", "-nopw",
"-bg", "-quiet", "-xkb"
])
# 3. Start Window Manager (Matchbox)
if shutil.which("matchbox-window-manager"):
logger.info("Starting Matchbox Window Manager...")
subprocess.Popen("matchbox-window-manager -use_titlebar no", shell=True)
# 4. Start the Application (Opera)
logger.info("Starting Opera Browser...")
opera_cmd = (
"opera "
"--no-sandbox "
"--start-maximized "
"--user-data-dir=/home/user/opera-data "
"--disable-infobars "
"--disable-dev-shm-usage "
"--disable-gpu "
"--no-first-run "
)
subprocess.Popen(opera_cmd, shell=True)
# --- WebRTC DataChannel to VNC TCP Bridge ---
async def bridge_vnc_to_datachannel(channel):
"""
Connects to the local VNC TCP port and pipes data bidirectionally
to the WebRTC DataChannel.
"""
retry_count = 5
reader, writer = None, None
# Try to connect to the local VNC server
while retry_count > 0:
try:
reader, writer = await asyncio.open_connection('127.0.0.1', VNC_PORT)
break
except ConnectionRefusedError:
retry_count -= 1
logger.warning(f"VNC connection refused, retrying... ({retry_count} left)")
await asyncio.sleep(1)
if not writer:
logger.error("Could not connect to local VNC server.")
channel.close()
return
logger.info("Successfully bridged DataChannel to local VNC.")
# Task: Local VNC Socket (TCP) -> WebRTC DataChannel
async def vnc_to_webrtc():
try:
while True:
data = await reader.read(16384) # Read binary chunks
if not data:
break
channel.send(data)
except Exception as e:
logger.error(f"VNC to WebRTC Bridge error: {e}")
finally:
logger.info("VNC Socket closed.")
# Task: WebRTC DataChannel -> Local VNC Socket (TCP)
@channel.on("message")
def on_message(message):
if isinstance(message, bytes):
# Write raw RFB protocol bytes to the VNC server
writer.write(message)
elif isinstance(message, str):
# Handle potential JSON metadata (like manual resize requests)
try:
data = json.loads(message)
if data.get("type") == "resize":
w, h = data["width"], data["height"]
subprocess.run(["xrandr", "--fb", f"{w}x{h}"], check=False)
except:
pass
try:
await vnc_to_webrtc()
finally:
writer.close()
await writer.wait_closed()
# --- HTTP / WebRTC Handlers ---
pcs = set()
async def offer(request):
"""Handles the WebRTC signaling offer."""
try:
params = await request.json()
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
except Exception as e:
return web.Response(status=400, text=str(e))
# Configure WebRTC with STUN/TURN
pc = RTCPeerConnection(RTCConfiguration(iceServers=[
RTCIceServer(urls=["turns:turn.cloudflare.com:443?transport=tcp", "turn:turn.cloudflare.com:3478?transport=udp"],
username=TURN_USER, credential=TURN_PASS),
RTCIceServer(urls=["stun:stun.l.google.com:19302"])
]))
pcs.add(pc)
@pc.on("connectionstatechange")
async def on_state():
if pc.connectionState in ["failed", "closed"]:
await pc.close()
pcs.discard(pc)
@pc.on("datachannel")
def on_dc(channel):
# When noVNC creates a DataChannel, start the TCP bridge
asyncio.create_task(bridge_vnc_to_datachannel(channel))
await pc.setRemoteDescription(offer)
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
return web.Response(
content_type="application/json",
text=json.dumps({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}),
headers={"Access-Control-Allow-Origin": "*"}
)
async def options(request):
"""CORS preflight handler."""
return web.Response(headers={
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
})
async def on_shutdown(app):
"""Clean up PeerConnections on server stop."""
coros = [pc.close() for pc in pcs]
await asyncio.gather(*coros)
pcs.clear()
# --- Main Entry ---
if __name__ == "__main__":
# Start the X11 and VNC backend
start_system()
# Start the WebRTC Signaling Server
app = web.Application()
app.on_shutdown.append(on_shutdown)
app.router.add_get("/", lambda r: web.Response(text="WebRTC VNC Bridge is running."))
app.router.add_post("/offer", offer)
app.router.add_options("/offer", options)
web.run_app(app, host=HOST, port=PORT)