feat: defer image load

This commit is contained in:
Kieran 2024-11-06 20:19:25 +00:00
parent 10cd15d942
commit 621686564d
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
15 changed files with 55 additions and 54 deletions

8
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "ab_glyph" name = "ab_glyph"
@ -1379,7 +1379,7 @@ dependencies = [
[[package]] [[package]]
name = "egui-video" name = "egui-video"
version = "0.8.0" version = "0.8.0"
source = "git+https://github.com/v0l/egui-video.git?rev=e01405c9771e523a04ba36ffc5a08876ac0b274a#e01405c9771e523a04ba36ffc5a08876ac0b274a" source = "git+https://github.com/v0l/egui-video.git?rev=bb3f6b83ba3a0619b1b9de0d4da88acb7fafd257#bb3f6b83ba3a0619b1b9de0d4da88acb7fafd257"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atomic", "atomic",
@ -1389,7 +1389,6 @@ dependencies = [
"ffmpeg-rs-raw", "ffmpeg-rs-raw",
"log", "log",
"nom", "nom",
"url",
] ]
[[package]] [[package]]
@ -1605,11 +1604,12 @@ dependencies = [
[[package]] [[package]]
name = "ffmpeg-rs-raw" name = "ffmpeg-rs-raw"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.v0l.io/Kieran/ffmpeg-rs-raw.git?rev=bc39a2ad99ad6489fa1525b9ad4fbca5dbf9cd07#bc39a2ad99ad6489fa1525b9ad4fbca5dbf9cd07" source = "git+https://git.v0l.io/Kieran/ffmpeg-rs-raw.git?rev=8b6166f1db18ffb322a5a634d6b6deaddb79ecbf#8b6166f1db18ffb322a5a634d6b6deaddb79ecbf"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ffmpeg-sys-the-third", "ffmpeg-sys-the-third",
"libc", "libc",
"log",
"slimbox", "slimbox",
] ]

View File

@ -28,7 +28,7 @@ resvg = { version = "0.44.0", default-features = false }
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1.0.214", features = ["derive"] }
serde_with = { version = "3.11.0", features = ["hex"] } serde_with = { version = "3.11.0", features = ["hex"] }
egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "e01405c9771e523a04ba36ffc5a08876ac0b274a" } egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "bb3f6b83ba3a0619b1b9de0d4da88acb7fafd257" }
#egui-video = { path = "../egui-video" } #egui-video = { path = "../egui-video" }
[target.'cfg(not(target_os = "android"))'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies]

View File

@ -1,6 +1,6 @@
use crate::app::NativeLayerOps; use crate::app::NativeLayerOps;
use crate::link::NostrLink; use crate::link::NostrLink;
use crate::login::{Login, LoginKind}; use crate::login::Login;
use crate::note_util::OwnedNote; use crate::note_util::OwnedNote;
use crate::route::home::HomePage; use crate::route::home::HomePage;
use crate::route::login::LoginPage; use crate::route::login::LoginPage;

View File

