Add playback and basic player UI

This commit is contained in:
Elias Projahn 2020-11-07 16:11:08 +01:00
parent c12d8b01bd
commit 435fa23d76
14 changed files with 806 additions and 49 deletions

View file

@ -1,4 +1,5 @@
use super::database::*;
use crate::player::*;
use anyhow::{anyhow, Result};
use futures_channel::oneshot::Sender;
use futures_channel::{mpsc, oneshot};
@ -50,6 +51,7 @@ pub struct Backend {
action_sender: RefCell<Option<std::sync::mpsc::Sender<BackendAction>>>,
settings: gio::Settings,
music_library_path: RefCell<Option<PathBuf>>,
player: RefCell<Option<Rc<Player>>>,
}
impl Backend {
@ -62,6 +64,7 @@ impl Backend {
action_sender: RefCell::new(None),
settings: gio::Settings::new("de.johrpan.musicus"),
music_library_path: RefCell::new(None),
player: RefCell::new(None),
}
}
@ -267,10 +270,16 @@ impl Backend {
self.music_library_path.borrow().clone()
}
pub fn get_player(&self) -> Option<Rc<Player>> {
self.player.borrow().clone()
}
async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> {
self.music_library_path.replace(Some(path.clone()));
self.set_state(BackendState::Loading);
self.music_library_path.replace(Some(path.clone()));
self.player.replace(Some(Player::new(path.clone())));
if let Some(action_sender) = self.action_sender.borrow_mut().take() {
action_sender.send(Stop)?;
}

View file

@ -15,6 +15,7 @@ mod config;
mod backend;
mod database;
mod dialogs;
mod player;
mod screens;
mod widgets;

View file

@ -63,10 +63,14 @@ sources = files(
'widgets/mod.rs',
'widgets/navigator.rs',
'widgets/person_list.rs',
'widgets/player_bar.rs',
'widgets/poe_list.rs',
'widgets/selector_row.rs',
'backend.rs',
'config.rs',
'config.rs.in',
'main.rs',
'player.rs',
'resources.rs',
'resources.rs.in',
'window.rs',

291
src/player.rs Normal file
View file

@ -0,0 +1,291 @@
use crate::database::*;
use anyhow::anyhow;
use anyhow::Result;
use gstreamer_player::prelude::*;
use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::rc::Rc;
#[derive(Clone)]
pub struct PlaylistItem {
pub recording: RecordingDescription,
pub tracks: Vec<TrackDescription>,
}
pub struct Player {
music_library_path: PathBuf,
player: gstreamer_player::Player,
playlist: RefCell<Vec<PlaylistItem>>,
current_item: Cell<Option<usize>>,
current_track: Cell<Option<usize>>,
playing: Cell<bool>,
playlist_cb: RefCell<Option<Box<dyn Fn(Vec<PlaylistItem>) -> ()>>>,
track_cb: RefCell<Option<Box<dyn Fn(usize, usize) -> ()>>>,
duration_cb: RefCell<Option<Box<dyn Fn(u64) -> ()>>>,
playing_cb: RefCell<Option<Box<dyn Fn(bool) -> ()>>>,
position_cb: RefCell<Option<Box<dyn Fn(u64) -> ()>>>,
}
impl Player {
pub fn new(music_library_path: PathBuf) -> Rc<Self> {
let dispatcher = gstreamer_player::PlayerGMainContextSignalDispatcher::new(None);
let player = gstreamer_player::Player::new(None, Some(&dispatcher.upcast()));
let mut config = player.get_config();
config.set_position_update_interval(250);
player.set_config(config).unwrap();
player.set_video_track_enabled(false);
let result = Rc::new(Self {
music_library_path,
player: player.clone(),
playlist: RefCell::new(Vec::new()),
current_item: Cell::new(None),
current_track: Cell::new(None),
playing: Cell::new(false),
playlist_cb: RefCell::new(None),
track_cb: RefCell::new(None),
duration_cb: RefCell::new(None),
playing_cb: RefCell::new(None),
position_cb: RefCell::new(None),
});
let clone = fragile::Fragile::new(result.clone());
player.connect_end_of_stream(move |_| {
let clone = clone.get();
if clone.has_next() {
clone.next().unwrap();
} else {
clone.player.stop();
if let Some(cb) = &*clone.playing_cb.borrow() {
cb(false);
}
}
});
let clone = fragile::Fragile::new(result.clone());
player.connect_position_updated(move |_, position| {
if let Some(cb) = &*clone.get().position_cb.borrow() {
cb(position.mseconds().unwrap());
}
});
let clone = fragile::Fragile::new(result.clone());
player.connect_duration_changed(move |_, duration| {
if let Some(cb) = &*clone.get().duration_cb.borrow() {
cb(duration.mseconds().unwrap());
}
});
result
}
pub fn set_playlist_cb<F: Fn(Vec<PlaylistItem>) -> () + 'static>(&self, cb: F) {
self.playlist_cb.replace(Some(Box::new(cb)));
}
pub fn set_track_cb<F: Fn(usize, usize) -> () + 'static>(&self, cb: F) {
self.track_cb.replace(Some(Box::new(cb)));
}
pub fn set_duration_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) {
self.duration_cb.replace(Some(Box::new(cb)));
}
pub fn set_playing_cb<F: Fn(bool) -> () + 'static>(&self, cb: F) {
self.playing_cb.replace(Some(Box::new(cb)));
}
pub fn set_position_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) {
self.position_cb.replace(Some(Box::new(cb)));
}
pub fn get_playlist(&self) -> Vec<PlaylistItem> {
self.playlist.borrow().clone()
}
pub fn get_current_item(&self) -> Option<usize> {
self.current_item.get()
}
pub fn get_current_track(&self) -> Option<usize> {
self.current_track.get()
}
pub fn get_duration(&self) -> gstreamer::ClockTime {
self.player.get_duration()
}
pub fn is_playing(&self) -> bool {
self.playing.get()
}
pub fn add_item(&self, item: PlaylistItem) -> Result<()> {
if item.tracks.is_empty() {
Err(anyhow!(
"Tried to add playlist item without tracks to playlist!"
))
} else {
let was_empty = {
let mut playlist = self.playlist.borrow_mut();
let was_empty = playlist.is_empty();
playlist.push(item);
was_empty
};
if let Some(cb) = &*self.playlist_cb.borrow() {
cb(self.playlist.borrow().clone());
}
if was_empty {
self.set_track(0, 0)?;
self.player.play();
self.playing.set(true);
if let Some(cb) = &*self.playing_cb.borrow() {
cb(true);
}
}
Ok(())
}
}
pub fn play_pause(&self) {
if self.is_playing() {
self.player.pause();
self.playing.set(false);
if let Some(cb) = &*self.playing_cb.borrow() {
cb(false);
}
} else {
self.player.play();
self.playing.set(true);
if let Some(cb) = &*self.playing_cb.borrow() {
cb(true);
}
}
}
pub fn has_previous(&self) -> bool {
if let Some(current_item) = self.current_item.get() {
if let Some(current_track) = self.current_track.get() {
current_track > 0 || current_item > 0
} else {
false
}
} else {
false
}
}
pub fn previous(&self) -> Result<()> {
let mut current_item = self.current_item.get().ok_or(anyhow!("No current item!"))?;
let mut current_track = self
.current_track
.get()
.ok_or(anyhow!("No current track!"))?;
let playlist = self.playlist.borrow();
if current_track > 0 {
current_track -= 1;
} else if current_item > 0 {
current_item -= 1;
current_track = playlist[current_item].tracks.len() - 1;
} else {
return Err(anyhow!("No previous track!"));
}
self.set_track(current_item, current_track)
}
pub fn has_next(&self) -> bool {
if let Some(current_item) = self.current_item.get() {
if let Some(current_track) = self.current_track.get() {
let playlist = self.playlist.borrow();
let item = &playlist[current_item];
current_track + 1 < item.tracks.len() || current_item + 1 < playlist.len()
} else {
false
}
} else {
false
}
}
pub fn next(&self) -> Result<()> {
let mut current_item = self.current_item.get().ok_or(anyhow!("No current item!"))?;
let mut current_track = self
.current_track
.get()
.ok_or(anyhow!("No current track!"))?;
let playlist = self.playlist.borrow();
let item = &playlist[current_item];
if current_track + 1 < item.tracks.len() {
current_track += 1;
} else if current_item + 1 < playlist.len() {
current_item += 1;
current_track = 0;
} else {
return Err(anyhow!("No next track!"));
}
self.set_track(current_item, current_track)
}
pub fn set_track(&self, current_item: usize, current_track: usize) -> Result<()> {
let uri = format!(
"file://{}",
self.music_library_path
.join(
self.playlist
.borrow()
.get(current_item)
.ok_or(anyhow!("Playlist item doesn't exist!"))?
.tracks
.get(current_track)
.ok_or(anyhow!("Track doesn't exist!"))?
.file_name
.clone(),
)
.to_str()
.unwrap(),
);
self.player.set_uri(&uri);
if self.is_playing() {
self.player.play();
}
self.current_item.set(Some(current_item));
self.current_track.set(Some(current_track));
if let Some(cb) = &*self.track_cb.borrow() {
cb(current_item, current_track);
}
Ok(())
}
pub fn clear(&self) {
self.player.stop();
self.playing.set(false);
self.current_item.set(None);
self.current_track.set(None);
self.playlist.replace(Vec::new());
if let Some(cb) = &*self.playing_cb.borrow() {
cb(false);
}
if let Some(cb) = &*self.playlist_cb.borrow() {
cb(Vec::new());
}
}
}

