encode

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package encode turns captured frames into a shareable artifact: an animated GIF, a numbered PNG directory, or — when ffmpeg is on PATH — an MP4/WebM.

It is deliberately PURE with respect to the browser: the input is a slice of already-captured frames plus options, and the output is bytes. There is no CDP and no knowledge of how the frames were produced. That is the whole reason RFC-0011 splits it out — frame timing through a real Chrome is inherently variable, so the parts that must be exactly right (the palette, the frame delays, the annotation compositing, the --max-size reduction) are tested here on synthetic frames instead.

The one thing that does cross the boundary is a context, because --max-size re-encodes the whole recording up to nine times and the command that asked for it has a --timeout. See reduce().

Two properties are load-bearing and any change must preserve both:

  • Annotation is composited HERE, at export, never at capture. Without Options.Annotate the exported frames are pixel-identical to what was captured, which is what makes a recording usable as a README asset (RFC-0011 US-5 / VS-13) and what lets one capture be exported both ways.

  • The GIF path uses only the standard library's image/gif. A Go CLI distributed as a single static binary must be able to produce its default format with no external program; ffmpeg is required only for mp4/webm, and its absence is a NAMED error rather than a silent fallback (VS-10).

Index

Constants

This section is empty.

Variables

View Source
var ErrNoEncoder = errors.New("no encoder available for this format")

ErrNoEncoder reports that the requested format needs a program that is not installed. It is a distinct sentinel because the CLI maps it to `usage` / exit 2 with a message naming the requirement, and — crucially — checks it BEFORE draining the recording, so the frames stay exportable as GIF (VS-10).

View Source
var ErrNoFrames = errors.New("no frames were captured")

ErrNoFrames reports an export with nothing to export.

Functions

func Available

func Available(f Format) error

Available reports whether this machine can produce the format, wrapping ErrNoEncoder with the missing requirement NAMED.

The CLI calls it before the recording is drained, so a missing ffmpeg costs the user an exit 2 and nothing else — the frames are still in the daemon and still exportable as GIF (VS-10).

func Extensions

func Extensions() string

Extensions lists the file extensions a format is inferred from, for error messages.

func IsNoEncoder

func IsNoEncoder(err error) bool

IsNoEncoder reports whether err is the missing-encoder condition.

Types

type File

type File struct {
	Name string
	Data []byte
}

File is one numbered frame of a `frames` export. The caller writes it; this package does no I/O of its own for that format.

type Format

type Format string

Format names an export format.

const (
	FormatGIF    Format = "gif"
	FormatMP4    Format = "mp4"
	FormatWebM   Format = "webm"
	FormatFrames Format = "frames"
)

The export formats. gif is the default because it is the one that works with no external dependency and embeds everywhere a bug report or README does (RFC-0011 open question 2).

func FormatFromPath

func FormatFromPath(path string) (Format, bool)

FormatFromPath infers the format from an output path's extension, reporting false when the path carries no extension this package recognises.

func ParseFormat

func ParseFormat(s string) (Format, error)

ParseFormat resolves a user-supplied format name.

type Frame

type Frame struct {
	Data []byte    `json:"data"`
	TS   time.Time `json:"ts"`

	// CSSWidth/CSSHeight are the frame's size in page CSS pixels. They exist so
	// a Mark's page coordinates map onto image pixels when the capture was
	// scaled down. Zero means "the image's own pixel dimensions", i.e. 1:1.
	CSSWidth  float64 `json:"css_width,omitempty"`
	CSSHeight float64 `json:"css_height,omitempty"`

	// Marks are the actions that landed while this frame was on screen. They are
	// carried, not drawn: Options.Annotate decides at export whether they become
	// pixels.
	Marks []Mark `json:"marks,omitempty"`
}

Frame is one captured frame plus what is needed to draw on it.

Data is the image exactly as Chrome produced it (a screencast JPEG, or a PNG in tests); it is decoded here and never re-compressed lossily.

type Mark

type Mark struct {
	X       float64 `json:"x"`
	Y       float64 `json:"y"`
	Command string  `json:"command,omitempty"`
}

Mark is one action to draw on the frames it overlaps: where the pointer landed, and which command put it there.

The coordinates are PAGE (CSS) pixels, the same space the pointer verbs resolve and report, so nothing here has to know about device scale factors — Frame.CSSWidth/CSSHeight carry the mapping onto image pixels.

type Options

type Options struct {
	Format Format

	// FPS is the playback cadence used when the frames carry no usable
	// timestamps, and the floor/ceiling is applied to timestamp-derived delays
	// either way.
	FPS float64

	// Loop is the GIF loop count: 0 loops forever, n > 0 plays n times.
	Loop int

	// Annotate composites the position markers. Off by default — a README asset
	// must not carry the tool's own drawing (US-5).
	Annotate bool

	// MaxBytes is a best-effort size ceiling. See reduce(): the export is
	// re-encoded at reduced scale (and then a reduced frame count) until it
	// fits, and the values actually used are reported in Result.
	MaxBytes int
}

Options controls one export.

type Result

type Result struct {
	Data  []byte // gif/mp4/webm bytes (nil for FormatFrames)
	Files []File // FormatFrames only

	Format     Format
	Frames     int
	FPS        float64
	Scale      float64 // 1 unless --max-size forced a reduction
	Width      int
	Height     int
	Bytes      int
	DurationMs int64
	Annotated  bool
	Reduced    bool // --max-size changed the scale and/or the frame count

	// DecodeFailures counts captured frames that did not decode as an image and
	// were therefore left out. They are reported rather than fatal: see
	// decodeAll.
	DecodeFailures int

	// WithinMaxSize reports whether the ceiling was actually met. False means
	// the reduction ladder bottomed out first — a best-effort bound, reported
	// rather than silently missed.
	WithinMaxSize bool

	// ReductionTimedOut reports that the --max-size ladder was cut short by the
	// context rather than by its own bounds, so this is the best COMPLETE
	// attempt and not the best attempt available.
	ReductionTimedOut bool
}

Result is the artifact plus everything the envelope reports about it. Every field is a fact about what was PRODUCED, not what was requested, which is the point: --max-size can change the scale and the frame count, and a caller that only saw its own flags back would not know.

func Encode

func Encode(ctx context.Context, frames []Frame, opts Options) (Result, error)

Encode renders frames into the requested artifact.

ctx bounds the work. Encoding is not a fixed cost — --max-size runs the whole pipeline up to nine times — so the command's deadline has to reach in here, or `record stop --max-size` runs for minutes past --timeout with no output.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL