Documentation
¶
Overview ¶
Package transition implements the video transition engine for dissolves, dips, wipes, fade-to-black, and stinger transitions.
All blending operates directly in YUV420 (BT.709) space, matching hardware broadcast mixers (ATEM, Ross) and avoiding costly YUV-RGB round-trip conversions. The Engine is created per-transition and destroyed on complete or abort, returning to zero-CPU passthrough between transitions.
Key types:
- Engine: Per-transition lifecycle (start, ingest frames, complete/abort)
- FrameBlender: YUV420 blending for mix, dip, wipe, FTB, and stinger
- EngineConfig: Encoder/decoder factories and transition parameters
- StingerData: Pre-decoded PNG sequence with per-pixel alpha plane
Wipe transitions support 6 directions (horizontal, vertical, box) using per-pixel threshold masks with a 4px soft edge. The T-bar provides manual position control via throttled REST updates at 20 Hz.
Index ¶
- Constants
- Variables
- func BlendDip10(dst, a, b []byte, width, height, pos256 int, limitedRange bool) error
- func BlendFTB10(dst, src []byte, width, height, pos256 int, limitedRange bool) error
- func BlendMix10(dst, a, b []byte, width, height, pos256 int) error
- func BlendStinger10(dst, base, overlay []byte, width, height int, alpha []byte) error
- func BlendUniformBytes(dst, a, b []byte, pos int)
- func BlendWipe10(dst, a, b []byte, width, height int, alpha []byte) error
- func BoxShrink2xYUV420(src []byte, srcW, srcH int, dst []byte, dstW, dstH int)
- func RGBToYUV420(rgb []byte, width, height int, yuv []byte)
- func RGBToYUV_BT709Limited(r, g, b uint8) (y, cb, cr uint8)
- func ScaleYUV420(src []byte, srcW, srcH int, dst []byte, dstW, dstH int)
- func ScaleYUV420Lanczos(src []byte, srcW, srcH int, dst []byte, dstW, dstH int)
- func ScaleYUV420Preview(src []byte, srcW, srcH int, dst []byte, dstW, dstH int, boxBuf *[]byte)
- func ScaleYUV420WithQuality(src []byte, srcW, srcH int, dst []byte, dstW, dstH int, quality ScaleQuality)
- func ScaleYUV422P10(src []byte, srcW, srcH int, dst []byte, dstW, dstH int) error
- func ScaleYUV422P10Lanczos(src []byte, srcW, srcH int, dst []byte, dstW, dstH int) error
- func ScaleYUV422P10WithQuality(src []byte, srcW, srcH int, dst []byte, dstW, dstH int, quality ScaleQuality) error
- func YUV420ToRGB(yuv []byte, width, height int, rgb []byte)
- func YUV420ToRGBLimited(yuv []byte, width, height int, rgb []byte)
- func YUVToRGB_BT709Limited(y, cb, cr uint8) (r, g, b uint8)
- type DecoderFactory
- type EasingConfig
- type EasingCurve
- type EasingType
- type EncoderFactory
- type Engine
- func (e *Engine) Abort()
- func (e *Engine) AutoAdvancePosition(frameDurationMs float64) (float64, bool)
- func (e *Engine) BlendRaw(fromYUV []byte, fromW, fromH int, toYUV []byte, toW, toH int, position float64) ([]byte, int, int)
- func (e *Engine) BlendRawInto(dst []byte, fromYUV []byte, fromW, fromH int, toYUV []byte, toW, toH int, ...) ([]byte, int, int)
- func (e *Engine) Easing() EasingType
- func (e *Engine) ForceComplete()
- func (e *Engine) FromSource() string
- func (e *Engine) IngestFrame(sourceKey string, wireData []byte, pts int64, isKeyframe bool)
- func (e *Engine) IngestRawFrame(sourceKey string, yuv []byte, width, height int, pts int64)
- func (e *Engine) Position() float64
- func (e *Engine) SetColorFormat(f video.Format)
- func (e *Engine) SetPosition(pos float64)
- func (e *Engine) SetTimeout(d time.Duration)
- func (e *Engine) Start(from, to string, ttype Type, durationMs int) error
- func (e *Engine) StartSync(from, to string, ttype Type, durationMs int)
- func (e *Engine) State() State
- func (e *Engine) StingerFrameAt(pos float64) (yuv, alpha []byte, width, height int, cutPoint float64)
- func (e *Engine) Stop()
- func (e *Engine) Timeout() time.Duration
- func (e *Engine) Timing() map[string]any
- func (e *Engine) ToSource() string
- func (e *Engine) TransitionType() Type
- func (e *Engine) WarmupComplete()
- func (e *Engine) WarmupDecode(sourceKey string, wireData []byte)
- func (e *Engine) WipeDirection() WipeDirection
- func (e *Engine) WipeGradient() []byte
- func (e *Engine) WipeMapConfig() *wipemap.WipeConfig
- type EngineConfig
- type FrameBlender
- func (fb *FrameBlender) BlendDip(yuvA, yuvB []byte, position float64) []byte
- func (fb *FrameBlender) BlendFTB(yuvA []byte, position float64) []byte
- func (fb *FrameBlender) BlendMix(yuvA, yuvB []byte, position float64) []byte
- func (fb *FrameBlender) BlendNAM(yuvA, yuvB []byte, position float64) []byte
- func (fb *FrameBlender) BlendStinger(baseYUV []byte, stingerYUV []byte, alpha []byte) []byte
- func (fb *FrameBlender) BlendWipe(yuvA, yuvB []byte, position float64, direction WipeDirection) []byte
- func (fb *FrameBlender) BlendWipeMap(yuvA, yuvB []byte, position float64, gradientMap []byte, ...) []byte
- func (fb *FrameBlender) SetLimitedRange(limited bool)
- type ScaleQuality
- type State
- type StingerData
- type StingerFrameData
- type Type
- type VideoDecoder
- type VideoEncoder
- type WipeDirection
Examples ¶
Constants ¶
const ( DefaultBitrate = 4_000_000 // 4 Mbps DefaultFPS = 30.0 DefaultGOPSecs = 2 // IDR interval in seconds (program output) // PreviewGOPSecs is a shorter IDR interval for preview/relay encodes. // Shorter GOPs mean faster recovery after transport stalls — the browser // waits at most 1s for the next keyframe instead of 2s. PreviewGOPSecs = 1 // PreviewGOPFrames is the IDR interval in frames for preview encodes. // ~15 frames at 30fps = 0.5s GOPs. Halving the GOP reduces the // damaged-group cascade impact on jittery networks: each drop loses // at most ~14 frames instead of ~29. PreviewGOPFrames = 15 )
Default encoder parameters for the pipeline codec pool.
const DefaultTimeout = 10 * time.Second
DefaultTimeout is the default watchdog timeout for transition frame starvation. If no frames arrive from either source for this duration, the transition is aborted.
Variables ¶
var ( ErrActive = errors.New("transition: already active") ErrFTBActive = errors.New("transition: FTB is active") )
Sentinel errors for the transition package.
var ValidEasingTypes = map[EasingType]bool{ EasingLinear: true, EasingEase: true, EasingEaseIn: true, EasingEaseOut: true, EasingEaseInOut: true, EasingSmoothstep: true, EasingCustom: true, }
ValidEasingTypes is the set of all recognized easing types.
var ValidWipeDirections = map[WipeDirection]bool{ WipeHLeft: true, WipeHRight: true, WipeVTop: true, WipeVBottom: true, WipeBoxCenterOut: true, WipeBoxEdgesIn: true, }
ValidWipeDirections is the set of all valid wipe directions.
Functions ¶
func BlendDip10 ¶
BlendDip10 performs a two-phase dip-to-black transition between two YUV422P10LE frames. pos256 ranges from 0 (100% A) to 256 (100% B), with midpoint (128) being full black. Phase 1 (0-128): A fades to black. Phase 2 (128-256): black fades to B. limitedRange controls the black level: true uses Y=64, false uses Y=0.
func BlendFTB10 ¶
BlendFTB10 fades a single YUV422P10LE source to black. pos256 ranges from 0 (full source) to 256 (fully black). limitedRange controls the black level: true uses Y=64, false uses Y=0. Chroma fades to neutral (512) in both ranges.
func BlendMix10 ¶
BlendMix10 performs linear interpolation between two YUV422P10LE frames. pos256 ranges from 0 (100% A) to 256 (100% B). SIMD-accelerated on amd64 (AVX2) and arm64 (NEON), ~15x faster than pure Go.
func BlendStinger10 ¶
BlendStinger10 composites a stinger overlay onto a base YUV422P10LE frame using per-pixel alpha. alpha is an 8-bit per-luma-pixel alpha mask (0 = base, 255 = overlay), same dimensions as Y plane. For chroma planes, alpha is downsampled 2:1 horizontally only (4:2:2 subsampling).
func BlendUniformBytes ¶
BlendUniformBytes applies uniform alpha blend across byte slices:
dst[i] = (a[i]*(256-pos) + b[i]*pos + 128) >> 8
pos must be 0-256. SIMD-accelerated on amd64 and arm64. dst, a, and b must all have the same length. Returns immediately if len(dst) == 0.
func BlendWipe10 ¶
BlendWipe10 performs a per-pixel alpha wipe between two YUV422P10LE frames. alpha is an 8-bit per-luma-pixel alpha mask (0 = A, 255 = B), same dimensions as Y plane. For chroma planes, alpha is downsampled 2:1 horizontally only (4:2:2 subsampling).
func BoxShrink2xYUV420 ¶
BoxShrink2xYUV420 downscales a YUV420 frame by exactly 2x in both dimensions using box-average (2×2 block average for Y, 2×2 for chroma which is already at half-res). This is ~4x faster than bilinear for the same pixel count because each source pixel is read exactly once (no scatter-gather).
srcW and srcH must be even. dstW = srcW/2, dstH = srcH/2.
func RGBToYUV420 ¶
RGBToYUV420 converts interleaved RGB to YUV420 planar (full-range) using BT.709 coefficients.
func RGBToYUV_BT709Limited ¶
RGBToYUV_BT709Limited converts an RGB pixel to limited-range BT.709 YCbCr. Output ranges: Y [16, 235], Cb [16, 240], Cr [16, 240].
func ScaleYUV420 ¶
ScaleYUV420 scales a YUV420 planar frame from (srcW x srcH) to (dstW x dstH) using bilinear interpolation. Both src and dst must be sized for their respective resolutions: video.YUV420FrameSize(w,h) bytes (Y[w*h] + Cb[w/2*h/2] + Cr[w/2*h/2]).
When source and destination dimensions match, a plain copy is performed (zero interpolation overhead).
The scaler uses 16.16 fixed-point arithmetic for sub-pixel coordinate mapping, which avoids floating-point per-pixel while maintaining accuracy for broadcast resolutions up to 4K.
func ScaleYUV420Lanczos ¶
ScaleYUV420Lanczos scales a YUV420 planar frame from (srcW x srcH) to (dstW x dstH) using Lanczos-3 interpolation. Each plane is scaled at its native resolution: full for Y, half for Cb/Cr.
The scaler uses separable filtering (horizontal pass then vertical pass) with precomputed kernel weights for performance. The Lanczos-3 kernel has a support radius of 3 pixels and produces sharper output than bilinear.
func ScaleYUV420Preview ¶
ScaleYUV420Preview scales a YUV420 frame optimized for preview encoding. On amd64, uses box-shrink 2x + bilinear for the remainder when the source is ≥1.5x the destination. This avoids the expensive scalar bilinear gather on 75% of the source pixels.
func ScaleYUV420WithQuality ¶
func ScaleYUV420WithQuality(src []byte, srcW, srcH int, dst []byte, dstW, dstH int, quality ScaleQuality)
ScaleYUV420WithQuality scales a YUV420 planar frame using the selected algorithm. Same buffer layout as ScaleYUV420: src and dst must be video.YUV420FrameSize(w,h) bytes.
func ScaleYUV422P10 ¶
ScaleYUV422P10 scales a YUV422P10LE frame using bilinear interpolation. src is srcW*srcH*4 bytes, dst is dstW*dstH*4 bytes.
Buffer layout (YUV422P10LE planar):
- Y plane: srcW * srcH samples × 2 bytes (offset 0)
- Cb plane: srcW/2 * srcH samples × 2 bytes
- Cr plane: srcW/2 * srcH samples × 2 bytes
- Total: srcW * srcH * 4 bytes
The scaler uses 16.16 fixed-point arithmetic for sub-pixel coordinate mapping (same approach as the 8-bit ScaleYUV420 scaler) but operates on uint16 samples with 32-bit intermediate math and clamps to [0, 1023].
Chroma planes are full height (4:2:2), unlike 4:2:0 which halves vertically.
func ScaleYUV422P10Lanczos ¶
ScaleYUV422P10Lanczos scales a YUV422P10LE frame using Lanczos-3 interpolation. Each plane is scaled at its native resolution: full for Y, half-width for Cb/Cr. SIMD optimization is deferred; this is a pure Go implementation.
func ScaleYUV422P10WithQuality ¶
func ScaleYUV422P10WithQuality(src []byte, srcW, srcH int, dst []byte, dstW, dstH int, quality ScaleQuality) error
ScaleYUV422P10WithQuality scales a YUV422P10LE frame using the selected algorithm.
func YUV420ToRGB ¶
YUV420ToRGB converts YUV420 planar (full-range) to interleaved RGB using BT.709 coefficients. yuv layout: Y[w*h] + U[w/2 * h/2] + V[w/2 * h/2] rgb layout: R,G,B,R,G,B,... (w*h*3 bytes)
Example ¶
package main
import (
"fmt"
"github.com/zsiec/switchframe/server/transition"
)
func main() {
// 2x2 pure white frame in YUV420 full-range.
// Y=255 for all pixels, Cb=128 (neutral), Cr=128 (neutral).
yuv := []byte{
255, 255, 255, 255, // Y plane (2x2)
128, // Cb plane (1x1, subsampled)
128, // Cr plane (1x1, subsampled)
}
rgb := make([]byte, 2*2*3)
transition.YUV420ToRGB(yuv, 2, 2, rgb)
// With neutral chroma, Y=255 maps to RGB(255, 255, 255).
fmt.Printf("pixel[0]: R=%d G=%d B=%d\n", rgb[0], rgb[1], rgb[2])
fmt.Printf("pixel[3]: R=%d G=%d B=%d\n", rgb[9], rgb[10], rgb[11])
}
Output: pixel[0]: R=255 G=255 B=255 pixel[3]: R=255 G=255 B=255
func YUV420ToRGBLimited ¶
YUV420ToRGBLimited converts YUV420 planar (limited-range BT.709) to interleaved RGB. Input ranges: Y [16, 235], Cb [16, 240], Cr [16, 240]. Super-white/super-black values are handled gracefully via output clamping.
func YUVToRGB_BT709Limited ¶
YUVToRGB_BT709Limited converts a limited-range BT.709 YCbCr pixel to RGB. Input ranges: Y [16, 235], Cb [16, 240], Cr [16, 240]. Values outside these ranges (super-white, super-black) are handled gracefully via clamping on the RGB output.
Types ¶
type DecoderFactory ¶
type DecoderFactory func() (VideoDecoder, error)
DecoderFactory creates a new VideoDecoder. Allows tests to inject mock factories without cgo.
type EasingConfig ¶
type EasingConfig struct {
Type string `json:"type"`
X1 float64 `json:"x1,omitempty"`
Y1 float64 `json:"y1,omitempty"`
X2 float64 `json:"x2,omitempty"`
Y2 float64 `json:"y2,omitempty"`
}
EasingConfig is the JSON-serializable easing configuration for API requests.
type EasingCurve ¶
type EasingCurve struct {
Type EasingType
X1, Y1 float64
X2, Y2 float64
}
EasingCurve defines a timing curve for transition easing. It supports CSS-style cubic-bezier presets, the legacy smoothstep, and custom curves.
func NewCustomEasingCurve ¶
func NewCustomEasingCurve(x1, y1, x2, y2 float64) (*EasingCurve, error)
NewCustomEasingCurve creates a custom cubic-bezier easing curve. x1 and x2 must be in [0, 1]; y1 and y2 may be any value (allows overshoot).
func NewEasingCurve ¶
func NewEasingCurve(preset EasingType) *EasingCurve
NewEasingCurve returns an EasingCurve for the given preset type. Unknown types fall back to linear.
func (*EasingCurve) Ease ¶
func (c *EasingCurve) Ease(t float64) float64
Ease maps a linear time value t in [0,1] to an eased position. Input is clamped to [0,1]. A nil receiver returns t (linear fallback).
type EasingType ¶
type EasingType string
EasingType identifies the easing curve used for transition timing.
const ( EasingLinear EasingType = "linear" // y = t EasingEase EasingType = "ease" // CSS: cubic-bezier(0.25, 0.1, 0.25, 1.0) EasingEaseIn EasingType = "ease-in" // CSS: cubic-bezier(0.42, 0, 1.0, 1.0) EasingEaseOut EasingType = "ease-out" // CSS: cubic-bezier(0, 0, 0.58, 1.0) EasingEaseInOut EasingType = "ease-in-out" // CSS: cubic-bezier(0.42, 0, 0.58, 1.0) EasingSmoothstep EasingType = "smoothstep" // Hermite: t*(3-2t) EasingCustom EasingType = "custom" // User-defined cubic-bezier )
type EncoderFactory ¶
type EncoderFactory func(width, height, bitrate, fpsNum, fpsDen int) (VideoEncoder, error)
EncoderFactory creates a new VideoEncoder with the given parameters. Allows tests to inject mock factories without cgo.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine manages the dissolve pipeline lifecycle. Created when a transition starts, destroyed when it completes or aborts.
func NewEngine ¶
func NewEngine(config EngineConfig) *Engine
NewEngine creates a new engine with the given configuration.
func (*Engine) Abort ¶
func (e *Engine) Abort()
Abort cancels the active transition and invokes OnComplete(aborted=true). Safe to call from any goroutine. Idempotent — calling on an idle engine is a no-op.
func (*Engine) AutoAdvancePosition ¶
AutoAdvancePosition advances the transition position by one tick based on frame duration. Returns the eased position and whether the transition is complete (linear t >= 1.0). On completion, the engine state is set to Idle. This is the reference-clock path — no wall-clock timing, purely tick-driven.
The internal e.position tracks linear progress (0→1). The returned value has the easing curve applied, matching what currentPosition() produces in the wall-clock path.
func (*Engine) BlendRaw ¶
func (e *Engine) BlendRaw( fromYUV []byte, fromW, fromH int, toYUV []byte, toW, toH int, position float64, ) ([]byte, int, int)
BlendRaw performs a synchronous blend of two YUV420 frames at the given position. This is the reference-clock path — no goroutines, no internal timing, no codec pipeline.
Returns the blended YUV420 data, output width, and output height. If frames have different dimensions, toYUV is scaled to match fromYUV. For FTB transitions, toYUV may be nil.
The returned slice is freshly allocated per call; two successive calls yield independent slices. This contract is pinned by TestBlendRawReturnsCopy — a future refactor that pools the result into an engine field would break it. Callers on the per-frame hot path should use BlendRawInto with a reusable dst buffer to avoid the ~3 MB per-frame allocation for the result.
func (*Engine) BlendRawInto ¶
func (e *Engine) BlendRawInto( dst []byte, fromYUV []byte, fromW, fromH int, toYUV []byte, toW, toH int, position float64, ) ([]byte, int, int)
BlendRawInto is the scratch-reuse variant of BlendRaw. The blended frame is written into dst (grown if necessary, reslice-preserving any existing capacity) and the grown slice is returned. Pass nil for a freshly allocated result; pass a caller-owned buffer to reuse across frames and avoid the 3–9 MB/frame allocation pattern on resolution-mismatched stinger paths.
Single-caller invariant: BlendRawInto uses engine-scoped scratch buffers for the intermediate scaled frames. The transition state machine serializes ticks per engine, so the scratch is safe without additional locking beyond what BlendRaw already takes for the blender cache.
func (*Engine) Easing ¶
func (e *Engine) Easing() EasingType
Easing returns the current easing type, or "smoothstep" if nil.
func (*Engine) ForceComplete ¶
func (e *Engine) ForceComplete()
WarmupComplete marks the end of warmup. Sets flush flags so that if the first live frame from either source is a keyframe, the decoder is flushed to discard stale warmup references before decoding the fresh IDR. No-op when decoders are nil (raw-only mode). ForceComplete triggers transition completion from the caller (GPU transition path). Used in SkipBlend mode where auto-complete is deferred so the GPU blend frame is produced BEFORE the transition state changes.
func (*Engine) FromSource ¶
FromSource returns the outgoing source key.
func (*Engine) IngestFrame ¶
IngestFrame processes a video frame from one of the two transition sources. Decodes frame, stores as latest YUV420. If sourceKey matches the incoming source (toSource), triggers blend+encode+output with the source's PTS to maintain timestamp continuity on the program stream. For FTB, the fromSource triggers blend (no toSource).
Lock scope is minimized: decode happens outside the lock so that two sources sending frames near-simultaneously don't block each other during the 3-16ms decode step.
func (*Engine) IngestRawFrame ¶
IngestRawFrame accepts a pre-decoded YUV420 frame (e.g., from MXL sources). Skips H.264 decode — stores YUV directly and triggers blend. The frame is scaled to the engine's resolution if dimensions don't match.
func (*Engine) SetColorFormat ¶
SetColorFormat sets the pipeline color format for blend operations. Must be called before Start(). When set to video.YUV422_10bit, the engine uses 10-bit blend functions and 10-bit frame sizing.
func (*Engine) SetPosition ¶
SetPosition sets the T-bar manual position (0.0-1.0). Switches to manual control mode. pos>=1.0 triggers completion. pos<=0.0 triggers abort (only if previously moved past 0).
func (*Engine) SetTimeout ¶
SetTimeout configures the watchdog timeout. If no frames arrive from either source for this duration during an active transition, the transition is aborted. Must be called before Start().
func (*Engine) Start ¶
Start initializes the transition pipeline. Creates decoders and blender. Returns error if already active.
func (*Engine) StartSync ¶
StartSync configures the engine for synchronous blending (no goroutines). Sets transition type, duration, and direction but does NOT spawn any goroutines — no watchdog, no decoders, no codec pipeline. This is the StartSync initializes a transition for the reference-clock path. Unlike Start(), it does NOT use wall-clock timing — the caller drives position via AutoAdvancePosition. No watchdog goroutine is started because the sync loop handles completion directly.
func (*Engine) StingerFrameAt ¶
func (e *Engine) StingerFrameAt(pos float64) (yuv, alpha []byte, width, height int, cutPoint float64)
StingerFrameAt returns the stinger overlay YUV, alpha, and cut point for the given transition position. Returns nil slices if no stinger is configured. Used by the GPU transition path to upload stinger data to GPU.
func (*Engine) Timing ¶
Timing returns a snapshot of the engine's timing instrumentation. Safe to call from any goroutine (all fields are atomic).
func (*Engine) TransitionType ¶
TransitionType returns the current transition type.
func (*Engine) WarmupComplete ¶
func (e *Engine) WarmupComplete()
func (*Engine) WarmupDecode ¶
WarmupDecode feeds a frame to the decoder for the given source side, populating latestYUVA/latestYUVB so the first live IngestFrame can produce blended output immediately. Produces no output callbacks. No-op if the engine is not active.
func (*Engine) WipeDirection ¶
func (e *Engine) WipeDirection() WipeDirection
WipeDirection returns the wipe direction for the current transition.
func (*Engine) WipeGradient ¶
WipeGradient returns the resolved gradient map (single-channel, w*h bytes), or nil if no gradient-map wipe is active.
func (*Engine) WipeMapConfig ¶
func (e *Engine) WipeMapConfig() *wipemap.WipeConfig
WipeMapConfig returns the current wipe map config, or nil if using legacy directional wipe.
type EngineConfig ¶
type EngineConfig struct {
DecoderFactory DecoderFactory
Output func(yuv []byte, width, height int, pts int64, isKeyframe bool)
OnComplete func(aborted bool)
// WipeDirection specifies the wipe direction when Type is "wipe".
// Ignored for other transition types. Used only for legacy directional wipes;
// gradient-map wipes use WipeConfig instead.
WipeDirection WipeDirection
// WipeConfig specifies gradient-map-based wipe parameters. When set and
// Type is "wipe", the engine resolves the gradient map from WipeMapStore
// and uses BlendWipeMap instead of BlendWipe. Takes precedence over WipeDirection.
WipeConfig *wipemap.WipeConfig
// WipeMapStore resolves gradient maps by pattern ID. Required when
// WipeConfig is set; ignored otherwise.
WipeMapStore *wipemap.WipeMapStore
// Stinger holds the pre-decoded stinger overlay data. Required when
// Type is "stinger", ignored for other types.
Stinger *StingerData
// Easing sets the easing curve for the transition. If nil, the engine
// falls back to legacy smoothstep for backward compatibility.
Easing *EasingCurve
// HintWidth/HintHeight pre-initialize the blender at Start() time.
// When set, the engine can produce output (via black frame fallback)
// even before any decode succeeds. Set from the pipeline's known
// resolution to eliminate output gaps during B-frame reorder warmup.
HintWidth int
HintHeight int
// SkipBlend skips the CPU pixel blend in blendAndOutput but still
// tracks position and handles auto-complete timing. Used when GPU
// transitions are active — the GPU pipeline performs the blend, and
// the CPU engine only manages transition state.
SkipBlend bool
}
EngineConfig configures the Engine.
type FrameBlender ¶
type FrameBlender struct {
// contains filtered or unexported fields
}
FrameBlender alpha-blends two YUV420 planar byte slices directly, avoiding the cost and chroma resampling error of a YUV->RGB->YUV round-trip. This matches how hardware broadcast mixers (ATEM, Ross, Datavideo) and FFmpeg's xfade filter operate: blending in the native Y'CbCr domain.
All blend loops use fixed-point integer arithmetic (0-256 weight with >>8 shift) instead of float64, eliminating per-pixel float conversions. The result of blending two [0,255] values with a [0,256] weight always fits in [0,65280], so no clamping is needed after the shift.
Five blend modes are supported:
- Mix: linear interpolation between source A and source B
- Dip: two-phase blend through black (A fades out, then B fades in)
- Wipe: precomputed alpha map with 4px soft edge (6 directions)
- FTB: fade to black (single source fades to black)
- Stinger: per-pixel alpha composite from stinger overlay
The blender pre-allocates its output buffer at construction time and reuses it across frames. All blend methods return the internal yuvBufOut slice -- callers must consume it before the next call.
YUV420 layout: Y[w*h] + Cb[w/2*h/2] + Cr[w/2*h/2] Black in full-range YUV: Y=0, Cb=128, Cr=128
func NewFrameBlender ¶
func NewFrameBlender(width, height int) (*FrameBlender, error)
NewFrameBlender creates a FrameBlender with a pre-allocated output buffer sized for the given resolution in YUV420 format. Returns an error if width or height is odd (YUV420 requires even dimensions).
func (*FrameBlender) BlendDip ¶
func (fb *FrameBlender) BlendDip(yuvA, yuvB []byte, position float64) []byte
BlendDip performs a two-phase dip-to-black transition in YUV420 space. Phase 1 (position 0.0-0.5): source A fades to black. Phase 2 (position 0.5-1.0): source B fades up from black. At position 0.5, the output is fully black. Black level depends on SetLimitedRange: Y=0 (full-range) or Y=16 (limited-range). Uses fixed-point integer math: weight 0-256 with >>8 division.
func (*FrameBlender) BlendFTB ¶
func (fb *FrameBlender) BlendFTB(yuvA []byte, position float64) []byte
BlendFTB fades a single source to black in YUV420 space. position 0.0 = full source, position 1.0 = fully black. Black level depends on SetLimitedRange: Y=0 (full-range) or Y=16 (limited-range). Chroma neutral is 128 in both ranges. Uses fixed-point integer math: weight 0-256 with >>8 division.
func (*FrameBlender) BlendMix ¶
func (fb *FrameBlender) BlendMix(yuvA, yuvB []byte, position float64) []byte
BlendMix performs linear interpolation between yuvA and yuvB in YUV420 space. position 0.0 = all A, position 1.0 = all B. Uses fixed-point integer math: weight 0-256 with >>8 division.
func (*FrameBlender) BlendNAM ¶
func (fb *FrameBlender) BlendNAM(yuvA, yuvB []byte, position float64) []byte
BlendNAM performs a Non-Additive Mix between yuvA and yuvB in YUV420 space. For each pixel, the output is the brighter (higher gained Y) of the two sources. position 0.0 = all A, position 1.0 = all B. At any position, gain_a = 1-position and gain_b = position. Per luma pixel: if A*gain_a >= B*gain_b, output A*gain_a (and use A's chroma); otherwise output B*gain_b (and use B's chroma). Chroma is resolved per 2x2 block by majority vote of the luma winners. Uses fixed-point integer math for the luma comparison and scaling.
func (*FrameBlender) BlendStinger ¶
func (fb *FrameBlender) BlendStinger(baseYUV []byte, stingerYUV []byte, alpha []byte) []byte
BlendStinger composites a stinger frame (with alpha) over a base YUV420 source. The stinger frame's YUV data is blended with the base using per-pixel alpha. alpha is a per-luma-pixel alpha map [0-255], same dimensions as the Y plane. stingerYUV is YUV420 planar format matching base dimensions. Uses integer math with >>8 shift (GPU-standard approximation for /255).
func (*FrameBlender) BlendWipe ¶
func (fb *FrameBlender) BlendWipe(yuvA, yuvB []byte, position float64, direction WipeDirection) []byte
BlendWipe performs a directional wipe transition between yuvA and yuvB in YUV420 space. A precomputed alpha map is generated once per call at the Y-plane resolution, then the fast integer blend loop applies it. For linear wipes, alpha is constant along the perpendicular axis, so only one value per row/column is computed and replicated.
Wipe directions:
- h-left: wipes from left to right (B reveals from the left)
- h-right: wipes from right to left (B reveals from the right)
- v-top: wipes from top to bottom (B reveals from the top)
- v-bottom: wipes from bottom to top (B reveals from the bottom)
- box-center-out: B reveals from center expanding outward
- box-edges-in: B reveals from edges contracting inward
func (*FrameBlender) BlendWipeMap ¶
func (fb *FrameBlender) BlendWipeMap(yuvA, yuvB []byte, position float64, gradientMap []byte, config wipemap.WipeConfig) []byte
BlendWipeMap performs a gradient-map-based wipe transition between yuvA and yuvB. The gradientMap is a single-channel byte buffer (w*h) where 0 = revealed first and 255 = revealed last. The config controls soft edge, border, reverse, and multiply.
Approach:
- Convert gradient map → per-pixel alpha map (applying reverse, multiply, soft edge)
- Handle border pixels with a second pass
- Use existing SIMD blendAlpha for Y plane
- Downsample alpha to chroma, blendAlpha for Cb/Cr planes
- Overwrite border pixels with border color
func (*FrameBlender) SetLimitedRange ¶
func (fb *FrameBlender) SetLimitedRange(limited bool)
SetLimitedRange configures the blender for limited-range (broadcast) or full-range YUV. Limited-range uses Y=16 for black; full-range uses Y=0. The default is limited-range (Y=16) to match BT.709 broadcast standard.
type ScaleQuality ¶
type ScaleQuality int
ScaleQuality selects the scaling algorithm.
const ( // ScaleQualityFast uses bilinear interpolation. Suitable for real-time // preview or when CPU budget is tight. This is the zero value so that // uninitialized ScaleQuality defaults to the cheap path. ScaleQualityFast ScaleQuality = iota // ScaleQualityHigh uses Lanczos-3 interpolation for broadcast-quality // scaling. Produces sharper output than bilinear, especially on downscales, // at the cost of ~3-4x more computation. ScaleQualityHigh )
type StingerData ¶
type StingerData struct {
// Frames holds YUV420 + alpha data for each stinger frame.
Frames []StingerFrameData
// Width and Height of the stinger frames.
Width, Height int
// CutPoint is the position [0.0-1.0] where the underlying source switches from A to B.
CutPoint float64
// Audio is optional stinger audio (interleaved float32 PCM).
Audio []float32
AudioSampleRate int
AudioChannels int
}
StingerData holds pre-decoded stinger overlay frames for use during a stinger transition. Populated by the switcher from a stinger.Clip.
type StingerFrameData ¶
type StingerFrameData struct {
YUV []byte // YUV420 planar
Alpha []byte // per-luma-pixel alpha [0-255]
}
StingerFrameData is a single stinger overlay frame.
type Type ¶
type Type string
Type identifies the visual transition effect.
const ( Mix Type = "mix" Dip Type = "dip" FTB Type = "ftb" FTBReverse Type = "ftb_reverse" Wipe Type = "wipe" Stinger Type = "stinger" // DVE transition types TypeDVEPushLeft Type = "dve-push-left" TypeDVEPushRight Type = "dve-push-right" TypeDVEPushUp Type = "dve-push-up" TypeDVEPushDown Type = "dve-push-down" TypeDVESqueeze Type = "dve-squeeze" TypeDVESpin Type = "dve-spin" TypeDVEZoom Type = "dve-zoom" TypeDVEFly Type = "dve-fly" TypeDVECube Type = "dve-cube" TypeDVEFlip Type = "dve-flip" TypeDVEDoor Type = "dve-door" // Extended 2D TypeDVESwingLeft Type = "dve-swing-left" TypeDVESwingRight Type = "dve-swing-right" TypeDVERevealLeft Type = "dve-reveal-left" TypeDVERevealRight Type = "dve-reveal-right" TypeDVERevealUp Type = "dve-reveal-up" TypeDVERevealDown Type = "dve-reveal-down" TypeDVEScaleRotate Type = "dve-scale-rotate" TypeDVECrossZoom Type = "dve-cross-zoom" TypeDVEShrinkGrow Type = "dve-shrink-grow" TypeDVEBarnH Type = "dve-barn-h" TypeDVEBarnV Type = "dve-barn-v" TypeDVEMosaic Type = "dve-mosaic" TypeDVECornerPeel Type = "dve-corner-peel" // Extended 3D TypeDVECarousel Type = "dve-carousel" TypeDVETumble Type = "dve-tumble" TypeDVESwingDoor Type = "dve-swing-door" TypeDVEFold Type = "dve-fold" TypeDVECubeRoll Type = "dve-cube-roll" TypeDVEGlobe Type = "dve-globe" // B2 extended transitions TypeDVEPageTurn Type = "dve-page-turn" TypeDVEBounce Type = "dve-bounce" TypeDVEBlurDissolve Type = "dve-blur-dissolve" // NAM is a blend mode, not a DVE transition TypeNAM Type = "nam" )
type VideoDecoder ¶
type VideoDecoder interface {
// Decode decodes encoded video data and returns YUV420 planar bytes,
// width, height, and any error. The returned YUV buffer length is
// video.YUV420FrameSize(width, height) bytes for YUV420 8-bit.
Decode(data []byte) (yuv []byte, width, height int, err error)
// Close releases decoder resources.
Close()
}
VideoDecoder decodes AVC1/Annex B wire data into YUV420 planar buffers. Implementations: codec.FFmpegDecoder (cgo), codec.OpenH264Decoder (cgo+openh264), mockDecoder (tests).
func NewMockDecoder ¶
func NewMockDecoder(width, height int) VideoDecoder
NewMockDecoder creates a mock decoder for cross-package testing.
type VideoEncoder ¶
type VideoEncoder interface {
// Encode encodes a YUV420 planar frame. pts is the presentation
// timestamp in 90 kHz MPEG-TS units, passed through to the encoded
// bitstream for A/V sync. If forceIDR is true, the encoder produces
// a keyframe. Returns encoded data, whether the frame is a keyframe,
// and any error.
Encode(yuv []byte, pts int64, forceIDR bool) (data []byte, isKeyframe bool, err error)
// Close releases encoder resources.
Close()
}
VideoEncoder encodes YUV420 planar frames into AVC1/Annex B wire data. Implementations: codec.FFmpegEncoder (cgo), codec.OpenH264Encoder (cgo+openh264), mockEncoder (tests).
func NewMockEncoder ¶
func NewMockEncoder() VideoEncoder
NewMockEncoder creates a mock encoder for cross-package testing.
type WipeDirection ¶
type WipeDirection string
WipeDirection specifies the direction for a wipe transition.
const ( WipeHLeft WipeDirection = "h-left" WipeHRight WipeDirection = "h-right" WipeVTop WipeDirection = "v-top" WipeVBottom WipeDirection = "v-bottom" WipeBoxCenterOut WipeDirection = "box-center-out" WipeBoxEdgesIn WipeDirection = "box-edges-in" )
Source Files
¶
- blend.go
- blend_10bit.go
- blend_10bit_dispatch.go
- blend_10bit_kernels_amd64.go
- blend_export.go
- blend_kernels_amd64.go
- codec.go
- color.go
- doc.go
- downsample_10bit_dispatch.go
- downsample_10bit_kernels_amd64.go
- downsample_kernels_amd64.go
- easing.go
- engine.go
- scaler.go
- scaler_10bit.go
- scaler_10bit_dispatch.go
- scaler_10bit_kernels_amd64.go
- scaler_box.go
- scaler_kernels_amd64.go
- scaler_lanczos.go
- scaler_lanczos_10bit.go
- scaler_lanczos_10bit_dispatch.go
- scaler_lanczos_10bit_kernels_amd64.go
- scaler_lanczos_kernels_amd64.go
- scaler_preview_amd64.go
- types.go
- upconvert_10bit.go