Compare commits

..

No commits in common. "6ff166120cc447f52f7f96848659386fae99731c" and "24428186a3f931db4813e1078c3c66faa84e9c9a" have entirely different histories.

7 changed files with 1456 additions and 2638 deletions

3649
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ keywords = ["gui", "iced", "video"]
categories = ["gui", "multimedia"] categories = ["gui", "multimedia"]
version = "0.6.0" version = "0.6.0"
authors = ["jazzfool"] authors = ["jazzfool"]
edition = "2024" edition = "2021"
resolver = "2" resolver = "2"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
exclude = [ exclude = [
@ -16,8 +16,8 @@ exclude = [
] ]
[dependencies] [dependencies]
# iced = { git = "https://github.com/iced-rs/iced", branch = "master", features = ["image", "advanced", "wgpu"] } iced = { version = "0.13", features = ["image", "advanced", "wgpu"] }
iced_wgpu = { git = "https://github.com/iced-rs/iced", branch = "master"} iced_wgpu = "0.13"
gstreamer = "0.23" gstreamer = "0.23"
gstreamer-app = "0.23" # appsink gstreamer-app = "0.23" # appsink
gstreamer-base = "0.23" # basesrc gstreamer-base = "0.23" # basesrc
@ -25,11 +25,7 @@ glib = "0.20" # gobject traits and error type
log = "0.4" log = "0.4"
thiserror = "1" thiserror = "1"
url = "2" # media uri url = "2" # media uri
html-escape = "0.2.13"
[dependencies.iced]
git = "https://github.com/iced-rs/iced"
branch = "master"
features = ["wgpu", "image", "advanced", "svg", "canvas", "hot", "debug", "lazy", "tokio"]
[package.metadata.nix] [package.metadata.nix]
systems = ["x86_64-linux"] systems = ["x86_64-linux"]

187
flake.lock generated
View file

@ -1,126 +1,69 @@
{ {
"nodes": { "nodes": {
"fenix": { "devshell": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": { "locked": {
"lastModified": 1755585599, "lastModified": 1629275356,
"narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=", "narHash": "sha256-R17M69EKXP6q8/mNHaK53ECwjFo1pdF+XaJC9Qq8zjg=",
"owner": "nix-community", "owner": "numtide",
"repo": "fenix", "repo": "devshell",
"rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42", "rev": "26f25a12265f030917358a9632cd600b51af1d97",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "numtide",
"repo": "fenix", "repo": "devshell",
"type": "github" "type": "github"
} }
}, },
"fenix_2": { "flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1627913399,
"narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"nixCargoIntegration": {
"inputs": { "inputs": {
"devshell": "devshell",
"nixpkgs": [ "nixpkgs": [
"naersk",
"nixpkgs" "nixpkgs"
], ],
"rust-analyzer-src": "rust-analyzer-src_2" "rustOverlay": "rustOverlay"
}, },
"locked": { "locked": {
"lastModified": 1752475459, "lastModified": 1629871751,
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=", "narHash": "sha256-QjnDg34ApcnjmXlNLnbHswT9OroCPY7Wip6r9Zkgkfo=",
"owner": "nix-community", "owner": "yusdacra",
"repo": "fenix", "repo": "nix-cargo-integration",
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f", "rev": "4f164ecad242537d5893426eef02c47c9e5ced59",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "yusdacra",
"repo": "fenix", "repo": "nix-cargo-integration",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"fenix": "fenix_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1752689277,
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
"owner": "nix-community",
"repo": "naersk",
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1755186698, "lastModified": 1629618782,
"narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", "narHash": "sha256-2K8SSXu3alo/URI3MClGdDSns6Gb4ZaW4LET53UWyKk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1752077645,
"narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6", "rev": "870959c7fb3a42af1863bed9e1756086a74eb649",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1756125398,
"narHash": "sha256-XexyKZpf46cMiO5Vbj+dWSAXOnr285GHsMch8FBoHbc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3b9f00d7a7bf68acd4c4abb9d43695afb04e03a5",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
@ -128,58 +71,24 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"fenix": "fenix", "flakeCompat": "flakeCompat",
"flake-utils": "flake-utils", "nixCargoIntegration": "nixCargoIntegration",
"naersk": "naersk", "nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs_3"
} }
}, },
"rust-analyzer-src": { "rustOverlay": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1755504847, "lastModified": 1629857564,
"narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=", "narHash": "sha256-dClWiHkbaCDaIl520Miri66UOA8OecWbaVTWJBajHyM=",
"owner": "rust-lang", "owner": "oxalica",
"repo": "rust-analyzer", "repo": "rust-overlay",
"rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75", "rev": "88848c36934318e16c86097f65dbf97a57968d81",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "rust-lang", "owner": "oxalica",
"ref": "nightly", "repo": "rust-overlay",
"repo": "rust-analyzer",
"type": "github"
}
},
"rust-analyzer-src_2": {
"flake": false,
"locked": {
"lastModified": 1752428706,
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github" "type": "github"
} }
} }

