use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use crate::decode::{DecoderConfig, Hardware};
use crate::encode::{self, Backend, Encoder, EncoderConfig, Level};
use crate::error::{Error, Result};
use crate::frame::Frame;
use crate::identify::{Container, Encoding};
use crate::media::{Packet, Rational};
use crate::mux::{self, Muxer, VideoTrack};
#[derive(Clone, Debug)]
pub struct Settings {
pub encoding: Option<Encoding>,
pub quality: f32,
pub max_size: Option<(u32, u32)>,
pub level: Level,
pub keyframe_interval: Duration,
pub realtime: bool,
pub hardware: Hardware,
pub threads: usize,
}
impl Default for Settings {
fn default() -> Self {
Settings {
encoding: None,
quality: 0.68,
max_size: None,
level: Level::L4_1,
keyframe_interval: Duration::from_secs(2),
realtime: false,
hardware: Hardware::Prefer,
threads: 0,
}
}
}
impl Settings {
pub fn proxy() -> Self {
Settings {
quality: 0.3,
max_size: Some((640, 640)),
realtime: true,
..Settings::default()
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Progress {
pub frames: u64,
pub total: Option<u64>,
pub fraction: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Summary {
pub encoding: Encoding,
pub container: Container,
pub encoder: Backend,
pub width: u32,
pub height: u32,
pub frames: u64,
pub duration: Duration,
pub bytes: u64,
pub decoder_hardware: bool,
pub encoder_hardware: bool,
}
pub fn transcode(
input: impl AsRef<Path>,
output: impl AsRef<Path>,
settings: &Settings,
mut progress: impl FnMut(Progress),
cancel: &AtomicBool,
) -> Result<Summary> {
let (input, output) = (input.as_ref(), output.as_ref());
let result = Pipeline::open(input, output, settings)
.and_then(|pipeline| pipeline.run(&mut progress, cancel));
if result.is_err() {
let _ = std::fs::remove_file(output);
}
result
}
pub fn plan(input: impl AsRef<Path>, settings: &Settings) -> Result<(Encoding, u32, u32)> {
let demuxer = crate::open_media(input.as_ref())?;
let track = demuxer
.info()
.video()
.ok_or_else(|| no_video(input.as_ref()))?;
let encoding = choose_encoding(settings)?;
let (width, height) = output_size(
track.width,
track.height,
frame_rate_of(track.frame_rate),
encoding,
settings,
);
Ok((encoding, width, height))
}
pub(crate) fn choose_encoding(settings: &Settings) -> Result<Encoding> {
match settings.encoding {
Some(encoding) => {
if !encode::is_available(encoding, settings.hardware) {
let config = EncoderConfig {
hardware: settings.hardware,
..EncoderConfig::new(
encoding,
16,
16,
Rational::new(30, 1),
Default::default(),
)
};
encode::open(&config)?;
}
Ok(encoding)
}
None => encode::default_encoding(settings.hardware).ok_or_else(|| Error::NoEncoder {
encoding: Encoding::H264,
remedy: "H.264 is the default and only ever the OS's encoder — VideoToolbox, \
Media Foundation, or MediaCodec — and none is usable here (or none in \
the way `hardware` asks). Ask for AV1 explicitly to write it with rav1e"
.to_string(),
}),
}
}
fn output_size(
width: u32,
height: u32,
frame_rate: Rational,
encoding: Encoding,
settings: &Settings,
) -> (u32, u32) {
let (mut width, mut height) = match settings.max_size {
Some((max_width, max_height)) => crate::scale::fit_within(width, height, max_width, max_height),
None => (width, height),
};
if encoding == Encoding::H264 {
(width, height) = encode::fit_level(width, height, frame_rate, settings.level);
}
width = (width & !1).max(2);
height = (height & !1).max(2);
(width, height)
}
fn frame_rate_of(stated: Option<Rational>) -> Rational {
stated
.filter(|rate| rate.numerator > 0)
.unwrap_or(Rational::new(30, 1))
}
fn no_video(path: &Path) -> Error {
Error::unsupported(format!("{}: there is no video track in it", path.display()))
}
struct Pipeline<'a> {
output: &'a Path,
demuxer: Box<dyn crate::demux::Demuxer>,
decoder: Box<dyn crate::decode::Decoder>,
track_id: u32,
encoding: Encoding,
frame_rate: Rational,
width: u32,
height: u32,
total: Option<u64>,
settings: Settings,
encoder: Option<Box<dyn Encoder>>,
color: crate::color::ColorSpace,
muxer: Option<Box<dyn Muxer>>,
waiting: Vec<Packet>,
origin: Option<Duration>,
frames: u64,
}
impl<'a> Pipeline<'a> {
fn open(input: &Path, output: &'a Path, settings: &Settings) -> Result<Self> {
let demuxer = crate::open_media(input)?;
let track = demuxer.info().video().ok_or_else(|| no_video(input))?.clone();
let decoder =
crate::decode::open_with(&DecoderConfig::from_track(&track)?, settings.hardware)?;
let encoding = choose_encoding(settings)?;
let frame_rate = frame_rate_of(track.frame_rate);
let (width, height) = output_size(track.width, track.height, frame_rate, encoding, settings);
let total = (!track.duration.is_zero() && track.frame_rate.is_some())
.then(|| (track.duration.as_secs_f64() * frame_rate.as_f64()).round() as u64)
.filter(|total| *total > 0);
Ok(Pipeline {
output,
demuxer,
decoder,
track_id: track.id,
encoding,
frame_rate,
width,
height,
total,
settings: settings.clone(),
encoder: None,
color: crate::color::ColorSpace::default(),
muxer: None,
waiting: Vec::new(),
origin: None,
frames: 0,
})
}
fn run(mut self, progress: &mut impl FnMut(Progress), cancel: &AtomicBool) -> Result<Summary> {
while let Some(packet) = self.demuxer.next_packet()? {
if cancel.load(Ordering::Relaxed) {
return Err(Error::Cancelled);
}
if packet.track_id != self.track_id {
continue;
}
if let Some(frame) = self.decoder.decode(&packet)? {
self.picture(frame, progress)?;
}
}
for frame in self.decoder.flush()? {
if cancel.load(Ordering::Relaxed) {
return Err(Error::Cancelled);
}
self.picture(frame, progress)?;
}
let Some(encoder) = self.encoder.as_mut() else {
return Err(Error::Decode {
encoding: self.decoder.encoding(),
reason: "the file decoded to no pictures at all".to_string(),
});
};
let rest = encoder.finish()?;
self.deliver(rest)?;
let encoder = self.encoder.take().expect("checked above");
let muxer = self
.muxer
.take()
.ok_or_else(|| encode_error("the encoder never said what its stream was"))?;
let written = muxer.finish()?;
progress(Progress {
frames: self.frames,
total: self.total,
fraction: 1.0,
});
Ok(Summary {
encoding: self.encoding,
container: mux::container_for(self.encoding)?,
encoder: encoder.backend(),
width: self.width,
height: self.height,
frames: written.samples,
duration: written.duration,
bytes: written.bytes,
decoder_hardware: self.decoder.is_hardware(),
encoder_hardware: encoder.is_hardware(),
})
}
fn picture(&mut self, frame: Frame, progress: &mut impl FnMut(Progress)) -> Result<()> {
let origin = *self.origin.get_or_insert(frame.pts());
let frame = if (frame.width(), frame.height()) == (self.width, self.height) {
frame
} else {
crate::scale::resize(&frame, self.width, self.height)?
};
let frame = frame.with_pts(frame.pts().saturating_sub(origin));
if self.encoder.is_none() {
let config = EncoderConfig {
quality: self.settings.quality,
keyframe_interval: self.settings.keyframe_interval,
realtime: self.settings.realtime,
hardware: self.settings.hardware,
level: self.settings.level,
threads: self.settings.threads,
..EncoderConfig::new(
self.encoding,
self.width,
self.height,
self.frame_rate,
frame.color(),
)
};
self.color = frame.color();
self.encoder = Some(encode::open(&config)?);
}
let packets = self
.encoder
.as_mut()
.expect("opened above")
.encode(&frame)?;
self.deliver(packets)?;
self.frames += 1;
progress(Progress {
frames: self.frames,
total: self.total,
fraction: self
.total
.map_or(0.0, |total| (self.frames as f32 / total as f32).min(0.99)),
});
Ok(())
}
fn deliver(&mut self, packets: Vec<Packet>) -> Result<()> {
self.waiting.extend(packets);
if self.muxer.is_none() {
let Some(record) = self
.encoder
.as_ref()
.and_then(|encoder| encoder.config_record())
else {
return Ok(());
};
self.muxer = Some(mux::create(
self.output,
VideoTrack {
encoding: self.encoding,
width: self.width,
height: self.height,
frame_rate: self.frame_rate,
color: self.color,
config_record: record,
},
)?);
}
let muxer = self.muxer.as_mut().expect("opened above");
for packet in self.waiting.drain(..) {
muxer.write(packet)?;
}
Ok(())
}
}
fn encode_error(reason: impl Into<String>) -> Error {
Error::Encode {
reason: reason.into(),
}
}