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 ¶
- func Connect(ctx context.Context, cfg Config) (device.Device, error)
- func ResolveAudio(opts *ffmpeg.EncodeOptions, caps media.Renderer, src media.ProbeInfo)
- func ResolveSource(ctx context.Context, cfg Config, stream *media.Stream) (*media.Stream, string, error)
- func ResolveVideo(ctx context.Context, opts *ffmpeg.EncodeOptions, caps media.Renderer, ...)
- func Serve(ctx context.Context, dev device.Device, p OpenParams) error
- type Config
- type DeliveryMode
- type DeviceConfig
- type NetworkConfig
- type OpenParams
- type Plan
- type Sink
- type SubtitleMode
- type TranscodeConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Connect ¶
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 ¶
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):
- 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;
- 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;
- 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 ¶
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 ¶
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 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 ¶
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 ¶
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.