#![allow(unsafe_code)]
#![allow(unsafe_op_in_unsafe_fn)]
use std::ffi::CString;
use std::path::Path;
use std::ptr;
use std::time::Duration;
use ff_sys::{
AVCodecID_AV_CODEC_ID_GIF, AVCodecID_AV_CODEC_ID_PNG, AVPixelFormat_AV_PIX_FMT_RGB24,
AVRational, av_buffersink_get_frame, av_frame_alloc, av_frame_free, av_frame_get_buffer,
av_interleaved_write_frame, av_opt_set, av_packet_alloc, av_packet_free, av_packet_unref,
av_write_trailer, avcodec, avfilter_get_by_name, avfilter_graph_alloc, avfilter_graph_config,
avfilter_graph_create_filter, avfilter_graph_free, avfilter_link, avformat,
avformat_alloc_output_context2, avformat_free_context, avformat_new_stream,
avformat_write_header, swscale,
};
use crate::EncodeError;
unsafe fn probe_video_duration_secs(path: &Path) -> Result<f64, EncodeError> {
let fmt_ctx = avformat::open_input(path).map_err(EncodeError::from_ffmpeg_error)?;
if let Err(e) = avformat::find_stream_info(fmt_ctx) {
let mut p = fmt_ctx;
avformat::close_input(&mut p);
return Err(EncodeError::from_ffmpeg_error(e));
}
let duration_av = (*fmt_ctx).duration;
let mut p = fmt_ctx;
avformat::close_input(&mut p);
if duration_av <= 0 {
return Err(EncodeError::MediaOperationFailed {
reason: "cannot determine video duration".to_string(),
});
}
#[allow(clippy::cast_precision_loss)]
let secs = duration_av as f64 / 1_000_000.0;
Ok(secs)
}
pub(super) fn generate_sprite_sheet(
input: &Path,
cols: u32,
rows: u32,
frame_width: u32,
frame_height: u32,
output: &Path,
) -> Result<(), EncodeError> {
unsafe { generate_sprite_sheet_unsafe(input, cols, rows, frame_width, frame_height, output) }
}
unsafe fn generate_sprite_sheet_unsafe(
input: &Path,
cols: u32,
rows: u32,
frame_width: u32,
frame_height: u32,
output: &Path,
) -> Result<(), EncodeError> {
let duration_secs = probe_video_duration_secs(input)?;
let n = cols * rows;
let fps_arg = format!("{n}/{duration_secs:.6}");
macro_rules! bail {
($graph:expr, $reason:expr) => {{
let mut g = $graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::MediaOperationFailed {
reason: format!("{}", $reason),
});
}};
}
let path_str = input
.to_string_lossy()
.replace('\\', "/")
.replace(':', "\\:");
let movie_args = CString::new(format!("filename={path_str}")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "input path contains null byte".to_string(),
}
})?;
let fps_cstr =
CString::new(fps_arg.as_str()).map_err(|_| EncodeError::MediaOperationFailed {
reason: "fps arg contains null byte".to_string(),
})?;
let scale_args = CString::new(format!("{frame_width}:{frame_height}")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "scale args contain null byte".to_string(),
}
})?;
let tile_args = CString::new(format!("{cols}x{rows}:padding=0:margin=0")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "tile args contain null byte".to_string(),
}
})?;
let graph = avfilter_graph_alloc();
if graph.is_null() {
return Err(EncodeError::MediaOperationFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let movie_filt = avfilter_get_by_name(c"movie".as_ptr());
if movie_filt.is_null() {
bail!(graph, "filter not found: movie");
}
let mut movie_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut movie_ctx,
movie_filt,
c"sprite_movie".as_ptr(),
movie_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("movie create_filter failed code={ret}"));
}
let fps_filt = avfilter_get_by_name(c"fps".as_ptr());
if fps_filt.is_null() {
bail!(graph, "filter not found: fps");
}
let mut fps_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut fps_ctx,
fps_filt,
c"sprite_fps".as_ptr(),
fps_cstr.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("fps create_filter failed code={ret}"));
}
let scale_filt = avfilter_get_by_name(c"scale".as_ptr());
if scale_filt.is_null() {
bail!(graph, "filter not found: scale");
}
let mut scale_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut scale_ctx,
scale_filt,
c"sprite_scale".as_ptr(),
scale_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("scale create_filter failed code={ret}"));
}
let tile_filt = avfilter_get_by_name(c"tile".as_ptr());
if tile_filt.is_null() {
bail!(graph, "filter not found: tile");
}
let mut tile_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut tile_ctx,
tile_filt,
c"sprite_tile".as_ptr(),
tile_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("tile create_filter failed code={ret}"));
}
let buffersink_filt = avfilter_get_by_name(c"buffersink".as_ptr());
if buffersink_filt.is_null() {
bail!(graph, "filter not found: buffersink");
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut sink_ctx,
buffersink_filt,
c"sprite_sink".as_ptr(),
ptr::null_mut(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("buffersink create_filter failed code={ret}"));
}
let ret = avfilter_link(movie_ctx, 0, fps_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link movie→fps failed code={ret}"));
}
let ret = avfilter_link(fps_ctx, 0, scale_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link fps→scale failed code={ret}"));
}
let ret = avfilter_link(scale_ctx, 0, tile_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link scale→tile failed code={ret}"));
}
let ret = avfilter_link(tile_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link tile→buffersink failed code={ret}")
);
}
let ret = avfilter_graph_config(graph, ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let tile_frame = av_frame_alloc();
if tile_frame.is_null() {
bail!(graph, "av_frame_alloc failed for tile frame");
}
let ret = av_buffersink_get_frame(sink_ctx, tile_frame);
let got_frame = ret >= 0;
if !got_frame {
let mut f = tile_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
bail!(graph, "tile filter produced no output frame");
}
let encode_result =
encode_frame_as_png(tile_frame, output, cols, rows, frame_width, frame_height);
let mut f = tile_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
encode_result?;
log::info!(
"sprite sheet generated cols={cols} rows={rows} output={}",
output.display()
);
Ok(())
}
unsafe fn encode_frame_as_png(
frame: *mut ff_sys::AVFrame,
output: &Path,
cols: u32,
rows: u32,
frame_width: u32,
frame_height: u32,
) -> Result<(), EncodeError> {
let _ = (cols, rows, frame_width, frame_height);
let width = (*frame).width;
let height = (*frame).height;
let src_pix_fmt = (*frame).format;
let converted_frame: *mut ff_sys::AVFrame;
let needs_conversion = src_pix_fmt != AVPixelFormat_AV_PIX_FMT_RGB24;
if needs_conversion {
let cf = av_frame_alloc();
if cf.is_null() {
return Err(EncodeError::Ffmpeg {
code: 0,
message: "av_frame_alloc failed for rgb24 conversion frame".to_string(),
});
}
(*cf).width = width;
(*cf).height = height;
(*cf).format = AVPixelFormat_AV_PIX_FMT_RGB24;
let ret = av_frame_get_buffer(cf, 0);
if ret < 0 {
let mut f = cf;
av_frame_free(std::ptr::addr_of_mut!(f));
return Err(EncodeError::from_ffmpeg_error(ret));
}
let sws_ctx = swscale::get_context(
width,
height,
src_pix_fmt,
width,
height,
AVPixelFormat_AV_PIX_FMT_RGB24,
swscale::scale_flags::BILINEAR,
)
.map_err(|e| {
let mut f = cf;
av_frame_free(std::ptr::addr_of_mut!(f));
EncodeError::from_ffmpeg_error(e)
})?;
let scale_ret = swscale::scale(
sws_ctx,
(*frame).data.as_ptr().cast::<*const u8>(),
(*frame).linesize.as_ptr(),
0,
height,
(*cf).data.as_mut_ptr().cast_const(),
(*cf).linesize.as_mut_ptr(),
);
swscale::free_context(sws_ctx);
if let Err(e) = scale_ret {
let mut f = cf;
av_frame_free(std::ptr::addr_of_mut!(f));
return Err(EncodeError::from_ffmpeg_error(e));
}
converted_frame = cf;
} else {
converted_frame = frame;
}
let encode_result = encode_frame_as_png_inner(converted_frame, output, width, height);
if needs_conversion {
let mut f = converted_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
}
encode_result
}
unsafe fn encode_frame_as_png_inner(
frame: *mut ff_sys::AVFrame,
output: &Path,
width: i32,
height: i32,
) -> Result<(), EncodeError> {
let pix_fmt = AVPixelFormat_AV_PIX_FMT_RGB24;
let mut fmt_ctx: *mut ff_sys::AVFormatContext = ptr::null_mut();
let c_path = CString::new(
output
.to_str()
.ok_or_else(|| EncodeError::CannotCreateFile {
path: output.to_path_buf(),
})?,
)
.map_err(|_| EncodeError::CannotCreateFile {
path: output.to_path_buf(),
})?;
let ret = avformat_alloc_output_context2(
&mut fmt_ctx,
ptr::null_mut(),
c"image2".as_ptr(),
c_path.as_ptr(),
);
if ret < 0 || fmt_ctx.is_null() {
return Err(EncodeError::from_ffmpeg_error(ret));
}
let stream = avformat_new_stream(fmt_ctx, ptr::null());
if stream.is_null() {
avformat_free_context(fmt_ctx);
return Err(EncodeError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed".to_string(),
});
}
let codec = avcodec::find_encoder(AVCodecID_AV_CODEC_ID_PNG).ok_or_else(|| {
EncodeError::UnsupportedCodec {
codec: "png".to_string(),
}
})?;
let codec_ctx = match avcodec::alloc_context3(codec) {
Ok(ctx) => ctx,
Err(e) => {
avformat_free_context(fmt_ctx);
return Err(EncodeError::from_ffmpeg_error(e));
}
};
(*codec_ctx).width = width;
(*codec_ctx).height = height;
(*codec_ctx).time_base = AVRational { num: 1, den: 1 };
(*codec_ctx).pix_fmt = pix_fmt;
if let Err(e) = avcodec::open2(codec_ctx, codec, ptr::null_mut()) {
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
return Err(EncodeError::from_ffmpeg_error(e));
}
let par = (*stream).codecpar;
(*par).codec_id = AVCodecID_AV_CODEC_ID_PNG;
(*par).codec_type = ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO;
(*par).width = width;
(*par).height = height;
(*par).format = pix_fmt;
let io_ctx = match avformat::open_output(output, avformat::avio_flags::WRITE) {
Ok(ctx) => ctx,
Err(e) => {
let mut cc = codec_ctx;
avcodec::free_context(&mut cc);
avformat_free_context(fmt_ctx);
return Err(EncodeError::from_ffmpeg_error(e));
}
};
(*fmt_ctx).pb = io_ctx;
let ret = avformat_write_header(fmt_ctx, ptr::null_mut());
if ret < 0 {
avformat::close_output(&mut (*fmt_ctx).pb);
let mut cc = codec_ctx;
avcodec::free_context(&mut cc);
avformat_free_context(fmt_ctx);
return Err(EncodeError::from_ffmpeg_error(ret));
}
let packet = av_packet_alloc();
if packet.is_null() {
av_write_trailer(fmt_ctx);
avformat::close_output(&mut (*fmt_ctx).pb);
let mut cc = codec_ctx;
avcodec::free_context(&mut cc);
avformat_free_context(fmt_ctx);
return Err(EncodeError::Ffmpeg {
code: 0,
message: "av_packet_alloc failed".to_string(),
});
}
(*frame).pts = 0;
let encode_result = (|| -> Result<(), EncodeError> {
avcodec::send_frame(codec_ctx, frame).map_err(EncodeError::from_ffmpeg_error)?;
drain_packets(codec_ctx, fmt_ctx, packet, false)?;
avcodec::send_frame(codec_ctx, ptr::null()).map_err(EncodeError::from_ffmpeg_error)?;
drain_packets(codec_ctx, fmt_ctx, packet, true)?;
Ok(())
})();
av_write_trailer(fmt_ctx);
avformat::close_output(&mut (*fmt_ctx).pb);
av_packet_free(&mut { packet });
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
encode_result
}
unsafe fn drain_packets(
codec_ctx: *mut ff_sys::AVCodecContext,
fmt_ctx: *mut ff_sys::AVFormatContext,
packet: *mut ff_sys::AVPacket,
until_eof: bool,
) -> Result<(), EncodeError> {
loop {
match avcodec::receive_packet(codec_ctx, packet) {
Ok(()) => {
(*packet).stream_index = 0;
let ret = av_interleaved_write_frame(fmt_ctx, packet);
av_packet_unref(packet);
if ret < 0 {
return Err(EncodeError::from_ffmpeg_error(ret));
}
}
Err(e) if e == ff_sys::error_codes::EOF => break,
Err(e) if !until_eof && e == ff_sys::error_codes::EAGAIN => break,
Err(e) => return Err(EncodeError::from_ffmpeg_error(e)),
}
}
Ok(())
}
pub(super) fn generate_gif_preview(
input: &Path,
start: Duration,
duration: Duration,
fps: f64,
width: u32,
output: &Path,
) -> Result<(), EncodeError> {
unsafe { generate_gif_preview_unsafe(input, start, duration, fps, width, output) }
}
unsafe fn generate_gif_preview_unsafe(
input: &Path,
start: Duration,
duration: Duration,
fps: f64,
width: u32,
output: &Path,
) -> Result<(), EncodeError> {
let start_sec = start.as_secs_f64();
let dur_sec = duration.as_secs_f64();
let palette_path =
std::env::temp_dir().join(format!("ff_gif_palette_{}.png", std::process::id()));
let palette_result =
generate_palette_unsafe(input, start_sec, dur_sec, fps, width, &palette_path);
if let Err(e) = palette_result {
let _ = std::fs::remove_file(&palette_path);
return Err(e);
}
let gif_result =
encode_gif_unsafe(input, start_sec, dur_sec, fps, width, &palette_path, output);
let _ = std::fs::remove_file(&palette_path);
gif_result?;
log::info!(
"gif preview generated start={start:?} duration={duration:?} output={}",
output.display()
);
Ok(())
}
unsafe fn generate_palette_unsafe(
input: &Path,
start_sec: f64,
dur_sec: f64,
fps: f64,
width: u32,
palette_path: &Path,
) -> Result<(), EncodeError> {
macro_rules! bail {
($graph:expr, $reason:expr) => {{
let mut g = $graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::MediaOperationFailed {
reason: format!("{}", $reason),
});
}};
}
let path_str = input
.to_string_lossy()
.replace('\\', "/")
.replace(':', "\\:");
let movie_args = CString::new(format!("filename={path_str}")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "input path contains null byte".to_string(),
}
})?;
let trim_args =
CString::new(format!("start={start_sec:.6}:duration={dur_sec:.6}")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "trim args contain null byte".to_string(),
}
})?;
let fps_cstr =
CString::new(format!("{fps:.4}")).map_err(|_| EncodeError::MediaOperationFailed {
reason: "fps arg contains null byte".to_string(),
})?;
let scale_args = CString::new(format!("{width}:-2:flags=lanczos")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "scale args contain null byte".to_string(),
}
})?;
let graph = avfilter_graph_alloc();
if graph.is_null() {
return Err(EncodeError::MediaOperationFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let movie_filt = avfilter_get_by_name(c"movie".as_ptr());
if movie_filt.is_null() {
bail!(graph, "filter not found: movie");
}
let mut movie_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut movie_ctx,
movie_filt,
c"pal_movie".as_ptr(),
movie_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("movie create_filter failed code={ret}"));
}
let trim_filt = avfilter_get_by_name(c"trim".as_ptr());
if trim_filt.is_null() {
bail!(graph, "filter not found: trim");
}
let mut trim_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut trim_ctx,
trim_filt,
c"pal_trim".as_ptr(),
trim_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("trim create_filter failed code={ret}"));
}
let fps_filt = avfilter_get_by_name(c"fps".as_ptr());
if fps_filt.is_null() {
bail!(graph, "filter not found: fps");
}
let mut fps_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut fps_ctx,
fps_filt,
c"pal_fps".as_ptr(),
fps_cstr.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("fps create_filter failed code={ret}"));
}
let scale_filt = avfilter_get_by_name(c"scale".as_ptr());
if scale_filt.is_null() {
bail!(graph, "filter not found: scale");
}
let mut scale_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut scale_ctx,
scale_filt,
c"pal_scale".as_ptr(),
scale_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("scale create_filter failed code={ret}"));
}
let palettegen_filt = avfilter_get_by_name(c"palettegen".as_ptr());
if palettegen_filt.is_null() {
bail!(graph, "filter not found: palettegen");
}
let mut palettegen_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut palettegen_ctx,
palettegen_filt,
c"pal_palettegen".as_ptr(),
c"stats_mode=diff".as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("palettegen create_filter failed code={ret}"));
}
let sink_filt = avfilter_get_by_name(c"buffersink".as_ptr());
if sink_filt.is_null() {
bail!(graph, "filter not found: buffersink");
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut sink_ctx,
sink_filt,
c"pal_sink".as_ptr(),
ptr::null_mut(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("buffersink create_filter failed code={ret}"));
}
let ret = avfilter_link(movie_ctx, 0, trim_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link movie→trim failed code={ret}"));
}
let ret = avfilter_link(trim_ctx, 0, fps_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link trim→fps failed code={ret}"));
}
let ret = avfilter_link(fps_ctx, 0, scale_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link fps→scale failed code={ret}"));
}
let ret = avfilter_link(scale_ctx, 0, palettegen_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link scale→palettegen failed code={ret}")
);
}
let ret = avfilter_link(palettegen_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link palettegen→sink failed code={ret}")
);
}
let ret = avfilter_graph_config(graph, ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let mut palette_frame: *mut ff_sys::AVFrame = ptr::null_mut();
loop {
let candidate = av_frame_alloc();
if candidate.is_null() {
break;
}
let ret = av_buffersink_get_frame(sink_ctx, candidate);
if ret >= 0 {
if !palette_frame.is_null() {
let mut prev = palette_frame;
av_frame_free(std::ptr::addr_of_mut!(prev));
}
palette_frame = candidate;
} else {
let mut c = candidate;
av_frame_free(std::ptr::addr_of_mut!(c));
break;
}
}
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
if palette_frame.is_null() {
return Err(EncodeError::MediaOperationFailed {
reason: "palettegen produced no palette frame".to_string(),
});
}
let encode_result = encode_frame_as_png(palette_frame, palette_path, 0, 0, 0, 0);
let mut f = palette_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
encode_result
}
unsafe fn encode_gif_unsafe(
input: &Path,
start_sec: f64,
dur_sec: f64,
fps: f64,
width: u32,
palette_path: &Path,
output: &Path,
) -> Result<(), EncodeError> {
macro_rules! bail {
($graph:expr, $reason:expr) => {{
let mut g = $graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::MediaOperationFailed {
reason: format!("{}", $reason),
});
}};
}
let path_str = input
.to_string_lossy()
.replace('\\', "/")
.replace(':', "\\:");
let movie_vid_args = CString::new(format!("filename={path_str}")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "input path contains null byte".to_string(),
}
})?;
let pal_str = palette_path
.to_string_lossy()
.replace('\\', "/")
.replace(':', "\\:");
let movie_pal_args = CString::new(format!("filename={pal_str}")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "palette path contains null byte".to_string(),
}
})?;
let trim_args =
CString::new(format!("start={start_sec:.6}:duration={dur_sec:.6}")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "trim args contain null byte".to_string(),
}
})?;
let fps_cstr =
CString::new(format!("{fps:.4}")).map_err(|_| EncodeError::MediaOperationFailed {
reason: "fps arg contains null byte".to_string(),
})?;
let scale_args = CString::new(format!("{width}:-2:flags=lanczos")).map_err(|_| {
EncodeError::MediaOperationFailed {
reason: "scale args contain null byte".to_string(),
}
})?;
let graph = avfilter_graph_alloc();
if graph.is_null() {
return Err(EncodeError::MediaOperationFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let movie_filt = avfilter_get_by_name(c"movie".as_ptr());
if movie_filt.is_null() {
bail!(graph, "filter not found: movie");
}
let mut movie_vid_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut movie_vid_ctx,
movie_filt,
c"gif_movie_vid".as_ptr(),
movie_vid_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("movie_vid create_filter failed code={ret}"));
}
let mut movie_pal_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut movie_pal_ctx,
movie_filt,
c"gif_movie_pal".as_ptr(),
movie_pal_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("movie_pal create_filter failed code={ret}"));
}
let trim_filt = avfilter_get_by_name(c"trim".as_ptr());
if trim_filt.is_null() {
bail!(graph, "filter not found: trim");
}
let mut trim_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut trim_ctx,
trim_filt,
c"gif_trim".as_ptr(),
trim_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("trim create_filter failed code={ret}"));
}
let fps_filt = avfilter_get_by_name(c"fps".as_ptr());
if fps_filt.is_null() {
bail!(graph, "filter not found: fps");
}
let mut fps_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut fps_ctx,
fps_filt,
c"gif_fps".as_ptr(),
fps_cstr.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("fps create_filter failed code={ret}"));
}
let scale_filt = avfilter_get_by_name(c"scale".as_ptr());
if scale_filt.is_null() {
bail!(graph, "filter not found: scale");
}
let mut scale_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut scale_ctx,
scale_filt,
c"gif_scale".as_ptr(),
scale_args.as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("scale create_filter failed code={ret}"));
}
let paletteuse_filt = avfilter_get_by_name(c"paletteuse".as_ptr());
if paletteuse_filt.is_null() {
bail!(graph, "filter not found: paletteuse");
}
let mut paletteuse_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut paletteuse_ctx,
paletteuse_filt,
c"gif_paletteuse".as_ptr(),
c"dither=bayer:bayer_scale=5:diff_mode=rectangle".as_ptr(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("paletteuse create_filter failed code={ret}"));
}
let sink_filt = avfilter_get_by_name(c"buffersink".as_ptr());
if sink_filt.is_null() {
bail!(graph, "filter not found: buffersink");
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = ptr::null_mut();
let ret = avfilter_graph_create_filter(
&raw mut sink_ctx,
sink_filt,
c"gif_sink".as_ptr(),
ptr::null_mut(),
ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("buffersink create_filter failed code={ret}"));
}
let ret = avfilter_link(movie_vid_ctx, 0, trim_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link movie_vid→trim failed code={ret}")
);
}
let ret = avfilter_link(trim_ctx, 0, fps_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link trim→fps failed code={ret}"));
}
let ret = avfilter_link(fps_ctx, 0, scale_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link fps→scale failed code={ret}"));
}
let ret = avfilter_link(scale_ctx, 0, paletteuse_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link scale→paletteuse[0] failed code={ret}")
);
}
let ret = avfilter_link(movie_pal_ctx, 0, paletteuse_ctx, 1);
if ret < 0 {
bail!(
graph,
format!("avfilter_link movie_pal→paletteuse[1] failed code={ret}")
);
}
let ret = avfilter_link(paletteuse_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link paletteuse→sink failed code={ret}")
);
}
let ret = avfilter_graph_config(graph, ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let mut fmt_ctx: *mut ff_sys::AVFormatContext = ptr::null_mut();
let c_path = CString::new(
output
.to_str()
.ok_or_else(|| EncodeError::CannotCreateFile {
path: output.to_path_buf(),
})?,
)
.map_err(|_| EncodeError::CannotCreateFile {
path: output.to_path_buf(),
})?;
let ret = avformat_alloc_output_context2(
&mut fmt_ctx,
ptr::null_mut(),
c"gif".as_ptr(),
c_path.as_ptr(),
);
if ret < 0 || fmt_ctx.is_null() {
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::from_ffmpeg_error(ret));
}
let stream = avformat_new_stream(fmt_ctx, ptr::null());
if stream.is_null() {
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed".to_string(),
});
}
let codec = avcodec::find_encoder(AVCodecID_AV_CODEC_ID_GIF).ok_or_else(|| {
EncodeError::UnsupportedCodec {
codec: "gif".to_string(),
}
})?;
let codec_ctx = match avcodec::alloc_context3(codec) {
Ok(ctx) => ctx,
Err(e) => {
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::from_ffmpeg_error(e));
}
};
let first_frame = av_frame_alloc();
if first_frame.is_null() {
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::Ffmpeg {
code: 0,
message: "av_frame_alloc failed".to_string(),
});
}
let ret = av_buffersink_get_frame(sink_ctx, first_frame);
if ret < 0 {
let mut f = first_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::MediaOperationFailed {
reason: format!("no frames from GIF filter graph code={ret}"),
});
}
let out_width = (*first_frame).width;
let out_height = (*first_frame).height;
let out_pix_fmt = (*first_frame).format;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let fps_int = fps.round().max(1.0) as u32;
(*codec_ctx).width = out_width;
(*codec_ctx).height = out_height;
(*codec_ctx).time_base = AVRational {
num: 1,
den: fps_int as i32,
};
(*codec_ctx).pix_fmt = out_pix_fmt;
let _ = av_opt_set(
(*codec_ctx).priv_data.cast(),
c"loop".as_ptr(),
c"0".as_ptr(),
0,
);
if let Err(e) = avcodec::open2(codec_ctx, codec, ptr::null_mut()) {
let mut f = first_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::from_ffmpeg_error(e));
}
let par = (*stream).codecpar;
(*par).codec_id = AVCodecID_AV_CODEC_ID_GIF;
(*par).codec_type = ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO;
(*par).width = out_width;
(*par).height = out_height;
(*par).format = out_pix_fmt;
let io_ctx = match avformat::open_output(output, avformat::avio_flags::WRITE) {
Ok(ctx) => ctx,
Err(e) => {
let mut f = first_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::from_ffmpeg_error(e));
}
};
(*fmt_ctx).pb = io_ctx;
let ret = avformat_write_header(fmt_ctx, ptr::null_mut());
if ret < 0 {
avformat::close_output(&mut (*fmt_ctx).pb);
let mut f = first_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::from_ffmpeg_error(ret));
}
let packet = av_packet_alloc();
if packet.is_null() {
av_write_trailer(fmt_ctx);
avformat::close_output(&mut (*fmt_ctx).pb);
let mut f = first_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(EncodeError::Ffmpeg {
code: 0,
message: "av_packet_alloc failed".to_string(),
});
}
let encode_result = (|| -> Result<(), EncodeError> {
let mut frame_counter: i64 = 0;
(*first_frame).pts = frame_counter;
frame_counter += 1;
avcodec::send_frame(codec_ctx, first_frame).map_err(EncodeError::from_ffmpeg_error)?;
drain_packets(codec_ctx, fmt_ctx, packet, false)?;
loop {
let frame = av_frame_alloc();
if frame.is_null() {
break;
}
let ret = av_buffersink_get_frame(sink_ctx, frame);
if ret < 0 {
let mut f = frame;
av_frame_free(std::ptr::addr_of_mut!(f));
break;
}
(*frame).pts = frame_counter;
frame_counter += 1;
let send_result =
avcodec::send_frame(codec_ctx, frame).map_err(EncodeError::from_ffmpeg_error);
let mut f = frame;
av_frame_free(std::ptr::addr_of_mut!(f));
send_result?;
drain_packets(codec_ctx, fmt_ctx, packet, false)?;
}
avcodec::send_frame(codec_ctx, ptr::null()).map_err(EncodeError::from_ffmpeg_error)?;
drain_packets(codec_ctx, fmt_ctx, packet, true)?;
Ok(())
})();
av_write_trailer(fmt_ctx);
avformat::close_output(&mut (*fmt_ctx).pb);
av_packet_free(&mut { packet });
let mut f = first_frame;
av_frame_free(std::ptr::addr_of_mut!(f));
avcodec::free_context(&mut { codec_ctx });
avformat_free_context(fmt_ctx);
let mut g = graph;
avfilter_graph_free(std::ptr::addr_of_mut!(g));
encode_result
}