refactor: move VideoRenderConfig from domain to adapter, inject at construction
Some checks failed
CI / Check / Test (push) Failing after 44s
Some checks failed
CI / Check / Test (push) Failing after 44s
This commit is contained in:
@@ -1,26 +1,24 @@
|
||||
use domain::errors::DomainError;
|
||||
use domain::ports::VideoRenderConfig;
|
||||
use tokio::process::Command;
|
||||
|
||||
pub async fn stitch_slides(
|
||||
slides: &[Vec<u8>],
|
||||
config: &VideoRenderConfig,
|
||||
ffmpeg_path: &str,
|
||||
slide_duration_secs: u32,
|
||||
resolution: (u32, u32),
|
||||
) -> Result<Vec<u8>, DomainError> {
|
||||
let dir = tempfile::tempdir().map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
|
||||
// Write slide PNGs
|
||||
for (i, png) in slides.iter().enumerate() {
|
||||
let path = dir.path().join(format!("slide_{:04}.png", i));
|
||||
std::fs::write(&path, png).map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
}
|
||||
|
||||
let output_path = dir.path().join("output.mp4");
|
||||
let framerate = format!("1/{}", slide_duration_secs);
|
||||
let (w, h) = resolution;
|
||||
|
||||
// -framerate 1/N makes each image last N seconds
|
||||
let framerate = format!("1/{}", config.slide_duration_secs);
|
||||
let (w, h) = config.resolution;
|
||||
|
||||
let status = Command::new(&config.ffmpeg_path)
|
||||
let status = Command::new(ffmpeg_path)
|
||||
.args([
|
||||
"-y",
|
||||
"-framerate",
|
||||
|
||||
@@ -4,14 +4,34 @@ mod slides;
|
||||
use async_trait::async_trait;
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::wrapup::WrapUpReport;
|
||||
use domain::ports::{VideoRenderAssets, VideoRenderConfig, WrapUpVideoRenderer};
|
||||
use domain::ports::{VideoRenderAssets, WrapUpVideoRenderer};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FfmpegWrapUpRenderer;
|
||||
pub struct RendererConfig {
|
||||
pub slide_duration_secs: u32,
|
||||
pub transition_duration_secs: f32,
|
||||
pub resolution: (u32, u32),
|
||||
pub ffmpeg_path: String,
|
||||
pub font_path: Option<String>,
|
||||
pub logo_path: Option<String>,
|
||||
pub bg_dir: Option<String>,
|
||||
}
|
||||
|
||||
pub struct FfmpegWrapUpRenderer {
|
||||
config: RendererConfig,
|
||||
slide_renderer: slides::SlideRenderer,
|
||||
}
|
||||
|
||||
impl FfmpegWrapUpRenderer {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
pub fn new(config: RendererConfig) -> Result<Self, DomainError> {
|
||||
let slide_renderer = slides::SlideRenderer::new(
|
||||
config.font_path.as_deref(),
|
||||
config.logo_path.as_deref(),
|
||||
config.bg_dir.as_deref(),
|
||||
)?;
|
||||
Ok(Self {
|
||||
config,
|
||||
slide_renderer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,21 +41,14 @@ impl WrapUpVideoRenderer for FfmpegWrapUpRenderer {
|
||||
&self,
|
||||
report: &WrapUpReport,
|
||||
assets: VideoRenderAssets,
|
||||
config: &VideoRenderConfig,
|
||||
) -> Result<Vec<u8>, DomainError> {
|
||||
let (width, height) = config.resolution;
|
||||
|
||||
let renderer = slides::SlideRenderer::new(
|
||||
config.font_path.as_deref(),
|
||||
config.logo_path.as_deref(),
|
||||
config.bg_dir.as_deref(),
|
||||
)?;
|
||||
let (width, height) = self.config.resolution;
|
||||
|
||||
let mut slide_pngs = Vec::new();
|
||||
slide_pngs.push(renderer.render_hero(report, width, height)?);
|
||||
slide_pngs.push(renderer.render_ratings(report, width, height)?);
|
||||
slide_pngs.push(self.slide_renderer.render_hero(report, width, height)?);
|
||||
slide_pngs.push(self.slide_renderer.render_ratings(report, width, height)?);
|
||||
if !report.top_directors.is_empty() {
|
||||
slide_pngs.push(renderer.render_directors(
|
||||
slide_pngs.push(self.slide_renderer.render_directors(
|
||||
report,
|
||||
&assets.cast_images,
|
||||
width,
|
||||
@@ -43,23 +56,35 @@ impl WrapUpVideoRenderer for FfmpegWrapUpRenderer {
|
||||
)?);
|
||||
}
|
||||
if !report.top_actors.is_empty() {
|
||||
slide_pngs.push(renderer.render_actors(report, &assets.cast_images, width, height)?);
|
||||
slide_pngs.push(
|
||||
self.slide_renderer
|
||||
.render_actors(report, &assets.cast_images, width, height)?,
|
||||
);
|
||||
}
|
||||
if !report.top_genres.is_empty() {
|
||||
slide_pngs.push(renderer.render_genres(report, width, height)?);
|
||||
slide_pngs.push(self.slide_renderer.render_genres(report, width, height)?);
|
||||
}
|
||||
slide_pngs.push(renderer.render_highlights(
|
||||
slide_pngs.push(self.slide_renderer.render_highlights(
|
||||
report,
|
||||
&assets.poster_images,
|
||||
width,
|
||||
height,
|
||||
)?);
|
||||
if !assets.poster_images.is_empty() {
|
||||
slide_pngs.push(renderer.render_mosaic(&assets.poster_images, width, height)?);
|
||||
slide_pngs.push(
|
||||
self.slide_renderer
|
||||
.render_mosaic(&assets.poster_images, width, height)?,
|
||||
);
|
||||
} else {
|
||||
tracing::warn!("no poster images resolved, skipping mosaic slide");
|
||||
}
|
||||
|
||||
ffmpeg::stitch_slides(&slide_pngs, config).await
|
||||
ffmpeg::stitch_slides(
|
||||
&slide_pngs,
|
||||
&self.config.ffmpeg_path,
|
||||
self.config.slide_duration_secs,
|
||||
self.config.resolution,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user