feat: Update iced_video_player to master

This commit is contained in:
uttarayan21
2025-12-13 03:40:12 +05:30
parent c7afcd3f0d
commit 253d27c176
18 changed files with 1224 additions and 1565 deletions

View File

@@ -1,3 +1,2 @@
/target
.direnv
.media
.direnv

File diff suppressed because it is too large Load Diff

View File

@@ -8,24 +8,23 @@ keywords = ["gui", "iced", "video"]
categories = ["gui", "multimedia"]
version = "0.6.0"
authors = ["jazzfool"]
edition = "2021"
edition = "2024"
resolver = "2"
license = "MIT OR Apache-2.0"
exclude = [".media/test.mp4"]
exclude = [
".media/test.mp4"
]
[dependencies]
iced = { git = "https://github.com/iced-rs/iced", features = [
"image",
"advanced",
"wgpu",
] }
iced_wgpu = { git = "https://github.com/iced-rs/iced" }
gstreamer = { version = "0.24", features = ["v1_26"] }
gstreamer-app = { version = "0.24", features = ["v1_26"] } # appsink
gstreamer-base = { version = "0.24", features = ["v1_26"] } # basesrc
glib = "0.21" # gobject traits and error type
iced = { version = "0.14", features = ["image", "advanced", "wgpu"] }
iced_wgpu = "0.14"
gstreamer = "0.23"
gstreamer-app = "0.23" # appsink
gstreamer-base = "0.23" # basesrc
gstreamer-video = "0.23" # VideoMeta
glib = "0.20" # gobject traits and error type
log = "0.4"
thiserror = "2"
thiserror = "1"
url = "2" # media uri
[package.metadata.nix]
@@ -33,33 +32,15 @@ systems = ["x86_64-linux"]
app = true
build = true
runtimeLibs = [
"vulkan-loader",
"wayland",
"wayland-protocols",
"libxkbcommon",
"xorg.libX11",
"xorg.libXrandr",
"xorg.libXi",
"gst_all_1.gstreamer",
"gst_all_1.gstreamermm",
"gst_all_1.gst-plugins-bad",
"gst_all_1.gst-plugins-ugly",
"gst_all_1.gst-plugins-good",
"gst_all_1.gst-plugins-base",
"glib",
"glib-networking",
]
buildInputs = [
"libxkbcommon",
"gst_all_1.gstreamer",
"gst_all_1.gstreamermm",
"gst_all_1.gst-plugins-bad",
"gst_all_1.gst-plugins-ugly",
"gst_all_1.gst-plugins-good",
"gst_all_1.gst-plugins-base",
"glib",
"glib-networking",
"vulkan-loader",
"wayland",
"wayland-protocols",
"libxkbcommon",
"xorg.libX11",
"xorg.libXrandr",
"xorg.libXi", "gst_all_1.gstreamer", "gst_all_1.gstreamermm", "gst_all_1.gst-plugins-bad", "gst_all_1.gst-plugins-ugly", "gst_all_1.gst-plugins-good", "gst_all_1.gst-plugins-base",
]
buildInputs = ["libxkbcommon", "gst_all_1.gstreamer", "gst_all_1.gstreamermm", "gst_all_1.gst-plugins-bad", "gst_all_1.gst-plugins-ugly", "gst_all_1.gst-plugins-good", "gst_all_1.gst-plugins-base"]
[package.metadata.docs.rs]
rustc-args = ["--cfg", "docsrs"]

View File

