diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..c215681 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +- [Player] HLS demuxer +- [Login] Proper key storage +- [NDB] Handle PRE's +- [UX] Render non-ascii chars with better font \ No newline at end of file diff --git a/src/android.rs b/src/android.rs index 70ef139..cffbed6 100644 --- a/src/android.rs +++ b/src/android.rs @@ -1,5 +1,4 @@ use crate::app::{NativeLayerOps, ZapStreamApp}; -use crate::av_log_redirect; use eframe::Renderer; use egui::{Margin, ViewportBuilder}; use serde::de::DeserializeOwned; @@ -13,9 +12,6 @@ pub fn start_android(app: AndroidApp) { android_logger::init_once( android_logger::Config::default().with_max_level(log::LevelFilter::Info), ); - unsafe { - egui_video::ffmpeg_sys_the_third::av_log_set_callback(Some(av_log_redirect)); - } let mut options = eframe::NativeOptions::default(); options.renderer = Renderer::Glow; diff --git a/src/bin/zap_stream_app.rs b/src/bin/zap_stream_app.rs index 145c588..cb6f0a1 100644 --- a/src/bin/zap_stream_app.rs +++ b/src/bin/zap_stream_app.rs @@ -8,15 +8,11 @@ use std::ops::Deref; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zap_stream_app::app::{NativeLayerOps, ZapStreamApp}; -use zap_stream_app::av_log_redirect; #[tokio::main] async fn main() { pretty_env_logger::init(); - unsafe { - egui_video::ffmpeg_sys_the_third::av_log_set_callback(Some(av_log_redirect)); - } let mut options = eframe::NativeOptions::default(); options.viewport = ViewportBuilder::default().with_inner_size(Vec2::new(1300., 900.)); diff --git a/src/lib.rs b/src/lib.rs index de84cd1..1f317e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,47 +13,6 @@ mod widgets; #[cfg(target_os = "android")] use android_activity::AndroidApp; -use log::log; -use std::ffi::CStr; -use std::ptr; - -#[cfg(target_os = "macos")] -type VaList = egui_video::ffmpeg_sys_the_third::va_list; -#[cfg(target_os = "linux")] -type VaList = *mut egui_video::ffmpeg_sys_the_third::__va_list_tag; -#[cfg(target_os = "android")] -type VaList = [u64; 4]; - -#[no_mangle] -pub unsafe extern "C" fn av_log_redirect( - av_class: *mut libc::c_void, - level: libc::c_int, - fmt: *const libc::c_char, - args: VaList, -) { - use egui_video::ffmpeg_sys_the_third::*; - let log_level = match level { - AV_LOG_DEBUG => log::Level::Debug, - AV_LOG_WARNING => log::Level::Debug, // downgrade to debug (spammy) - AV_LOG_INFO => log::Level::Info, - AV_LOG_ERROR => log::Level::Error, - AV_LOG_PANIC => log::Level::Error, - AV_LOG_FATAL => log::Level::Error, - _ => log::Level::Trace, - }; - let mut buf: [u8; 1024] = [0; 1024]; - let mut prefix: libc::c_int = 1; - av_log_format_line( - av_class, - level, - fmt, - args, - buf.as_mut_ptr() as *mut libc::c_char, - 1024, - ptr::addr_of_mut!(prefix), - ); - log!(target: "ffmpeg", log_level, "{}", CStr::from_ptr(buf.as_ptr() as *const libc::c_char).to_str().unwrap().trim()); -} #[cfg(target_os = "android")] #[no_mangle] diff --git a/src/services/image_cache.rs b/src/services/image_cache.rs index cd728e0..5d22d0c 100644 --- a/src/services/image_cache.rs +++ b/src/services/image_cache.rs @@ -1,14 +1,14 @@ use crate::services::ffmpeg_loader::FfmpegLoader; use crate::theme::NEUTRAL_800; -use anyhow::Error; +use anyhow::{Error, Result}; use egui::{ColorImage, Context, Image, ImageData, TextureHandle, TextureOptions}; use itertools::Itertools; -use log::{error, info}; +use log::{info, warn}; use lru::LruCache; use nostr_sdk::util::hex; use resvg::usvg::Transform; use sha2::{Digest, Sha256}; -use std::collections::HashSet; +use std::collections::VecDeque; use std::fs; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -16,12 +16,15 @@ use std::sync::{Arc, Mutex}; type ImageCacheStore = Arc>>; +#[derive(PartialEq, Eq, Hash, Clone)] +struct LoadRequest(String); + pub struct ImageCache { ctx: Context, dir: PathBuf, placeholder: TextureHandle, cache: ImageCacheStore, - fetch_cache: Arc>>, + fetch_queue: Arc>>, } impl ImageCache { @@ -34,24 +37,60 @@ impl ImageCache { ImageData::from(ColorImage::new([1, 1], NEUTRAL_800)), TextureOptions::default(), ); + let cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1_000).unwrap()))); + let fetch_queue = Arc::new(Mutex::new(VecDeque::::new())); + let cc = cache.clone(); + let fq = fetch_queue.clone(); + let out_dir = out.clone(); + let ctx_clone = ctx.clone(); + let placeholder_clone = placeholder.clone(); + tokio::spawn(async move { + loop { + let next = fq.lock().unwrap().pop_front(); + if let Some(next) = next { + let path = Self::find(&out_dir, &next.0); + if path.exists() { + let th = Self::load_image_texture(&ctx_clone, path, &next.0) + .unwrap_or(placeholder_clone.clone()); + cc.lock().unwrap().put(next.0, th); + ctx_clone.request_repaint(); + } else { + match Self::download_image_to_disk(&path, &next.0).await { + Ok(()) => { + let th = Self::load_image_texture(&ctx_clone, path, &next.0) + .unwrap_or(placeholder_clone.clone()); + cc.lock().unwrap().put(next.0, th); + ctx_clone.request_repaint(); + } + Err(e) => { + warn!("Failed to download image {}: {}", next.0, e); + cc.lock().unwrap().put(next.0, placeholder_clone.clone()); + ctx_clone.request_repaint(); + } + } + } + } else { + tokio::time::sleep(std::time::Duration::from_millis(30)).await; + } + } + }); Self { ctx, dir: out, placeholder, - cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1000).unwrap()))), - fetch_cache: Arc::new(Mutex::new(HashSet::new())), + cache, + fetch_queue, } } - pub fn find(&self, url: U) -> PathBuf + pub fn find(dir: &PathBuf, url: U) -> PathBuf where U: Into, { let mut sha = Sha256::new(); sha2::digest::Update::update(&mut sha, url.into().as_bytes()); let hash = hex::encode(sha.finalize()); - self.dir - .join(PathBuf::from(hash[0..2].to_string())) + dir.join(PathBuf::from(hash[0..2].to_string())) .join(PathBuf::from(hash)) } @@ -92,49 +131,28 @@ impl ImageCache { return Image::from_texture(i); } } - let path = self.find(&u); - if !path.exists() && !u.is_empty() { - let path = path.clone(); - let cache = self.cache.clone(); - let ctx = self.ctx.clone(); - let fetch_cache = self.fetch_cache.clone(); - let placeholder = self.placeholder.clone(); - tokio::spawn(async move { - if fetch_cache.lock().unwrap().insert(u.clone()) { - info!("Fetching image: {}", &u); - if let Ok(data) = reqwest::get(&u).await { - tokio::fs::create_dir_all(path.parent().unwrap()) - .await - .unwrap(); - let img_data = data.bytes().await.unwrap(); - if let Err(e) = tokio::fs::write(path.clone(), img_data).await { - error!("Failed to write file: {}", e); - } - let t = Self::load_image(&ctx, path, &u) - .await - .unwrap_or(placeholder); - cache.lock().unwrap().put(u.clone(), t); - ctx.request_repaint(); - } - } - }); - } else if path.exists() { - let path = path.clone(); - let ctx = self.ctx.clone(); - let cache = self.cache.clone(); - let placeholder = self.placeholder.clone(); - tokio::spawn(async move { - let t = Self::load_image(&ctx, path, &u) - .await - .unwrap_or(placeholder); - cache.lock().unwrap().put(u.clone(), t); - ctx.request_repaint(); - }); + if let Ok(mut ql) = self.fetch_queue.lock() { + let lr = LoadRequest(u.clone()); + if !ql.contains(&lr) { + ql.push_back(lr); + } } Image::from_texture(&self.placeholder) } - async fn load_image(ctx: &Context, path: PathBuf, key: &str) -> Option { + /// Download an image to disk + async fn download_image_to_disk(dst: &PathBuf, u: &str) -> Result<()> { + info!("Fetching image: {}", &u); + tokio::fs::create_dir_all(dst.parent().unwrap()).await?; + + let data = reqwest::get(u).await?; + let img_data = data.bytes().await?; + tokio::fs::write(dst, img_data).await?; + Ok(()) + } + + /// Load an image from disk into an egui texture handle + fn load_image_texture(ctx: &Context, path: PathBuf, key: &str) -> Option { let loader = FfmpegLoader::new(); match loader.load_image(path) { Ok(i) => Some(ctx.load_texture(key, ImageData::from(i), TextureOptions::default())),