use anyhow::Result;
use ffprobe::Stream;
use crate::config::RuntimeConfig;
use crate::parsers::{ParseResult, media_helpers};
use crate::results::VideoMetadata as OutputVideoMetadata;
use crate::utils::ffprobe_handler::{check_ffprobe_available, run_ffprobe_safe};
pub fn extract_video_metadata(
_content_ref: &[u8],
stats_ref: &ParseResult,
_config_ref: &RuntimeConfig,
) -> Result<OutputVideoMetadata> {
check_ffprobe_available()?;
let probe_result = run_ffprobe_safe(&stats_ref.file_path)?;
let (container_format, duration_seconds, bitrate) =
media_helpers::extract_format_metadata(&probe_result);
let video_stream = media_helpers::find_stream_by_type(&probe_result, "video");
let audio_stream = media_helpers::find_stream_by_type(&probe_result, "audio");
let (width, height) = video_stream.map_or((0, 0), |s| {
(
s.width.and_then(|w| usize::try_from(w).ok()).unwrap_or(0),
s.height.and_then(|h| usize::try_from(h).ok()).unwrap_or(0),
)
});
let coded_width =
video_stream.and_then(|s| s.coded_width.and_then(|w| usize::try_from(w).ok()));
let coded_height =
video_stream.and_then(|s| s.coded_height.and_then(|h| usize::try_from(h).ok()));
let has_b_frames = video_stream
.and_then(|s| s.has_b_frames)
.map(|count| count > 0);
let video_codec = video_stream.and_then(|s| s.codec_name.clone());
let video_codec_profile = media_helpers::extract_codec_profile(video_stream);
let video_codec_level = video_stream
.and_then(|s| s.level)
.map(|level| format!("{}.{}", level / 10, level % 10));
let pixel_format = video_stream.and_then(|s| s.pix_fmt.clone());
let bit_depth = extract_bit_depth(video_stream, pixel_format.as_ref());
let color_space = video_stream.and_then(|s| s.color_space.clone());
let chroma_subsampling = pixel_format.as_deref().and_then(extract_chroma_subsampling);
let display_aspect_ratio = video_stream.and_then(|s| s.display_aspect_ratio.clone());
let sample_aspect_ratio = video_stream.and_then(|s| s.sample_aspect_ratio.clone());
let video_language = media_helpers::extract_language(video_stream);
let creation_time = media_helpers::extract_creation_time(video_stream, &probe_result);
let scan_type = video_stream
.and_then(|s| s.field_order.as_deref())
.and_then(extract_scan_type);
let frame_rate = video_stream.and_then(|s| {
parse_frame_rate(&s.r_frame_rate).or_else(|| parse_frame_rate(&s.avg_frame_rate))
});
let frame_count = video_stream
.and_then(|s| s.nb_frames.as_ref())
.and_then(|f| f.parse::<u64>().ok());
let video_bitrate = media_helpers::extract_stream_bitrate(video_stream);
let video_stream_size = media_helpers::calculate_stream_size(video_bitrate, duration_seconds);
let encoded_library = media_helpers::extract_encoded_library(video_stream, &probe_result);
let bitrate_mode = media_helpers::extract_bitrate_mode(
video_codec.as_ref(),
encoded_library.as_ref(),
None, );
let audio_metadata = media_helpers::extract_audio_stream_metadata(audio_stream);
let audio_codec = audio_metadata.codec;
let audio_channels = audio_metadata.channels;
let audio_channel_layout = audio_metadata.channel_layout;
let audio_sample_rate = audio_metadata.sample_rate;
let audio_bitrate = audio_metadata.bitrate;
let audio_stream_size = media_helpers::calculate_stream_size(audio_bitrate, duration_seconds);
let audio_language = media_helpers::extract_language(audio_stream);
let aspect_ratio = (height > 0).then(|| width as f64 / height as f64);
Ok(OutputVideoMetadata {
width,
height,
aspect_ratio,
display_aspect_ratio,
sample_aspect_ratio,
coded_width,
coded_height,
has_b_frames,
video_language,
creation_time,
duration_seconds,
video_codec,
video_codec_profile,
video_codec_level,
pixel_format,
bit_depth,
color_space,
chroma_subsampling,
scan_type,
frame_rate,
frame_count,
bitrate,
video_bitrate,
bitrate_mode,
audio_codec,
audio_bitrate,
audio_channels,
audio_channel_layout,
audio_sample_rate,
audio_language,
container_format,
video_stream_size,
audio_stream_size,
stream_size: Some(stats_ref.byte_count),
encoded_library,
})
}
fn extract_bit_depth(
stream_ref: Option<&Stream>,
pixel_format_ref: Option<&String>,
) -> Option<u32> {
stream_ref
.and_then(|s| s.bits_per_raw_sample.as_ref())
.and_then(|b| b.parse::<u32>().ok())
.or_else(|| {
pixel_format_ref.and_then(|pix_fmt| {
if pix_fmt.contains("10") {
Some(10)
} else if pix_fmt.contains("12") {
Some(12)
} else if pix_fmt.contains("14") {
Some(14)
} else if pix_fmt.contains("16") {
Some(16)
} else if !pix_fmt.contains('8') {
Some(8)
} else {
None
}
})
})
}
fn extract_chroma_subsampling(pix_fmt_ref: &str) -> Option<String> {
if pix_fmt_ref.contains("420") {
Some("4:2:0".to_string())
} else if pix_fmt_ref.contains("422") {
Some("4:2:2".to_string())
} else if pix_fmt_ref.contains("444") {
Some("4:4:4".to_string())
} else if pix_fmt_ref.contains("411") {
Some("4:1:1".to_string())
} else {
None
}
}
fn extract_scan_type(field_order_ref: &str) -> Option<String> {
match field_order_ref {
"progressive" => Some("progressive".to_string()),
"tt" | "tb" => Some("interlaced (top field first)".to_string()),
"bb" | "bt" => Some("interlaced (bottom field first)".to_string()),
_ => None,
}
}
fn parse_frame_rate(fr_str_ref: &str) -> Option<f64> {
if fr_str_ref.is_empty() {
return None;
}
if fr_str_ref.contains('/') {
let parts: Vec<&str> = fr_str_ref.split('/').collect();
if parts.len() == 2
&& let (Ok(num), Ok(den)) = (parts[0].parse::<f64>(), parts[1].parse::<f64>())
&& den != 0.0
{
return Some(num / den);
}
None
} else {
fr_str_ref.parse::<f64>().ok()
}
}
crate::no_template_mining!(
extract_video_templates,
"Videos don't have templates, return empty result."
);