@@ -1,6 +1,6 @@
use iced::{
widget::{Button, Column, Container, Row, Slider, Text},
Element,
widget::{Button, Column, Container, Row, Slider, Text},
};
use iced_video_player::{Video, VideoPlayer};
use std::time::Duration;
@@ -29,10 +29,17 @@ impl Default for App {
fn default() -> Self {
App {
video: Video::new(
&url::Url::parse("https://jellyfin.tsuba.darksailor.dev/Videos/1d7e2012-e17d-edbb-25c3-2dbcc803d6b6/stream?static=true")
.expect("Failed to parse URL"),
&url::Url::from_file_path(
std::path::PathBuf::from(file!())
.parent()
.unwrap()
.join("../.media/test.mp4")
.canonicalize()
.unwrap(),
)
.unwrap(),
)
.expect("Failed to create video"),
.unwrap(),
position: 0.0,
dragging: false,
}

View File

@@ -1,69 +1,28 @@
{
"nodes": {
"crane": {
"flake": false,
"devshell": {
"locked": {
"lastModified": 1758758545,
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
"owner": "ipetkov",
"repo": "crane",
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
"lastModified": 1629275356,
"narHash": "sha256-R17M69EKXP6q8/mNHaK53ECwjFo1pdF+XaJC9Qq8zjg=",
"owner": "numtide",
"repo": "devshell",
"rev": "26f25a12265f030917358a9632cd600b51af1d97",
"type": "github"
},
"original": {
"owner": "ipetkov",
"ref": "v0.21.1",
"repo": "crane",
"type": "github"
}
},
"dream2nix": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"nixpkgs"
],
"purescript-overlay": "purescript-overlay",
"pyproject-nix": "pyproject-nix"
},
"locked": {
"lastModified": 1763413832,
"narHash": "sha256-dkqBwDXiv8MPoFyIvOuC4bVubAP+TlVZUkVMB78TTSg=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "5658fba3a0b6b7d5cb0460b949651f64f644a743",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "dream2nix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"lastModified": 1627913399,
"narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
"type": "github"
},
"original": {
@@ -72,40 +31,20 @@
"type": "github"
}
},
"mk-naked-shell": {
"flake": false,
"locked": {
"lastModified": 1681286841,
"narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
"owner": "90-008",
"repo": "mk-naked-shell",
"rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
"type": "github"
},
"original": {
"owner": "90-008",
"repo": "mk-naked-shell",
"type": "github"
}
},
"nixCargoIntegration": {
"inputs": {
"crane": "crane",
"dream2nix": "dream2nix",
"mk-naked-shell": "mk-naked-shell",
"devshell": "devshell",
"nixpkgs": [
"nixpkgs"
],
"parts": "parts",
"rust-overlay": "rust-overlay",
"treefmt": "treefmt"
"rustOverlay": "rustOverlay"
},
"locked": {
"lastModified": 1763619566,
"narHash": "sha256-92rSHIwh5qTXjcktVEWyKu5EPB3/7UdgjgjtWZ5ET6w=",
"lastModified": 1629871751,
"narHash": "sha256-QjnDg34ApcnjmXlNLnbHswT9OroCPY7Wip6r9Zkgkfo=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "ac45d8c0d6876e6547d62bc729654c7b9a79c760",
"rev": "4f164ecad242537d5893426eef02c47c9e5ced59",
"type": "github"
},
"original": {
@@ -116,11 +55,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1763421233,
"narHash": "sha256-Stk9ZYRkGrnnpyJ4eqt9eQtdFWRRIvMxpNRf4sIegnw=",
"lastModified": 1629618782,
"narHash": "sha256-2K8SSXu3alo/URI3MClGdDSns6Gb4ZaW4LET53UWyKk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "89c2b2330e733d6cdb5eae7b899326930c2c0648",
"rev": "870959c7fb3a42af1863bed9e1756086a74eb649",
"type": "github"
},
"original": {
@@ -130,73 +69,6 @@
"type": "github"
}
},
"parts": {
"inputs": {
"nixpkgs-lib": [
"nixCargoIntegration",
"nixpkgs"
]
},
"locked": {
"lastModified": 1762980239,
"narHash": "sha256-8oNVE8TrD19ulHinjaqONf9QWCKK+w4url56cdStMpM=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "52a2caecc898d0b46b2b905f058ccc5081f842da",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"purescript-overlay": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"nixCargoIntegration",
"dream2nix",
"nixpkgs"
],
"slimlock": "slimlock"
},
"locked": {
"lastModified": 1728546539,
"narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=",
"owner": "thomashoneyman",
"repo": "purescript-overlay",
"rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4",
"type": "github"
},
"original": {
"owner": "thomashoneyman",
"repo": "purescript-overlay",
"type": "github"
}
},
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"dream2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1752481895,
"narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "16ee295c25107a94e59a7fc7f2e5322851781162",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": {
"inputs": {
"flakeCompat": "flakeCompat",
@@ -204,19 +76,14 @@
"nixpkgs": "nixpkgs"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"nixpkgs"
]
},
"rustOverlay": {
"flake": false,
"locked": {
"lastModified": 1763606317,
"narHash": "sha256-lsq4Urmb9Iyg2zyg2yG6oMQk9yuaoIgy+jgvYM4guxA=",
"lastModified": 1629857564,
"narHash": "sha256-dClWiHkbaCDaIl520Miri66UOA8OecWbaVTWJBajHyM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a5615abaf30cfaef2e32f1ff9bd5ca94e2911371",
"rev": "88848c36934318e16c86097f65dbf97a57968d81",
"type": "github"
},
"original": {
@@ -224,50 +91,6 @@
"repo": "rust-overlay",
"type": "github"
}
},
"slimlock": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"dream2nix",
"purescript-overlay",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688756706,
"narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=",
"owner": "thomashoneyman",
"repo": "slimlock",
"rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c",
"type": "github"
},
"original": {
"owner": "thomashoneyman",
"repo": "slimlock",
"type": "github"
}
},
"treefmt": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"nixpkgs"
]
},
"locked": {
"lastModified": 1762938485,
"narHash": "sha256-AlEObg0syDl+Spi4LsZIBrjw+snSVU4T8MOeuZJUJjM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",

View File

@@ -11,28 +11,18 @@
};
};
outputs = inputs: let
pkgs = import inputs.nixpkgs {
system = "x86_64-linux";
outputs = inputs:
inputs.nixCargoIntegration.lib.makeOutputs {
root = ./.;
overrides = {
shell = common: prev: {
env = prev.env ++ [
{
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}";
}
];
};
};
};
in {
devShells."x86_64-linux".default = pkgs.mkShell {
# "GST_PLUGIN_PATH" = "${pkgs.gst_all_1.gstreamer}:${pkgs.gst_all_1.gst-plugins-bad}:${pkgs.gst_all_1.gst-plugins-ugly}:${pkgs.gst_all_1.gst-plugins-good}:${pkgs.gst_all_1.gst-plugins-base}";
buildInputs = with pkgs; [
gst_all_1.gstreamer
gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-ugly
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-base
libxkbcommon
wayland
rustup
];
nativeBuildInputs = with pkgs; [
pkg-config
wayland
];
packages = with pkgs; [wayland];
};
};
}

