• ReplayReader is an offline, simulator-independent API for observations, events, lifecycle records, camera media, and calibration.
  • plugin://fastsim/replay is a managed FastSim control_producer whose default render mode applies recorded visual state without control or physics; explicit resimulate mode submits portable authored ControlChunk records through a fresh control session.

The offline package does not import FastSim, UniRoboSim, Isaac Lab, MuJoCo, PyBullet, NumPy, or a rendering SDK.

Version 0.2.17 aligns the managed Replay extra with FastSim Core >=0.1.0a21,<0.2, Record 0.3.10, and fastsim-recording-format[images]==0.3.3. It validates and reconstructs bounded render.state trajectories during prepare and applies them on Core's paused render-frame clock as fast as valid. It also validates and reconstructs compact fluid.emissions batches into exact Core replay envelopes during prepare, then requires fluid.replay and loads the complete schedule before Runtime starts. A present empty fluid stream still loads (), disabling live emitters; an absent stream preserves legacy behavior. It also bounds recorded-preemption submissions, applies one absolute teardown deadline, retains failed cleanup handles for retry, and rejects authored control spanning generation resets. The offline facade borrows the format package's typed reader instead of maintaining a second decoder. Legacy rgb24-srgb, jpeg-rgb24-srgb, and png-rgb24-srgb camera streams all decode transparently to the same immutable RGBFrame value. It reads FSR binary 1.0 strong inventories, binary 1.1 fast frame digests, and the default binary 1.2 structural profile. Structural open validates the complete container layout and metadata while seeking over payload bytes. ReplayReader.file_sha256 keeps the same public return type but computes and caches the exact whole-file digest only when explicitly accessed. A pinned expected_sha256 is an explicit digest request and triggers that one lazy pass; an unpinned offline open does not. Release 0.2.7 added preservation of the recorded terminal timeline. The offline reader is verified against Record 0.3 camera streams, including RGB, intrinsics, world translation, world quaternion, and their shared calibration digest. In recorded_sim_time mode, Replay reads the formal terminal_semantic_cut from the recorded run.lifecycle stream and preserves the complete source simulation timeline after the final authored command. The final terminal policy therefore remains active for the same recorded tail instead of ending as soon as the last Chunk reports completion. Files created before terminal cuts were recorded retain the previous behavior. The catalog keeps all manifests through 0.2.14 for existing locks, while new compilation selects 0.2.17; all three supported binary minors remain under FSR v1.

Installation

Python 3.11 and 3.12 are supported.

bash
python -m pip install \
  "fastsim-recording-format[images]==0.3.3" \
  "fastsim-plugin-replay==0.2.17"

This installs the SDK-free offline reader. For managed command Replay, install the managed extra; it uses the reviewed compatible Core alpha range with the next minor line as a review boundary:

bash
python -m pip install "fastsim-plugin-replay[managed]==0.2.17"

For local source development:

bash
python -m pip install ./packages/fastsim-recording-format
python -m pip install ./packages/fastsim-plugin-replay

The managed plugin is discovered through the fastsim.plugins and fastsim.components entry-point groups. FastSim Core remains outside the base runtime dependencies so an offline inspection environment stays SDK-free; the managed extra adds the bounded Core dependency required for command Replay.

Offline ReplayReader

Opening a file validates its FSR header, canonical metadata, inventory projections, frame ranges, sequence and clock continuity, index, footer, truncation, and trailer before exposing any record. Digest checks depend on the file profile; structural files do not claim that payload bytes are tamper-evident.

python
from fastsim_plugin_replay import ReplayReader

with ReplayReader.open("run.fsr", max_bytes=64 * 1024**3) as replay:
    print(replay.footer.primary_integrity)

    sample = replay.seek(
        "observations.robot.joints",
        generation=1,
        tick=120,
        mode="at_or_after",
    )
    if sample is not None:
        print(sample.metadata, sample.decoded)

    for frame in replay.iter_media():
        print(frame.ref.sim_time_s, frame.decoded)

    # Optional: this explicit request performs one lazy whole-file digest pass.
    print(replay.file_sha256)

Seek is available by generation, simulation tick, or simulation time. Legacy raw RGB24 plus Record 0.3.6 JPEG and PNG camera payloads are decoded only when their stream definitions declare a known, self-describing codec. All three return the canonical fastsim_recording_format.RGBFrame. Unknown codecs, inconsistent shape/dtype metadata, non-finite numeric data, duplicate JSON keys, excessive JSON nesting, and oversized values fail closed.

Path-based opening uses a non-following, nonblocking POSIX open, accepts only regular files, pins the file descriptor, and optionally compares a compiled SHA-256 digest with one lazily computed exact whole-file digest. FIFO, device, final-component symlink, and post-resolution pathname-substitution inputs are rejected. Callers may also construct ReplayReader from an already opened binary port and retain ownership of that port. Managed preparation performs this work on one Replay-instance-owned worker. Cancellation is checked before every bounded read and seek, then the worker is joined and its pinned descriptor is closed before cancellation returns.

Managed render Replay

Managed Replay defaults to mode: render. It requires a source FSR containing the optional Record render.state trajectory, starts Core paused, and loads frames through the public render-state application boundary. Each load commits the source simulation tick and sim_time_s, triggers normal observation and camera-media participants, and does not open a control session, execute a controller, call the normal physics step, or sleep for wall-clock pacing. Frames are applied sequentially as fast as the target can settle them.

Run Replay and Record together to produce a new FSR:

