#![allow(clippy::missing_safety_doc)]
use crate::AudioTimelineSamples;
use crate::MediaProvider;
use crate::ResolvedRenderingTimeline;
use crate::Scenes;
use crate::Video;
use crate::VideoDecodersWorker;
use crate::VideoSize;
use crate::usvgr::fontdb;
use crate::{AudioData, FFramesContext, ScenesWithAudio, TimeBase};
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use super::EncoderOptions;
use super::FFramesLoggerVariant;
use super::FFramesRenderBackend;
use super::fframes_logger;
use super::renderer_error::FFramesRendererResult;
use super::renderer_font_source::RendererFontSource;
#[derive(Debug, Clone)]
pub struct RenderOptions<'a, 'media> {
pub media: Option<&'media dyn MediaProvider<'media>>,
pub logger: FFramesLoggerVariant,
pub audio_encoder_options: EncoderOptions<'a>,
pub video_encoder_options: EncoderOptions<'a>,
pub override_fps: Option<usize>,
pub tmp_files_directory: Option<&'a PathBuf>,
pub scale_resolution: f64,
pub load_system_fonts: bool,
pub default_font: &'a str,
pub abort_signal: Option<&'media crate::AbortSignal>,
pub frame_range: Option<std::ops::Range<usize>>,
pub audio_mix: crate::AudioMixOptions,
}
impl RenderOptions<'_, '_> {
pub fn output_frame_range(&self, duration_in_frames: usize) -> std::ops::Range<usize> {
match &self.frame_range {
Some(range) => range.start.min(duration_in_frames)..range.end.min(duration_in_frames),
None => 0..duration_in_frames,
}
}
}
impl Default for RenderOptions<'_, '_> {
fn default() -> Self {
Self {
media: None,
scale_resolution: 1.0,
logger: FFramesLoggerVariant::Compact,
audio_encoder_options: Default::default(),
video_encoder_options: Default::default(),
override_fps: None,
load_system_fonts: false,
default_font: "Arial",
abort_signal: None,
tmp_files_directory: None,
frame_range: None,
audio_mix: Default::default(),
}
}
}
#[doc(hidden)]
pub struct FFramesRendererRuntime<'a> {
pub time_base: TimeBase,
pub timeline: ResolvedRenderingTimeline<'a, AudioTimelineSamples>,
pub font_source: RendererFontSource,
}
impl<'a, 'media: 'a> FFramesRendererRuntime<'a> {
pub fn new<TVideo: Video>(
time_base: TimeBase,
video: &'a TVideo,
scenes: &Scenes<'a>,
media: Option<&'media dyn MediaProvider<'media>>,
) -> FFramesRendererResult<Self> {
let timeline = crate::resolve_timeline(
&video.duration(),
&ScenesWithAudio::new(scenes),
&time_base,
&video.audio(),
|name| {
let media = media.ok_or_else(|| {
crate::error::FFramesError::RequiredAudioNotFound(name.to_owned())
})?;
let video_duration = media
.resolve_video(name)
.and_then(|video| video.metadata)
.map(|metadata| metadata.duration as f64);
video_duration
.or_else(|| {
media
.resolve_audio(name)
.and_then(|main_audio| match main_audio {
AudioData::Preloaded(data) => {
Some(data.samples.len() as f64 / data.sample_rate as f64)
}
_ => None,
})
})
.ok_or_else(|| {
crate::error::FFramesError::CanNotProcessAudioDuration(name.to_owned())
})
},
)?;
let mut font_source = RendererFontSource {
fontdb: fontdb::Database::new(),
};
if let Some(media) = media {
media.populate_font_source(&mut font_source);
}
Ok(Self {
time_base,
timeline,
font_source,
})
}
}
pub fn render<
'a,
'media: 'a,
TBackend: FFramesRenderBackend,
TVideo: Video + Sync + Sized + Send,
>(
output: impl AsRef<Path>,
video: &'a TVideo,
render_backend: TBackend,
options: &'a RenderOptions<'a, 'media>,
) -> FFramesRendererResult<()> {
let logger = fframes_logger::make_logger(options.logger.clone());
let output = PathBuf::from(output.as_ref());
let scenes = video.define_scenes();
let FFramesRendererRuntime {
timeline,
time_base,
mut font_source,
} = FFramesRendererRuntime::new(
TimeBase {
fps: TVideo::FPS,
sample_rate: options.audio_encoder_options.sample_rate,
},
video,
&scenes,
options.media,
)?;
let mut image_source = HashMap::new();
if let Some(media) = options.media {
media.populate_image_source(&mut image_source);
}
let usvg_options = usvgr::Options {
image_data: Some(&image_source),
font_family: options.default_font.to_string(),
..Default::default()
};
if options.load_system_fonts {
font_source.fontdb.load_system_fonts();
}
let frame_range = options.output_frame_range(timeline.duration_in_frames);
if frame_range.is_empty() {
return Err(super::FFramesRendererError::Custom(format!(
"frame range {:?} is empty or outside the video (0..{})",
options.frame_range, timeline.duration_in_frames
)));
}
logger.init_frames_rendering(frame_range.len())?;
let ctx = FFramesContext {
time_base,
mode: crate::FFramesMode::Renderer,
media_source: options.media,
duration_in_frames: timeline.duration_in_frames,
scenes: timeline.scenes.as_ref(),
font_source: Some(&font_source),
abort_signal: options.abort_signal,
current_video_size: VideoSize::new_scaled(
TVideo::WIDTH,
TVideo::HEIGHT,
options.scale_resolution,
),
};
render_backend.render(
&output,
video,
logger.clone(),
&usvg_options,
options,
font_source.as_db_ref(),
&timeline,
&ctx,
)?;
for (kind, name) in crate::diagnostics::take_missing_media() {
logger.warn(&format!(
"{kind:?} \"{name}\" was requested by render_frame but is not in the media provider"
));
}
Ok(())
}
pub fn render_frame<
'a,
'media: 'a,
TBackend: FFramesRenderBackend,
TVideo: Video + Sync + Sized + Send,
>(
frame_index: usize,
video: &'a TVideo,
render_backend: TBackend,
options: &RenderOptions<'a, 'media>,
) -> FFramesRendererResult<Vec<u8>> {
let scenes = video.define_scenes();
let FFramesRendererRuntime {
timeline,
time_base,
mut font_source,
} = FFramesRendererRuntime::new(
TimeBase {
fps: TVideo::FPS,
sample_rate: options.audio_encoder_options.sample_rate,
},
video,
&scenes,
options.media,
)?;
let mut image_source = HashMap::new();
if let Some(media) = options.media {
media.populate_image_source(&mut image_source);
}
if options.load_system_fonts {
font_source.fontdb.load_system_fonts();
}
let ctx = FFramesContext {
time_base,
mode: crate::FFramesMode::Renderer,
media_source: options.media,
duration_in_frames: timeline.duration_in_frames,
scenes: timeline.scenes.as_ref(),
font_source: Some(&font_source),
abort_signal: None,
current_video_size: VideoSize::new_scaled(
TVideo::WIDTH,
TVideo::HEIGHT,
options.scale_resolution,
),
};
let decoders = VideoDecodersWorker::new(1);
render_backend.render_frame(
crate::Frame::__internal_make_for_renderer(
frame_index,
frame_index,
TVideo::FPS,
None,
decoders,
),
video,
&crate::usvgr::Options {
image_data: Some(&image_source),
font_family: options.default_font.to_string(),
..Default::default()
},
font_source.as_db_ref(),
ctx,
)
}
pub fn get_thread_count() -> usize {
if let Some(threads) = std::env::var("FFRAMES_NUM_THREADS")
.ok()
.and_then(|threads| threads.parse().ok())
{
return threads;
}
rayon::current_num_threads()
}