feat(capture): lock decision #1 and prove Xvfb+Chromium frame capture
Decision #1: watch the real Discord screen share by running a real web client under a virtual display (Xvfb) and capturing rendered frames — not bot/selfbot protocol receive. PLAN.md records all locked decisions and the step list. PoC (poc/capture_xvfb.py + test_page.html) launches system Chrome non-headless under Xvfb via Playwright and captures 6 changing, non-blank frames -> de-risks the display+capture chain on the .9 host.
This commit is contained in:
75
poc/capture_xvfb.py
Normal file
75
poc/capture_xvfb.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""PoC: prove that a non-headless Chromium under a virtual display (Xvfb) can
|
||||
render a live/animated page and that we can capture it as frames.
|
||||
|
||||
This de-risks the hardest unknown of decision #1 (watch the real Discord screen
|
||||
share by capturing a real client's rendered pixels) BEFORE touching Discord.
|
||||
|
||||
Run under a virtual display:
|
||||
|
||||
xvfb-run -a --server-args="-screen 0 1280x720x24" \
|
||||
.venv/bin/python poc/capture_xvfb.py
|
||||
|
||||
Success = frames are non-blank AND change over time (proving live rendering +
|
||||
capture actually work on this headless server).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
PAGE = (HERE / "test_page.html").resolve()
|
||||
OUT = HERE / "frames"
|
||||
N = 6
|
||||
INTERVAL = 0.4
|
||||
|
||||
|
||||
def main() -> int:
|
||||
OUT.mkdir(exist_ok=True)
|
||||
hashes: list[str] = []
|
||||
sizes: list[int] = []
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(
|
||||
channel="chrome", # use system google-chrome, no browser download
|
||||
headless=False, # WebRTC/video needs a real (virtual) display
|
||||
args=["--no-sandbox", "--disable-gpu", "--autoplay-policy=no-user-gesture-required"],
|
||||
)
|
||||
page = browser.new_page(viewport={"width": 1280, "height": 720})
|
||||
page.goto(PAGE.as_uri())
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
for i in range(N):
|
||||
png = page.screenshot() # captures the rendered virtual screen
|
||||
f = OUT / f"frame_{i:02d}.png"
|
||||
f.write_bytes(png)
|
||||
h = hashlib.sha256(png).hexdigest()[:12]
|
||||
hashes.append(h)
|
||||
sizes.append(len(png))
|
||||
print(f" frame {i}: {len(png):>7} bytes sha={h}")
|
||||
time.sleep(INTERVAL)
|
||||
|
||||
browser.close()
|
||||
|
||||
distinct = len(set(hashes))
|
||||
min_size = min(sizes)
|
||||
print(f"\n{N} frames, {distinct} distinct, min size {min_size} bytes")
|
||||
|
||||
# A blank/black 1280x720 PNG compresses to a few KB; a real rendered scene is
|
||||
# much larger. Require non-trivial size and that the scene actually moved.
|
||||
ok_nonblank = min_size > 8_000
|
||||
ok_changing = distinct >= 3
|
||||
if ok_nonblank and ok_changing:
|
||||
print("PoC PASS: Xvfb + Chromium rendered a live scene and we captured changing, non-blank frames.")
|
||||
return 0
|
||||
print(f"PoC FAIL: nonblank={ok_nonblank} changing={ok_changing}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user