View File

@@ -1,12 +1,12 @@
use crate::video::Frame;
use iced_wgpu::primitive::Primitive;
use iced_wgpu::primitive::{Pipeline, Primitive};
use iced_wgpu::wgpu;
use std::{
collections::{btree_map::Entry, BTreeMap},
collections::{BTreeMap, btree_map::Entry},
num::NonZero,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
};
@@ -35,8 +35,8 @@ pub(crate) struct VideoPipeline {
videos: BTreeMap<u64, VideoEntry>,
}
impl VideoPipeline {
fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
impl Pipeline for VideoPipeline {
fn new(device: &wgpu::Device, _queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_video_player shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
@@ -97,7 +97,7 @@ impl VideoPipeline {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
@@ -114,7 +114,7 @@ impl VideoPipeline {
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
compilation_options: Default::default(),
}),
multiview: None,
cache: None,
@@ -143,6 +143,23 @@ impl VideoPipeline {
}
}
fn trim(&mut self) {
let ids: Vec<_> = self
.videos
.iter()
.filter_map(|(id, entry)| (!entry.alive.load(Ordering::SeqCst)).then_some(*id))
.collect();
for id in ids {
if let Some(video) = self.videos.remove(&id) {
video.texture_y.destroy();
video.texture_uv.destroy();
video.instances.destroy();
}
}
}
}
impl VideoPipeline {
fn upload(
&mut self,
device: &wgpu::Device,
@@ -151,7 +168,10 @@ impl VideoPipeline {
alive: &Arc<AtomicBool>,
(width, height): (u32, u32),
frame: &[u8],
stride: Option<u32>,
) {
// Use stride from GStreamer's VideoMeta if available, otherwise assume stride == width
let stride = stride.unwrap_or(width);
if let Entry::Vacant(entry) = self.videos.entry(video_id) {
let texture_y = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_video_player texture"),
@@ -192,7 +212,7 @@ impl VideoPipeline {
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
usage: Some(wgpu::TextureUsages::empty()),
usage: None,
});
let view_uv = texture_uv.create_view(&wgpu::TextureViewDescriptor {
@@ -204,7 +224,7 @@ impl VideoPipeline {
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
usage: Some(wgpu::TextureUsages::empty()),
usage: None,
});
let instances = device.create_buffer(&wgpu::BufferDescriptor {
@@ -266,10 +286,10 @@ impl VideoPipeline {
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&frame[..(width * height) as usize],
&frame[..(stride * height) as usize],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width),
bytes_per_row: Some(stride),
rows_per_image: Some(height),
},
wgpu::Extent3d {
@@ -286,10 +306,10 @@ impl VideoPipeline {
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&frame[(width * height) as usize..],
&frame[(stride * height) as usize..],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width),
bytes_per_row: Some(stride),
rows_per_image: Some(height / 2),
},
wgpu::Extent3d {
@@ -300,21 +320,6 @@ impl VideoPipeline {
);
}
fn cleanup(&mut self) {
let ids: Vec<_> = self
.videos
.iter()
.filter_map(|(id, entry)| (!entry.alive.load(Ordering::SeqCst)).then_some(*id))
.collect();
for id in ids {
if let Some(video) = self.videos.remove(&id) {
video.texture_y.destroy();
video.texture_uv.destroy();
video.instances.destroy();
}
}
}
fn prepare(&mut self, queue: &wgpu::Queue, video_id: u64, bounds: &iced::Rectangle) {
if let Some(video) = self.videos.get_mut(&video_id) {
let uniforms = Uniforms {
@@ -340,8 +345,6 @@ impl VideoPipeline {
video.prepare_index.fetch_add(1, Ordering::Relaxed);
video.render_index.store(0, Ordering::Relaxed);
}
self.cleanup();
}
fn draw(
@@ -414,39 +417,33 @@ impl VideoPrimitive {
}
impl Primitive for VideoPrimitive {
type Renderer = VideoPipeline;
fn initialize(
&self,
device: &wgpu::Device,
_queue: &wgpu::Queue,
format: wgpu::TextureFormat,
) -> Self::Renderer {
VideoPipeline::new(device, format)
}
type Pipeline = VideoPipeline;
fn prepare(
&self,
renderer: &mut Self::Renderer,
pipeline: &mut VideoPipeline,
device: &wgpu::Device,
queue: &wgpu::Queue,
bounds: &iced::Rectangle,
viewport: &iced_wgpu::graphics::Viewport,
) {
if self.upload_frame {
if let Some(readable) = self.frame.lock().expect("lock frame mutex").readable() {
renderer.upload(
let frame_guard = self.frame.lock().expect("lock frame mutex");
let stride = frame_guard.stride();
if let Some(readable) = frame_guard.readable() {
pipeline.upload(
device,
queue,
self.video_id,
&self.alive,
self.size,
readable.as_slice(),
stride,
);
}
};
}
renderer.prepare(
pipeline.prepare(
queue,
self.video_id,
&(*bounds
@@ -459,11 +456,11 @@ impl Primitive for VideoPrimitive {
fn render(
&self,
renderer: &Self::Renderer,
pipeline: &Self::Pipeline,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clip_bounds: &iced::Rectangle<u32>,
) {
renderer.draw(target, encoder, clip_bounds, self.video_id);
pipeline.draw(target, encoder, clip_bounds, self.video_id);
}
}

View File

@@ -38,24 +38,19 @@ fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput {
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let yuv2r = vec3<f32>(1.164, 0.0, 1.596);
let yuv2g = vec3<f32>(1.164, -0.391, -0.813);
let yuv2b = vec3<f32>(1.164, 2.018, 0.0);
// BT.709 precomputed coefficents
let yuv2rgb = mat3x3<f32>(
1, 0, 1.5748,
1, -0.1873, -0.4681,
1, 1.8556, 0,
);
var yuv = vec3<f32>(0.0);
yuv.x = textureSample(tex_y, s, in.uv).r - 0.0625;
yuv.y = textureSample(tex_uv, s, in.uv).r - 0.5;
yuv.z = textureSample(tex_uv, s, in.uv).g - 0.5;
yuv.x = (textureSample(tex_y, s, in.uv).r - 0.0625) / 0.8588;
yuv.y = (textureSample(tex_uv, s, in.uv).r - 0.5) / 0.8784;
yuv.z = (textureSample(tex_uv, s, in.uv).g - 0.5) / 0.8784;
var rgb = vec3<f32>(0.0);
rgb.x = dot(yuv, yuv2r);
rgb.y = dot(yuv, yuv2g);
rgb.z = dot(yuv, yuv2b);
let threshold = rgb <= vec3<f32>(0.04045);
let hi = pow((rgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
let lo = rgb * vec3<f32>(1.0 / 12.92);
rgb = select(hi, lo, threshold);
var rgb = clamp(yuv * yuv2rgb, vec3<f32>(0), vec3<f32>(1));
return vec4<f32>(rgb, 1.0);
}

View File

@@ -2,6 +2,7 @@ use crate::Error;
use gstreamer as gst;
use gstreamer_app as gst_app;
use gstreamer_app::prelude::*;
use gstreamer_video::VideoMeta;
use iced::widget::image as img;
use std::num::NonZeroU8;
use std::ops::{Deref, DerefMut};
@@ -49,9 +50,19 @@ impl Frame {
Self(gst::Sample::builder().build())
}
pub fn readable(&self) -> Option<gst::BufferMap<'_, gst::buffer::Readable>> {
pub fn readable(&self) -> Option<gst::BufferMap<gst::buffer::Readable>> {
self.0.buffer().and_then(|x| x.map_readable().ok())
}
/// Get the Y-plane stride (line pitch) in bytes from the frame's VideoMeta.
/// This is critical for proper NV12 decoding, as the stride may differ from width.
pub fn stride(&self) -> Option<u32> {
self.0.buffer().and_then(|buffer| {
buffer
.meta::<VideoMeta>()
.map(|meta| meta.stride()[0] as u32)
})
}
}
#[derive(Debug)]
@@ -284,8 +295,6 @@ impl Video {
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))?;
// 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 = framerate.numer() as f64 / framerate.denom() as f64;
@@ -305,7 +314,7 @@ impl Video {
.unwrap_or(0),
);
let sync_av = pipeline.has_property("av-offset");
let sync_av = pipeline.has_property("av-offset", None);
// NV12 = 12bpp
let frame = Arc::new(Mutex::new(Frame::empty()));
@@ -612,11 +621,12 @@ impl Video {
}
let frame_guard = inner.frame.lock().map_err(|_| Error::Lock)?;
let frame = frame_guard.readable().ok_or(Error::Lock)?;
let stride = frame_guard.stride();
Ok(img::Handle::from_rgba(
inner.width as u32 / downscale,
inner.height as u32 / downscale,
yuv_to_rgba(frame.as_slice(), width as _, height as _, downscale),
yuv_to_rgba(frame.as_slice(), width as _, height as _, downscale, stride),
))
})
.collect()
@@ -630,8 +640,17 @@ impl Video {
}
}
fn yuv_to_rgba(yuv: &[u8], width: u32, height: u32, downscale: u32) -> Vec<u8> {
let uv_start = width * height;
fn yuv_to_rgba(
yuv: &[u8],
width: u32,
height: u32,
downscale: u32,
stride: Option<u32>,
) -> Vec<u8> {
// Use stride from VideoMeta if available, otherwise assume stride == width
let stride = stride.unwrap_or(width);
let uv_start = stride * height;
let mut rgba = vec![];
for y in 0..height / downscale {
@@ -639,11 +658,16 @@ fn yuv_to_rgba(yuv: &[u8], width: u32, height: u32, downscale: u32) -> Vec<u8> {
let x_src = x * downscale;
let y_src = y * downscale;
let uv_i = uv_start + width * (y_src / 2) + x_src / 2 * 2;
// NV12 memory layout with stride:
// Y plane: stride bytes per row, starting at offset 0
// UV plane: stride bytes per row (same stride), starting at offset stride * height
// Each pixel is 1 byte Y, and every 2x2 block shares 2 bytes (U, V)
let y_offset = (y_src * stride + x_src) as usize;
let uv_offset = (uv_start + (y_src / 2) * stride + (x_src / 2) * 2) as usize;
let y = yuv[(y_src * width + x_src) as usize] as f32;
let u = yuv[uv_i as usize] as f32;
let v = yuv[(uv_i + 1) as usize] as f32;
let y = yuv[y_offset] as f32;
let u = yuv[uv_offset] as f32;
let v = yuv[uv_offset + 1] as f32;
let r = 1.164 * (y - 16.0) + 1.596 * (v - 128.0);
let g = 1.164 * (y - 16.0) - 0.813 * (v - 128.0) - 0.391 * (u - 128.0);

View File

@@ -1,12 +1,12 @@
use crate::{pipeline::VideoPrimitive, video::Video};
use gstreamer as gst;
use iced::{
advanced::{self, layout, widget, Widget},
Element,
advanced::{self, Widget, layout, widget},
};
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).
@@ -214,7 +214,7 @@ where
fn update(
&mut self,
_state: &mut widget::Tree,
_tree: &mut widget::Tree,
event: &iced::Event,
_layout: advanced::Layout<'_>,
_cursor: advanced::mouse::Cursor,
@@ -228,6 +228,7 @@ where
if let iced::Event::Window(iced::window::Event::RedrawRequested(_)) = event {
if inner.restart_stream || (!inner.is_eos && !inner.paused()) {
let mut restart_stream = false;
let emit_eos = !inner.restart_stream;
if inner.restart_stream {
restart_stream = true;
// Set flag to false to avoid potentially multiple seeks
@@ -247,7 +248,9 @@ where
};
}
gst::MessageView::Eos(_eos) => {
if let Some(on_end_of_stream) = self.on_end_of_stream.clone() {
if emit_eos
&& let Some(on_end_of_stream) = self.on_end_of_stream.clone()
{
shell.publish(on_end_of_stream);
}
if inner.looping {
@@ -285,8 +288,11 @@ where
}
shell.request_redraw();
shell.capture_event();
} else {
shell.request_redraw();
shell.request_redraw_at(iced::window::RedrawRequest::At(
Instant::now() + Duration::from_millis(32),
));
}
}
}