yaml
plugins:
  replay:
    use: plugin://fastsim/replay
    config:
      mode: render
      source:
        uri: ./recordings/source.fsr
        sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
      timing: {mode: as_fast_as_valid, speed: 1.0}

  record:
    use: plugin://fastsim/record
    config:
      output: {name: rendered.fsr}
      streams:
        observations: {mode: none, include: {}}
        render_state: {enabled: true, rate_hz: 30}
        cameras:
          overview:
            rgb: sensors.overview.camera.rgb
            intrinsics: sensors.overview.camera.intrinsics
            translation: sensors.overview.camera.extrinsics.translation
            quaternion_xyzw: sensors.overview.camera.extrinsics.quaternion_xyzw
            required: true
            every_n_samples: 1
            encoding: jpeg
            quality: 92

See examples/render-replay/run.yaml for the complete Run skeleton. render.max_frames, render.max_bytes, and render.completion_timeout_s bound prepare-time allocation and execution. Record authenticates Replay's prepared event and writes the source FSR SHA-256 and mode: render into the new FSR footer. A legacy command-only FSR fails with an actionable error; select mode: resimulate explicitly to use command Replay.

Managed command Replay (mode: resimulate)

The component identity is plugin://fastsim/replay. A normal plugin instance supplies the recording and can select sequential or recorded-preemption execution:

yaml
plugins:
  replay:
    use: plugin://fastsim/replay
    bindings:
      robot: droid
    access:
      control:
        robot: [arm, gripper]
    config:
      mode: resimulate
      source:
        uri: ./recordings/droid-source.fsr
        sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
      timing:
        mode: recorded_sim_time
        speed: 1.0
      execution:
        mode: sequential
        chunk_timeout_s: 120
        max_in_flight: 32
      input:
        max_bytes: 1099511627776

Compilation turns source.uri into the safe recording artifact selector. At runtime, Replay performs this complete preflight before opening a control session:

  1. resolve and rehash artifact://recording;
  2. enforce the configured input byte limit;
  3. reopen a pinned regular-file descriptor and verify the compiled digest again;
  4. validate the complete FSR inventory and footer;
  5. decode every authored command, reject duplicate IDs, and require one source generation;
  6. compare the source and target simulation contracts; and
  7. validate every target resource, controller, command space, axis, unit, frame, interpolation mode, semantic requirement, and terminal policy.

Replay then opens a new scoped control session. Each command receives a fresh run ID, session ID, generation, world dependency, observation reference, target entity/group, and chunk ID. Original identity and digest fields remain in provenance, while accepted record order and relative simulation timing are retained.

execution.mode: sequential waits for the fresh terminal ChunkResult before submitting the next chunk. recorded_preemption admits commands in source order and retains their relative simulation-time starts, allowing a later command to preempt an earlier one. Its rolling active window is bounded by execution.max_in_flight; a full window waits for its oldest terminal result before admitting another command. timing.mode: as_fast_as_valid removes source-time waiting while retaining admission order.

Cancellation, per-chunk cancellation tickets, heartbeat shutdown, and session close share one absolute execution.chunk_timeout_s teardown deadline. A failed close or completion request keeps its handle and success flag unset so an idempotent stop or close retry can finish cleanup.

With timing.mode: recorded_sim_time, Replay also maps the source run.lifecycle / terminal_semantic_cut timestamp onto the target clock. The scoped control session and its heartbeat stay active until that mapped time, so hold, zero, or controller-default terminal behavior continues through the recorded tail. This wait is cancellable and is bounded by execution.chunk_timeout_s; if an operator pauses the simulation, Replay waits without advancing the source clock and either resumes normally or fails at that configured bound. An older valid FSR without the lifecycle cut skips this step rather than being rejected.

Authored commands are not applied evidence

Command Replay accepts exactly this required stream contract:

Field Value
stream control.authored
schema fastsim-control/1
media type application/vnd.fastsim.control+json
payload codec fastsim-control-json-v1
attributes.kind control.authored

control.applied / fastsim-applied-control-frame/1 records describe what a backend actually received. They are evidence only and are never converted into Replay commands. A recording containing applied frames without the authored stream is rejected before control access.

Lifecycle behavior

Replay requests completion exactly once after all fresh terminal results are known:

  • succeeded / REPLAY_COMPLETED after all checks and results pass;
  • failed / REPLAY_FAILED after compatibility, admission, execution, or result failure; or
  • cancelled / REPLAY_CANCELLED after managed cancellation or Server replacement.

These are exact Core RunCompletionRequest values. The stable uppercase reason codes are validated at the lifecycle boundary rather than translated by a local look-alike type.

stop() and close() are idempotent. Active submitted chunks are cancelled and the fresh control session is closed during teardown.

Verification status

Phase 03A includes package tests for offline seek and decoding, full integrity failure, size limits, FIFO/device/symlink/path-substitution attacks, JSON bounds, applied-frame rejection, target preflight, fresh authority rebinding, sequential execution, recorded preemption, terminal-tail timing, paused-clock timeout, heartbeat continuity, cancellation, exact Core completion requests, exactly-once completion, Wheel contents, SDK-free isolated installation on Python 3.11 and 3.12, installed-Wheel managed discovery against the current Core Wheel, and absolute-time preemption against the real Core ControlChunkExecutor.

The remaining integrated release gate is outside this package checkpoint:

  • real DROID source recording plus Isaac Lab, MuJoCo, and PyBullet Replay/Record runs.

License and authorship

Author: Hofee lexhofee@gmail.com

The package currently uses LicenseRef-Pending until the repository license is finalized.