Sync Session Recorder & Player
A utility package for recording and playing back network events broadcast in an OTIO sync session. This allows for testing, debugging, and simulating active review sessions.
Features
- Session Recording: Capture all messages sent over a session’s RabbitMQ exchange (or UDP broadcast).
- Session Playback: Replay recorded events with accurate relative delays.
- Timestamp Updating: Automatically updates all payload timestamp fields (e.g.
sync_timestamp) to the current system time during playback. - Procedural and CLI APIs: Can be used as command-line tools or integrated directly into other applications’ event loops.
Command Line Usage
Ensure your Python virtual environment is active and sys.path is configured correctly, or run from the repository root.
Recording a Session
To record all events on a session named review-session to a file:
python -m sync_recorder.recorder --session review-session --output my_recording.jsonl
Options:
--session: The ID of the session to record (default:otio-sync-demo).--host: RabbitMQ host (default:127.0.0.1).--port: RabbitMQ port (default:5672).-o,--output: Path to write the output JSON Lines file (Required).--no-handshake: Disables initial state capture. By default, when starting, the recorder requests the current timeline snapshot from the session master and records it as the first event.--periodic-state: Periodically request a freshSTATE_SNAPSHOTfrom the master at settle points. Intended for thesync_testframework to validate live client state. Off by default.--min-silence SECONDS: Stream-silence required before an active periodic state request is issued (default:1.5). Only relevant with--periodic-state.--min-interval SECONDS: Minimum seconds between active periodic state requests (default:5.0). Only relevant with--periodic-state.
Replaying a Recording
To play back a recording into a session:
python -m sync_recorder.player --session review-session --input my_recording.jsonl
Options:
--session: The ID of the session to play back to (default:otio-sync-demo).--host: RabbitMQ host (default:127.0.0.1).--port: RabbitMQ port (default:5672).-i,--input: Path to the recording file to play back (Required).--speed: Playback speed multiplier, e.g.2.0plays twice as fast (default:1.0).--loop: Loops playback indefinitely.--keep-guids: Keeps original source GUIDs instead of replacing them with the player’s own unique GUID.--wait-for-peer: Hold playback until a peer has joined and received theSTATE_SNAPSHOT, then wait--post-snapshot-delayseconds before sending the first recorded event. The player will also start early if it detects peer activity before the delay expires.--post-snapshot-delay SECONDS: Seconds to wait after delivering the state snapshot before playback begins (default:3.0). Only used with--wait-for-peer.
Converting a Recording to an OTIO Timeline
To review a recorded session offline as a single, continuous clip, convert the recording into an OTIO timeline. The converter replays the recording’s playback, scrub, clip-switching, and drawing events over wall-clock time and reconstructs them as OTIO cuts:
python -m sync_recorder.convert_recording_to_timeline -i my_recording.jsonl -o my_timeline.otio
Options:
-i,--input: Path to the input recording.jsonl(Required).-o,--output: Path to write the output.otiotimeline (Required).--fps: Target frame rate for the output timeline (default:24.0).
How it works:
- Playback projection model — the converter maintains a live projection over the OTIO structure carried by the recording’s
STATE_SNAPSHOT(and any laterADD_TIMELINE). It treats eachcurrent_timeas a timeline/view frame and resolves it to a real media frame through the clip under the playhead (using the clip’ssource_range, or itsmedia_reference.available_rangewhensource_rangeisNone, e.g. media with embedded timecode). This is why a clip that reads as “frame 0” in the session correctly maps to its true media frame (such as98499). - Playing segments advance through the media at wall-clock rate, splitting into separate clips as the playhead crosses cuts in a multi-clip sequence, and wrapping to the sequence start when playback reaches the end in
loopmode. - Pause / scrub segments become freeze frames — a clip holding the resolved media frame via a
LinearTimeWarp(time_scalar=0.0)effect, stretched to the wall-clock duration of the hold. - Drawing events are rendered to transparent PNG overlays in a
<output-stem>_annotations/folder beside the.otio, laid out on a second “Annotations Overlay” track anchored to the same resolved media frames so overlays sit on the picture they were drawn on.
The output has a “Background Media” track and, when the session contained drawings, an “Annotations Overlay” track.
Session Initialization & Replay Handshake
For a joining peer (like an empty OpenRV session) to successfully apply replayed events, its internal timeline structure and GUIDs must match the recording. The package handles this automatically using a Master/Joiner handshake:
- Initial State Capture: When
SyncRecorderstarts, it automatically queries the active master for aSTATE_SNAPSHOTand records it at the beginning of the file. - Master Simulation: When
SyncPlayerplays back a recording that contains aSTATE_SNAPSHOT, it runs as a master simulator. It listens forWHO_IS_MASTERandSTATE_REQUESTmessages from new peers. When a joining peer requests state, the player dynamically intercepts the request and serves the recordedSTATE_SNAPSHOTtargeted to the peer’s GUID with updated timestamps. This initializes the peer with the correct timelines, tracks, and GUIDs, allowing subsequent annotations and playhead updates to apply perfectly.
Procedural API Usage
The tools can be integrated directly into other Python scripts, event loops, or plugins.
Using the Recorder
1. Background Thread Mode (Non-blocking)
import time
from sync_recorder import SyncRecorder
# Initialize the recorder
recorder = SyncRecorder(session_id="review-session")
# Start recording to a file in a background thread
recorder.start(output_file="session_log.jsonl")
# Let it run for a while
time.sleep(10.0)
# Stop recording and clean up network resources
recorder.stop()
2. Manual Tick Mode (Integrates with GUI/App Loops)
from sync_recorder import SyncRecorder
recorder = SyncRecorder(session_id="review-session")
# Start recording without a background thread
recorder.start(output_file="session_log.jsonl")
# Call in your application's idle or timer loop:
def on_idle_or_timeout():
new_events = recorder.tick()
for event in new_events:
print(f"Recorded event: {event['payload']['command']}")
Using the Player
1. Blocking Playback
from sync_recorder import SyncPlayer
player = SyncPlayer(session_id="review-session")
player.load_recording("session_log.jsonl")
# Plays back all events, blocks until finished
player.play(speed=1.0, loop=False)
2. Non-blocking Tick Mode (Integrates with GUI/App Loops)
import time
from sync_recorder import SyncPlayer
player = SyncPlayer(session_id="review-session")
player.load_recording("session_log.jsonl")
# Initialize the playback state
player.start_playback(speed=1.0, loop=False)
# Call repeatedly in your application's idle or timer loop:
# Return value is True if playback is still active, False if complete.
playing = True
while playing:
playing = player.tick()
time.sleep(0.01)
Running Unit Tests
Run the package tests using Python’s unit test runner from the repository root:
.venv/bin/python -m unittest tests/otio_sync/test_sync_recorder.py