sample

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package sample extracts a sample from one or several videos: a file made of scenes of each, chosen on their spatial and temporal information (the most complex ones, representative ones, a mix of both, or the easiest), copied from the sources without re-encoding.

The scenes are whole GOPs: a stream is only copied from a keyframe to the next. The sample is therefore about as long as asked, not exactly, and its frames are the sources' own, bit for bit.

Index

Examples

Constants

View Source
const (
	DefaultDuration = 60 * media.Duration(time.Second)
	DefaultPiece    = 2 * media.Duration(time.Second)
	DefaultTopShare = 0.5
)

Defaults of Options.

View Source
const (
	StageInspect  = "inspect"
	StageAnalysis = "analysis"
	StageExtract  = "extract"
	StageVerify   = "verify"
)

Stages reported through Options.Progress.

Variables

View Source
var (
	// ErrNoSource is returned when no video is given.
	ErrNoSource = errors.New("no source")
	// ErrInvalidSource is returned for a source without a usable video.
	ErrInvalidSource = errors.New("no usable video stream")
	// ErrFormat is returned when the sources cannot be copied into one
	// stream: another codec, resolution, frame rate, bit depth or dynamic
	// range.
	ErrFormat = errors.New("the videos of a sample must share their codec and format (they are copied, not re-encoded)")
	// ErrOptions is returned for options out of range.
	ErrOptions = errors.New("invalid sample options")
	// ErrDestination is returned when the sample would overwrite a source.
	ErrDestination = errors.New("the sample cannot be written over a source")
	// ErrNoKeyframe is returned for a source whose frames show no keyframe
	// to cut at.
	ErrNoKeyframe = errors.New("no keyframe to cut at")
)

Functions

This section is empty.

Types

type Check

type Check struct {
	// Frames is the frame count of the file, to compare with
	// Result.Frames: a stream whose GOPs are not closed loses the frames
	// displayed before a keyframe but stored after it.
	Frames int `json:"frames"`
	// Decoded is set when every frame decoded without an error.
	Decoded bool `json:"decoded"`
	// Note says what went wrong, "" when the sample is as planned.
	Note string `json:"note,omitempty"`
}

Check is the sample read back once written: its frame count, that no two frames share a timestamp, and that every frame decodes.

func (Check) OK

func (c Check) OK() bool

OK reports whether the sample is as planned.

type Complexity

type Complexity struct {
	SI      float64 `json:"si"`
	TI      float64 `json:"ti"`
	VideoSI float64 `json:"videoSi"`
	VideoTI float64 `json:"videoTi"`
}

Complexity is the spatial and temporal information of the frames taken of a video, next to the video's.

type Cutter

type Cutter interface {
	Copy(
		ctx context.Context,
		spec encode.CopySpec,
	) error
	Decodes(
		ctx context.Context,
		path string,
	) error
}

Cutter copies scenes of videos into one file, without re-encoding, and checks that a file decodes (*encode.FFmpeg).

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine extracts samples.

func NewEngine

func NewEngine(
	inspector Inspector,
	cutter Cutter,
) *Engine

NewEngine returns an Engine reading the sources with inspector and writing samples with cutter.

func (*Engine) Extract

func (e *Engine) Extract(
	ctx context.Context,
	sources []string,
	destination string,
	opts Options,
) (*Result, error)

Extract writes to destination a sample of sources, as opts ask: every video gives the same length, in scenes that are whole GOPs, copied as they are, the most complex first when complex scenes are asked for (see Result.Order). A video shorter than its share is taken whole. The sources must share their codec and format. The sample is then read back: its frame count against the scenes taken, and a decode of every frame.

Example

A minute of the most complex scenes of three episodes, copied into one file without re-encoding. The sample plays the most complex scene first.

package main

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/eko/qc/analysis"
	"github.com/eko/qc/bitstream"
	"github.com/eko/qc/decode"
	"github.com/eko/qc/encode"
	"github.com/eko/qc/media"
	"github.com/eko/qc/probe"
	"github.com/eko/qc/sample"
)

