Alvin3y1 commited on
Commit
adcc74c
·
verified ·
1 Parent(s): 8a41dce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +40 -54
app.py CHANGED
@@ -20,7 +20,7 @@ PORT = 7860
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
 
@@ -33,7 +33,7 @@ class X11Engine:
33
  self.dirty_rects = []
34
  self.lock = threading.Lock()
35
  self.use_damage = False
36
- self.last_full_refresh = 0
37
  self.alive = True
38
 
39
  try:
@@ -50,39 +50,36 @@ class X11Engine:
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:
@@ -91,18 +88,18 @@ class X11Engine:
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,
@@ -118,7 +115,6 @@ class X11Engine:
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)
@@ -127,32 +123,32 @@ class X11Engine:
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):
@@ -164,29 +160,19 @@ async def offer(request):
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):
@@ -194,26 +180,26 @@ async def index(request):
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)
 
20
  DISPLAY_NUM = ":99"
21
  WIDTH, HEIGHT = 1280, 720
22
 
23
+ # TURN/STUN Config
24
  TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6"
25
  TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d"
26
 
 
33
  self.dirty_rects = []
34
  self.lock = threading.Lock()
35
  self.use_damage = False
36
+ self.last_full_refresh = 0
37
  self.alive = True
38
 
39
  try:
 
50
  self.d = None
51
 
52
  def close(self):
 
53
  self.alive = False
54
  if self.d:
55
  try:
56
  self.d.close()
57
+ except: pass
 
58
 
59
  def collect_damage(self):
60
  if not self.d or not self.use_damage: return
61
  try:
 
62
  while self.d.pending_events() > 0:
63
  ev = self.d.next_event()
64
  if ev.type == self.d.extension_event.DamageNotify:
65
  with self.lock:
66
  self.dirty_rects.append((ev.area.x, ev.area.y, ev.area.width, ev.area.height))
67
  self.d.damage_subtract(self.damage_ext, 0, 0)
68
+ except: pass
 
69
 
70
  def get_patches(self):
71
  if not self.alive: return []
72
 
73
  with self.lock:
74
  now = time.time()
75
+ # Force full refresh if it's been >3s OR if it's the very first frame (0)
76
+ force = (self.last_full_refresh == 0) or (now - self.last_full_refresh > 3.0)
77
+
78
+ if force:
79
  rects = [(0, 0, WIDTH, HEIGHT)]
80
  self.last_full_refresh = now
81
  self.dirty_rects = []
82
  elif self.dirty_rects:
 
83
  rects = list(self.dirty_rects[-10:])
84
  self.dirty_rects = []
85
  else:
 
88
  patches = []
89
  for (x, y, w, h) in rects:
90
  try:
 
91
  x = max(0, min(x, WIDTH - 1))
92
  y = max(0, min(y, HEIGHT - 1))
93
  w = max(1, min(w, WIDTH - x))
94
  h = max(1, min(h, HEIGHT - y))
95
 
96
  sct_img = self.sct.grab({"top": y, "left": x, "width": w, "height": h})
 
 
97
  img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
98
+
99
  buf = BytesIO()
100
+ # Lower quality to 30 ensures the first big frame doesn't exceed UDP/DataChannel limits
101
+ q = 30 if (w > 1000 and h > 500) else 60
102
+ img.save(buf, format="JPEG", quality=q, optimize=True)
103
 
104
  patches.append({
105
  "x": x, "y": y, "w": w, "h": h,
 
115
  try:
116
  t = msg.get("type")
117
  if t == "mousemove":
 
118
  tx = int(max(0, min(msg["x"], 1.0)) * WIDTH)
119
  ty = int(max(0, min(msg["y"], 1.0)) * HEIGHT)
120
  xtest.fake_motion(self.d, tx, ty)
 
123
  elif t == "mouseup":
124
  xtest.fake_button_event(self.d, msg.get("button", 0) + 1, False)
125
  self.d.sync()
126
+ except: pass
 
127
 
128
  async def patch_stream_loop(channel, engine):
129
  logger.info("Stream started")
130
  try:
131
+ # Reset refresh timer to force an immediate full frame on connect
132
+ engine.last_full_refresh = 0
133
+
134
  while channel.readyState == "open":
135
  start_time = time.perf_counter()
 
 
136
  await asyncio.to_thread(engine.collect_damage)
137
  patches = await asyncio.to_thread(engine.get_patches)
138
 
139
  if patches:
140
  try:
141
+ payload = json.dumps({"type": "patch", "p": patches})
142
+ channel.send(payload)
143
+ # logger.info(f"Sent {len(patches)} patches ({len(payload)} bytes)")
144
+ except Exception as e:
145
+ logger.error(f"Send failed: {e}")
146
+ break
147
 
148
+ await asyncio.sleep(max(0.01, 0.033 - (time.perf_counter() - start_time)))
 
 
149
  except Exception as e:
150
  logger.error(f"Stream Loop Error: {e}")
151
  finally:
 
152
  engine.close()
153
 
154
  async def offer(request):
 
160
  RTCIceServer(urls=["stun:stun.l.google.com:19302"])
161
  ]))
162
 
 
163
  engine = X11Engine()
 
164
  @pc.on("datachannel")
165
  def on_dc(channel):
166
  @channel.on("open")
167
+ def on_open(): asyncio.create_task(patch_stream_loop(channel, engine))
 
 
168
  @channel.on("message")
169
+ def on_message(msg): engine.inject_input(json.loads(msg))
 
170
 
171
  await pc.setRemoteDescription(RTCSessionDescription(sdp=params["sdp"], type=params["type"]))
172
  answer = await pc.createAnswer()
173
  await pc.setLocalDescription(answer)
174
+ return web.json_response({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}, headers={"Access-Control-Allow-Origin": "*"})
 
 
 
 
175
  except Exception as e:
 
176
  return web.json_response({"error": str(e)}, status=500)
177
 
178
  async def index(request):
 
180
  return web.Response(content_type="text/html", text=content)
181
 
182
  if __name__ == "__main__":
 
183
  os.environ["DISPLAY"] = DISPLAY_NUM
184
+
185
+ # 1. Start Xvfb
186
  logger.info("Starting Xvfb...")
187
+ subprocess.Popen(["Xvfb", DISPLAY_NUM, "-screen", "0", f"{WIDTH}x{HEIGHT}x24", "+extension", "DAMAGE", "-ac", "-noreset"])
188
+ time.sleep(2)
189
 
190
+ # 2. Start Fluxbox (Window Manager) - Fixes black screen issues
191
+ logger.info("Starting Fluxbox...")
192
+ subprocess.Popen(["fluxbox"], env=os.environ)
193
+ time.sleep(1)
194
+
195
+ # 3. Start Opera
196
  logger.info("Starting Browser...")
197
+ cmd = "opera --no-sandbox --disable-gpu --disable-dev-shm-usage --start-maximized --window-size=1280,720 --window-position=0,0"
198
+ subprocess.Popen(cmd, shell=True, env=os.environ)
 
 
 
 
 
199
 
200
+ # 4. Start Server
201
  app = web.Application()
202
  app.router.add_get("/", index)
203
  app.router.add_post("/offer", offer)
204
+ logger.info(f"Server running on port {PORT}")
 
205
  web.run_app(app, host=HOST, port=PORT)