Alvin3y1 commited on
Commit
94cc7a2
·
verified ·
1 Parent(s): 36acdf5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -32
app.py CHANGED
@@ -20,6 +20,7 @@ PORT = 7860
20
  DISPLAY_NUM = ":99"
21
  WIDTH, HEIGHT = 1280, 720
22
 
 
23
  TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6"
24
  TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d"
25
 
@@ -33,6 +34,7 @@ class X11Engine:
33
  self.lock = threading.Lock()
34
  self.use_damage = False
35
  self.last_full_refresh = 0
 
36
 
37
  try:
38
  self.d = display.Display(DISPLAY_NUM)
@@ -42,32 +44,46 @@ class X11Engine:
42
  self.use_damage = True
43
  logger.info("X11 Damage Extension enabled.")
44
  else:
45
- logger.warning("DAMAGE extension missing. Using polling fallback.")
46
  except Exception as e:
47
- logger.error(f"X11 connection failed: {e}")
48
  self.d = None
49
 
 
 
 
 
 
 
 
 
 
50
  def collect_damage(self):
51
  if not self.d or not self.use_damage: return
52
  try:
 
53
  while self.d.pending_events() > 0:
54
  ev = self.d.next_event()
55
  if ev.type == self.d.extension_event.DamageNotify:
56
  with self.lock:
57
  self.dirty_rects.append((ev.area.x, ev.area.y, ev.area.width, ev.area.height))
58
  self.d.damage_subtract(self.damage_ext, 0, 0)
59
- except: pass
 
60
 
61
  def get_patches(self):
 
 
62
  with self.lock:
63
- # Force a full frame every 2 seconds to ensure the client isn't stuck on black
64
  now = time.time()
65
- if now - self.last_full_refresh > 2.0:
 
66
  rects = [(0, 0, WIDTH, HEIGHT)]
67
  self.last_full_refresh = now
68
  self.dirty_rects = []
69
  elif self.dirty_rects:
70
- rects = list(self.dirty_rects[-5:])
 
71
  self.dirty_rects = []
72
  else:
73
  return []
@@ -75,20 +91,26 @@ class X11Engine:
75
  patches = []
76
  for (x, y, w, h) in rects:
77
  try:
78
- # Screen bounds safety
79
- x, y = max(0, x), max(0, y)
80
- w, h = min(w, WIDTH-x), min(h, HEIGHT-y)
81
- if w <= 0 or h <= 0: continue
 
82
 
83
  sct_img = self.sct.grab({"top": y, "left": x, "width": w, "height": h})
 
 
84
  img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
85
  buf = BytesIO()
86
- img.save(buf, format="JPEG", quality=60)
 
87
  patches.append({
88
  "x": x, "y": y, "w": w, "h": h,
89
- "d": base64.b64encode(buf.getvalue()).decode()
90
  })
91
- except: continue
 
 
92
  return patches
93
 
94
  def inject_input(self, msg):
@@ -96,22 +118,42 @@ class X11Engine:
96
  try:
97
  t = msg.get("type")
98
  if t == "mousemove":
99
- xtest.fake_motion(self.d, int(msg["x"] * WIDTH), int(msg["y"] * HEIGHT))
 
 
 
100
  elif t == "mousedown":
101
  xtest.fake_button_event(self.d, msg.get("button", 0) + 1, True)
102
  elif t == "mouseup":
103
  xtest.fake_button_event(self.d, msg.get("button", 0) + 1, False)
104
  self.d.sync()
105
- except: pass
 
106
 
107
  async def patch_stream_loop(channel, engine):
108
- while channel.readyState == "open":
109
- start = time.perf_counter()
110
- await asyncio.to_thread(engine.collect_damage)
111
- patches = await asyncio.to_thread(engine.get_patches)
112
- if patches:
113
- channel.send(json.dumps({"type": "patch", "p": patches}))
114
- await asyncio.sleep(max(0, 0.033 - (time.perf_counter() - start)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  async def offer(request):
117
  try:
@@ -121,32 +163,57 @@ async def offer(request):
121
  username=TURN_USER, credential=TURN_PASS),
122
  RTCIceServer(urls=["stun:stun.l.google.com:19302"])
123
  ]))
 
 
124
  engine = X11Engine()
 
125
  @pc.on("datachannel")
126
  def on_dc(channel):
127
  @channel.on("open")
128
- def on_open(): asyncio.create_task(patch_stream_loop(channel, engine))
 
 
129
  @channel.on("message")