@ -1,6 +1,6 @@
use anyhow::Error; use anyhow::Error;
use egui::ColorImage; use egui::ColorImage;
use egui_video::ffmpeg_rs_raw::{Decoder, Demuxer, Scaler}; use egui_video::ffmpeg_rs_raw::{get_frame_from_hw, Decoder, Demuxer, Scaler};
use egui_video::ffmpeg_sys_the_third::{av_frame_free, av_packet_free, AVPixelFormat}; use egui_video::ffmpeg_sys_the_third::{av_frame_free, av_packet_free, AVPixelFormat};
use egui_video::media_player::video_frame_to_image; use egui_video::media_player::video_frame_to_image;
use std::path::PathBuf; use std::path::PathBuf;
@ -36,25 +36,23 @@ impl FfmpegLoader {
anyhow::bail!("Not a video/image"); anyhow::bail!("Not a video/image");
}; };
let mut decode = Decoder::new(); let mut decode = Decoder::new();
let rgb = AVPixelFormat::AV_PIX_FMT_RGB24; let rgb = AVPixelFormat::AV_PIX_FMT_RGBA;
let mut scaler = Scaler::new(rgb); let mut scaler = Scaler::new();
decode.setup_decoder(bv, None)?; decode.setup_decoder(bv, None)?;
let mut n_pkt = 0; let mut n_pkt = 0;
loop { loop {
let (mut pkt, stream) = demuxer.get_packet()?; let (mut pkt, stream) = demuxer.get_packet()?;
if pkt.is_null() {
break;
}
if (*stream).index as usize == bv.index { if (*stream).index as usize == bv.index {
let frames = decode.decode_pkt(pkt, stream)?; let frames = decode.decode_pkt(pkt, stream)?;
if let Some((frame, _)) = frames.first() { if let Some((frame, _)) = frames.first() {
let mut frame = *frame; let mut frame = get_frame_from_hw(*frame)?;
let frame_rgb = scaler.process_frame( let frame_rgb = scaler.process_frame(
frame, frame,
(*frame).width as u16, (*frame).width as u16,
(*frame).height as u16, (*frame).height as u16,
rgb,
)?; )?;
av_frame_free(&mut frame); av_frame_free(&mut frame);

View File

@ -1,9 +1,7 @@
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;
use eframe::epaint::Color32; use egui::{ColorImage, Context, Image, ImageData, TextureHandle, TextureOptions};
use egui::load::SizedTexture;
use egui::{ColorImage, Context, Image, ImageData, SizeHint, TextureHandle, TextureOptions};
use itertools::Itertools; use itertools::Itertools;
use log::{error, info}; use log::{error, info};
use lru::LruCache; use lru::LruCache;
@ -13,7 +11,6 @@ use sha2::{Digest, Sha256};
use std::collections::HashSet; use std::collections::HashSet;
use std::fs; use std::fs;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -62,7 +59,7 @@ impl ImageCache {
if url.ends_with(".svg") { if url.ends_with(".svg") {
Self::load_svg(bytes) Self::load_svg(bytes)
} else { } else {
let mut loader = FfmpegLoader::new(); let loader = FfmpegLoader::new();
loader.load_image_bytes(url, bytes) loader.load_image_bytes(url, bytes)
} }
} }
@ -138,7 +135,7 @@ impl ImageCache {
} }
async fn load_image(ctx: &Context, path: PathBuf, key: &str) -> Option<TextureHandle> { async fn load_image(ctx: &Context, path: PathBuf, key: &str) -> Option<TextureHandle> {
let mut 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())),
Err(e) => { Err(e) => {
@ -153,7 +150,7 @@ impl ImageCache {
use resvg::usvg::{Options, Tree}; use resvg::usvg::{Options, Tree};
let opt = Options::default(); let opt = Options::default();
let mut rtree = Tree::from_data(svg, &opt) let rtree = Tree::from_data(svg, &opt)
.map_err(|err| err.to_string()) .map_err(|err| err.to_string())
.map_err(|e| Error::msg(e))?; .map_err(|e| Error::msg(e))?;

View File

@ -1,47 +1,42 @@
use crate::route::RouteServices; use crate::route::RouteServices;
use crate::services::image_cache::ImageCache;
use crate::services::ndb_wrapper::SubWrapper; use crate::services::ndb_wrapper::SubWrapper;
use egui::{vec2, Color32, Image, Pos2, Response, Rounding, Sense, Ui, Vec2, Widget}; use egui::{vec2, Color32, Pos2, Response, Rounding, Sense, Ui, Vec2, Widget};
use nostrdb::NdbProfile; use nostrdb::NdbProfile;
pub struct Avatar<'a> { pub struct Avatar<'a> {
image: Option<Image<'a>>, image: Option<&'a str>,
sub: Option<SubWrapper>, sub: Option<SubWrapper>,
size: Option<f32>, size: Option<f32>,
services: &'a RouteServices<'a>,
} }
impl<'a> Avatar<'a> { impl<'a> Avatar<'a> {
pub fn new(img: Image<'a>) -> Self { pub fn new_optional(img: Option<&'a str>, services: &'a RouteServices<'a>) -> Self {
Self {
image: Some(img),
sub: None,
size: None,
}
}
pub fn new_optional(img: Option<Image<'a>>) -> Self {
Self { Self {
image: img, image: img,
sub: None, sub: None,
size: None, size: None,
services,
} }
} }
pub fn from_profile(p: &'a Option<NdbProfile<'a>>, svc: &'a ImageCache) -> Self { pub fn from_profile(p: &'a Option<NdbProfile<'a>>, services: &'a RouteServices<'a>) -> Self {
let img = p.map_or(None, |f| f.picture().map(|f| svc.load(f))); let img = p.map(|f| f.picture()).unwrap_or(None);
Self { Self {
image: img, image: img,
sub: None, sub: None,
size: None, size: None,
services,
} }
} }
pub fn pubkey(pk: &[u8; 32], svc: &'a RouteServices<'a>) -> Self { pub fn pubkey(pk: &[u8; 32], services: &'a RouteServices<'a>) -> Self {
let (p, sub) = svc.ndb.fetch_profile(svc.tx, pk); let (p, sub) = services.ndb.fetch_profile(services.tx, pk);
Self { Self {
image: p.and_then(|p| p.picture().map(|p| svc.img_cache.load(p))), image: p.map(|f| f.picture()).unwrap_or(None),
sub, sub,
size: None, size: None,
services,
} }
} }
@ -65,10 +60,14 @@ impl<'a> Widget for Avatar<'a> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let size_v = self.size.unwrap_or(40.); let size_v = self.size.unwrap_or(40.);
let size = Vec2::new(size_v, size_v); let size = Vec2::new(size_v, size_v);
if !ui.is_rect_visible(ui.cursor()) { if !ui.is_visible() {
return Self::placeholder(ui, size_v); return Self::placeholder(ui, size_v);
} }
match self.image { match self
.image
.as_ref()
.map(|i| self.services.img_cache.load(*i))
{
Some(img) => img Some(img) => img
.fit_to_exact_size(size) .fit_to_exact_size(size)
.rounding(Rounding::same(size_v)) .rounding(Rounding::same(size_v))

View File

@ -4,7 +4,7 @@ use crate::route::RouteServices;
use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper}; use crate::services::ndb_wrapper::{NDBWrapper, SubWrapper};
use crate::widgets::chat_message::ChatMessage; use crate::widgets::chat_message::ChatMessage;
use crate::widgets::NostrWidget; use crate::widgets::NostrWidget;
use egui::{Frame, Margin, Response, ScrollArea, Ui, Widget}; use egui::{Frame, Margin, Response, ScrollArea, Ui};
use itertools::Itertools; use itertools::Itertools;
use nostrdb::{Filter, Note, NoteKey, Transaction}; use nostrdb::{Filter, Note, NoteKey, Transaction};
@ -68,10 +68,11 @@ impl NostrWidget for Chat {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.spacing_mut().item_spacing.y = 8.0; ui.spacing_mut().item_spacing.y = 8.0;
for ev in events for ev in events
.iter() .into_iter()
.sorted_by(|a, b| a.created_at().cmp(&b.created_at())) .sorted_by(|a, b| a.created_at().cmp(&b.created_at()))
{ {
ChatMessage::new(&stream, ev, services).ui(ui); let c = ChatMessage::new(&stream, &ev, services);
ui.add(c);
} }
}) })
}) })

