I am trying to encode a video using the ffmpeg_next crate. I got everything working and it successfully creates an output video.
The only problem is that the time_base of my stream is wrongly written to the file.
I can confirm that I set the timebase correctly for both the encoder as well as the stream.
By debug prints I was able to narrow the problem down. octx.write_header().unwrap();
causes the stream timebase to be reset from Rational(1/30) to Rational(1/15360). Changing the timebase back afterwards has no effect. The wrong value must have been written to the header.
I modified the src code of ffmpeg-next and recompiled it. I can confirm that the correct value is set before the call to avformat_write_header
pub fn write_header(&mut self) -> Result<(), Error> {
println!(
"_________________ {:?}",
self.stream(0).unwrap().time_base()
);
unsafe {
match avformat_write_header(self.as_mut_ptr(), ptr::null_mut()) {
0 => Ok(()),
e => Err(Error::from(e)),
}
}
}
To my understanding this must be a bug in the crate but I dont want to accuse someone with my non existing knowledge about ffmpeg. Also the examples in the github repo seem not to have this problem. My fault then? Unfortunately I was not able to get the transcode-x264 to run. Most of my code comes from this example.
Relevant code bits are these. I dont know how much the set_parameters influences anything. My testing said it has no influence. I also tried to set the timebase at the very end of the function if it gets reset my the parameters. This is not working
let mut ost = octx.add_stream(codec)?;
ost.set_time_base(Rational::new(1, FPS));
ost.set_parameters(&encoder);
encoder.set_time_base(Rational::new(1, FPS));
ost.set_parameters(&opened_encoder);
By default and in the above example the streams timebase is 0/0. If I leave it out or change it to this manually it has no effect.
I also noticed that changing the value inside set_pts influences the output fps. Although not the timebase. I think this is more of a sideeffect.
I will leave a minimal reproducible example below. Any help or hints would be appreciated
abstract main function
fn main() {
let output_file = "output.mp4";
let x264_opts = parse_opts("preset=medium".to_string()).expect("invalid x264 options string");
ffmpeg_next::init().unwrap();
let mut octx = format::output(output_file).unwrap();
let mut encoder = Encoder::new(&mut octx, x264_opts).unwrap();
format::context::output::dump(&octx, 0, Some(&output_file));
//This line somehow clears the streams time base
octx.write_header().unwrap();
// Without this line, the next logs returns Rational(1/30) Rational(1/15360) indicating streams timebase is wrong. even thought I set it above
// this line changes it back but only for the print but not the actual output. Because the faulty data is written into the header
// octx.stream_mut(0)
// .unwrap()
// .set_time_base(Rational::new(1, FPS));
println!(
"---------------- {:?} {:?}",
encoder.encoder.time_base(),
octx.stream(0).unwrap().time_base(),
);
for frame_num in 0..100 {
let mut frame = encoder.create_frame();
frame.set_pts(Some(frame_num));
encoder.add_frame(&frame, &mut octx);
}
encoder.close(&mut octx);
octx.write_trailer().unwrap();
}
Encoder struct containing the implementation logic
struct Encoder {
encoder: encoder::Video,
}
impl Encoder {
fn new(
octx: &mut format::context::Output,
x264_opts: Dictionary,
) -> Result<Self, ffmpeg_next::Error> {
let set_header = octx
.format()
.flags()
.contains(ffmpeg_next::format::flag::Flags::GLOBAL_HEADER);
let codec = encoder::find(codec::Id::H264);
let mut ost = octx.add_stream(codec)?;
ost.set_time_base(Rational::new(1, FPS));
let mut encoder = codec::context::Context::new_with_codec(
encoder::find(codec::Id::H264)
.ok_or(ffmpeg_next::Error::InvalidData)
.unwrap(),
)
.encoder()
.video()
.unwrap();
ost.set_parameters(&encoder);
encoder.set_width(WIDTH);
encoder.set_height(HEIGHT);
encoder.set_aspect_ratio(WIDTH as f64 / HEIGHT as f64);
encoder.set_format(util::format::Pixel::YUV420P);
encoder.set_frame_rate(Some(Rational::new(FPS, 1)));
encoder.set_time_base(Rational::new(1, FPS));
if set_header {
encoder.set_flags(ffmpeg_next::codec::flag::Flags::GLOBAL_HEADER);
}
let opened_encoder = encoder
.open_with(x264_opts.to_owned())
.expect("error opening x264 with supplied settings");
ost.set_parameters(&opened_encoder);
println!(
"nost time_base: {}; encoder time_base: {}; encoder frame_rate: {}n",
ost.time_base(),
&opened_encoder.time_base(),
&opened_encoder.frame_rate()
);
Ok(Self {
encoder: opened_encoder,
})
}
fn add_frame(&mut self, frame: &frame::Video, octx: &mut format::context::Output) {
self.encoder.send_frame(frame).unwrap();
self.process_packets(octx);
}
fn close(&mut self, octx: &mut format::context::Output) {
self.encoder.send_eof().unwrap();
self.process_packets(octx);
}
fn process_packets(&mut self, octx: &mut format::context::Output) {
let mut encoded = Packet::empty();
while self.encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(octx).unwrap();
}
}
fn create_frame(&self) -> frame::Video {
return frame::Video::new(
self.encoder.format(),
self.encoder.width(),
self.encoder.height(),
);
}
}
other util stuff
use ffmpeg_next::{
codec::{self},
encoder, format, frame, util, Dictionary, Packet, Rational,
};
const FPS: i32 = 30;
const WIDTH: u32 = 720;
const HEIGHT: u32 = 1080;
fn parse_opts<'a>(s: String) -> Option<Dictionary<'a>> {
let mut dict = Dictionary::new();
for keyval in s.split_terminator(',') {
let tokens: Vec<&str> = keyval.split('=').collect();
match tokens[..] {
[key, val] => dict.set(key, val),
_ => return None,
}
}
Some(dict)
}
4