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
|
/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"
|
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"]
|
||||||
|
|
|
@ -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,22 +26,24 @@ impl Application for App {
|
||||||
type Flags = ();
|
type Flags = ();
|
||||||
|
|
||||||
fn new(_flags: ()) -> (Self, Command<Message>) {
|
fn new(_flags: ()) -> (Self, Command<Message>) {
|
||||||
|
let video = VideoPlayer::new(
|
||||||
|
&url::Url::from_file_path(
|
||||||
|
std::path::PathBuf::from(file!())
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("../.media/test.mp4")
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
(
|
(
|
||||||
App {
|
App {
|
||||||
video: VideoPlayer::new(
|
video,
|
||||||
&url::Url::from_file_path(
|
|
||||||
std::path::PathBuf::from(file!())
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.join("../.media/test.mp4")
|
|
||||||
.canonicalize()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
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
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 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);
|
||||||
|
}
|
||||||
|
} else if eos_pause {
|
||||||
|
self.is_eos = true;
|
||||||
|
self.set_paused(true);
|
||||||
|
}
|
||||||
|
return Command::batch(cmds);
|
||||||
}
|
}
|
||||||
_ => Command::none(),
|
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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue