Alvin3y1 commited on
Commit
e3a745f
·
verified ·
1 Parent(s): 0d72fd8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -0
app.py CHANGED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import time
7
+ import logging
8
+ from aiohttp import web
9
+ from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceServer, RTCConfiguration
10
+
11
+ # --- Configuration ---
12
+ HOST = "0.0.0.0"
13
+ PORT = 7860
14
+ DISPLAY_NUM = ":99"
15
+ VNC_PORT = 5900
16
+
17
+ # Initial Resolution
18
+ DEFAULT_WIDTH = 1280
19
+ DEFAULT_HEIGHT = 720
20
+
21
+ # Cloudflare TURN Credentials (Optional but recommended for remote access)
22
+ TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6"
23
+ TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d"
24
+
25
+ logging.basicConfig(level=logging.INFO)
26
+ logger = logging.getLogger("VNC-RTC-Bridge")
27
+
28
+ # --- System Process Management ---
29
+
30
+ def start_system():
31
+ """Initializes the virtual display environment and VNC server."""
32
+ os.environ["DISPLAY"] = DISPLAY_NUM
33
+
34
+ # 1. Start Xvfb (Virtual Framebuffer)
35
+ logger.info(f"Starting Xvfb on {DISPLAY_NUM}...")
36
+ subprocess.Popen([
37
+ "Xvfb", DISPLAY_NUM,
38
+ "-screen", "0", f"{DEFAULT_WIDTH}x{DEFAULT_HEIGHT}x24",
39
+ "-ac", "-noreset"
40
+ ])
41
+ time.sleep(2) # Wait for X to initialize
42
+
43
+ # 2. Start x11vnc (The VNC Server)
44
+ # -forever: keep running after client disconnects
45
+ # -shared: allow multiple connections
46
+ # -nopw: no password for this internal bridge
47
+ # -rfbport: VNC standard port
48
+ logger.info(f"Starting x11vnc on port {VNC_PORT}...")
49
+ subprocess.Popen([
50
+ "x11vnc", "-display", DISPLAY_NUM,
51
+ "-rfbport", str(VNC_PORT),
52
+ "-forever", "-shared", "-nopw",
53
+ "-bg", "-quiet", "-xkb"
54
+ ])
55
+
56
+ # 3. Start Window Manager (Matchbox)
57
+ if shutil.which("matchbox-window-manager"):
58
+ logger.info("Starting Matchbox Window Manager...")
59
+ subprocess.Popen("matchbox-window-manager -use_titlebar no", shell=True)
60
+
61
+ # 4. Start the Application (Opera)
62
+ logger.info("Starting Opera Browser...")
63
+ opera_cmd = (
64
+ "opera "
65
+ "--no-sandbox "
66
+ "--start-maximized "
67
+ "--user-data-dir=/home/user/opera-data "
68
+ "--disable-infobars "
69
+ "--disable-dev-shm-usage "
70
+ "--disable-gpu "
71
+ "--no-first-run "
72
+ )
73
+ subprocess.Popen(opera_cmd, shell=True)
74
+
75
+ # --- WebRTC DataChannel to VNC TCP Bridge ---
76
+
77
+ async def bridge_vnc_to_datachannel(channel):
78
+ """
79
+ Connects to the local VNC TCP port and pipes data bidirectionally
80
+ to the WebRTC DataChannel.
81
+ """
82
+ retry_count = 5
83
+ reader, writer = None, None
84
+
85
+ # Try to connect to the local VNC server
86
+ while retry_count > 0:
87
+ try:
88
+ reader, writer = await asyncio.open_connection('127.0.0.1', VNC_PORT)
89
+ break
90
+ except ConnectionRefusedError:
91
+ retry_count -= 1
92
+ logger.warning(f"VNC connection refused, retrying... ({retry_count} left)")
93
+ await asyncio.sleep(1)
94
+
95
+ if not writer:
96
+ logger.error("Could not connect to local VNC server.")
97
+ channel.close()
98
+ return
99
+
100
+ logger.info("Successfully bridged DataChannel to local VNC.")
101
+
102
+ # Task: Local VNC Socket (TCP) -> WebRTC DataChannel
103
+ async def vnc_to_webrtc():
104
+ try:
105
+ while True:
106
+ data = await reader.read(16384) # Read binary chunks
107
+ if not data:
108
+ break
109
+ channel.send(data)
110
+ except Exception as e:
111
+ logger.error(f"VNC to WebRTC Bridge error: {e}")
112
+ finally:
113
+ logger.info("VNC Socket closed.")
114
+
115
+ # Task: WebRTC DataChannel -> Local VNC Socket (TCP)
116
+ @channel.on("message")
117
+ def on_message(message):
118
+ if isinstance(message, bytes):
119
+ # Write raw RFB protocol bytes to the VNC server
120
+ writer.write(message)
121
+ elif isinstance(message, str):
122
+ # Handle potential JSON metadata (like manual resize requests)
123
+ try:
124
+ data = json.loads(message)
125
+ if data.get("type") == "resize":
126
+ w, h = data["width"], data["height"]
127
+ subprocess.run(["xrandr", "--fb", f"{w}x{h}"], check=False)
128
+ except:
129
+ pass
130
+
131
+ try:
132
+ await vnc_to_webrtc()
133
+ finally:
134
+ writer.close()
135
+ await writer.wait_closed()
136
+
137
+ # --- HTTP / WebRTC Handlers ---
138
+
139
+ pcs = set()
140
+
141
+ async def offer(request):
142
+ """Handles the WebRTC signaling offer."""
143
+ try:
144
+ params = await request.json()
145
+ offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
146
+ except Exception as e:
147
+ return web.Response(status=400, text=str(e))
148
+
149
+ # Configure WebRTC with STUN/TURN
150
+ pc = RTCPeerConnection(RTCConfiguration(iceServers=[
151
+ RTCIceServer(urls=["turns:turn.cloudflare.com:443?transport=tcp", "turn:turn.cloudflare.com:3478?transport=udp"],
152
+ username=TURN_USER, credential=TURN_PASS),
153
+ RTCIceServer(urls=["stun:stun.l.google.com:19302"])
154
+ ]))
155
+ pcs.add(pc)
156
+
157
+ @pc.on("connectionstatechange")
158
+ async def on_state():
159
+ if pc.connectionState in ["failed", "closed"]:
160
+ await pc.close()
161
+ pcs.discard(pc)
162
+
163
+ @pc.on("datachannel")
164
+ def on_dc(channel):
165
+ # When noVNC creates a DataChannel, start the TCP bridge
166
+ asyncio.create_task(bridge_vnc_to_datachannel(channel))
167
+
168
+ await pc.setRemoteDescription(offer)
169
+ answer = await pc.createAnswer()
170
+ await pc.setLocalDescription(answer)
171
+
172
+ return web.Response(
173
+ content_type="application/json",
174
+ text=json.dumps({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}),
175
+ headers={"Access-Control-Allow-Origin": "*"}
176
+ )
177
+
178
+ async def options(request):
179
+ """CORS preflight handler."""
180
+ return web.Response(headers={
181
+ "Access-Control-Allow-Origin": "*",
182
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
183
+ "Access-Control-Allow-Headers": "Content-Type"
184
+ })
185
+
186
+ async def on_shutdown(app):
187
+ """Clean up PeerConnections on server stop."""
188
+ coros = [pc.close() for pc in pcs]
189
+ await asyncio.gather(*coros)
190
+ pcs.clear()
191
+
192
+ # --- Main Entry ---
193
+
194
+ if __name__ == "__main__":
195
+ # Start the X11 and VNC backend
196
+ start_system()
197
+
198
+ # Start the WebRTC Signaling Server
199
+ app = web.Application()
200
+ app.on_shutdown.append(on_shutdown)
201
+
202
+ app.router.add_get("/", lambda r: web.Response(text="WebRTC VNC Bridge is running."))
203
+ app.router.add_post("/offer", offer)
204
+ app.router.add_options("/offer", options)
205
+
206
+ web.run_app(app, host=HOST, port=PORT)