This commit is contained in:
Nikita
2025-12-21 13:37:19 -08:00
commit 8cd4b943a5
173 changed files with 44266 additions and 0 deletions
+297
View File
@@ -0,0 +1,297 @@
use std::path::Path;
use std::process::Stdio;
use anyhow::{Context, Result};
use tokio::process::{Child, Command};
use tracing::info;
pub struct ReceiverHandle {
child: Child,
}
impl ReceiverHandle {
pub async fn stop(mut self) {
// Send SIGTERM
if let Err(e) = self.child.kill().await {
tracing::warn!("Failed to kill ffmpeg: {e}");
}
}
}
/// Start ffmpeg to receive SRT stream and segment into files.
///
/// Output files: segment_dir/stream-YYYYMMDD-HHMMSS-NNN.ts
pub async fn start(
ffmpeg_path: &Path,
srt_port: u16,
segment_dir: &Path,
segment_duration: u32,
) -> Result<ReceiverHandle> {
let srt_url = format!("srt://0.0.0.0:{srt_port}?mode=listener");
let output_pattern = segment_dir.join("stream-%Y%m%d-%H%M%S-%%03d.ts");
// ffmpeg command:
// - Listen for SRT input
// - Copy video and audio (no re-encoding)
// - Segment into files with timestamps
let mut cmd = Command::new(ffmpeg_path);
cmd.args([
"-hide_banner",
"-loglevel",
"warning",
// Input
"-i",
&srt_url,
// Copy codecs (no re-encoding)
"-c",
"copy",
// Segment muxer
"-f",
"segment",
"-segment_time",
&segment_duration.to_string(),
"-segment_format",
"mpegts",
// Use strftime for timestamp in filename
"-strftime",
"1",
// Reset timestamps for each segment
"-reset_timestamps",
"1",
// Output pattern
output_pattern.to_str().unwrap(),
]);
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
info!("Starting ffmpeg receiver: {:?}", cmd);
let child = cmd
.spawn()
.with_context(|| format!("spawn ffmpeg at {}", ffmpeg_path.display()))?;
Ok(ReceiverHandle { child })
}
/// Start ffmpeg to receive SRT stream and output HLS for web playback.
///
/// Generates a live HLS playlist (.m3u8) and segment files (.ts)
/// that can be served via HTTP for web players.
pub async fn start_hls(
ffmpeg_path: &Path,
srt_port: u16,
hls_dir: &Path,
segment_duration: u32,
) -> Result<ReceiverHandle> {
let srt_url = format!("srt://0.0.0.0:{srt_port}?mode=listener");
let playlist_path = hls_dir.join("stream.m3u8");
let segment_pattern = hls_dir.join("stream%03d.ts");
// Ensure HLS directory exists
std::fs::create_dir_all(hls_dir)?;
// ffmpeg command for HLS output:
// - Receive SRT input (already H.264 from Mac hardware encoder)
// - Copy video codec (no re-encoding)
// - Ensure audio is AAC (required for HLS)
// - Output HLS playlist and segments
let mut cmd = Command::new(ffmpeg_path);
cmd.args([
"-hide_banner",
"-loglevel",
"warning",
// Input from SRT
"-i",
&srt_url,
// Video: copy (no re-encoding)
"-c:v",
"copy",
// Audio: AAC for HLS compatibility
"-c:a",
"aac",
"-b:a",
"128k",
"-ar",
"44100",
// HLS output settings
"-f",
"hls",
"-hls_time",
&segment_duration.to_string(),
"-hls_list_size",
"10", // Keep 10 segments in playlist
"-hls_flags",
"delete_segments+append_list",
"-hls_segment_filename",
segment_pattern.to_str().unwrap(),
// Output playlist
playlist_path.to_str().unwrap(),
]);
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
info!("Starting HLS output to {}", hls_dir.display());
let child = cmd
.spawn()
.with_context(|| format!("spawn ffmpeg at {}", ffmpeg_path.display()))?;
Ok(ReceiverHandle { child })
}
/// Start ffmpeg to receive SRT stream and forward to YouTube RTMP.
///
/// Receives hardware-encoded H.264 from Mac, applies optional filters,
/// and streams directly to YouTube with minimal re-encoding.
pub async fn start_youtube(
ffmpeg_path: &Path,
srt_port: u16,
rtmp_url: &str,
stream_key: &str,
) -> Result<ReceiverHandle> {
let srt_url = format!("srt://0.0.0.0:{srt_port}?mode=listener");
let youtube_url = format!("{}/{}", rtmp_url, stream_key);
// ffmpeg command for YouTube streaming:
// - Receive SRT input (already H.264 from Mac hardware encoder)
// - Copy video if already H.264, or re-encode if filtering needed
// - Ensure audio is AAC (YouTube requirement)
// - Output to RTMP
let mut cmd = Command::new(ffmpeg_path);
cmd.args([
"-hide_banner",
"-loglevel",
"warning",
// Reconnect on errors
"-reconnect",
"1",
"-reconnect_streamed",
"1",
"-reconnect_delay_max",
"5",
// Input from SRT
"-i",
&srt_url,
// Video: copy if already H.264 (from Mac VideoToolbox)
"-c:v",
"copy",
// Audio: ensure AAC for YouTube
"-c:a",
"aac",
"-b:a",
"128k",
"-ar",
"44100",
// FLV container for RTMP
"-f",
"flv",
// Buffer settings for stable streaming
"-flvflags",
"no_duration_filesize",
"-max_muxing_queue_size",
"1024",
// Output to YouTube
&youtube_url,
]);
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
info!("Starting YouTube stream to {}", rtmp_url);
let child = cmd
.spawn()
.with_context(|| format!("spawn ffmpeg at {}", ffmpeg_path.display()))?;
Ok(ReceiverHandle { child })
}
/// Start ffmpeg to receive SRT, apply filters, and stream to YouTube.
///
/// This variant decodes, applies filters, and re-encodes.
/// Use when you need video processing on the Linux side.
pub async fn start_youtube_with_filter(
ffmpeg_path: &Path,
srt_port: u16,
rtmp_url: &str,
stream_key: &str,
video_filter: Option<&str>,
) -> Result<ReceiverHandle> {
let srt_url = format!("srt://0.0.0.0:{srt_port}?mode=listener");
let youtube_url = format!("{}/{}", rtmp_url, stream_key);
let mut cmd = Command::new(ffmpeg_path);
let mut args = vec![
"-hide_banner".to_string(),
"-loglevel".to_string(),
"warning".to_string(),
"-reconnect".to_string(),
"1".to_string(),
"-reconnect_streamed".to_string(),
"1".to_string(),
"-reconnect_delay_max".to_string(),
"5".to_string(),
"-i".to_string(),
srt_url,
];
// Add video filter if specified
if let Some(filter) = video_filter {
args.extend(["-vf".to_string(), filter.to_string()]);
}
// Video encoding (x264 with fast preset for low latency)
args.extend([
"-c:v".to_string(),
"libx264".to_string(),
"-preset".to_string(),
"veryfast".to_string(),
"-tune".to_string(),
"zerolatency".to_string(),
"-b:v".to_string(),
"4500k".to_string(),
"-maxrate".to_string(),
"4500k".to_string(),
"-bufsize".to_string(),
"9000k".to_string(),
"-g".to_string(),
"60".to_string(), // Keyframe every 2s at 30fps
]);
// Audio
args.extend([
"-c:a".to_string(),
"aac".to_string(),
"-b:a".to_string(),
"128k".to_string(),
"-ar".to_string(),
"44100".to_string(),
]);
// Output
args.extend([
"-f".to_string(),
"flv".to_string(),
"-flvflags".to_string(),
"no_duration_filesize".to_string(),
youtube_url,
]);
cmd.args(&args);
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
info!("Starting YouTube stream with filter");
let child = cmd
.spawn()
.with_context(|| format!("spawn ffmpeg at {}", ffmpeg_path.display()))?;
Ok(ReceiverHandle { child })
}