use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use ntgcalls::{
AudioDescription, DeviceInfo, MediaDescription, MediaDevices, MediaSource, NTgCalls,
VideoDescription,
};
use crate::error::TgCallsError;
fn has_stream(path: &str, selector: &str) -> bool {
let Ok(child) = Command::new("ffprobe")
.args([
"-v",
"error",
"-select_streams",
selector,
"-show_entries",
"stream=index",
"-of",
"csv=p=0",
path,
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
else {
return false;
};
match run_with_timeout(child, PROBE_TIMEOUT) {
Some(output) => output.status.success() && !output.stdout.is_empty(),
None => false,
}
}
pub fn auto_media(source: &str, width: i16, height: i16, fps: u8) -> MediaDescription {
let has_video = has_stream(source, "v");
let has_audio = has_stream(source, "a");
match (has_video, has_audio) {
(true, true) => Media::av(source, source, width, height, fps),
(true, false) => Media::video(source, width, height, fps),
_ => Media::audio(source),
}
}
pub fn auto_media_at(
source: &str,
offset: Duration,
width: i16,
height: i16,
fps: u8,
) -> MediaDescription {
let has_video = has_stream(source, "v");
let has_audio = has_stream(source, "a");
match (has_video, has_audio) {
(true, true) => Media::av_at(source, source, offset, width, height, fps),
(true, false) => Media::video_at(source, offset, width, height, fps),
_ => Media::audio_at(source, offset),
}
}
pub struct Media;
const PROBE_TIMEOUT: Duration = Duration::from_secs(8);
const RECONNECT_FLAGS: &str =
"-reconnect 1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2";
fn needs_reconnect(path: &str) -> bool {
!Path::new(path).exists()
}
fn run_with_timeout(
mut child: std::process::Child,
timeout: Duration,
) -> Option<std::process::Output> {
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) => {
if start.elapsed() > timeout {
let _ = child.kill();
let _ = child.wait();
return None;
}
std::thread::sleep(Duration::from_millis(50));
}
Err(_) => return None,
}
}
child.wait_with_output().ok()
}
pub fn probe_duration(path: &str) -> Option<Duration> {
let child = Command::new("ffprobe")
.args([
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
path,
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
let output = run_with_timeout(child, PROBE_TIMEOUT)?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
let secs: f64 = text.trim().parse().ok()?;
(secs > 0.0 && secs.is_finite()).then(|| Duration::from_secs_f64(secs))
}
fn probe_dimensions(path: &str) -> Option<(u32, u32)> {
let child = Command::new("ffprobe")
.args([
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height",
"-of",
"csv=s=x:p=0",
path,
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
let output = run_with_timeout(child, PROBE_TIMEOUT)?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
let line = text.lines().find(|l| l.contains('x'))?;
let mut parts = line.trim().split('x');
let w: u32 = parts.next()?.parse().ok()?;
let h: u32 = parts.next()?.parse().ok()?;
(w > 0 && h > 0).then_some((w, h))
}
fn fit_even(orig_w: u32, orig_h: u32, target_w: u32, target_h: u32) -> (u32, u32) {
let ratio = orig_w as f64 / orig_h as f64;
let mut new_w = target_w.min(orig_w).max(2);
let mut new_h = (new_w as f64 / ratio).round() as u32;
if new_h > target_h {
new_h = target_h.max(2);
new_w = (new_h as f64 * ratio).round() as u32;
}
if !new_w.is_multiple_of(2) {
new_w -= 1;
}
if !new_h.is_multiple_of(2) {
new_h -= 1;
}
(new_w.max(2), new_h.max(2))
}
fn resolve_dims(path: &str, width: i16, height: i16) -> (u32, u32) {
let base_w = (width.max(2) as u32) & !1;
let base_h = (height.max(2) as u32) & !1;
match probe_dimensions(path) {
Some((ow, oh)) => fit_even(ow, oh, base_w, base_h),
None => (base_w, base_h),
}
}
fn audio_cmd(path: &str, offset: Duration) -> String {
let reconnect = if needs_reconnect(path) {
RECONNECT_FLAGS
} else {
""
};
let seek = seek_flag(offset);
format!(
"ffmpeg -v error {seek}{reconnect} -i {path:?} -vn -f s16le -ar 48000 -ac 2 pipe:1",
seek = seek,
reconnect = reconnect,
path = path,
)
}
fn video_cmd(path: &str, w: u32, h: u32, fps: u8, offset: Duration) -> String {
let reconnect = if needs_reconnect(path) {
RECONNECT_FLAGS
} else {
""
};
let seek = seek_flag(offset);
format!(
"ffmpeg -v error {seek}{reconnect} -i {path:?} -an -f rawvideo -pix_fmt yuv420p \
-vf scale={w}:{h},fps={fps} pipe:1",
seek = seek,
reconnect = reconnect,
path = path,
)
}
fn seek_flag(offset: Duration) -> String {
if offset.is_zero() {
String::new()
} else {
format!("-ss {:.3} ", offset.as_secs_f64())
}
}
fn record_audio_cmd(path: &str) -> String {
format!("ffmpeg -v error -f s16le -ar 48000 -ac 2 -i pipe:0 -y {path:?}")
}
fn record_video_cmd(path: &str, w: u32, h: u32, fps: u8) -> String {
format!(
"ffmpeg -v error -f rawvideo -pix_fmt yuv420p -s {w}x{h} -r {fps} -i pipe:0 -y {path:?}"
)
}
impl Media {
pub fn audio(path: impl Into<String>) -> MediaDescription {
let path = path.into();
MediaDescription {
microphone: Some(AudioDescription {
media_source: MediaSource::Shell,
sample_rate: 48000,
channel_count: 2,
input: audio_cmd(&path, Duration::ZERO),
keep_open: false,
}),
speaker: None,
camera: None,
screen: None,
}
}
pub fn audio_at(path: impl Into<String>, offset: Duration) -> MediaDescription {
let path = path.into();
MediaDescription {
microphone: Some(AudioDescription {
media_source: MediaSource::Shell,
sample_rate: 48000,
channel_count: 2,
input: audio_cmd(&path, offset),
keep_open: false,
}),
speaker: None,
camera: None,
screen: None,
}
}
pub fn audio_raw(path: impl Into<String>) -> MediaDescription {
MediaDescription {
microphone: Some(AudioDescription {
media_source: MediaSource::File,
sample_rate: 48000,
channel_count: 2,
input: path.into(),
keep_open: false,
}),
speaker: None,
camera: None,
screen: None,
}
}
pub fn video(path: impl Into<String>, width: i16, height: i16, fps: u8) -> MediaDescription {
let path = path.into();
let (w, h) = resolve_dims(&path, width, height);
MediaDescription {
microphone: None,
speaker: None,
camera: Some(VideoDescription {
media_source: MediaSource::Shell,
width: w as i16,
height: h as i16,
fps,
input: video_cmd(&path, w, h, fps, Duration::ZERO),
keep_open: false,
}),
screen: None,
}
}
pub fn video_at(
path: impl Into<String>,
offset: Duration,
width: i16,
height: i16,
fps: u8,
) -> MediaDescription {
let path = path.into();
let (w, h) = resolve_dims(&path, width, height);
MediaDescription {
microphone: None,
speaker: None,
camera: Some(VideoDescription {
media_source: MediaSource::Shell,
width: w as i16,
height: h as i16,
fps,
input: video_cmd(&path, w, h, fps, offset),
keep_open: false,
}),
screen: None,
}
}
pub fn av(
audio_path: impl Into<String>,
video_path: impl Into<String>,
width: i16,
height: i16,
fps: u8,
) -> MediaDescription {
let audio_path = audio_path.into();
let video_path = video_path.into();
let (w, h) = resolve_dims(&video_path, width, height);
MediaDescription {
microphone: Some(AudioDescription {
media_source: MediaSource::Shell,
sample_rate: 48000,
channel_count: 2,
input: audio_cmd(&audio_path, Duration::ZERO),
keep_open: false,
}),
speaker: None,
camera: Some(VideoDescription {
media_source: MediaSource::Shell,
width: w as i16,
height: h as i16,
fps,
input: video_cmd(&video_path, w, h, fps, Duration::ZERO),
keep_open: false,
}),
screen: None,
}
}
pub fn av_at(
audio_path: impl Into<String>,
video_path: impl Into<String>,
offset: Duration,
width: i16,
height: i16,
fps: u8,
) -> MediaDescription {
let audio_path = audio_path.into();
let video_path = video_path.into();
let (w, h) = resolve_dims(&video_path, width, height);
MediaDescription {
microphone: Some(AudioDescription {
media_source: MediaSource::Shell,
sample_rate: 48000,
channel_count: 2,
input: audio_cmd(&audio_path, offset),
keep_open: false,
}),
speaker: None,
camera: Some(VideoDescription {
media_source: MediaSource::Shell,
width: w as i16,
height: h as i16,
fps,
input: video_cmd(&video_path, w, h, fps, offset),
keep_open: false,
}),
screen: None,
}
}
pub fn screen(width: i16, height: i16, fps: u8) -> VideoDescription {
VideoDescription {
media_source: MediaSource::Desktop,
width,
height,
fps,
input: String::new(),
keep_open: false,
}
}
pub fn external_video(width: i16, height: i16, fps: u8) -> VideoDescription {
VideoDescription {
media_source: MediaSource::External,
width,
height,
fps,
input: String::new(),
keep_open: true,
}
}
pub fn external_audio(sample_rate: u32, channels: u8) -> AudioDescription {
AudioDescription {
media_source: MediaSource::External,
sample_rate,
channel_count: channels,
input: String::new(),
keep_open: true,
}
}
pub fn list_devices() -> Result<MediaDevices, TgCallsError> {
Ok(NTgCalls::get_media_devices()?)
}
pub fn microphone(device: &DeviceInfo) -> AudioDescription {
AudioDescription {
media_source: MediaSource::Device,
sample_rate: 48000,
channel_count: 2,
input: device.metadata.clone(),
keep_open: true,
}
}
pub fn camera(device: &DeviceInfo, width: i16, height: i16, fps: u8) -> VideoDescription {
VideoDescription {
media_source: MediaSource::Device,
width,
height,
fps,
input: device.metadata.clone(),
keep_open: true,
}
}
pub fn speaker(device: &DeviceInfo) -> AudioDescription {
AudioDescription {
media_source: MediaSource::Device,
sample_rate: 48000,
channel_count: 2,
input: device.metadata.clone(),
keep_open: true,
}
}
pub fn record_audio(path: impl Into<String>) -> MediaDescription {
let path = path.into();
MediaDescription {
microphone: Some(AudioDescription {
media_source: MediaSource::Shell,
sample_rate: 48000,
channel_count: 2,
input: record_audio_cmd(&path),
keep_open: false,
}),
speaker: None,
camera: None,
screen: None,
}
}
pub fn record_video(
path: impl Into<String>,
width: i16,
height: i16,
fps: u8,
) -> MediaDescription {
let path = path.into();
let w = (width.max(2) as u32) & !1;
let h = (height.max(2) as u32) & !1;
MediaDescription {
microphone: None,
speaker: None,
camera: Some(VideoDescription {
media_source: MediaSource::Shell,
width: w as i16,
height: h as i16,
fps,
input: record_video_cmd(&path, w, h, fps),
keep_open: false,
}),
screen: None,
}
}
pub fn record_screen(
path: impl Into<String>,
width: i16,
height: i16,
fps: u8,
) -> MediaDescription {
let path = path.into();
let w = (width.max(2) as u32) & !1;
let h = (height.max(2) as u32) & !1;
MediaDescription {
microphone: None,
speaker: None,
camera: None,
screen: Some(VideoDescription {
media_source: MediaSource::Shell,
width: w as i16,
height: h as i16,
fps,
input: record_video_cmd(&path, w, h, fps),
keep_open: false,
}),
}
}
}