use crate::core::context::encoder_stream::EncSyncHandle;
use crate::core::context::ffmpeg_context::FfmpegContext;
use crate::core::context::obj_pool::ObjPool;
use crate::core::scheduler::dec_task::dec_init;
use crate::core::scheduler::demux_task::demux_init;
use crate::core::scheduler::enc_task::enc_init;
use crate::core::scheduler::filter_task::filter_graph_init;
use crate::core::scheduler::frame_filter_pipeline::{input_pipeline_init, output_pipeline_init};
use crate::core::scheduler::input_controller::InputController;
use crate::core::scheduler::mux_task::{mux_init, ready_to_init_mux};
use crate::core::scheduler::sync_queue::SyncQueue;
use crate::error::{AllocFrameError, AllocPacketError};
use crate::util::thread_synchronizer::ThreadSynchronizer;
use ffmpeg_next::packet::{Mut, Ref};
use ffmpeg_next::{Frame, Packet};
use ffmpeg_sys_next::AVMediaType::{AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO};
use ffmpeg_sys_next::{av_frame_alloc, av_frame_unref, av_packet_unref};
use std::marker::PhantomData;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, OnceLock};
use std::time::Duration;
pub struct Initialization;
pub struct Running;
pub struct Paused;
pub struct Ended;
struct RunningGuard {
status: Arc<AtomicUsize>,
thread_sync: ThreadSynchronizer,
demux_waiters: Vec<Arc<crate::util::sch_waiter::SchWaiter>>,
_interrupt_state: Arc<crate::core::context::InterruptState>,
}
impl Drop for RunningGuard {
fn drop(&mut self) {
if !is_stopping(self.status.load(Ordering::Acquire)) {
log::debug!("Drop called, setting STATUS_END");
self.status.store(STATUS_END, Ordering::Release);
}
notify_pause_waiters();
for waiter in &self.demux_waiters {
waiter.set(false);
}
log::debug!("Drop waiting for all threads to finish");
self.thread_sync.wait_for_all_threads();
}
}
pub struct FfmpegScheduler<S> {
ffmpeg_context: FfmpegContext,
status: Arc<AtomicUsize>,
thread_sync: ThreadSynchronizer,
result: Arc<Mutex<Option<crate::error::Result<()>>>>,
state: PhantomData<S>,
_guard: Option<RunningGuard>,
}
pub(crate) const STATUS_INIT: usize = 0;
pub(crate) const STATUS_RUN: usize = 1;
pub(crate) const STATUS_PAUSE: usize = 2;
pub(crate) const STATUS_ABORT: usize = 3;
pub(crate) const STATUS_END: usize = 4;
pub(crate) fn is_stopping(status: usize) -> bool {
status == STATUS_END || status == STATUS_ABORT
}
impl<S: 'static> FfmpegScheduler<S> {
#[allow(dead_code)]
fn is_state<T: 'static>(&self) -> bool {
std::any::TypeId::of::<S>() == std::any::TypeId::of::<T>()
}
fn into_state<T>(self) -> FfmpegScheduler<T> {
FfmpegScheduler {
ffmpeg_context: self.ffmpeg_context,
status: self.status,
thread_sync: self.thread_sync,
result: self.result,
state: Default::default(),
_guard: self._guard, }
}
fn signal_stop(&self) {
self.status.store(STATUS_END, Ordering::Release);
notify_pause_waiters();
self.wake_demux_waiters();
}
fn wake_demux_waiters(&self) {
for demux in &self.ffmpeg_context.demuxs {
let crate::core::scheduler::input_controller::SchNode::Demux { waiter, .. } =
demux.node.as_ref()
else {
continue;
};
waiter.set(false);
}
}
pub fn is_ended(&self) -> bool {
is_stopping(self.status.load(Ordering::Acquire))
}
}
impl FfmpegScheduler<Initialization> {
pub fn new(ffmpeg_context: FfmpegContext) -> FfmpegScheduler<Initialization> {
let status = ffmpeg_context.scheduler_status.clone();
FfmpegScheduler {
ffmpeg_context,
state: Default::default(),
thread_sync: ThreadSynchronizer::new(),
status,
result: Arc::new(Mutex::new(None)),
_guard: None,
}
}
pub fn start(mut self) -> crate::error::Result<FfmpegScheduler<Running>> {
let packet_pool = match ObjPool::new(64, new_packet, unref_packet, packet_is_null) {
Ok(pool) => pool,
Err(e) => {
Self::cleanup(&self.status, &self.ffmpeg_context);
return Err(e);
}
};
let frame_pool = match ObjPool::new(64, new_frame, unref_frame, frame_is_null) {
Ok(pool) => pool,
Err(e) => {
Self::cleanup(&self.status, &self.ffmpeg_context);
return Err(e);
}
};
let scheduler_status = self.status.clone();
scheduler_status.store(STATUS_RUN, Ordering::Release);
let thread_sync = self.thread_sync.clone();
let scheduler_result = self.result.clone();
let demux_nodes = self
.ffmpeg_context
.demuxs
.iter()
.map(|demux| demux.node.clone())
.collect::<Vec<_>>();
let mux_stream_nodes = self
.ffmpeg_context
.muxs
.iter()
.flat_map(|mux| mux.mux_stream_nodes.clone())
.collect::<Vec<_>>();
let input_controller = InputController::new(demux_nodes, mux_stream_nodes);
let input_controller = Arc::new(input_controller);
let mux_done_remaining = Arc::new(AtomicUsize::new(self.ffmpeg_context.muxs.len()));
for _ in 0..self.ffmpeg_context.muxs.len() {
thread_sync.thread_start();
}
for (mux_idx, mux) in self.ffmpeg_context.muxs.iter_mut().enumerate() {
if mux.is_ready() {
if let Err(e) = mux_init(
mux_idx,
mux,
packet_pool.clone(),
input_controller.clone(),
mux.mux_stream_nodes.clone(),
scheduler_status.clone(),
thread_sync.clone(),
scheduler_result.clone(),
mux_done_remaining.clone(),
) {
Self::cleanup(&scheduler_status, &self.ffmpeg_context);
return Err(e);
}
}
}
let ffmpeg_context = &mut self.ffmpeg_context;
for (mux_idx, mux) in ffmpeg_context.muxs.iter_mut().enumerate() {
if let Some(frame_pipelines) = mux.frame_pipelines.take() {
for frame_pipeline in frame_pipelines {
if let Err(e) = output_pipeline_init(
mux_idx,
frame_pipeline,
mux.get_streams_mut(),
frame_pool.clone(),
scheduler_status.clone(),
thread_sync.clone(),
scheduler_result.clone(),
) {
Self::cleanup(&scheduler_status, ffmpeg_context);
return Err(e);
}
}
}
}
let ffmpeg_context = &mut self.ffmpeg_context;
for (mux_idx, mux) in &mut ffmpeg_context.muxs.iter_mut().enumerate() {
let ready_sender = match ready_to_init_mux(
mux_idx,
mux,
packet_pool.clone(),
input_controller.clone(),
scheduler_status.clone(),
thread_sync.clone(),
scheduler_result.clone(),
mux_done_remaining.clone(),
) {
Ok(sender) => sender,
Err(e) => {
Self::cleanup(&scheduler_status, ffmpeg_context);
return Err(e);
}
};
if mux.shortest {
let buf_us = mux.shortest_buf_duration_us;
let av: Vec<_> = mux
.get_streams()
.iter()
.enumerate()
.filter(|(_, s)| {
s.codec_type == AVMEDIA_TYPE_VIDEO || s.codec_type == AVMEDIA_TYPE_AUDIO
})
.map(|(i, s)| (i, s.stream_index, s.codec_type))
.collect();
if av.len() >= 2 {
let mut sq = SyncQueue::new(buf_us);
let members: Vec<(usize, usize, Option<Arc<AtomicBool>>)> = av
.iter()
.map(|&(pos, osi, ct)| {
let sq_idx = sq.add_stream(true);
let max_frames = if ct == AVMEDIA_TYPE_VIDEO {
mux.max_video_frames
} else {
mux.max_audio_frames
};
if let Some(max_frames) = max_frames {
if max_frames >= 0 {
sq.sq_limit_frames(sq_idx, max_frames as u64);
}
}
(pos, sq_idx, mux.stream_source_finished(osi))
})
.collect();
let sq_finished: Arc<[AtomicBool]> =
(0..av.len()).map(|_| AtomicBool::new(false)).collect();
let queue = Arc::new((Mutex::new(sq), Condvar::new()));
let streams = mux.get_streams_mut();
for (pos, sq_idx, source_finished) in members {
streams[pos].set_sync_queue(EncSyncHandle {
queue: queue.clone(),
sq_idx,
sq_finished: sq_finished.clone(),
source_finished,
});
}
}
}
for enc_stream in mux.take_streams_mut() {
if let Err(e) = enc_init(
mux_idx,
enc_stream,
ready_sender.clone(),
mux.start_time_us,
mux.recording_time_us,
mux.bits_per_raw_sample,
mux.max_video_frames,
mux.max_audio_frames,
mux.max_subtitle_frames,
&mux.video_codec_opts,
&mux.audio_codec_opts,
&mux.subtitle_codec_opts,
mux.oformat_flags,
frame_pool.clone(),
packet_pool.clone(),
scheduler_status.clone(),
thread_sync.clone(),
mux.enc_handle_sender(),
scheduler_result.clone(),
input_controller.clone(),
) {
Self::cleanup(&scheduler_status, ffmpeg_context);
return Err(e);
}
}
}
let ffmpeg_context = &mut self.ffmpeg_context;
for (i, filter_graph) in ffmpeg_context.filter_graphs.iter_mut().enumerate() {
if let Err(e) = filter_graph_init(
i,
filter_graph,
frame_pool.clone(),
input_controller.clone(),
filter_graph.node.clone(),
scheduler_status.clone(),
thread_sync.clone(),
scheduler_result.clone(),
) {
Self::cleanup(&scheduler_status, ffmpeg_context);
return Err(e);
}
}
let ffmpeg_context = &mut self.ffmpeg_context;
for (demux_idx, demux) in ffmpeg_context.demuxs.iter_mut().enumerate() {
if let Some(frame_pipelines) = demux.frame_pipelines.take() {
for frame_pipeline in frame_pipelines {
if let Err(e) = input_pipeline_init(
demux_idx,
frame_pipeline,
demux.get_streams_mut(),
frame_pool.clone(),
scheduler_status.clone(),
thread_sync.clone(),
scheduler_result.clone(),
) {
Self::cleanup(&scheduler_status, ffmpeg_context);
return Err(e);
}
}
}
}
for (demux_idx, demux) in &mut ffmpeg_context.demuxs.iter_mut().enumerate() {
let exit_on_error = demux.exit_on_error;
for dec_stream in demux.get_streams_mut() {
if let Err(e) = dec_init(
demux_idx,
dec_stream,
exit_on_error,
frame_pool.clone(),
packet_pool.clone(),
scheduler_status.clone(),
thread_sync.clone(),
scheduler_result.clone(),
) {
Self::cleanup(&scheduler_status, ffmpeg_context);
return Err(e);
}
}
}
for (demux_idx, demux) in ffmpeg_context.demuxs.iter_mut().enumerate() {
if let Err(e) = demux_init(
demux_idx,
demux,
ffmpeg_context.independent_readrate,
packet_pool.clone(),
demux.node.clone(),
scheduler_status.clone(),
thread_sync.clone(),
scheduler_result.clone(),
) {
Self::cleanup(&scheduler_status, ffmpeg_context);
return Err(e);
}
}
input_controller.as_ref().update_locked(&scheduler_status);
let mut running_scheduler = self.into_state::<Running>();
let demux_waiters = running_scheduler
.ffmpeg_context
.demuxs
.iter()
.filter_map(|demux| {
let crate::core::scheduler::input_controller::SchNode::Demux { waiter, .. } =
demux.node.as_ref()
else {
return None;
};
Some(waiter.clone())
})
.collect();
running_scheduler._guard = Some(RunningGuard {
status: running_scheduler.status.clone(),
thread_sync: running_scheduler.thread_sync.clone(),
demux_waiters,
_interrupt_state: running_scheduler.ffmpeg_context.interrupt_state.clone(),
});
Ok(running_scheduler)
}
fn cleanup(scheduler_status: &Arc<AtomicUsize>, _ffmpeg_context: &FfmpegContext) {
scheduler_status.store(STATUS_END, Ordering::Release);
notify_pause_waiters();
}
}
impl FfmpegScheduler<Running> {
pub fn pause(self) -> FfmpegScheduler<Paused> {
let _ = self.status.compare_exchange(
STATUS_RUN,
STATUS_PAUSE,
Ordering::AcqRel,
Ordering::Acquire,
);
self.into_state()
}
pub fn wait(self) -> crate::error::Result<()> {
if !is_stopping(self.status.load(Ordering::Acquire)) {
self.thread_sync.wait_for_all_threads();
self.status.store(STATUS_END, Ordering::Release);
}
let option = self
.result
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
match option {
None => {
log::info!("FFmpeg task succeeded.");
Ok(())
}
Some(result) => {
log::error!("FFmpeg task failed.");
result
}
}
}
pub fn abort(self) {
self.status.store(STATUS_ABORT, Ordering::Release);
notify_pause_waiters();
self.wake_demux_waiters();
}
pub fn stop(self) {
self.signal_stop();
self.thread_sync.wait_for_all_threads();
log::debug!("stop() completed, all threads finished");
}
}
#[cfg(feature = "async")]
impl std::future::Future for FfmpegScheduler<Running> {
type Output = crate::error::Result<()>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
fn ready(
result: &Arc<Mutex<Option<crate::error::Result<()>>>>,
) -> std::task::Poll<crate::error::Result<()>> {
let option = result
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
std::task::Poll::Ready(match option {
None => {
log::info!("FFmpeg task succeeded.");
Ok(())
}
Some(result) => {
log::error!("FFmpeg task failed.");
result
}
})
}
let this = self.get_mut();
if is_stopping(this.status.load(Ordering::Acquire)) {
return ready(&this.result);
}
let thread_sync = this.thread_sync.clone();
let waker = cx.waker().clone();
thread_sync.set_waker(waker);
if is_stopping(this.status.load(Ordering::Acquire)) {
return ready(&this.result);
}
std::task::Poll::Pending
}
}
impl FfmpegScheduler<Paused> {
pub fn resume(self) -> FfmpegScheduler<Running> {
if self
.status
.compare_exchange(
STATUS_PAUSE,
STATUS_RUN,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
notify_pause_waiters();
}
self.into_state()
}
pub fn abort(self) {
self.status.store(STATUS_ABORT, Ordering::Release);
notify_pause_waiters();
self.wake_demux_waiters();
}
}
fn new_frame() -> crate::error::Result<Frame> {
let frame = unsafe { av_frame_alloc() };
if frame.is_null() {
return Err(AllocFrameError::OutOfMemory.into());
}
Ok(unsafe { Frame::wrap(frame) })
}
fn new_packet() -> crate::error::Result<Packet> {
let packet = Packet::empty();
if packet.as_ptr().is_null() {
return Err(AllocPacketError::OutOfMemory.into());
}
Ok(packet)
}
#[inline]
pub(crate) fn unref_frame(frame: &mut Frame) {
unsafe { av_frame_unref(frame.as_mut_ptr()) };
}
#[inline]
pub(crate) fn unref_packet(packet: &mut Packet) {
unsafe { av_packet_unref(packet.as_mut_ptr()) };
}
#[inline]
pub(crate) fn frame_is_null(frame: &Frame) -> bool {
unsafe { frame.as_ptr().is_null() }
}
#[inline]
pub(crate) fn packet_is_null(packet: &Packet) -> bool {
packet.as_ptr().is_null()
}
fn pause_wait() -> &'static (Mutex<()>, Condvar) {
static PAUSE_WAIT: OnceLock<(Mutex<()>, Condvar)> = OnceLock::new();
PAUSE_WAIT.get_or_init(|| (Mutex::new(()), Condvar::new()))
}
pub(crate) fn wait_until_not_paused(scheduler_status: &Arc<AtomicUsize>) -> usize {
let status = scheduler_status.load(Ordering::Acquire);
if status != STATUS_PAUSE {
return status;
}
let (lock, cond) = pause_wait();
let mut guard = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
loop {
let status = scheduler_status.load(Ordering::Acquire);
if status != STATUS_PAUSE {
return status;
}
let (g, _timeout) = cond
.wait_timeout(guard, Duration::from_millis(200))
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard = g;
}
}
pub(crate) fn notify_pause_waiters() {
let (lock, cond) = pause_wait();
let _guard = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
cond.notify_all();
}
pub(crate) fn set_scheduler_error(
scheduler_status: &Arc<AtomicUsize>,
scheduler_result: &Arc<Mutex<Option<crate::error::Result<()>>>>,
error: impl Into<crate::error::Error>,
) {
let mut scheduler_result = scheduler_result
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if scheduler_result.is_none() {
scheduler_result.replace(Err(error.into()));
scheduler_status.store(STATUS_END, Ordering::Release);
notify_pause_waiters();
}
}
#[cfg(test)]
mod tests {
use crate::core::context::ffmpeg_context::FfmpegContext;
use crate::core::context::input::Input;
use crate::core::context::output::Output;
use crate::core::filter::frame_filter::NoopFilter;
use crate::core::scheduler::ffmpeg_scheduler::{
FfmpegScheduler, Initialization, Paused, Running, STATUS_INIT, STATUS_PAUSE, STATUS_RUN,
};
use crate::filter::frame_pipeline_builder::FramePipelineBuilder;
use ffmpeg_sys_next::AVMediaType;
use log::{info, warn};
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex};
use std::thread::sleep;
use std::time::Duration;
#[test]
fn pause_gate_wakes_all_waiters_on_transition() {
use crate::core::scheduler::ffmpeg_scheduler::{
notify_pause_waiters, wait_until_not_paused,
};
use std::sync::atomic::AtomicUsize;
use std::sync::mpsc;
let status = Arc::new(AtomicUsize::new(STATUS_PAUSE));
let (tx, rx) = mpsc::channel();
let mut handles = Vec::new();
for _ in 0..4 {
let status = Arc::clone(&status);
let tx = tx.clone();
handles.push(std::thread::spawn(move || {
tx.send(wait_until_not_paused(&status)).unwrap();
}));
}
drop(tx);
sleep(Duration::from_millis(50));
assert!(
rx.try_recv().is_err(),
"a paused worker returned before resume"
);
status.store(STATUS_RUN, Ordering::Release);
notify_pause_waiters();
for _ in 0..4 {
let observed = rx
.recv_timeout(Duration::from_secs(5))
.expect("a paused worker never woke after the transition");
assert_eq!(observed, STATUS_RUN);
}
for h in handles {
h.join().unwrap();
}
}
#[test]
fn set_scheduler_error_survives_a_poisoned_result_lock() {
use crate::core::scheduler::ffmpeg_scheduler::{set_scheduler_error, STATUS_END};
use std::sync::atomic::AtomicUsize;
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let result: Arc<Mutex<Option<crate::error::Result<()>>>> = Arc::new(Mutex::new(None));
let poisoned = Arc::clone(&result);
let _ = std::thread::spawn(move || {
let _guard = poisoned.lock().unwrap();
panic!("worker panicked while holding the result lock");
})
.join();
assert!(result.is_poisoned(), "test setup must poison the lock");
set_scheduler_error(&status, &result, crate::error::Error::NotStarted);
assert_eq!(status.load(Ordering::Acquire), STATUS_END);
assert!(result
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_some());
}
#[test]
fn test_img_to_video() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let result = FfmpegContext::builder()
.input(
Input::from("logo.jpg")
.set_input_opt("loop", "1")
.set_recording_time_us(10 * 1000_000),
)
.filter_desc("scale=1280:720")
.output(Output::from("output_img_to_video.mp4"))
.build()
.unwrap()
.start()
.unwrap()
.wait();
assert!(result.is_ok());
}
#[test]
fn test_copy() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let result = FfmpegContext::builder()
.input("test.mp4")
.output(
Output::from("output_copy.mp4")
.add_stream_map_with_copy("0:v")
.add_stream_map_with_copy("0:a"),
)
.build()
.unwrap()
.start()
.unwrap()
.wait();
assert!(result.is_ok());
}
#[test]
fn test_concat() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let result = FfmpegContext::builder()
.input("test.mp4")
.input("test.mp4")
.input("test.mp4")
.filter_desc("concat=n=3:v=1:a=1")
.output("output_concat.mp4")
.build()
.unwrap()
.start()
.unwrap()
.wait();
assert!(result.is_ok());
std::thread::sleep(Duration::from_secs(1));
}
#[cfg(target_os = "macos")]
#[test]
fn test_to_stdout() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let input: Input = "test.mp4".into();
let output: Output = "-".into();
let result = FfmpegContext::builder()
.input(input.set_hwaccel("videotoolbox"))
.output(output.set_format("null").set_video_codec("h264"))
.build()
.unwrap()
.start()
.unwrap()
.wait();
assert!(result.is_ok());
}
#[cfg(target_os = "macos")]
#[test]
fn test_thumbnail() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let mut input: Input = "test.mp4".into();
let output: Output = "output.jpg".into();
input.hwaccel = Some("videotoolbox".to_string());
let result = FfmpegContext::builder()
.input(input)
.filter_desc("scale='min(160,iw)':-1")
.output(output.set_max_video_frames(1))
.build()
.unwrap()
.start()
.unwrap()
.wait();
assert!(result.is_ok());
}
#[test]
fn test_read_write_callback_mp4() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
let input_file = "test.mp4";
let output_file = "output_rw_callback.mp4";
let input_file = Arc::new(Mutex::new(
File::open(input_file).expect("Failed to open input file"),
));
let read_callback: Box<dyn FnMut(&mut [u8]) -> i32 + Send> = {
let input = Arc::clone(&input_file);
Box::new(move |buf: &mut [u8]| -> i32 {
let mut input = input.lock().unwrap();
match input.read(buf) {
Ok(0) => ffmpeg_sys_next::AVERROR_EOF,
Ok(bytes_read) => bytes_read as i32,
Err(_) => ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO),
}
})
};
let seek_callback: Box<dyn FnMut(i64, i32) -> i64 + Send> = {
let input = Arc::clone(&input_file);
Box::new(move |offset: i64, whence: i32| -> i64 {
let mut input = input.lock().unwrap();
if whence == ffmpeg_sys_next::AVSEEK_SIZE {
if let Ok(size) = input.metadata().map(|m| m.len() as i64) {
info!("FFmpeg requested stream size: {}", size);
return size;
}
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
}
let seek_result = match whence {
ffmpeg_sys_next::SEEK_SET => input.seek(SeekFrom::Start(offset as u64)),
ffmpeg_sys_next::SEEK_CUR => input.seek(SeekFrom::Current(offset)),
ffmpeg_sys_next::SEEK_END => input.seek(SeekFrom::End(offset)),
_ => {
warn!("Unsupported seek mode: {whence}");
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64;
}
};
match seek_result {
Ok(new_pos) => new_pos as i64,
Err(_) => ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64,
}
})
};
let mut input: Input = read_callback.into();
input.seek_callback = Some(seek_callback);
let output_file = Arc::new(Mutex::new(
File::create(output_file).expect("Failed to create output file"),
));
let write_callback: Box<dyn FnMut(&[u8]) -> i32 + Send> = {
let output_file = Arc::clone(&output_file);
Box::new(move |buf: &[u8]| -> i32 {
let mut output_file = output_file.lock().unwrap();
match output_file.write_all(buf) {
Ok(_) => buf.len() as i32,
Err(_) => ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO),
}
})
};
let seek_callback: Box<dyn FnMut(i64, i32) -> i64 + Send> = {
let output_file = Arc::clone(&output_file);
Box::new(move |offset: i64, whence: i32| -> i64 {
let mut file = output_file.lock().unwrap();
if whence == ffmpeg_sys_next::AVSEEK_SIZE {
if let Ok(size) = file.metadata().map(|m| m.len() as i64) {
println!("FFmpeg requested stream size: {}", size);
return size;
}
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
}
if (whence & ffmpeg_sys_next::AVSEEK_FLAG_BYTE) != 0 {
println!(
"FFmpeg requested byte-based seeking. Seeking to byte offset: {}",
offset
);
if let Ok(new_pos) = file.seek(SeekFrom::Start(offset as u64)) {
return new_pos as i64;
}
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
}
let normalized_whence = whence & !ffmpeg_sys_next::AVSEEK_FORCE;
match normalized_whence {
ffmpeg_sys_next::SEEK_SET => file.seek(SeekFrom::Start(offset as u64)),
ffmpeg_sys_next::SEEK_CUR => file.seek(SeekFrom::Current(offset)),
ffmpeg_sys_next::SEEK_END => file.seek(SeekFrom::End(offset)),
_ => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Unsupported seek mode",
)),
}
.map_or(
ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64,
|pos| pos as i64,
)
})
};
let mut output: Output = write_callback.into();
output.seek_callback = Some(seek_callback);
output.format = Some("mp4".to_string());
let context = FfmpegContext::builder()
.input(input)
.filter_desc("hue=s=0")
.output(output)
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
let scheduler = scheduler.start().unwrap();
let result = scheduler.wait();
assert!(result.is_ok());
}
#[test]
fn test_pipeline() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let out_path = std::env::temp_dir().join("ez_test_pipeline_out.mp4");
let output: Output = out_path.to_str().unwrap().into();
let frame_pipeline_builder: FramePipelineBuilder = AVMediaType::AVMEDIA_TYPE_VIDEO.into();
let frame_pipeline_builder = frame_pipeline_builder.filter(
"test",
Box::new(NoopFilter::new(AVMediaType::AVMEDIA_TYPE_VIDEO)),
);
let output = output.add_frame_pipeline(frame_pipeline_builder);
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output(output)
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
let scheduler = scheduler.start().unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !scheduler.is_ended() {
assert!(
std::time::Instant::now() < deadline,
"output frame pipeline did not terminate within 60s"
);
std::thread::sleep(std::time::Duration::from_millis(20));
}
scheduler.wait().unwrap();
let _ = std::fs::remove_file(&out_path);
}
#[test]
fn test_is_ended() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output("output_is_ended.mp4")
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
let scheduler = scheduler.start().unwrap();
sleep(Duration::from_secs(2));
assert!(scheduler.is_ended())
}
#[cfg(target_os = "macos")]
#[test]
fn test_hwaccel() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let input: Input = "test.mp4".into();
let output: Output = "output_hwaccel.mp4".into();
let result = FfmpegContext::builder()
.input(input.set_hwaccel("videotoolbox"))
.filter_desc("hue=s=0")
.output(output.set_video_codec("h264_videotoolbox"))
.build()
.unwrap()
.start()
.unwrap()
.wait();
assert!(result.is_ok());
}
#[cfg(feature = "async")]
#[tokio::test]
async fn test_async() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output("output_async.mp4")
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
let scheduler = scheduler.start().unwrap();
let result = scheduler.await;
assert!(result.is_ok());
}
#[test]
fn test_pause() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output("output_pause.mp4")
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
let scheduler = scheduler.start().unwrap();
let scheduler = scheduler.pause();
assert!(scheduler.is_state::<Paused>());
sleep(Duration::from_secs(1));
let scheduler = scheduler.resume();
let result = scheduler.wait();
assert!(result.is_ok());
}
#[test]
fn test_pause_abort() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output("output_pause_abort.mp4")
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
let scheduler = scheduler.start().unwrap();
let scheduler = scheduler.pause();
assert!(scheduler.is_state::<Paused>());
sleep(Duration::from_secs(1));
scheduler.abort();
}
#[test]
fn test_wait() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output("output_wait.mp4")
.build()
.unwrap();
let result = FfmpegScheduler::new(context).start().unwrap().wait();
assert!(result.is_ok());
}
#[test]
fn test_status() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output("output_status.mp4")
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
assert!(!scheduler.is_state::<Paused>());
assert!(scheduler.is_state::<Initialization>());
assert_eq!(scheduler.status.load(Ordering::Acquire), STATUS_INIT);
let scheduler = scheduler.start().unwrap();
assert_eq!(scheduler.status.load(Ordering::Acquire), STATUS_RUN);
assert!(scheduler.is_state::<Running>());
let scheduler = scheduler.pause();
assert_eq!(scheduler.status.load(Ordering::Acquire), STATUS_PAUSE);
assert!(scheduler.is_state::<Paused>());
let scheduler = scheduler.resume();
assert_eq!(scheduler.status.load(Ordering::Acquire), STATUS_RUN);
assert!(scheduler.is_state::<Running>());
scheduler.abort();
sleep(Duration::from_secs(1));
}
#[test]
fn test_stop() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Trace)
.is_test(true)
.try_init();
let _ = std::fs::remove_file("output_stop.mp4");
let context = FfmpegContext::builder()
.input("test.mp4")
.filter_desc("hue=s=0")
.output("output_stop.mp4")
.build()
.unwrap();
let scheduler = FfmpegScheduler::new(context);
let scheduler = scheduler.start().unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(30);
while !std::fs::metadata("output_stop.mp4")
.map(|m| m.len() > 0)
.unwrap_or(false)
{
assert!(
std::time::Instant::now() < deadline,
"pipeline wrote no output within 30s"
);
sleep(Duration::from_millis(50));
}
scheduler.stop();
let metadata = std::fs::metadata("output_stop.mp4").unwrap();
assert!(
metadata.len() > 0,
"Output file should have content after stop()"
);
}
}