diff --git a/flake.lock b/flake.lock index 18679db..8c45108 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1747587869, - "narHash": "sha256-Zay3WJdSvC2VQmNqWSVLBOg/1iS/0/Q0c9JOBsB+3qw=", + "lastModified": 1746291859, + "narHash": "sha256-DdWJLA+D5tcmrRSg5Y7tp/qWaD05ATI4Z7h22gd1h7Q=", "owner": "ipetkov", "repo": "crane", - "rev": "76603d32f18e0e378d9f6335c8fc286413493655", + "rev": "dfd9a8dfd09db9aad544c4d3b6c47b12562544a5", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1747542820, - "narHash": "sha256-GaOZntlJ6gPPbbkTLjbd8BMWaDYafhuuYRNrxCGnPJw=", + "lastModified": 1746461020, + "narHash": "sha256-7+pG1I9jvxNlmln4YgnlW4o+w0TZX24k688mibiFDUE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "292fa7d4f6519c074f0a50394dbbe69859bb6043", + "rev": "3730d8a308f94996a9ba7c7138ede69c1b9ac4ae", "type": "github" }, "original": { diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index 65f12c9..0c3e036 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -3,8 +3,7 @@ use std::{ process::Stdio, }; -use color_eyre::eyre::{ContextCompat, Result, bail}; -use futures::channel::oneshot; +use color_eyre::eyre::{ContextCompat, Result}; use relm4::{ Sender, tokio::{ @@ -23,10 +22,9 @@ struct FfprobeOut { } #[derive(serde::Deserialize)] -#[serde(tag = "codec_type", rename_all = "lowercase")] -enum StreamInfo { - Video { width: usize, height: usize }, - Audio, +struct StreamInfo { + width: usize, + height: usize, } #[derive(serde::Deserialize)] @@ -38,7 +36,6 @@ struct FormatInfo { pub struct VideoMeta { pub width: usize, pub height: usize, - pub audio_streams: usize, pub duration_us: usize, } @@ -64,8 +61,10 @@ pub async fn get_video_meta(path: &Path) -> Result { .args([ "-v", "error", + "-select_streams", + "v:0", "-show_entries", - "stream=width,height,codec_type : format=duration", + "stream=width,height : format=duration", "-of", "json", ]) @@ -77,28 +76,17 @@ pub async fn get_video_meta(path: &Path) -> Result { let str = String::from_utf8(output.stdout)?; let output: FfprobeOut = serde_json::from_str(&str)?; - let (width, height) = output + let stream = output .streams - .iter() - .find_map(|s| match s { - StreamInfo::Video { width, height } => Some((*width, *height)), - _ => None, - }) + .first() .wrap_err("could not get stream information from ffprobe")?; - let audio_streams = output - .streams - .iter() - .filter(|s| matches!(s, StreamInfo::Audio)) - .count(); - let duration_sec = output.format.duration.parse::()?; let duration_us = (duration_sec * 1_000_000.0).ceil() as usize; Ok(VideoMeta { - width, - height, - audio_streams, + width: stream.width, + height: stream.height, duration_us, }) } @@ -110,22 +98,14 @@ pub async fn convert_video( merge_tracks: bool, sender: Sender, ) -> Result { - let out_filename = out_filename - .or_else(|| { - path.file_name() - .and_then(|s| s.to_str()) - .map(|s| s.to_owned()) - }) - .unwrap_or("out.webm".into()); - - let mut out_path = std::env::temp_dir().join(out_filename); - out_path.set_extension("webm"); + let out_path = PathBuf::from("/tmp/out.webm"); let codec_args: &[&str] = match out_codec { Codec::AV1 => &["-c:v", "libsvtav1"], Codec::VP9 => &["-c:v", "libvpx-vp9", "-row-mt", "1"], }; + // TODO: maybe check if the video has 2 audio tracks? or at least use a "fail-safe" method let merge_args: &[&str] = if merge_tracks { &["-ac", "2", "-filter_complex", "amerge=inputs=2"] } else { @@ -138,7 +118,7 @@ pub async fn convert_video( .args(["-c:a", "libopus", "-b:a", "96k"]) .args(codec_args) .args(merge_args) - .args(["-y", "-loglevel", "error", "-progress", "-", "-nostats"]) + .args(["-loglevel", "error", "-progress", "-", "-nostats"]) .arg(&out_path) .stdout(Stdio::piped()) .spawn()?; @@ -147,10 +127,11 @@ pub async fn convert_video( let mut reader = BufReader::new(stdout).lines(); // make sure the process is actually started and awaited - let (tx, rx) = oneshot::channel(); tokio::spawn(async move { - tx.send(child.wait().await) - .expect("could not send exit status"); + child + .wait() + .await + .expect("ffmpeg process encountered an error"); }); while let Some(line) = reader.next_line().await? { @@ -167,10 +148,5 @@ pub async fn convert_video( } } - let status = rx.await??; - if !status.success() { - bail!("ffmpeg process errored: {status}"); - } - Ok(out_path) } diff --git a/src/main.rs b/src/main.rs index b8926dd..fb0be26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,6 @@ use relm4::{ Component, ComponentController, Controller, RelmApp, Sender, adw::{self, prelude::*}, gtk::{self, gio}, - loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentParts}, tokio, }; @@ -95,7 +94,6 @@ struct Tyrolienne { step: Step, progress: usize, total: usize, - can_merge: bool, video_path: Option, out_filename: Option, out_codec: ffmpeg::Codec, @@ -118,7 +116,7 @@ impl Tyrolienne { config: self.config.clone(), out_filename: self.out_filename.clone(), out_codec: self.out_codec, - merge_tracks: self.can_merge && self.merge_tracks, + merge_tracks: self.merge_tracks, video_path: self.video_path.clone(), folder: self.folder.clone(), } @@ -192,10 +190,7 @@ impl AsyncComponent for Tyrolienne { adw::SwitchRow { set_title: "Merge audio tracks", - #[watch] - set_sensitive: model.can_merge, - #[watch] - set_active: model.can_merge && model.merge_tracks, + set_active: true, connect_active_notify[sender] => move |s| sender.input(Message::SetMergeTracks(s.is_active())), }, @@ -251,7 +246,6 @@ impl AsyncComponent for Tyrolienne { step: Step::Waiting, progress: 0, total: 1, - can_merge: false, video_path: None, out_filename: None, out_codec: ffmpeg::Codec::VP9, @@ -265,6 +259,8 @@ impl AsyncComponent for Tyrolienne { .forward(sender.input_sender(), |_| Message::Nothing), }; + // TODO consider using the "loading" (?) mechanism from relm4 + // https://relm4.org/book/stable/threads_and_async/async.html let folders = match zipline::get_folders(&model.config).await { Ok(v) => v, Err(e) => { @@ -295,21 +291,6 @@ impl AsyncComponent for Tyrolienne { AsyncComponentParts { model, widgets } } - fn init_loading_widgets(root: Self::Root) -> Option { - relm4::view! { - #[local] - root { - #[name(spinner)] - gtk::Spinner { - start: (), - set_halign: gtk::Align::Center, - } - } - } - - Some(LoadingWidgets::new(root, spinner)) - } - async fn update( &mut self, message: Self::Input, @@ -325,8 +306,6 @@ impl AsyncComponent for Tyrolienne { .await; if let Some(file) = file { - let meta = ffmpeg::get_video_meta(file.path()).await; - self.can_merge = meta.map(|m| m.audio_streams == 2).unwrap_or(false); self.video_path = Some(file.path().to_owned()); } } @@ -374,7 +353,9 @@ impl AsyncComponent for Tyrolienne { self.step = Step::Waiting; self.progress = 0; self.total = 1; - self.toast.emit(ToastInput::ShowAndCopy(url)); + + // TODO copy to clipboard here instead, the toast disappears after a short while + self.toast.emit(ToastInput::Show(url)); } ProgressMessage::Error(e) => { self.step = Step::Waiting; @@ -478,6 +459,7 @@ async fn the_process(app: UploadInfo, sender: &Sender) -> Resul .thumbnail_url(&app.config) .ok_or_eyre("could not get thumbnail url")?; + // TODO get w&h from video Ok(format!( "https://autocompressor.net/av1?v={}&i={}&w={}&h={}", Encoded(&zp_file.url), diff --git a/src/relm.rs b/src/relm.rs index 0b0c578..4a230f4 100644 --- a/src/relm.rs +++ b/src/relm.rs @@ -1,4 +1,3 @@ -use arboard::Clipboard; use relm4::{ SimpleComponent, adw::{self, prelude::*}, @@ -92,7 +91,8 @@ pub struct Toast { #[derive(Debug)] pub enum ToastInput { - ShowAndCopy(String), + Show(String), + Copy, Dismiss, } @@ -107,6 +107,8 @@ impl SimpleComponent for Toast { adw::Toast { #[watch] set_title: &model.text, + set_button_label: Some("Copy"), + connect_button_clicked => ToastInput::Copy, connect_dismissed => ToastInput::Dismiss, } } @@ -128,14 +130,18 @@ impl SimpleComponent for Toast { fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender) { match message { - ToastInput::ShowAndCopy(text) => { + ToastInput::Show(text) => { self.visible = true; - - if let Err(e) = Clipboard::new().and_then(|mut c| c.set_text(text)) { + self.text = text; + } + ToastInput::Copy => { + if let Err(e) = + arboard::Clipboard::new().and_then(|mut c| c.set_text(self.text.clone())) + { tracing::error!("could not copy url to clipboard: {e}"); self.text = "Could not copy".into(); } else { - self.text = "Copied url to clipboard".into(); + self.visible = false; } } ToastInput::Dismiss => self.visible = false,