use std::collections::HashMap;
use std::path::Path;
use image::{DynamicImage, ImageBuffer, Rgb};
use thiserror::Error;
use crate::frame_valid::is_effectively_blank;
use super::{ffmpeg_log, ffmpeg_probe};
#[derive(Error, Debug)]
pub enum VideoFrameError {
#[error("无法打开视频: {0}")]
OpenFailed(String),
#[error("StreamNotFound")]
NoVideoStream,
#[error("视频尺寸无效")]
InvalidSize,
#[error("packet budget exhausted")]
PacketBudgetExhausted,
#[error("consecutive decode failures")]
ConsecutiveDecodeFailures,
#[error("no frame decoded")]
NoFrame,
}
const MAX_VIDEO_PACKETS: u32 = 48;
const MAX_VIDEO_PACKETS_AFTER_SEEK: u32 = 64;
const MAX_DECODE_FAILS: u32 = 8;
const MAX_BLANK_FRAMES: u32 = 8;
#[cfg(feature = "video")]
pub fn decode_first_frame(path: &Path) -> Option<DynamicImage> {
ffmpeg_log::init_ffmpeg_logging();
if is_swf(path) {
return decode_swf_first_frame(path);
}
decode_first_frame_with_probe(path, ffmpeg_probe::probe_options_for_path(path))
}
#[cfg(feature = "video")]
pub fn decode_first_frame_with_probe(
path: &Path,
probe: HashMap<String, String>,
) -> Option<DynamicImage> {
ffmpeg_log::init_ffmpeg_logging();
use video_rs::decode::DecoderBuilder;
use video_rs::Options;
let opts: Options = probe.into();
let mut decoder = DecoderBuilder::new(path)
.with_options(&opts)
.build()
.ok()?;
let (w, h) = decoder.size();
if w == 0 || h == 0 {
return None;
}
let frame = decoder.decode().ok()?.1;
frame_to_rgb8(w, h, frame.as_slice()?)
}
#[cfg(feature = "video")]
pub fn decode_video_first_frame(
path: &Path,
seek_secs: f64,
probe: HashMap<String, String>,
) -> Result<DynamicImage, VideoFrameError> {
ffmpeg_log::init_ffmpeg_logging();
decode_video_first_frame_inner(path, seek_secs, Some(probe))
}
#[cfg(feature = "video")]
pub fn probe_duration_secs(path: &Path, probe: HashMap<String, String>) -> Option<f64> {
use ffmpeg_next as ffmpeg;
use ffmpeg::format::input_with_dictionary;
use ffmpeg::Dictionary;
ffmpeg_log::init_ffmpeg_logging();
ffmpeg::init().ok()?;
let dict: Dictionary = probe.into_iter().collect();
let ictx = input_with_dictionary(path, dict).ok()?;
let duration = ictx.duration();
if duration <= 0 {
return None;
}
Some(duration as f64 / ffmpeg::ffi::AV_TIME_BASE as f64)
}
#[cfg(feature = "video")]
pub fn probe_video_meta(
path: &Path,
probe: HashMap<String, String>,
) -> (Option<(u32, u32)>, Option<f64>) {
use ffmpeg::Dictionary;
use ffmpeg::format::input_with_dictionary;
use ffmpeg::media::Type;
use ffmpeg_next as ffmpeg;
ffmpeg_log::init_ffmpeg_logging();
if ffmpeg::init().is_err() {
return (None, None);
}
let dict: Dictionary = probe.into_iter().collect();
let Ok(ictx) = input_with_dictionary(path, dict) else {
return (None, None);
};
let duration_secs = {
let duration = ictx.duration();
(duration > 0).then(|| duration as f64 / ffmpeg::ffi::AV_TIME_BASE as f64)
};
let dimensions = ictx
.streams()
.best(Type::Video)
.and_then(|stream| {
ffmpeg::codec::context::Context::from_parameters(stream.parameters())
.ok()?
.decoder()
.video()
.ok()
.map(|decoder| (decoder.width(), decoder.height()))
})
.filter(|(w, h)| *w > 0 && *h > 0);
(dimensions, duration_secs)
}
#[cfg(feature = "video")]
pub fn decode_swf_first_frame(path: &Path) -> Option<DynamicImage> {
decode_video_first_frame(path, 0.0, HashMap::new()).ok()
}
#[cfg(feature = "video")]
pub fn decode_video_sample_frames(path: &Path, max_frames: usize) -> Vec<DynamicImage> {
ffmpeg_log::init_ffmpeg_logging();
let probe = ffmpeg_probe::probe_options_for_path(path);
let duration = probe_duration_secs(path, probe.clone()).unwrap_or(0.0);
let count = max_frames.clamp(1, 5);
let seek_points: Vec<f64> = if duration <= 0.5 {
vec![0.0]
} else {
(0..count)
.map(|i| duration * i as f64 / count as f64)
.collect()
};
let mut frames = Vec::new();
for secs in seek_points {
if let Ok(frame) = decode_video_first_frame(path, secs, probe.clone()) {
frames.push(frame);
}
}
if frames.is_empty() {
if let Ok(frame) = decode_video_first_frame(path, 0.0, probe) {
frames.push(frame);
}
}
frames
}
#[cfg(feature = "video")]
fn decode_video_first_frame_inner(
path: &Path,
seek_secs: f64,
probe: Option<HashMap<String, String>>,
) -> Result<DynamicImage, VideoFrameError> {
use ffmpeg_next as ffmpeg;
use ffmpeg::format::input_with_dictionary;
use ffmpeg::media::Type;
use ffmpeg::software::scaling::{context::Context, flag::Flags};
use ffmpeg::util::frame::video::Video;
use ffmpeg::Dictionary;
ffmpeg::init().map_err(|e| VideoFrameError::OpenFailed(e.to_string()))?;
let dict: Dictionary = probe
.unwrap_or_default()
.into_iter()
.collect();
let mut ictx = input_with_dictionary(path, dict)
.map_err(|e| VideoFrameError::OpenFailed(format!("{e:?}")))?;
let input_stream = ictx
.streams()
.best(Type::Video)
.ok_or(VideoFrameError::NoVideoStream)?;
let stream_index = input_stream.index();
let context = ffmpeg::codec::context::Context::from_parameters(input_stream.parameters())
.map_err(|e| VideoFrameError::OpenFailed(e.to_string()))?;
let mut decoder = context
.decoder()
.video()
.map_err(|e| VideoFrameError::OpenFailed(e.to_string()))?;
let (dec_w, dec_h) = (decoder.width(), decoder.height());
if dec_w == 0 || dec_h == 0 {
return Err(VideoFrameError::InvalidSize);
}
if seek_secs > 0.0 {
let ts = (seek_secs * ffmpeg::ffi::AV_TIME_BASE as f64).round() as i64;
let flags = ffmpeg::ffi::AVSEEK_FLAG_BACKWARD;
let ret = unsafe {
ffmpeg::ffi::avformat_seek_file(
ictx.as_mut_ptr(),
-1,
i64::MIN,
ts,
ts,
flags,
)
};
if ret < 0 {
log::debug!("seek 失败 seek_secs={seek_secs} path={path:?}");
}
decoder.flush();
}
let max_packets = if seek_secs > 0.0 {
MAX_VIDEO_PACKETS_AFTER_SEEK
} else {
MAX_VIDEO_PACKETS
};
let mut scaler = Context::get(
decoder.format(),
dec_w,
dec_h,
ffmpeg::format::pixel::Pixel::RGB24,
dec_w,
dec_h,
Flags::BILINEAR,
)
.map_err(|e| VideoFrameError::OpenFailed(e.to_string()))?;
let mut decoded = Video::empty();
let mut rgb_frame = Video::empty();
let mut video_packets = 0u32;
let mut consecutive_fails = 0u32;
let mut blank_frames = 0u32;
let mut blank_fallback: Option<DynamicImage> = None;
for (stream, packet) in ictx.packets() {
if stream.index() != stream_index {
continue;
}
video_packets += 1;
if video_packets > max_packets {
return Err(VideoFrameError::PacketBudgetExhausted);
}
if decoder.send_packet(&packet).is_err() {
consecutive_fails += 1;
if consecutive_fails >= MAX_DECODE_FAILS {
return Err(VideoFrameError::ConsecutiveDecodeFailures);
}
continue;
}
let mut got_frame = false;
while decoder.receive_frame(&mut decoded).is_ok() {
got_frame = true;
consecutive_fails = 0;
scaler
.run(&decoded, &mut rgb_frame)
.map_err(|e| VideoFrameError::OpenFailed(e.to_string()))?;
if let Some(img) = rgb_video_to_dynamic(&rgb_frame) {
if is_effectively_blank(&img) {
blank_fallback = Some(img);
blank_frames += 1;
if blank_frames < MAX_BLANK_FRAMES {
continue;
}
return Ok(blank_fallback.take().expect("blank fallback"));
}
return Ok(img);
}
}
if !got_frame {
consecutive_fails += 1;
if consecutive_fails >= MAX_DECODE_FAILS {
return Err(VideoFrameError::ConsecutiveDecodeFailures);
}
}
}
if let Some(img) = blank_fallback {
return Ok(img);
}
Err(VideoFrameError::NoFrame)
}
#[cfg(feature = "video")]
fn rgb_video_to_dynamic(frame: &ffmpeg_next::util::frame::video::Video) -> Option<DynamicImage> {
let w = frame.width();
let h = frame.height();
let data = frame.data(0);
let stride = frame.stride(0);
let mut pixels = Vec::with_capacity((w * h * 3) as usize);
for row in 0..h as usize {
let start = row * stride;
pixels.extend_from_slice(&data[start..start + (w as usize) * 3]);
}
ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(w, h, pixels).map(DynamicImage::ImageRgb8)
}
#[cfg(feature = "video")]
fn is_swf(path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("swf"))
}
#[cfg(feature = "video")]
fn frame_to_rgb8(w: u32, h: u32, buf: &[u8]) -> Option<DynamicImage> {
let expected = (w as usize).checked_mul(h as usize)?.checked_mul(3)?;
if buf.len() < expected {
return None;
}
let rgb = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(w, h, buf[..expected].to_vec())?;
Some(DynamicImage::ImageRgb8(rgb))
}
#[cfg(not(feature = "video"))]
pub fn decode_first_frame(_path: &Path) -> Option<DynamicImage> {
None
}
#[cfg(not(feature = "video"))]
pub fn decode_first_frame_with_probe(
_path: &Path,
_probe: HashMap<String, String>,
) -> Option<DynamicImage> {
None
}
#[cfg(not(feature = "video"))]
pub fn probe_duration_secs(_path: &Path, _probe: HashMap<String, String>) -> Option<f64> {
None
}
#[cfg(not(feature = "video"))]
pub fn decode_swf_first_frame(_path: &Path) -> Option<DynamicImage> {
None
}
#[cfg(not(feature = "video"))]
pub fn decode_video_sample_frames(_path: &Path, _max_frames: usize) -> Vec<DynamicImage> {
Vec::new()
}
#[cfg(not(feature = "video"))]
pub fn decode_video_first_frame(
_path: &Path,
_seek_secs: f64,
_probe: HashMap<String, String>,
) -> Result<DynamicImage, VideoFrameError> {
Err(VideoFrameError::NoFrame)
}
#[cfg(feature = "video")]
pub fn decode_media_first_frame_ffmpeg_next(path: &Path) -> Option<DynamicImage> {
decode_swf_first_frame(path)
}
#[cfg(not(feature = "video"))]
pub fn decode_media_first_frame_ffmpeg_next(_path: &Path) -> Option<DynamicImage> {
None
}