Skip to content

Instantly share code, notes, and snippets.

@zmwangx
Created August 3, 2020 11:41
Show Gist options
  • Save zmwangx/bacd12df8b8dce4c91db47a34a637004 to your computer and use it in GitHub Desktop.
Save zmwangx/bacd12df8b8dce4c91db47a34a637004 to your computer and use it in GitHub Desktop.
extern crate ffmpeg_next as ffmpeg;
use ffmpeg::*;
use std::env;
const DEFAULT_INPUT: &str = "input.gif";
const DEFAULT_OUTPUT: &str = "output.gif";
const DEFAULT_TARGET_FPS: f64 = 25.0;
fn main() {
let input_file = env::args()
.nth(1)
.unwrap_or_else(|| DEFAULT_INPUT.to_string());
let output_file = env::args()
.nth(2)
.unwrap_or_else(|| DEFAULT_OUTPUT.to_string());
let target_fps: f64 = env::args()
.nth(3)
.unwrap_or_else(|| DEFAULT_TARGET_FPS.to_string())
.parse()
.unwrap();
init()
.map_err(|e| format!("Unable to initialize ffmpeg: {}", e))
.unwrap();
util::log::set_level(util::log::Level::Trace);
let mut input_context = format::input(&input_file)
.map_err(|e| format!("Unable to open input file: {}", e))
.unwrap();
let mut output_context = format::output(&output_file)
.map_err(|e| format!("Unable to open output file: {}", e))
.unwrap();
let stream = input_context
.streams()
.best(media::Type::Video)
.ok_or("The file has no video tracks")
.unwrap();
let stream_index = stream.index();
let mut decoder = stream
.codec()
.decoder()
.video()
.map_err(|e| format!("Unable to decode the codec used in the video: {}", e))
.unwrap();
let format_aspect_ratio = |sar: util::rational::Rational| match sar.numerator() {
0 => "1".to_string(),
_ => format!("{}/{}", sar.numerator(), sar.denominator()),
};
let buffer_args = format!(
"width={}:height={}:pix_fmt={}:time_base={}:sar={}",
decoder.width(),
decoder.height(),
decoder.format().descriptor().unwrap().name(),
stream.time_base(),
format_aspect_ratio(decoder.aspect_ratio()),
);
let mut filter = filter::Graph::new();
filter
.add(&filter::find("buffer").unwrap(), "in", &buffer_args)
.unwrap();
filter
.add(&filter::find("buffersink").unwrap(), "out", "")
.unwrap();
filter
.output("in", 0)
.unwrap()
.input("out", 0)
.unwrap()
.parse(&format!("fps=fps={},format=bgr8", target_fps)[..])
.unwrap();
filter.validate().unwrap();
let codec = ffmpeg::encoder::find(
output_context
.format()
.codec(&DEFAULT_OUTPUT, media::Type::Video),
)
.expect("Unable to find encoder")
.video()
.unwrap();
let mut output_stream = output_context.add_stream(codec).unwrap();
let mut encoder = output_stream.codec().encoder().video().unwrap();
encoder.set_format(format::Pixel::BGR8);
encoder.set_width(decoder.width());
encoder.set_height(decoder.height());
encoder.set_time_base((1, 100));
output_stream.set_time_base((1, 100));
let mut encoder = encoder.open_as(codec).unwrap();
output_stream.set_parameters(&encoder);
output_context.write_header().unwrap();
let mut input_frame_count = 0;
let mut output_frame_count = 0;
let mut input_pts = 0;
let mut output_pts = 0;
let mut write_frame = |rgba_encoded: &mut Packet| {
rgba_encoded.set_stream(0);
rgba_encoded.set_pts(Option::from(output_pts));
rgba_encoded.write_interleaved(&mut output_context).unwrap();
output_pts += (1.0 / target_fps * 100.0) as i64;
output_frame_count += 1;
};
for (s, packet) in input_context.packets() {
if s.index() != stream_index {
continue;
}
input_pts += packet.duration();
let mut input_frame = util::frame::video::Video::empty();
if !decoder.decode(&packet, &mut input_frame).unwrap() {
continue;
}
input_frame_count += 1;
let mut rgba_frame = util::frame::video::Video::empty();
let mut rgba_encoded = Packet::empty();
filter
.get("in")
.unwrap()
.source()
.add(&input_frame)
.unwrap();
while filter
.get("out")
.unwrap()
.sink()
.frame(&mut rgba_frame)
.is_ok()
{
if let Ok(true) = encoder.encode(&rgba_frame, &mut rgba_encoded) {
write_frame(&mut rgba_encoded);
}
}
}
let mut rgba_frame = util::frame::video::Video::empty();
let mut rgba_encoded = Packet::empty();
// Major change 1: use close() instead of flush() (behind the scenes:
// av_buffersrc_close instead of av_buffersrc_add_frame with NULL frame).
// Need to track input pts for this.
// filter.get("in").unwrap().source().flush().unwrap();
filter.get("in").unwrap().source().close(input_pts).unwrap();
while filter
.get("out")
.unwrap()
.sink()
.frame(&mut rgba_frame)
.is_ok()
{
if let Ok(true) = encoder.encode(&rgba_frame, &mut rgba_encoded) {
write_frame(&mut rgba_encoded);
}
}
// Major change 2: flush encoder.
if let Ok(true) = encoder.flush(&mut rgba_encoded) {
write_frame(&mut rgba_encoded);
}
// Major change 3: write trailer.
output_context.write_trailer().unwrap();
println!("Total input frames: {}", input_frame_count);
println!("Total output frames: {}", output_frame_count);
println!("Total duration: {:.2}s", output_pts as f64 / 100.0);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment