every.channel: sanitized baseline

This commit is contained in:
every.channel 2026-02-15 16:17:27 -05:00
commit 897e556bea
No known key found for this signature in database
258 changed files with 74298 additions and 0 deletions

4193
crates/ec-node/src/main.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,283 @@
use anyhow::{anyhow, Result};
use clap::ValueEnum;
use ec_chopper::{deterministic_h264_profile, ffmpeg_profile_args};
use ec_core::SourceId;
use ec_hdhomerun::{find_lineup_entry_by_name, find_lineup_entry_by_number};
use ec_linux_iptv::LinuxDvbConfig;
use std::io::Read;
use std::process::{Child, Command, Stdio};
use std::thread;
pub trait StreamSource: Send {
fn open_stream(&self) -> Result<Box<dyn Read + Send>>;
fn source_id(&self) -> SourceId;
}
#[derive(Debug, Clone)]
pub struct HdhrSource {
pub host: Option<String>,
pub device_id: Option<String>,
pub channel: Option<String>,
pub name: Option<String>,
pub prefer_mdns: bool,
}
impl StreamSource for HdhrSource {
fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
let device = resolve_hdhr_device(self)?;
let lineup = ec_hdhomerun::fetch_lineup(&device)?;
let entry = if let Some(channel) = &self.channel {
find_lineup_entry_by_number(&lineup, channel)
.or_else(|| find_lineup_entry_by_name(&lineup, channel))
.ok_or_else(|| anyhow!("channel not found: {channel}"))?
} else if let Some(name) = &self.name {
find_lineup_entry_by_name(&lineup, name)
.ok_or_else(|| anyhow!("channel not found: {name}"))?
} else {
return Err(anyhow!("--channel or --name required for hdhr"));
};
Ok(Box::new(ec_hdhomerun::open_stream_entry(entry, None)?))
}
fn source_id(&self) -> SourceId {
let device_id = self.device_id.clone().or_else(|| self.host.clone());
SourceId {
kind: "hdhr".to_string(),
device_id,
channel: self.channel.clone().or_else(|| self.name.clone()),
}
}
}
fn resolve_hdhr_device(source: &HdhrSource) -> Result<ec_hdhomerun::HdhomerunDevice> {
if let Some(host) = &source.host {
return ec_hdhomerun::discover_from_host(host);
}
if let Some(device_id) = &source.device_id {
let host = format!("{device_id}.local");
return ec_hdhomerun::discover_from_host(&host);
}
if source.prefer_mdns {
if let Ok(device) = ec_hdhomerun::discover_from_host("hdhomerun.local") {
return Ok(device);
}
}
let mut devices = ec_hdhomerun::discover()?;
devices
.pop()
.ok_or_else(|| anyhow!("no HDHomeRun devices found"))
}
#[derive(Debug, Clone)]
pub struct LinuxDvbSource {
pub adapter: u32,
pub dvr: u32,
pub tune_cmd: Vec<String>,
pub tune_wait_ms: Option<u64>,
}
impl StreamSource for LinuxDvbSource {
fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
let config = LinuxDvbConfig {
adapter: self.adapter,
frontend: 0,
dvr: self.dvr,
tune_command: if self.tune_cmd.is_empty() {
None
} else {
Some(self.tune_cmd.clone())
},
tune_timeout_ms: self.tune_wait_ms,
};
Ok(Box::new(ec_linux_iptv::open_stream(&config)?))
}
fn source_id(&self) -> SourceId {
SourceId {
kind: "linux-dvb".to_string(),
device_id: Some(format!("adapter{}:dvr{}", self.adapter, self.dvr)),
channel: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TsSource {
pub input: String,
}
impl StreamSource for TsSource {
fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
if self.input.starts_with("http://") || self.input.starts_with("https://") {
Ok(Box::new(ec_hdhomerun::open_stream_url(&self.input, None)?))
} else {
Ok(Box::new(std::fs::File::open(&self.input)?))
}
}
fn source_id(&self) -> SourceId {
SourceId {
kind: "ts".to_string(),
device_id: None,
channel: None,
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum HlsMode {
Passthrough,
Remux,
Transcode,
}
impl Default for HlsMode {
fn default() -> Self {
HlsMode::Passthrough
}
}
#[derive(Debug, Clone)]
pub struct HlsSource {
pub url: String,
pub mode: HlsMode,
}
impl StreamSource for HlsSource {
fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
let mut cmd = Command::new("ffmpeg");
cmd.arg("-hide_banner")
.arg("-loglevel")
.arg("error")
.arg("-nostdin")
.arg("-i")
.arg(&self.url);
match self.mode {
HlsMode::Passthrough => {
cmd.arg("-c").arg("copy");
}
HlsMode::Remux => {
cmd.arg("-fflags").arg("+genpts").arg("-c").arg("copy");
}
HlsMode::Transcode => {
let profile = deterministic_h264_profile();
for arg in ffmpeg_profile_args(&profile) {
cmd.arg(arg);
}
}
}
cmd.arg("-f")
.arg("mpegts")
.arg("pipe:1")
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd
.spawn()
.map_err(|err| anyhow!("failed to spawn ffmpeg: {err}"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("ffmpeg stdout unavailable"))?;
Ok(Box::new(FfmpegChildStream { child, stdout }))
}
fn source_id(&self) -> SourceId {
SourceId {
kind: "hls".to_string(),
device_id: None,
channel: Some(self.url.clone()),
}
}
}
struct FfmpegChildStream {
child: Child,
stdout: std::process::ChildStdout,
}
impl Read for FfmpegChildStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.stdout.read(buf)
}
}
impl Drop for FfmpegChildStream {
fn drop(&mut self) {
let _ = self.child.kill();
}
}
pub fn deterministic_transcode_stream(
reader: Box<dyn Read + Send>,
) -> Result<Box<dyn Read + Send>> {
let profile = deterministic_h264_profile();
let mut cmd = Command::new("ffmpeg");
cmd.arg("-hide_banner")
.arg("-loglevel")
.arg("error")
.arg("-nostdin")
.arg("-i")
.arg("pipe:0");
for arg in ffmpeg_profile_args(&profile) {
cmd.arg(arg);
}
cmd.arg("-f")
.arg("mpegts")
.arg("pipe:1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd
.spawn()
.map_err(|err| anyhow!("failed to spawn ffmpeg: {err}"))?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("ffmpeg stdout unavailable"))?;
let writer = thread::spawn(move || {
let mut reader = reader;
let _ = std::io::copy(&mut reader, &mut stdin);
});
Ok(Box::new(FfmpegTranscodeStream {
child,
stdout,
writer: Some(writer),
}))
}
struct FfmpegTranscodeStream {
child: Child,
stdout: std::process::ChildStdout,
writer: Option<thread::JoinHandle<()>>,
}
impl Read for FfmpegTranscodeStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.stdout.read(buf)
}
}
impl Drop for FfmpegTranscodeStream {
fn drop(&mut self) {
let _ = self.child.kill();
if let Some(writer) = self.writer.take() {
let _ = writer.join();
}
}
}