some fixing but did try to look into the long stopping it takes... and honestly too hard to fix! current black screen time is acceptable enough.

did lower memory usage
This commit is contained in:
RabbyDevs 2025-05-09 01:20:07 +03:00
parent 049e170abf
commit 4c87d13eb8
5 changed files with 43 additions and 214 deletions

View file

@ -526,6 +526,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -867,7 +876,6 @@ dependencies = [
"futures-core",
"futures-task",
"futures-util",
"num_cpus",
]
[[package]]
@ -1244,12 +1252,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.4.0"
@ -1392,6 +1394,7 @@ dependencies = [
name = "iced_video_player"
version = "0.6.0"
dependencies = [
"crossbeam-channel",
"glib",
"gstreamer",
"gstreamer-app",
@ -1399,6 +1402,7 @@ dependencies = [
"iced",
"iced_wgpu",
"log",
"parking_lot",
"thiserror 2.0.12",
"url",
"yuvutils-rs",
@ -2078,16 +2082,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.3.9",
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.3"
@ -2457,7 +2451,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi 0.4.0",
"hermit-abi",
"pin-project-lite",
"rustix",
"tracing",

View file

@ -46,3 +46,9 @@ buildInputs = ["libxkbcommon", "gst_all_1.gstreamer", "gst_all_1.gstreamermm", "
rustc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs"]
targets = ["wasm32-unknown-unknown"]
[profile.release]
strip = true # Automatically strip symbols from the binary.
lto = true # Link-time optimization.
opt-level = 3 # Optimize for speed.
codegen-units = 1 # Maximum size reduction optimizations.

View file

@ -7,7 +7,7 @@ use std::num::NonZeroU8;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant};
use std::time::Duration;
/// Position in the media.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -69,19 +69,12 @@ pub(crate) struct Internal {
pub(crate) framerate: f64,
pub(crate) duration: Duration,
pub(crate) speed: f64,
pub(crate) sync_av: bool,
pub(crate) frame: Arc<Mutex<Frame>>,
pub(crate) upload_frame: Arc<AtomicBool>,
pub(crate) last_frame_time: Arc<Mutex<Instant>>,
pub(crate) looping: bool,
pub(crate) is_eos: bool,
pub(crate) restart_stream: bool,
pub(crate) sync_av_avg: u64,
pub(crate) sync_av_counter: u64,
pub(crate) subtitle_text: Arc<Mutex<Option<String>>>,
pub(crate) upload_text: Arc<AtomicBool>,
}
impl Internal {
@ -118,9 +111,6 @@ impl Internal {
)?,
};
*self.subtitle_text.lock().expect("lock subtitle_text") = None;
self.upload_text.store(true, Ordering::SeqCst);
Ok(())
}
@ -176,19 +166,6 @@ impl Internal {
pub(crate) fn paused(&self) -> bool {
self.source.state(gst::ClockTime::ZERO).1 == gst::State::Paused
}
/// Syncs audio with video when there is (inevitably) latency presenting the frame.
pub(crate) fn set_av_offset(&mut self, offset: Duration) {
if self.sync_av {
self.sync_av_counter += 1;
self.sync_av_avg = self.sync_av_avg * (self.sync_av_counter - 1) / self.sync_av_counter
+ offset.as_nanos() as u64 / self.sync_av_counter;
if self.sync_av_counter % 128 == 0 {
self.source
.set_property("av-offset", -(self.sync_av_avg as i64));
}
}
}
}
/// A multimedia video loaded from a URI (e.g., a local file path or HTTP stream).
@ -228,7 +205,7 @@ impl Video {
pub fn new(uri: &url::Url) -> Result<Self, Error> {
gst::init()?;
let pipeline = format!("playbin uri=\"{}\" text-sink=\"appsink name=iced_text sync=true drop=true\" video-sink=\"videoscale ! videoconvert ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1\"", &uri.as_str());
let pipeline = format!("playbin uri=\"{}\" video-sink=\"videoscale ! videoconvert ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1\"", &uri.as_str());
let pipeline = gst::parse::launch(pipeline.as_ref())?
.downcast::<gst::Pipeline>()
.map_err(|_| Error::Cast)?;
@ -244,14 +221,10 @@ impl Video {
let video_sink = bin.by_name("iced_video").unwrap();
let video_sink = video_sink.downcast::<gst_app::AppSink>().unwrap();
let text_sink: gst::Element = pipeline.property("text-sink");
let text_sink = text_sink.downcast::<gst_app::AppSink>().unwrap();
Self::from_gst_pipeline(
// uri,
pipeline,
video_sink,
Some(text_sink)
video_sink
)
}
@ -267,39 +240,27 @@ impl Video {
// uri: url::Url,
pipeline: gst::Pipeline,
video_sink: gst_app::AppSink,
text_sink: Option<gst_app::AppSink>,
) -> Result<Self, Error> {
gst::init()?;
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
// We need to ensure we stop the pipeline if we hit an error,
// or else there may be audio left playing in the background.
macro_rules! cleanup {
($expr:expr) => {
$expr.map_err(|e| {
let _ = pipeline.set_state(gst::State::Null);
e
})
};
}
let pad = video_sink.pads().first().cloned().unwrap();
cleanup!(pipeline.set_state(gst::State::Playing))?;
pipeline.set_state(gst::State::Playing)?;
// wait for up to 5 seconds until the decoder gets the source capabilities
cleanup!(pipeline.state(gst::ClockTime::from_seconds(5)).0)?;
pipeline.state(gst::ClockTime::from_seconds(5)).0?;
// extract resolution and framerate
// TODO(jazzfool): maybe we want to extract some other information too?
let caps = cleanup!(pad.current_caps().ok_or(Error::Caps))?;
let s = cleanup!(caps.structure(0).ok_or(Error::Caps))?;
let width = cleanup!(s.get::<i32>("width").map_err(|_| Error::Caps))?;
let height = cleanup!(s.get::<i32>("height").map_err(|_| Error::Caps))?;
let caps = pad.current_caps().ok_or(Error::Caps)?;
let s = caps.structure(0).ok_or(Error::Caps)?;
let width = s.get::<i32>("width").map_err(|_| Error::Caps)?;
let height = s.get::<i32>("height").map_err(|_| Error::Caps)?;
// resolution should be mod4
let width = ((width + 4 - 1) / 4) * 4;
let framerate = cleanup!(s.get::<gst::Fraction>("framerate").map_err(|_| Error::Caps))?;
let framerate = s.get::<gst::Fraction>("framerate").map_err(|_| Error::Caps)?;
let framerate = framerate.numer() as f64 / framerate.denom() as f64;
if framerate.is_nan()
@ -318,28 +279,18 @@ impl Video {
.unwrap_or(0),
);
let sync_av = pipeline.has_property("av-offset", None);
// NV12 = 12bpp
let frame = Arc::new(Mutex::new(Frame::empty()));
let upload_frame = Arc::new(AtomicBool::new(false));
let alive = Arc::new(AtomicBool::new(true));
let last_frame_time = Arc::new(Mutex::new(Instant::now()));
let frame_ref = Arc::clone(&frame);
let upload_frame_ref = Arc::clone(&upload_frame);
let alive_ref = Arc::clone(&alive);
let last_frame_time_ref = Arc::clone(&last_frame_time);
let subtitle_text = Arc::new(Mutex::new(None));
let upload_text = Arc::new(AtomicBool::new(false));
let subtitle_text_ref = Arc::clone(&subtitle_text);
let upload_text_ref = Arc::clone(&upload_text);
let pipeline_ref = pipeline.clone();
let worker = std::thread::spawn(move || {
let mut clear_subtitles_at = None;
while alive_ref.load(Ordering::Acquire) {
if let Err(gst::FlowError::Error) = (|| -> Result<(), gst::FlowError> {
@ -353,15 +304,6 @@ impl Video {
.try_pull_sample(gst::ClockTime::from_mseconds(16))
.ok_or(gst::FlowError::Eos)?
};
*last_frame_time_ref
.lock()
.map_err(|_| gst::FlowError::Error)? = Instant::now();
let frame_segment = sample.segment().cloned().ok_or(gst::FlowError::Error)?;
let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
let frame_pts = buffer.pts().ok_or(gst::FlowError::Error)?;
let frame_duration = buffer.duration().ok_or(gst::FlowError::Error)?;
{
let mut frame_guard =
frame_ref.lock().map_err(|_| gst::FlowError::Error)?;
@ -370,55 +312,6 @@ impl Video {
upload_frame_ref.swap(true, Ordering::SeqCst);
if let Some(at) = clear_subtitles_at {
if frame_pts >= at {
*subtitle_text_ref
.lock()
.map_err(|_| gst::FlowError::Error)? = None;
upload_text_ref.store(true, Ordering::SeqCst);
clear_subtitles_at = None;
}
}
let text = text_sink
.as_ref()
.and_then(|sink| sink.try_pull_sample(gst::ClockTime::from_seconds(0)));
if let Some(text) = text {
let text_segment = text.segment().ok_or(gst::FlowError::Error)?;
let text = text.buffer().ok_or(gst::FlowError::Error)?;
let text_pts = text.pts().ok_or(gst::FlowError::Error)?;
let text_duration = text.duration().ok_or(gst::FlowError::Error)?;
let frame_running_time = frame_segment.to_running_time(frame_pts).value();
let frame_running_time_end = frame_segment
.to_running_time(frame_pts + frame_duration)
.value();
let text_running_time = text_segment.to_running_time(text_pts).value();
let text_running_time_end = text_segment
.to_running_time(text_pts + text_duration)
.value();
// see gst-plugins-base/ext/pango/gstbasetextoverlay.c (gst_base_text_overlay_video_chain)
// as an example of how to correctly synchronize the text+video segments
if text_running_time_end > frame_running_time
&& frame_running_time_end > text_running_time
{
let duration = text.duration().unwrap_or(gst::ClockTime::ZERO);
let map = text.map_readable().map_err(|_| gst::FlowError::Error)?;
let text = std::str::from_utf8(map.as_slice())
.map_err(|_| gst::FlowError::Error)?
.to_string();
*subtitle_text_ref
.lock()
.map_err(|_| gst::FlowError::Error)? = Some(text);
upload_text_ref.store(true, Ordering::SeqCst);
clear_subtitles_at = Some(text_pts + duration);
}
}
Ok(())
})() {
log::error!("error pulling frame");
@ -440,19 +333,12 @@ impl Video {
framerate,
duration,
speed: 1.0,
sync_av,
frame,
upload_frame,
last_frame_time,
looping: false,
is_eos: false,
restart_stream: false,
sync_av_avg: 0,
sync_av_counter: 0,
subtitle_text,
upload_text,
})))
}
@ -478,30 +364,6 @@ impl Video {
self.read().framerate
}
/// Set the volume multiplier of the audio.
/// `0.0` = 0% volume, `1.0` = 100% volume.
///
/// This uses a linear scale, for example `0.5` is perceived as half as loud.
pub fn set_volume(&mut self, volume: f64) {
self.get_mut().source.set_property("volume", volume);
self.set_muted(self.muted()); // for some reason gstreamer unmutes when changing volume?
}
/// Get the volume multiplier of the audio.
pub fn volume(&self) -> f64 {
self.read().source.property("volume")
}
/// Set if the audio is muted or not, without changing the volume.
pub fn set_muted(&mut self, muted: bool) {
self.get_mut().source.set_property("mute", muted);
}
/// Get if the audio is muted or not.
pub fn muted(&self) -> bool {
self.read().source.property("mute")
}
/// Get if the stream ended or not.
pub fn eos(&self) -> bool {
self.read().is_eos
@ -600,11 +462,9 @@ impl Video {
let downscale = u8::from(downscale) as u32;
let paused = self.paused();
let muted = self.muted();
let pos = self.position();
self.set_paused(false);
self.set_muted(true);
let out = {
let inner = self.read();
@ -631,7 +491,6 @@ impl Video {
};
self.set_paused(paused);
self.set_muted(muted);
self.seek(pos, true)?;
out

View file

@ -6,7 +6,7 @@ use iced::{
};
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
use log::error;
use std::{marker::PhantomData, sync::atomic::Ordering};
use std::{marker::PhantomData, sync::atomic::Ordering, time::Duration};
use std::{sync::Arc, time::Instant};
/// Video player widget which displays the current frame of a [`Video`](crate::Video).
@ -20,7 +20,6 @@ where
height: iced::Length,
on_end_of_stream: Option<Message>,
on_new_frame: Option<Message>,
on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message + 'a>>,
on_error: Option<Box<dyn Fn(&glib::Error) -> Message + 'a>>,
_phantom: PhantomData<(Theme, Renderer)>,
}
@ -38,7 +37,6 @@ where
height: iced::Length::Shrink,
on_end_of_stream: None,
on_new_frame: None,
on_subtitle_text: None,
on_error: None,
_phantom: Default::default(),
}
@ -84,17 +82,6 @@ where
}
}
/// Message to send when the video receives a new frame.
pub fn on_subtitle_text<F>(self, on_subtitle_text: F) -> Self
where
F: 'static + Fn(Option<String>) -> Message,
{
VideoPlayer {
on_subtitle_text: Some(Box::new(on_subtitle_text)),
..self
}
}
/// Message to send when the video playback encounters an error.
pub fn on_error<F>(self, on_error: F) -> Self
where
@ -156,7 +143,7 @@ where
_cursor: advanced::mouse::Cursor,
_viewport: &iced::Rectangle,
) {
let mut inner = self.video.write();
let inner = self.video.write();
// bounds based on `Image::draw`
let image_size = iced::Size::new(inner.width as f32, inner.height as f32);
@ -183,15 +170,6 @@ where
let upload_frame = inner.upload_frame.swap(false, Ordering::SeqCst);
if upload_frame {
let last_frame_time = inner
.last_frame_time
.lock()
.map(|time| *time)
.unwrap_or_else(|_| Instant::now());
inner.set_av_offset(Instant::now() - last_frame_time);
}
let render = |renderer: &mut Renderer| {
renderer.draw_primitive(
drawing_bounds,
@ -276,17 +254,12 @@ where
}
}
if let Some(on_subtitle_text) = &self.on_subtitle_text {
if inner.upload_text.swap(false, Ordering::SeqCst) {
if let Ok(text) = inner.subtitle_text.try_lock() {
shell.publish(on_subtitle_text(text.clone()));
}
}
}
shell.request_redraw();
} else {
shell.request_redraw();
let frame_time = 1.0 / inner.framerate;
shell.request_redraw_at(iced::window::RedrawRequest::At(
Instant::now() + Duration::from_secs_f64(frame_time),
));
}
}
}

View file

@ -43,7 +43,7 @@ pub fn main() -> iced::Result {
let settings = Settings {
decorations: false,
icon: Some(icon::from_rgba(rgba_vec, icon_image.width(), icon_image.height()).unwrap()),
size: Size::new(2000.0, 1000.0),
size: Size::new(0.0, 0.0),
maximized: false,
fullscreen: false,
position: window::Position::Centered,
@ -63,7 +63,7 @@ pub fn main() -> iced::Result {
.window(settings)
.font(segoe_assets)
.font(main_font)
.window_size((1280.0, 760.0))
.window_size((1280.0, 720.0))
.run()
}
@ -108,14 +108,14 @@ struct State {
installed_games: Vec<PossibleGames>,
installed_game_servers: Vec<PossibleGames>,
db_software_installed: bool,
backgrounds: HashMap<PossibleGames, LauncherBackground>,
background: Option<LauncherBackground>,
splash_images: HashMap<PossibleGames, DynamicImage>,
icon_images: HashMap<PossibleGames, DynamicImage>
}
impl State {
fn get_background_element(&self) -> Option<Element<Message>> {
if let Some(background) = self.backgrounds.get(&self.selected_game) {
if let Some(background) = &self.background {
match background {
LauncherBackground::Video(video) => Some(VideoPlayer::new(video).into()),
LauncherBackground::Image(img) => Some(image(image::Handle::from_rgba(img.width(), img.height(), img.to_rgba8().into_raw())).into()),
@ -187,7 +187,7 @@ impl State {
installed_games: saved_state.installed_games,
installed_game_servers: saved_state.installed_game_servers,
db_software_installed: saved_state.db_software_installed,
backgrounds: self.backgrounds,
background: self.background,
splash_images: self.splash_images,
icon_images: self.icon_images,
})
@ -328,12 +328,7 @@ impl Launcher {
Task::none()
},
Message::LoadBackground(()) => {
let mut bg_map = HashMap::new();
for game in PossibleGames::iter() {
bg_map.insert(game.clone(), get_game_background(&game));
}
state.backgrounds = bg_map;
state.background = Some(get_game_background(&state.selected_game));
let (width, height) = state.selected_game.get_game_preferred_size();
window::get_latest().and_then(move |id: window::Id| {
@ -349,6 +344,8 @@ impl Launcher {
},
Message::GameSelected(game) => {
state.selected_game = game;
state.background = Some(get_game_background(&state.selected_game));
let (width, height) = state.selected_game.get_game_preferred_size();
window::get_latest().and_then(move |id: window::Id| {
window::resize(id, Size { width: width as f32, height: height as f32 })
@ -404,7 +401,7 @@ impl Launcher {
..container::Style::default()
}))
.on_press(Message::DragWindow)
.interaction(Interaction::Grab)
.interaction(Interaction::Move)
];
let bottom_bar = container(row![