every.channel: sanitized baseline
This commit is contained in:
commit
897e556bea
258 changed files with 74298 additions and 0 deletions
4193
crates/ec-node/src/main.rs
Normal file
4193
crates/ec-node/src/main.rs
Normal file
File diff suppressed because it is too large
Load diff
283
crates/ec-node/src/source.rs
Normal file
283
crates/ec-node/src/source.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue