diff --git a/src/ffmpeg_api/api.rs b/src/ffmpeg_api/api.rs index b1de9f291250d3ad46d5381bf67ac960a73578d6..a5ab7c27b7e4fea8c7420ca749d805deafea06bc 100644 --- a/src/ffmpeg_api/api.rs +++ b/src/ffmpeg_api/api.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use fraction::Fraction; use crate::ffmpeg_api::enums::*; -use crate::media_time; +use crate::util::media_time; pub struct AVFormatContext { base: *mut ffi::AVFormatContext, @@ -293,11 +293,11 @@ impl<'a> AVStream<'a> { ) } - pub fn timestamp(self: &AVStream<'a>, timestamp: i64) -> media_time::MediaTime { + pub fn timestamp(self: &AVStream<'a>, timestamp: i64) -> Result<media_time::MediaTime, failure::Error> { media_time::MediaTime::from_rational(timestamp, self.time_base()) } - pub fn duration(&self) -> media_time::MediaTime { + pub fn duration(&self) -> Result<media_time::MediaTime, failure::Error> { self.timestamp(self.base.duration) } diff --git a/src/main.rs b/src/main.rs index df06960d065567667e1b58f8d37d35f1b8f80a4a..10f2842bec690c67173c0d534da92ee9e7a61c86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,140 +1,18 @@ #![allow(dead_code)] pub(crate) mod ffmpeg_api; -pub(crate) mod webvtt; -pub(crate) mod media_time; -pub(crate) mod spritesheet; +pub(crate) mod thumbnail; +pub(crate) mod util; -use crate::ffmpeg_api::enums::*; -use crate::ffmpeg_api::api::*; -use crate::media_time::MediaTime; +use crate::util::media_time::MediaTime; -fn main() -> Result<(), std::io::Error> { - let mut before = std::time::SystemTime::now(); - - let input = "/home/janne/Workspace/justflix/data/video.mp4"; - let output = "/home/janne/Workspace/justflix/data/spritesheets"; - - let mut avformat_context = AVFormatContext::new().unwrap_or_else(|error| { - panic!("Could not allocate a context to process the video: {:?}", error) - }); - avformat_context.open_input(input).unwrap_or_else(|error| { - panic!("Could not open video input: {:?}", error) - }); - - let mut spritesheet_manager = spritesheet::SpritesheetManager::new( - 160, - 5, 5, +fn main() -> Result<(), failure::Error> { + thumbnail::extract::extract( + 160, 5, 5, MediaTime::from_seconds(10), - output, - ); - - let mut stream: AVStream = avformat_context.find_stream(|stream| { - stream.codec_parameters().codec_type() == AVMediaType::Video - }).unwrap_or_else(|| { - panic!("Could not find video stream") - }); - - stream.set_discard(AVDiscard::NonKey); - - let codec_parameters = stream.codec_parameters(); - let local_codec = codec_parameters.find_decoder(); - - println!( - "Stream #{}, type: {:#?}, codec: {:#?}", - stream.index(), - codec_parameters.codec_type(), - local_codec.name() - ); - - let mut output_frame = AVFrame::new().unwrap_or_else(|error| { - panic!("Could not create output frame: {:?}", error) - }); - - if codec_parameters.codec_type() == AVMediaType::Video { - let mut codec_context = AVCodecContext::new(&local_codec).unwrap_or_else(|error| { - panic!("Could not init codec context: {:?}", error) - }); - codec_context.set_parameters(&codec_parameters); - codec_context.open(&local_codec); - - codec_context.set_skip_loop_filter(AVDiscard::NonKey); - codec_context.set_skip_idct(AVDiscard::NonKey); - codec_context.set_skip_frame(AVDiscard::NonKey); - - let mut packet = AVPacket::new().unwrap_or_else(|error| { - panic!("Could not init temporary packet: {:?}", error) - }); - - let mut frame = AVFrame::new().unwrap_or_else(|error| { - panic!("Could not create input frame: {:?}", error) - }); - - println!("Time: {:#?}", before.elapsed().unwrap()); - before = std::time::SystemTime::now(); - - let mut scale_context = SwsContext::new(); - - while avformat_context.read_frame(&mut packet).is_ok() { - if packet.stream_index() == stream.index() { - codec_context.in_packet(&mut packet).unwrap_or_else(|error| { - panic!("Could not load packet: {:?}", error) - }); - while codec_context.out_frame(&mut frame).is_ok() { - println!( - "Frame {}: {} @ {}", - frame.coded_picture_number(), - stream.timestamp(frame.pts()), - frame.key_frame() - ); - println!("Reading Time: {:#?}", before.elapsed().unwrap()); - before = std::time::SystemTime::now(); - - if spritesheet_manager.fulfils_frame_interval(stream.timestamp(frame.pts())) { - if !spritesheet_manager.initialized() { - spritesheet_manager.initialize(frame.width() as u32, frame.height() as u32); - output_frame.init( - spritesheet_manager.sprite_width() as i32, - spritesheet_manager.sprite_height() as i32, - AVPixelFormat::RGB24, - ).unwrap_or_else(|error| { - panic!("Could not init output frame: {:?}", error) - }); - scale_context.reinit( - &frame, - &output_frame, - SwsScaler::FastBilinear, - ).unwrap_or_else(|error| { - panic!("Could not reinit scale context: {:?}", error) - }); - } - - scale_context.scale(&frame, &mut output_frame); - - println!("Processing Time: {:#?}", before.elapsed().unwrap()); - before = std::time::SystemTime::now(); - - spritesheet_manager.add_image( - stream.timestamp(frame.pts()), - image::ImageBuffer::from_raw( - output_frame.width() as u32, - output_frame.height() as u32, - output_frame.data(0).to_vec(), - ).unwrap_or_else(|| { - panic!("Could not process frame") - }), - ); - - println!("Writing Time: {:#?}", before.elapsed().unwrap()); - before = std::time::SystemTime::now(); - } - } - } - } - - spritesheet_manager.end_frame(stream.duration()); - spritesheet_manager.save(); - } + "/home/janne/Workspace/justflix/data/video.mp4", + "/home/janne/Workspace/justflix/data/spritesheets" + )?; Ok(()) } diff --git a/src/thumbnail/extract.rs b/src/thumbnail/extract.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ee06a8da2b5fe3c94f95dd7c8df353fac6f0d0c --- /dev/null +++ b/src/thumbnail/extract.rs @@ -0,0 +1,126 @@ +use crate::ffmpeg_api::api::*; +use crate::ffmpeg_api::enums::*; +use crate::util::media_time::*; +use crate::thumbnail::spritesheet::*; + +use failure::format_err; + +pub fn extract<T: AsRef<str>, U: AsRef<str>>( + max_size: u32, + num_horizontal: u32, num_vertical: u32, + frame_interval: MediaTime, + input_file: T, + output_folder: U, +) -> 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| { + format_err!("Could not open video input: {:?}", error) + })?; + + let mut spritesheet_manager = SpritesheetManager::new( + max_size, + num_horizontal, num_vertical, + frame_interval, + output_folder, + ); + + let mut stream: AVStream = avformat_context.find_stream(|stream| { + stream.codec_parameters().codec_type() == AVMediaType::Video + }).ok_or_else(|| { + format_err!("Could not find video stream") + })?; + + stream.set_discard(AVDiscard::NonKey); + + let codec_parameters = stream.codec_parameters(); + let local_codec = codec_parameters.find_decoder(); + + println!( + "Stream #{}, type: {:#?}, codec: {:#?}", + stream.index(), + codec_parameters.codec_type(), + local_codec.name() + ); + + let mut output_frame = AVFrame::new().map_err(|error| { + format_err!("Could not create output frame: {:?}", error) + })?; + + if codec_parameters.codec_type() == AVMediaType::Video { + let mut codec_context = AVCodecContext::new(&local_codec).map_err(|error| { + format_err!("Could not init codec context: {:?}", error) + })?; + codec_context.set_parameters(&codec_parameters); + codec_context.open(&local_codec); + + codec_context.set_skip_loop_filter(AVDiscard::NonKey); + codec_context.set_skip_idct(AVDiscard::NonKey); + codec_context.set_skip_frame(AVDiscard::NonKey); + + let mut packet = AVPacket::new().map_err(|error| { + format_err!("Could not init temporary packet: {:?}", error) + })?; + + let mut frame = AVFrame::new().map_err(|error| { + format_err!("Could not create input frame: {:?}", error) + })?; + + let mut scale_context = SwsContext::new(); + + while avformat_context.read_frame(&mut packet).is_ok() { + if packet.stream_index() == stream.index() { + codec_context.in_packet(&mut packet).map_err(|error| { + format_err!("Could not load packet: {:?}", error) + })?; + while codec_context.out_frame(&mut frame).is_ok() { + println!( + "Frame {}: {} @ {}", + frame.coded_picture_number(), + stream.timestamp(frame.pts())?, + frame.key_frame() + ); + + if spritesheet_manager.fulfils_frame_interval(stream.timestamp(frame.pts())?) { + if !spritesheet_manager.initialized() { + spritesheet_manager.initialize(frame.width() as u32, frame.height() as u32); + output_frame.init( + spritesheet_manager.sprite_width() as i32, + spritesheet_manager.sprite_height() as i32, + AVPixelFormat::RGB24, + ).map_err(|error| { + format_err!("Could not init output frame: {:?}", error) + })?; + scale_context.reinit( + &frame, + &output_frame, + SwsScaler::FastBilinear, + ).map_err(|error| { + format_err!("Could not reinit scale context: {:?}", error) + })?; + } + + scale_context.scale(&frame, &mut output_frame); + + spritesheet_manager.add_image( + stream.timestamp(frame.pts())?, + image::ImageBuffer::from_raw( + output_frame.width() as u32, + output_frame.height() as u32, + output_frame.data(0).to_vec(), + ).ok_or_else(|| { + format_err!("Could not process frame") + })? + )?; + } + } + } + } + + spritesheet_manager.end_frame(stream.duration()?); + spritesheet_manager.save()?; + } + + Ok(()) +} \ No newline at end of file diff --git a/src/thumbnail/mod.rs b/src/thumbnail/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..59bb3356ff31b492b41b552db2252d486c1442f2 --- /dev/null +++ b/src/thumbnail/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod extract; +pub(crate) mod spritesheet; \ No newline at end of file diff --git a/src/spritesheet.rs b/src/thumbnail/spritesheet.rs similarity index 86% rename from src/spritesheet.rs rename to src/thumbnail/spritesheet.rs index 1e0114abb1a17b7d50e4264e207c4efa60bc0ac8..1065df69f07e2a8824de9d294621f0e216816bb6 100644 --- a/src/spritesheet.rs +++ b/src/thumbnail/spritesheet.rs @@ -1,6 +1,8 @@ use image::{RgbImage, ImageBuffer}; -use crate::media_time::MediaTime; -use crate::webvtt::{WebVTTFile, WebVTTCue}; +use failure::{bail, format_err}; + +use crate::util::media_time::MediaTime; +use crate::util::webvtt::{WebVTTFile, WebVTTCue}; pub struct SpritesheetManager { num_horizontal: u32, @@ -88,9 +90,9 @@ impl SpritesheetManager { self.current_image == 0 || timestamp - self.last_timestamp > self.frame_interval } - pub fn add_image(&mut self, timestamp: MediaTime, image: RgbImage) { + pub fn add_image(&mut self, timestamp: MediaTime, image: RgbImage) -> Result<(), failure::Error> { if image.width() != self.sprite_width || image.height() != self.sprite_height { - panic!( + bail!( "Wrong image size: {}x{}, but expected {}x{}", image.width(), image.height(), self.sprite_width, self.sprite_height @@ -113,8 +115,10 @@ impl SpritesheetManager { self.current_image += 1; if self.sprite_index(self.current_image) == 0 { - self.save(); + self.save()?; } + + Ok(()) } pub fn end_frame(&mut self, timestamp: MediaTime) { @@ -132,19 +136,21 @@ impl SpritesheetManager { )); } - fn save_spritesheet(&mut self) { + fn save_spritesheet(&mut self) -> Result<(), failure::Error> { self.spritesheet.save( format!("{}/spritesheet_{}.png", self.output_path, self.spritesheet_index(self.current_image)) - ).unwrap_or_else(|error| { - panic!("Could not write spritesheet: {}", error) - }); + ).map_err(|error| { + format_err!("Could not write spritesheet: {}", error) + })?; self.reinit_buffer(); + Ok(()) } - pub fn save(&mut self) { - self.save_spritesheet(); - self.metadata.save(format!("{}/spritesheet.vtt", self.output_path)).unwrap_or_else(|error| { - panic!("Could not write spritesheet metadata: {}", error) - }); + pub fn save(&mut self) -> Result<(), failure::Error> { + self.save_spritesheet()?; + self.metadata.save(format!("{}/spritesheet.vtt", self.output_path)).map_err(|error| { + format_err!("Could not write spritesheet metadata: {}", error) + })?; + Ok(()) } } \ No newline at end of file diff --git a/src/media_time.rs b/src/util/media_time.rs similarity index 74% rename from src/media_time.rs rename to src/util/media_time.rs index a5d07f22c3d7e009d162b07d508a09ed91548681..34396093f2d48336cfc36e956df2b04dfa6de03d 100644 --- a/src/media_time.rs +++ b/src/util/media_time.rs @@ -1,20 +1,21 @@ use fraction::Fraction; +use failure::format_err; #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct MediaTime(time::Duration); impl MediaTime { - pub fn from_rational(timestamp: i64, base: Fraction) -> MediaTime { - let num: u64 = *base.numer().unwrap_or_else(|| { - panic!("time base of unusable format") - }); - let den: u64 = *base.denom().unwrap_or_else(|| { - panic!("time base of unusable format") - }); - - MediaTime(time::Duration::milliseconds( + pub fn from_rational(timestamp: i64, base: Fraction) -> Result<MediaTime, failure::Error> { + let num: u64 = *base.numer().ok_or_else(|| { + format_err!("time base of unusable format") + })?; + let den: u64 = *base.denom().ok_or_else(|| { + format_err!("time base of unusable format") + })?; + + Ok(MediaTime(time::Duration::milliseconds( 1000 * timestamp * num as i64 / den as i64 - )) + ))) } pub fn from_millis(timestamp: i64) -> MediaTime { diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..f6c6f2a78a72b7245ce465a7c047a29dcff45608 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod webvtt; +pub(crate) mod media_time; \ No newline at end of file diff --git a/src/webvtt.rs b/src/util/webvtt.rs similarity index 96% rename from src/webvtt.rs rename to src/util/webvtt.rs index 88deb4db1bf8dc8140ead83072424243ab402c4b..64f56034c7b0b92efbee8ff28ee9ea316cbb6790 100644 --- a/src/webvtt.rs +++ b/src/util/webvtt.rs @@ -1,8 +1,9 @@ -use crate::media_time::MediaTime; use std::fs::File; use std::io::prelude::*; use std::io::LineWriter; +use crate::util::media_time::MediaTime; + pub struct WebVTTFile { cues: Vec<WebVTTCue> }