core

package
v1.8.1 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package core holds the device-agnostic decision layer and the machinery every cast shares: config, source resolution, device discovery/connect, the pure Plan (delivery/subtitle/output axes) with its copy-vs-encode resolvers, and the replay-server delivery. It knows nothing about any specific device family; the pipeline executor and the device adapters import core, never the other way round, so a device concern physically cannot leak across the boundary or into this core.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Connect

func Connect(ctx context.Context, cfg Config) (device.Device, error)

Connect locates and connects the renderer named in cfg. cast.Play calls it once, up front: the plan needs the renderer's advertised capabilities (SelfFetch, accepted containers, decodable codecs) to fix its axes before any stage runs, so discovery can no longer overlap the pull the way the old per-device strategies arranged. device.Connect dispatches on device type internally, which is why nothing above here carries a device-type switch.

func ResolveAudio

func ResolveAudio(opts *ffmpeg.EncodeOptions, caps media.Renderer, src media.ProbeInfo)

ResolveAudio fills opts' audio fields from the renderer's advertised audio support and the source's probed track. It is the shared audio decision for both served paths (the read-once spool off its spool probe, the network remux off a source probe):

  1. copy — the renderer decodes the source codec (and channel count), so a 5.1/7.1 track passes through untouched, no quality loss and no downmix;
  2. surround re-encode — a multichannel source the renderer can't copy is re-encoded to a Dolby codec it advertises (E-AC-3, else AC-3), keeping the layout up to that codec's channel ceiling;
  3. stereo AAC — the floor every renderer decodes, when neither applies (also what a set advertising nothing surround-capable, or a failed probe's zero ProbeInfo, gets — the pre-surround behaviour).

func ResolveSource

func ResolveSource(ctx context.Context, cfg Config, stream *media.Stream) (*media.Stream, string, error)

ResolveSource runs the do-or-die prelude that doesn't depend on the renderer: resolve the source URL (HLS variant selection) and find our local IPv4. The device is discovered separately so its latency can overlap the puller.

func ResolveVideo

func ResolveVideo(ctx context.Context, opts *ffmpeg.EncodeOptions, caps media.Renderer, src media.ProbeInfo, cfg Config)

ResolveVideo fills opts' video fields from the renderer's advertised support and the source's probed track: the copy-vs-encode decision the served path makes once it holds a probe (the read-once spool probe, or the network-remux source probe). It is the video counterpart to ResolveAudio and, like it, a mutator over opts that queries capability methods:

  • copy: no subtitles to burn, the source fits under the height ceiling, and the renderer decodes the source envelope natively: the bitstream passes through untouched (VideoEncoder nil), no quality loss;
  • re-encode: otherwise, to the most efficient codec the renderer advertises and this host can hardware-encode (HEVC at half the bitrate, else H.264), bounded by that codec's VBV-capped target.

A subtitle burn-in always forces the re-encode: drawtext needs decoded frames, so a copied bitstream cannot carry cues. The signal is opts.SubtitleTextFile, which the caller wires before calling this (the coupling EncodeArgs documents), so the decision stays a function of opts.

func Serve

func Serve(ctx context.Context, dev device.Device, p OpenParams) error

Serve runs one served cast end to end and is the single delivery entry point: pick the mechanism from the format's DeliveryKind, open it, wait until the device can be handed a URL, play, and block until delivered or ctx ends, tearing the encoder and server down on every return. It names no device family.

Types

type Config

type Config struct {
	Device    DeviceConfig
	Network   NetworkConfig
	Transcode TranscodeConfig
	Resolver  resolve.Config

	// Whisper is the subtitle-transcription knob NewPlan reads to choose the
	// subtitle axis (Enable gates burn-in). Its type lives in the cgo-free
	// subtitle package, not the whisper transcriber, so this decision core carries
	// it without importing whisper's cgo. A device that never serves (or a disabled
	// transcriber) simply resolves to SubtitleOff.
	Whisper subtitle.Whisper
}

Config is the device-neutral configuration the planner and every stage share: the target device, network, transcode binary, source resolver, and the subtitle transcriber. It composes only domain types, so core imports no application state; the app config assembles this and hands it in.

type DeliveryMode

type DeliveryMode int

DeliveryMode is how the renderer receives the stream bytes: it either fetches the source URL itself (pass-through) or castor produces a stream locally and serves it. This is the first axis of a Plan; the copy-vs-encode and subtitle decisions only ever apply on a served cast, so several stages gate on it.

const (
	// DeliverPassthrough hands the device the source URL and lets it pull the
	// bytes directly: no local ffmpeg, no served stream. Chosen only when the
	// renderer self-fetches AND already accepts the source container, so nothing
	// needs rewrapping.
	DeliverPassthrough DeliveryMode = iota
	// DeliverServe produces the stream locally (a remux or a transcode) and serves
	// it to the device. A push-only renderer always lands here (it never
	// self-fetches), as does a self-fetching renderer whose source container it
	// will not take.
	DeliverServe
)

func ResolveDelivery

func ResolveDelivery(caps media.Renderer, source *media.Stream) DeliveryMode

ResolveDelivery decides how the renderer receives the stream: pass-through (the device fetches the source URL itself) when it both self-fetches AND already accepts the source container, so there is nothing to rewrap; otherwise castor serves a locally produced stream. It is decided purely from capability data: a self-fetching renderer hands an accepted container straight to the device and has the rest remuxed, while a push-only renderer always serves.

type DeviceConfig

type DeviceConfig = device.Config

DeviceConfig is the device section, owned by the device package so this device-agnostic layer neither defines nor names any device family. Core reads only its generic Name/Type (for discovery) and forwards the whole value to device.Connect, which alone interprets any family-specific field.

type NetworkConfig

type NetworkConfig struct {
	Timeout   time.Duration `yaml:"timeout" validate:"required"`
	Interface string        `yaml:"interface"`
}

type OpenParams

type OpenParams struct {
	FFmpegPath string
	Opts       ffmpeg.EncodeOptions
	StartOpts  []ffmpeg.StartOption
	LocalIP    string
	WorkDir    string
	Format     media.FormatInfo
	// OnStarted, if set, runs immediately after the encoder starts (before the
	// server is fronted), so a caller can wire a concurrent consumer of a second
	// output pipe (the spool path follows -progress on proc.Extra) without that
	// coupling leaking into this package.
	OnStarted func(*ffmpeg.Process)
}

OpenParams are the device-blind inputs to open a delivery: the encoder to run and how its input/output is wired, where to serve from, and the format that selects the mechanism. Headers are filled by Serve from the device, not here.

type Plan

type Plan struct {
	// Delivery is pass-through versus locally served.
	Delivery DeliveryMode
	// Subtitle is off versus burned-in.
	Subtitle SubtitleMode
	// OutputContentType is the MIME type the device is told it is fetching: the
	// container the local ffmpeg muxes on a served cast, taken from the renderer's
	// declared ServedContainer. A pass-through cast reads the source's own content
	// type and never consults this, so it is left empty there.
	OutputContentType string
}

Plan is the pure decision record for one cast: what the executor must do, derived from (source, renderer capabilities, config) with no I/O. It is the single hand-off between the decision layer (this package) and the mechanism layer (the stages that pull, transcode, and serve).

It fixes only the axes that need no probe. It deliberately carries no encode invocation: copy-vs-encode needs a ProbeInfo, and the probe that matters is unavailable at planning time. The served spool path probes its local spool (the source URL can be single-use) and the network remux path probes the source URL, each obtained only once its stage runs, so each served stage builds its own EncodeOptions from the right probe (ResolveVideo + ResolveAudio). Nothing about the encode lives on the Plan.

func NewPlan

func NewPlan(source *media.Stream, caps media.Renderer, cfg Config) Plan

NewPlan computes the pure Plan for a cast from the source, the renderer's advertised capabilities, and config. It performs no I/O and reads no probe: it fixes only the axes that need none (delivery, subtitle, and the served output container). Each served stage then completes the encode against the probe it obtains (the source URL for a network remux, the local spool for the read-once spool path), which is why nothing about the encode lives on the Plan.

type Sink

type Sink interface {
	URL() *url.URL
	Wait(ctx context.Context) error
}

Sink is a running local server fronting a produced stream for one cast: it exposes the URL the renderer fetches and blocks until the stream is fully delivered. Closing it is the opener's job (its teardown holds the concrete server), so the driver only ever needs these two. Both replay.Server and hlsserve.Server satisfy it unchanged.

type SubtitleMode

type SubtitleMode int

SubtitleMode is the subtitle axis of a Plan. Stage 1 carries only the two modes today's code produces; a future SubtitleSidecar (a separately served caption track the device loads) is wired as a new enum value so the executor gains a branch without any reshuffle of the existing ones.

const (
	// SubtitleOff ships no subtitles: every pass-through cast, every served cast
	// with the transcriber disabled, and (this stage) every self-fetching renderer.
	SubtitleOff SubtitleMode = iota
	// SubtitleBurnIn transcribes the audio and draws the cues into the video
	// frames during the encode (whisper hardsubs). It forces a video re-encode
	// (drawtext needs decoded frames) and so is only reachable on a served cast.
	SubtitleBurnIn
)

func ResolveSubtitle

func ResolveSubtitle(caps media.Renderer, cfg Config) SubtitleMode

ResolveSubtitle decides the subtitle axis from the renderer and config. Stage 1 has one active mechanism, whisper burn-in, chosen exactly when the transcriber is enabled AND the renderer does not fetch its own bytes. A self-fetching renderer either passes the source through or remuxes it, and in neither case is there a drawtext encode to draw cues into (it takes captions as a native track in Stage 2); a push-only renderer always serves a local encode, which is exactly what burn-in needs. A future SubtitleSidecar will branch here on the renderer's advertised caption support.

type TranscodeConfig

type TranscodeConfig struct {
	FFmpegPath string        `yaml:"ffmpeg_path" validate:"required"`
	RWTimeout  time.Duration `yaml:"rw_timeout" validate:"required"`
}

TranscodeConfig holds the small set of ffmpeg settings that aren't decided by the plan. Codec/bitrate/format choices are resolved from capabilities (see the Resolve* functions); only the binary path and the upstream I/O timeout, which no capability can determine, come from config.

Jump to

Keyboard shortcuts

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