encode

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 20 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

View Source
const HARTimeLayout = "2006-01-02T15:04:05.000Z07:00"

HARTimeLayout is the RFC 3339 UTC, millisecond-precision layout both `started_at` (RFC-0017) and this encoder's fallback use, so a fallback timestamp is indistinguishable in shape from one filled from a real row.

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 AnnotateImage added in v0.3.0

func AnnotateImage(data []byte, format string, quality int, cssW, cssH float64, labels []Label) ([]byte, []bool, error)

AnnotateImage decodes data (png or jpeg), draws each label, and re-encodes as format ("png" | "jpeg"; quality applies to jpeg only). It reports, per label, whether it put any pixel on the canvas — the meaning Annotated has for a recording. It is pure: synthetic-image tests pin the drawing.

A label whose centre falls outside the decoded image draws nothing (disc, ring, AND badge) rather than clamping the badge onto the canvas anyway — internal/chrome has already dropped every candidate outside the capture's clip, so this only matters for a caller (a test, a future format) that hands in a point it never checked.

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 HAR added in v0.3.0

func HAR(entries []NetEntry, opts HAROpts) ([]byte, error)

HAR renders the entries as an HTTP Archive 1.2 document: `json.Encoder` with two-space indent and HTML escaping OFF, so a redacted value ("<redacted>") survives byte-for-byte instead of becoming <redacted> — a reader grepping the file for the marker has to be able to find it.

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 HAROpts added in v0.3.0

type HAROpts struct {
	Version string
	Now     time.Time
}

HAROpts parameterises an export. Version is the CLI build version for log.creator; Now is the export instant, used only for a row with no start.

type Label added in v0.3.0

type Label struct {
	N    int
	X, Y float64
}

Label is a numbered position marker: the disc-and-ring of a Mark plus the number N. X, Y are CSS pixels from the capture's top-left, Mark's space.

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 NetEntry added in v0.3.0

type NetEntry struct {
	ID           string  `json:"id"`
	Method       string  `json:"method"`
	URL          string  `json:"url"`
	Type         string  `json:"type"`
	Status       *int64  `json:"status"` // nil: no status (pending or network-level failure)
	StatusText   string  `json:"status_text"`
	StartedAt    string  `json:"started_at"` // RFC 3339 UTC, ms; "" when the row carries none
	StartedMs    int64   `json:"started_ms"`
	DurationMs   *int64  `json:"duration_ms"` // nil: not finished
	RequestSize  int64   `json:"request_size"`
	ResponseSize int64   `json:"response_size"`
	FromCache    bool    `json:"from_cache"`
	Failed       bool    `json:"failed"`
	Error        *string `json:"error"`
	Pending      bool    `json:"pending"`

	RequestHeaders         map[string]string `json:"request_headers"`
	ResponseHeaders        map[string]string `json:"response_headers"`
	RequestBody            *string           `json:"request_body"`
	RequestBodyUnavailable bool              `json:"request_body_unavailable"`
	ResponseBody           *string           `json:"response_body"`
	BodyUnavailable        bool              `json:"body_unavailable"`
	BodyTruncated          bool              `json:"body_truncated"`
}

NetEntry is one request as the `net` envelope reports it. The JSON tags ARE the envelope keys (RFC-0003, RFC-0017): the CLI fills it from the rows `Net` returns by a JSON round trip, which also normalises the int64/float64 split between the in-process and daemon paths.

func DecodeNetEntries added in v0.3.0

func DecodeNetEntries(rows any) ([]NetEntry, error)

DecodeNetEntries turns the `requests` value of a net result into typed rows. It is a JSON round trip (marshal, then unmarshal into []NetEntry) rather than a type switch, because that is what also normalises the int64/float64 split between an in-process call and one that crossed the daemon's RPC.

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