File size: 7,628 Bytes
02155fc b74a704 c536017 b74a704 c536017 e603465 c536017 02155fc b74a704 c536017 b74a704 c536017 adcc74c b74a704 3966aff b74a704 e603465 adcc74c 94cc7a2 e603465 3966aff e603465 94cc7a2 3966aff 94cc7a2 e0a6159 c536017 94cc7a2 adcc74c 94cc7a2 c536017 e0a6159 3966aff adcc74c c536017 b74a704 94cc7a2 b74a704 e0a6159 adcc74c e0a6159 94cc7a2 e0a6159 b74a704 3966aff 94cc7a2 e0a6159 3966aff adcc74c 3966aff adcc74c 94cc7a2 e0a6159 94cc7a2 e0a6159 94cc7a2 b74a704 e603465 b74a704 94cc7a2 b74a704 adcc74c b74a704 94cc7a2 adcc74c 94cc7a2 adcc74c 94cc7a2 adcc74c 94cc7a2 3966aff 02155fc 3966aff 94cc7a2 3966aff adcc74c 3966aff adcc74c 3966aff adcc74c 3966aff 8f006d4 94cc7a2 c536017 02155fc b74a704 adcc74c 94cc7a2 adcc74c 94cc7a2 adcc74c 94cc7a2 adcc74c 94cc7a2 adcc74c 02155fc 3966aff 02155fc adcc74c b74a704 |
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
import asyncio
import json
import os
import subprocess
import time
import logging
import base64
import mss
import threading
from io import BytesIO
from PIL import Image
from Xlib import display, X
from Xlib.ext import xtest
from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceServer, RTCConfiguration
# --- Configuration ---
HOST = "0.0.0.0"
PORT = 7860
DISPLAY_NUM = ":99"
WIDTH, HEIGHT = 1280, 720
# TURN/STUN Config
TURN_USER = "g08abe68c81a07f098bb5f0914549bb32440e5aad0b216c7fba2b61e76fd62c6"
TURN_PASS = "aed1a10dd10eba9401ad9d99e5c66036d8a970eab5ba8e6dc9845ab57c771a7d"
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("X11-RTC")
class X11Engine:
def __init__(self):
self.sct = mss.mss()
self.dirty_rects = []
self.lock = threading.Lock()
self.use_damage = False
self.last_full_refresh = 0
self.alive = True
try:
self.d = display.Display(DISPLAY_NUM)
self.root = self.d.screen().root
if self.d.has_extension('DAMAGE'):
self.damage_ext = self.d.damage_create(self.root, X.DamageReportLevelNonEmpty)
self.use_damage = True
logger.info("X11 Damage Extension enabled.")
else:
logger.warning("DAMAGE extension missing. Polling mode.")
except Exception as e:
logger.error(f"X11 Init Failed: {e}")
self.d = None
def close(self):
self.alive = False
if self.d:
try:
self.d.close()
except: pass
def collect_damage(self):
if not self.d or not self.use_damage: return
try:
while self.d.pending_events() > 0:
ev = self.d.next_event()
if ev.type == self.d.extension_event.DamageNotify:
with self.lock:
self.dirty_rects.append((ev.area.x, ev.area.y, ev.area.width, ev.area.height))
self.d.damage_subtract(self.damage_ext, 0, 0)
except: pass
def get_patches(self):
if not self.alive: return []
with self.lock:
now = time.time()
# Force full refresh if it's been >3s OR if it's the very first frame (0)
force = (self.last_full_refresh == 0) or (now - self.last_full_refresh > 3.0)
if force:
rects = [(0, 0, WIDTH, HEIGHT)]
self.last_full_refresh = now
self.dirty_rects = []
elif self.dirty_rects:
rects = list(self.dirty_rects[-10:])
self.dirty_rects = []
else:
return []
patches = []
for (x, y, w, h) in rects:
try:
x = max(0, min(x, WIDTH - 1))
y = max(0, min(y, HEIGHT - 1))
w = max(1, min(w, WIDTH - x))
h = max(1, min(h, HEIGHT - y))
sct_img = self.sct.grab({"top": y, "left": x, "width": w, "height": h})
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
buf = BytesIO()
# Lower quality to 30 ensures the first big frame doesn't exceed UDP/DataChannel limits
q = 30 if (w > 1000 and h > 500) else 60
img.save(buf, format="JPEG", quality=q, optimize=True)
patches.append({
"x": x, "y": y, "w": w, "h": h,
"d": base64.b64encode(buf.getvalue()).decode('ascii')
})
except Exception as e:
logger.error(f"Capture error: {e}")
continue
return patches
def inject_input(self, msg):
if not self.d: return
try:
t = msg.get("type")
if t == "mousemove":
tx = int(max(0, min(msg["x"], 1.0)) * WIDTH)
ty = int(max(0, min(msg["y"], 1.0)) * HEIGHT)
xtest.fake_motion(self.d, tx, ty)
elif t == "mousedown":
xtest.fake_button_event(self.d, msg.get("button", 0) + 1, True)
elif t == "mouseup":
xtest.fake_button_event(self.d, msg.get("button", 0) + 1, False)
self.d.sync()
except: pass
async def patch_stream_loop(channel, engine):
logger.info("Stream started")
try:
# Reset refresh timer to force an immediate full frame on connect
engine.last_full_refresh = 0
while channel.readyState == "open":
start_time = time.perf_counter()
await asyncio.to_thread(engine.collect_damage)
patches = await asyncio.to_thread(engine.get_patches)
if patches:
try:
payload = json.dumps({"type": "patch", "p": patches})
channel.send(payload)
# logger.info(f"Sent {len(patches)} patches ({len(payload)} bytes)")
except Exception as e:
logger.error(f"Send failed: {e}")
break
await asyncio.sleep(max(0.01, 0.033 - (time.perf_counter() - start_time)))
except Exception as e:
logger.error(f"Stream Loop Error: {e}")
finally:
engine.close()
async def offer(request):
try:
params = await request.json()
pc = RTCPeerConnection(RTCConfiguration(iceServers=[
RTCIceServer(urls=["turns:turn.cloudflare.com:443?transport=tcp", "turn:turn.cloudflare.com:3478?transport=udp"],
username=TURN_USER, credential=TURN_PASS),
RTCIceServer(urls=["stun:stun.l.google.com:19302"])
]))
engine = X11Engine()
@pc.on("datachannel")
def on_dc(channel):
@channel.on("open")
def on_open(): asyncio.create_task(patch_stream_loop(channel, engine))
@channel.on("message")
def on_message(msg): engine.inject_input(json.loads(msg))
await pc.setRemoteDescription(RTCSessionDescription(sdp=params["sdp"], type=params["type"]))
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
return web.json_response({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}, headers={"Access-Control-Allow-Origin": "*"})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
async def index(request):
content = open("client2.html", "r").read()
return web.Response(content_type="text/html", text=content)
if __name__ == "__main__":
os.environ["DISPLAY"] = DISPLAY_NUM
# 1. Start Xvfb
logger.info("Starting Xvfb...")
subprocess.Popen(["Xvfb", DISPLAY_NUM, "-screen", "0", f"{WIDTH}x{HEIGHT}x24", "+extension", "DAMAGE", "-ac", "-noreset"])
time.sleep(2)
# 2. Start Fluxbox (Window Manager) - Fixes black screen issues
logger.info("Starting Fluxbox...")
subprocess.Popen(["fluxbox"], env=os.environ)
time.sleep(1)
# 3. Start Opera
logger.info("Starting Browser...")
cmd = "opera --no-sandbox --disable-gpu --disable-dev-shm-usage --start-maximized --window-size=1280,720 --window-position=0,0"
subprocess.Popen(cmd, shell=True, env=os.environ)
# 4. Start Server
app = web.Application()
app.router.add_get("/", index)
app.router.add_post("/offer", offer)
logger.info(f"Server running on port {PORT}")
web.run_app(app, host=HOST, port=PORT) |