diff --git a/Cargo.lock b/Cargo.lock index 510aef4c960f1e020263407ccdfff02826587d6b..8596e43a96453fe5659bb3fa2da29f60bc932d36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,12 @@ dependencies = [ "adler32", ] +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" + [[package]] name = "jpeg-decoder" version = "0.1.18" @@ -450,6 +456,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "miniz_oxide" version = "0.3.6" @@ -698,6 +710,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -725,6 +743,37 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "serde" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "0.1.1" @@ -809,6 +858,9 @@ dependencies = [ "fraction", "image", "log", + "mime", + "serde", + "serde_json", "time", ] diff --git a/Cargo.toml b/Cargo.toml index 094af427155cb12f45e918fe685aed5b954af049..35ac6cf6e322806c5b89b226a810914e532456db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,7 @@ image = "0.23.0" enum_primitive = "0.1.1" failure = "0.1.6" fraction = "0.6.2" -time = "0.2" \ No newline at end of file +mime = "0.3.16" +time = "0.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/src/ffmpeg_api/api.rs b/src/ffmpeg_api/api.rs index f166b40390f5f82372b2c8aff94bec81aaff4f7e..cf6b7eae916f1d96374c6f2f48f3ff7a05c70bd6 100644 --- a/src/ffmpeg_api/api.rs +++ b/src/ffmpeg_api/api.rs @@ -1,11 +1,13 @@ -use ffmpeg_dev::sys as ffi; -use failure::bail; -use enum_primitive::*; use std::marker::PhantomData; + +use enum_primitive::*; +use failure::bail; +use ffmpeg_dev::sys as ffi; use fraction::Fraction; use crate::ffmpeg_api::enums::*; use crate::util::media_time; +use std::path::Path; pub struct AVFormatContext { base: *mut ffi::AVFormatContext, @@ -20,13 +22,16 @@ impl<'a> AVFormatContext { Ok(AVFormatContext { base }) } - pub fn open_input(&mut self, path: &str) -> Result<(), failure::Error> { + pub fn open_input(&mut self, path: &Path) -> Result<(), failure::Error> { + let path = path.to_str() + .ok_or_else(|| failure::format_err!("Could not convert path to c string"))?; + let path = std::ffi::CString::new(path) + .map_err(|_| failure::format_err!("Could not convert path to c string"))?; + match unsafe { ffi::avformat_open_input( &mut self.base, - std::ffi::CString::new(path) - .map_err(|_| failure::format_err!("Could not convert path to c string"))? - .as_ptr(), + path.as_ptr(), std::ptr::null_mut(), std::ptr::null_mut(), ) @@ -36,6 +41,10 @@ impl<'a> AVFormatContext { } } + pub fn input_format(&self) -> AVInputFormat { + AVInputFormat::new(unsafe { (*self.base).iformat.as_mut() }.expect("not null")) + } + pub fn streams(&self) -> Vec<AVStream> { Vec::from( unsafe { @@ -69,6 +78,15 @@ impl<'a> AVFormatContext { errno => Err(failure::format_err!("Error while decoding frame: {}", errno)) } } + + pub fn duration(&self) -> Result<media_time::MediaTime, failure::Error> { + media_time::MediaTime::from_rational( + unsafe { (*self.base).duration }, Fraction::new( + 1 as u64, + ffi::AV_TIME_BASE as u64, + ), + ) + } } impl Drop for AVFormatContext { @@ -77,6 +95,89 @@ impl Drop for AVFormatContext { } } +pub struct AVInputFormat<'a> { + base: &'a mut ffi::AVInputFormat +} + +impl<'a> AVInputFormat<'a> { + fn new(base: &'a mut ffi::AVInputFormat) -> Self { + return AVInputFormat { base }; + } + + pub fn long_name(&self) -> Result<String, failure::Error> { + let raw: *const ::std::os::raw::c_char = self.base.long_name; + + if raw.is_null() { + Err(failure::format_err!("No mime type found")) + } else { + Ok(String::from(unsafe { + std::ffi::CStr::from_ptr(raw) + }.to_str().map_err(|err| { + failure::format_err!("Could not convert mime type to string: {}", err) + })?)) + } + } + + pub fn name(&self) -> Result<String, failure::Error> { + let raw: *const ::std::os::raw::c_char = self.base.name; + + if raw.is_null() { + Err(failure::format_err!("No mime type found")) + } else { + Ok(String::from(unsafe { + std::ffi::CStr::from_ptr(raw) + }.to_str().map_err(|err| { + failure::format_err!("Could not convert mime type to string: {}", err) + })?)) + } + } + + pub fn mime(&self) -> Result<String, failure::Error> { + let raw: *const ::std::os::raw::c_char = self.base.mime_type; + + if raw.is_null() { + Err(failure::format_err!("No mime type found")) + } else { + Ok(String::from(unsafe { + std::ffi::CStr::from_ptr(raw) + }.to_str().map_err(|err| { + failure::format_err!("Could not convert mime type to string: {}", err) + })?)) + } + } + + pub fn determine_mime<T: AsRef<str>>(&self, stream_codec: T) -> Result<String, failure::Error> { + let containers = self.name()?; + let stream_codec = stream_codec.as_ref(); + + for container in containers.split(",") { + match container { + "mp4" => match stream_codec { + "h264" => { + return Ok(String::from("video/mp4")); + } + _ => { + // Do nothing + } + } + "webm" => match stream_codec { + "vp8" | "vp9" | "av1" => { + return Ok(String::from("video/webm")); + } + _ => { + // Do nothing + } + } + _ => { + // Do nothing + } + } + } + + return Err(failure::format_err!("Could not determine mime type: {} video in {} container", stream_codec, containers)); + } +} + pub struct AVBuffer { base: *mut u8, size: usize, @@ -349,6 +450,10 @@ impl<'a> AVCodecParameters<'a> { AVCodecID::from_u32(self.base.codec_id) } + pub fn bit_rate(&self) -> i64 { + self.base.bit_rate + } + pub fn find_decoder(&self) -> AVCodec { AVCodec::new( unsafe { ffi::avcodec_find_decoder(self.base.codec_id).as_mut() }.expect("Decoder not found"), diff --git a/src/thumbnail/extract.rs b/src/ingest/extract.rs similarity index 84% rename from src/thumbnail/extract.rs rename to src/ingest/extract.rs index 0086d2c91fd377e3c5525cf0e4ef10991ac804fd..9c610e6b723f57c1ede6e4226346c5957bd7d6c5 100644 --- a/src/thumbnail/extract.rs +++ b/src/ingest/extract.rs @@ -1,29 +1,34 @@ +use std::path::Path; +use failure::format_err; + use crate::ffmpeg_api::api::*; use crate::ffmpeg_api::enums::*; use crate::util::media_time::*; -use crate::thumbnail::spritesheet::*; - -use failure::format_err; +use crate::ingest::spritesheet::*; +use crate::util::stream_metadata::*; -pub fn extract<T: AsRef<str>, U: AsRef<str>>( +pub fn extract( max_size: u32, num_horizontal: u32, num_vertical: u32, frame_interval: MediaTime, - input_file: T, - output_folder: U, + input_file: &Path, + output_folder: &Path, ) -> Result<(), failure::Error> { let mut avformat_context = AVFormatContext::new().map_err(|error| { format_err!("Could not allocate a context to process the video: {:?}", error) })?; - avformat_context.open_input(input_file.as_ref()).map_err(|error| { + avformat_context.open_input(input_file).map_err(|error| { format_err!("Could not open video input: {:?}", error) })?; + let duration = avformat_context.duration()?; + let spritesheet_path = output_folder.join("spritesheets"); + std::fs::create_dir_all(&spritesheet_path)?; let mut spritesheet_manager = SpritesheetManager::new( max_size, num_horizontal, num_vertical, frame_interval, - &output_folder, + spritesheet_path, "preview" ); @@ -36,7 +41,6 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>( let index = stream.index(); let time_base = stream.time_base(); - let duration = stream.duration()?; let codec_parameters = stream.codec_parameters(); let local_codec = codec_parameters.find_decoder(); @@ -48,6 +52,12 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>( local_codec.name() ); + let mut metadata = StreamMetadata::new( + avformat_context.input_format().determine_mime(local_codec.name())?, + duration, + codec_parameters.bit_rate() / 1000 + ); + let mut output_frame = AVFrame::new().map_err(|error| { format_err!("Could not create output frame: {:?}", error) })?; @@ -91,6 +101,7 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>( if spritesheet_manager.fulfils_frame_interval(timestamp) { if !spritesheet_manager.initialized() { spritesheet_manager.initialize(frame.width() as u32, frame.height() as u32); + metadata.set_frame_size(frame.width(), frame.height()); output_frame.init( spritesheet_manager.sprite_width() as i32, spritesheet_manager.sprite_height() as i32, @@ -128,5 +139,11 @@ pub fn extract<T: AsRef<str>, U: AsRef<str>>( spritesheet_manager.save()?; } + metadata.save( + output_folder.join("metadata.json") + ).map_err(|error| { + format_err!("Could not write stream metadata: {}", error) + })?; + Ok(()) } \ No newline at end of file diff --git a/src/thumbnail/mod.rs b/src/ingest/mod.rs similarity index 100% rename from src/thumbnail/mod.rs rename to src/ingest/mod.rs diff --git a/src/thumbnail/spritesheet.rs b/src/ingest/spritesheet.rs similarity index 89% rename from src/thumbnail/spritesheet.rs rename to src/ingest/spritesheet.rs index 251664b7c171da73c5fc763d54e791bbb9be011d..e0e05b806cfbb0d000b72fa96c0ec4968c215e26 100644 --- a/src/thumbnail/spritesheet.rs +++ b/src/ingest/spritesheet.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use image::{RgbImage, ImageBuffer}; use failure::{bail, format_err}; @@ -15,13 +17,13 @@ pub struct SpritesheetManager { last_timestamp: MediaTime, frame_interval: MediaTime, metadata: WebVTTFile, - output_path: std::string::String, + output_path: PathBuf, name: std::string::String, initialized: bool, } impl SpritesheetManager { - pub fn new<T: AsRef<str>, U: AsRef<str>>(max_side: u32, num_horizontal: u32, num_vertical: u32, frame_interval: MediaTime, output_path: T, name: U) -> SpritesheetManager { + pub fn new<T: AsRef<str>, U: Into<PathBuf>>(max_side: u32, num_horizontal: u32, num_vertical: u32, frame_interval: MediaTime, output_path: U, name: T) -> SpritesheetManager { SpritesheetManager { num_horizontal, num_vertical, @@ -33,7 +35,7 @@ impl SpritesheetManager { last_timestamp: MediaTime::from_millis(0), frame_interval, metadata: WebVTTFile::new(), - output_path: std::string::String::from(output_path.as_ref()), + output_path: output_path.into(), name: std::string::String::from(name.as_ref()), initialized: false, } @@ -141,7 +143,7 @@ impl SpritesheetManager { fn save_spritesheet(&mut self) -> Result<(), failure::Error> { self.spritesheet.save( - format!("{}/{}_{}.jpg", self.output_path, self.name, self.spritesheet_index(self.current_image)) + self.output_path.join(format!("{}_{}.jpg", self.name, self.spritesheet_index(self.current_image))) ).map_err(|error| { format_err!("Could not write spritesheet: {}", error) })?; @@ -151,7 +153,9 @@ impl SpritesheetManager { pub fn save(&mut self) -> Result<(), failure::Error> { self.save_spritesheet()?; - self.metadata.save(format!("{}/{}.vtt", self.output_path, self.name)).map_err(|error| { + self.metadata.save( + self.output_path.join(format!("{}.vtt", self.name)) + ).map_err(|error| { format_err!("Could not write spritesheet metadata: {}", error) })?; Ok(()) diff --git a/src/main.rs b/src/main.rs index b6c2e49bde5a5128dd848a72fe41eca76b7df496..d8ce00983baa405e5e74dddf2da5e161000f939f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,18 @@ #![allow(dead_code)] pub(crate) mod ffmpeg_api; -pub(crate) mod thumbnail; +pub(crate) mod ingest; pub(crate) mod util; use crate::util::media_time::MediaTime; +use std::path::Path; fn main() -> Result<(), failure::Error> { - thumbnail::extract::extract( + ingest::extract::extract( 160, 5, 5, MediaTime::from_seconds(2), - "/home/janne/Workspace/justflix/data/video.mp4", - "/home/janne/Workspace/justflix/data/spritesheets" + Path::new("/home/kuschku/Workspace/projects/mediaflix/data/movie.mp4"), + Path::new("/home/kuschku/Workspace/projects/mediaflix/data/output") )?; Ok(()) diff --git a/src/util/media_time.rs b/src/util/media_time.rs index 34396093f2d48336cfc36e956df2b04dfa6de03d..54bd366ce68a807a25f81cb2833263516bf2f94a 100644 --- a/src/util/media_time.rs +++ b/src/util/media_time.rs @@ -14,21 +14,29 @@ impl MediaTime { })?; Ok(MediaTime(time::Duration::milliseconds( - 1000 * timestamp * num as i64 / den as i64 + (1000 * timestamp as i128 * num as i128 / den as i128) as i64 ))) } + #[inline(always)] pub fn from_millis(timestamp: i64) -> MediaTime { MediaTime(time::Duration::milliseconds(timestamp)) } + #[inline(always)] pub fn from_seconds(timestamp: i64) -> MediaTime { MediaTime(time::Duration::seconds(timestamp)) } + #[inline(always)] pub fn is_zero(&self) -> bool { self.0.is_zero() } + + #[inline(always)] + pub fn seconds(&self) -> i64 { + self.0.whole_seconds() + } } impl std::fmt::Display for MediaTime { diff --git a/src/util/mod.rs b/src/util/mod.rs index f6c6f2a78a72b7245ce465a7c047a29dcff45608..28d153216d4d47d35355a14b65f6104ee04866b5 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,2 +1,3 @@ +pub(crate) mod media_time; +pub(crate) mod stream_metadata; pub(crate) mod webvtt; -pub(crate) mod media_time; \ No newline at end of file diff --git a/src/util/stream_metadata.rs b/src/util/stream_metadata.rs new file mode 100644 index 0000000000000000000000000000000000000000..35e1412844529367fac1bced5553f639015edf2b --- /dev/null +++ b/src/util/stream_metadata.rs @@ -0,0 +1,41 @@ +use std::fs::File; +use std::path::Path; +use std::io::BufWriter; + +use serde::{Deserialize, Serialize}; + +use crate::util::media_time::MediaTime; + +#[derive(Serialize, Deserialize)] +pub struct StreamMetadata { + content_type: String, + duration: i64, + bitrate: i64, + aspect_ratio: f32, + width: i32, + height: i32, +} + +impl StreamMetadata { + pub fn new<T: AsRef<str>>(content_type: T, duration: MediaTime, bitrate: i64) -> StreamMetadata { + StreamMetadata { + content_type: String::from(content_type.as_ref()), + duration: duration.seconds(), + bitrate, + aspect_ratio: 0.0, + width: 0, + height: 0, + } + } + + pub fn set_frame_size(&mut self, width: i32, height: i32) { + self.width = width; + self.height = height; + self.aspect_ratio = (width as f64 / height as f64) as f32; + } + + pub fn save<T: AsRef<Path>>(&self, path: T) -> Result<(), std::io::Error> { + serde_json::to_writer(BufWriter::new(File::create(path)?), self)?; + Ok(()) + } +} \ No newline at end of file diff --git a/src/util/webvtt.rs b/src/util/webvtt.rs index 64f56034c7b0b92efbee8ff28ee9ea316cbb6790..c26b8e9fd6ad343451fbb5f586518f2627394071 100644 --- a/src/util/webvtt.rs +++ b/src/util/webvtt.rs @@ -1,6 +1,8 @@ use std::fs::File; use std::io::prelude::*; use std::io::LineWriter; +use std::path::Path; +use std::string::String; use crate::util::media_time::MediaTime; @@ -11,7 +13,7 @@ pub struct WebVTTFile { pub struct WebVTTCue { start: MediaTime, end: MediaTime, - payload: std::string::String, + payload: String, } impl WebVTTFile { @@ -25,7 +27,7 @@ impl WebVTTFile { self.cues.push(cue); } - pub fn save(&self, path: std::string::String) -> Result<(), std::io::Error> { + pub fn save<T: AsRef<Path>>(&self, path: T) -> Result<(), std::io::Error> { let file = File::create(path)?; let mut file = LineWriter::new(file); file.write_all(b"WEBVTT\n\n")?; @@ -38,7 +40,7 @@ impl WebVTTFile { } impl WebVTTCue { - pub fn new(start: MediaTime, end: MediaTime, payload: std::string::String) -> WebVTTCue { + pub fn new(start: MediaTime, end: MediaTime, payload: String) -> WebVTTCue { WebVTTCue { start, end, payload } }