116
flake.nix
View file

@ -1,100 +1,28 @@
{ {
description = "A video widget";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flakeCompat = {
naersk.url = "github:nix-community/naersk"; url = "github:edolstra/flake-compat";
flake-utils.url = "github:numtide/flake-utils"; flake = false;
fenix.url = "github:nix-community/fenix"; };
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = inputs: with inputs; outputs = inputs:
flake-utils.lib.eachDefaultSystem inputs.nixCargoIntegration.lib.makeOutputs {
(system: root = ./.;
let overrides = {
pkgs = import nixpkgs { shell = common: prev: {
inherit system; env = prev.env ++ [
overlays = [fenix.overlays.default]; {
# overlays = [cargo2nix.overlays.default]; name = "GST_PLUGIN_PATH";
}; value = "${common.pkgs.gst_all_1.gstreamer}:${common.pkgs.gst_all_1.gst-plugins-bad}:${common.pkgs.gst_all_1.gst-plugins-ugly}:${common.pkgs.gst_all_1.gst-plugins-good}:${common.pkgs.gst_all_1.gst-plugins-base}";
naersk' = pkgs.callPackage naersk {}; }
nbi = with pkgs; [
# Rust tools
alejandra
(pkgs.fenix.stable.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
])
rust-analyzer
vulkan-loader
wayland
wayland-protocols
libxkbcommon
pkg-config
sccache
]; ];
};
bi = with pkgs; [ };
gcc };
stdenv
gnumake
gdb
lldb
cmake
makeWrapper
vulkan-headers
vulkan-loader
vulkan-tools
libGL
cargo-flamegraph
fontconfig
glib
alsa-lib
gst_all_1.gst-libav
gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-ugly
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-rs
gst_all_1.gst-vaapi
gst_all_1.gstreamer
# podofo
# mpv
ffmpeg-full
# yt-dlp
just
cargo-watch
];
in rec
{
devShell = pkgs.mkShell.override {
# stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv;
} {
nativeBuildInputs = nbi;
buildInputs = bi;
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
with pkgs;
pkgs.lib.makeLibraryPath [
pkgs.vulkan-loader
pkgs.wayland
pkgs.wayland-protocols
pkgs.libxkbcommon
]
}";
};
defaultPackage = naersk'.buildPackage {
src = ./.;
};
packages = {
default = naersk'.buildPackage {
src = ./.;
};
};
}
);
} }

View file

