feat: single task image loader

This commit is contained in:
kieran 2024-11-20 10:43:29 +00:00
parent 93e412a07c
commit 56b73b879e
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
5 changed files with 70 additions and 97 deletions

4
TODO.md Normal file
View File

@ -0,0 +1,4 @@
- [Player] HLS demuxer
- [Login] Proper key storage
- [NDB] Handle PRE's
- [UX] Render non-ascii chars with better font

View File

@ -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;

View File

@ -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.));

View File

@ -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]

View File

@ -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<Mutex<LruCache<String, TextureHandle>>>;
#[derive(PartialEq, Eq, Hash, Clone)]
struct LoadRequest(String);
pub struct ImageCache {
ctx: Context,
dir: PathBuf,
placeholder: TextureHandle,
cache: ImageCacheStore,
fetch_cache: Arc<Mutex<HashSet<String>>>,
fetch_queue: Arc<Mutex<VecDeque<LoadRequest>>>,
}
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::<LoadRequest>::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<U>(&self, url: U) -> PathBuf
pub fn find<U>(dir: &PathBuf, url: U) -> PathBuf
where
U: Into<String>,
{
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);
if let Ok(mut ql) = self.fetch_queue.lock() {
let lr = LoadRequest(u.clone());
if !ql.contains(&lr) {
ql.push_back(lr);
}
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();
});
}
Image::from_texture(&self.placeholder)
}
async fn load_image(ctx: &Context, path: PathBuf, key: &str) -> Option<TextureHandle> {
/// 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<TextureHandle> {
let loader = FfmpegLoader::new();
match loader.load_image(path) {
Ok(i) => Some(ctx.load_texture(key, ImageData::from(i), TextureOptions::default())),