Skip to content

Latest commit

 

History

History
176 lines (141 loc) · 7.25 KB

capture.adoc

File metadata and controls

176 lines (141 loc) · 7.25 KB

Screenshots and recordings

An app can save its own rendered output as PNG screenshots, PNG sequences, animated GIFs, or WebM video, and read pixels back. With a hidden window and scripted input, an app can produce images or demo videos by itself, with nobody at the keyboard.

Because the app records its own frames, a recording shows exactly what the app drew, with no screen-recording tool involved. With fixed timing, the same scripted run produces the same frames every time (see Timing and repeatable recordings), which is useful for demos and documentation, and for comparing one run with another. This chapter assumes [app-model] and [tasks].

The output directory

Every capture path is relative to the app’s output directory, which is the working directory unless the config says otherwise:

config = App.default.with_output_dir("captures")

Captures are the app’s own output, so writing them needs no declaration. In return, the directory is restricted: it must be relative to the working directory, with no .., and the app refuses to start with one that is not. Every capture path is inside it, and a path that would leave it is refused rather than rewritten. The directory is created on first use.

Screenshots

io.capture().screenshot!(path) writes one PNG of the next presented frame. It waits for that frame to be drawn, read back, and written, so it is legal only in a task. It is not legal even in init!, which runs before any frame exists. To capture the first frame, spawn the task on the first cycle:

if input.time.cycle_count == 0 {
    Task.spawn!(input, || Shot(io.capture().screenshot!("frame0.png")))
}

Here Shot is a variant of the app’s Msg that holds the result, and io is the App.Io that update! received; the task captures it.

Only one screenshot can be pending at a time. Asking for a second one while the first is waiting returns AlreadyPending. A headless run has no framebuffer, so a screenshot there returns Ok({}) without writing anything.

io.capture().screenshot_texture!(target, path) exports an offscreen Draw.RenderTexture instead (see [drawing]), at the texture’s own size. This is useful for an image larger than the window. It keeps transparency, and it is legal in init! and tasks. Postcard Studio exports at twice the window size.

Recordings

A recording is a description built from Capture.default: a 25 frames per second, half-scale GIF of at most 300 frames, with FixedStep timing. Change it with its with_* methods:

recording = Capture.default
    .with_path("demo.webm")
    .with_format(WebM)
    .with_fps(30)
    .with_timing(FixedStep)
Setting Choices

with_format

Png (numbered stills), Gif, or WebM (VP8). GIF and WebM are encoded one frame at a time, so they need memory for only one frame.

with_scale

Full, Half, Quarter, or Ratio(...), applied after rendering; it shrinks the file without changing the window

with_timing

FixedStep (the default), which reports exactly one recorded frame’s worth of time as each cycle’s elapsed time, or RealTime, which reports the time that actually passed

with_max_frames, with_every_nth

How long to record, and how many frames to skip

with_cursor

NoCursor or DrawCursor; the operating system’s pointer is not part of the framebuffer

with_quality

GIF palette effort: Best, Balanced, or Fast

Start and stop it with the capture service, from init!, update!, or a task:

_ = io.capture().start!(recording)
# ...later
_ = io.capture().stop!()

App.default.with_recording(recording) stores a recording in the config, but only as a description: starting it is always an explicit start!.

The outcome arrives in input.capture on the following cycles: Active with the frames recorded so far, then Finished with the frame count and file size, or Failed with a reason. When nothing is recording it is Idle. A recording also finishes when it reaches its frame limit or the app exits.

Timing and repeatable recordings

Reading each frame back from the graphics card takes time, so a recorded run is slower and less even than a normal one. With RealTime timing, that unevenness appears in the recording, because input.time.elapsed_seconds reports the real, uneven time between cycles.

With FixedStep timing, while the recording is active every cycle reports exactly 1/fps seconds as its elapsed time (divided by every_nth when frames are skipped), however long the frame really took. The app advances by the same amount every cycle, so the recording is smooth. It is also repeatable: run the same app with the same random seed, the same scripted input, and no task results that depend on the outside world, and it records the same frames every time. Real input from a person, the wall clock, and network or file replies are not controlled by FixedStep, so a recording that depends on them can differ between runs. [input-time] explains elapsed_seconds.

Hidden windows

App.default.with_visible(Bool.False) opens the window hidden. It still renders on the GPU, so capture works exactly as with a visible window, and an app can render a chart or a clip to a file without a window appearing. A hidden window still needs a display server; on a machine without one, wrap the run in xvfb-run. Capture Plot is a hidden-window WebM batch render.

Scripted input

Keys.set_source!, Keys.set_text!, and Mouse.set_source! let an app drive its own keyboard, typed text, and pointer. Scripted input goes through the same path as real input, so a recorded demo runs the app’s ordinary code:

Keys.set_source!(Keys.holding([KeyRight]))
Keys.set_text!(Keys.typing("hello"))

While a script drives a device, real input from that device is ignored. Capture UI Demo records a reproducible interface walkthrough this way. The host’s --host-keys and --host-text switches script input from the command line instead; see [testing].

Reading pixels

Capture.pixel_at!(source, { x, y }) returns one pixel, and Capture.read_region!(source, region) a rectangle of them as RGBA bytes, without writing a file. The source is Screen, the last frame shown, or Target(render_texture). Both are legal in init!, update!, and tasks, not in render!.

The host only keeps a copy of the screen once an app starts reading it, so the first Screen read of a run returns Unavailable and later reads succeed. A headless run has no pixels and always returns Unavailable. Input Inspector uses one as an eyedropper.

Further reading