diff --git a/Cargo.lock b/Cargo.lock index 73c5ecc..33262a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1280,7 +1280,7 @@ dependencies = [ [[package]] name = "flex" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "chrono", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "flex-anilist" -version = "0.17.0" +version = "0.18.0" dependencies = [ "flex-plugin", "ureq", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "flex-core" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "directories", @@ -1337,7 +1337,7 @@ dependencies = [ [[package]] name = "flex-lotus-lamp" -version = "0.17.0" +version = "0.18.0" dependencies = [ "flex-plugin", "lotus-lamp", @@ -1346,7 +1346,7 @@ dependencies = [ [[package]] name = "flex-plugin" -version = "0.17.0" +version = "0.18.0" dependencies = [ "serde", "serde_json", @@ -1354,7 +1354,7 @@ dependencies = [ [[package]] name = "flex-trakt" -version = "0.17.0" +version = "0.18.0" dependencies = [ "flex-plugin", "ureq", @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "flex-web" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index c990c98..2b6d4f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ default-members = ["crates/*"] [workspace.package] edition = "2024" -version = "0.17.0" +version = "0.18.0" license = "MIT" [profile.release] diff --git a/README.md b/README.md index 7af08cc..6543737 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,8 @@ Videos take a few seconds to start, since yt-dlp has to ask YouTube for them fir Flex starts on whichever video you have highlighted, so it's usually ready by the time you press OK. Hold OK on a video to mark it watched, see related videos or jump to its channel. Settings → -YouTube also has the quality limit (1080p unless you change it), whether to show Shorts +YouTube also has the quality limit (1080p unless you change it), an H.264 option for +machines that struggle to decode VP9, whether to show Shorts (hidden by default) and which SponsorBlock categories to skip (sponsors, self-promotion and "like and subscribe" reminders by default). diff --git a/crates/flex-app/Cargo.toml b/crates/flex-app/Cargo.toml index e72e5c5..48512e6 100644 --- a/crates/flex-app/Cargo.toml +++ b/crates/flex-app/Cargo.toml @@ -9,9 +9,9 @@ anyhow = "1.0.104" chrono = { version = "0.4.45", default-features = false, features = ["clock"] } eframe = { version = "0.36", default-features = false, features = ["glow", "default_fonts"] } env_logger = "0.11.11" -flex-core = { version = "0.17.0", path = "../flex-core" } -flex-plugin = { version = "0.17.0", path = "../flex-plugin" } -flex-web = { version = "0.17.0", path = "../flex-web" } +flex-core = { version = "0.18.0", path = "../flex-core" } +flex-plugin = { version = "0.18.0", path = "../flex-plugin" } +flex-web = { version = "0.18.0", path = "../flex-web" } image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } libloading = "0.9" log = "0.4.34" diff --git a/crates/flex-app/src/mpv/mod.rs b/crates/flex-app/src/mpv/mod.rs index b602f96..42440d7 100644 --- a/crates/flex-app/src/mpv/mod.rs +++ b/crates/flex-app/src/mpv/mod.rs @@ -379,7 +379,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let tools = flex_core::youtube::Tools::find().expect("yt-dlp in the tools folder or on the PATH"); let started = Instant::now(); - let video = tools.resolve("aqz-KE-bpKQ", 1080).unwrap(); + let video = tools.resolve("aqz-KE-bpKQ", 1080, false).unwrap(); eprintln!("resolved in {:?}: {} ({:?}p)", started.elapsed(), video.title, video.height); let mpv = Mpv::headless(&[("vo", "null"), ("ao", "null"), ("hwdec", "no")]).unwrap(); diff --git a/crates/flex-app/src/screens/youtube.rs b/crates/flex-app/src/screens/youtube.rs index 503d9ae..e7ff625 100644 --- a/crates/flex-app/src/screens/youtube.rs +++ b/crates/flex-app/src/screens/youtube.rs @@ -793,7 +793,7 @@ impl Screen for YouTubeSettings { if press.is(Action::Back) { return Nav::Pop; } - match self.list.handle(press, 7) { + match self.list.handle(press, 8) { ListInput::AtTop => Nav::FocusTopBar, ListInput::Activate(0) => { let on = !sh.youtube_enabled(); @@ -815,17 +815,22 @@ impl Screen for YouTubeSettings { Nav::None } ListInput::Activate(3) => { + let on = !sh.youtube_h264(); + sh.set_youtube_h264(on); + Nav::None + } + ListInput::Activate(4) => { let on = !sh.youtube_shorts(); sh.set_youtube_shorts(on); Nav::None } - ListInput::Activate(4) => { + ListInput::Activate(5) => { let current = sh.youtube_continue_limit(); let i = CONTINUE_LIMITS.iter().position(|n| *n == current).unwrap_or(3); sh.set_youtube_continue_limit(CONTINUE_LIMITS[(i + 1) % CONTINUE_LIMITS.len()]); Nav::None } - ListInput::Activate(5) => Nav::push(SponsorBlockSettings::default()), + ListInput::Activate(6) => Nav::push(SponsorBlockSettings::default()), ListInput::Activate(_) => Nav::push(ChannelList::default()), ListInput::None => Nav::None, } @@ -847,6 +852,8 @@ impl Screen for YouTubeSettings { Row::new(Icon::None, format!("YouTube: {}", if on { "on" } else { "off" })).detail("Adds YouTube to Home"), Row::new(Icon::Refresh, tools).detail("OK to update now (they also update daily)"), Row::new(Icon::None, format!("Quality: up to {}p", sh.youtube_quality())).detail("OK to change"), + Row::new(Icon::None, format!("Codec: {}", if sh.youtube_h264() { "H.264 when available" } else { "any (VP9 or H.264)" })) + .detail("H.264 is easier on slow machines · OK to change"), Row::new(Icon::None, format!("Shorts: {}", if sh.youtube_shorts() { "shown" } else { "hidden" })).detail("OK to change"), Row::new(Icon::None, match sh.youtube_continue_limit() { 0 => "Continue Watching: no YouTube videos".to_owned(), diff --git a/crates/flex-app/src/shared.rs b/crates/flex-app/src/shared.rs index 8c69193..ac20891 100644 --- a/crates/flex-app/src/shared.rs +++ b/crates/flex-app/src/shared.rs @@ -1641,6 +1641,17 @@ impl Shared { pub fn set_youtube_quality(&mut self, height: u32) { self.set_setting(crate::youtube::QUALITY_SETTING, &height.to_string()); + self.youtube.forget_streams(); + } + + /// Whether to prefer H.264 videos (easier to decode than VP9). + pub fn youtube_h264(&self) -> bool { + self.setting_bool(crate::youtube::H264_SETTING) + } + + pub fn set_youtube_h264(&mut self, on: bool) { + self.set_setting_bool(crate::youtube::H264_SETTING, on); + self.youtube.forget_streams(); } pub fn youtube_shorts(&self) -> bool { @@ -1671,8 +1682,8 @@ impl Shared { /// Starts finding a video's streams (and what to skip) ahead of playing it. pub fn prepare_youtube(&mut self, id: &str) { - let (quality, categories) = (self.youtube_quality(), self.sponsorblock_categories()); - self.youtube.prepare(id, quality, categories); + let (quality, h264, categories) = (self.youtube_quality(), self.youtube_h264(), self.sponsorblock_categories()); + self.youtube.prepare(id, quality, h264, categories); } pub fn yt_channels(&self) -> Vec { diff --git a/crates/flex-app/src/youtube.rs b/crates/flex-app/src/youtube.rs index 75819d1..500fae8 100644 --- a/crates/flex-app/src/youtube.rs +++ b/crates/flex-app/src/youtube.rs @@ -15,6 +15,8 @@ use flex_core::youtube::{Tools, Video}; pub const ENABLED_SETTING: &str = "youtube"; pub const QUALITY_SETTING: &str = "youtube_quality"; +/// Whether to prefer H.264, which slow machines decode more easily than VP9. +pub const H264_SETTING: &str = "youtube_h264"; pub const SHORTS_SETTING: &str = "youtube_shorts"; pub const SPONSORBLOCK_SETTING: &str = "youtube_sponsorblock"; pub const CONTINUE_SETTING: &str = "youtube_continue"; @@ -115,7 +117,7 @@ impl YouTube { /// Starts finding a video's streams and SponsorBlock segments, unless that's done or /// under way. Called when a video is highlighted, so it's usually ready by OK. - pub fn prepare(&mut self, id: &str, max_height: u32, categories: Vec) { + pub fn prepare(&mut self, id: &str, max_height: u32, h264: bool, categories: Vec) { let fresh = self.resolved.get(id).is_some_and(|(at, r)| r.is_ok() && at.elapsed() < RESOLVED_FOR); if fresh || self.resolving.contains(id) { return; @@ -125,7 +127,7 @@ impl YouTube { self.resolving.insert(id.to_owned()); let video_id = id.to_owned(); self.spawn(move |tx| { - let mut result = tools.resolve(&video_id, max_height); + let mut result = tools.resolve(&video_id, max_height, h264); if let Err(e) = &result { // YouTube may have changed something that a newer yt-dlp handles. log::warn!("youtube: {video_id}: {e:#}; checking for a newer yt-dlp"); @@ -133,7 +135,7 @@ impl YouTube { && report.updated.iter().any(|(t, _)| *t == Tool::YtDlp) && let Some(tools) = Tools::find() { - result = tools.resolve(&video_id, max_height); + result = tools.resolve(&video_id, max_height, h264); } } let _ = tx.send(Msg::Resolved(video_id, result.map_err(|e| format!("{e:#}")))); @@ -155,6 +157,11 @@ impl YouTube { self.resolved.get(id).map(|(_, r)| r) } + /// Forgets every video's streams, after the quality or codec setting changes. + pub fn forget_streams(&mut self) { + self.resolved.clear(); + } + /// Forgets a failed attempt so playing again tries again. pub fn forget(&mut self, id: &str) { if self.resolved.get(id).is_some_and(|(_, r)| r.is_err()) { diff --git a/crates/flex-core/Cargo.toml b/crates/flex-core/Cargo.toml index dbd1203..996eca9 100644 --- a/crates/flex-core/Cargo.toml +++ b/crates/flex-core/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] anyhow = "1.0.104" directories = "6.0.0" -flex-plugin = { version = "0.17.0", path = "../flex-plugin" } +flex-plugin = { version = "0.18.0", path = "../flex-plugin" } hunch = "2" lofty = "0.25.4" log = "0.4.34" diff --git a/crates/flex-core/src/youtube/mod.rs b/crates/flex-core/src/youtube/mod.rs index 2b03fa0..7d9b07f 100644 --- a/crates/flex-core/src/youtube/mod.rs +++ b/crates/flex-core/src/youtube/mod.rs @@ -72,15 +72,16 @@ impl Tools { } /// Finds the streams for a video (a URL or an 11-character id), at most `max_height` - /// pixels tall. Takes a few seconds (it asks YouTube). - pub fn resolve(&self, video: &str, max_height: u32) -> Result