// exampleEngine wires a sample Engine on the ffmpeg and ffprobe binaries:
// the analyzer inspects and analyses the sources (no quality meter is
// needed), encode.FFmpeg copies the scenes and checks the result.
func exampleEngine() *sample.Engine {
	analyzer := analysis.New(slog.Default(),
		probe.NewFFprobe("ffprobe"),
		bitstream.NewFFprobeReader("ffprobe"),
		decode.NewFFmpeg("ffmpeg", 0),
		nil,
	)

	return sample.NewEngine(analyzer, encode.NewFFmpeg("ffmpeg"))
}

func main() {
	sources := []string{"episode-01.mov", "episode-02.mov", "episode-03.mov"}

	res, err := exampleEngine().Extract(context.Background(), sources, "sample.mkv", sample.Options{
		Duration: media.Seconds(60),
		Scenes:   sample.ScenesTop,
	})
	if err != nil {
		fmt.Println(err)

		return
	}

	// The scenes as the sample plays them.
	for _, scene := range res.Order {
		fmt.Printf("%s %s-%s SI %.1f TI %.1f\n", res.Sources[scene.Source].Path, scene.Start, scene.End, scene.SI, scene.TI)
	}

	// The file read back: its frame count, its timestamps, a full decode.
	if !res.Check.OK() {
		fmt.Println("do not trust this sample:", res.Check.Note)
	}
}
Example (Mixed)

A mixed sample: 30% of the most complex scenes, the rest representative of each video, with what every video gave.

package main

import (
	"context"
	"fmt"
	"log/slog"

	"github.com/eko/qc/analysis"
	"github.com/eko/qc/bitstream"
	"github.com/eko/qc/decode"
	"github.com/eko/qc/encode"
	"github.com/eko/qc/media"
	"github.com/eko/qc/probe"
	"github.com/eko/qc/sample"
)

// exampleEngine wires a sample Engine on the ffmpeg and ffprobe binaries:
// the analyzer inspects and analyses the sources (no quality meter is
// needed), encode.FFmpeg copies the scenes and checks the result.
func exampleEngine() *sample.Engine {
	analyzer := analysis.New(slog.Default(),
		probe.NewFFprobe("ffprobe"),
		bitstream.NewFFprobeReader("ffprobe"),
		decode.NewFFmpeg("ffmpeg", 0),
		nil,
	)

	return sample.NewEngine(analyzer, encode.NewFFmpeg("ffmpeg"))
}

func main() {
	res, err := exampleEngine().Extract(context.Background(), []string{"film.mp4"}, "mix.mp4", sample.Options{
		Duration: media.Seconds(120),
		Scenes:   sample.ScenesMixed,
		TopShare: 0.3,
		Progress: func(p sample.Progress) { fmt.Println(p.Stage, p.Done, p.Total) },
	})
	if err != nil {
		fmt.Println(err)

		return
	}

	for _, video := range res.Sources {
		c := video.Complexity
		fmt.Printf("%s: %s taken in %d scenes, SI %.1f (video %.1f), TI %.1f (video %.1f)\n",
			video.Path, video.Taken, len(video.Segments), c.SI, c.VideoSI, c.TI, c.VideoTI)
	}
}

type Inspector

type Inspector interface {
	Analyze(
		ctx context.Context,
		path string,
		opts analysis.Options,
	) (*analysis.Report, error)
}

Inspector inspects and analyses the sources (*analysis.Analyzer).

type Options

type Options struct {
	// Duration is the length asked of the sample, shared equally between
	// the videos. Default one minute.
	Duration media.Duration
	// Scenes is which scenes are taken. Default ScenesMixed.
	Scenes Scenes
	// TopShare is the share of a mixed sample given to the most complex
	// scenes, between 0 and 1 excluded. Default a half.
	TopShare float64
	// Piece is the least length of a scene taken: GOPs are joined until
	// they reach it. Default 2 s.
	Piece media.Duration
	// Progress is called as the extraction advances, never concurrently.
	Progress func(Progress)
}

Options configures Engine.Extract. The zero value takes a minute of mixed scenes.

type Progress

type Progress struct {
	Stage       string
	Done, Total int
}

Progress reports the advancement of an extraction: Done out of Total units of Stage (frames analysed, scenes copied).

type Result

type Result struct {
	SchemaVersion int       `json:"schemaVersion"`
	GeneratedAt   time.Time `json:"generatedAt"`
	// Path is the sample written.
	Path   string `json:"path"`
	Scenes Scenes `json:"scenes"`
	// TopShare is the share of the most complex scenes asked of a mixed
	// sample.
	TopShare float64 `json:"topShare,omitempty"`
	// Asked is the length asked for, Duration what the whole GOPs taken
	// make.
	Asked    media.Duration `json:"asked"`
	Duration media.Duration `json:"duration"`
	Frames   int            `json:"frames"`
	// Sources are the videos, in the order given, each with its scenes in
	// its own order.
	Sources []SourceResult `json:"sources"`
	// Order lists the scenes as they follow each other in the sample: the
	// most complex first for ScenesTop, the easiest first for ScenesEasy,
	// the complex ones then the representative ones for ScenesMixed, and
	// the order of the videos for ScenesAverage.
	Order []Scene `json:"order"`
	// Check is the sample read back.
	Check   Check          `json:"check"`
	Elapsed media.Duration `json:"elapsed"`
}

Result describes an extracted sample.

type Scene

type Scene struct {
	// Source is the index of its video in Result.Sources.
	Source int `json:"source"`
	Segment
}

Scene is a scene of the sample: a segment of one of its sources.

type Scenes

type Scenes string

Scenes is which scenes of the videos a sample takes.

const (
	// ScenesTop takes the most complex scenes: the highest spatial ×
	// temporal information, what costs an encoder most.
	ScenesTop Scenes = "top"
	// ScenesMixed takes the most complex scenes for a share of the sample
	// (Options.TopShare) and representative scenes for the rest.
	ScenesMixed Scenes = "mixed"
	// ScenesAverage takes scenes spread over each video that together
	// have its spatial and temporal information: what the video is on
	// average.
	ScenesAverage Scenes = "average"
	// ScenesEasy takes the easiest scenes: the lowest spatial × temporal
	// information.
	ScenesEasy Scenes = "easy"
)

func AllScenes

func AllScenes() []Scenes

AllScenes lists the choices of scenes, in the order they are offered.

type Segment

type Segment struct {
	media.Interval
	Frames int `json:"frames"`
	// SI and TI are the mean spatial and temporal information of its
	// frames (0 for a video taken whole).
	SI float64 `json:"si,omitempty"`
	TI float64 `json:"ti,omitempty"`
	// Kind tells why the scene was taken: ScenesTop, ScenesAverage or
	// ScenesEasy; empty for a video taken whole.
	Kind Scenes `json:"kind,omitempty"`
}

Segment is a scene of a video copied into the sample: whole GOPs, from its first frame's time to the next keyframe's.

func (Segment) Score

func (s Segment) Score() float64

Score is what the scene costs an encoder, relative to the others: its spatial times its temporal information.

type SourceResult

type SourceResult struct {
	Path     string         `json:"path"`
	Duration media.Duration `json:"duration"`
	// Whole is set for a video no longer than its share, taken as it is
	// and not analysed.
	Whole    bool      `json:"whole,omitempty"`
	Segments []Segment `json:"segments"`
	// Taken and Frames are the length and frame count of the segments.
	Taken  media.Duration `json:"taken"`
	Frames int            `json:"frames"`
	// Complexity compares the frames taken with those of the video, nil
	// for a video taken whole.
	Complexity *Complexity `json:"complexity,omitempty"`
}

SourceResult is what the sample took of one video.

Jump to

Keyboard shortcuts

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