View file

@ -1,5 +1,6 @@
use crate::backend::*;
use crate::database::*;
use crate::player::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
@ -13,13 +14,13 @@ pub struct RecordingScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
tracks: RefCell<Vec<TrackDescription>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingScreen {
pub fn new(backend: Rc<Backend>, recording: RecordingDescription) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui");
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, libhandy::HeaderBar, header);
@ -27,6 +28,7 @@ impl RecordingScreen {
get_widget!(builder, gtk::MenuButton, menu_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Frame, frame);
get_widget!(builder, gtk::Button, add_to_playlist_button);
header.set_title(Some(&recording.work.get_title()));
header.set_subtitle(Some(&recording.get_performers()));
@ -88,6 +90,7 @@ impl RecordingScreen {
backend,
widget,
stack,
tracks: RefCell::new(Vec::new()),
navigator: RefCell::new(None),
});
@ -98,13 +101,25 @@ impl RecordingScreen {
}
}));
add_to_playlist_button.connect_clicked(
clone!(@strong result, @strong recording => move |_| {
if let Some(player) = result.backend.get_player() {
player.add_item(PlaylistItem {
recording: (*recording).clone(),
tracks: result.tracks.borrow().clone(),
}).unwrap();
}
}),
);
let context = glib::MainContext::default();
let clone = result.clone();
let id = recording.id;
context.spawn_local(async move {
let tracks = clone.backend.get_tracks(id).await.unwrap();
list.show_items(tracks);
list.show_items(tracks.clone());
clone.stack.set_visible_child_name("content");
clone.tracks.replace(tracks);
});
result

View file

@ -7,6 +7,9 @@ pub use navigator::*;
pub mod person_list;
pub use person_list::*;
pub mod player_bar;
pub use player_bar::*;
pub mod poe_list;
pub use poe_list::*;

161
src/widgets/player_bar.rs Normal file
View file

@ -0,0 +1,161 @@
use crate::player::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
pub struct PlayerBar {
pub widget: gtk::Revealer,
title_label: gtk::Label,
subtitle_label: gtk::Label,
previous_button: gtk::Button,
play_button: gtk::Button,
next_button: gtk::Button,
position_label: gtk::Label,
duration_label: gtk::Label,
play_image: gtk::Image,
pause_image: gtk::Image,
player: Rc<RefCell<Option<Rc<Player>>>>,
}
impl PlayerBar {
pub fn new() -> Self {
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_bar.ui");
get_widget!(builder, gtk::Revealer, widget);
get_widget!(builder, gtk::Label, title_label);
get_widget!(builder, gtk::Label, subtitle_label);
get_widget!(builder, gtk::Button, previous_button);
get_widget!(builder, gtk::Button, play_button);
get_widget!(builder, gtk::Button, next_button);
get_widget!(builder, gtk::Label, position_label);
get_widget!(builder, gtk::Label, duration_label);
get_widget!(builder, gtk::Image, play_image);
get_widget!(builder, gtk::Image, pause_image);
let player = Rc::new(RefCell::new(None::<Rc<Player>>));
previous_button.connect_clicked(clone!(@strong player => move |_| {
if let Some(player) = &*player.borrow() {
player.previous().unwrap();
}
}));
play_button.connect_clicked(clone!(@strong player => move |_| {
if let Some(player) = &*player.borrow() {
player.play_pause();
}
}));
next_button.connect_clicked(clone!(@strong player => move |_| {
if let Some(player) = &*player.borrow() {
player.next().unwrap();
}
}));
Self {
widget,
title_label,
subtitle_label,
previous_button,
play_button,
next_button,
position_label,
duration_label,
play_image,
pause_image,
player: player,
}
}
pub fn set_player(&self, player: Option<Rc<Player>>) {
self.player.replace(player.clone());
if let Some(player) = player {
let playlist = Rc::new(RefCell::new(Vec::<PlaylistItem>::new()));
player.set_playlist_cb(clone!(
@strong player,
@strong self.widget as widget,
@strong self.previous_button as previous_button,
@strong self.next_button as next_button,
@strong playlist
=> move |new_playlist| {
widget.set_reveal_child(!new_playlist.is_empty());
playlist.replace(new_playlist);
previous_button.set_sensitive(player.has_previous());
next_button.set_sensitive(player.has_next());
}
));
player.set_track_cb(clone!(
@strong player,
@strong playlist,
@strong self.previous_button as previous_button,
@strong self.next_button as next_button,
@strong self.title_label as title_label,
@strong self.subtitle_label as subtitle_label,
@strong self.position_label as position_label
=> move |current_item, current_track| {
previous_button.set_sensitive(player.has_previous());
next_button.set_sensitive(player.has_next());
let item = &playlist.borrow()[current_item];
let track = &item.tracks[current_track];
let mut parts = Vec::<String>::new();
for part in &track.work_parts {
parts.push(item.recording.work.parts[*part].title.clone());
}
let mut title = item.recording.work.get_title();
if !parts.is_empty() {
title = format!("{}: {}", title, parts.join(", "));
}
title_label.set_text(&title);
subtitle_label.set_text(&item.recording.get_performers());
position_label.set_text("0:00");
}
));
player.set_duration_cb(clone!(
@strong self.duration_label as duration_label
=> move |ms| {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
duration_label.set_text(&format!("{}:{:02}", min, sec));
}
));
player.set_playing_cb(clone!(
@strong self.play_button as play_button,
@strong self.play_image as play_image,
@strong self.pause_image as pause_image
=> move |playing| {
if let Some(child) = play_button.get_child() {
play_button.remove( &child);
}
play_button.add(if playing {
&pause_image
} else {
&play_image
});
}
));
player.set_position_cb(clone!(
@strong self.position_label as position_label
=> move |ms| {
let min = ms / 60000;
let sec = (ms % 60000) / 1000;
position_label.set_text(&format!("{}:{:02}", min, sec));
}
));
} else {
self.widget.set_reveal_child(false);
}
}
}

View file

@ -19,6 +19,7 @@ pub struct Window {
sidebar_box: gtk::Box,
poe_list: Rc<PoeList>,
navigator: Rc<Navigator>,
player_bar: PlayerBar,
}
impl Window {
@ -28,6 +29,7 @@ impl Window {
get_widget!(builder, libhandy::ApplicationWindow, window);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::Button, select_music_library_path_button);
get_widget!(builder, gtk::Box, content_box);
get_widget!(builder, libhandy::Leaflet, leaflet);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Box, sidebar_box);
@ -42,6 +44,9 @@ impl Window {
leaflet.set_visible_child(&sidebar_box);
}));
let player_bar = PlayerBar::new();
content_box.add(&player_bar.widget);
let result = Rc::new(Self {
backend,
window,
@ -50,6 +55,7 @@ impl Window {
sidebar_box,
poe_list,
navigator,
player_bar,
});
result.window.set_application(Some(app));
@ -290,6 +296,9 @@ impl Window {
BackendState::Ready => {
clone.stack.set_visible_child_name("content");
clone.poe_list.clone().reload();
let player = clone.backend.get_player().unwrap();
clone.player_bar.set_player(Some(player));
}
}
}