#[cfg(feature = "ffmpeg")]
pub mod decode;
#[cfg(feature = "ffmpeg")]
pub mod encode;
#[cfg(feature = "ffmpeg-cli")]
pub mod ffmpeg_cli;
use std::path::PathBuf;
use std::str::FromStr;
use crate::pattern::FrameStamp;
use crate::{Error, FrameRate, Result};
#[derive(Clone, Copy, Debug, Default)]
pub struct VideoSample {
pub time: f64,
pub stamp: Option<FrameStamp>,
pub qr_located: bool,
pub old_format: bool,
pub exposure: Option<f64>,
pub readout: Option<f64>,
}
#[derive(Clone, Debug)]
pub struct AudioTrack {
pub sample_rate: u32,
pub start_time: f64,
pub samples: Vec<f32>,
}
#[derive(Clone, Debug)]
pub struct MediaScan {
pub video: Vec<VideoSample>,
pub audio: Option<AudioTrack>,
pub width: u32,
pub height: u32,
pub stream_fps: Option<FrameRate>,
}
#[derive(Clone, Copy, Debug)]
pub struct ScanWindow {
pub start: f64,
pub end: Option<f64>,
pub audio_margin: f64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum VideoCodec {
#[default]
H264,
Ffv1,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AudioCodec {
#[default]
Aac,
Pcm,
}
impl FromStr for VideoCodec {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_ascii_lowercase().as_str() {
"h264" => Ok(VideoCodec::H264),
"ffv1" => Ok(VideoCodec::Ffv1),
_ => Err(Error::InvalidOption(format!(
"video codec must be h264 or ffv1, not `{s}`"
))),
}
}
}
impl FromStr for AudioCodec {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_ascii_lowercase().as_str() {
"aac" => Ok(AudioCodec::Aac),
"pcm" => Ok(AudioCodec::Pcm),
_ => Err(Error::InvalidOption(format!(
"audio codec must be aac or pcm, not `{s}`"
))),
}
}
}
#[derive(Clone, Debug)]
pub struct GenerateOptions {
pub output: PathBuf,
pub fps: FrameRate,
pub duration: f64,
pub width: u32,
pub height: u32,
pub sample_rate: u32,
pub video_codec: VideoCodec,
pub audio_codec: AudioCodec,
pub audio_delay: f64,
pub music: bool,
}
impl GenerateOptions {
pub fn new(output: impl Into<PathBuf>, fps: FrameRate, duration: f64) -> Self {
GenerateOptions {
output: output.into(),
fps,
duration,
width: 1280,
height: 720,
sample_rate: 48000,
video_codec: VideoCodec::default(),
audio_codec: AudioCodec::default(),
audio_delay: 0.0,
music: true,
}
}
pub fn total_frames(&self) -> u64 {
self.fps.frames_in(self.duration)
}
#[cfg(any(feature = "ffmpeg", feature = "ffmpeg-cli"))]
pub(crate) fn validate(&self) -> Result<()> {
use crate::pattern::barcode::MAX_FRAME_ID;
let opts = self;
let invalid = |msg: String| Err(Error::InvalidOption(msg));
if !(opts.duration.is_finite() && opts.duration > 0.0) {
return invalid(format!("duration must be positive, got {}", opts.duration));
}
if opts.total_frames() > u64::from(MAX_FRAME_ID) + 1 {
return invalid("duration exceeds 2^24 frames".into());
}
if !(8000..=192_000).contains(&opts.sample_rate) {
return invalid(format!("sample rate {} is out of range", opts.sample_rate));
}
if !opts.audio_delay.is_finite() || opts.audio_delay.abs() > 60.0 {
return invalid("audio delay must be within ยฑ60 s".into());
}
let ext = opts
.output
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.unwrap_or_default();
let flexible = matches!(ext.as_str(), "mkv" | "mov");
if !flexible && !matches!(ext.as_str(), "mp4" | "m4v") {
return invalid(format!(
"unsupported output container `.{ext}` (use .mp4, .mov or .mkv)"
));
}
if !flexible && opts.audio_codec == AudioCodec::Pcm {
return invalid("PCM audio needs a .mov or .mkv output".into());
}
if !flexible && opts.video_codec == VideoCodec::Ffv1 {
return invalid("FFV1 video needs a .mov or .mkv output".into());
}
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
pub struct Progress {
pub frames_done: u64,
pub frames_total: u64,
}
#[derive(Clone, Debug)]
pub struct AudioTrackBuilder {
from: f64,
to: Option<f64>,
tolerance: f64,
track: Option<AudioTrack>,
}
impl AudioTrackBuilder {
pub fn new(from: f64, to: Option<f64>, tolerance: f64) -> Self {
AudioTrackBuilder {
from,
to,
tolerance,
track: None,
}
}
pub fn push(&mut self, time: f64, rate: u32, data: &[f32]) -> Result<()> {
let r = f64::from(rate);
let track = match &mut self.track {
Some(track) => {
if track.sample_rate != rate {
return Err(Error::Unsupported(
"audio sample rate changes mid-stream".into(),
));
}
track
}
None => {
let skip = ((self.from - time) * r).ceil().max(0.0) as usize;
if skip >= data.len() {
return Ok(());
}
self.track = Some(AudioTrack {
sample_rate: rate,
start_time: time + skip as f64 / r,
samples: data[skip..].to_vec(),
});
self.trim();
return Ok(());
}
};
let mismatch = time - track.start_time - track.samples.len() as f64 / r;
let gap = if mismatch.abs() <= self.tolerance {
0
} else {
(mismatch * r).round() as i64
};
let data = if gap > 0 {
track
.samples
.resize(track.samples.len() + gap as usize, 0.0);
data
} else {
&data[((-gap) as usize).min(data.len())..]
};
track.samples.extend_from_slice(data);
self.trim();
Ok(())
}
pub fn is_full(&self) -> bool {
match (self.to, &self.track) {
(Some(to), Some(track)) => {
track.samples.len() as f64 >= (to - track.start_time) * f64::from(track.sample_rate)
}
_ => false,
}
}
pub fn track(&self) -> Option<&AudioTrack> {
self.track.as_ref()
}
pub fn finish(self) -> Option<AudioTrack> {
self.track
}
fn trim(&mut self) {
let (Some(to), Some(track)) = (self.to, self.track.as_mut()) else {
return;
};
let keep = ((to - track.start_time) * f64::from(track.sample_rate))
.ceil()
.max(0.0) as usize;
track.samples.truncate(keep);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_a_continuous_track() {
let mut b = AudioTrackBuilder::new(0.5, Some(2.0), 0.002);
b.push(0.0, 1000, &[1.0; 1000]).unwrap();
assert_eq!(b.track().unwrap().start_time, 0.5);
assert_eq!(b.track().unwrap().samples.len(), 500);
b.push(1.001, 1000, &[2.0; 100]).unwrap();
assert_eq!(b.track().unwrap().samples.len(), 600);
b.push(1.15, 1000, &[3.0; 100]).unwrap();
let t = b.track().unwrap();
assert_eq!(t.samples.len(), 750);
assert_eq!((t.samples[620], t.samples[660]), (0.0, 3.0));
b.push(1.23, 1000, &[4.0; 100]).unwrap();
assert_eq!(b.track().unwrap().samples.len(), 830);
assert!(!b.is_full());
b.push(1.33, 1000, &[5.0; 1000]).unwrap();
assert!(b.is_full());
assert_eq!(b.finish().unwrap().samples.len(), 1500);
}
#[test]
fn rejects_a_rate_change() {
let mut b = AudioTrackBuilder::new(0.0, None, 0.002);
b.push(0.0, 48000, &[0.0; 10]).unwrap();
assert!(b.push(0.1, 44100, &[0.0; 10]).is_err());
}
}