use std::path::{Path, PathBuf};
use streamsafe::{Result as SsResult, Sink};
use super::{gif_sink, other_err, png_sink};
use crate::types::{CapturedFrame, GifConfig, OutputFormat};
pub enum FormatSink {
Gif {
path: PathBuf,
config: GifConfig,
frames: Vec<CapturedFrame>,
},
Png {
path: PathBuf,
last: Option<CapturedFrame>,
},
}
impl FormatSink {
pub fn for_format(format: &OutputFormat, scene_name: &str, output_dir: &Path) -> Option<Self> {
match format {
OutputFormat::Gif(cfg) => Some(gif_sink(
output_dir.join(format!("{scene_name}.gif")),
cfg.clone(),
)),
OutputFormat::Png(_) => Some(png_sink(output_dir.join(format!("{scene_name}.png")))),
OutputFormat::Mp4(_) => {
tracing::warn!("MP4 output requires ffmpeg in PATH — skipping");
None
}
}
}
pub fn output_path(&self) -> &Path {
match self {
FormatSink::Gif { path, .. } => path,
FormatSink::Png { path, .. } => path,
}
}
}
impl Sink for FormatSink {
type Input = CapturedFrame;
async fn consume(&mut self, frame: CapturedFrame) -> SsResult<()> {
match self {
FormatSink::Gif { frames, .. } => frames.push(frame),
FormatSink::Png { last, .. } => *last = Some(frame),
}
Ok(())
}
async fn finish(&mut self) -> SsResult<()> {
match self {
FormatSink::Gif {
path,
config,
frames,
} if !frames.is_empty() => {
let frames_taken = std::mem::take(frames);
let path = path.clone();
let config = config.clone();
tokio::task::spawn_blocking(move || {
crate::convert::gif::frames_to_gif(&frames_taken, &path, &config)
})
.await
.map_err(other_err)?
.map_err(|e| other_err(anyhow_to_io(e)))
}
FormatSink::Gif { .. } => Ok(()),
FormatSink::Png { path, last } => {
if let Some(frame) = last.take() {
let path = path.clone();
tokio::task::spawn_blocking(move || std::fs::write(&path, &frame.png_data))
.await
.map_err(other_err)?
.map_err(other_err)?;
}
Ok(())
}
}
}
}
fn anyhow_to_io(err: anyhow::Error) -> std::io::Error {
std::io::Error::other(format!("{err:#}"))
}