| | 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 |
| |
|
| | |
| | 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 |
| | |
| | |
| | |
| | |
| | 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" |
| | ]) |
| | time.sleep(2) |
| |
|
| | |
| | if shutil.which("matchbox-window-manager"): |
| | subprocess.Popen("matchbox-window-manager -use_titlebar no", shell=True) |
| |
|
| | |
| | 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: |
| | |
| | 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) |
| | 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": |
| | |
| | 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() |
| |
|
| | |
| |
|
| | 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) |