130
- def on_message(msg): engine.inject_input(json.loads(msg))
 
131
 
132
  await pc.setRemoteDescription(RTCSessionDescription(sdp=params["sdp"], type=params["type"]))
133
  answer = await pc.createAnswer()
134
  await pc.setLocalDescription(answer)
135
- return web.json_response({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type},
136
- headers={"Access-Control-Allow-Origin": "*"})
 
 
 
137
  except Exception as e:
 
138
  return web.json_response({"error": str(e)}, status=500)
139
 
140
- async def index(request): return web.Response(text="hello world")
141
- async def options(r): return web.Response(headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type"})
 
142
 
143
  if __name__ == "__main__":
 
144
  os.environ["DISPLAY"] = DISPLAY_NUM
145
- subprocess.Popen(["Xvfb", DISPLAY_NUM, "-screen", "0", f"{WIDTH}x{HEIGHT}x24", "+extension", "DAMAGE", "-ac", "-noreset"])
146
- time.sleep(5)
147
- subprocess.Popen("opera --no-sandbox --disable-gpu --start-maximized", shell=True)
 
 
 
 
 
 
 
 
 
 
 
 
148
  app = web.Application()
149
  app.router.add_get("/", index)
150
  app.router.add_post("/offer", offer)
151
- app.router.add_options("/offer", options)
 
152
  web.run_app(app, host=HOST, port=PORT)
 
20
  DISPLAY_NUM = ":99"
21
  WIDTH, HEIGHT = 1280, 720
22
 
23
+ # Use public STUN/TURN if specific creds fail, but keeping your original config
24
  TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6"
25
  TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d"
26
 
 
34
  self.lock = threading.Lock()
35
  self.use_damage = False
36
  self.last_full_refresh = 0
37
+ self.alive = True
38
 
39
  try:
40
  self.d = display.Display(DISPLAY_NUM)
 
44
  self.use_damage = True
45
  logger.info("X11 Damage Extension enabled.")
46
  else:
47
+ logger.warning("DAMAGE extension missing. Polling mode.")
48
  except Exception as e:
49
+ logger.error(f"X11 Init Failed: {e}")
50
  self.d = None
51
 
52
+ def close(self):
53
+ """Cleanup resources"""
54
+ self.alive = False
55
+ if self.d:
56
+ try:
57
+ self.d.close()
58
+ except:
59
+ pass
60
+
61
  def collect_damage(self):
62
  if not self.d or not self.use_damage: return
63
  try:
64
+ # Check pending events without blocking indefinitely
65
  while self.d.pending_events() > 0:
66
  ev = self.d.next_event()
67
  if ev.type == self.d.extension_event.DamageNotify:
68
  with self.lock:
69
  self.dirty_rects.append((ev.area.x, ev.area.y, ev.area.width, ev.area.height))
70
  self.d.damage_subtract(self.damage_ext, 0, 0)
71
+ except Exception as e:
72
+ logger.debug(f"Event error: {e}")
73
 
74
  def get_patches(self):
75
+ if not self.alive: return []
76
+
77
  with self.lock:
 
78
  now = time.time()
79
+ # Force refresh every 3 seconds or if no damage system
80
+ if (now - self.last_full_refresh > 3.0) or (not self.use_damage and now - self.last_full_refresh > 0.1):
81
  rects = [(0, 0, WIDTH, HEIGHT)]
82
  self.last_full_refresh = now
83
  self.dirty_rects = []
84
  elif self.dirty_rects:
85
+ # Merge small updates or take last 10 to prevent overload
86
+ rects = list(self.dirty_rects[-10:])
87
  self.dirty_rects = []
88
  else:
89
  return []
 
91
  patches = []
92
  for (x, y, w, h) in rects:
93
  try:
94
+ # Sanitize bounds for MSS (Critical to prevent crash)
95
+ x = max(0, min(x, WIDTH - 1))
96
+ y = max(0, min(y, HEIGHT - 1))
97
+ w = max(1, min(w, WIDTH - x))
98
+ h = max(1, min(h, HEIGHT - y))
99
 
100
  sct_img = self.sct.grab({"top": y, "left": x, "width": w, "height": h})
101
+
102
+ # Convert to JPEG
103
  img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
104
  buf = BytesIO()
105
+ img.save(buf, format="JPEG", quality=60, optimize=True)
106
+
107
  patches.append({
108
  "x": x, "y": y, "w": w, "h": h,
109
+ "d": base64.b64encode(buf.getvalue()).decode('ascii')
110
  })
111
+ except Exception as e:
112
+ logger.error(f"Capture error: {e}")
113
+ continue
114
  return patches
115
 
116
  def inject_input(self, msg):
 
118
  try:
119
  t = msg.get("type")
120
  if t == "mousemove":
121
+ # Ensure coordinates are within bounds
122
+ tx = int(max(0, min(msg["x"], 1.0)) * WIDTH)
123
+ ty = int(max(0, min(msg["y"], 1.0)) * HEIGHT)
124
+ xtest.fake_motion(self.d, tx, ty)
125
  elif t == "mousedown":
126
  xtest.fake_button_event(self.d, msg.get("button", 0) + 1, True)
127
  elif t == "mouseup":
128
  xtest.fake_button_event(self.d, msg.get("button", 0) + 1, False)
129
  self.d.sync()
130
+ except Exception:
131
+ pass
132
 
133
  async def patch_stream_loop(channel, engine):
134
+ logger.info("Stream started")
135
+ try:
136
+ while channel.readyState == "open":
137
+ start_time = time.perf_counter()
138
+
139
+ # Run X11/MSS operations in a thread to avoid blocking AsyncIO
140
+ await asyncio.to_thread(engine.collect_damage)
141
+ patches = await asyncio.to_thread(engine.get_patches)
142
+
143
+ if patches:
144
+ try:
145
+ channel.send(json.dumps({"type": "patch", "p": patches}))
146
+ except Exception:
147
+ break # Channel likely closed
148
+
149
+ # Maintain roughly 30 FPS cap
150
+ elapsed = time.perf_counter() - start_time
151
+ await asyncio.sleep(max(0.01, 0.033 - elapsed))
152
+ except Exception as e:
153
+ logger.error(f"Stream Loop Error: {e}")
154
+ finally:
155
+ logger.info("Stream stopped, cleaning up engine")
156
+ engine.close()
157
 
158
  async def offer(request):
159
  try:
 
163
  username=TURN_USER, credential=TURN_PASS),
