mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 07:01:42 +01:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7550de169 | ||
|
|
b26e34ce09 | ||
|
|
ab4b58b1c7 | ||
|
|
74089fb2f1 |
No files matched your search
@@ -25,6 +25,8 @@ See what the headset sees, install games and Android apps, move files and text a
|
||||
|
||||
<sub>Unofficial hobby project, not affiliated with Valve. Free and open source.</sub>
|
||||
|
||||
[See what was tested on a real Frame, in a VM, or only in automated checks](docs/current-support-matrix.md).
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.25",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.25",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.24",
|
||||
"version": "0.4.25",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# What we have tested
|
||||
|
||||
Current for Frame Control **v0.4.25**, 25 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
|
||||
| Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof |
|
||||
|---|---|---|---|---|
|
||||
| Windows desktop in the headset | No physical Windows PC test | Windows 11 VM sent a whole desktop to a real Frame; this used an intermediate fix, not the final native DLL | Physical GPU, capture and controller input; individual windows unavailable in bundled DXGI build | 24 Oct, v0.4.10 installer + candidate fix: [VM log](evidence/windows-vm-2026-10-24.md), [PC guide](pc-in-headset.md) |
|
||||
| Linux desktop in the headset | No | No | Native Linux PC capture is disabled; lxso2 lacked a desktop portal and PipeWire session. The separate Steam `-pipewire` helper is not Frame Control's PC capture | 24 Oct, v0.4.17: [host audit](evidence/linux-pc-host-lxso2-2026-10-24.md), [Steam helper PR #163](https://github.com/saphid/frame-control/pull/163) |
|
||||
| Mac desktop in the headset | A physical Mac sent its screen/window to a real Frame | No | Current v0.4.25 Mac installer and worn-headset controller input have not been repeated | 28 Sep, pre-v0.4 / Frame OS 0.4.1: [Mac guide and measurement](streaming.md#b-show-the-macs-desktop-inside-the-frame) |
|
||||
| OBS headset source on Linux | Real Frame to OBS 30 on a Linux host; H.264 video and a synthetic 440 Hz output tone reached a saved recording | No | Real game audio, audible monitoring, perceptual A/V sync and awake headset view | 25 Oct, PR #174 head, shipped in v0.4.22: [OBS log](evidence/obs-media-source-real-2026-10-25.md) |
|
||||
| OBS headset source on Windows | No physical Windows PC test | No OBS/Frame VM end-to-end test | App Start/Stop path has automated Windows checks only | 25 Oct, v0.4.23: [Windows PR #179](https://github.com/saphid/frame-control/pull/179), [OBS guide](obs-headset-source.md) |
|
||||
| OBS headset source on macOS | No | No | Helper and OBS Media Source not tested on a Mac with a Frame | 25 Oct, v0.4.22: [OBS PR #174](https://github.com/saphid/frame-control/pull/174), [OBS guide](obs-headset-source.md) |
|
||||
| Wired connection doctor | No physical PC/Frame cable test of the doctor | No | Synthetic route/SSH tests; a separate Mac USB-C address observation does not test the doctor or game stream | 25 Oct, v0.4.23: [PR #178](https://github.com/saphid/frame-control/pull/178), [wired guide](wired-connection.md) |
|
||||
| Windows Wireless Adapter doctor | No physical Steam Wireless Adapter/dropout test | Windows 11 VM ran read-only checks without the adapter or WLAN | Adapter identification, dropouts and recovery on hardware | 24–25 Oct, v0.4.20–0.4.25: [VM log](evidence/windows-wireless-2026-10-24.md), [doctor guide](windows-wireless-doctor.md), [comparison PR #184](https://github.com/saphid/frame-control/pull/184) |
|
||||
| Linux Wireless Adapter doctor | No Steam Wireless Adapter hardware test | No | Read-only Linux host checks and mocked adapter states; no adapter-specific hardware observation | 24 Oct, v0.4.20: [PR #165](https://github.com/saphid/frame-control/pull/165), [doctor guide](linux-wireless-adapter-doctor.md) |
|
||||
| Switch PC windows | No physical Windows PC test | No real VM-to-Frame Alt+Tab check recorded | Explicit chord is covered by tests; actual window focus, keyboard layouts and controller interaction are unverified | 25 Oct, v0.4.24: [PR #181](https://github.com/saphid/frame-control/pull/181), [PC guide](pc-in-headset.md) |
|
||||
| Keyboard and trackpad from Frame Control | A Linux PC sent a click and ASCII keys to a temporary focused window on the real Frame | No | Worn-headset interaction, Unicode delivery and pixel-accurate pointing | 24 Oct, v0.4.19: [received-event log](evidence/pad-gamescope-2026-10-24.md), [PR #66](https://github.com/saphid/frame-control/pull/66) |
|
||||
| Backup and restore after reset | Same-headset backup, no-op save restore and Return to Game Mode checks | No | Restore **after a factory reset**, replacement restore and Android data restore have not been tested on a headset. No headset reset was done | 24 Oct, v0.4.17: [backup guide](backup-and-recovery.md), [Return to Game Mode log](evidence/return-game-mode-packaged-2026-10-24.json) |
|
||||
| Passthrough in recordings | No active-passthrough mirror check | No | Standby frame was uniform; whether visible room pixels enter `/dev/video99` is **unknown**. Raw cameras are never captured by Frame Control | 25 Oct, v0.4.21: [headset log](evidence/round3-headset-2026-10-25.md), [recording guide](recording.md#does-record-include-passthrough) |
|
||||
|
||||
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
|
||||
@@ -0,0 +1,58 @@
|
||||
# OBS relay and MP4 follow-up, 25 October 2026 UTC
|
||||
|
||||
This check used a Linux x86-64 host (`lxso2`) and the real Frame running
|
||||
SteamOS 0.5.6, build `20261022.6153909`. The host reached `ssh frame`. Before
|
||||
device work, the task atomically acquired `/tmp/frame-test.lock`, wrote its
|
||||
owner marker, and after cleanup removed only that marker and its lock. Battery
|
||||
was 99–100%; SteamVR 2.18.2 reported `activity_level: 3` (standby). Steam and
|
||||
SteamVR were left running. No passthrough session or game was started.
|
||||
|
||||
The production `scripts/obs-headset-source.py` relay reached its first local
|
||||
UDP packet in 0.65–1.15 seconds across several runs. A 12-second wall-clock
|
||||
capture of its MPEG-TS packets contained 325 H.264 frames at 1280×720,
|
||||
spanning 10.83 seconds of video timestamps: 30.0 fps. All 324 adjacent frame
|
||||
intervals were 33.333–33.334 ms, with none over 50 ms. The 826 captured TS
|
||||
packets had zero sync-byte, transport-error-indicator, or per-PID continuity
|
||||
errors. Six final TS packets arrived after the receiver's 12-second cutoff and
|
||||
were not included in those counts. Static standby pixels cannot establish
|
||||
whether a moving game would drop frames.
|
||||
|
||||
After the fix, the `--height 1080 --fps 60 --no-audio` fallback reached its
|
||||
first packet in 0.597 seconds. An eight-second wall-clock capture contained
|
||||
447 H.264 frames at 1920×1080 over 7.45 seconds of video timestamps: 60.0 fps.
|
||||
All frame intervals were 16.666–16.667 ms, none over 25 ms, and its 756 TS
|
||||
packets had zero continuity or transport-error-indicator errors. It had no
|
||||
audio stream, as requested. The helper exited 0 on Stop, and the owned lock
|
||||
was released.
|
||||
|
||||
The stream also contained 48 kHz stereo AAC. A 440 Hz synthetic tone played
|
||||
through the Frame's default speaker output with real-time pacing appeared in
|
||||
the capture for about 0.89 seconds; decoded audio had a -21.0 dB peak. This
|
||||
proves an audio signal reached the output monitor and OBS transport. It does
|
||||
not prove audible monitoring, real game audio, or audio/video sync: there was
|
||||
no simultaneous visible marker to compare with the tone.
|
||||
|
||||
The production MP4 recording commands were run separately for six seconds,
|
||||
then Stop, remux, and copy to the host completed. Stop returned after 7.27
|
||||
seconds from Start, including six seconds of recording; the remote ffmpeg
|
||||
exited 0. The copied 36,929-byte MP4 contained 188 H.264 frames at 1920×1080
|
||||
and 30 fps (6.266 seconds), plus 281 AAC packets (5.994 seconds). One decoded
|
||||
frame at two seconds had a single color, RGB `(22, 27, 53)`, across all
|
||||
2,073,600 pixels. The test's remote files were removed; the host copy stayed
|
||||
under `/tmp` for inspection.
|
||||
|
||||
The relay previously returned an error when the app deliberately closed its
|
||||
control pipe, because its SSH child commonly exits 255 after termination.
|
||||
The same test reproduced this (`helper_exit: 1`) on the real Frame. The follow-up
|
||||
fix recognizes deliberate Stop and adds bounded startup and stall timeouts,
|
||||
with fake-SSH regression tests. A new locked real-Frame check after the fix
|
||||
received its first 1,316-byte UDP datagram in 0.697 seconds and returned
|
||||
`helper_exit_after_stop: 0` after closing the app control pipe. The longer
|
||||
transport and MP4 measurements above were taken before the code change; the
|
||||
fix does not alter codecs or capture commands.
|
||||
|
||||
Prior [OBS 30 Media Source evidence](obs-media-source-real-2026-10-25.md)
|
||||
records a real Frame stream in OBS on Linux. This follow-up measured the relay
|
||||
with an FFmpeg receiver; OBS itself was not relaunched. Windows and macOS
|
||||
application paths, wearer-visible image latency, real game sound, audible
|
||||
playback, perceptual A/V sync, and active passthrough pixels remain unverified.
|
||||
@@ -27,6 +27,9 @@ Ctrl+C stops the checkout helper. The app closes a control pipe when you click
|
||||
Stop or quit; the helper then closes its SSH child, including on Windows. On
|
||||
Windows, install Python 3 and OpenSSH Client if Frame Control cannot find them.
|
||||
The Windows app Start/Stop path still needs a physical Windows/Frame test.
|
||||
If no first packet arrives within 20 seconds, or an active stream sends no
|
||||
packet for 10 seconds, the helper closes SSH and shows an error instead of
|
||||
leaving **Start OBS source** waiting or a frozen source running indefinitely.
|
||||
|
||||
To share the OBS view in a call, see the short
|
||||
[Discord setup guide](streaming-to-discord.md).
|
||||
@@ -49,3 +52,23 @@ default output was present in the saved audio. The headset was in standby, so
|
||||
the video showed the standby mirror. Nobody listened to OBS's monitoring output,
|
||||
and audio/video sync and active passthrough remain unverified. See the
|
||||
[test evidence](evidence/obs-media-source-real-2026-10-25.md).
|
||||
The [follow-up Linux/Frame transport check](evidence/obs-followup-2026-10-25.md)
|
||||
measured packet continuity and a short MP4 recording on the current SteamOS
|
||||
build. The headset was still in standby.
|
||||
|
||||
## Known issue: game audio in OBS
|
||||
|
||||
A Frame owner [reported](https://x.com/AzizahStellazio/status/2114147449468293171)
|
||||
that OBS **Game Capture / Application Audio Capture** can lose game audio when
|
||||
the headset turns on. OBS support attributed this to Valve/SteamVR. Frame
|
||||
Control cannot change that game-audio routing. If it happens, use Frame
|
||||
Control's **Start OBS source** and OBS **Media Source** steps above to capture
|
||||
the headset view and its output audio. You can also try OBS **Desktop Audio
|
||||
Capture** for the computer's output, then check its meter and a short test
|
||||
recording before you stream. Do not leave two active sources for the same audio
|
||||
unless you want duplicate sound.
|
||||
|
||||
The real Frame/OBS check above proved that a synthetic tone from the Frame's
|
||||
output reached the Media Source recording. It did **not** use a running game,
|
||||
listen to the audio, reproduce the reported Game Capture failure, or measure
|
||||
audio/video sync. Real game audio while the headset is awake remains unverified.
|
||||
@@ -52,6 +52,7 @@
|
||||
<body>
|
||||
<main>
|
||||
<h1>Frame Control 0.3.1: features by OS</h1>
|
||||
<p><strong>This is a September 2026 archive.</strong> For current coverage and hardware limits, read the <a href="current-support-matrix.md">v0.4.25 support matrix</a>.</p>
|
||||
<p class="sub">Which features are built for each OS, and which were tested against a real Steam Frame
|
||||
(SteamOS 0.3.0, build 20260922.6101926). Status as of 26 September 2026, for
|
||||
<a href="https://github.com/saphid/steam-frame/pull/2">PR #2</a> (0.3.1). Every build now bundles its own
|
||||
|
||||
@@ -11,10 +11,13 @@ import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
PACKET = 188
|
||||
DATAGRAM = PACKET * 7
|
||||
STARTUP_TIMEOUT = 20
|
||||
STREAM_STALL = 10
|
||||
|
||||
|
||||
def remote_command(height=720, fps=30, audio=True):
|
||||
@@ -37,7 +40,7 @@ def remote_command(height=720, fps=30, audio=True):
|
||||
f'{maps} {encode} -f mpegts -flush_packets 1 -')
|
||||
|
||||
|
||||
def relay(source, destination):
|
||||
def relay(source, destination, on_packet=None):
|
||||
"""Preserve 188-byte MPEG-TS packet boundaries in loopback datagrams."""
|
||||
pending = bytearray()
|
||||
total = 0
|
||||
@@ -46,6 +49,8 @@ def relay(source, destination):
|
||||
size = min(len(pending) // PACKET * PACKET, DATAGRAM)
|
||||
if size:
|
||||
destination.send(bytes(pending[:size]))
|
||||
if on_packet:
|
||||
on_packet()
|
||||
del pending[:size]
|
||||
total += size
|
||||
if pending:
|
||||
@@ -53,7 +58,7 @@ def relay(source, destination):
|
||||
return total
|
||||
|
||||
|
||||
def watch_control(stream, proc):
|
||||
def watch_control(stream, proc, stopped):
|
||||
"""A desktop app closes its pipe to stop SSH, including on Windows.
|
||||
|
||||
Windows TerminateProcess does not run Python's finally block. The pipe also
|
||||
@@ -63,10 +68,53 @@ def watch_control(stream, proc):
|
||||
stream.read(1)
|
||||
except OSError:
|
||||
pass
|
||||
stopped.set()
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
|
||||
|
||||
class StreamWatchdog:
|
||||
"""Close SSH if the first packet or a later packet takes too long."""
|
||||
|
||||
def __init__(self, proc, startup=None, stall=None):
|
||||
self.proc = proc
|
||||
self.startup = STARTUP_TIMEOUT if startup is None else startup
|
||||
self.stall = STREAM_STALL if stall is None else stall
|
||||
self.condition = threading.Condition()
|
||||
self.last_packet = time.monotonic()
|
||||
self.ready = False
|
||||
self.closed = False
|
||||
self.reason = ''
|
||||
self.thread = threading.Thread(target=self._watch, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def packet(self):
|
||||
with self.condition:
|
||||
self.ready = True
|
||||
self.last_packet = time.monotonic()
|
||||
self.condition.notify()
|
||||
|
||||
def close(self):
|
||||
with self.condition:
|
||||
self.closed = True
|
||||
self.condition.notify()
|
||||
self.thread.join(timeout=1)
|
||||
|
||||
def _watch(self):
|
||||
with self.condition:
|
||||
while not self.closed:
|
||||
left = self.last_packet + (self.stall if self.ready else self.startup) - time.monotonic()
|
||||
if left <= 0:
|
||||
self.reason = (f'The Frame sent no OBS video for {self.startup:g} seconds' if not self.ready
|
||||
else f'The Frame OBS video stopped for {self.stall:g} seconds')
|
||||
break
|
||||
self.condition.wait(left)
|
||||
else:
|
||||
return
|
||||
if self.proc.poll() is None:
|
||||
self.proc.terminate()
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--frame', default='frame', help='SSH host or alias (default: frame)')
|
||||
@@ -87,11 +135,17 @@ def main(argv=None):
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp:
|
||||
proc = subprocess.Popen(ssh, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE)
|
||||
stopped = threading.Event()
|
||||
watchdog = StreamWatchdog(proc)
|
||||
if args.control_stdin:
|
||||
threading.Thread(target=watch_control, args=(sys.stdin.buffer, proc), daemon=True).start()
|
||||
threading.Thread(target=watch_control, args=(sys.stdin.buffer, proc, stopped), daemon=True).start()
|
||||
try:
|
||||
amount = relay(proc.stdout, _Sender(udp, target))
|
||||
amount = relay(proc.stdout, _Sender(udp, target), watchdog.packet)
|
||||
code = proc.wait(timeout=10)
|
||||
if stopped.is_set():
|
||||
return 0
|
||||
if watchdog.reason:
|
||||
raise RuntimeError(watchdog.reason)
|
||||
if code or not amount:
|
||||
raise RuntimeError(f'Frame stream stopped (SSH exit {code}; {amount} bytes). '
|
||||
'Check SteamVR and the audio monitor; try --no-audio if needed.')
|
||||
@@ -99,6 +153,7 @@ def main(argv=None):
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
finally:
|
||||
watchdog.close()
|
||||
proc.stdout.close()
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
<div class="head">
|
||||
<span class="kicker">Download</span>
|
||||
<h2>Get Frame Control</h2>
|
||||
<p>Choose your computer’s OS and processor, not the headset’s. Python is bundled; Linux needs <code>ssh</code>, and arm64 Linux also needs your distribution’s <code>adb</code> for Android apps. <span hidden>Version <span data-version></span>.</span> <a href="https://github.com/saphid/frame-control/releases">Release notes</a></p>
|
||||
<p>Choose your computer’s OS and processor, not the headset’s. Python is bundled; Linux needs <code>ssh</code>, and arm64 Linux also needs your distribution’s <code>adb</code> for Android apps. <span hidden>Version <span data-version></span>.</span> <a href="https://github.com/saphid/frame-control/releases">Release notes</a> · <a href="https://github.com/saphid/frame-control/blob/main/docs/current-support-matrix.md">What we tested</a></p>
|
||||
</div>
|
||||
<div class="downloads">
|
||||
<div class="dl" id="download-mac" data-os="mac">
|
||||
|
||||
@@ -9,8 +9,10 @@ import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / 'scripts' / 'obs-headset-source.py'
|
||||
@@ -55,6 +57,29 @@ class ObsSourceTests(unittest.TestCase):
|
||||
self.assertIn('-f mpegts', with_audio)
|
||||
self.assertNotIn('@DEFAULT_MONITOR@', obs.remote_command(audio=False))
|
||||
|
||||
def test_watchdog_ends_a_stalled_child_on_every_host_platform(self):
|
||||
class Child:
|
||||
def __init__(self):
|
||||
self.stopped = threading.Event()
|
||||
|
||||
def poll(self):
|
||||
return 255 if self.stopped.is_set() else None
|
||||
|
||||
def terminate(self):
|
||||
self.stopped.set()
|
||||
|
||||
for ready in (False, True):
|
||||
with self.subTest(ready=ready):
|
||||
child = Child()
|
||||
watchdog = obs.StreamWatchdog(child, startup=0.1, stall=0.1)
|
||||
if ready:
|
||||
watchdog.packet()
|
||||
try:
|
||||
self.assertTrue(child.stopped.wait(1), 'a stalled SSH child was not terminated')
|
||||
self.assertIn('stopped' if ready else 'no OBS video', watchdog.reason)
|
||||
finally:
|
||||
watchdog.close()
|
||||
|
||||
@unittest.skipIf(sys.platform == 'win32', 'the packaged Start/Stop control is Linux/macOS only')
|
||||
def test_relay_stops_its_ssh_child_on_app_signal_or_control_pipe(self):
|
||||
for mode in ('signal', 'control-pipe'):
|
||||
@@ -70,7 +95,7 @@ class ObsSourceTests(unittest.TestCase):
|
||||
"import os, signal, sys, time\n"
|
||||
"def stop(*_):\n"
|
||||
" open(os.environ['FAKE_SSH_STOPPED'], 'w').close()\n"
|
||||
" sys.exit(0)\n"
|
||||
" sys.exit(255)\n"
|
||||
"signal.signal(signal.SIGTERM, stop)\n"
|
||||
"sys.stdout.buffer.write(b'G' * 1316)\n"
|
||||
"sys.stdout.buffer.flush()\n"
|
||||
@@ -112,6 +137,33 @@ class ObsSourceTests(unittest.TestCase):
|
||||
process.stdout.close()
|
||||
process.stderr.close()
|
||||
|
||||
@unittest.skipIf(sys.platform == 'win32', 'the stand-in SSH script uses a POSIX shebang')
|
||||
def test_startup_and_stream_stalls_end_ssh_with_a_clear_error(self):
|
||||
for mode in ('startup', 'stream'):
|
||||
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as folder:
|
||||
directory = Path(folder)
|
||||
fake = directory / 'ssh'
|
||||
fake.write_text("#!/usr/bin/env python3\n"
|
||||
"import os, signal, sys, time\n"
|
||||
"def stop(*_):\n"
|
||||
" open(os.environ['FAKE_SSH_STOPPED'], 'w').close()\n"
|
||||
" sys.exit(255)\n"
|
||||
"signal.signal(signal.SIGTERM, stop)\n"
|
||||
"if os.environ['FAKE_SSH_MODE'] == 'stream':\n"
|
||||
" sys.stdout.buffer.write(b'G' * 1316)\n"
|
||||
" sys.stdout.buffer.flush()\n"
|
||||
"while True: time.sleep(1)\n")
|
||||
fake.chmod(0o755)
|
||||
stopped = directory / 'stopped'
|
||||
env = {'PATH': str(directory) + os.pathsep + os.environ.get('PATH', ''),
|
||||
'FAKE_SSH_STOPPED': str(stopped), 'FAKE_SSH_MODE': mode}
|
||||
with mock.patch.dict(os.environ, env), \
|
||||
mock.patch.object(obs, 'STARTUP_TIMEOUT', 0.3), \
|
||||
mock.patch.object(obs, 'STREAM_STALL', 0.3):
|
||||
with self.assertRaisesRegex(RuntimeError, 'no OBS video' if mode == 'startup' else 'video stopped'):
|
||||
obs.main(['--frame', 'fake-frame'])
|
||||
self.assertTrue(stopped.exists(), 'SSH survived the relay timeout')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user