View File

@ -57,7 +57,7 @@ impl<'a> Widget for ChatMessage<'a> {
format.color = Color32::WHITE; format.color = Color32::WHITE;
job.append(self.ev.content(), 5.0, format.clone()); job.append(self.ev.content(), 5.0, format.clone());
ui.add(Avatar::from_profile(&profile, self.services.img_cache).size(24.)); ui.add(Avatar::from_profile(&profile, self.services).size(24.));
ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap)); ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap));
}) })
.response .response

View File

@ -1,10 +1,9 @@
use crate::login::LoginKind;
use crate::route::{RouteServices, Routes}; use crate::route::{RouteServices, Routes};
use crate::widgets::avatar::Avatar; use crate::widgets::avatar::Avatar;
use crate::widgets::{Button, NostrWidget}; use crate::widgets::{Button, NostrWidget};
use eframe::emath::Align; use eframe::emath::Align;
use eframe::epaint::Vec2; use eframe::epaint::Vec2;
use egui::{CursorIcon, Frame, Image, Layout, Margin, Response, Sense, Ui, Widget}; use egui::{CursorIcon, Frame, Layout, Margin, Response, Sense, Ui, Widget};
pub struct Header; pub struct Header;

View File

@ -12,6 +12,7 @@ pub struct Profile<'a> {
profile: Option<NdbProfile<'a>>, profile: Option<NdbProfile<'a>>,
sub: Option<SubWrapper>, sub: Option<SubWrapper>,
img_cache: &'a ImageCache, img_cache: &'a ImageCache,
services: &'a RouteServices<'a>,
} }
impl<'a> Profile<'a> { impl<'a> Profile<'a> {
@ -24,6 +25,7 @@ impl<'a> Profile<'a> {
profile: p, profile: p,
img_cache: services.img_cache, img_cache: services.img_cache,
sub, sub,
services,
} }
} }
@ -37,7 +39,7 @@ impl<'a> Widget for Profile<'a> {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.; ui.spacing_mut().item_spacing.x = 8.;
ui.add(Avatar::from_profile(&self.profile, self.img_cache).size(self.size)); ui.add(Avatar::from_profile(&self.profile, self.services).size(self.size));
ui.add(Username::new(&self.profile, FONT_SIZE)) ui.add(Username::new(&self.profile, FONT_SIZE))
}) })
.response .response