164
  RTCIceServer(urls=["stun:stun.l.google.com:19302"])
165
  ]))
166
+
167
+ # Create a new engine for this connection
168
  engine = X11Engine()
169
+
170
  @pc.on("datachannel")
171
  def on_dc(channel):
172
  @channel.on("open")
173
+ def on_open():
174
+ asyncio.create_task(patch_stream_loop(channel, engine))
175
+
176
  @channel.on("message")
177
+ def on_message(msg):
178
+ engine.inject_input(json.loads(msg))
179
 
180
  await pc.setRemoteDescription(RTCSessionDescription(sdp=params["sdp"], type=params["type"]))
181
  answer = await pc.createAnswer()
182
  await pc.setLocalDescription(answer)
183
+
184
+ return web.json_response(
185
+ {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type},
186
+ headers={"Access-Control-Allow-Origin": "*"}
187
+ )
188
  except Exception as e:
189
+ logger.error(f"Offer failed: {e}")
190
  return web.json_response({"error": str(e)}, status=500)
191
 
192
+ async def index(request):
193
+ content = open("client2.html", "r").read()
194
+ return web.Response(content_type="text/html", text=content)
195
 
196
  if __name__ == "__main__":
197
+ # 1. Start Virtual Screen
198
  os.environ["DISPLAY"] = DISPLAY_NUM
199
+ logger.info("Starting Xvfb...")
200
+ xvfb = subprocess.Popen(["Xvfb", DISPLAY_NUM, "-screen", "0", f"{WIDTH}x{HEIGHT}x24", "+extension", "DAMAGE", "-ac", "-noreset"])
201
+ time.sleep(3) # Wait for X server
202
+
203
+ # 2. Start Opera with Docker-safe flags
204
+ logger.info("Starting Browser...")
205
+ # --disable-dev-shm-usage is CRITICAL in Docker
206
+ browser_cmd = (
207
+ "opera --no-sandbox --disable-gpu --disable-dev-shm-usage "
208
+ "--start-maximized --window-size=1280,720 --window-position=0,0 "
209
+ "--remote-debugging-port=9222"
210
+ )
211
+ subprocess.Popen(browser_cmd, shell=True)
212
+
213
+ # 3. Start Web Server
214
  app = web.Application()
215
  app.router.add_get("/", index)
216
  app.router.add_post("/offer", offer)
217
+
218
+ logger.info(f"Server running on {HOST}:{PORT}")
219
  web.run_app(app, host=HOST, port=PORT)