diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..5b097ce --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,11 @@ +[target.aarch64-apple-darwin] +rustflags = [ + "-L", "/opt/homebrew/lib", + "-l", "c++" +] + +[target.x86_64-apple-darwin] +rustflags = [ + "-L", "/usr/local/lib", + "-l", "c++" +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11dd245 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +*.rs.bk +.DS_Store +test.mp4 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2de256d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,279 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ffmpeg-next" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d658424d233cbd993a972dd73a66ca733acd12a494c68995c9ac32ae1fe65b40" +dependencies = [ + "bitflags", + "ffmpeg-sys-next", + "libc", +] + +[[package]] +name = "ffmpeg-sys-next" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bca20aa4ee774fe384c2490096c122b0b23cf524a9910add0686691003d797b" +dependencies = [ + "bindgen", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "video_analyzer" +version = "0.1.0" +dependencies = [ + "ffmpeg-next", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2510f3d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "video_analyzer" +version = "0.1.0" +edition = "2024" + +[dependencies] +# 啟用 "static" feature 進行靜態編譯 +# 注意:版本號可能需要根據實際情況調整,目前推薦使用 7.0 或最新版本 +ffmpeg-next = { version = "8.0", features = ["static"] } + +# [可選] 如果你希望進一步減小體積或指定編譯選項,可以添加 build-dependencies +# 但通常 ffmpeg-next 的 static feature 會自動處理大部分事情 diff --git a/README.md b/README.md index e69de29..5136783 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,14 @@ +# Video Analyzer + +A Rust-based tool to analyze video file metadata using FFmpeg. + +## Features +- Extract video format, resolution, duration +- Analyze audio streams (sample rate, channels) +- Static/Dynamic linking support + +## Usage +```bash +cargo build --release +./target/release/video_analyzer +``` diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f7b90c5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,135 @@ +use ffmpeg_next as ffmpeg; +use ffmpeg::format::{input, Pixel}; +use ffmpeg::media::Type; +use ffmpeg::codec::{context::Context, decoder}; +use std::path::Path; + +fn main() -> Result<(), Box> { + ffmpeg::init()?; + + let input_path = "test.mp4"; + + if !Path::new(input_path).exists() { + eprintln!("錯誤:找不到檔案 '{}'", input_path); + eprintln!("請將一個視頻文件重命名為 'test.mp4' 並放在專案根目錄。"); + return Ok(()); + } + + let ictx = input(&input_path)?; + + println!("=== 檔案基本資訊 ==="); + println!("格式名稱 (Format): {}", ictx.format().name()); + println!("長描述: {}", ictx.format().description()); + + let duration_sec = ictx.duration() as f64 / ffmpeg::ffi::AV_TIME_BASE as f64; + println!("總長度 (Duration): {:.2} 秒", duration_sec); + + println!("\n=== 串流資訊 ==="); + + for (index, stream) in ictx.streams().enumerate() { + let codec_params = stream.parameters(); + let codec_id = codec_params.id(); + let media_type = codec_params.medium(); + + println!("[串流 #{}]", index); + println!(" 類型: {:?}", media_type); + println!(" 編碼器 ID: {:?}", codec_id); + + if let Some(codec_descriptor) = decoder::find(codec_id) { + let mut context = Context::new_with_codec(codec_descriptor); + + if let Err(e) = context.set_parameters(codec_params) { + eprintln!(" 警告:無法設置參數: {}", e); + continue; + } + + unsafe { + let ptr = context.as_mut_ptr(); + + match media_type { + Type::Video => { + let width = (*ptr).width; + let height = (*ptr).height; + let pix_fmt_val = (*ptr).pix_fmt; + let format = Pixel::from(pix_fmt_val); + + let frame_rate = stream.avg_frame_rate(); + let fps = if frame_rate.numerator() != 0 { + frame_rate.numerator() as f64 / frame_rate.denominator() as f64 + } else { + 0.0 + }; + + println!(" 解析度: {}x{}", width, height); + println!(" 像素格式: {:?}", format); + println!(" 幀率: {:.2} fps", fps); + + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + }, + Type::Audio => { + let sample_rate = (*ptr).sample_rate; + + // 【關鍵修復】新版本使用 ch_layout.nb_channels 獲取聲道數 + // ch_layout 是一個 AVChannelLayout 結構體 + let channels = (*ptr).ch_layout.nb_channels; + + let sample_fmt_val = (*ptr).sample_fmt; + let format = ffmpeg::format::Sample::from(sample_fmt_val); + + println!(" 採樣率: {} Hz", sample_rate); + println!(" 聲道數: {}", channels); + println!(" 音訊格式: {:?}", format); + + // 可選:打印聲道佈局描述 (例如 "stereo", "5.1") + // 需要引入 ffi 來調用 av_channel_layout_describe + /* + let mut buf = vec![0u8; 1024]; + let ret = ffmpeg::ffi::av_channel_layout_describe( + &(*ptr).ch_layout, + buf.as_mut_ptr() as *mut i8, + buf.len() as i32 + ); + if ret >= 0 { + if let Ok(layout_str) = std::ffi::CStr::from_bytes_until_nul(&buf) { + println!(" 聲道佈局: {}", layout_str.to_string_lossy()); + } + } + */ + + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + }, + Type::Subtitle => { + println!(" (字幕串流)"); + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + }, + _ => { + println!(" (其他類型串流)"); + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + } + } + } // end unsafe + } else { + println!(" (未找到對應的解碼器)"); + } + + // 輸出 Metadata + for (key, value) in stream.metadata().iter() { + println!(" Metadata [{}]: {}", key, value); + } + } + + println!("\n=== 檔案 Metadata ==="); + for (key, value) in ictx.metadata().iter() { + println!("{}: {}", key, value); + } + + Ok(()) +} diff --git a/src/main.rs.bak b/src/main.rs.bak new file mode 100644 index 0000000..48a8b23 --- /dev/null +++ b/src/main.rs.bak @@ -0,0 +1,135 @@ +use ffmpeg_next as ffmpeg; +use ffmpeg::format::{input, Pixel}; +use ffmpeg::media::Type; +use ffmpeg::codec::{context::Context, decoder}; +use std::path::Path; + +fn main() -> Result<(), Box> { + ffmpeg::init()?; + + let input_path = "test.mp4"; + + if !Path::new(input_path).exists() { + eprintln!("錯誤:找不到檔案 '{}'", input_path); + eprintln!("請將一個視頻文件重命名為 'test.mp4' 並放在專案根目錄。"); + return Ok(()); + } + + let mut ictx = input(&input_path)?; + + println!("=== 檔案基本資訊 ==="); + println!("格式名稱 (Format): {}", ictx.format().name()); + println!("長描述: {}", ictx.format().description()); + + let duration_sec = ictx.duration() as f64 / ffmpeg::ffi::AV_TIME_BASE as f64; + println!("總長度 (Duration): {:.2} 秒", duration_sec); + + println!("\n=== 串流資訊 ==="); + + for (index, stream) in ictx.streams().enumerate() { + let codec_params = stream.parameters(); + let codec_id = codec_params.id(); + let media_type = codec_params.medium(); + + println!("[串流 #{}]", index); + println!(" 類型: {:?}", media_type); + println!(" 編碼器 ID: {:?}", codec_id); + + if let Some(codec_descriptor) = decoder::find(codec_id) { + let mut context = Context::new_with_codec(codec_descriptor); + + if let Err(e) = context.set_parameters(codec_params) { + eprintln!(" 警告:無法設置參數: {}", e); + continue; + } + + unsafe { + let ptr = context.as_mut_ptr(); + + match media_type { + Type::Video => { + let width = (*ptr).width; + let height = (*ptr).height; + let pix_fmt_val = (*ptr).pix_fmt; + let format = Pixel::from(pix_fmt_val); + + let frame_rate = stream.avg_frame_rate(); + let fps = if frame_rate.numerator() != 0 { + frame_rate.numerator() as f64 / frame_rate.denominator() as f64 + } else { + 0.0 + }; + + println!(" 解析度: {}x{}", width, height); + println!(" 像素格式: {:?}", format); + println!(" 幀率: {:.2} fps", fps); + + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + }, + Type::Audio => { + let sample_rate = (*ptr).sample_rate; + + // 【關鍵修復】新版本使用 ch_layout.nb_channels 獲取聲道數 + // ch_layout 是一個 AVChannelLayout 結構體 + let channels = (*ptr).ch_layout.nb_channels; + + let sample_fmt_val = (*ptr).sample_fmt; + let format = ffmpeg::format::Sample::from(sample_fmt_val); + + println!(" 採樣率: {} Hz", sample_rate); + println!(" 聲道數: {}", channels); + println!(" 音訊格式: {:?}", format); + + // 可選:打印聲道佈局描述 (例如 "stereo", "5.1") + // 需要引入 ffi 來調用 av_channel_layout_describe + /* + let mut buf = vec![0u8; 1024]; + let ret = ffmpeg::ffi::av_channel_layout_describe( + &(*ptr).ch_layout, + buf.as_mut_ptr() as *mut i8, + buf.len() as i32 + ); + if ret >= 0 { + if let Ok(layout_str) = std::ffi::CStr::from_bytes_until_nul(&buf) { + println!(" 聲道佈局: {}", layout_str.to_string_lossy()); + } + } + */ + + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + }, + Type::Subtitle => { + println!(" (字幕串流)"); + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + }, + _ => { + println!(" (其他類型串流)"); + if let Some(codec_name) = context.codec() { + println!(" 編碼器名稱: {}", codec_name.name()); + } + } + } + } // end unsafe + } else { + println!(" (未找到對應的解碼器)"); + } + + // 輸出 Metadata + for (key, value) in stream.metadata().iter() { + println!(" Metadata [{}]: {}", key, value); + } + } + + println!("\n=== 檔案 Metadata ==="); + for (key, value) in ictx.metadata().iter() { + println!("{}: {}", key, value); + } + + Ok(()) +}