@ -1,4 +1,3 @@
use crate::video::Frame;
use iced_wgpu::primitive::Primitive; use iced_wgpu::primitive::Primitive;
use iced_wgpu::wgpu; use iced_wgpu::wgpu;
use std::{ use std::{
@ -384,7 +383,7 @@ impl VideoPipeline {
pub(crate) struct VideoPrimitive { pub(crate) struct VideoPrimitive {
video_id: u64, video_id: u64,
alive: Arc<AtomicBool>, alive: Arc<AtomicBool>,
frame: Arc<Mutex<Frame>>, frame: Arc<Mutex<Vec<u8>>>,
size: (u32, u32), size: (u32, u32),
upload_frame: bool, upload_frame: bool,
} }
@ -393,7 +392,7 @@ impl VideoPrimitive {
pub fn new( pub fn new(
video_id: u64, video_id: u64,
alive: Arc<AtomicBool>, alive: Arc<AtomicBool>,
frame: Arc<Mutex<Frame>>, frame: Arc<Mutex<Vec<u8>>>,
size: (u32, u32), size: (u32, u32),
upload_frame: bool, upload_frame: bool,
) -> Self { ) -> Self {
@ -424,16 +423,14 @@ impl Primitive for VideoPrimitive {
let pipeline = storage.get_mut::<VideoPipeline>().unwrap(); let pipeline = storage.get_mut::<VideoPipeline>().unwrap();
if self.upload_frame { if self.upload_frame {
if let Some(readable) = self.frame.lock().expect("lock frame mutex").readable() { pipeline.upload(
pipeline.upload( device,
device, queue,
queue, self.video_id,
self.video_id, &self.alive,
&self.alive, self.size,
self.size, self.frame.lock().expect("lock frame mutex").as_slice(),
readable.as_slice(), );
);
}
} }
pipeline.prepare( pipeline.prepare(

View file

@ -41,19 +41,6 @@ impl From<u64> for Position {
} }
} }
#[derive(Debug)]
pub(crate) struct Frame(gst::Sample);
impl Frame {
pub fn empty() -> Self {
Self(gst::Sample::builder().build())
}
pub fn readable(&self) -> Option<gst::BufferMap<gst::buffer::Readable>> {
self.0.buffer().and_then(|x| x.map_readable().ok())
}
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Internal { pub(crate) struct Internal {
pub(crate) id: u64, pub(crate) id: u64,
@ -70,7 +57,7 @@ pub(crate) struct Internal {
pub(crate) speed: f64, pub(crate) speed: f64,
pub(crate) sync_av: bool, pub(crate) sync_av: bool,
pub(crate) frame: Arc<Mutex<Frame>>, pub(crate) frame: Arc<Mutex<Vec<u8>>>,
pub(crate) upload_frame: Arc<AtomicBool>, pub(crate) upload_frame: Arc<AtomicBool>,
pub(crate) last_frame_time: Arc<Mutex<Instant>>, pub(crate) last_frame_time: Arc<Mutex<Instant>>,
pub(crate) looping: bool, pub(crate) looping: bool,
@ -117,9 +104,6 @@ impl Internal {
)?, )?,
}; };
*self.subtitle_text.lock().expect("lock subtitle_text") = None;
self.upload_text.store(true, Ordering::SeqCst);
Ok(()) Ok(())
} }
@ -205,12 +189,7 @@ impl Drop for Video {
inner.alive.store(false, Ordering::SeqCst); inner.alive.store(false, Ordering::SeqCst);
if let Some(worker) = inner.worker.take() { if let Some(worker) = inner.worker.take() {
if let Err(err) = worker.join() { worker.join().expect("failed to stop video thread");
match err.downcast_ref::<String>() {
Some(e) => log::error!("Video thread panicked: {e}"),
None => log::error!("Video thread panicked with unknown reason"),
}
}
} }
} }
} }
@ -221,7 +200,7 @@ impl Video {
pub fn new(uri: &url::Url) -> Result<Self, Error> { pub fn new(uri: &url::Url) -> Result<Self, Error> {
gst::init()?; 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=\"{}\" text-sink=\"appsink name=iced_text sync=true caps=text/x-raw\" 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())? let pipeline = gst::parse::launch(pipeline.as_ref())?
.downcast::<gst::Pipeline>() .downcast::<gst::Pipeline>()
.map_err(|_| Error::Cast)?; .map_err(|_| Error::Cast)?;
@ -238,6 +217,7 @@ impl Video {
let video_sink = video_sink.downcast::<gst_app::AppSink>().unwrap(); let video_sink = video_sink.downcast::<gst_app::AppSink>().unwrap();
let text_sink: gst::Element = pipeline.property("text-sink"); let text_sink: gst::Element = pipeline.property("text-sink");
//let pad = text_sink.pads().get(0).cloned().unwrap();
let text_sink = text_sink.downcast::<gst_app::AppSink>().unwrap(); let text_sink = text_sink.downcast::<gst_app::AppSink>().unwrap();
Self::from_gst_pipeline(pipeline, video_sink, Some(text_sink)) Self::from_gst_pipeline(pipeline, video_sink, Some(text_sink))
@ -308,7 +288,11 @@ impl Video {
let sync_av = pipeline.has_property("av-offset", None); let sync_av = pipeline.has_property("av-offset", None);
// NV12 = 12bpp // NV12 = 12bpp
let frame = Arc::new(Mutex::new(Frame::empty())); let frame = Arc::new(Mutex::new(vec![
0u8;
(width as usize * height as usize * 3)
.div_ceil(2)
]));
let upload_frame = Arc::new(AtomicBool::new(false)); let upload_frame = Arc::new(AtomicBool::new(false));
let alive = Arc::new(AtomicBool::new(true)); let alive = Arc::new(AtomicBool::new(true));
let last_frame_time = Arc::new(Mutex::new(Instant::now())); let last_frame_time = Arc::new(Mutex::new(Instant::now()));
@ -345,20 +329,18 @@ impl Video {
.lock() .lock()
.map_err(|_| gst::FlowError::Error)? = Instant::now(); .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 buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
let frame_pts = buffer.pts().ok_or(gst::FlowError::Error)?; let pts = buffer.pts().unwrap_or_default();
let frame_duration = buffer.duration().ok_or(gst::FlowError::Error)?; let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
{
let mut frame_guard = let mut frame = frame_ref.lock().map_err(|_| gst::FlowError::Error)?;
frame_ref.lock().map_err(|_| gst::FlowError::Error)?; let frame_len = frame.len();
*frame_guard = Frame(sample); frame.copy_from_slice(&map.as_slice()[..frame_len]);
}
upload_frame_ref.swap(true, Ordering::SeqCst); upload_frame_ref.swap(true, Ordering::SeqCst);
if let Some(at) = clear_subtitles_at { if let Some(at) = clear_subtitles_at {
if frame_pts >= at { if pts >= at {
*subtitle_text_ref *subtitle_text_ref
.lock() .lock()
.map_err(|_| gst::FlowError::Error)? = None; .map_err(|_| gst::FlowError::Error)? = None;
@ -371,39 +353,22 @@ impl Video {
.as_ref() .as_ref()
.and_then(|sink| sink.try_pull_sample(gst::ClockTime::from_seconds(0))); .and_then(|sink| sink.try_pull_sample(gst::ClockTime::from_seconds(0)));
if let Some(text) = text { 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 = text.buffer().ok_or(gst::FlowError::Error)?;
let text_pts = text.pts().ok_or(gst::FlowError::Error)?; let pts = text.pts().unwrap_or_default();
let text_duration = text.duration().ok_or(gst::FlowError::Error)?; let duration = text.duration().unwrap_or(gst::ClockTime::ZERO);
let map = text.map_readable().map_err(|_| gst::FlowError::Error)?;
let frame_running_time = frame_segment.to_running_time(frame_pts).value(); let text = html_escape::decode_html_entities(
let frame_running_time_end = frame_segment std::str::from_utf8(map.as_slice())
.to_running_time(frame_pts + frame_duration) .map_err(|_| gst::FlowError::Error)?,
.value(); )
.to_string();
*subtitle_text_ref
.lock()
.map_err(|_| gst::FlowError::Error)? = Some(text);
upload_text_ref.store(true, Ordering::SeqCst);
let text_running_time = text_segment.to_running_time(text_pts).value(); clear_subtitles_at = Some(pts + duration);
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(()) Ok(())
@ -604,13 +569,15 @@ impl Video {
while !inner.upload_frame.load(Ordering::SeqCst) { while !inner.upload_frame.load(Ordering::SeqCst) {
std::hint::spin_loop(); std::hint::spin_loop();
} }
let frame_guard = inner.frame.lock().map_err(|_| Error::Lock)?;
let frame = frame_guard.readable().ok_or(Error::Lock)?;
Ok(img::Handle::from_rgba( Ok(img::Handle::from_rgba(
inner.width as u32 / downscale, inner.width as u32 / downscale,
inner.height as u32 / downscale, inner.height as u32 / downscale,
yuv_to_rgba(frame.as_slice(), width as _, height as _, downscale), yuv_to_rgba(
&inner.frame.lock().map_err(|_| Error::Lock)?,
width as _,
height as _,
downscale,
),
)) ))
}) })
.collect() .collect()

View file

@ -107,8 +107,8 @@ where
} }
} }
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for VideoPlayer<'_, Message, Theme, Renderer> for VideoPlayer<'a, Message, Theme, Renderer>
where where
Message: Clone, Message: Clone,
Renderer: PrimitiveRenderer, Renderer: PrimitiveRenderer,