#!/usr/bin/env python3 """The stage message must go up once per cue, not on every resync or pause/resume. ProPresenter re-shows (flashes) the stage message on every PUT, even with the same text. The bridge used to re-send it every resync_seconds and on each play/pause flip, so a stalling Resi feed made the stage message flash over and over. Runs the real loop against a FAKE ProPresenter on localhost -- the real one is never touched. About 40 seconds. python3 test_stage_message.py """ import http.server import json import threading import time import resi_to_propresenter as R CALLS = [] class FakePP(http.server.BaseHTTPRequestHandler): def _ok(self): n = int(self.headers.get("Content-Length") or 0) body = self.rfile.read(n) if n else b"" CALLS.append((time.time(), self.command, self.path, body.decode())) reply = b"" if self.path == "/v1/timers": # the startup check looks for the timer by name reply = json.dumps([{"id": {"name": "NextCue"}}]).encode() self.send_response(200) self.end_headers() self.wfile.write(reply) do_GET = do_PUT = do_DELETE = _ok def log_message(self, *a): pass t0 = time.time() def fmt(ms): ms = int(max(0, ms)) return "%02d:%02d:%02d.%03d" % (ms // 3600000, (ms % 3600000) // 60000, (ms % 60000) // 1000, ms % 1000) class FakeResi: def __init__(self, *a, **k): self.customer_id = "C" def login(self): pass def venues(self): return [{"uuid": "V1"}] def players(self, venue_uuid): media = 10 * 60000 + int(time.time() - t0) * 1000 # moves every second return [{"playerHardwareId": "DEC1", "userName": "Test", "position": fmt(media), "eventId": "EV1"}] def event(self, event_id): return {"name": "SIM", "eventProfileId": "P1"} def cues(self, profile_id, event_id): return [{"name": "Message", "ms": 30 * 60000}] # one cue, far away def run_for(seconds, stage_message, flap): """Run the real loop; with flap, playback reads as paused/playing every 3s -- what a stalling Resi feed looks like to the loop.""" del CALLS[:] server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), FakePP) threading.Thread(target=server.serve_forever, daemon=True).start() real_read = R.Playhead.read def flapping(self): est, playing, cadence = real_read(self) if flap and int(time.time() - t0) // 3 % 2: return est, False, cadence return est, playing, cadence R.Playhead.read = flapping R.Resi = FakeResi R.keychain_password = lambda account: "x" cfg = dict(R.DEFAULTS) cfg.update({"resi_user": "t@example.com", "player": "DEC1", "propresenter": "http://127.0.0.1:%d" % server.server_address[1], "timer_name": "NextCue", "poll_seconds": 0.5, "resync_seconds": 2, "stage_message": stage_message}) # The loop's only exit is an exception from its sleep; BaseException gets past its # catch-all, so the end of the scenario stops it cleanly. stop = threading.Event() class Done(BaseException): pass def sleep(s): if stop.wait(s): raise Done() def worker(): try: R.run(cfg, False) except Done: pass R.time = type("T", (), {"time": staticmethod(time.time), "sleep": staticmethod(sleep), "strftime": staticmethod(time.strftime)}) th = threading.Thread(target=worker, daemon=True) th.start() time.sleep(seconds) stop.set() th.join(5) R.time = time R.Playhead.read = real_read server.shutdown() return list(CALLS) def count(calls, method, path): return sum(1 for c in calls if c[1] == method and c[2] == path) def main(): failures = [] calls = run_for(12, stage_message=True, flap=False) arms = count(calls, "PUT", "/v1/timer/NextCue/start") msgs = count(calls, "PUT", "/v1/stage/message") print("steady play, resync every 2s: %d timer pushes, %d stage-message sends" % (arms, msgs)) if arms < 4: failures.append("sanity: the timer should have been resynced several times (%d)" % arms) if msgs != 1: failures.append("steady play sent the stage message %d times, want 1" % msgs) calls = run_for(16, stage_message=True, flap=True) stops = count(calls, "GET", "/v1/timer/NextCue/stop") msgs = count(calls, "PUT", "/v1/stage/message") print("pause/resume every 3s: %d timer stops, %d stage-message sends" % (stops, msgs)) if stops < 2: failures.append("sanity: the flapping should have stopped the timer repeatedly (%d)" % stops) if msgs != 1: failures.append("pause/resume sent the stage message %d times, want 1" % msgs) calls = run_for(8, stage_message=False, flap=True) touched = [c for c in calls if c[2] == "/v1/stage/message"] print("stage_message off: %d stage-message calls" % len(touched)) if touched: failures.append("stage_message off still touched the stage message: %s" % touched) if failures: raise SystemExit("\nFAILED:\n - " + "\n - ".join(failures)) print("\nstage message tests passed") if __name__ == "__main__": main()