File size: 4,245 Bytes
6540af4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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)