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 DEFAULT_RES = "1280x720" TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6" TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d" logging.basicConfig(level=logging.INFO) logger = logging.getLogger("TigerVNC-Bridge") def start_system(): os.environ["DISPLAY"] = DISPLAY_NUM # 1. Start TigerVNC (Xvnc) # SecurityTypes=None: Allows connection without password (since it's only on localhost) # -alwaysshared: Allows multiple WebRTC clients to see the same screen logger.info(f"Starting TigerVNC (Xvnc) on {DISPLAY_NUM}...") subprocess.Popen([ "Xvnc", DISPLAY_NUM, "-geometry", DEFAULT_RES, "-depth", "24", "-rfbport", str(VNC_PORT), "-SecurityTypes", "None", "-alwaysshared", "-AcceptKeyEvents", "-AcceptPointerEvents", "-AcceptSetDesktopSize", "-localhost" # Only allow connections from this script ]) time.sleep(2) # 2. Start Window Manager if shutil.which("matchbox-window-manager"): subprocess.Popen("matchbox-window-manager -use_titlebar no", shell=True) # 3. Start Opera opera_cmd = "opera --no-sandbox --start-maximized --user-data-dir=/home/user/opera-data" subprocess.Popen(opera_cmd, shell=True) async def bridge_vnc_to_datachannel(channel): try: # Connect to TigerVNC TCP socket reader, writer = await asyncio.open_connection('127.0.0.1', VNC_PORT) logger.info("Connected to TigerVNC.") async def vnc_to_webrtc(): try: while True: data = await reader.read(32768) # Larger buffer for TigerVNC speed if not data: break channel.send(data) except: pass @channel.on("message") def on_message(message): if isinstance(message, bytes): writer.write(message) elif isinstance(message, str): try: d = json.loads(message) if d.get("type") == "resize": # TigerVNC supports dynamic resizing via xrandr subprocess.run(["xrandr", "-s", f"{d['width']}x{d['height']}"], check=False) except: pass await vnc_to_webrtc() except Exception as e: logger.error(f"Bridge error: {e}") finally: if 'writer' in locals(): writer.close() # --- Standard WebRTC Signaling Handlers --- pcs = set() async def offer(request): params = await request.json() pc = RTCPeerConnection(RTCConfiguration(iceServers=[ RTCIceServer(urls=["stun:stun.l.google.com:19302"]), RTCIceServer(urls=["turns:turn.cloudflare.com:443?transport=tcp"], username=TURN_USER, credential=TURN_PASS) ])) pcs.add(pc) @pc.on("datachannel") def on_dc(channel): asyncio.create_task(bridge_vnc_to_datachannel(channel)) @pc.on("connectionstatechange") async def on_state(): if pc.connectionState in ["failed", "closed"]: await pc.close() pcs.discard(pc) await pc.setRemoteDescription(RTCSessionDescription(sdp=params["sdp"], type=params["type"])) 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): return web.Response(headers={ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type" }) if __name__ == "__main__": start_system() app = web.Application() app.router.add_post("/offer", offer) app.router.add_options("/offer", options) web.run_app(app, host=HOST, port=PORT)