diff options
author | rabite <rabite@posteo.de> | 2019-05-21 20:50:54 +0200 |
---|---|---|
committer | rabite <rabite@posteo.de> | 2019-05-21 20:52:04 +0200 |
commit | e8d9c6ad1f82d6b0bbf3cf9449ce53dd525cb77a (patch) | |
tree | e9615c4cf832f1a17a640a229c5dd79710ad601e /src | |
parent | 6f8a6366039282cfce272a4a430590de83f48045 (diff) |
merge in media-preview stuff
Squashed commit of the following:
commit 4087cee2f2a72bc2d3e44832809cbe99d25a3309
Author: rabite <rabite@posteo.de>
Date: Tue May 21 20:40:27 2019 +0200
rename to preview-gen
commit 275fd420a01c24393ae9cc07287204fde309130a
Author: rabite <rabite@posteo.de>
Date: Tue May 21 20:30:50 2019 +0200
update Cargo.toml and README
commit bf4c3fec0f8e0ff4628153297ac64fe165b4e443
Author: rabite <rabite@posteo.de>
Date: Tue May 21 20:30:33 2019 +0200
add config options for media
commit 3bf681cd078177d634507c51d04a45bdae184f1b
Author: rabite <rabite@posteo.de>
Date: Tue May 21 13:46:00 2019 +0200
always use unicode icons for play/pause/mute
commit ed2a0b31d1911e4d42086699ca1db636c327bacf
Author: rabite <rabite@posteo.de>
Date: Tue May 21 13:04:11 2019 +0200
rename to mediaview and some fixups
commit bda3f8f83bec07c3f52d06cf5f8eaad7fdbd85a0
Author: rabite <rabite@posteo.de>
Date: Tue May 21 11:44:52 2019 +0200
fix warnings
commit 9c917903cfa958b225f268ed4fb9f55f27162be0
Author: rabite <rabite@posteo.de>
Date: Tue May 21 01:10:58 2019 +0200
play audio through viedo widget
commit 9fe208b213710fae889f7701bdb3bf96d599c9a8
Author: rabite <rabite@posteo.de>
Date: Tue May 21 00:15:12 2019 +0200
fixed play when autoplay is off
commit d4036d52864d0a4e08f7da2cf6f7419ef070c1a9
Author: rabite <rabite@posteo.de>
Date: Mon May 20 13:15:44 2019 +0200
add icons for play/pause/mute
commit 29f0d203cfd7501b50d80501de146da9d89388bb
Author: rabite <rabite@posteo.de>
Date: Mon May 20 12:26:53 2019 +0200
add basic gif support
commit 8d28e4c23a91b0ef83739554c3a2f6671a9ff6f5
Author: rabite <rabite@posteo.de>
Date: Mon May 20 11:43:57 2019 +0200
fixed play after pause
commit 142017df78de77bcfffd8f3dd9612a06927e183b
Author: rabite <rabite@posteo.de>
Date: Sun May 19 14:22:16 2019 +0200
change to stale on drop
commit c0026c7f56f6a841df18e41bd1c9b33543e26cfb
Author: rabite <rabite@posteo.de>
Date: Sat May 18 23:46:27 2019 +0200
handle errors in gstreamer closure
commit ef37f872c54b9d21ed5505c2edb480c793afaef9
Author: rabite <rabite@posteo.de>
Date: Sat May 18 23:40:24 2019 +0200
add image-only mode to preview-gen
commit 15752464563463c6ecf892c1d8a14651bf32e1e5
Author: rabite <rabite@posteo.de>
Date: Sat May 18 23:28:33 2019 +0200
add copyright note
commit 4c9f08fc4de5d3bffefd42284b60aa81cfbae9f7
Author: rabite <rabite@posteo.de>
Date: Sat May 18 23:27:30 2019 +0200
make image/video previews optional
commit 9c2c2db2b20ecd4c30acbb1c77ad18c71f528c4c
Author: rabite <rabite@posteo.de>
Date: Sat May 18 22:13:25 2019 +0200
remove blank space under video
commit f7056d70fbe9147b3b95d7c10950653eadbb4f48
Author: rabite <rabite@posteo.de>
Date: Sat May 18 22:06:49 2019 +0200
remove dbg
commit cce820657ff1258cdb78d455dd3cf04564450cea
Author: rabite <rabite@posteo.de>
Date: Sat May 18 21:50:25 2019 +0200
added error handling to preview-gen
commit 80bbe15bacb99d1f4f97504a5d10ecf59544993f
Author: rabite <rabite@posteo.de>
Date: Sat May 18 21:04:31 2019 +0200
add time and visual seek bars to videoview
commit 4349945cc5549334f246be64bc7c8e2db43f9150
Author: rabite <rabite@posteo.de>
Date: Thu May 16 22:39:19 2019 +0200
fix sneaky process
commit 373128b7096d4de29924617e549a613a3aeea5b2
Author: rabite <rabite@posteo.de>
Date: Thu May 16 22:07:27 2019 +0200
fix out of controll processes
commit e061e065e913ab3923ffac1079a9f0a2be0df532
Author: rabite <rabite@posteo.de>
Date: Thu May 16 20:51:56 2019 +0200
add media_preview
commit f68c754895a9718d1a962980de9fbf02e5cb48cd
Author: rabite <rabite@posteo.de>
Date: Thu May 16 19:37:04 2019 +0200
beta image/video previews
commit 9a5460e553dd26ebccffaf819c64a8dc6ba74818
Author: rabite <rabite@posteo.de>
Date: Tue May 14 20:36:25 2019 +0200
external img preview
commit 3c4edfcb7611b36f6e537c73743e0b6bd269037b
Author: rabite <rabite@posteo.de>
Date: Tue May 14 20:35:12 2019 +0200
video preview
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 12 | ||||
-rw-r--r-- | src/fail.rs | 19 | ||||
-rw-r--r-- | src/file_browser.rs | 7 | ||||
-rw-r--r-- | src/files.rs | 11 | ||||
-rw-r--r-- | src/imgview.rs | 86 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/mediaview.rs | 442 | ||||
-rw-r--r-- | src/preview-gen.rs | 412 | ||||
-rw-r--r-- | src/preview.rs | 162 | ||||
-rw-r--r-- | src/widget.rs | 7 |
10 files changed, 1116 insertions, 49 deletions
diff --git a/src/config.rs b/src/config.rs index 496ce9b..8938025 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,9 @@ pub struct Config { pub show_hidden: bool, pub select_cmd: String, pub cd_cmd: String, - pub icons: bool + pub icons: bool, + pub media_autoplay: bool, + pub media_mute: bool, } @@ -18,7 +20,9 @@ impl Config { show_hidden: false, select_cmd: "find -type f | fzf -m".to_string(), cd_cmd: "find -type d | fzf".to_string(), - icons: false + icons: false, + media_autoplay: false, + media_mute: false } } @@ -47,6 +51,10 @@ impl Config { let cmd = cmd.to_string(); config.cd_cmd = cmd; } + Ok(("media_autoplay", "on")) => { config.media_autoplay = true; }, + Ok(("media_autoplay", "off")) => { config.media_autoplay = false; }, + Ok(("media_mute", "on")) => { config.media_mute = true; }, + Ok(("media_mute", "off")) => { config.media_mute = false; }, _ => { HError::config_error::<Config>(line.to_string()).log(); } } config diff --git a/src/fail.rs b/src/fail.rs index 99d98ec..0590ce5 100644 --- a/src/fail.rs +++ b/src/fail.rs @@ -97,6 +97,10 @@ pub enum HError { ConfigLineError(String), #[fail(display = "New input in Minibuffer")] MiniBufferInputUpdated(String), + #[fail(display = "Failed to parse into UTF8")] + UTF8ParseError(std::str::Utf8Error), + #[fail(display = "Failed to parse integer!")] + ParseIntError(std::num::ParseIntError), } impl HError { @@ -308,3 +312,18 @@ impl From<async_value::AError> for HError { err } } + +impl From<std::str::Utf8Error> for HError { + fn from(error: std::str::Utf8Error) -> Self { + let err = HError::UTF8ParseError(error); + err + } +} + + +impl From<std::num::ParseIntError> for HError { + fn from(error: std::num::ParseIntError) -> Self { + let err = HError::ParseIntError(error); + err + } +} diff --git a/src/file_browser.rs b/src/file_browser.rs index d76c6cb..45c99df 100644 --- a/src/file_browser.rs +++ b/src/file_browser.rs @@ -1280,7 +1280,12 @@ impl Widget for FileBrowser { Key::Char('g') => self.show_log()?, Key::Char('z') => self.run_subshell()?, Key::Char('c') => self.toggle_colums(), - _ => { self.main_widget_mut()?.on_key(key)?; }, + _ => { + let main_widget_result = self.main_widget_mut()?.on_key(key); + if let Err(HError::WidgetUndefinedKeyError{..}) = main_widget_result { + self.preview_widget_mut()?.on_key(key)?; + } + }, } if !self.columns.zoom_active { self.update_preview().log(); } Ok(()) diff --git a/src/files.rs b/src/files.rs index 17b2612..25e3393 100644 --- a/src/files.rs +++ b/src/files.rs @@ -18,6 +18,8 @@ use failure::Error; use notify::DebouncedEvent; use rayon::{ThreadPool, ThreadPoolBuilder}; use alphanumeric_sort::compare_str; +use mime_guess; + use pathbuftools::PathBufTools; use async_value::{Async, Stale}; @@ -830,9 +832,12 @@ impl File { Ok((size, unit)) } - // pub fn get_mime(&self) -> String { - // tree_magic::from_filepath(&self.path) - // } + pub fn get_mime(&self) -> Option<mime_guess::Mime> { + if let Some(ext) = self.path.extension() { + let mime = mime_guess::get_mime_type(&ext.to_string_lossy()); + Some(mime) + } else { None } + } pub fn is_text(&self) -> bool { tree_magic::match_filepath("text/plain", &self.path) diff --git a/src/imgview.rs b/src/imgview.rs new file mode 100644 index 0000000..c52abee --- /dev/null +++ b/src/imgview.rs @@ -0,0 +1,86 @@ +use crate::widget::{Widget, WidgetCore}; +use crate::fail::HResult; + +use std::path::{Path, PathBuf}; + +impl std::cmp::PartialEq for ImgView { + fn eq(&self, other: &Self) -> bool { + self.core == other.core && + self.buffer == other.buffer + } +} + +pub struct ImgView { + pub core: WidgetCore, + pub buffer: Vec<String>, + pub file: PathBuf +} + +impl ImgView { + pub fn new_from_file(core: WidgetCore, file: &Path) -> HResult<ImgView> { + let (xsize, ysize) = core.coordinates.size_u(); + + let output = std::process::Command::new("preview-gen") + .arg(format!("{}", (xsize))) + .arg(format!("{}", (ysize+1))) + .arg("image") + .arg(format!("true")) + .arg(format!("true")) + .arg(file.to_string_lossy().to_string()) + .output()? + .stdout; + + let output = std::str::from_utf8(&output)?; + let output = output.lines() + .map(|l| l.to_string()) + .collect(); + + Ok(ImgView { + core: core, + buffer: output, + file: file.to_path_buf() + }) + } + + pub fn set_image_data(&mut self, img_data: Vec<String>) { + self.buffer = img_data; + } + + pub fn lines(&self) -> usize { + self.buffer.len() + } +} + + +impl Widget for ImgView { + fn get_core(&self) -> HResult<&WidgetCore> { + Ok(&self.core) + } + + fn get_core_mut(&mut self) -> HResult<&mut WidgetCore> { + Ok(&mut self.core) + } + + fn refresh(&mut self) -> HResult<()> { + + Ok(()) + } + + fn get_drawlist(&self) -> HResult<String> { + let (xpos, ypos) = self.core.coordinates.position_u(); + + let mut draw = self.buffer + .iter() + .enumerate() + .fold(String::new(), |mut draw, (pos, line)| { + draw += &format!("{}", crate::term::goto_xy_u(xpos+1, + ypos + pos)); + draw += line; + draw + }); + + draw += &format!("{}", termion::style::Reset); + + Ok(draw) + } +} diff --git a/src/main.rs b/src/main.rs index 4df39ab..bd94046 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,8 @@ extern crate parse_ansi; extern crate signal_notify; extern crate tree_magic; extern crate systemstat; +extern crate mime_guess; + extern crate osstrtools; extern crate pathbuftools; extern crate async_value; @@ -52,6 +54,11 @@ mod config; mod stats; mod icon; +#[cfg(feature = "img")] +mod imgview; +#[cfg(feature = "video")] +mod mediaview; + diff --git a/src/mediaview.rs b/src/mediaview.rs new file mode 100644 index 0000000..ee84d69 --- /dev/null +++ b/src/mediaview.rs @@ -0,0 +1,442 @@ +use lazy_static; +use termion::event::{Event, Key}; + +use crate::widget::{Widget, WidgetCore}; +use crate::async_value::Stale; +use crate::fail::{HResult, HError, ErrorLog}; +use crate::imgview::ImgView; + +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, RwLock, + mpsc::{channel, Sender}}; + +use std::io::{BufRead, BufReader, Write}; +use std::process::Child; + +impl std::cmp::PartialEq for MediaView { + fn eq(&self, other: &Self) -> bool { + self.core == other.core + } +} + +lazy_static! { + static ref MUTE: Arc<RwLock<bool>> = Arc::new(RwLock::new(false)); + static ref AUTOPLAY: Arc<RwLock<bool>> = Arc::new(RwLock::new(true)); +} + +pub struct MediaView { + core: WidgetCore, + imgview: Arc<Mutex<ImgView>>, + file: PathBuf, + controller: Sender<String>, + paused: bool, + media_type: MediaType, + position: Arc<Mutex<usize>>, + duration: Arc<Mutex<usize>>, + stale: Stale, + process: Arc<Mutex<Option<Child>>>, + preview_runner: Option<Box<dyn FnOnce(bool, + bool, + Arc<Mutex<usize>>, + Arc<Mutex<usize>>) + -> HResult<()> + Send + 'static>> +} + +#[derive(Clone,Debug)] +pub enum MediaType { + Video, + Audio +} + +impl MediaType { + pub fn to_str(&self) -> &str { + match self { + MediaType::Video => "video", + MediaType::Audio => "audio" + } + } +} + +impl MediaView { + pub fn new_from_file(core: WidgetCore, + file: &Path, + media_type: MediaType) -> MediaView { + let (xsize, ysize) = core.coordinates.size_u(); + let (tx_cmd, rx_cmd) = channel(); + + let imgview = ImgView { + core: core.clone(), + buffer: vec![], + file: file.to_path_buf() + }; + + let imgview = Arc::new(Mutex::new(imgview)); + let thread_imgview = imgview.clone(); + + let path = file.to_string_lossy().to_string(); + let sender = core.get_sender(); + let stale = Stale::new(); + let tstale = stale.clone(); + let rx_cmd = Arc::new(Mutex::new(rx_cmd)); + let process = Arc::new(Mutex::new(None)); + let cprocess = process.clone(); + let ctype = media_type.clone(); + + + let run_preview = Box::new(move | auto, + mute, + position: Arc<Mutex<usize>>, + duration: Arc<Mutex<usize>>| -> HResult<()> { + loop { + if tstale.is_stale()? { + return Ok(()); + } + + let mut previewer = std::process::Command::new("preview-gen") + .arg(format!("{}", (xsize))) + // Leave space for position/seek bar + .arg(format!("{}", (ysize-3))) + .arg(format!("{}", ctype.to_str())) + .arg(format!("{}", auto)) + .arg(format!("{}", mute)) + .arg(&path) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .spawn()?; + + let mut stdout = BufReader::new(previewer.stdout.take()?); + let mut stdin = previewer.stdin.take()?; + + cprocess.lock().map(|mut p| *p = Some(previewer))?; + + let mut frame = vec![]; + let newline = String::from("\n"); + let mut line_buf = String::new(); + let rx_cmd = rx_cmd.clone(); + + std::thread::spawn(move || -> HResult<()> { + for cmd in rx_cmd.lock()?.iter() { + write!(stdin, "{}", cmd)?; + write!(stdin, "\n")?; + stdin.flush()?; + } + Ok(()) + }); + + loop { + // Check if preview-gen finished and break out of loop to restart + if let Ok(Some(code)) = cprocess.lock()? + .as_mut()? + .try_wait() { + if code.success() { + break; + } else { return Ok(()); } + } + + + stdout.read_line(&mut line_buf)?; + + + // Newline means frame is complete + if line_buf == newline { + line_buf.clear(); + stdout.read_line(&mut line_buf)?; + let pos = &line_buf.trim(); + *position.lock().unwrap() = pos + .parse::<usize>()?; + + + line_buf.clear(); + stdout.read_line(&mut line_buf)?; + let dur = &line_buf.trim(); + *duration.lock().unwrap() = dur + .parse::<usize>()?; + + + if let Ok(mut imgview) = thread_imgview.lock() { + imgview.set_image_data(frame); + sender.send(crate::widget::Events::WidgetReady) + .map_err(|e| HError::from(e)) + .log();; + } + + line_buf.clear(); + frame = vec![]; + continue; + } else { + frame.push(line_buf); + line_buf = String::new(); + } + } + } + }); + + + MediaView { + core: core.clone(), + imgview: imgview, + file: file.to_path_buf(), + media_type: media_type, + controller: tx_cmd, + paused: false, + position: Arc::new(Mutex::new(0)), + duration: Arc::new(Mutex::new(0)), + stale: stale, + process: process, + preview_runner: Some(run_preview) + } + } + + pub fn start_video(&mut self) -> HResult<()> { + let runner = self.preview_runner.take(); + let stale = self.stale.clone(); + let autoplay = self.autoplay(); + let mute = self.mute(); + let position = self.position.clone(); + let duration = self.duration.clone(); + + if runner.is_some() { + self.clear().log(); + std::thread::spawn(move || -> HResult<()> { + let sleeptime = std::time::Duration::from_millis(50); + std::thread::sleep(sleeptime); + + if !stale.is_stale()? { + runner.map(|runner| runner(autoplay, + mute, + position, + duration)); + } + Ok(()) + }); + } + Ok(()) + } + + pub fn play(&self) -> HResult<()> { + Ok(self.controller.send(String::from("p"))?) + } + + pub fn pause(&self) -> HResult<()> { + Ok(self.controller.send(String::from ("a"))?) + } + + pub fn progress_bar(&self) -> HResult<String> { + let xsize = self.core.coordinates.xsize_u(); + + let position = self.position.lock()?.clone(); + let duration = self.duration.lock()?.clone(); + + if duration == 0 || position == 0 { + Ok(format!("{:elements$}", "|", elements=xsize)) + } else { + let element_percent = 100 as f32 / xsize as f32; + let progress_percent = position as f32 / duration as f32 * 100 as f32; + let element_count = progress_percent as f32 / element_percent as f32; + + Ok(format!("{:|>elements$}|{: >empty$}", + "", + "", + empty=xsize - (element_count as usize + 1), + elements=element_count as usize)) + } + } + + pub fn progress_string(&self) -> HResult<String> { + let position = self.position.lock()?.clone(); + let duration = self.duration.lock()?.clone(); + + let fposition = self.format_secs(position); + let fduration = self.format_secs(duration); + + Ok(format!("{} / {}", fposition, fduration)) + } + + pub fn get_icons(&self, lines: usize) -> HResult<String> { + let (xpos, ypos) = self.core.coordinates.position_u(); + let (xsize, _) = self.core.coordinates.size_u(); + + let mute_char = "🔇"; + let pause_char = "⏸"; + let play_char = "⏴"; + + let mut icons = String::new(); + + if *MUTE.read()? == true { + icons += &crate::term::goto_xy_u(xpos+xsize-2, ypos+lines); + icons += mute_char; + } else { + // Clear the mute symbol, or it doesn't go away + icons += &crate::term::goto_xy_u(xpos+xsize-2, ypos+lines); + icons += " "; + } + + if *AUTOPLAY.read()? == true { + icons += &crate::term::goto_xy_u(xpos+xsize-4, ypos+lines); + icons += play_char; + } else { + icons += &crate::term::goto_xy_u(xpos+xsize-4, ypos+lines); + icons += pause_char; + } + + Ok(icons) + } + + pub fn format_secs(&self, secs: usize) -> String { + let hours = if secs >= 60*60 { (secs / 60) / 60 } else { 0 }; + let mins = if secs >= 60 { (secs / 60) %60 } else { 0 }; + + + format!("{:02}:{:02}:{:02}", hours, mins, secs % 60) + } + + pub fn toggle_pause(&mut self) -> HResult<()> { + let auto = AUTOPLAY.read()?.clone(); + let pos = self.position.lock()?.clone(); + + // This combination means only first frame show, since + // self.paused will be false, even with autoplay off + if pos == 0 && auto == false && self.paused == false { + self.toggle_autoplay(); + + // Since GStreamer sucks, just create a new instace + let mut view = MediaView::new_from_file(self.core.clone(), + &self.file.clone(), + self.media_type.clone()); + + // Insert buffer to prevent flicker + let buffer = self.imgview.lock()?.buffer.clone(); + view.imgview.lock()?.buffer = buffer; + + view.start_video()?; + view.paused = false; + view.play()?; + std::mem::swap(self, &mut &mut view); + + + return Ok(()) + } + if self.paused { + self.toggle_autoplay(); + self.play()?; + self.paused = false; + } else { + self.pause()?; + self.toggle_autoplay(); + self.paused = true; + } + Ok(()) + } + + pub fn quit(&self) -> HResult<()> { + Ok(self.controller.send(String::from("q"))?) + } + + pub fn seek_forward(&self) -> HResult<()> { + Ok(self.controller.send(String::from(">"))?) + } + + pub fn seek_backward(&self) -> HResult<()> { + Ok(self.controller.send(String::from("<"))?) + } + + pub fn autoplay(&self) -> bool { + if let Ok(autoplay) = AUTOPLAY.read() { + return *autoplay; + } + return true; + } + + pub fn mute(&self) -> bool { + if let Ok(mute) = MUTE.read() { + return *mute; + } + return false; + } + + pub fn toggle_autoplay(&self) { + if let Ok(mut autoplay) = AUTOPLAY.write() { + *autoplay = !*autoplay; + } + } + + pub fn toggle_mute(&self) { + if let Ok(mut mute) = MUTE.write() { + *mute = !*mute; + if *mute { + self.controller.send(String::from("m")).ok(); + } else { + self.controller.send(String::from("u")).ok(); + } + } + } + + pub fn kill(&mut self) -> HResult<()> { + let proc = self.process.clone(); + std::thread::spawn(move || -> HResult<()> { + proc.lock()? + .as_mut() + .map(|p| { + p.kill().map_err(|e| HError::from(e)).log(); + p.wait().map_err(|e| HError::from(e)).log(); + }); + Ok(()) + }); + Ok(()) + } +} + +impl Widget for MediaView { + fn get_core(&self) -> HResult<&WidgetCore> { + Ok(&self.core) + } + + fn get_core_mut(&mut self) -> HResult<&mut WidgetCore> { + Ok(&mut self.core) + } + + fn refresh(&mut self) -> HResult<()> { + self.start_video().log(); + Ok(()) + } + + fn get_drawlist(&self) -> HResult<String> { + let (xpos, ypos) = self.core.coordinates.position_u(); + let progress_str = self.progress_string()?; + let progress_bar = self.progress_bar()?; + + let (frame, lines) = self.imgview + .lock() + .map(|img| (img.get_drawlist(), img.lines()))?; + + let mut frame = frame?; + + frame += &crate::term::goto_xy_u(xpos+1, ypos+lines); + frame += &progress_str; + frame += &self.get_icons(lines)?; + frame += &crate::term::goto_xy_u(xpos+1, ypos+lines+1); + frame += &progress_bar; + + Ok(frame) + } + + fn on_key(&mut self, key: Key) -> HResult<()> { + match key { + Key::Alt('>') => self.seek_forward(), + Key::Alt('<') => self.seek_backward(), + Key::Alt('m') => self.toggle_pause(), + Key::Alt('M') => Ok(self.toggle_mute()), + _ => self.bad(Event::Key(key)) + } + } +} + +impl Drop for MediaView { + fn drop(&mut self) { + self.stale.set_stale().ok(); + self.kill().log(); + + self.clear().log(); + } +} diff --git a/src/preview-gen.rs b/src/preview-gen.rs new file mode 100644 index 0000000..6abbf80 --- /dev/null +++ b/src/preview-gen.rs @@ -0,0 +1,412 @@ +// Based on https://github.com/jD91mZM2/termplay +// MIT License + +use image::{Pixel, FilterType, DynamicImage, GenericImageView}; + +use termion::{color::{Bg, Fg, Rgb}, + input::TermRead, + event::Key}; + +#[cfg(feature = "video")] +use gstreamer::{self, prelude::*}; +#[cfg(feature = "video")] +use gstreamer_app; + +use failure::{Error, format_err}; + +use rayon::prelude::*; + +use std::io::Write; + +pub type MResult<T> = Result<T, Error>; + +fn main() -> MResult<()> { + let args = std::env::args().collect::<Vec<String>>(); + let xsize: usize = args.get(1) + .expect("Provide xsize") + .parse::<usize>() + .unwrap(); + let ysize = args.get(2) + .expect("provide ysize") + .parse() + .unwrap(); + let preview_type = args.get(3) + .expect("Provide preview type") + .parse::<String>() + .unwrap(); + let autoplay = args.get(4) + .expect("Autoplay?") + .parse::<bool>() + .unwrap(); + let mute = args.get(5) + .expect("Muted?") + .parse::<bool>() + .unwrap(); + let path = args.get(6).expect("Provide path"); + + #[cfg(feature = "video")] + let result = + match preview_type.as_ref() { + "video" => video_preview(path, xsize, ysize, autoplay, mute), + "image" => image_preview(path, xsize, ysize), + "audio" => audio_preview(path, autoplay, mute), + _ => { panic!("Available types: video/image/audio") } + }; + + + + #[cfg(not(feature = "video"))] + let result = image_preview(path, xsize, ysize); + + if result.is_err() { + println!("{:?}", &result); + result + } else { + Ok(()) + } +} + +fn image_preview(path: &str, + xsize: usize, + ysize: usize) -> MResult<()> { + let img = image::open(&path)?; + + let renderer = Renderer { + xsize, + ysize + }; + + renderer.send_image(img)?; + Ok(()) +} + +fn video_preview(path: &String, + xsize: usize, + ysize: usize, + autoplay: bool, + mute: bool) + -> MResult<()> { + let (player, appsink) = make_gstreamer()?; + + let uri = format!("file://{}", &path); + + player.set_property("uri", &uri)?; + + + let renderer = Renderer { + xsize, + ysize + }; + + let p = player.clone(); + + appsink.set_callbacks( + gstreamer_app::AppSinkCallbacks::new() + .new_sample({ + move |sink| { + let sample = match sink.pull_sample() { + Some(sample) => sample, + None => return gstreamer::FlowReturn::Eos, + }; + + let position = p.query_position::<gstreamer::ClockTime>() + .map(|p| p.seconds().unwrap_or(0)) + .unwrap_or(0); + + let duration = p.query_duration::<gstreamer::ClockTime>() + .map(|d| d.seconds().unwrap_or(0)) + .unwrap_or(0); + + match renderer.send_frame(&*sample, + position, + duration) { + Ok(()) => { + if autoplay == false { + // Just render first frame to get a static image + match p.set_state(gstreamer::State::Paused) + .into_result() { + Ok(_) => gstreamer::FlowReturn::Eos, + Err(_) => gstreamer::FlowReturn::Error + } + } else { + gstreamer::FlowReturn::Ok + } + } + Err(err) => { + println!("{:?}", err); + gstreamer::FlowReturn::Error + } + } + + } + }) + .eos({ + move |_| { + std::process::exit(0); + } + }) + .build() + ); + + if mute == true || autoplay == false { + player.set_property("volume", &0.0)?; + } + player.set_state(gstreamer::State::Playing).into_result()?; + + + read_keys(player)?; + + Ok(()) +} + +pub fn read_keys(player: gstreamer::Element) -> MResult<()> { + let seek_time = gstreamer::ClockTime::from_seconds(5); + for key in std::io::stdin().keys() { + match key { + Ok(Key::Char('q')) => std::process::exit(0), + Ok(Key::Char('>')) => { + if let Some(mut time) = player.query_position::<gstreamer::ClockTime>() { + time += seek_time; + + player.seek_simple( + gstreamer::SeekFlags::FLUSH, + gstreamer::format::GenericFormattedValue::from_time(time) + )?; + } + }, + Ok(Key::Char('<')) => { + if let Some(mut time) = player.query_position::<gstreamer::ClockTime>() { + if time >= seek_time { + time -= seek_time; + } else { + time = gstreamer::ClockTime(Some(0)); + } + + player.seek_simple( + gstreamer::SeekFlags::FLUSH, + gstreamer::format::GenericFormattedValue::from_time(time) + )?; + } + } + Ok(Key::Char('p')) => { + player.set_state(gstreamer::State::Playing).into_result()?; + + // To actually start playing again + if let Some(time) = player.query_position::<gstreamer::ClockTime>() { + player.seek_simple( + gstreamer::SeekFlags::FLUSH, + gstreamer::format::GenericFormattedValue::from_time(time) + )?; + } + } + Ok(Key::Char('a')) => { + player.set_state(gstreamer::State::Paused).into_result()?; + } + Ok(Key::Char('m')) => { + player.set_property("volume", &0.0)?; + } + Ok(Key::Char('u')) => { + player.set_property("volume", &1.0)?; + } + + + _ => {} + |