mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 04:04:21 +02:00
Mac in the headset: end the Frame's viewer browser after Stop; relay fixes
- Chromium on the Frame outlived its last viewer window (verified: 11 processes left after a run). Once nothing is shown, Stop ends it, unless Show was pressed again meanwhile; its profile is Frame Control's own. - Relay --delay: if the agent side fails, close the viewer side too (review round 9). - Docs: the final scroll run captured 57 fps; don't blame ScreenCaptureKit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
71200c5179
commit
7f769ca2f0
4 files changed
+49
-8
No files matched your search
@@ -204,7 +204,7 @@ about 5 ms to scroll compared with the baseline's way of measuring:
|
|||||||
| Scenario | Content | Input to drawn | fps drawn | Grades |
|
| Scenario | Content | Input to drawn | fps drawn | Grades |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| test | 10.5 / 16.7 | 29.6 / 36.3 | 60 | all within target |
|
| test | 10.5 / 16.7 | 29.6 / 36.3 | 60 | all within target |
|
||||||
| scroll | 19.8 / 27.4 | – | 55.9 | fps, late frames (4.8%) and worst gap (222 ms) only "acceptable": Wi-Fi stalls and ScreenCaptureKit's 46–53 fps from virtual displays |
|
| scroll | 19.8 / 27.4 | – | 55.9 | fps, late frames (4.8%) and worst gap (222 ms) only "acceptable": Wi-Fi stalls (the Mac captured 57 fps in this run; earlier runs got 46–53 from virtual displays) |
|
||||||
| type | 15.0 / 21.4 | 43.0 / 60.5 | – | all within target |
|
| type | 15.0 / 21.4 | 43.0 / 60.5 | – | all within target |
|
||||||
|
|
||||||
Of the targets, click to photon is met without the Frame's compositor (the
|
Of the targets, click to photon is met without the Frame's compositor (the
|
||||||
|
|||||||
@@ -172,11 +172,18 @@ class Relay:
|
|||||||
await writer.drain()
|
await writer.drain()
|
||||||
writer.close()
|
writer.close()
|
||||||
|
|
||||||
sender = asyncio.ensure_future(send())
|
async def pump():
|
||||||
while data := await reader.read(65536):
|
while data := await reader.read(65536):
|
||||||
await queue.put((self.loop.time() + self.delay, data))
|
await queue.put((self.loop.time() + self.delay, data))
|
||||||
await queue.put((0, None))
|
await queue.put((0, None))
|
||||||
await sender
|
|
||||||
|
sender, pumper = asyncio.ensure_future(send()), asyncio.ensure_future(pump())
|
||||||
|
done, _ = await asyncio.wait({sender, pumper}, return_when=asyncio.FIRST_COMPLETED)
|
||||||
|
if sender in done: # the agent's side went away: stop reading, so the connection closes
|
||||||
|
pumper.cancel()
|
||||||
|
writer.close()
|
||||||
|
return
|
||||||
|
await sender # the viewer's side ended: deliver what's queued first
|
||||||
|
|
||||||
async def _shaped(self, reader, writer): # agent -> Frame
|
async def _shaped(self, reader, writer): # agent -> Frame
|
||||||
queue = asyncio.Queue()
|
queue = asyncio.Queue()
|
||||||
@@ -461,7 +468,7 @@ def run_scenario(args, scenario, agent_port, token, frame_ssh):
|
|||||||
relay.loop.call_soon_threadsafe(relay.loop.stop)
|
relay.loop.call_soon_threadsafe(relay.loop.stop)
|
||||||
if chrome:
|
if chrome:
|
||||||
chrome.terminate()
|
chrome.terminate()
|
||||||
time.sleep(1.5)
|
time.sleep(3) # Stop ends the Frame's viewer browser 2 s later
|
||||||
|
|
||||||
|
|
||||||
def composited(f):
|
def composited(f):
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
@@ -82,6 +83,22 @@ class Helpers(unittest.TestCase):
|
|||||||
mv.show("display:1")
|
mv.show("display:1")
|
||||||
self.assertIn("Screen Recording", str(cm.exception))
|
self.assertIn("Screen Recording", str(cm.exception))
|
||||||
|
|
||||||
|
def test_stop_all_ends_the_viewer_browser_unless_shown_again(self):
|
||||||
|
calls = []
|
||||||
|
mv = frame_macview.MacView(["ssh"], lambda remote, **kw: calls.append(remote) or "", "frame")
|
||||||
|
mv.agent = mock.Mock(poll=lambda: None)
|
||||||
|
mv.call = lambda path, **kw: {"closed": 1}
|
||||||
|
mv.shown = {"window:5"}
|
||||||
|
with mock.patch.object(frame_macview.time, "sleep"):
|
||||||
|
gen = mv.shows
|
||||||
|
mv.shown.clear()
|
||||||
|
mv._end_viewer_browser(gen)
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertIn("pkill -f '[f]rame-control/mac-view", calls[0])
|
||||||
|
mv.shows += 1 # Show pressed during the wait: leave the new viewer alone
|
||||||
|
mv._end_viewer_browser(gen)
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
|
||||||
|
|
||||||
class WS:
|
class WS:
|
||||||
"""A minimal WebSocket client (masked frames out, plain frames in)."""
|
"""A minimal WebSocket client (masked frames out, plain frames in)."""
|
||||||
|
|||||||
+18
-1
@@ -128,6 +128,7 @@ class MacView:
|
|||||||
self.remote_port = None
|
self.remote_port = None
|
||||||
self.supervisor = None
|
self.supervisor = None
|
||||||
self.closing = False
|
self.closing = False
|
||||||
|
self.shows = 0 # counts Show presses, so a late cleanup can't close a new viewer
|
||||||
self.shown = set() # sources with a viewer out there, connected or retrying
|
self.shown = set() # sources with a viewer out there, connected or retrying
|
||||||
self.browser_flags = list(BROWSER_FLAGS)
|
self.browser_flags = list(BROWSER_FLAGS)
|
||||||
|
|
||||||
@@ -281,6 +282,7 @@ class MacView:
|
|||||||
def show(self, src, quality="balanced", width=None, height=None):
|
def show(self, src, quality="balanced", width=None, height=None):
|
||||||
if src != "test" and not src.startswith(("window:", "display:", "separate:")):
|
if src != "test" and not src.startswith(("window:", "display:", "separate:")):
|
||||||
raise MacViewError("Pick a window or display to show.")
|
raise MacViewError("Pick a window or display to show.")
|
||||||
|
self.shows += 1
|
||||||
q = QUALITY.get(quality) or QUALITY["balanced"]
|
q = QUALITY.get(quality) or QUALITY["balanced"]
|
||||||
state = self.call("/status")
|
state = self.call("/status")
|
||||||
if src != "test" and not state.get("screen"):
|
if src != "test" and not state.get("screen"):
|
||||||
@@ -325,7 +327,22 @@ class MacView:
|
|||||||
self.shown.clear()
|
self.shown.clear()
|
||||||
if not (self.agent and self.agent.poll() is None):
|
if not (self.agent and self.agent.poll() is None):
|
||||||
return {"closed": 0}
|
return {"closed": 0}
|
||||||
return self.call("/close", method="POST", **({"src": src} if src else {}))
|
out = self.call("/close", method="POST", **({"src": src} if src else {}))
|
||||||
|
if not self.shown:
|
||||||
|
threading.Thread(target=self._end_viewer_browser, args=(self.shows,), daemon=True).start()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _end_viewer_browser(self, shows):
|
||||||
|
"""Chromium on the Frame outlives its last viewer window (verified
|
||||||
|
2026-09-28), so once nothing is shown, end it. It runs with a profile
|
||||||
|
of its own, so nothing else is touched."""
|
||||||
|
time.sleep(2) # the viewers close their windows first
|
||||||
|
if self.shown or self.shows != shows:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.run("pkill -f '[f]rame-control/mac-view|[d]ata/frame-mac-view' || true", timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def state(self):
|
def state(self):
|
||||||
reason = self.unavailable()
|
reason = self.unavailable()
|
||||||
|
|||||||
Reference in new issue
Block a user