View File

@ -16,7 +16,7 @@ impl<'a> StreamList<'a> {
pub fn new( pub fn new(
id: egui::Id, id: egui::Id,
streams: &'a NoteStore<'a>, streams: &'a NoteStore<'a>,
services: &'a RouteServices, services: &'a RouteServices<'a>,
heading: Option<impl Into<WidgetText>>, heading: Option<impl Into<WidgetText>>,
) -> Self { ) -> Self {
Self { Self {

View File

@ -1,5 +1,5 @@
use crate::widgets::PlaceholderRect; use crate::widgets::PlaceholderRect;
use egui::{Context, Response, Ui, Vec2, Widget}; use egui::{Context, Response, Ui, Widget};
use egui_video::{Player, PlayerControls}; use egui_video::{Player, PlayerControls};
pub struct StreamPlayer { pub struct StreamPlayer {

View File

@ -32,10 +32,15 @@ impl Widget for StreamEvent<'_> {
let w = ui.available_width(); let w = ui.available_width();
let h = (w / 16.0) * 9.0; let h = (w / 16.0) * 9.0;
let cover = self.event.image().map(|p| self.services.img_cache.load(p));
let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click()); let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click());
let cover = if ui.is_visible() {
self.event.image().map(|p| self.services.img_cache.load(p))
} else {
None
};
if let Some(cover) = cover.map(|c| { if let Some(cover) = cover.map(|c| {
c.rounding(Rounding::same(12.)) c.rounding(Rounding::same(12.))
.load_for_size(painter.ctx(), Vec2::new(w, h)) .load_for_size(painter.ctx(), Vec2::new(w, h))
@ -111,7 +116,7 @@ impl Widget for StreamEvent<'_> {
}); });
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add(Avatar::from_profile(&host_profile, self.services.img_cache).size(40.)); ui.add(Avatar::from_profile(&host_profile, self.services).size(40.));
let title = RichText::new(self.event.title().unwrap_or("Untitled")) let title = RichText::new(self.event.title().unwrap_or("Untitled"))
.size(16.) .size(16.)
.color(Color32::WHITE); .color(Color32::WHITE);

View File

@ -2,7 +2,7 @@ use crate::note_util::NoteUtil;
use crate::route::RouteServices; use crate::route::RouteServices;
use crate::stream_info::StreamInfo; use crate::stream_info::StreamInfo;
use crate::widgets::{NostrWidget, Profile}; use crate::widgets::{NostrWidget, Profile};
use egui::{Color32, Frame, Label, Margin, Response, RichText, TextWrapMode, Ui, Widget}; use egui::{Color32, Frame, Label, Margin, Response, RichText, TextWrapMode, Ui};
use nostrdb::Note; use nostrdb::Note;
pub struct StreamTitle<'a> { pub struct StreamTitle<'a> {
@ -26,7 +26,7 @@ impl<'a> NostrWidget for StreamTitle<'a> {
.color(Color32::WHITE); .color(Color32::WHITE);
ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Truncate)); ui.add(Label::new(title.strong()).wrap_mode(TextWrapMode::Truncate));
Profile::new(self.event.host(), services).size(32.).ui(ui); ui.add(Profile::new(self.event.host(), services).size(32.));
if let Some(summary) = self if let Some(summary) = self
.event .event

View File

@ -1,9 +1,9 @@
use crate::link::NostrLink; use crate::link::NostrLink;
use crate::route::{RouteAction, RouteServices}; use crate::route::RouteServices;
use crate::theme::{MARGIN_DEFAULT, NEUTRAL_900, ROUNDING_DEFAULT}; use crate::theme::{MARGIN_DEFAULT, NEUTRAL_900, ROUNDING_DEFAULT};
use crate::widgets::{NativeTextInput, NostrWidget}; use crate::widgets::{NativeTextInput, NostrWidget};
use eframe::emath::Align; use eframe::emath::Align;
use egui::{Frame, Image, Layout, Margin, Response, Rounding, Sense, Stroke, TextEdit, Ui, Widget}; use egui::{Frame, Layout, Response, Sense, Ui, Widget};
use log::info; use log::info;
pub struct WriteChat { pub struct WriteChat {