feat: add looping, fix some issues with minimal example, update deps

This commit is contained in:
Yusuf Bera Ertan 2021-08-25 16:39:23 +03:00
parent cb7edfea36
commit 00019a036a
No known key found for this signature in database
GPG key ID: 1D8F8FAF2294D6EA
9 changed files with 3248 additions and 84 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake || use nix shell.nix

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
/target /target
Cargo.lock .direnv

2956
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,15 +3,31 @@ name = "iced_video_player"
version = "0.1.0" version = "0.1.0"
authors = ["jazzfool"] authors = ["jazzfool"]
edition = "2018" edition = "2018"
resolver = "2"
[dependencies] [dependencies]
iced = { git = "https://github.com/hecrj/iced.git", features = ["image", "tokio"] } iced = { git = "https://github.com/yusdacra/iced.git", branch = "crust", features = ["image", "tokio"] }
iced_native = "0.3.0" iced_native = { git = "https://github.com/yusdacra/iced.git", branch = "crust" }
gstreamer = "0.16" gstreamer = "0.17"
gstreamer-app = "0.16" # appsink gstreamer-app = "0.17" # appsink
glib = "0.10" # gobject traits and error type glib = "0.14" # gobject traits and error type
tokio = { version = "1.2.0", features = ["time"] } tokio = { version = "1", features = ["time"] }
thiserror = "1" thiserror = "1"
url = "2" # media uri url = "2" # media uri
num-rational = "0.3" # framerates come in rationals num-rational = "0.4" # framerates come in rationals
num-traits = "0.2" # convert rationals to floats (ToPrimitive) num-traits = "0.2" # convert rationals to floats (ToPrimitive)
[package.metadata.nix]
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",
]
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"]

View file

