feat: add looping, fix some issues with minimal example, update deps
This commit is contained in:
parent
cb7edfea36
commit
00019a036a
9 changed files with 3248 additions and 84 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake || use nix shell.nix
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
.direnv
|
2956
Cargo.lock
generated
Normal file
2956
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
30
Cargo.toml
30
Cargo.toml
|
@ -3,15 +3,31 @@ name = "iced_video_player"
|
|||
version = "0.1.0"
|
||||
authors = ["jazzfool"]
|
||||
edition = "2018"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
iced = { git = "https://github.com/hecrj/iced.git", features = ["image", "tokio"] }
|
||||
iced_native = "0.3.0"
|
||||
gstreamer = "0.16"
|
||||
gstreamer-app = "0.16" # appsink
|
||||
glib = "0.10" # gobject traits and error type
|
||||
tokio = { version = "1.2.0", features = ["time"] }
|
||||
iced = { git = "https://github.com/yusdacra/iced.git", branch = "crust", features = ["image", "tokio"] }
|
||||
iced_native = { git = "https://github.com/yusdacra/iced.git", branch = "crust" }
|
||||
gstreamer = "0.17"
|
||||
gstreamer-app = "0.17" # appsink
|
||||
glib = "0.14" # gobject traits and error type
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
thiserror = "1"
|
||||
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)
|
||||
|
||||
[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"]
|
||||
|
|
|
@ -4,18 +4,20 @@ use iced::{
|
|||
use iced_video_player::{VideoPlayer, VideoPlayerMessage};
|
||||
|
||||
fn main() {
|
||||
App::run(Default::default());
|
||||
App::run(Default::default()).unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
TogglePause,
|
||||
ToggleLoop,
|
||||
VideoPlayerMessage(VideoPlayerMessage),
|
||||
}
|
||||
|
||||
struct App {
|
||||
video: VideoPlayer,
|
||||
pause_btn: button::State,
|
||||
loop_btn: button::State,
|
||||
}
|
||||
|
||||
impl Application for App {
|
||||
|
@ -24,9 +26,7 @@ impl Application for App {
|
|||
type Flags = ();
|
||||
|
||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||
(
|
||||
App {
|
||||
video: VideoPlayer::new(
|
||||
let video = VideoPlayer::new(
|
||||
&url::Url::from_file_path(
|
||||
std::path::PathBuf::from(file!())
|
||||
.parent()
|
||||
|
@ -38,8 +38,12 @@ impl Application for App {
|
|||
.unwrap(),
|
||||
false,
|
||||
)
|
||||
.unwrap(),
|
||||
.unwrap();
|
||||
(
|
||||
App {
|
||||
video,
|
||||
pause_btn: Default::default(),
|
||||
loop_btn: Default::default(),
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
|
@ -49,13 +53,16 @@ impl Application for App {
|
|||
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 {
|
||||
Message::TogglePause => {
|
||||
self.video.set_paused(!self.video.paused());
|
||||
}
|
||||
Message::ToggleLoop => {
|
||||
self.video.set_looping(!self.video.looping());
|
||||
}
|
||||
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),
|
||||
)
|
||||
.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!(
|
||||
"{:#?}s / {:#?}s",
|
||||
self.video.position().unwrap().as_secs(),
|
||||
self.video.position().as_secs(),
|
||||
self.video.duration().as_secs()
|
||||
))),
|
||||
)
|
||||
|
|
98
flake.lock
generated
Normal file
98
flake.lock
generated
Normal 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
28
flake.nix
Normal 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
12
shell.nix
Normal 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
|
153
src/lib.rs
153
src/lib.rs
|
@ -3,6 +3,8 @@ use gstreamer as gst;
|
|||
use gstreamer_app as gst_app;
|
||||
use iced::{image as img, Command, Image, Subscription};
|
||||
use num_traits::ToPrimitive;
|
||||
use std::convert::identity;
|
||||
use std::future;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
@ -22,7 +24,7 @@ impl From<Position> for gst::GenericFormattedValue {
|
|||
fn from(pos: Position) -> Self {
|
||||
match pos {
|
||||
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,
|
||||
}
|
||||
|
||||
impl VideoPlayerMessage {
|
||||
fn into_cmd(self) -> Command<Self> {
|
||||
Command::perform(future::ready(self), identity)
|
||||
}
|
||||
}
|
||||
|
||||
/// Video player which handles multimedia playback.
|
||||
pub struct VideoPlayer {
|
||||
bus: gst::Bus,
|
||||
|
@ -83,6 +91,9 @@ pub struct VideoPlayer {
|
|||
wait: mpsc::Receiver<()>,
|
||||
paused: bool,
|
||||
muted: bool,
|
||||
looping: bool,
|
||||
is_eos: bool,
|
||||
restart_stream: bool,
|
||||
}
|
||||
|
||||
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 = source.downcast::<gst::Bin>().unwrap();
|
||||
|
||||
let video_sink: gst::Element = source
|
||||
.get_property("video-sink")
|
||||
.unwrap()
|
||||
.get()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let pad = video_sink.get_pads().get(0).cloned().unwrap();
|
||||
let video_sink: gst::Element = source.property("video-sink").unwrap().get().unwrap();
|
||||
let pad = video_sink.pads().get(0).cloned().unwrap();
|
||||
let pad = pad.dynamic_cast::<gst::GhostPad>().unwrap();
|
||||
let bin = pad
|
||||
.get_parent_element()
|
||||
.parent_element()
|
||||
.unwrap()
|
||||
.downcast::<gst::Bin>()
|
||||
.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 frame = Arc::new(Mutex::new(None));
|
||||
|
@ -131,21 +137,15 @@ impl VideoPlayer {
|
|||
gst_app::AppSinkCallbacks::builder()
|
||||
.new_sample(move |sink| {
|
||||
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 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 s = caps.get_structure(0).ok_or(gst::FlowError::Error)?;
|
||||
let width = s
|
||||
.get::<i32>("width")
|
||||
.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)?;
|
||||
let caps = pad.current_caps().ok_or(gst::FlowError::Error)?;
|
||||
let s = caps.structure(0).ok_or(gst::FlowError::Error)?;
|
||||
let width = s.get::<i32>("width").map_err(|_| gst::FlowError::Error)?;
|
||||
let height = s.get::<i32>("height").map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
*frame_ref.lock().map_err(|_| gst::FlowError::Error)? =
|
||||
Some(img::Handle::from_pixels(
|
||||
|
@ -164,44 +164,36 @@ impl VideoPlayer {
|
|||
source.set_state(gst::State::Playing)?;
|
||||
|
||||
// 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
|
||||
// TODO(jazzfool): maybe we want to extract some other information too?
|
||||
let caps = pad.get_current_caps().ok_or(Error::Caps)?;
|
||||
let s = caps.get_structure(0).ok_or(Error::Caps)?;
|
||||
let width = s
|
||||
.get::<i32>("width")
|
||||
.map_err(|_| Error::Caps)?
|
||||
.ok_or(Error::Caps)?;
|
||||
let height = s
|
||||
.get::<i32>("height")
|
||||
.map_err(|_| Error::Caps)?
|
||||
.ok_or(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)?;
|
||||
let framerate = s
|
||||
.get::<gst::Fraction>("framerate")
|
||||
.map_err(|_| Error::Caps)?
|
||||
.ok_or(Error::Caps)?;
|
||||
.map_err(|_| Error::Caps)?;
|
||||
|
||||
let duration = if !live {
|
||||
std::time::Duration::from_nanos(
|
||||
source
|
||||
.query_duration::<gst::ClockTime>()
|
||||
.ok_or(Error::Duration)?
|
||||
.nanoseconds()
|
||||
.ok_or(Error::Duration)?,
|
||||
.nseconds(),
|
||||
)
|
||||
} else {
|
||||
std::time::Duration::from_secs(0)
|
||||
};
|
||||
|
||||
Ok(VideoPlayer {
|
||||
bus: source.get_bus().unwrap(),
|
||||
bus: source.bus().unwrap(),
|
||||
source,
|
||||
|
||||
width,
|
||||
height,
|
||||
framerate: num_rational::Rational::new(
|
||||
framerate: num_rational::Rational32::new(
|
||||
*framerate.numer() as _,
|
||||
*framerate.denom() as _,
|
||||
)
|
||||
|
@ -212,15 +204,20 @@ impl VideoPlayer {
|
|||
wait,
|
||||
paused: false,
|
||||
muted: false,
|
||||
looping: false,
|
||||
is_eos: false,
|
||||
restart_stream: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the size/resolution of the video as `(width, height)`.
|
||||
#[inline(always)]
|
||||
pub fn size(&self) -> (i32, i32) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Get the framerate of the video as frames per second.
|
||||
#[inline(always)]
|
||||
pub fn framerate(&self) -> f64 {
|
||||
self.framerate
|
||||
}
|
||||
|
@ -240,13 +237,28 @@ impl VideoPlayer {
|
|||
}
|
||||
|
||||
/// Get if the audio is muted or not.
|
||||
#[inline(always)]
|
||||
pub fn muted(&self) -> bool {
|
||||
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.
|
||||
pub fn set_paused(&mut self, paused: bool) {
|
||||
self.paused = paused;
|
||||
self.source
|
||||
.set_state(if paused {
|
||||
gst::State::Paused
|
||||
|
@ -254,9 +266,14 @@ impl VideoPlayer {
|
|||
gst::State::Playing
|
||||
})
|
||||
.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.
|
||||
#[inline(always)]
|
||||
pub fn paused(&self) -> bool {
|
||||
self.paused
|
||||
}
|
||||
|
@ -270,16 +287,17 @@ impl VideoPlayer {
|
|||
}
|
||||
|
||||
/// 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(
|
||||
self.source
|
||||
.query_position::<gst::ClockTime>()?
|
||||
.nanoseconds()?,
|
||||
.query_position::<gst::ClockTime>()
|
||||
.map_or(0, |pos| pos.nseconds()),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Get the media duration.
|
||||
#[inline(always)]
|
||||
pub fn duration(&self) -> std::time::Duration {
|
||||
self.duration
|
||||
}
|
||||
|
@ -289,8 +307,8 @@ impl VideoPlayer {
|
|||
/// 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.
|
||||
pub fn thumbnails(&mut self, positions: &[Position]) -> Result<Vec<img::Handle>, Error> {
|
||||
let paused = self.paused;
|
||||
let pos = self.position().ok_or(Error::Duration)?;
|
||||
let paused = self.paused();
|
||||
let pos = self.position();
|
||||
self.set_paused(false);
|
||||
let out = positions
|
||||
.iter()
|
||||
|
@ -308,28 +326,45 @@ impl VideoPlayer {
|
|||
pub fn update(&mut self, message: VideoPlayerMessage) -> Command<VideoPlayerMessage> {
|
||||
match message {
|
||||
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() {
|
||||
match msg.view() {
|
||||
gst::MessageView::Error(err) => panic!("{:#?}", err),
|
||||
gst::MessageView::Eos(_eos) => {
|
||||
cmd = Command::batch(vec![
|
||||
cmd,
|
||||
Command::perform(async {}, |_| VideoPlayerMessage::EndOfPlayback),
|
||||
])
|
||||
cmds.push(VideoPlayerMessage::EndOfPlayback.into_cmd());
|
||||
if self.looping {
|
||||
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> {
|
||||
if !self.paused {
|
||||
time::every(Duration::from_secs_f64(0.5 / self.framerate))
|
||||
if self.restart_stream || (!self.is_eos && !self.paused()) {
|
||||
iced::time::every(Duration::from_secs_f64(0.5 / self.framerate))
|
||||
.map(|_| VideoPlayerMessage::NextFrame)
|
||||
} else {
|
||||
Subscription::none()
|
||||
|
@ -349,11 +384,11 @@ impl VideoPlayer {
|
|||
pub fn frame_view(&mut self) -> Image {
|
||||
Image::new(self.frame_image())
|
||||
}
|
||||
}
|
||||
|
||||
// until iced 0.2 is released, which has this built-in
|
||||
mod time {
|
||||
pub fn every(duration: std::time::Duration) -> iced::Subscription<std::time::Instant> {
|
||||
iced::time::every(duration)
|
||||
pub fn restart_stream(&mut self) -> Result<(), Error> {
|
||||
self.is_eos = false;
|
||||
self.set_paused(false);
|
||||
self.seek(0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue