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 ¶
const ( DefaultDuration = 60 * media.Duration(time.Second) DefaultPiece = 2 * media.Duration(time.Second) )
Defaults of Options.
const ( StageInspect = "inspect" StageAnalysis = "analysis" StageExtract = "extract" StageVerify = "verify" )
Stages reported through Options.Progress.
Variables ¶
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.
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 ¶
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)
}
}
Output:
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)
}
}
Output:
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
// 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 ¶
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"`
// 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" )
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.
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.