@ -4,18 +4,20 @@ use iced::{
use iced_video_player::{VideoPlayer, VideoPlayerMessage}; use iced_video_player::{VideoPlayer, VideoPlayerMessage};
fn main() { fn main() {
App::run(Default::default()); App::run(Default::default()).unwrap();
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum Message { enum Message {
TogglePause, TogglePause,
ToggleLoop,
VideoPlayerMessage(VideoPlayerMessage), VideoPlayerMessage(VideoPlayerMessage),
} }
struct App { struct App {
video: VideoPlayer, video: VideoPlayer,
pause_btn: button::State, pause_btn: button::State,
loop_btn: button::State,
} }
impl Application for App { impl Application for App {
@ -24,9 +26,7 @@ impl Application for App {
type Flags = (); type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) { fn new(_flags: ()) -> (Self, Command<Message>) {
( let video = VideoPlayer::new(
App {
video: VideoPlayer::new(
&url::Url::from_file_path( &url::Url::from_file_path(
std::path::PathBuf::from(file!()) std::path::PathBuf::from(file!())
.parent() .parent()
@ -38,8 +38,12 @@ impl Application for App {
.unwrap(), .unwrap(),
false, false,
) )
.unwrap(), .unwrap();
(
App {
video,
pause_btn: Default::default(), pause_btn: Default::default(),
loop_btn: Default::default(),
}, },
Command::none(), Command::none(),
) )
@ -49,13 +53,16 @@ impl Application for App {
String::from("Video Player") String::from("Video Player")
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message, _: &mut iced::Clipboard) -> Command<Message> {
match message { match message {
Message::TogglePause => { Message::TogglePause => {
self.video.set_paused(!self.video.paused()); self.video.set_paused(!self.video.paused());
} }
Message::ToggleLoop => {
self.video.set_looping(!self.video.looping());
}
Message::VideoPlayerMessage(msg) => { Message::VideoPlayerMessage(msg) => {
self.video.update(msg); return self.video.update(msg).map(Message::VideoPlayerMessage);
} }
} }
@ -79,9 +86,20 @@ impl Application for App {
) )
.on_press(Message::TogglePause), .on_press(Message::TogglePause),
) )
.push(
Button::new(
&mut self.loop_btn,
Text::new(if self.video.looping() {
"Disable Loop"
} else {
"Enable Loop"
}),
)
.on_press(Message::ToggleLoop),
)
.push(Text::new(format!( .push(Text::new(format!(
"{:#?}s / {:#?}s", "{:#?}s / {:#?}s",
self.video.position().unwrap().as_secs(), self.video.position().as_secs(),
self.video.duration().as_secs() self.video.duration().as_secs()
))), ))),
) )

98
flake.lock generated Normal file
View file

@ -0,0 +1,98 @@
{
"nodes": {
"devshell": {
"locked": {
"lastModified": 1629275356,
"narHash": "sha256-R17M69EKXP6q8/mNHaK53ECwjFo1pdF+XaJC9Qq8zjg=",
"owner": "numtide",
"repo": "devshell",
"rev": "26f25a12265f030917358a9632cd600b51af1d97",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"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": {
"devshell": "devshell",
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": "rustOverlay"
},
"locked": {
"lastModified": 1629871751,
"narHash": "sha256-QjnDg34ApcnjmXlNLnbHswT9OroCPY7Wip6r9Zkgkfo=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "4f164ecad242537d5893426eef02c47c9e5ced59",
"type": "github"
},
"original": {
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1629618782,
"narHash": "sha256-2K8SSXu3alo/URI3MClGdDSns6Gb4ZaW4LET53UWyKk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "870959c7fb3a42af1863bed9e1756086a74eb649",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs"
}
},
"rustOverlay": {
"flake": false,
"locked": {
"lastModified": 1629857564,
"narHash": "sha256-dClWiHkbaCDaIl520Miri66UOA8OecWbaVTWJBajHyM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "88848c36934318e16c86097f65dbf97a57968d81",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

28
flake.nix Normal file
View file

@ -0,0 +1,28 @@
{
inputs = {
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
};
};
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}";
}
];
};
};
};
}

12
shell.nix Normal file
View file

@ -0,0 +1,12 @@
# Flake's devShell for non-flake-enabled nix instances
(import
(
let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url =
"https://github.com/edolstra/flake-compat/archive/${lock.nodes.flakeCompat.locked.rev}.tar.gz";
sha256 = lock.nodes.flakeCompat.locked.narHash;
}
)
{ src = ./.; }).shellNix.default

View file

@ -3,6 +3,8 @@ use gstreamer as gst;
use gstreamer_app as gst_app; use gstreamer_app as gst_app;
use iced::{image as img, Command, Image, Subscription}; use iced::{image as img, Command, Image, Subscription};
use num_traits::ToPrimitive; use num_traits::ToPrimitive;
use std::convert::identity;
use std::future;
use std::sync::{mpsc, Arc, Mutex}; use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration; use std::time::Duration;
use thiserror::Error; use thiserror::Error;
@ -22,7 +24,7 @@ impl From<Position> for gst::GenericFormattedValue {
fn from(pos: Position) -> Self { fn from(pos: Position) -> Self {
match pos { match pos {
Position::Time(t) => gst::ClockTime::from_nseconds(t.as_nanos() as _).into(), Position::Time(t) => gst::ClockTime::from_nseconds(t.as_nanos() as _).into(),
Position::Frame(f) => gst::format::Default(Some(f)).into(), Position::Frame(f) => gst::format::Default(f).into(),
} }
} }
} }
@ -69,6 +71,12 @@ pub enum VideoPlayerMessage {
EndOfPlayback, EndOfPlayback,
} }
impl VideoPlayerMessage {
fn into_cmd(self) -> Command<Self> {
Command::perform(future::ready(self), identity)
}
}
/// Video player which handles multimedia playback. /// Video player which handles multimedia playback.
pub struct VideoPlayer { pub struct VideoPlayer {
bus: gst::Bus, bus: gst::Bus,
@ -83,6 +91,9 @@ pub struct VideoPlayer {
wait: mpsc::Receiver<()>, wait: mpsc::Receiver<()>,
paused: bool, paused: bool,
muted: bool, muted: bool,
looping: bool,
is_eos: bool,
restart_stream: bool,
} }
impl Drop for VideoPlayer { impl Drop for VideoPlayer {
@ -105,21 +116,16 @@ impl VideoPlayer {
let source = gst::parse_launch(&format!("playbin uri=\"{}\" video-sink=\"videoconvert ! videoscale ! appsink name=app_sink caps=video/x-raw,format=BGRA,pixel-aspect-ratio=1/1\"", uri.as_str()))?; let source = gst::parse_launch(&format!("playbin uri=\"{}\" video-sink=\"videoconvert ! videoscale ! appsink name=app_sink caps=video/x-raw,format=BGRA,pixel-aspect-ratio=1/1\"", uri.as_str()))?;
let source = source.downcast::<gst::Bin>().unwrap(); let source = source.downcast::<gst::Bin>().unwrap();
let video_sink: gst::Element = source let video_sink: gst::Element = source.property("video-sink").unwrap().get().unwrap();
.get_property("video-sink") let pad = video_sink.pads().get(0).cloned().unwrap();
.unwrap()
.get()
.unwrap()
.unwrap();
let pad = video_sink.get_pads().get(0).cloned().unwrap();
let pad = pad.dynamic_cast::<gst::GhostPad>().unwrap(); let pad = pad.dynamic_cast::<gst::GhostPad>().unwrap();
let bin = pad let bin = pad
.get_parent_element() .parent_element()
.unwrap() .unwrap()
.downcast::<gst::Bin>() .downcast::<gst::Bin>()
.unwrap(); .unwrap();
let app_sink = bin.get_by_name("app_sink").unwrap(); let app_sink = bin.by_name("app_sink").unwrap();
let app_sink = app_sink.downcast::<gst_app::AppSink>().unwrap(); let app_sink = app_sink.downcast::<gst_app::AppSink>().unwrap();
let frame = Arc::new(Mutex::new(None)); let frame = Arc::new(Mutex::new(None));
@ -131,21 +137,15 @@ impl VideoPlayer {
gst_app::AppSinkCallbacks::builder() gst_app::AppSinkCallbacks::builder()
.new_sample(move |sink| { .new_sample(move |sink| {
let sample = sink.pull_sample().map_err(|_| gst::FlowError::Eos)?; let sample = sink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
let buffer = sample.get_buffer().ok_or(gst::FlowError::Error)?; let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?; let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
let pad = sink.get_static_pad("sink").ok_or(gst::FlowError::Error)?; let pad = sink.static_pad("sink").ok_or(gst::FlowError::Error)?;
let caps = pad.get_current_caps().ok_or(gst::FlowError::Error)?; let caps = pad.current_caps().ok_or(gst::FlowError::Error)?;
let s = caps.get_structure(0).ok_or(gst::FlowError::Error)?; let s = caps.structure(0).ok_or(gst::FlowError::Error)?;
let width = s let width = s.get::<i32>("width").map_err(|_| gst::FlowError::Error)?;
.get::<i32>("width") let height = s.get::<i32>("height").map_err(|_| gst::FlowError::Error)?;
.map_err(|_| gst::FlowError::Error)?
.ok_or(gst::FlowError::Error)?;
let height = s
.get::<i32>("height")
.map_err(|_| gst::FlowError::Error)?
.ok_or(gst::FlowError::Error)?;
*frame_ref.lock().map_err(|_| gst::FlowError::Error)? = *frame_ref.lock().map_err(|_| gst::FlowError::Error)? =
Some(img::Handle::from_pixels( Some(img::Handle::from_pixels(
@ -164,44 +164,36 @@ impl VideoPlayer {
source.set_state(gst::State::Playing)?; source.set_state(gst::State::Playing)?;
// wait for up to 5 seconds until the decoder gets the source capabilities // wait for up to 5 seconds until the decoder gets the source capabilities
source.get_state(gst::ClockTime::from_seconds(5)).0?; source.state(gst::ClockTime::from_seconds(5)).0?;
// extract resolution and framerate // extract resolution and framerate
// TODO(jazzfool): maybe we want to extract some other information too? // TODO(jazzfool): maybe we want to extract some other information too?
let caps = pad.get_current_caps().ok_or(Error::Caps)?; let caps = pad.current_caps().ok_or(Error::Caps)?;
let s = caps.get_structure(0).ok_or(Error::Caps)?; let s = caps.structure(0).ok_or(Error::Caps)?;
let width = s let width = s.get::<i32>("width").map_err(|_| Error::Caps)?;
.get::<i32>("width") let height = s.get::<i32>("height").map_err(|_| Error::Caps)?;
.map_err(|_| Error::Caps)?
.ok_or(Error::Caps)?;
let height = s
.get::<i32>("height")
.map_err(|_| Error::Caps)?
.ok_or(Error::Caps)?;
let framerate = s let framerate = s
.get::<gst::Fraction>("framerate") .get::<gst::Fraction>("framerate")
.map_err(|_| Error::Caps)? .map_err(|_| Error::Caps)?;
.ok_or(Error::Caps)?;
let duration = if !live { let duration = if !live {
std::time::Duration::from_nanos( std::time::Duration::from_nanos(
source source
.query_duration::<gst::ClockTime>() .query_duration::<gst::ClockTime>()
.ok_or(Error::Duration)? .ok_or(Error::Duration)?
.nanoseconds() .nseconds(),
.ok_or(Error::Duration)?,
) )
} else { } else {
std::time::Duration::from_secs(0) std::time::Duration::from_secs(0)
}; };
Ok(VideoPlayer { Ok(VideoPlayer {
bus: source.get_bus().unwrap(), bus: source.bus().unwrap(),
source, source,
width, width,
height, height,
framerate: num_rational::Rational::new( framerate: num_rational::Rational32::new(
*framerate.numer() as _, *framerate.numer() as _,
*framerate.denom() as _, *framerate.denom() as _,
) )
@ -212,15 +204,20 @@ impl VideoPlayer {
wait, wait,
paused: false, paused: false,
muted: false, muted: false,
looping: false,
is_eos: false,
restart_stream: false,
}) })
} }
/// Get the size/resolution of the video as `(width, height)`. /// Get the size/resolution of the video as `(width, height)`.
#[inline(always)]
pub fn size(&self) -> (i32, i32) { pub fn size(&self) -> (i32, i32) {
(self.width, self.height) (self.width, self.height)
} }
/// Get the framerate of the video as frames per second. /// Get the framerate of the video as frames per second.
#[inline(always)]
pub fn framerate(&self) -> f64 { pub fn framerate(&self) -> f64 {
self.framerate self.framerate
} }
@ -240,13 +237,28 @@ impl VideoPlayer {
} }
/// Get if the audio is muted or not. /// Get if the audio is muted or not.
#[inline(always)]
pub fn muted(&self) -> bool { pub fn muted(&self) -> bool {
self.muted self.muted
} }
#[inline(always)]
pub fn eos(&self) -> bool {
self.is_eos
}
#[inline(always)]
pub fn looping(&self) -> bool {
self.looping
}
#[inline(always)]
pub fn set_looping(&mut self, looping: bool) {
self.looping = looping;
}
/// Set if the media is paused or not. /// Set if the media is paused or not.
pub fn set_paused(&mut self, paused: bool) { pub fn set_paused(&mut self, paused: bool) {
self.paused = paused;
self.source self.source
.set_state(if paused { .set_state(if paused {
gst::State::Paused gst::State::Paused
@ -254,9 +266,14 @@ impl VideoPlayer {
gst::State::Playing gst::State::Playing
}) })
.unwrap(/* state was changed in ctor; state errors caught there */); .unwrap(/* state was changed in ctor; state errors caught there */);
self.paused = paused;
if self.is_eos && !paused {
self.restart_stream = true;
}
} }
/// Get if the media is paused or not. /// Get if the media is paused or not.
#[inline(always)]
pub fn paused(&self) -> bool { pub fn paused(&self) -> bool {
self.paused self.paused
} }
@ -270,16 +287,17 @@ impl VideoPlayer {
} }
/// Get the current playback position in time. /// Get the current playback position in time.
pub fn position(&self) -> Option<std::time::Duration> { pub fn position(&self) -> std::time::Duration {
std::time::Duration::from_nanos( std::time::Duration::from_nanos(
self.source self.source
.query_position::<gst::ClockTime>()? .query_position::<gst::ClockTime>()
.nanoseconds()?, .map_or(0, |pos| pos.nseconds()),
) )
.into() .into()
} }
/// Get the media duration. /// Get the media duration.
#[inline(always)]
pub fn duration(&self) -> std::time::Duration { pub fn duration(&self) -> std::time::Duration {
self.duration self.duration
} }
@ -289,8 +307,8 @@ impl VideoPlayer {
/// Slow; only needs to be called once for each instance. /// Slow; only needs to be called once for each instance.
/// It's best to call this at the very start of playback, otherwise the position may shift. /// It's best to call this at the very start of playback, otherwise the position may shift.
pub fn thumbnails(&mut self, positions: &[Position]) -> Result<Vec<img::Handle>, Error> { pub fn thumbnails(&mut self, positions: &[Position]) -> Result<Vec<img::Handle>, Error> {
let paused = self.paused; let paused = self.paused();
let pos = self.position().ok_or(Error::Duration)?; let pos = self.position();
self.set_paused(false); self.set_paused(false);
let out = positions let out = positions
.iter() .iter()
@ -308,28 +326,45 @@ impl VideoPlayer {
pub fn update(&mut self, message: VideoPlayerMessage) -> Command<VideoPlayerMessage> { pub fn update(&mut self, message: VideoPlayerMessage) -> Command<VideoPlayerMessage> {
match message { match message {
VideoPlayerMessage::NextFrame => { VideoPlayerMessage::NextFrame => {
let mut cmd = Command::none(); let mut cmds = Vec::new();
let mut restart_stream = false;
if self.restart_stream {
restart_stream = true;
self.restart_stream = false;
}
let mut eos_pause = false;
for msg in self.bus.iter() { for msg in self.bus.iter() {
match msg.view() { match msg.view() {
gst::MessageView::Error(err) => panic!("{:#?}", err), gst::MessageView::Error(err) => panic!("{:#?}", err),
gst::MessageView::Eos(_eos) => { gst::MessageView::Eos(_eos) => {
cmd = Command::batch(vec![ cmds.push(VideoPlayerMessage::EndOfPlayback.into_cmd());
cmd, if self.looping {
Command::perform(async {}, |_| VideoPlayerMessage::EndOfPlayback), restart_stream = true;
]) } else {
eos_pause = true;
}
} }
_ => {} _ => {}
} }
} }
cmd if restart_stream {
if let Err(err) = self.restart_stream() {
eprintln!("cannot restart stream (can't seek): {:#?}", err);
} }
_ => Command::none(), } else if eos_pause {
self.is_eos = true;
self.set_paused(true);
} }
return Command::batch(cmds);
}
VideoPlayerMessage::EndOfPlayback => {}
}
Command::none()
} }
pub fn subscription(&self) -> Subscription<VideoPlayerMessage> { pub fn subscription(&self) -> Subscription<VideoPlayerMessage> {
if !self.paused { if self.restart_stream || (!self.is_eos && !self.paused()) {
time::every(Duration::from_secs_f64(0.5 / self.framerate)) iced::time::every(Duration::from_secs_f64(0.5 / self.framerate))
.map(|_| VideoPlayerMessage::NextFrame) .map(|_| VideoPlayerMessage::NextFrame)
} else { } else {
Subscription::none() Subscription::none()
@ -349,11 +384,11 @@ impl VideoPlayer {
pub fn frame_view(&mut self) -> Image { pub fn frame_view(&mut self) -> Image {
Image::new(self.frame_image()) Image::new(self.frame_image())
} }
}
// until iced 0.2 is released, which has this built-in pub fn restart_stream(&mut self) -> Result<(), Error> {
mod time { self.is_eos = false;
pub fn every(duration: std::time::Duration) -> iced::Subscription<std::time::Instant> { self.set_paused(false);
iced::time::every(duration) self.seek(0)?;
Ok(())
} }
} }