feat: single task image loader
This commit is contained in:
parent
93e412a07c
commit
56b73b879e
4
TODO.md
Normal file
4
TODO.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- [Player] HLS demuxer
|
||||||
|
- [Login] Proper key storage
|
||||||
|
- [NDB] Handle PRE's
|
||||||
|
- [UX] Render non-ascii chars with better font
|
@ -1,5 +1,4 @@
|
|||||||
use crate::app::{NativeLayerOps, ZapStreamApp};
|
use crate::app::{NativeLayerOps, ZapStreamApp};
|
||||||
use crate::av_log_redirect;
|
|
||||||
use eframe::Renderer;
|
use eframe::Renderer;
|
||||||
use egui::{Margin, ViewportBuilder};
|
use egui::{Margin, ViewportBuilder};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
@ -13,9 +12,6 @@ pub fn start_android(app: AndroidApp) {
|
|||||||
android_logger::init_once(
|
android_logger::init_once(
|
||||||
android_logger::Config::default().with_max_level(log::LevelFilter::Info),
|
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();
|
let mut options = eframe::NativeOptions::default();
|
||||||
options.renderer = Renderer::Glow;
|
options.renderer = Renderer::Glow;
|
||||||
|
@ -8,15 +8,11 @@ use std::ops::Deref;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use zap_stream_app::app::{NativeLayerOps, ZapStreamApp};
|
use zap_stream_app::app::{NativeLayerOps, ZapStreamApp};
|
||||||
use zap_stream_app::av_log_redirect;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
pretty_env_logger::init();
|
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();
|
let mut options = eframe::NativeOptions::default();
|
||||||
options.viewport = ViewportBuilder::default().with_inner_size(Vec2::new(1300., 900.));
|
options.viewport = ViewportBuilder::default().with_inner_size(Vec2::new(1300., 900.));
|
||||||
|
|
||||||
|
41
src/lib.rs
41
src/lib.rs
@ -13,47 +13,6 @@ mod widgets;
|
|||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
use android_activity::AndroidApp;
|
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")]
|
#[cfg(target_os = "android")]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
use crate::services::ffmpeg_loader::FfmpegLoader;
|
use crate::services::ffmpeg_loader::FfmpegLoader;
|
||||||
use crate::theme::NEUTRAL_800;
|
use crate::theme::NEUTRAL_800;
|
||||||
use anyhow::Error;
|
use anyhow::{Error, Result};
|
||||||
use egui::{ColorImage, Context, Image, ImageData, TextureHandle, TextureOptions};
|
use egui::{ColorImage, Context, Image, ImageData, TextureHandle, TextureOptions};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{error, info};
|
use log::{info, warn};
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use nostr_sdk::util::hex;
|
use nostr_sdk::util::hex;
|
||||||
use resvg::usvg::Transform;
|
use resvg::usvg::Transform;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::HashSet;
|
use std::collections::VecDeque;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@ -16,12 +16,15 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
type ImageCacheStore = Arc<Mutex<LruCache<String, TextureHandle>>>;
|
type ImageCacheStore = Arc<Mutex<LruCache<String, TextureHandle>>>;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
|
struct LoadRequest(String);
|
||||||
|
|
||||||
pub struct ImageCache {
|
pub struct ImageCache {
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
placeholder: TextureHandle,
|
placeholder: TextureHandle,
|
||||||
cache: ImageCacheStore,
|
cache: ImageCacheStore,
|
||||||
fetch_cache: Arc<Mutex<HashSet<String>>>,
|
fetch_queue: Arc<Mutex<VecDeque<LoadRequest>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageCache {
|
impl ImageCache {
|
||||||
@ -34,24 +37,60 @@ impl ImageCache {
|
|||||||
ImageData::from(ColorImage::new([1, 1], NEUTRAL_800)),
|
ImageData::from(ColorImage::new([1, 1], NEUTRAL_800)),
|
||||||
TextureOptions::default(),
|
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 {
|
Self {
|
||||||
ctx,
|
ctx,
|
||||||
dir: out,
|
dir: out,
|
||||||
placeholder,
|
placeholder,
|
||||||
cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1000).unwrap()))),
|
cache,
|
||||||
fetch_cache: Arc::new(Mutex::new(HashSet::new())),
|
fetch_queue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find<U>(&self, url: U) -> PathBuf
|
pub fn find<U>(dir: &PathBuf, url: U) -> PathBuf
|
||||||
where
|
where
|
||||||
U: Into<String>,
|
U: Into<String>,
|
||||||
{
|
{
|
||||||
let mut sha = Sha256::new();
|
let mut sha = Sha256::new();
|
||||||
sha2::digest::Update::update(&mut sha, url.into().as_bytes());
|
sha2::digest::Update::update(&mut sha, url.into().as_bytes());
|
||||||
let hash = hex::encode(sha.finalize());
|
let hash = hex::encode(sha.finalize());
|
||||||
self.dir
|
dir.join(PathBuf::from(hash[0..2].to_string()))
|
||||||
.join(PathBuf::from(hash[0..2].to_string()))
|
|
||||||
.join(PathBuf::from(hash))
|
.join(PathBuf::from(hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,49 +131,28 @@ impl ImageCache {
|
|||||||
return Image::from_texture(i);
|
return Image::from_texture(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let path = self.find(&u);
|
if let Ok(mut ql) = self.fetch_queue.lock() {
|
||||||
if !path.exists() && !u.is_empty() {
|
let lr = LoadRequest(u.clone());
|
||||||
let path = path.clone();
|
if !ql.contains(&lr) {
|
||||||
let cache = self.cache.clone();
|
ql.push_back(lr);
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Image::from_texture(&self.placeholder)
|
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();
|
let loader = FfmpegLoader::new();
|
||||||
match loader.load_image(path) {
|
match loader.load_image(path) {
|
||||||
Ok(i) => Some(ctx.load_texture(key, ImageData::from(i), TextureOptions::default())),
|
Ok(i) => Some(ctx.load_texture(key, ImageData::from(i), TextureOptions::default())),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user