mod cli;
mod config;
mod flags;
mod gpu;
mod iso;
mod pipeline;
mod probe;
mod scanner;
mod setup;
mod telemetry;
mod transcode;
mod ui;
mod util;
use anyhow::{Context, Result};
use clap::Parser;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use cli::Cli;
use pipeline::worker::WorkerCtx;
use ui::{detect_symbols, embedded_config_banner, max_name_for_cols, terminal_cols};
use util::format_size;
struct LdGuard {
flags: Arc<flags::Flags>,
telemetry: telemetry::Telemetry,
}
impl Drop for LdGuard {
fn drop(&mut self) {
self.flags.close();
self.telemetry.shutdown();
}
}
fn main() -> Result<()> {
env_logger::init();
let cli = Cli::parse();
if cli.setup_launchdarkly {
let api_key = cli
.ld_api_key
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--setup-launchdarkly requires --ld-api-key <KEY>"))?;
return setup::run(api_key);
}
if cli.dump_config {
print!("{}", config::EMBEDDED);
return Ok(());
}
let sdk_key = cli.launchdarkly_sdk_key.as_deref();
let mut ld_flags = flags::Flags::new(sdk_key);
let path = cli.path.as_deref().unwrap();
install_sigint_handler();
let sym = detect_symbols();
let use_unicode = std::ptr::eq(sym, &ui::UNICODE_SYMBOLS);
let stdout_is_pipe = unsafe { libc::isatty(libc::STDOUT_FILENO) == 0 };
let max_name = max_name_for_cols(terminal_cols());
let gpu = gpu::detect_gpu()?;
let cfg = match &cli.config {
Some(p) => config::Config::load(p)
.with_context(|| format!("Failed to load config from {:?}", p))?,
None => {
if !cli.quiet {
eprintln!("{}", embedded_config_banner(use_unicode));
}
config::Config::from_embedded()
}
};
eprintln!("GPU: {} ({})", gpu.name, gpu.encoder);
if !gpu.supports_10bit_hevc {
eprintln!(
" note: this GPU's NVENC does not support 10-bit HEVC; \
10-bit sources will be skipped."
);
}
let gpu_kind = match gpu.kind {
gpu::GpuKind::Nvidia => "nvidia",
gpu::GpuKind::Intel => "intel",
gpu::GpuKind::Apple => "apple",
};
ld_flags.set_gpu(&gpu.name, &gpu.encoder, gpu_kind);
let max_sessions = gpu::max_encode_sessions(&gpu);
let mut ld_guard = LdGuard {
flags: Arc::new(ld_flags),
telemetry: telemetry::Telemetry::new(sdk_key),
};
let flags = Arc::clone(&ld_guard.flags);
flags.track_gpu_detected(&gpu.name, &gpu.encoder, gpu_kind, max_sessions);
if let Ok(avail) = util::available_disk_space(path) {
eprintln!("Disk: {} available", format_size(avail));
}
if let Some(fs) = scanner::detect_network_mount(path) {
eprintln!(
"Note: {:?} is on a {} mount; ffprobe/scan operations may hang \
on unresponsive shares. Override probe timeout with --probe-timeout.",
path, fs
);
}
let files = scanner::scan(path, &cfg.media_extensions)?;
if files.is_empty() {
eprintln!("No media files found in {:?}", path);
return Ok(());
}
let scan_result = pipeline::scan::expand(&files);
let mut errors = scan_result.errors;
eprintln!("Scanning {} files...", scan_result.items.len());
let total_bytes: u64 = {
let mut seen = std::collections::HashSet::new();
scan_result
.items
.iter()
.filter(|item| seen.insert(&item.file))
.filter_map(|item| std::fs::metadata(&item.file).ok())
.map(|m| m.len())
.sum()
};
flags.track_scan_completed(scan_result.items.len(), total_bytes);
let part = pipeline::partition::partition(&scan_result.items, &cli, &cfg, &gpu);
let skipped = part.skipped;
let resumed = part.resumed;
errors += part.errors;
let to_transcode = part.to_transcode;
if to_transcode.is_empty() {
if resumed > 0 {
eprintln!(
"Nothing to do: {} already HEVC, {} already transcoded",
skipped, resumed
);
} else {
eprintln!("All {} files already meet target.", scan_result.items.len());
}
return Ok(());
}
let jobs_specified = cli.jobs.is_some();
let jobs = cli
.jobs
.unwrap_or_else(|| gpu::max_encode_sessions(&gpu) + 3)
.max(1);
let auto_ramp = !jobs_specified;
eprintln!(
"{} to transcode, {} already HEVC{}, {}",
to_transcode.len(),
skipped,
if resumed > 0 {
format!(", {} resumed", resumed)
} else {
String::new()
},
if auto_ramp {
"auto concurrency".to_string()
} else {
format!("{} jobs", jobs)
},
);
if cli.dry_run {
for item in &to_transcode {
let name = item.path.file_name().unwrap_or_default().to_string_lossy();
eprintln!(
" {name} ({}, {} kbps, {})",
item.pix_fmt,
item.bitrate_kbps,
format_size(item.source_size)
);
}
let total_size: u64 = to_transcode.iter().map(|i| i.source_size).sum();
eprintln!(
"\nDry run: {} would be transcoded ({})",
to_transcode.len(),
format_size(total_size)
);
return Ok(());
}
if !flags.enable_transcoding() {
eprintln!("Aborted: LaunchDarkly enable-transcoding flag is false.");
return Ok(());
}
{
let mut dirs = pipeline::TMP_DIRS.lock().unwrap();
for item in &to_transcode {
if let Some(parent) = item.path.parent() {
let p = parent.to_path_buf();
if !dirs.contains(&p) {
dirs.push(p);
}
}
}
}
let transcoded = Arc::new(AtomicU32::new(0));
let phase3_errors = Arc::new(AtomicU32::new(0));
let bytes_saved = Arc::new(AtomicU64::new(0));
let bytes_input = Arc::new(AtomicU64::new(0));
let bytes_output = Arc::new(AtomicU64::new(0));
let total_transcode_bytes: u64 = to_transcode.iter().map(|i| i.source_size).sum();
flags.track_run_started(to_transcode.len(), total_transcode_bytes, jobs, auto_ramp);
let to_transcode = Arc::new(to_transcode);
run_transcode_phase(
Arc::clone(&to_transcode),
&cli,
cfg.clone(),
gpu.clone(),
jobs,
auto_ramp,
stdout_is_pipe,
sym,
max_name,
Arc::clone(&transcoded),
Arc::clone(&phase3_errors),
Arc::clone(&bytes_saved),
Arc::clone(&bytes_input),
Arc::clone(&bytes_output),
Arc::clone(&flags),
);
pipeline::cleanup_tmp_dirs();
if pipeline::LD_KILL.load(Ordering::Relaxed) {
eprintln!(
"\nStopped: {} transcoded before LaunchDarkly kill-switch",
transcoded.load(Ordering::Relaxed)
);
} else if pipeline::CANCELLED.load(Ordering::Relaxed) {
eprintln!(
"\nCancelled: {} transcoded before interrupt",
transcoded.load(Ordering::Relaxed)
);
std::process::exit(130);
}
if pipeline::LD_KILL.load(Ordering::Relaxed) {
eprintln!(
"\nStopped by LaunchDarkly: enable-transcoding=false ({} transcoded)",
transcoded.load(Ordering::Relaxed)
);
}
if cli.replace && !cli.overwrite() {
pipeline::replace::run(&to_transcode, &cli, &cfg, sym);
}
drop(to_transcode);
let final_transcoded = transcoded.load(Ordering::Relaxed);
let final_errors = errors + phase3_errors.load(Ordering::Relaxed);
let total_saved = bytes_saved.load(Ordering::Relaxed);
let total_input = bytes_input.load(Ordering::Relaxed);
let total_output = bytes_output.load(Ordering::Relaxed);
eprintln!(
"\nDone: {} transcoded, {} skipped, {} errors",
final_transcoded, skipped, final_errors
);
if total_input > 0 {
let pct = (total_saved as f64 / total_input as f64) * 100.0;
eprintln!(
"Size: {} {} {} (saved {}, {:.0}% reduction)",
format_size(total_input),
sym.arrow,
format_size(total_output),
format_size(total_saved),
pct
);
}
flags.track_run_completed(
final_transcoded,
skipped,
final_errors,
total_saved,
total_input,
total_output,
);
flags.close();
ld_guard.telemetry.shutdown();
let _ = (resumed,);
if final_errors > 0 {
std::process::exit(1);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn run_transcode_phase(
to_transcode: Arc<Vec<pipeline::WorkItem>>,
cli: &Cli,
cfg: config::Config,
gpu: gpu::GpuInfo,
jobs: usize,
auto_ramp: bool,
stdout_is_pipe: bool,
sym: &'static ui::Symbols,
max_name: usize,
transcoded: Arc<AtomicU32>,
phase3_errors: Arc<AtomicU32>,
bytes_saved: Arc<AtomicU64>,
bytes_input: Arc<AtomicU64>,
bytes_output: Arc<AtomicU64>,
flags: Arc<flags::Flags>,
) {
let total_units = to_transcode.len() as u64 * 1000;
let file_count = to_transcode.len() as u64;
let worker_slots: Vec<Arc<pipeline::WorkerSlot>> = (0..jobs)
.map(|_| Arc::new(pipeline::WorkerSlot::new()))
.collect();
let completed_lines: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let completed_units = Arc::new(AtomicU64::new(0));
let active_encoders = Arc::new(AtomicU32::new(0));
let initial_max = if auto_ramp { 1u32 } else { jobs as u32 };
let max_encoders = Arc::new(AtomicU32::new(initial_max));
let ramping = Arc::new(AtomicBool::new(auto_ramp));
let worker_count = Arc::new(AtomicU32::new(jobs as u32));
let session_limit_hits = Arc::new(AtomicU32::new(0));
let session_limit_frozen = Arc::new(AtomicBool::new(false));
let min_observed_max = Arc::new(AtomicU32::new(u32::MAX));
let disk_reserved = Arc::new(AtomicU64::new(0));
let next_idx = Arc::new(AtomicU32::new(0));
let cfg = Arc::new(cfg);
let gpu = Arc::new(gpu);
let output_dir = cli.output_dir.clone();
let overwrite = cli.overwrite();
std::thread::scope(|s| {
{
let render_ctx = pipeline::render::RenderCtx {
slots: worker_slots.iter().map(Arc::clone).collect(),
completed_lines: Arc::clone(&completed_lines),
completed_units: Arc::clone(&completed_units),
transcoded: Arc::clone(&transcoded),
errors: Arc::clone(&phase3_errors),
max_encoders: Arc::clone(&max_encoders),
ramping: Arc::clone(&ramping),
session_limit_frozen: Arc::clone(&session_limit_frozen),
total_units,
file_count,
sym,
flags: Arc::clone(&flags),
};
s.spawn(move || pipeline::render::run_render(render_ctx));
}
for slot in &worker_slots {
let ctx = WorkerCtx {
to_transcode: Arc::clone(&to_transcode),
next_idx: Arc::clone(&next_idx),
cfg: Arc::clone(&cfg),
gpu: Arc::clone(&gpu),
output_dir: output_dir.clone(),
overwrite,
auto_ramp,
jobs,
stdout_is_pipe,
sym,
max_name,
transcoded: Arc::clone(&transcoded),
error_count: Arc::clone(&phase3_errors),
bytes_saved: Arc::clone(&bytes_saved),
bytes_input: Arc::clone(&bytes_input),
bytes_output: Arc::clone(&bytes_output),
completed_units: Arc::clone(&completed_units),
completed_lines: Arc::clone(&completed_lines),
active_encoders: Arc::clone(&active_encoders),
max_encoders: Arc::clone(&max_encoders),
worker_count: Arc::clone(&worker_count),
ramping: Arc::clone(&ramping),
session_limit_hits: Arc::clone(&session_limit_hits),
session_limit_frozen: Arc::clone(&session_limit_frozen),
min_observed_max: Arc::clone(&min_observed_max),
disk_reserved: Arc::clone(&disk_reserved),
flags: Arc::clone(&flags),
};
let my_slot = Arc::clone(slot);
s.spawn(move || pipeline::worker::run_worker(ctx, my_slot));
}
});
if !pipeline::CANCELLED.load(Ordering::Relaxed) {
let lines = completed_lines.lock().unwrap();
for line in lines.iter() {
eprintln!("{}", line);
}
}
}
extern "C" fn sigint_handler(_: libc::c_int) {
const RESET: &[u8] = b"\x1b[0m\r\n";
const MSG: &[u8] = b"Cancelling after current encodes finish... (Ctrl-C again to force quit)\n";
unsafe { libc::write(2, RESET.as_ptr() as *const libc::c_void, RESET.len()) };
if pipeline::CANCELLED.load(Ordering::Relaxed) {
unsafe { libc::_exit(130) };
}
pipeline::CANCELLED.store(true, Ordering::Relaxed);
unsafe { libc::write(2, MSG.as_ptr() as *const libc::c_void, MSG.len()) };
}
fn install_sigint_handler() {
unsafe {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = sigint_handler as *const () as libc::sighandler_t;
libc::sigemptyset(&mut sa.sa_mask);
sa.sa_flags = libc::SA_RESTART;
libc::sigaction(libc::SIGINT, &sa, std::ptr::null_mut());
}
}