Environment renderings
metrana.log_rendering records reinforcement-learning environment video. Each frame is appended to a
per-(env_id, episode) 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)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")How episodes become files
Section titled “How episodes become files”When the (env_id, episode) pair changes, the open encoder for that env is closed and a new one is opened for
the next episode — so each episode of each environment becomes its own MP4, written under the configured output
directory.
Frames route to encoder threads by env_id: every frame for a given env 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). When that cap is reached,
frames for a new env are subject to the configured backpressure/drop behaviour 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): 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 |
Behaviour |
|---|---|
"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( workspace="my-workspace", project="my-project", run="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( workspace="my-workspace", project="my-project", run="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")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")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)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.