Created
August 3, 2020 11:41
-
-
Save zmwangx/bacd12df8b8dce4c91db47a34a637004 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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