[Feat] AB repeat and frame stepping (#241)
* feat: Add A-B repeat and frame navigation features with new key binds and menu entries. * playback options * fix merge
This commit is contained in:
parent
069337e2ca
commit
6f5040a953
4 changed files with 139 additions and 21 deletions
|
|
@ -37,3 +37,11 @@ quit = Quit
|
||||||
|
|
||||||
repeat-disabled = Repeat disabled
|
repeat-disabled = Repeat disabled
|
||||||
repeat-track = Repeat track
|
repeat-track = Repeat track
|
||||||
|
|
||||||
|
playback = Playback
|
||||||
|
next-frame = Next Frame
|
||||||
|
previous-frame = Previous Frame
|
||||||
|
ab-repeat = A-B Repeat
|
||||||
|
ab-repeat-set-a = A-B Repeat (A)
|
||||||
|
ab-repeat-set-b = A-B Repeat (B)
|
||||||
|
ab-repeat-clear = Clear A-B Repeat
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ pub fn key_binds() -> HashMap<KeyBind, Action> {
|
||||||
bind!([], Key::Character(" ".into()), PlayPause);
|
bind!([], Key::Character(" ".into()), PlayPause);
|
||||||
bind!([], Key::Named(Named::ArrowLeft), SeekBackward);
|
bind!([], Key::Named(Named::ArrowLeft), SeekBackward);
|
||||||
bind!([], Key::Named(Named::ArrowRight), SeekForward);
|
bind!([], Key::Named(Named::ArrowRight), SeekForward);
|
||||||
|
bind!([], Key::Character(".".into()), NextFrame);
|
||||||
|
bind!([], Key::Character(",".into()), PreviousFrame);
|
||||||
|
bind!([], Key::Character("a".into()), AbRepeat);
|
||||||
|
|
||||||
key_binds
|
key_binds
|
||||||
}
|
}
|
||||||
|
|
|
||||||
95
src/main.rs
95
src/main.rs
|
|
@ -80,6 +80,17 @@ fn language_name(code: &str) -> Option<String> {
|
||||||
Some(name.to_string())
|
Some(name.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_framerate(video: &Video) -> Option<f64> {
|
||||||
|
let pipeline = video.pipeline();
|
||||||
|
let video_sink: gst::Element = pipeline.property("video-sink");
|
||||||
|
let binding = video_sink.pads();
|
||||||
|
let pad = binding.first()?;
|
||||||
|
let caps = pad.current_caps()?;
|
||||||
|
let structure = caps.structure(0)?;
|
||||||
|
let framerate = structure.get::<gst::Fraction>("framerate").ok()?;
|
||||||
|
Some(framerate.numer() as f64 / framerate.denom() as f64)
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs application with these settings
|
/// Runs application with these settings
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
|
@ -178,6 +189,9 @@ pub enum Action {
|
||||||
PlayPause,
|
PlayPause,
|
||||||
SeekBackward,
|
SeekBackward,
|
||||||
SeekForward,
|
SeekForward,
|
||||||
|
NextFrame,
|
||||||
|
PreviousFrame,
|
||||||
|
AbRepeat,
|
||||||
WindowClose,
|
WindowClose,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,6 +212,9 @@ impl MenuAction for Action {
|
||||||
Self::PlayPause => Message::PlayPause,
|
Self::PlayPause => Message::PlayPause,
|
||||||
Self::SeekBackward => Message::SeekRelative(-10.0),
|
Self::SeekBackward => Message::SeekRelative(-10.0),
|
||||||
Self::SeekForward => Message::SeekRelative(10.0),
|
Self::SeekForward => Message::SeekRelative(10.0),
|
||||||
|
Self::NextFrame => Message::NextFrame,
|
||||||
|
Self::PreviousFrame => Message::PreviousFrame,
|
||||||
|
Self::AbRepeat => Message::AbRepeat,
|
||||||
Self::WindowClose => Message::WindowClose,
|
Self::WindowClose => Message::WindowClose,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -292,11 +309,14 @@ pub enum Message {
|
||||||
SeekRelative(f64),
|
SeekRelative(f64),
|
||||||
SeekRelease,
|
SeekRelease,
|
||||||
PlayNext,
|
PlayNext,
|
||||||
|
NextFrame,
|
||||||
|
PreviousFrame,
|
||||||
EndOfStream,
|
EndOfStream,
|
||||||
MissingPlugin(gst::Message),
|
MissingPlugin(gst::Message),
|
||||||
MprisChannel(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>),
|
MprisChannel(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>),
|
||||||
NewFrame,
|
NewFrame,
|
||||||
Reload,
|
Reload,
|
||||||
|
AbRepeat,
|
||||||
ShowControls,
|
ShowControls,
|
||||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||||
WindowClose,
|
WindowClose,
|
||||||
|
|
@ -326,6 +346,7 @@ pub struct App {
|
||||||
current_audio: i32,
|
current_audio: i32,
|
||||||
text_codes: Vec<TextCode>,
|
text_codes: Vec<TextCode>,
|
||||||
current_text: Option<i32>,
|
current_text: Option<i32>,
|
||||||
|
ab_repeat: Option<(Option<f64>, Option<f64>)>,
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[cfg(feature = "xdg-portal")]
|
||||||
inhibit: tokio::sync::watch::Sender<bool>,
|
inhibit: tokio::sync::watch::Sender<bool>,
|
||||||
}
|
}
|
||||||
|
|
@ -890,6 +911,7 @@ impl Application for App {
|
||||||
current_audio: -1,
|
current_audio: -1,
|
||||||
text_codes: Vec::new(),
|
text_codes: Vec::new(),
|
||||||
current_text: None,
|
current_text: None,
|
||||||
|
ab_repeat: None,
|
||||||
#[cfg(feature = "xdg-portal")]
|
#[cfg(feature = "xdg-portal")]
|
||||||
inhibit,
|
inhibit,
|
||||||
};
|
};
|
||||||
|
|
@ -1036,6 +1058,7 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
Message::FileLoad(url) => {
|
Message::FileLoad(url) => {
|
||||||
self.flags.url_opt = Some(url);
|
self.flags.url_opt = Some(url);
|
||||||
|
self.ab_repeat = None;
|
||||||
return self.load();
|
return self.load();
|
||||||
}
|
}
|
||||||
Message::FileOpen => {
|
Message::FileOpen => {
|
||||||
|
|
@ -1385,11 +1408,63 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::NextFrame => {
|
||||||
|
if let Some(video) = &mut self.video_opt {
|
||||||
|
video.pipeline().send_event(gst::event::Step::new(
|
||||||
|
gst::format::Buffers::from_u64(1),
|
||||||
|
1.0,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
video.set_paused(true);
|
||||||
|
self.update_controls(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::PreviousFrame => {
|
||||||
|
if let Some(video) = &mut self.video_opt {
|
||||||
|
// TODO: Improve Accuracy.
|
||||||
|
let current = video.position();
|
||||||
|
let fps = get_framerate(video).unwrap_or(30.0);
|
||||||
|
let frame_duration = Duration::from_secs_f64(1.0 / fps);
|
||||||
|
let target = current.saturating_sub(frame_duration + Duration::from_millis(1));
|
||||||
|
|
||||||
|
video.seek(target, true).expect("seek");
|
||||||
|
video.set_paused(true);
|
||||||
|
self.update_controls(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::AbRepeat => {
|
||||||
|
let current_opt = self.video_opt.as_ref().map(|v| v.position().as_secs_f64());
|
||||||
|
if let Some(current) = current_opt {
|
||||||
|
match self.ab_repeat {
|
||||||
|
None => {
|
||||||
|
self.ab_repeat = Some((Some(current), None));
|
||||||
|
}
|
||||||
|
Some((a, None)) => {
|
||||||
|
self.ab_repeat = Some((a, Some(current)));
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
self.ab_repeat = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.update_controls(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Message::EndOfStream => {
|
Message::EndOfStream => {
|
||||||
println!(
|
if let Some((a, _)) = self.ab_repeat {
|
||||||
"end of stream, repeat={:?}",
|
let target = a.unwrap_or(0.0);
|
||||||
self.flags.config_state.player_state.repeat
|
if let Some(video) = &mut self.video_opt {
|
||||||
);
|
video
|
||||||
|
.seek(Duration::from_secs_f64(target), true)
|
||||||
|
.expect("seek");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"end of stream, repeat={:?}",
|
||||||
|
self.flags.config_state.player_state.repeat
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::MissingPlugin(element) => {
|
Message::MissingPlugin(element) => {
|
||||||
|
|
@ -1458,9 +1533,18 @@ impl Application for App {
|
||||||
self.update_mpris_state();
|
self.update_mpris_state();
|
||||||
}
|
}
|
||||||
Message::NewFrame => {
|
Message::NewFrame => {
|
||||||
if let Some(video) = &self.video_opt {
|
if let Some(video) = &mut self.video_opt {
|
||||||
if !self.dragging {
|
if !self.dragging {
|
||||||
self.position = video.position().as_secs_f64();
|
self.position = video.position().as_secs_f64();
|
||||||
|
|
||||||
|
if let Some((a, b)) = self.ab_repeat {
|
||||||
|
let target_a = a.unwrap_or(0.0);
|
||||||
|
let target_b = b.unwrap_or(self.duration);
|
||||||
|
if self.position >= target_b {
|
||||||
|
let _ = video.seek(Duration::from_secs_f64(target_a), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.update_controls(self.dropdown_opt.is_some());
|
self.update_controls(self.dropdown_opt.is_some());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1487,6 +1571,7 @@ impl Application for App {
|
||||||
&self.flags.config_state,
|
&self.flags.config_state,
|
||||||
&self.key_binds,
|
&self.key_binds,
|
||||||
&self.projects,
|
&self.projects,
|
||||||
|
&self.ab_repeat,
|
||||||
)]
|
)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
54
src/menu.rs
54
src/menu.rs
|
|
@ -17,6 +17,7 @@ pub fn menu_bar<'a>(
|
||||||
config_state: &ConfigState,
|
config_state: &ConfigState,
|
||||||
key_binds: &HashMap<KeyBind, Action>,
|
key_binds: &HashMap<KeyBind, Action>,
|
||||||
projects: &[(String, PathBuf)],
|
projects: &[(String, PathBuf)],
|
||||||
|
ab_repeat: &Option<(Option<f64>, Option<f64>)>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let home_dir_opt = std::env::home_dir();
|
let home_dir_opt = std::env::home_dir();
|
||||||
let format_path = |path: &PathBuf| -> String {
|
let format_path = |path: &PathBuf| -> String {
|
||||||
|
|
@ -87,23 +88,44 @@ pub fn menu_bar<'a>(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuBar::new(vec![menu::Tree::with_children(
|
MenuBar::new(vec![
|
||||||
RcElementWrapper::new(Element::from(menu::root(fl!("file")))),
|
menu::Tree::with_children(
|
||||||
menu::items(
|
RcElementWrapper::new(Element::from(menu::root(fl!("file")))),
|
||||||
key_binds,
|
menu::items(
|
||||||
vec![
|
key_binds,
|
||||||
menu::Item::Button(fl!("open-media"), None, Action::FileOpen),
|
vec![
|
||||||
menu::Item::Folder(fl!("open-recent-media"), recent_files),
|
menu::Item::Button(fl!("open-media"), None, Action::FileOpen),
|
||||||
menu::Item::Button(fl!("close-file"), None, Action::FileClose),
|
menu::Item::Folder(fl!("open-recent-media"), recent_files),
|
||||||
menu::Item::Divider,
|
menu::Item::Button(fl!("close-file"), None, Action::FileClose),
|
||||||
menu::Item::Button(fl!("open-media-folder"), None, Action::FolderOpen),
|
menu::Item::Divider,
|
||||||
menu::Item::Folder(fl!("open-recent-media-folder"), recent_projects),
|
menu::Item::Button(fl!("open-media-folder"), None, Action::FolderOpen),
|
||||||
menu::Item::Folder(fl!("close-media-folder"), close_projects),
|
menu::Item::Folder(fl!("open-recent-media-folder"), recent_projects),
|
||||||
menu::Item::Divider,
|
menu::Item::Folder(fl!("close-media-folder"), close_projects),
|
||||||
menu::Item::Button(fl!("quit"), None, Action::WindowClose),
|
menu::Item::Divider,
|
||||||
],
|
menu::Item::Button(fl!("quit"), None, Action::WindowClose),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)])
|
menu::Tree::with_children(
|
||||||
|
RcElementWrapper::new(Element::from(menu::root(fl!("playback")))),
|
||||||
|
menu::items(
|
||||||
|
key_binds,
|
||||||
|
vec![
|
||||||
|
menu::Item::Button(fl!("next-frame"), None, Action::NextFrame),
|
||||||
|
menu::Item::Button(fl!("previous-frame"), None, Action::PreviousFrame),
|
||||||
|
menu::Item::Button(
|
||||||
|
match ab_repeat {
|
||||||
|
None => fl!("ab-repeat-set-a"),
|
||||||
|
Some((_, None)) => fl!("ab-repeat-set-b"),
|
||||||
|
Some((_, Some(_))) => fl!("ab-repeat-clear"),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
Action::AbRepeat,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])
|
||||||
.item_height(ItemHeight::Dynamic(40))
|
.item_height(ItemHeight::Dynamic(40))
|
||||||
.item_width(ItemWidth::Uniform(320))
|
.item_width(ItemWidth::Uniform(320))
|
||||||
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
|
.spacing(theme::active().cosmic().spacing.space_xxxs.into())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue