mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-10-09 11:01:42 -04:00
* initial ffprobe commit * Working slim down version ffprobe * Auto format ffprobe and deps source * Remove show_pixel_formats logic - Fix do_bitexact incorrect check in main after last changes - Fix some clangd warning * Remove show_* and print_format options and their respective logic * Rework ffprobe into simple_ffprobe - Simplify ffprobe logic into a simple program that gather and print a media file metadata * Reduce the amount of ffmpeg log messages while generating thumbnails * Fix completly wrong comments * mend * Start modeling ffmpeg extracted metadata on schema - Start porting ffprobe code to rust - Rename some references to media_data to exif_data * Finish modeling media info data - Add MediaProgram, MediaStream, MediaCodec, MediaVideoProps, MediaAudioProps, MediaSubtitleProps to Schema - Fix simple_ffproble to use its custom print_codec, instead of ffmpeg's impl * Add relation between MediaInfo and FilePath - Remove shared properties from MediaInfo and related structs - Implement Iterator for FFmpegDict * Fix and update schema * Data models and start populating MediaInfo in rust * Finish populating media info, chapters and program * Improve FFmpegFormatContext data raw pointer access - Implement stream data gathering * Impl FFmpegCodecContext, retrieve codec information - Improve some unsafe pointer uses - Impl from FFmpegFormatContext to MediaInfo conversion * Fix FFmpegDict Drop * Fix some crago warnings * Impl retrieval of video props - Fix C char* to Rust String convertion * Impl retrieval of audio and subtitle props - Fill props for MediaCodec * Remove simple_ffprobe now that the Rust impl is done * Fix schema to match actually retrieved media info - Fix import some FFmpeg constants instead of directly using values * Rework movie_decoder - Re-implement create_scale_string and add support anamorphic video - Improve C pointer access for FFmpegFormatContext and FFmpegCodecContext - Use newer FFmpeg abstractions in movie_decoder * Fix incorrect props when initializing MovieDecoder * Remove unecessary lifetimes * Added more native wrappers for some FFmpeg native objects used in movie_decoder * Remove FFmpegPacket - Some more improvements to movie_decoder * WIP * Some small fixes * More fixes Rename movie_decoder to frame_decoder Remove more references to film_strips * fmt * Fix duplicate migration for job error changes * fix rebase * Solving segfaults, fuck C lang Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Update rust to version 1.77 - Pin rust version with rust-toolchain.toml - Change from dtolnay/rust-toolchain to IronCoreLabs/rust-toolchain for rust-toolchain support - Remove unused function and imports - Replace most CString uses with new c literal string * More segfault solving and other minor fixes Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Fix ffmpeg rotation filter breaking portrait video thumbnails #2150 - Plus some other misc fixes * Auto format * Retrieve video/audio metadata on frontend * Auto format * First draft on ffmpeg data save on db Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Fix some incorrect changes to prisma schema * Some fixes for the FFmpegData schema - Expand logic to save FFmpegData to db * A ton of things Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Integrating ffmpeg media data in jobs and API * Rspc can't BigInt * 🙄 * Add initial ffmpeg metadata entries to Inspector - Fix ephemeral metadata api to match the files metadata api call * Fix Inspector not showing ffmpeg metadata * Add bitrate, start time and chapters video metadata to Inspector - Fix backend BigInt conversion incorrectly using i32 instead of u32 - Change FFmpegFormatContext/FFmpegMetaData bit_rate to i64 - Rename byteSize to humanizeSize - Expand humanizeSize logic to allow handling bits and Binary units - Move capitalize to @sd/client utils * Solving some issues * Fix ffmpeg probe getting incorrect stream id and breaking database unique constraint - Fix humanizeSize breaking when receiving floating numbers - Fix incorrect equality in StatCard - Fix unhandled error in Dialog when trying to remove an unknown dialog * fmt * small improvements - Remove some unecessary recursion_limit directive - Remove unused app_image releated functions - Fix metadata query enabled flag * Add migration for ffmpeg media data * Fix cypress test * Requested changes * Implement feedback - Update locale keys for all languages - Add pnpm command to update all language keys * Fix thumb reactivity in non indexed locations --------- Co-authored-by: Ericson Soares <ericson.ds999@gmail.com> Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com>
428 lines
12 KiB
Rust
428 lines
12 KiB
Rust
use crate::{
|
|
codec_ctx::FFmpegCodecContext,
|
|
dict::FFmpegDictionary,
|
|
error::{Error, FFmpegError},
|
|
model::{FFmpegChapter, FFmpegMediaData, FFmpegMetadata, FFmpegProgram, FFmpegStream},
|
|
utils::check_error,
|
|
};
|
|
|
|
use ffmpeg_sys_next::{
|
|
av_cmp_q, av_display_rotation_get, av_read_frame, av_reduce, av_stream_get_side_data,
|
|
avformat_close_input, avformat_find_stream_info, avformat_open_input, AVChapter, AVCodecID,
|
|
AVDictionary, AVFormatContext, AVMediaType, AVPacket, AVPacketSideDataType, AVRational,
|
|
AVStream, AV_DISPOSITION_ATTACHED_PIC, AV_DISPOSITION_CAPTIONS, AV_DISPOSITION_CLEAN_EFFECTS,
|
|
AV_DISPOSITION_COMMENT, AV_DISPOSITION_DEFAULT, AV_DISPOSITION_DEPENDENT,
|
|
AV_DISPOSITION_DESCRIPTIONS, AV_DISPOSITION_DUB, AV_DISPOSITION_FORCED,
|
|
AV_DISPOSITION_HEARING_IMPAIRED, AV_DISPOSITION_KARAOKE, AV_DISPOSITION_LYRICS,
|
|
AV_DISPOSITION_METADATA, AV_DISPOSITION_NON_DIEGETIC, AV_DISPOSITION_ORIGINAL,
|
|
AV_DISPOSITION_STILL_IMAGE, AV_DISPOSITION_TIMED_THUMBNAILS, AV_DISPOSITION_VISUAL_IMPAIRED,
|
|
AV_NOPTS_VALUE,
|
|
};
|
|
|
|
use std::{collections::HashSet, ffi::CStr, ptr};
|
|
|
|
fn extract_name_and_convert_metadata(
|
|
metadata: *mut AVDictionary,
|
|
) -> (FFmpegMetadata, Option<String>) {
|
|
let mut metadata = FFmpegDictionary::new(unsafe { metadata.as_mut() });
|
|
let name = metadata.get(c"name");
|
|
if name.is_some() {
|
|
let _ = metadata.remove(c"name");
|
|
}
|
|
|
|
(metadata.into(), name)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FFmpegFormatContext(*mut AVFormatContext);
|
|
|
|
impl FFmpegFormatContext {
|
|
pub(crate) fn open_file(filename: &CStr) -> Result<Self, Error> {
|
|
let mut ptr = ptr::null_mut();
|
|
|
|
check_error(
|
|
unsafe {
|
|
avformat_open_input(&mut ptr, filename.as_ptr(), ptr::null(), ptr::null_mut())
|
|
},
|
|
"Fail to open an input stream and read the header",
|
|
)
|
|
.map(|()| Self(ptr))
|
|
}
|
|
|
|
pub(crate) fn as_ref(&self) -> &AVFormatContext {
|
|
unsafe { self.0.as_ref() }.expect("initialized on struct creation")
|
|
}
|
|
|
|
pub(crate) fn as_mut(&mut self) -> &mut AVFormatContext {
|
|
unsafe { self.0.as_mut() }.expect("initialized on struct creation")
|
|
}
|
|
|
|
pub(crate) fn duration(&self) -> Option<i64> {
|
|
let duration = self.as_ref().duration;
|
|
if duration == AV_NOPTS_VALUE {
|
|
return None;
|
|
}
|
|
|
|
Some(duration)
|
|
}
|
|
|
|
pub(crate) fn stream(&self, index: u32) -> Option<&mut AVStream> {
|
|
let streams = self.as_ref().streams;
|
|
if streams.is_null() {
|
|
return None;
|
|
}
|
|
|
|
let Ok(index) = isize::try_from(index) else {
|
|
return None;
|
|
};
|
|
|
|
unsafe { (*(streams.offset(index))).as_mut() }
|
|
}
|
|
|
|
pub(crate) fn get_stream_rotation_angle(&self, index: u32) -> f64 {
|
|
let Some(stream) = self.stream(index) else {
|
|
return 0.0;
|
|
};
|
|
|
|
/*
|
|
* This side data contains a 3x3 transformation matrix describing an affine transformation
|
|
* that needs to be applied to the decoded video frames for correct presentation.
|
|
*
|
|
* See libavutil/display.h for a detailed description of the data.
|
|
* https://github.com/FFmpeg/FFmpeg/blob/n6.1.1/libavutil/display.h#L32-L71
|
|
*
|
|
* The pointer conversion is due to the fact that av_stream_get_side_data is a generic function that has no prior
|
|
* knowledge of the type of the side data it is retrieving.
|
|
*/
|
|
#[allow(clippy::cast_ptr_alignment)]
|
|
let matrix = (unsafe {
|
|
av_stream_get_side_data(
|
|
stream,
|
|
AVPacketSideDataType::AV_PKT_DATA_DISPLAYMATRIX,
|
|
ptr::null_mut(),
|
|
)
|
|
} as *const i32);
|
|
|
|
if matrix.is_null() {
|
|
0.0
|
|
} else {
|
|
unsafe { av_display_rotation_get(matrix) }
|
|
}
|
|
}
|
|
|
|
pub(crate) fn read_frame(&mut self, packet: *mut AVPacket) -> Result<&mut Self, Error> {
|
|
check_error(
|
|
unsafe { av_read_frame(self.as_mut(), packet) },
|
|
"Fail to read the next frame of a media file",
|
|
)?;
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
pub(crate) fn find_stream_info(&mut self) -> Result<&mut Self, Error> {
|
|
check_error(
|
|
unsafe { avformat_find_stream_info(self.as_mut(), ptr::null_mut()) },
|
|
"Fail to read packets of a media file to get stream information",
|
|
)?;
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
pub(crate) fn find_preferred_video_stream(
|
|
&self,
|
|
prefer_embedded_metadata: bool,
|
|
) -> Result<(bool, &mut AVStream), Error> {
|
|
let mut video_streams = vec![];
|
|
let mut embedded_data_streams = vec![];
|
|
|
|
'outer: for stream_idx in 0..self.as_ref().nb_streams {
|
|
let Some(stream) = self.stream(stream_idx) else {
|
|
continue;
|
|
};
|
|
|
|
let Some((codec_type, codec_id)) = unsafe { stream.codecpar.as_ref() }
|
|
.map(|codec_params| (codec_params.codec_type, codec_params.codec_id))
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
if codec_type != AVMediaType::AVMEDIA_TYPE_VIDEO {
|
|
continue;
|
|
}
|
|
|
|
if !prefer_embedded_metadata
|
|
|| !(codec_id == AVCodecID::AV_CODEC_ID_MJPEG
|
|
|| codec_id == AVCodecID::AV_CODEC_ID_PNG)
|
|
{
|
|
video_streams.push(stream_idx);
|
|
continue;
|
|
}
|
|
|
|
if let Some(metadata) = unsafe { stream.metadata.as_mut() }
|
|
.map(|metadata| FFmpegDictionary::new(Some(metadata)))
|
|
{
|
|
for (key, value) in &metadata {
|
|
if let Some(value) = value {
|
|
if key == "filename" && value.starts_with("cover.") {
|
|
embedded_data_streams.insert(0, stream_idx);
|
|
continue 'outer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
embedded_data_streams.push(stream_idx);
|
|
}
|
|
|
|
if prefer_embedded_metadata && !embedded_data_streams.is_empty() {
|
|
for stream_index in embedded_data_streams {
|
|
if let Some(stream) = self.stream(stream_index) {
|
|
return Ok((true, stream));
|
|
}
|
|
}
|
|
}
|
|
|
|
for stream_index in video_streams {
|
|
if let Some(stream) = self.stream(stream_index) {
|
|
return Ok((false, stream));
|
|
}
|
|
}
|
|
|
|
Err(FFmpegError::StreamNotFound)?
|
|
}
|
|
|
|
fn formats(&self) -> Vec<String> {
|
|
unsafe { self.as_ref().iformat.as_ref() }
|
|
.and_then(|format| unsafe { format.name.as_ref() })
|
|
.map(|name| {
|
|
let cstr = unsafe { CStr::from_ptr(name) };
|
|
String::from_utf8_lossy(cstr.to_bytes())
|
|
.split(',')
|
|
.map(|entry| entry.trim().to_string())
|
|
.filter(|entry| !entry.is_empty())
|
|
.collect()
|
|
})
|
|
.unwrap_or(vec![])
|
|
}
|
|
|
|
fn start_time(&self) -> Option<i64> {
|
|
let start_time = self.as_ref().start_time;
|
|
if start_time == AV_NOPTS_VALUE {
|
|
return None;
|
|
}
|
|
|
|
Some(start_time)
|
|
}
|
|
|
|
fn bit_rate(&self) -> i64 {
|
|
self.as_ref().bit_rate
|
|
}
|
|
|
|
fn chapters(&self) -> Vec<FFmpegChapter> {
|
|
let chapters_ptr = self.as_ref().chapters;
|
|
(!chapters_ptr.is_null())
|
|
.then(|| {
|
|
(0..isize::try_from(self.as_ref().nb_chapters).unwrap_or(0))
|
|
.filter_map(|id| unsafe { (*(chapters_ptr.offset(id))).as_ref() })
|
|
.map(Into::into)
|
|
.collect()
|
|
})
|
|
.unwrap_or(vec![])
|
|
}
|
|
|
|
fn programs(&self) -> Vec<FFmpegProgram> {
|
|
let mut visited_streams: HashSet<u32> = HashSet::new();
|
|
let programs_ptr = self.as_ref().programs;
|
|
|
|
let mut programs = (!programs_ptr.is_null())
|
|
.then(|| {
|
|
(0..isize::try_from(self.as_ref().nb_programs).unwrap_or(0))
|
|
.filter_map(|id| unsafe { (*(programs_ptr.offset(id))).as_ref() })
|
|
.map(|program| {
|
|
let (metadata, name) = extract_name_and_convert_metadata(program.metadata);
|
|
|
|
let streams = (0..isize::try_from(program.nb_stream_indexes).unwrap_or(0))
|
|
.filter_map(|index| unsafe {
|
|
program.stream_index.offset(index).as_ref()
|
|
})
|
|
.copied()
|
|
.filter_map(|stream_index| {
|
|
visited_streams.insert(stream_index);
|
|
self.stream(stream_index)
|
|
})
|
|
.map(|stream| (&*stream).into())
|
|
.collect::<Vec<FFmpegStream>>();
|
|
|
|
FFmpegProgram {
|
|
id: program.id,
|
|
name,
|
|
streams,
|
|
metadata,
|
|
}
|
|
})
|
|
.collect::<Vec<FFmpegProgram>>()
|
|
})
|
|
.unwrap_or(vec![]);
|
|
|
|
let unvisited_streams = (0..self.as_ref().nb_streams)
|
|
.filter(|i| !visited_streams.contains(i))
|
|
.filter_map(|i| self.stream(i).map(|stream| (&*stream).into()))
|
|
.collect::<Vec<FFmpegStream>>();
|
|
if !unvisited_streams.is_empty() {
|
|
if let Ok(id) = i32::try_from(programs.len()) {
|
|
// Create an empty program to hold unvisited streams if there are any
|
|
programs.push(FFmpegProgram {
|
|
id,
|
|
name: Some("No Program".to_string()),
|
|
streams: unvisited_streams,
|
|
metadata: FFmpegMetadata::default(),
|
|
});
|
|
}
|
|
}
|
|
|
|
programs
|
|
}
|
|
|
|
fn metadata(&self) -> FFmpegMetadata {
|
|
let fmt_ctx = self.as_ref();
|
|
unsafe { fmt_ctx.metadata.as_mut() }.map_or_else(FFmpegMetadata::default, |metadata| {
|
|
FFmpegDictionary::new(Some(metadata)).into()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for FFmpegFormatContext {
|
|
fn drop(&mut self) {
|
|
if !self.0.is_null() {
|
|
unsafe { avformat_close_input(&mut self.0) };
|
|
self.0 = ptr::null_mut();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&FFmpegFormatContext> for FFmpegMediaData {
|
|
fn from(ctx: &FFmpegFormatContext) -> Self {
|
|
Self {
|
|
formats: ctx.formats(),
|
|
duration: ctx.duration(),
|
|
start_time: ctx.start_time(),
|
|
bit_rate: ctx.bit_rate(),
|
|
chapters: ctx.chapters(),
|
|
programs: ctx.programs(),
|
|
metadata: ctx.metadata(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&AVChapter> for FFmpegChapter {
|
|
fn from(
|
|
AVChapter {
|
|
id,
|
|
time_base,
|
|
start,
|
|
end,
|
|
metadata,
|
|
}: &AVChapter,
|
|
) -> Self {
|
|
Self {
|
|
// NOTICE: chapter.id is a i64, but I think it will be extremely rare to have a chapter id that doesn't fit in a i32
|
|
id: *id,
|
|
start: *start,
|
|
end: *end,
|
|
time_base_num: time_base.num,
|
|
time_base_den: time_base.den,
|
|
metadata: unsafe { metadata.as_mut() }
|
|
.map_or_else(FFmpegMetadata::default, |metadata| {
|
|
FFmpegDictionary::new(Some(metadata)).into()
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&AVStream> for FFmpegStream {
|
|
fn from(stream: &AVStream) -> Self {
|
|
let (metadata, name) = extract_name_and_convert_metadata(stream.metadata);
|
|
|
|
let aspect_ratio = unsafe { stream.codecpar.as_ref() }
|
|
.and_then(|codecpar| {
|
|
if stream.sample_aspect_ratio.num != 0
|
|
&& unsafe { av_cmp_q(stream.sample_aspect_ratio, codecpar.sample_aspect_ratio) }
|
|
!= 0
|
|
{
|
|
let mut display_aspect_ratio = AVRational { num: 0, den: 0 };
|
|
let num = i64::from(codecpar.width * codecpar.sample_aspect_ratio.num);
|
|
let den = i64::from(codecpar.height * codecpar.sample_aspect_ratio.den);
|
|
let max = 1024 * 1024;
|
|
unsafe {
|
|
av_reduce(
|
|
&mut display_aspect_ratio.num,
|
|
&mut display_aspect_ratio.den,
|
|
num,
|
|
den,
|
|
max,
|
|
);
|
|
}
|
|
|
|
Some(display_aspect_ratio)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or(stream.sample_aspect_ratio);
|
|
|
|
let dispositions = [
|
|
(AV_DISPOSITION_DEFAULT, "default"),
|
|
(AV_DISPOSITION_DUB, "dub"),
|
|
(AV_DISPOSITION_ORIGINAL, "original"),
|
|
(AV_DISPOSITION_COMMENT, "comment"),
|
|
(AV_DISPOSITION_LYRICS, "lyrics"),
|
|
(AV_DISPOSITION_KARAOKE, "karaoke"),
|
|
(AV_DISPOSITION_FORCED, "forced"),
|
|
(AV_DISPOSITION_HEARING_IMPAIRED, "hearing impaired"),
|
|
(AV_DISPOSITION_VISUAL_IMPAIRED, "visual impaired"),
|
|
(AV_DISPOSITION_CLEAN_EFFECTS, "clean effects"),
|
|
(AV_DISPOSITION_ATTACHED_PIC, "attached pic"),
|
|
(AV_DISPOSITION_TIMED_THUMBNAILS, "timed thumbnails"),
|
|
(AV_DISPOSITION_CAPTIONS, "captions"),
|
|
(AV_DISPOSITION_DESCRIPTIONS, "descriptions"),
|
|
(AV_DISPOSITION_METADATA, "metadata"),
|
|
(AV_DISPOSITION_DEPENDENT, "dependent"),
|
|
(AV_DISPOSITION_STILL_IMAGE, "still image"),
|
|
(AV_DISPOSITION_NON_DIEGETIC, "non-diegetic"),
|
|
]
|
|
.iter()
|
|
.filter_map(|&(flag, name)| {
|
|
if stream.disposition & flag != 0 {
|
|
Some(name.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<String>>();
|
|
|
|
let codec = unsafe { stream.codecpar.as_ref() }.and_then(|codec_params| {
|
|
FFmpegCodecContext::new()
|
|
.and_then(|mut codec| {
|
|
codec.parameters_to_context(codec_params)?;
|
|
Ok(codec)
|
|
})
|
|
.map(|codec| (&codec).into())
|
|
.ok()
|
|
});
|
|
|
|
Self {
|
|
id: stream.index,
|
|
name,
|
|
codec,
|
|
aspect_ratio_num: aspect_ratio.num,
|
|
aspect_ratio_den: aspect_ratio.den,
|
|
frames_per_second_num: stream.avg_frame_rate.num,
|
|
frames_per_second_den: stream.avg_frame_rate.den,
|
|
time_base_real_num: stream.time_base.num,
|
|
time_base_real_den: stream.time_base.den,
|
|
dispositions,
|
|
metadata,
|
|
}
|
|
}
|
|
}
|