mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 02:00:19 +02:00
Merge pull request #49 from saphid/theatre-media-device-fixes
Media player: keep playing through headset standby (real-Frame test fixes)
This commit is contained in:
5 files changed
+269
-15
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -75,6 +75,34 @@ All test media were generated by us. No paid content or DRM was involved.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
**Verified end to end, 2026-09-29** (same build; headset unworn): uploads
|
||||||
|
through the HTTP API, the web UI and `scripts/push-vr-video.sh`, all played by
|
||||||
|
the owned player. Our generated test files:
|
||||||
|
|
||||||
|
| Case | Result |
|
||||||
|
|---|---|
|
||||||
|
| H.264 half-SBS 1920×1080 with AAC, theatre | 240/240 frames in 8.09 s; audio stream "Frame Control Media" in PulseAudio |
|
||||||
|
| H.265 half-OU 1920×1080 | 180/180 frames in 6.03 s; red left eye, cyan right |
|
||||||
|
| H.264 full-SBS 3840×1080, `stereo_mode=left_right` only | Detected from metadata; 150/150 frames in 5.03 s |
|
||||||
|
| H.264 1280×720, explicit 2D | 150/150 frames in 5.02 s |
|
||||||
|
| SBS PNG, OU JPEG (theatre) | Correct eye in each capture |
|
||||||
|
| 3,000-Gaussian `.splat` | Rendered in about 5 s, then held until Stop |
|
||||||
|
| 2D file on Auto, `_SBS_OU` file, HEIC, VP9 | Refused with the documented message |
|
||||||
|
| Second Play while one runs | Refused: "Stop the current media…" |
|
||||||
|
|
||||||
|
Stop always left the unit inactive, and no player process remained.
|
||||||
|
|
||||||
|
**Standby (verified):** an unworn Frame turns its displays off a few seconds
|
||||||
|
after it wakes. `SetOverlayRaw` then returns `RequestFailed` (23). The
|
||||||
|
first run's movie died there. The player now drops frames while the headset
|
||||||
|
is in standby, keeps the audio and its clock going, and resumes the picture
|
||||||
|
when the headset wakes. The 8 s movie above dropped 40 frames and finished.
|
||||||
|
Stills and the theatre surround are re-sent after waking. Five minutes
|
||||||
|
without an accepted frame is reported as an error. Headset-view captures
|
||||||
|
taken during standby show a flat dark frame, not our screen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
**Verified failed route:** GStreamer 1.24.2's `playbin` selected
|
**Verified failed route:** GStreamer 1.24.2's `playbin` selected
|
||||||
`v4l2h264dec`, delivered the first RGBA sample and then segfaulted (exit 139)
|
`v4l2h264dec`, delivered the first RGBA sample and then segfaulted (exit 139)
|
||||||
in the basic appsink probe and the OpenVR probe. We do not ship that route.
|
in the basic appsink probe and the OpenVR probe. We do not ship that route.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"""Owned media planning, eye isolation, decoder choice and fake-Frame ownership."""
|
"""Owned media planning, eye isolation, decoder choice and fake-Frame ownership."""
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
@@ -16,6 +19,13 @@ import frame_splat as splat
|
|||||||
import server
|
import server
|
||||||
|
|
||||||
|
|
||||||
|
def stop_now():
|
||||||
|
"""What systemd's SIGTERM does to the player, without signalling the test process."""
|
||||||
|
handler = signal.getsignal(signal.SIGTERM)
|
||||||
|
if callable(handler):
|
||||||
|
handler(signal.SIGTERM, None)
|
||||||
|
|
||||||
|
|
||||||
class Media(unittest.TestCase):
|
class Media(unittest.TestCase):
|
||||||
def test_layout_evidence_and_override(self):
|
def test_layout_evidence_and_override(self):
|
||||||
for name, layout in [('film_SBS.mp4', 'sbs'), ('film.OU.mkv', 'ou'),
|
for name, layout in [('film_SBS.mp4', 'sbs'), ('film.OU.mkv', 'ou'),
|
||||||
@@ -56,6 +66,156 @@ class Media(unittest.TestCase):
|
|||||||
self.assertNotIn('-re', cmd)
|
self.assertNotIn('-re', cmd)
|
||||||
self.assertIn('-frames:v', cmd)
|
self.assertIn('-frames:v', cmd)
|
||||||
|
|
||||||
|
def play_with(self, name, busy=0, on_pixels=None, sleeps=None, info=None,
|
||||||
|
fail=None, layout='auto'):
|
||||||
|
"""Run the player against a fake OpenVR; returns (status, pixels calls).
|
||||||
|
|
||||||
|
Handle 0 is the theatre surround and 1 the screen. `fail(handle, n)` makes
|
||||||
|
the n-th upload busy; every status written is kept in self.writes."""
|
||||||
|
calls = []
|
||||||
|
self.writes = []
|
||||||
|
write_status = player.write_status
|
||||||
|
|
||||||
|
def record(path, **values):
|
||||||
|
self.writes.append(values)
|
||||||
|
write_status(path, **values)
|
||||||
|
|
||||||
|
class FakeOverlay:
|
||||||
|
def create(self, *a, **k):
|
||||||
|
return len(calls)
|
||||||
|
|
||||||
|
def call(self, *a):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pixels(self, handle, data, w, h):
|
||||||
|
calls.append((handle, w, h))
|
||||||
|
if on_pixels:
|
||||||
|
on_pixels(len(calls))
|
||||||
|
if len(calls) <= busy or (fail and fail(handle, len(calls))):
|
||||||
|
raise player.OverlayBusy('standby')
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
# A Stop landing during cleanup must be ignored, not become an error.
|
||||||
|
# Call the installed handler directly: a real SIGTERM kills Windows.
|
||||||
|
stop_now()
|
||||||
|
|
||||||
|
frame = bytes(4*2*4)
|
||||||
|
proc = unittest.mock.MagicMock()
|
||||||
|
proc.stdout = io.BytesIO(frame*4)
|
||||||
|
proc.wait.return_value = 0
|
||||||
|
proc.poll.return_value = 0
|
||||||
|
old = signal.getsignal(signal.SIGTERM), signal.getsignal(signal.SIGINT)
|
||||||
|
with tempfile.TemporaryDirectory() as d, \
|
||||||
|
patch.object(player, 'Overlay', FakeOverlay), \
|
||||||
|
patch.object(player, 'probe', return_value=(info or {'codec_name': 'h264', 'width': 4, 'height': 2}, False)), \
|
||||||
|
patch.object(player.subprocess, 'Popen', return_value=proc), \
|
||||||
|
patch.object(player, 'write_status', side_effect=record), \
|
||||||
|
patch.object(player.frame_splat, 'render', return_value=(bytes(4*4*2), 4, 2)), \
|
||||||
|
patch.object(player.time, 'sleep', side_effect=sleeps):
|
||||||
|
path = Path(d)/name
|
||||||
|
path.write_bytes(b'x')
|
||||||
|
status = Path(d)/'status.json'
|
||||||
|
try:
|
||||||
|
player.play(argparse.Namespace(file=str(path), layout=layout, theatre=True, status=str(status)))
|
||||||
|
finally:
|
||||||
|
signal.signal(signal.SIGTERM, old[0])
|
||||||
|
signal.signal(signal.SIGINT, old[1])
|
||||||
|
return json.loads(status.read_text()), calls
|
||||||
|
|
||||||
|
def test_video_survives_standby_and_stop_after_end_stays_ended(self):
|
||||||
|
# Verified 2026-09-29: an unworn Frame enters standby within seconds and
|
||||||
|
# SetOverlayRaw then returns RequestFailed (23) until it wakes.
|
||||||
|
result, calls = self.play_with('clip_SBS.mp4', busy=3)
|
||||||
|
self.assertEqual((result['state'], result['frames']), ('ended', 4))
|
||||||
|
# Two video frames were dropped; the surround (handle 0) waited and was re-sent.
|
||||||
|
self.assertEqual(result['dropped'], 2)
|
||||||
|
self.assertIn((0, 1, 1), calls[3:])
|
||||||
|
|
||||||
|
def test_stop_mid_video_reports_stopped(self):
|
||||||
|
result, _ = self.play_with('clip_SBS.mp4', on_pixels=lambda n: n == 3 and stop_now())
|
||||||
|
self.assertEqual(result['state'], 'stopped')
|
||||||
|
|
||||||
|
def test_video_errors_when_steamvr_never_takes_frames(self):
|
||||||
|
with patch.object(player, 'BUSY_LIMIT', -1), \
|
||||||
|
self.assertRaisesRegex(RuntimeError, 'stopped accepting frames'):
|
||||||
|
self.play_with('clip_SBS.mp4', busy=99)
|
||||||
|
|
||||||
|
def test_still_waits_out_standby_without_a_limit(self):
|
||||||
|
# Stills have no timeline: keep retrying (here past BUSY_LIMIT) until shown.
|
||||||
|
ticks = iter(range(10))
|
||||||
|
def sleep(_):
|
||||||
|
if next(ticks) == 8:
|
||||||
|
stop_now()
|
||||||
|
with patch.object(player, 'BUSY_LIMIT', -1):
|
||||||
|
result, calls = self.play_with('photo_SBS.png', busy=5, sleeps=sleep,
|
||||||
|
info={'codec_name': 'png', 'width': 4, 'height': 2})
|
||||||
|
self.assertEqual(result['state'], 'stopped')
|
||||||
|
screen = [c for c in calls if c[0] == 1]
|
||||||
|
self.assertGreater(len(screen), 1) # retried through standby
|
||||||
|
self.assertEqual(calls[-1], (0, 1, 1)) # surround drained once the screen took a frame
|
||||||
|
|
||||||
|
def still_with_late_surround(self, name, **kw):
|
||||||
|
# The screen takes its first frame while the surround is still refused
|
||||||
|
# (its first upload, the drain right after the screen, and one retry).
|
||||||
|
ticks = iter(range(10))
|
||||||
|
def sleep(_):
|
||||||
|
if next(ticks) == 5:
|
||||||
|
stop_now()
|
||||||
|
surround_tries = []
|
||||||
|
def fail(handle, n):
|
||||||
|
if handle == 0:
|
||||||
|
surround_tries.append(n)
|
||||||
|
return len(surround_tries) <= 3
|
||||||
|
return False
|
||||||
|
result, calls = self.play_with(name, sleeps=sleep, fail=fail, **kw)
|
||||||
|
self.assertEqual(result['state'], 'stopped')
|
||||||
|
screen = [c for c in calls if c[0] == 1]
|
||||||
|
surround = [c for c in calls if c[0] == 0]
|
||||||
|
self.assertEqual(len(screen), 1) # shown once, not re-sent every second
|
||||||
|
self.assertEqual(len(surround), 4) # kept retrying after the screen, until it took
|
||||||
|
self.assertEqual(calls[-1][0], 0)
|
||||||
|
return calls
|
||||||
|
|
||||||
|
def test_photo_surround_recovers_after_screen_is_shown(self):
|
||||||
|
self.still_with_late_surround('photo_SBS.png',
|
||||||
|
info={'codec_name': 'png', 'width': 4, 'height': 2})
|
||||||
|
|
||||||
|
def test_splat_surround_recovers_after_screen_is_shown(self):
|
||||||
|
self.still_with_late_surround('scene.splat')
|
||||||
|
|
||||||
|
def test_video_standby_limit_is_five_minutes_without_an_accepted_frame(self):
|
||||||
|
self.assertEqual(player.BUSY_LIMIT, 300)
|
||||||
|
def run(times, busy):
|
||||||
|
# Upload n happens at times[n] seconds on a fake clock; 1 is the surround.
|
||||||
|
clock = [1000.0]
|
||||||
|
def on_pixels(n):
|
||||||
|
clock[0] = 1000.0 + times.get(n, times[max(times)])
|
||||||
|
with patch.object(player.time, 'monotonic', side_effect=lambda: clock[0]):
|
||||||
|
return self.play_with('clip_SBS.mp4', on_pixels=on_pixels,
|
||||||
|
fail=lambda h, n: h == 1 and n in busy)
|
||||||
|
# Busy for 299 s, then a frame lands: no error.
|
||||||
|
result, _ = run({1: 0, 2: 0, 3: 299, 4: 299, 5: 299}, busy={2, 3})
|
||||||
|
self.assertEqual((result['state'], result['dropped']), ('ended', 2))
|
||||||
|
# An accepted frame resets the timer: 600 s busy in total, never 300 s in a row.
|
||||||
|
result, _ = run({1: 0, 2: 0, 3: 200, 4: 250, 5: 450}, busy={2, 3, 5})
|
||||||
|
self.assertEqual((result['state'], result['dropped']), ('ended', 3))
|
||||||
|
# 301 s in a row without an accepted frame is an error.
|
||||||
|
with self.assertRaisesRegex(RuntimeError, 'stopped accepting frames for 300 s'):
|
||||||
|
run({1: 0, 2: 0, 3: 301}, busy={2, 3, 4, 5})
|
||||||
|
|
||||||
|
def test_status_reports_where_the_layout_came_from(self):
|
||||||
|
video = {'codec_name': 'h264', 'width': 4, 'height': 2}
|
||||||
|
for name, layout, tags, expect in [
|
||||||
|
('clip_SBS.mp4', 'auto', None, ('sbs', 'filename')),
|
||||||
|
('clip.mkv', 'auto', {'stereo_mode': 'left_right'}, ('full-sbs', 'metadata')),
|
||||||
|
('clip_OU.mp4', 'sbs', None, ('sbs', 'explicit')),
|
||||||
|
('clip.mkv', 'mono', {'stereo_mode': 'left_right'}, ('mono', 'explicit'))]:
|
||||||
|
with self.subTest(name=name, layout=layout):
|
||||||
|
self.play_with(name, layout=layout, info=dict(video, tags=tags) if tags else video)
|
||||||
|
playing = self.writes[0]
|
||||||
|
self.assertEqual(playing['state'], 'playing')
|
||||||
|
self.assertEqual((playing['layout'], playing['source']), expect)
|
||||||
|
|
||||||
def test_fake_frame_library_and_traversal(self):
|
def test_fake_frame_library_and_traversal(self):
|
||||||
with tempfile.TemporaryDirectory() as d, patch.object(remote, 'ROOT', Path(d)):
|
with tempfile.TemporaryDirectory() as d, patch.object(remote, 'ROOT', Path(d)):
|
||||||
identity = 'a'*32+'/space and quote\'.png'
|
identity = 'a'*32+'/space and quote\'.png'
|
||||||
|
|||||||
+80
-14
@@ -33,6 +33,15 @@ SLOTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OverlayBusy(RuntimeError):
|
||||||
|
"""SetOverlayRaw's RequestFailed (23): SteamVR isn't taking frames, e.g. the
|
||||||
|
unworn headset is in standby (verified 2026-09-29). Transient, not fatal."""
|
||||||
|
|
||||||
|
|
||||||
|
# A screen that can't take a frame for this long is broken, not asleep.
|
||||||
|
BUSY_LIMIT = 300
|
||||||
|
|
||||||
|
|
||||||
class Overlay:
|
class Overlay:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.handles = []
|
self.handles = []
|
||||||
@@ -53,6 +62,8 @@ class Overlay:
|
|||||||
def call(self, name, *values):
|
def call(self, name, *values):
|
||||||
slot, args = SLOTS[name]
|
slot, args = SLOTS[name]
|
||||||
rc = C.CFUNCTYPE(C.c_int, *args)(self.table[slot])(*values)
|
rc = C.CFUNCTYPE(C.c_int, *args)(self.table[slot])(*values)
|
||||||
|
if rc == 23 and name == 'SetOverlayRaw':
|
||||||
|
raise OverlayBusy('SteamVR is not accepting frames (standby?)')
|
||||||
if rc:
|
if rc:
|
||||||
raise RuntimeError('OpenVR %s failed: %s' % (name, rc))
|
raise RuntimeError('OpenVR %s failed: %s' % (name, rc))
|
||||||
|
|
||||||
@@ -75,11 +86,14 @@ class Overlay:
|
|||||||
self.call('ShowOverlay', handle)
|
self.call('ShowOverlay', handle)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
try:
|
# Best effort: SteamVR removes a disconnected client's overlays anyway,
|
||||||
for h in reversed(self.handles):
|
# and a teardown error must not overwrite a finished playback's status.
|
||||||
|
for h in reversed(self.handles):
|
||||||
|
try:
|
||||||
self.call('DestroyOverlay', h)
|
self.call('DestroyOverlay', h)
|
||||||
finally:
|
except RuntimeError:
|
||||||
self.vr.VR_ShutdownInternal()
|
pass
|
||||||
|
self.vr.VR_ShutdownInternal()
|
||||||
|
|
||||||
|
|
||||||
def probe(path):
|
def probe(path):
|
||||||
@@ -114,6 +128,11 @@ def decoder_command(path, info, width, height, audio, photo=False):
|
|||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def ignore_signals():
|
||||||
|
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
|
||||||
|
|
||||||
def write_status(path, **values):
|
def write_status(path, **values):
|
||||||
tmp = path.with_suffix('.tmp')
|
tmp = path.with_suffix('.tmp')
|
||||||
tmp.write_text(json.dumps(values))
|
tmp.write_text(json.dumps(values))
|
||||||
@@ -135,6 +154,46 @@ def play(args):
|
|||||||
photo = plan['kind'] == 'photo'
|
photo = plan['kind'] == 'photo'
|
||||||
command = decoder_command(path, info, width, height, audio, photo)
|
command = decoder_command(path, info, width, height, audio, photo)
|
||||||
vr, proc, frames, started = None, None, 0, time.monotonic()
|
vr, proc, frames, started = None, None, 0, time.monotonic()
|
||||||
|
dropped, busy_since, pending = 0, None, []
|
||||||
|
|
||||||
|
def show(handle, data, w, h, video=False):
|
||||||
|
"""Submit a frame. During standby return False; a video frame is dropped."""
|
||||||
|
nonlocal dropped, busy_since
|
||||||
|
try:
|
||||||
|
vr.pixels(handle, data, w, h)
|
||||||
|
except OverlayBusy:
|
||||||
|
if not video:
|
||||||
|
return False # stills and the surround just wait; nothing is lost
|
||||||
|
dropped += 1
|
||||||
|
busy_since = busy_since or time.monotonic()
|
||||||
|
if time.monotonic() - busy_since > BUSY_LIMIT:
|
||||||
|
raise RuntimeError('SteamVR stopped accepting frames for %d s' % BUSY_LIMIT)
|
||||||
|
return False
|
||||||
|
if video:
|
||||||
|
busy_since = None
|
||||||
|
drain()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def drain():
|
||||||
|
"""Re-send anything that arrived during standby (e.g. the theatre surround)."""
|
||||||
|
while pending:
|
||||||
|
item = pending.pop(0)
|
||||||
|
try:
|
||||||
|
vr.pixels(*item)
|
||||||
|
except OverlayBusy:
|
||||||
|
pending.insert(0, item)
|
||||||
|
return
|
||||||
|
|
||||||
|
def hold(handle, data, w, h):
|
||||||
|
"""Keep a still (photo or splat) up until Stop, retrying through standby."""
|
||||||
|
shown = False
|
||||||
|
while True:
|
||||||
|
if shown:
|
||||||
|
drain()
|
||||||
|
else:
|
||||||
|
shown = show(handle, data, w, h)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# systemd sends SIGTERM to the whole unit, including ffmpeg. Python unwinds
|
# systemd sends SIGTERM to the whole unit, including ffmpeg. Python unwinds
|
||||||
# ownership; no unrelated Steam/SteamVR process or setting is touched.
|
# ownership; no unrelated Steam/SteamVR process or setting is touched.
|
||||||
def stop(signum, frame):
|
def stop(signum, frame):
|
||||||
@@ -146,14 +205,13 @@ def play(args):
|
|||||||
if args.theatre:
|
if args.theatre:
|
||||||
surround = vr.create('framecontrol.media.surround', 40, 4, order=0)
|
surround = vr.create('framecontrol.media.surround', 40, 4, order=0)
|
||||||
vr.call('SetOverlayAlpha', surround, .85)
|
vr.call('SetOverlayAlpha', surround, .85)
|
||||||
vr.pixels(surround, b'\x00\x00\x00\xff', 1, 1)
|
if not show(surround, b'\x00\x00\x00\xff', 1, 1):
|
||||||
|
pending.append((surround, b'\x00\x00\x00\xff', 1, 1))
|
||||||
screen = vr.create('framecontrol.media.screen', 3 if args.theatre else 1.6, 2,
|
screen = vr.create('framecontrol.media.screen', 3 if args.theatre else 1.6, 2,
|
||||||
plan['layout'] != 'mono', aspect)
|
plan['layout'] != 'mono', aspect)
|
||||||
if splat:
|
if splat:
|
||||||
vr.pixels(screen, data, width, height)
|
|
||||||
write_status(status, state='playing', file=path.name, frames=1, **plan)
|
write_status(status, state='playing', file=path.name, frames=1, **plan)
|
||||||
while True:
|
hold(screen, data, width, height)
|
||||||
time.sleep(1)
|
|
||||||
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
|
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
|
||||||
video_start = time.monotonic()
|
video_start = time.monotonic()
|
||||||
while True:
|
while True:
|
||||||
@@ -163,11 +221,14 @@ def play(args):
|
|||||||
data, outw, outh = frame_media.stereo_pixels(data, width, height, plan['layout'])
|
data, outw, outh = frame_media.stereo_pixels(data, width, height, plan['layout'])
|
||||||
if not photo:
|
if not photo:
|
||||||
time.sleep(max(0, video_start + frames/30 - time.monotonic()))
|
time.sleep(max(0, video_start + frames/30 - time.monotonic()))
|
||||||
vr.pixels(screen, data, outw, outh)
|
if photo:
|
||||||
|
still = data, outw, outh
|
||||||
|
else:
|
||||||
|
show(screen, data, outw, outh, video=True)
|
||||||
frames += 1
|
frames += 1
|
||||||
if frames == 1 or frames % 30 == 0:
|
if frames == 1 or frames % 30 == 0:
|
||||||
write_status(status, state='playing', file=path.name, frames=frames,
|
write_status(status, state='playing', file=path.name, frames=frames,
|
||||||
seconds=time.monotonic()-started, **plan)
|
dropped=dropped, seconds=time.monotonic()-started, **plan)
|
||||||
if not photo:
|
if not photo:
|
||||||
time.sleep(max(0, video_start + frames/30 - time.monotonic()))
|
time.sleep(max(0, video_start + frames/30 - time.monotonic()))
|
||||||
rc = proc.wait(timeout=10)
|
rc = proc.wait(timeout=10)
|
||||||
@@ -176,12 +237,17 @@ def play(args):
|
|||||||
if not frames:
|
if not frames:
|
||||||
raise RuntimeError('Decoder produced no frames')
|
raise RuntimeError('Decoder produced no frames')
|
||||||
if photo:
|
if photo:
|
||||||
while True:
|
hold(screen, *still)
|
||||||
time.sleep(1)
|
# From here on a Stop can't change the outcome; don't let it turn
|
||||||
write_status(status, state='ended', frames=frames, seconds=time.monotonic()-started)
|
# 'ended' into an error while we write status and clean up.
|
||||||
|
ignore_signals()
|
||||||
|
write_status(status, state='ended', frames=frames, dropped=dropped,
|
||||||
|
seconds=time.monotonic()-started)
|
||||||
except InterruptedError:
|
except InterruptedError:
|
||||||
write_status(status, state='stopped', frames=frames)
|
ignore_signals()
|
||||||
|
write_status(status, state='stopped', frames=frames, dropped=dropped)
|
||||||
finally:
|
finally:
|
||||||
|
ignore_signals()
|
||||||
if proc:
|
if proc:
|
||||||
if proc.poll() is None:
|
if proc.poll() is None:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ def run(body):
|
|||||||
'--property=StandardOutput=append:'+str(RUNTIME/'player.log'),
|
'--property=StandardOutput=append:'+str(RUNTIME/'player.log'),
|
||||||
'--property=StandardError=append:'+str(RUNTIME/'player.log'),
|
'--property=StandardError=append:'+str(RUNTIME/'player.log'),
|
||||||
'python3', str(RUNTIME/'frame_media_player.py'), str(path),
|
'python3', str(RUNTIME/'frame_media_player.py'), str(path),
|
||||||
'--layout', plan['layout'], '--status', str(STATUS)]
|
'--layout', body.get('layout', 'auto'), '--status', str(STATUS)]
|
||||||
if body.get('theatre'):
|
if body.get('theatre'):
|
||||||
command.append('--theatre')
|
command.append('--theatre')
|
||||||
started = subprocess.run(command, capture_output=True, text=True, timeout=15)
|
started = subprocess.run(command, capture_output=True, text=True, timeout=15)
|
||||||
|
|||||||
Reference in new issue
Block a user