Environment renderings
metrana.log_rendering records reinforcement-learning environment video. Each frame is appended to a
per-(env_id, episode, name) H.264 .mp4, encoded on background encoder threads so it never blocks the
training loop. Finalized videos are then uploaded to the portal (by default), where they surface in the
environment views alongside your metrics.
metrana.log_rendering(frame, rl_step, episode, env_id=None, *, name=None)Frame format
Section titled “Frame format”frame is a uint8 NumPy array:
(H, W, 3)— RGB(H, W)or(H, W, 1)— grayscale
Width and height must be even (a libx264 yuv420p requirement).
for t in range(episode_len): frame = env.render() # uint8 (H, W, 3) metrana.log_rendering(frame, rl_step=rl_step, episode=episode, env_id="env0")Multiple videos per episode
Section titled “Multiple videos per episode”Pass name to record several distinct videos of the same episode — for example, one per agent’s view when two
agents play the same game. Each distinct name becomes its own video, and the name is used verbatim as the
video’s display name in the portal:
for t in range(episode_len): for agent in ("agent_1", "agent_2"): frame = env.render(agent=agent) metrana.log_rendering(frame, rl_step=rl_step, episode=episode, env_id="env0", name=agent)The name identifies the video, so it is fixed for as long as the video is open: logging with a different name
always starts a different video. Without a name, the env records a single video named <env>/episode_<n>.
A name must be 1–256 characters and is keyword-only.
How episodes become files
Section titled “How episodes become files”Each (env_id, episode, name) gets its own encoder and its own MP4, written under the configured output
directory. An environment’s videos advance through episodes together: the first frame of a new episode on
any of an env’s videos closes every one of its videos still on an earlier episode (so a video that stops
receiving frames — say, an agent that finished early — is still finalized and uploaded promptly). Frames that
arrive afterwards for an earlier episode are dropped with a warning, since reopening that episode would
overwrite its finished video.
Frames route to encoder threads by env_id: every frame for a given env — across all its named videos — is
encoded in order by a single thread, while different envs encode in parallel (libx264 releases the
GIL). The number of encoder threads is rendering_num_encoder_threads (default min(4, cpu_count)); the
number of encoders open at once across all threads is capped globally by rendering_max_concurrent_encoders
(default 16). Every named video of every env holds its own encoder, so two agents’ views across eight envs
need 16 slots. When the cap is reached, frames for a new video are subject to the configured
backpressure/drop behavior until a slot frees.
Uploading to the portal
Section titled “Uploading to the portal”Finalized renderings are handed to a background upload stage — the training and encode threads are never
blocked on the network. Each video runs the full create → upload → commit flow, and uploads are idempotent
per (run, env_id, episode, name): a resumed or re-run job reuses the existing video instead of creating a
duplicate. Uploading authenticates with the run’s key from init().
upload_renderings on init() chooses what gets
uploaded:
upload_renderings |
Behavior |
|---|---|
"all" / True (default) |
Upload every finalized rendering. |
"triggered" |
Upload only renderings that accumulated a trigger event (see marking events); discard the rest locally. |
"off" / False |
Encode to local disk only — never upload. |
metrana.init( organization="my-org", workspace_name="my-workspace", project_name="my-project", run_name="run-1", upload_renderings="triggered", # only ship episodes worth watching)Renderings are never dropped because uploads lag: the backlog accumulates on disk (finalized files awaiting
upload), and a one-shot warning fires past rendering_upload_warn_threshold advising you to raise
rendering_upload_concurrency. By default the local .mp4 is deleted once its video commits
(rendering_delete_after_upload), so long multi-environment runs don’t fill the disk.
Bring your own bucket
Section titled “Bring your own bucket”Pass rendering_storage="s3://<bucket>/<prefix>" to keep the video bytes in an S3 bucket you own. Each
finalized rendering is uploaded to that bucket with your own AWS credentials (resolved from the ambient
boto3 chain — env vars, shared config, or instance/role — never sent to Metrana) and recorded on the video API
as a reference. Only metadata (the s3:// URI, digest, size, duration) reaches Metrana; the managed bucket
never receives the bytes.
metrana.init( organization="my-org", workspace_name="my-workspace", project_name="my-project", run_name="run-1", rendering_storage="s3://my-rl-videos/metrana", # your bucket)Objects are keyed by video id at <prefix>/<workspace>/videos/<video_id>.mp4. This path needs the s3 extra
(pip install 'metrana[s3]'); a bad URI or a missing extra fails fast in init(). Tagging, event marking, and
"triggered" mode all work identically — only where the bytes live changes.
Tagging renderings
Section titled “Tagging renderings”tag_rendering attaches a workspace video tag to a
rendering while it is still encoding — its video may not exist yet. The tag is buffered against
(env_id, episode) and applied (created-or-got by name) once the video commits.
if info["is_record"]: metrana.tag_rendering("personal-best", episode=episode, env_id="env0")By default the tag applies to every video of the episode, including named videos that only start later in
it. Pass name to tag just one:
metrana.tag_rendering("winner", episode=episode, env_id="env0", name="agent_2")It’s a no-op when uploading is disabled (upload_renderings="off"), since there’s no video to tag.
Marking events
Section titled “Marking events”mark_rendering_event records an event occurrence on the rendering at the current video frame — call it on
each frame the condition holds. Contiguous marks extend one occurrence; a gap closes it and opens the next. The
occurrences are buffered and applied against the committed video (event type created-or-got by
condition_name).
for t in range(episode_len): frame, info = env.render(), env.step(...) metrana.log_rendering(frame, rl_step=rl_step, episode=episode, env_id="env0") if info["collision"]: metrana.mark_rendering_event("collision", episode=episode, env_id="env0")With several named videos, a mark without a name lands on every video of the
episode that has frames so far, each at its own current frame (a mark before any of them has a frame lands
at frame 0 on all of them). A video whose first frame comes after the mark doesn’t get it, so log every video’s
frame for the step before marking. Pass name to mark just one:
if info["agent_1_hit"]: metrana.mark_rendering_event("hit", episode=episode, env_id="env0", name="agent_1")Set trigger=True to mark an occurrence as interesting. Under upload_renderings="triggered", a rendering
uploads only if it accumulated at least one trigger; everything else is discarded locally — the way to keep
just the episodes you care about:
if info["goal_reached"]: metrana.mark_rendering_event("goal", episode=episode, env_id="env0", trigger=True)A trigger is per video: an unnamed trigger uploads every video it marked, a named one uploads only that video.
Like tag_rendering, marking is a no-op when uploading is disabled.
Configuration
Section titled “Configuration”Renderings are configured on init():
| Argument | Default | Purpose |
|---|---|---|
rendering_output_dir |
None |
Where encoded MP4s are written. |
rendering_fps |
30 |
Output frame rate. |
rendering_num_encoder_threads |
min(4, cpu_count) |
Encoder threads. Each env is pinned to one thread; more threads = more envs encoding in parallel. |
rendering_max_concurrent_encoders |
16 |
Global cap on encoders open at once, across all threads. |
rendering_queue_max_size |
None |
Bound on each encoder thread’s frame queue. |
upload_renderings |
True |
"all"/True, "triggered", or "off"/False (see above). |
rendering_upload_concurrency |
4 |
Videos uploaded in parallel. |
rendering_upload_warn_threshold |
100 |
Pending-upload backlog depth at which a one-shot warning fires. |
rendering_delete_after_upload |
True |
Delete the local .mp4 once its video commits. Does not apply to non-triggered renderings under upload_renderings="triggered" — those are always discarded (see above). |
rendering_upload_close_timeout |
None |
Seconds to drain pending uploads on close(). None waits indefinitely. |
rendering_storage |
None |
s3://bucket/prefix to store bytes in your own bucket (needs the s3 extra). |
skip_drain_render_on_close |
None |
If True, close() drops queued frames instead of encoding them. None reads METRANA_SKIP_DRAIN_RENDER_ON_CLOSE. |
rendering_close_timeout |
None |
How long close() waits to drain queued frames. |
Error handling
Section titled “Error handling”Frame-encoding and upload errors are governed by rendering_error_strategy ("silent", "warn"
(default), "raise_on_log", "raise_on_close") — a separate knob from the engine’s delivery policy. It
controls only the Python rendering pipeline; metric/attribute delivery is governed by delivery_policy
(see Guarantees & retries). An upload failure
leaves the local file in place for a later/manual retry rather than losing it.
Closing with renderings
Section titled “Closing with renderings”By default close() drains and encodes any queued frames, then drains pending uploads, before returning.
Frame encoding and upload can lag well behind logging, so this can add real time to a close (bounded by
rendering_close_timeout for the encode drain and rendering_upload_close_timeout for the upload drain).
If you’d rather not wait on rendering frames at close, set init(skip_drain_render_on_close=True) (or the
METRANA_SKIP_DRAIN_RENDER_ON_CLOSE env var). It drops only the queued frames — your metric and attribute
delivery still gets close()’s full window, so you skip the video tail without risking metric loss.