From 7e7d27ecff2f1b17504618dc7e42a3fddeab66a7 Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Mon, 16 Dec 2024 22:25:17 -0600 Subject: [PATCH] a lot of changes to make videos a bit more robust I still have a problem in lagging while moving the mouse though. --- Cargo.lock | 2 + Cargo.toml | 2 + src/core/kinds.rs | 4 +- src/core/mod.rs | 1 + src/core/songs.rs | 10 +-- src/core/thumbnail.rs | 123 ++++++++++++++++++++++++++++++++ src/core/videos.rs | 4 +- src/main.rs | 85 ++++++++-------------- src/ui/mod.rs | 1 + src/ui/presenter.rs | 156 +++++++++++++++++++++++++++++++++-------- src/ui/video.rs | 3 + test_presentation.lisp | 3 +- test_song.lisp | 2 +- 13 files changed, 300 insertions(+), 96 deletions(-) create mode 100644 src/core/thumbnail.rs create mode 100644 src/ui/video.rs diff --git a/Cargo.lock b/Cargo.lock index 77be14e..27dae6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3580,6 +3580,8 @@ dependencies = [ "clap", "crisp", "dirs", + "gstreamer", + "gstreamer-app", "iced_video_player", "lexpr", "libcosmic", diff --git a/Cargo.toml b/Cargo.toml index c1e8a68..6af4281 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,4 +25,6 @@ dirs = "5.0.1" tokio = "1.41.1" crisp = { git = "https://git.tfcconnection.org/chris/crisp", version = "0.1.3" } rodio = { version = "0.20.1", features = ["symphonia-all", "tracing"] } +gstreamer = "0.23.3" +gstreamer-app = "0.23.3" diff --git a/src/core/kinds.rs b/src/core/kinds.rs index 0dbabdc..b366997 100644 --- a/src/core/kinds.rs +++ b/src/core/kinds.rs @@ -5,9 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::Slide; use super::{ - images::Image, - presentations::Presentation, - songs::Song, + images::Image, presentations::Presentation, songs::Song, videos::Video, }; diff --git a/src/core/mod.rs b/src/core/mod.rs index 2fd4e11..0d33cc5 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -6,4 +6,5 @@ pub mod presentations; pub mod service_items; pub mod slide; pub mod songs; +pub mod thumbnail; pub mod videos; diff --git a/src/core/songs.rs b/src/core/songs.rs index 50b48d2..7cfddd8 100644 --- a/src/core/songs.rs +++ b/src/core/songs.rs @@ -5,8 +5,7 @@ use crisp::types::{Keyword, Symbol, Value}; use miette::{miette, IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; use sqlx::{ - query, sqlite::SqliteRow, FromRow, Row, - SqliteConnection, + query, sqlite::SqliteRow, FromRow, Row, SqliteConnection, }; use tracing::{debug, error}; @@ -256,7 +255,9 @@ pub fn lisp_to_song(list: Vec) -> Song { None }; - let first_text_postiion = list.iter().position(|v| match v { + let first_text_postiion = list + .iter() + .position(|v| match v { Value::List(inner) => { (match &inner[0] { Value::Symbol(Symbol(text)) => { @@ -272,7 +273,8 @@ pub fn lisp_to_song(list: Vec) -> Song { }) } _ => false, - }).unwrap_or(1); + }) + .unwrap_or(1); let lyric_elements = &list[first_text_postiion..]; diff --git a/src/core/thumbnail.rs b/src/core/thumbnail.rs new file mode 100644 index 0000000..dc100c5 --- /dev/null +++ b/src/core/thumbnail.rs @@ -0,0 +1,123 @@ +use dirs; +use std::error::Error; +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str; +use tracing::debug; + +pub fn bg_from_video( + video: &Path, + screenshot: &Path, +) -> Result<(), Box> { + if !screenshot.exists() { + let output_duration = Command::new("ffprobe") + .args(["-i", &video.to_string_lossy()]) + .output() + .expect("failed to execute ffprobe"); + io::stderr().write_all(&output_duration.stderr).unwrap(); + let mut at_second = 5; + let mut log = str::from_utf8(&output_duration.stderr) + .expect("Using non UTF-8 characters") + .to_string(); + debug!(log); + if let Some(duration_index) = log.find("Duration") { + let mut duration = log.split_off(duration_index + 10); + duration.truncate(11); + // debug!("rust-duration-is: {duration}"); + let mut hours = String::from(""); + let mut minutes = String::from(""); + let mut seconds = String::from(""); + for (i, c) in duration.chars().enumerate() { + if i <= 1 { + hours.push(c); + } else if i > 2 && i <= 4 { + minutes.push(c); + } else if i > 5 && i <= 7 { + seconds.push(c); + } + } + let hours: i32 = hours.parse().unwrap_or_default(); + let mut minutes: i32 = + minutes.parse().unwrap_or_default(); + let mut seconds: i32 = + seconds.parse().unwrap_or_default(); + minutes += hours * 60; + seconds += minutes * 60; + at_second = seconds / 5; + debug!(hours, minutes, seconds, at_second); + } + let _output = Command::new("ffmpeg") + .args([ + "-i", + &video.to_string_lossy(), + "-ss", + &at_second.to_string(), + "-vframes", + "1", + "-y", + &screenshot.to_string_lossy(), + ]) + .output() + .expect("failed to execute ffmpeg"); + // io::stdout().write_all(&output.stdout).unwrap(); + // io::stderr().write_all(&output.stderr).unwrap(); + } else { + debug!("Screenshot already exists"); + } + Ok(()) +} + +pub fn bg_path_from_video(video: &Path) -> PathBuf { + let video = PathBuf::from(video); + debug!(?video); + let mut data_dir = dirs::data_local_dir().unwrap(); + data_dir.push("lumina"); + data_dir.push("thumbnails"); + if !data_dir.exists() { + fs::create_dir(&data_dir) + .expect("Could not create thumbnails dir"); + } + let mut screenshot = data_dir.clone(); + screenshot.push(video.file_name().unwrap()); + screenshot.set_extension("png"); + screenshot +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_bg_video_creation() { + let video = Path::new("/home/chris/vids/moms-funeral.mp4"); + let screenshot = bg_path_from_video(video); + let screenshot_string = + screenshot.to_str().expect("Should be thing"); + assert_eq!(screenshot_string, "/home/chris/.local/share/lumina/thumbnails/moms-funeral.png"); + + // let runtime = tokio::runtime::Runtime::new().unwrap(); + let result = bg_from_video(video, &screenshot); + // let result = runtime.block_on(future); + match result { + Ok(_o) => assert!(screenshot.exists()), + Err(e) => debug_assert!( + false, + "There was an error in the runtime future. {:?}", + e + ), + } + } + + #[test] + fn test_bg_not_same() { + let video = Path::new( + "/home/chris/vids/All WebDev Sucks and you know it.webm", + ); + let screenshot = bg_path_from_video(video); + let screenshot_string = + screenshot.to_str().expect("Should be thing"); + assert_ne!(screenshot_string, "/home/chris/.local/share/lumina/thumbnails/All WebDev Sucks and you know it.webm"); + } +} diff --git a/src/core/videos.rs b/src/core/videos.rs index c41a1e1..ed5ed03 100644 --- a/src/core/videos.rs +++ b/src/core/videos.rs @@ -82,9 +82,7 @@ impl From<&Value> for Video { }) { let pos = loop_pos + 1; list.get(pos) - .map(|l| { - String::from(l) == *"true" - }) + .map(|l| String::from(l) == *"true") .unwrap_or_default() } else { false diff --git a/src/main.rs b/src/main.rs index 348ae7e..2ea0b8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,18 +4,14 @@ use cosmic::app::context_drawer::ContextDrawer; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::keyboard::{Key, Modifiers}; use cosmic::iced::window::{Mode, Position}; -use cosmic::iced::{ - self, event, window, Length, Padding, Point, -}; +use cosmic::iced::{self, event, window, Length, Padding, Point}; use cosmic::iced_futures::Subscription; use cosmic::iced_widget::{column, row}; use cosmic::prelude::ElementExt; use cosmic::prelude::*; use cosmic::widget::nav_bar::nav_bar_style; use cosmic::widget::tooltip::Position as TPosition; -use cosmic::widget::{ - button, nav_bar, text, tooltip, Space, -}; +use cosmic::widget::{button, nav_bar, text, tooltip, Space}; use cosmic::widget::{icon, slider}; use cosmic::{executor, Application, ApplicationExt, Element}; use cosmic::{widget::Container, Theme}; @@ -281,9 +277,7 @@ impl cosmic::Application for App { .class(cosmic::theme::style::Button::HeaderBar) .on_press({ if self.presentation_open { - Message::CloseWindow( - presenter_window.copied(), - ) + Message::CloseWindow(presenter_window.copied()) } else { Message::OpenWindow } @@ -384,24 +378,20 @@ impl cosmic::Application for App { presenter::Message::NextSlide, )), (Key::Character(k), _) - if k == *"j" - || k == *"l" => + if k == *"j" || k == *"l" => { self.update(Message::Present( presenter::Message::NextSlide, )) } (Key::Character(k), _) - if k == *"k" - || k == *"h" => + if k == *"k" || k == *"h" => { self.update(Message::Present( presenter::Message::PrevSlide, )) } - (Key::Character(k), _) - if k == *"q" => - { + (Key::Character(k), _) if k == *"q" => { self.update(Message::Quit) } _ => Task::none(), @@ -456,13 +446,12 @@ impl cosmic::Application for App { Message::WindowOpened(id, _) => { debug!(?id, "Window opened"); if self.cli_mode - || id > self.core.main_window_id().unwrap() + || id > self.core.main_window_id().expect("Cosmic core seems to be missing a main window, was this started in cli mode?") { self.presentation_open = true; if let Some(video) = &mut self.presenter.video { video.set_muted(false); } - warn!(self.presentation_open); window::change_mode(id, Mode::Fullscreen) } else { Task::none() @@ -500,29 +489,23 @@ impl cosmic::Application for App { let icon_left = icon::from_name("arrow-left"); let icon_right = icon::from_name("arrow-right"); - let video_range: f32 = - if let Some(video) = &self.presenter.video { - let duration = video.duration(); - duration.as_secs_f32() - } else { - 0.0 - }; + let video_range = self.presenter.video.as_ref().map_or_else( + || 0.0, + |video| video.duration().as_secs_f32(), + ); let video_button_icon = if let Some(video) = &self.presenter.video { - if video.paused() { - button::icon(icon::from_name("media-play")) - .tooltip("Play") - .on_press(Message::Present( - presenter::Message::StartVideo, - )) + let (icon_name, tooltip) = if video.paused() { + ("media-play", "Play") } else { - button::icon(icon::from_name("media-pause")) - .tooltip("Pause") - .on_press(Message::Present( - presenter::Message::StartVideo, - )) - } + ("media-pause", "Pause") + }; + button::icon(icon::from_name(icon_name)) + .tooltip(tooltip) + .on_press(Message::Present( + presenter::Message::StartVideo, + )) } else { button::icon(icon::from_name("media-play")) .tooltip("Play") @@ -534,9 +517,7 @@ impl cosmic::Application for App { let slide_preview = column![ Space::with_height(Length::Fill), Container::new( - self.presenter - .view_preview() - .map(Message::Present), + self.presenter.view_preview().map(Message::Present), ) .align_bottom(Length::Fill), Container::new(if self.presenter.video.is_some() { @@ -610,9 +591,7 @@ impl cosmic::Application for App { let column = column![ Container::new(row).center_y(Length::Fill), Container::new( - self.presenter - .preview_bar() - .map(Message::Present) + self.presenter.preview_bar().map(Message::Present) ) .clip(true) .width(Length::Fill) @@ -625,7 +604,6 @@ impl cosmic::Application for App { // View for presentation fn view_window(&self, _id: window::Id) -> Element { - debug!("window"); self.presenter.view().map(Message::Present) } } @@ -634,22 +612,21 @@ impl App where Self: cosmic::Application, { - fn active_page_title(&mut self) -> &str { - // self.nav_model - // .text(self.nav_model.active()) - // .unwrap_or("Unknown Page") - "Lumina" + fn active_page_title(&self) -> &str { + let Some(label) = + self.nav_model.text(self.nav_model.active()) + else { + return "Lumina"; + }; + label } fn update_title(&mut self) -> Task { let header_title = self.active_page_title().to_owned(); let window_title = format!("{header_title} — Lumina"); - // self.set_header_title(header_title); - if let Some(id) = self.core.main_window_id() { + self.core.main_window_id().map_or_else(Task::none, |id| { self.set_window_title(window_title, id) - } else { - Task::none() - } + }) } fn show_window(&mut self) -> Task { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 642e8f7..34f84b4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1 +1,2 @@ pub mod presenter; +pub mod video; diff --git a/src/ui/presenter.rs b/src/ui/presenter.rs index 83d6178..c5cb239 100644 --- a/src/ui/presenter.rs +++ b/src/ui/presenter.rs @@ -1,3 +1,4 @@ +use miette::{miette, IntoDiagnostic, Result}; use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc}; use cosmic::{ @@ -21,9 +22,9 @@ use cosmic::{ }, Task, }; -use iced_video_player::{Position, Video, VideoPlayer}; +use iced_video_player::{gst_pbutils, Position, Video, VideoPlayer}; use rodio::{Decoder, OutputStream, Sink}; -use tracing::{debug, error}; +use tracing::{debug, error, info, warn}; use crate::{ core::{service_items::ServiceItemModel, slide::Slide}, @@ -45,7 +46,7 @@ pub(crate) struct Presenter { current_font: Font, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub(crate) enum Message { NextSlide, PrevSlide, @@ -56,42 +57,82 @@ pub(crate) enum Message { EndAudio, VideoPos(f32), VideoFrame, + MissingPlugin(gstreamer::Message), HoveredSlide(i32), ChangeFont(String), + Error(String), None, } impl Presenter { + fn create_video(url: Url) -> Result