diff --git a/Cargo.lock b/Cargo.lock index 2d4c4d8e7e53..15b491b6bbe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1627,9 +1627,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1637,7 +1637,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8487aa8f59b0..a1efead54c40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ async-trait = "0.1.77" axum = "=0.6.20" base64 = "0.21.5" blake3 = "1.5.0" -chrono = "0.4.31" +chrono = "0.4.38" clap = "4.4.7" futures = "0.3.30" futures-concurrency = "7.4.3" diff --git a/crates/ffmpeg/src/dict.rs b/crates/ffmpeg/src/dict.rs index 6019d3dc26b4..13736061a170 100644 --- a/crates/ffmpeg/src/dict.rs +++ b/crates/ffmpeg/src/dict.rs @@ -1,38 +1,61 @@ -use crate::{error::Error, utils::check_error}; +use crate::{error::Error, model::MediaMetadata, utils::check_error}; use std::{ ffi::{CStr, CString}, ptr, }; +use chrono::DateTime; use ffmpeg_sys_next::{ - av_dict_free, av_dict_iterate, av_dict_set, AVDictionary, AVDictionaryEntry, + av_dict_free, av_dict_get, av_dict_iterate, av_dict_set, AVDictionary, AVDictionaryEntry, }; #[derive(Debug)] -pub(crate) struct FFmpegDict(*mut AVDictionary); +pub(crate) struct FFmpegDict { + dict: *mut AVDictionary, + managed: bool, +} impl FFmpegDict { pub(crate) fn new(av_dict: Option<*mut AVDictionary>) -> Self { - Self(av_dict.unwrap_or(ptr::null_mut())) + match av_dict { + Some(ptr) => Self { + dict: ptr, + managed: false, + }, + None => Self { + dict: ptr::null_mut(), + managed: true, + }, + } } pub(crate) fn as_mut_ptr(&mut self) -> *mut AVDictionary { - self.0 + self.dict + } + + pub(crate) fn get(&self, key: CString) -> Option { + let entry = unsafe { av_dict_get(self.dict, key.as_ptr(), ptr::null(), 0) }; + if entry.is_null() { + return None; + } + + let cstr = unsafe { CStr::from_ptr((*entry).value) }; + Some(String::from_utf8_lossy(cstr.to_bytes()).to_string()) } pub(crate) fn set(&mut self, key: CString, value: CString) -> Result<(), Error> { check_error( - unsafe { av_dict_set(&mut self.0, key.as_ptr(), value.as_ptr(), 0) }, + unsafe { av_dict_set(&mut self.dict, key.as_ptr(), value.as_ptr(), 0) }, "Fail to set dictionary key-value pair", )?; Ok(()) } - pub(crate) fn reset(&mut self, key: CString) -> Result<(), Error> { + pub(crate) fn remove(&mut self, key: CString) -> Result<(), Error> { check_error( - unsafe { av_dict_set(&mut self.0, key.as_ptr(), ptr::null(), 0) }, + unsafe { av_dict_set(&mut self.dict, key.as_ptr(), ptr::null(), 0) }, "Fail to set dictionary key-value pair", )?; @@ -42,9 +65,9 @@ impl FFmpegDict { impl Drop for FFmpegDict { fn drop(&mut self) { - if !self.0.is_null() { - unsafe { av_dict_free(&mut self.0) }; - self.0 = std::ptr::null_mut(); + if !self.managed && !self.dict.is_null() { + unsafe { av_dict_free(&mut self.dict) }; + self.dict = std::ptr::null_mut(); } } } @@ -56,7 +79,7 @@ impl<'a> IntoIterator for &'a FFmpegDict { #[inline] fn into_iter(self) -> FFmpegDictIter<'a> { FFmpegDictIter { - dict: self.0, + dict: self.dict, prev: std::ptr::null(), _lifetime: std::marker::PhantomData, } @@ -83,8 +106,69 @@ impl<'a> Iterator for FFmpegDictIter<'a> { let key = unsafe { CStr::from_ptr((*self.prev).key) }; let value = unsafe { CStr::from_ptr((*self.prev).value) }; return Some(( - key.to_string_lossy().into_owned(), - value.to_string_lossy().into_owned(), + String::from_utf8_lossy(key.to_bytes()).to_string(), + String::from_utf8_lossy(value.to_bytes()).to_string(), )); } } + +impl From for MediaMetadata { + fn from(val: FFmpegDict) -> Self { + let mut media_metadata = MediaMetadata::default(); + + for (key, value) in val.into_iter() { + match key.as_str() { + "album" => media_metadata.album = Some(value.clone()), + "album_artist" => media_metadata.album_artist = Some(value.clone()), + "artist" => media_metadata.artist = Some(value.clone()), + "comment" => media_metadata.comment = Some(value.clone()), + "composer" => media_metadata.composer = Some(value.clone()), + "copyright" => media_metadata.copyright = Some(value.clone()), + "creation_time" => { + if let Ok(creation_time) = DateTime::parse_from_rfc2822(&value) { + media_metadata.creation_time = Some(creation_time.into()); + } else if let Ok(creation_time) = DateTime::parse_from_rfc3339(&value) { + media_metadata.creation_time = Some(creation_time.into()); + } + } + "date" => { + if let Ok(date) = DateTime::parse_from_rfc2822(&value) { + media_metadata.date = Some(date.into()); + } else if let Ok(date) = DateTime::parse_from_rfc3339(&value) { + media_metadata.date = Some(date.into()); + } + } + "disc" => { + if let Ok(disc) = value.parse() { + media_metadata.disc = Some(disc); + } + } + "encoder" => media_metadata.encoder = Some(value.clone()), + "encoded_by" => media_metadata.encoded_by = Some(value.clone()), + "filename" => media_metadata.filename = Some(value.clone()), + "genre" => media_metadata.genre = Some(value.clone()), + "language" => media_metadata.language = Some(value.clone()), + "performer" => media_metadata.performer = Some(value.clone()), + "publisher" => media_metadata.publisher = Some(value.clone()), + "service_name" => media_metadata.service_name = Some(value.clone()), + "service_provider" => media_metadata.service_provider = Some(value.clone()), + "title" => media_metadata.title = Some(value.clone()), + "track" => { + if let Ok(track) = value.parse() { + media_metadata.track = Some(track); + } + } + "variant_bitrate" => { + if let Ok(variant_bitrate) = value.parse() { + media_metadata.variant_bitrate = Some(variant_bitrate); + } + } + _ => { + media_metadata.custom.insert(key.clone(), value.clone()); + } + } + } + + media_metadata + } +} diff --git a/crates/ffmpeg/src/format_ctx.rs b/crates/ffmpeg/src/format_ctx.rs index 70a60a501000..865ad9118ef8 100644 --- a/crates/ffmpeg/src/format_ctx.rs +++ b/crates/ffmpeg/src/format_ctx.rs @@ -1,7 +1,13 @@ -use crate::{dict::FFmpegDict, error::Error, model::MediaMetadata, utils::check_error}; +use crate::{ + dict::FFmpegDict, + error::Error, + model::{MediaChapter, MediaMetadata, MediaProgram}, + utils::check_error, +}; use ffmpeg_sys_next::{ - avformat_close_input, avformat_find_stream_info, avformat_open_input, AVFormatContext, + av_q2d, avformat_close_input, avformat_find_stream_info, avformat_open_input, AVFormatContext, + AV_NOPTS_VALUE, AV_TIME_BASE, }; use std::{ @@ -9,134 +15,173 @@ use std::{ ptr, }; +use chrono::TimeDelta; + #[derive(Debug)] pub(crate) struct FFmpegFormatContext { data: *mut AVFormatContext, } impl FFmpegFormatContext { - pub(crate) fn formats(&self) -> Option> { + pub(crate) fn open_file(filename: CString, options: &mut FFmpegDict) -> Result { + let mut ctx = Self { + data: ptr::null_mut(), + }; + + check_error( + unsafe { + avformat_open_input( + &mut ctx.data, + filename.as_ptr(), + ptr::null(), + &mut options.as_mut_ptr(), + ) + }, + "Fail to open an input stream and read the header", + )?; + + Ok(ctx) + } + + pub(crate) fn as_mut_ptr(&mut self) -> *mut AVFormatContext { + self.data + } + + pub(crate) fn find_stream_info(&self) -> Result<(), Error> { + check_error( + unsafe { avformat_find_stream_info(self.data, ptr::null_mut()) }, + "Fail to read packets of a media file to get stream information", + )?; + + Ok(()) + } + + pub fn formats(&self) -> Vec { if self.data.is_null() { - return None; + return vec![]; } let format: *const ffmpeg_sys_next::AVInputFormat = unsafe { (*self.data).iformat }; if format.is_null() { - return None; + return vec![]; } let name = unsafe { (*format).name }; if name.is_null() { - return None; + return vec![]; } - let c_str = unsafe { CStr::from_ptr(name) }; - let string = c_str.to_string_lossy().into_owned(); - let entries: Vec = string + let cstr = unsafe { CStr::from_ptr(name) }; + let _string = cstr.to_string_lossy().into_owned(); + let entries: Vec = String::from_utf8_lossy(cstr.to_bytes()) .split(',') .map(|entry| entry.trim().to_string()) .filter(|entry| !entry.is_empty()) .collect(); - Some(entries) + entries } - pub(crate) fn metadata(&self) -> Option { + pub fn duration(&self) -> Option { if self.data.is_null() { return None; } - let metadata_ptr = unsafe { (*self.data).metadata }; - if metadata_ptr.is_null() { + let duration = unsafe { *self.data }.duration; + if duration == AV_NOPTS_VALUE { return None; } - let mut media_metadata = MediaMetadata::default(); - - let metadata = FFmpegDict::new(Some(metadata_ptr)); - for (key, value) in metadata.into_iter() { - match key.as_str() { - "album" => media_metadata.album = Some(value.clone()), - "album_artist" => media_metadata.album_artist = Some(value.clone()), - "artist" => media_metadata.artist = Some(value.clone()), - "comment" => media_metadata.comment = Some(value.clone()), - "composer" => media_metadata.composer = Some(value.clone()), - "copyright" => media_metadata.copyright = Some(value.clone()), - "creation_time" => { - if let Ok(creation_time) = chrono::DateTime::parse_from_rfc3339(&value) { - media_metadata.creation_time = Some(creation_time.into()); - } - } - "date" => { - if let Ok(date) = chrono::DateTime::parse_from_rfc3339(&value) { - media_metadata.date = Some(date.into()); - } - } - "disc" => { - if let Ok(disc) = value.parse() { - media_metadata.disc = Some(disc); - } - } - "encoder" => media_metadata.encoder = Some(value.clone()), - "encoded_by" => media_metadata.encoded_by = Some(value.clone()), - "filename" => media_metadata.filename = Some(value.clone()), - "genre" => media_metadata.genre = Some(value.clone()), - "language" => media_metadata.language = Some(value.clone()), - "performer" => media_metadata.performer = Some(value.clone()), - "publisher" => media_metadata.publisher = Some(value.clone()), - "service_name" => media_metadata.service_name = Some(value.clone()), - "service_provider" => media_metadata.service_provider = Some(value.clone()), - "title" => media_metadata.title = Some(value.clone()), - "track" => { - if let Ok(track) = value.parse() { - media_metadata.track = Some(track); - } - } - "variant_bitrate" => { - if let Ok(variant_bitrate) = value.parse() { - media_metadata.variant_bitrate = Some(variant_bitrate); - } - } - _ => { - media_metadata.custom.insert(key.clone(), value.clone()); - } - } + let ms = (duration % (AV_TIME_BASE as i64)).abs(); + TimeDelta::new(duration / (AV_TIME_BASE as i64), (ms * 1000) as u32) + } + + pub fn start_time(&self) -> Option { + if self.data.is_null() { + return None; } - Some(media_metadata) + let start_time = unsafe { *self.data }.start_time; + if start_time == AV_NOPTS_VALUE { + return None; + } + + let _secs = start_time / (AV_TIME_BASE as i64); + let ms = (start_time % (AV_TIME_BASE as i64)).abs(); + + TimeDelta::new(start_time / (AV_TIME_BASE as i64), (ms * 1000) as u32) } - pub(crate) fn as_mut_ptr(&mut self) -> *mut AVFormatContext { - self.data + pub fn bit_rate(&self) -> Option { + if self.data.is_null() { + return None; + } + + Some(unsafe { *self.data }.bit_rate) } - pub(crate) fn open_file(filename: CString, options: &mut FFmpegDict) -> Result { - let mut ctx = Self { - data: ptr::null_mut(), - }; + pub fn chapters(&self) -> Vec { + if self.data.is_null() { + return vec![]; + } - check_error( - unsafe { - avformat_open_input( - &mut ctx.data, - filename.as_ptr(), - ptr::null(), - &mut options.as_mut_ptr(), - ) - }, - "Fail to open an input stream and read the header", - )?; + let nb_chapters = unsafe { *self.data }.nb_chapters; + if nb_chapters == 0 { + return vec![]; + } - Ok(ctx) + let mut chapters = Vec::new(); + for id in 0..nb_chapters { + let chapter = unsafe { **(*self.data).chapters.offset(id as isize) }; + chapters.push(MediaChapter { + id, + start: chapter.start as f64 * unsafe { av_q2d(chapter.time_base) }, + end: chapter.end as f64 * unsafe { av_q2d(chapter.time_base) }, + metadata: FFmpegDict::new(Some(chapter.metadata)).into(), + }); + } + + chapters } - pub(crate) fn find_stream_info(&self) -> Result<(), Error> { - check_error( - unsafe { avformat_find_stream_info(self.data, ptr::null_mut()) }, - "Fail to read packets of a media file to get stream information", - )?; + pub fn programs(&self) -> Vec { + let mut programs = Vec::new(); + + if !self.data.is_null() && unsafe { (*self.data).nb_programs } > 0 { + for id in 0..unsafe { (*self.data).nb_programs } { + let program = unsafe { **(*self.data).programs.offset(id as isize) }; + let mut metadata = FFmpegDict::new(Some(program.metadata)); + let name = CString::new("name").map_or(None, |key| { + let name = metadata.get(key.to_owned()); + if name.is_some() { + let _ = metadata.remove(key); + } + name + }); + + programs.push(MediaProgram { + id, + name, + streams: Vec::new(), + metadata: metadata.into(), + }) + } + } - Ok(()) + programs + } + + pub fn metadata(&self) -> Option { + if self.data.is_null() { + return None; + } + + let metadata_ptr = unsafe { *self.data }.metadata; + if metadata_ptr.is_null() { + return None; + } + + Some(FFmpegDict::new(Some(metadata_ptr)).into()) } } diff --git a/crates/ffmpeg/src/model.rs b/crates/ffmpeg/src/model.rs index 1e4c21c24db5..54ceac64627f 100644 --- a/crates/ffmpeg/src/model.rs +++ b/crates/ffmpeg/src/model.rs @@ -1,3 +1,4 @@ +use chrono::TimeDelta; use std::collections::HashMap; #[derive(Default)] @@ -10,7 +11,7 @@ pub struct MediaMetadata { pub copyright: Option, pub creation_time: Option>, pub date: Option>, - pub disc: Option, + pub disc: Option, pub encoder: Option, pub encoded_by: Option, pub filename: Option, @@ -21,15 +22,15 @@ pub struct MediaMetadata { pub service_name: Option, pub service_provider: Option, pub title: Option, - pub track: Option, - pub variant_bitrate: Option, + pub track: Option, + pub variant_bitrate: Option, pub custom: HashMap, } pub struct MediaChapter { - pub id: i32, - pub start: Option, - pub end: Option, + pub id: u32, + pub start: f64, + pub end: f64, pub metadata: MediaMetadata, } @@ -93,16 +94,16 @@ pub struct MediaStream { } pub struct MediaProgram { - pub id: i32, + pub id: u32, pub name: Option, pub streams: Vec, pub metadata: MediaMetadata, } pub struct MediaInfo { - pub formats: Option>, - pub duration: Option, - pub start_time: Option, + pub formats: Vec, + pub duration: Option, + pub start_time: Option, pub bitrate: Option, pub chapters: Vec, pub programs: Vec, diff --git a/crates/ffmpeg/src/probe.rs b/crates/ffmpeg/src/probe.rs index 0f9a19838f52..c2f7d9f72841 100644 --- a/crates/ffmpeg/src/probe.rs +++ b/crates/ffmpeg/src/probe.rs @@ -10,7 +10,7 @@ use ffmpeg_sys_next::{av_log_set_level, AV_LOG_FATAL}; use std::{ffi::CString, path::Path}; -pub fn probe(filename: impl AsRef) -> Result<(), Error> { +pub fn probe(filename: impl AsRef) -> Result { let filename = filename.as_ref(); // Reduce the amount of logs generated by FFmpeg @@ -29,20 +29,20 @@ pub fn probe(filename: impl AsRef) -> Result<(), Error> { let fmt_ctx = FFmpegFormatContext::open_file(from_path(filename)?, &mut format_opts)?; // Reset MPEGTS specific option - format_opts.reset(scan_all_pmts)?; + format_opts.remove(scan_all_pmts)?; // Read packets of media file to get stream information. fmt_ctx.find_stream_info()?; let media_info = MediaInfo { formats: fmt_ctx.formats(), - duration: "" , // Option - start_time: "" , // Option - bitrate: "" , // Option - chapters: "" , // Vec - programs: "" , // Vec - metadata: fmt_ctx.metadata(), // MediaMetadata - }; - - Ok(()) + duration: fmt_ctx.duration(), + start_time: fmt_ctx.start_time(), + bitrate: fmt_ctx.bit_rate(), + chapters: fmt_ctx.chapters(), + programs: fmt_ctx.programs(), + metadata: fmt_ctx.metadata(), + }; + + Ok(media_info) }