#![forbid(unsafe_code)]
mod colour;
mod decode;
mod display;
mod extract;
mod framing;
mod help;
mod input;
mod md5;
#[cfg(test)]
mod outcome_tests;
mod params;
mod qp_replay;
mod rate_control;
mod session;
mod spatial_stats;
mod sse_log;
#[cfg(feature = "analysis")]
mod stat;
mod stdio;
mod timestamps;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Instant;
use crate::framing::IvfWriter;
use crate::spatial_stats::SpatialTelemetry;
use nc_av1::Av1Backend;
use nc_av2::Av2Backend;
use nc_common::{Picture, yuv};
use crate::input::{FrameReader, InputFormat};
use nc_encoder::{
Codec, CodecBackend, EncodedFrame, Encoder, EncoderConfig, FrameRate, MICROS_PER_SECOND,
ParamOverrides, RateControlConfig, RateControlStats, ScalabilityMode, StreamSignal,
TimingSignal,
};
use nc_h264::H264Backend;
use nc_hevc::HevcBackend;
use nc_vp9::Vp9Backend;
const DEFAULT_BLOCK_LOG2: u8 = 4;
const AV2_BLOCK_LOG2: u8 = 3;
const VVC_BLOCK_LOG2: u8 = 5;
const DEFAULT_INTRA_PERIOD_SECONDS: u32 = 10;
const ENCODE_INPUT_FLAGS: &[&str] = &[
"--input",
"--sse-reference",
"--timestamps",
"--params",
"--qp-replay",
];
const ENCODE_OUTPUT_FLAGS: &[&str] = &[
"--output",
"--recon",
"--display-recon",
"--layer-md5",
"--spatial-stats",
"--sse-log",
"--rate-log",
"--timestamps-out",
"--display",
];
#[derive(Debug)]
struct EncodeArgs {
codec: Codec,
input: PathBuf,
input_format: InputFormat,
output: Option<PathBuf>,
recon: Option<PathBuf>,
display_recon: Option<PathBuf>,
layer_md5: Option<PathBuf>,
spatial_stats: Option<PathBuf>,
sse_log: Option<(PathBuf, PathBuf)>,
width: usize,
height: usize,
frame_rate: FrameRate,
quantizer: u32,
frames: u32,
block_log2: u8,
intra_period: u32,
key_qp_offset: Option<i32>,
temporal_layers: u8,
signal: StreamSignal,
params: ParamOverrides,
rate_control: Option<RateControlConfig>,
rate_changes: Vec<rate_control::RateChange>,
resizes: Vec<(u32, usize, usize)>,
keyframes: Vec<u32>,
rate_log: Option<PathBuf>,
qp_replay: Option<qp_replay::Replay>,
timestamps: Option<Vec<i64>>,
timestamps_out: Option<PathBuf>,
simulate_loss: Option<SimulatedLoss>,
}
#[derive(Clone, Copy, Debug)]
struct SimulatedLoss {
frame: u64,
spatial_id: Option<u8>,
}
const VERSION: &str = concat!(
"nanocodec ",
env!("CARGO_PKG_VERSION"),
" ",
env!("NC_GIT_COMMIT"),
env!("NC_GIT_DIRTY")
);
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
if matches!(args.first().map(String::as_str), Some("--version" | "-V")) {
println!("{VERSION}");
return ExitCode::SUCCESS;
}
if matches!(
args.first().map(String::as_str),
Some("help" | "--help" | "-h")
) {
print!("{}", help::text());
return ExitCode::SUCCESS;
}
if args.first().map(String::as_str) == Some("params") {
let codec = args
.iter()
.position(|a| a == "--codec")
.and_then(|i| args.get(i + 1));
return match params::listing(codec.map(String::as_str)) {
Ok(text) => {
print!("{text}");
ExitCode::SUCCESS
}
Err(message) => {
eprintln!("nanocodec: {message}");
ExitCode::FAILURE
}
};
}
if args.first().map(String::as_str) == Some("decode") {
return match decode::run(&args[1..]) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("nanocodec: {message}");
ExitCode::FAILURE
}
};
}
#[cfg(feature = "analysis")]
if args.first().map(String::as_str) == Some("stat") {
return match stat::run(&args[1..]) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("nanocodec: {message}");
ExitCode::FAILURE
}
};
}
if args.first().map(String::as_str) == Some("extract") {
return match extract::run(&args[1..]) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("nanocodec: {message}");
ExitCode::FAILURE
}
};
}
if args.first().map(String::as_str) == Some("session") {
return match parse_encode(&args[1..]).and_then(|a| session::run(&a, &args[1..])) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("nanocodec: {message}");
ExitCode::FAILURE
}
};
}
if args.first().map(String::as_str) != Some("encode") {
eprint!("{}", help::text());
return ExitCode::from(2);
}
match parse_encode(&args[1..]).and_then(|a| encode(&a)) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("nanocodec: {message}");
ExitCode::FAILURE
}
}
}
fn parse_codec(name: &str) -> Result<Codec, String> {
match name {
"av1" => Ok(Codec::Av1),
"vp9" => Ok(Codec::Vp9),
"av2" => Ok(Codec::Av2),
"hevc" => Ok(Codec::Hevc),
"vvc" | "h266" => Ok(Codec::Vvc),
"h264" => Ok(Codec::H264),
other => Err(format!(
"unsupported codec '{other}' (implemented: av1, h264, hevc, vvc, vp9, av2)"
)),
}
}
fn parse_block_log2(block: Option<&str>, codec: Codec) -> Result<u8, String> {
match block {
None if codec == Codec::Av2 => Ok(AV2_BLOCK_LOG2),
None if codec == Codec::Vvc => Ok(VVC_BLOCK_LOG2),
None => Ok(DEFAULT_BLOCK_LOG2),
Some("8") => Ok(3),
Some("16") => Ok(4),
Some("32") => Ok(5),
Some("64") => Ok(6),
Some(other) => Err(format!("unsupported --block {other}")),
}
}
fn parse_encode(args: &[String]) -> Result<EncodeArgs, String> {
stdio::check_streams(args, ENCODE_INPUT_FLAGS, ENCODE_OUTPUT_FLAGS)?;
let get = |flag: &str| -> Option<String> {
let i = args.iter().position(|a| a == flag)?;
args.get(i + 1).cloned()
};
let required = |v: Option<String>, flag: &str| v.ok_or_else(|| format!("missing {flag}"));
let number = |v: String, flag: &str| {
v.parse::<u64>()
.map_err(|_| format!("bad value for {flag}: {v}"))
};
let codec = parse_codec(&required(get("--codec"), "--codec")?)?;
let rate_control = rate_control::parse(args)?;
let rate_changes = rate_control::changes(args, rate_control)?;
let timestamps = get("--timestamps")
.map(|path| {
timestamps::read(stdio::open(Path::new(&path))?).map_err(|e| format!("{path}: {e}"))
})
.transpose()?;
let qp_replay = get("--qp-replay")
.map(|path| {
let text = std::io::read_to_string(stdio::open(Path::new(&path))?)
.map_err(|e| format!("{path}: {e}"))?;
qp_replay::parse(&text)
})
.transpose()?;
if qp_replay.is_some() && rate_control.is_none() {
return Err("--qp-replay needs --bitrate".into());
}
let block_log2 = parse_block_log2(get("--block").as_deref(), codec)?;
let frame_rate = parse_fps(&required(get("--fps"), "--fps")?)?;
Ok(EncodeArgs {
codec,
input: required(get("--input"), "--input")?.into(),
input_format: get("--input-format")
.map_or(Ok(InputFormat::I420), |v| InputFormat::parse(&v))?,
output: get("--output").map(PathBuf::from),
recon: get("--recon").map(PathBuf::from),
display_recon: get("--display-recon").map(PathBuf::from),
layer_md5: get("--layer-md5").map(PathBuf::from),
spatial_stats: get("--spatial-stats").map(PathBuf::from),
sse_log: match (get("--sse-log"), get("--sse-reference")) {
(Some(log), Some(reference)) => Some((log.into(), reference.into())),
(None, None) => None,
_ => return Err("--sse-log and --sse-reference go together".into()),
},
width: number(required(get("--width"), "--width")?, "--width")? as usize,
height: number(required(get("--height"), "--height")?, "--height")? as usize,
frame_rate,
quantizer: match get("--qp") {
Some(value) => number(value, "--qp")? as u32,
None if rate_control.is_some() => {
if matches!(codec, Codec::Av1 | Codec::Vp9 | Codec::Av2) {
180
} else {
32
}
}
None => return Err("missing --qp (or specify --bitrate)".into()),
},
frames: number(required(get("--frames"), "--frames")?, "--frames")? as u32,
block_log2,
intra_period: match get("--intra-period") {
None => default_intra_period(frame_rate),
Some(v) => number(v, "--intra-period")? as u32,
},
key_qp_offset: get("--key-qp-offset")
.map(|v| v.parse().map_err(|_| format!("bad --key-qp-offset {v}")))
.transpose()?,
temporal_layers: parse_temporal_layers(args)?,
signal: StreamSignal {
colour: colour::parse(get)?,
timing: parse_timing(get("--timing").as_deref(), timestamps.is_some())?,
},
params: with_fixed_qp_profile(encode_params(args, codec)?, rate_control.is_none()),
rate_control,
rate_changes,
resizes: parse_resizes(args)?,
keyframes: parse_keyframes(args)?,
rate_log: get("--rate-log").map(PathBuf::from),
qp_replay,
timestamps,
timestamps_out: get("--timestamps-out").map(PathBuf::from),
simulate_loss: get("--simulate-loss")
.as_deref()
.map(parse_simulate_loss)
.transpose()?,
})
}
fn parse_simulate_loss(spec: &str) -> Result<SimulatedLoss, String> {
let bad = || format!("bad value for --simulate-loss: {spec} (expected FRAME or FRAME:LAYER)");
if let Some((frame, layer)) = spec.split_once(':') {
Ok(SimulatedLoss {
frame: frame.parse::<u64>().map_err(|_| bad())?,
spatial_id: Some(layer.parse::<u8>().map_err(|_| bad())?),
})
} else {
Ok(SimulatedLoss {
frame: spec.parse::<u64>().map_err(|_| bad())?,
spatial_id: None,
})
}
}
fn parse_scalability(args: &[String]) -> Result<Option<ScalabilityMode>, String> {
let Some(spec) = params::flag_value(args, "--scalability") else {
return Ok(None);
};
if args.iter().any(|a| a == "--temporal-layers") {
return Err("--scalability and --temporal-layers cannot be combined".into());
}
ScalabilityMode::parse(&spec).map(Some).ok_or_else(|| {
format!(
"unsupported --scalability '{spec}' (expected L1T1..L1T3, L2T1..L2T3[_KEY], L3T1..L3T3[_KEY])"
)
})
}
fn parse_temporal_layers(args: &[String]) -> Result<u8, String> {
if let Some(mode) = parse_scalability(args)? {
return Ok(mode.temporal_layers());
}
if !args.iter().any(|a| a == "--temporal-layers") {
return Ok(1);
}
let value = params::flag_value(args, "--temporal-layers")
.ok_or_else(|| "missing --temporal-layers".to_string())?;
value
.parse::<u8>()
.map_err(|_| format!("bad value for --temporal-layers: {value}"))
}
fn with_fixed_qp_profile(mut params: ParamOverrides, fixed_qp: bool) -> ParamOverrides {
if fixed_qp && !params.contains(FIXED_QP_PROFILE_PARAM) {
params.set(FIXED_QP_PROFILE_PARAM, 1.0);
}
params
}
const FIXED_QP_PROFILE_PARAM: &str = "rate.fixed_qp_profile";
fn encode_params(args: &[String], codec: Codec) -> Result<ParamOverrides, String> {
let number = |v: String, flag: &str| {
v.parse::<u64>()
.map_err(|_| format!("bad value for {flag}: {v}"))
};
let named = if args.iter().any(|a| a == "--still-camera") {
nc_encoder::still_camera_params(codec)
} else {
ParamOverrides::default()
};
let mut params = params::overrides_from_args_with_scalability(
args,
codec,
&named,
parse_scalability(args)?,
)?;
if let Some(period) = params::flag_value(args, "--intra-refresh") {
params.set("refresh.period", number(period, "--intra-refresh")? as f32);
}
if let Some(interval) = params::flag_value(args, "--ltr-interval") {
params.set("ltr.interval", number(interval, "--ltr-interval")? as f32);
}
if let Some(FilmGrainFlag { strength }) = parse_film_grain(args)? {
nc_av1::enable_film_grain(codec, strength, &mut params)
.map_err(|e| format!("--film-grain: {e}"))?;
}
Ok(params)
}
fn parse_resizes(args: &[String]) -> Result<Vec<(u32, usize, usize)>, String> {
let mut resizes = args
.windows(2)
.filter(|pair| pair[0] == "--resize")
.map(|pair| {
let spec = pair[1].as_str();
let bad = || format!("bad --resize {spec} (FRAME:WxH)");
let (frame, size) = spec.split_once(':').ok_or_else(bad)?;
let (width, height) = size.split_once('x').ok_or_else(bad)?;
Ok((
frame.parse().map_err(|_| bad())?,
width.parse().map_err(|_| bad())?,
height.parse().map_err(|_| bad())?,
))
})
.collect::<Result<Vec<_>, String>>()?;
resizes.sort_unstable();
Ok(resizes)
}
fn parse_keyframes(args: &[String]) -> Result<Vec<u32>, String> {
let mut frames = args
.windows(2)
.filter(|pair| pair[0] == "--keyframe")
.map(|pair| {
pair[1]
.parse()
.map_err(|_| format!("bad --keyframe {} (FRAME)", pair[1]))
})
.collect::<Result<Vec<u32>, String>>()?;
frames.sort_unstable();
Ok(frames)
}
fn parse_timing(text: Option<&str>, timestamped: bool) -> Result<TimingSignal, String> {
match text {
None if timestamped => Ok(TimingSignal::Variable),
None | Some("nominal") => Ok(TimingSignal::Nominal),
Some("constant") => Ok(TimingSignal::Constant),
Some("variable") => Ok(TimingSignal::Variable),
Some(other) => Err(format!(
"bad --timing {other} (nominal, constant, variable)"
)),
}
}
struct FilmGrainFlag {
strength: Option<f32>,
}
fn parse_film_grain(args: &[String]) -> Result<Option<FilmGrainFlag>, String> {
let Some(index) = args.iter().position(|a| a == "--film-grain") else {
return Ok(None);
};
match args.get(index + 1).filter(|next| !next.starts_with("--")) {
None => Ok(Some(FilmGrainFlag { strength: None })),
Some(text) => match text.parse::<f32>() {
Ok(strength) if strength.is_finite() && strength > 0.0 => Ok(Some(FilmGrainFlag {
strength: Some(strength),
})),
_ => Err(format!(
"bad --film-grain strength {text} (a positive number; 1 matches the measured source noise)"
)),
},
}
}
fn default_intra_period(rate: FrameRate) -> u32 {
let frames =
u64::from(rate.num) * u64::from(DEFAULT_INTRA_PERIOD_SECONDS) / u64::from(rate.den.max(1));
u32::try_from(frames).unwrap_or(u32::MAX).max(1)
}
fn parse_fps(text: &str) -> Result<FrameRate, String> {
let bad = || format!("bad --fps {text}");
if let Some((num, den)) = text.split_once('/') {
return Ok(FrameRate {
num: num.parse().map_err(|_| bad())?,
den: den.parse().map_err(|_| bad())?,
});
}
let fps: f64 = text.parse().map_err(|_| bad())?;
let ntsc = fps * 1001.0 / 1000.0;
if (ntsc - ntsc.round()).abs() < 1e-3 && (fps - fps.round()).abs() > 1e-3 {
return Ok(FrameRate {
num: (ntsc.round() as u32) * 1000,
den: 1001,
});
}
Ok(FrameRate {
num: fps.round() as u32,
den: 1,
})
}
fn encoder_config(args: &EncodeArgs) -> EncoderConfig {
EncoderConfig {
codec: args.codec,
width: args.width,
height: args.height,
frame_rate: args.frame_rate,
quantizer: args.quantizer,
intra_period: args.intra_period,
block_log2: args.block_log2,
key_qp_offset: args.key_qp_offset,
temporal_layers: args.temporal_layers,
signal: args.signal,
params: args.params.clone(),
}
}
fn encode(args: &EncodeArgs) -> Result<(), String> {
let config = encoder_config(args);
let path = args.output.as_ref().ok_or("missing --output")?;
let output = stdio::create(path)?;
match args.codec {
Codec::Av1 | Codec::Vp9 | Codec::Av2 => {
let dims =
|d: usize| u16::try_from(d).map_err(|_| "dimension too large for IVF".to_string());
let (base_den, base_num) = if args.timestamps.is_some() {
(MICROS_PER_SECOND as u32, 1)
} else {
(args.frame_rate.num, args.frame_rate.den)
};
let ivf = IvfWriter::new(
output,
args.codec,
dims(args.width)?,
dims(args.height)?,
base_den,
base_num,
args.frames,
)
.map_err(|e| e.to_string())?;
if args.codec == Codec::Vp9 {
run::<Vp9Backend, _>(args, config, ivf)
} else if args.codec == Codec::Av2 {
run::<Av2Backend, _>(args, config, ivf)
} else {
run::<Av1Backend, _>(args, config, ivf)
}
}
Codec::Hevc => run::<HevcBackend, _>(args, config, AnnexB(output)),
Codec::Vvc => run::<nc_vvc::VvcBackend, _>(args, config, AnnexB(output)),
Codec::H264 => run::<H264Backend, _>(args, config, AnnexB(output)),
}
}
fn write_rate_row(
log: &mut impl Write,
index: u32,
frame: &EncodedFrame,
stats: &RateControlStats,
(width, height): (usize, usize),
encode_us: u128,
) -> std::io::Result<()> {
let kind = if frame.metadata.dropped {
"Dropped".to_string()
} else if frame.metadata.held {
"Held".to_string()
} else {
format!("{:?}", frame.metadata.frame_type)
};
writeln!(
log,
"{index},{kind},{},{},{},{:.6},{},{},{},{},{:.0},{:.3},{},{width},{height},{:.0},{:.0},{},{:.3},{:.4},{:.0},{:.0},{:.0},{},{encode_us}",
frame.metadata.quantizer,
stats.current_quantizer,
frame.data.len(),
stats.buffer_fullness,
stats.actual_bps,
stats.total_bits,
stats.buffer_overflows,
stats.target_bps,
stats.key_loan_bits,
stats.residual_sigma,
stats.recoded_frames,
stats.plan.budget_bits,
stats.plan.predicted_bits,
stats.plan.level_quantizer,
stats.plan.cadence_offset,
stats.plan.static_fraction,
stats.plan.excess_bits,
stats.plan.debit_bits,
stats.plan.capacity_bits,
stats.plan.codings
)
}
trait FrameSink {
fn write_frame(&mut self, data: &[u8], pts: u64) -> Result<(), String>;
fn finish(self) -> Result<(), String>;
}
impl<W: Write> FrameSink for IvfWriter<W> {
fn write_frame(&mut self, data: &[u8], pts: u64) -> Result<(), String> {
IvfWriter::write_frame(self, data, pts).map_err(|e| e.to_string())
}
fn finish(self) -> Result<(), String> {
IvfWriter::finish(self).map(drop).map_err(|e| e.to_string())
}
}
struct AnnexB<W: Write>(W);
impl<W: Write> FrameSink for AnnexB<W> {
fn write_frame(&mut self, data: &[u8], _pts: u64) -> Result<(), String> {
self.0.write_all(data).map_err(|e| e.to_string())
}
fn finish(mut self) -> Result<(), String> {
self.0.flush().map_err(|e| e.to_string())
}
}
struct PendingChanges<'a> {
rate: std::iter::Peekable<std::slice::Iter<'a, rate_control::RateChange>>,
resize: std::iter::Peekable<std::slice::Iter<'a, (u32, usize, usize)>>,
keyframes: std::iter::Peekable<std::slice::Iter<'a, u32>>,
}
impl<'a> PendingChanges<'a> {
fn new(args: &'a EncodeArgs) -> Self {
Self {
rate: args.rate_changes.iter().peekable(),
resize: args.resizes.iter().peekable(),
keyframes: args.keyframes.iter().peekable(),
}
}
fn apply<B: CodecBackend, R: Read>(
&mut self,
index: u32,
encoder: &mut Encoder<B>,
input: &mut FrameReader<R>,
) -> Result<(), String> {
while let Some(change) = self.rate.next_if(|change| change.frame <= index) {
encoder
.set_rate_control(change.config)
.map_err(|error| error.to_string())?;
}
while let Some(&(_, width, height)) = self.resize.next_if(|resize| resize.0 <= index) {
encoder
.set_resolution(width, height)
.map_err(|error| error.to_string())?;
input.set_size(width, height);
}
if self.keyframes.next_if(|&&frame| frame <= index).is_some() {
while self.keyframes.next_if(|&&frame| frame <= index).is_some() {}
encoder.request_keyframe();
}
Ok(())
}
}
struct ReconOutputs {
coded: Option<stdio::Output>,
shown: Option<stdio::Output>,
layer_md5: Option<stdio::Output>,
sse: Option<sse_log::SseLog>,
display: display::Display,
}
impl ReconOutputs {
fn create(args: &EncodeArgs) -> Result<Self, String> {
let create = |path: &Option<PathBuf>| path.as_deref().map(stdio::create).transpose();
let mut layer_md5 = create(&args.layer_md5)?;
if let Some(out) = layer_md5.as_mut() {
writeln!(
out,
"tu,spatial_id,temporal_layer,frame_type,width,height,bytes,md5,droppable,op_mask,inter_layer_ref"
)
.map_err(|e| e.to_string())?;
}
Ok(Self {
coded: create(&args.recon)?,
shown: create(&args.display_recon)?,
layer_md5,
sse: args
.sse_log
.as_ref()
.map(|(log, reference)| sse_log::SseLog::create(reference, log))
.transpose()?,
display: display::Display::default(),
})
}
fn write(
&mut self,
index: u32,
picture: &Picture,
stream: (usize, usize),
coded: bool,
lost: bool,
) -> Result<(), String> {
if let (true, Some(out)) = (coded, self.coded.as_mut()) {
yuv::write_frame(out, picture).map_err(|e| e.to_string())?;
}
if lost || (self.shown.is_none() && self.sse.is_none()) {
return Ok(());
}
let shown = self.display.shown(picture, stream)?;
if let Some(out) = self.shown.as_mut() {
yuv::write_frame(out, shown).map_err(|e| e.to_string())?;
}
if let Some(sse) = self.sse.as_mut() {
sse.write(index, shown)?;
}
Ok(())
}
fn write_layers<B: CodecBackend>(
&mut self,
index: u32,
frame: &EncodedFrame,
encoder: &Encoder<B>,
) -> Result<(), String> {
let Some(out) = self.layer_md5.as_mut() else {
return Ok(());
};
if frame.layers.is_empty() {
let pic = encoder.reconstruction();
writeln!(
out,
"{index},0,{},{:?},{},{},{},{},0,1,0",
frame.metadata.temporal_layer,
frame.metadata.frame_type,
pic.width(),
pic.height(),
frame.data.len(),
md5::picture(pic)
)
.map_err(|e| e.to_string())?;
} else {
for layer in &frame.layers {
let pic = encoder
.layer_reconstruction(layer.spatial_id)
.ok_or("missing spatial layer reconstruction")?;
writeln!(
out,
"{index},{},{},{:?},{},{},{},{},{},{},{}",
layer.spatial_id,
layer.temporal_layer,
layer.frame_type,
pic.width(),
pic.height(),
layer.byte_len,
md5::picture(pic),
u8::from(layer.droppable),
layer.operating_point_mask,
u8::from(layer.inter_layer_ref)
)
.map_err(|e| e.to_string())?;
}
}
Ok(())
}
fn finish(self) -> Result<(), String> {
for mut out in [self.coded, self.shown, self.layer_md5]
.into_iter()
.flatten()
{
out.flush().map_err(|e| e.to_string())?;
}
self.sse.map_or(Ok(()), sse_log::SseLog::finish)
}
}
fn run<B: CodecBackend, S: FrameSink>(
args: &EncodeArgs,
config: EncoderConfig,
mut sink: S,
) -> Result<(), String> {
let mut encoder = Encoder::<B>::new(config).map_err(|e| e.to_string())?;
if let Some(rate_control) = args.rate_control {
encoder
.set_rate_control(rate_control)
.map_err(|error| error.to_string())?;
}
let mut rate_log = args.rate_log.as_deref().map(stdio::create).transpose()?;
if let Some(log) = &mut rate_log {
writeln!(log,"frame,type,planned_qp,qp,bytes,buffer_fullness,actual_bps,total_bits,buffer_overflows,target_bps,key_loan_bits,residual_sigma,recoded_frames,width,height,budget_bits,predicted_bits,level_q,cadence_offset,static_fraction,excess_bits,debit_bits,capacity_bits,codings,encode_us").map_err(|error|error.to_string())?;
}
let mut input = FrameReader::new(
stdio::open_raw(&args.input)?,
args.input_format,
args.width,
args.height,
);
let mut timestamps_out = args
.timestamps_out
.as_ref()
.map(|path| {
timestamps::Writer::new(stdio::create(path)?)
.map_err(|e| format!("{}: {e}", path.display()))
})
.transpose()?;
let mut recon_out = ReconOutputs::create(args)?;
let mut telemetry = SpatialTelemetry::default();
input.look_ahead_second(&mut encoder)?;
let mut pending = PendingChanges::new(args);
for index in 0..args.frames {
pending.apply(index, &mut encoder, &mut input)?;
if let Some(replay) = &args.qp_replay {
encoder.force_next_quantizer(replay.get(index as usize).copied().flatten());
}
let timestamp = frame_timestamp(args.timestamps.as_deref(), index)?;
let started = Instant::now();
let Some(mut frame) = input.encode_next(&mut encoder, timestamp)? else {
return Err(format!("input ended after {index} frames"));
};
let encode_us = started.elapsed().as_micros();
let pts = match (&args.timestamps, timestamp) {
(Some(times), Some(t)) => u64::try_from(t - times[0]).unwrap_or(0),
_ => u64::from(index),
};
let uncoded = frame.metadata.dropped || frame.metadata.held;
let lost = apply_simulated_loss(args.simulate_loss, &mut frame, &mut encoder, uncoded)?;
let coded = !uncoded && !lost;
if coded {
sink.write_frame(&frame.data, pts)?;
telemetry.observe(&frame);
recon_out.write_layers(index, &frame, &encoder)?;
}
if let (true, Some(out)) = (coded, timestamps_out.as_mut()) {
out.write(frame.metadata.timestamp_us)
.map_err(|e| e.to_string())?;
}
let shown_pic = frame
.layers
.last()
.and_then(|l| encoder.layer_reconstruction(l.spatial_id))
.unwrap_or_else(|| encoder.reconstruction());
if let Some(log) = &mut rate_log {
let stats = encoder
.rate_control_stats()
.ok_or("rate statistics unavailable")?;
let size = (shown_pic.width(), shown_pic.height());
write_rate_row(log, index, &frame, &stats, size, encode_us)
.map_err(|error| error.to_string())?;
}
let stream = (encoder.config().width, encoder.config().height);
recon_out.write(index, shown_pic, stream, coded, lost)?;
}
if let Some(log) = &mut rate_log {
log.flush().map_err(|error| error.to_string())?;
}
if let Some(path) = args.spatial_stats.as_deref() {
telemetry.write(path, &encoder)?;
}
recon_out.finish()?;
if let Some(out) = timestamps_out {
out.finish().map_err(|e| e.to_string())?;
}
sink.finish()
}
fn apply_simulated_loss<B: CodecBackend>(
simulate_loss: Option<SimulatedLoss>,
frame: &mut EncodedFrame,
encoder: &mut Encoder<B>,
uncoded: bool,
) -> Result<bool, String> {
let Some(sim) = simulate_loss else {
return Ok(false);
};
if uncoded {
return Ok(false);
}
let tu = frame.metadata.index;
if sim.frame != tu {
encoder.acknowledge(tu);
return Ok(false);
}
if let Some(lost_sid) = sim.spatial_id
&& lost_sid >= 1
&& !frame.layers.is_empty()
{
let mut lost_mask = 1u8 << lost_sid;
for layer in &frame.layers {
if layer.spatial_id > lost_sid
&& layer.inter_layer_ref
&& (lost_mask & (1u8 << (layer.spatial_id - 1))) != 0
{
lost_mask |= 1u8 << layer.spatial_id;
}
}
for layer in &frame.layers {
if (lost_mask & (1u8 << layer.spatial_id)) == 0 {
encoder.acknowledge_layer(tu, layer.spatial_id);
} else {
encoder.report_layer_loss(tu, layer.spatial_id);
}
}
if frame
.layers
.iter()
.any(|l| (lost_mask & (1u8 << l.spatial_id)) != 0)
{
frame.data = match encoder.config().codec {
Codec::Av1 => nc_av1::filter_temporal_unit_layers(&frame.data, |_, sid| {
(lost_mask & (1u8 << sid)) == 0
})
.map_err(|e| e.to_string())?,
Codec::Vp9 => {
let coded_sids: Vec<u8> = frame.layers.iter().map(|l| l.spatial_id).collect();
nc_vp9::filter_superframe_layers(&frame.data, |idx| {
coded_sids
.get(idx)
.is_some_and(|&sid| (lost_mask & (1u8 << sid)) == 0)
})
.map_err(|e| e.to_string())?
}
codec => return Err(format!("{codec:?} does not support spatial layer loss")),
};
frame
.layers
.retain(|l| (lost_mask & (1u8 << l.spatial_id)) == 0);
let mut offset = 0usize;
let remaining = frame.layers.len();
for (idx, layer) in frame.layers.iter_mut().enumerate() {
layer.byte_offset = offset;
if idx + 1 == remaining {
layer.byte_len = frame.data.len().saturating_sub(offset);
}
offset += layer.byte_len;
}
}
return Ok(false);
}
encoder.report_loss(tu);
Ok(true)
}
fn frame_timestamp(times: Option<&[i64]>, index: u32) -> Result<Option<i64>, String> {
match times {
Some(list) => list.get(index as usize).copied().map(Some).ok_or_else(|| {
format!(
"--timestamps lists {} frames, fewer than --frames",
list.len()
)
}),
None => Ok(None),
}
}