From 05e0e446a7416c523fabeaec468d5d2d5e48c95a Mon Sep 17 00:00:00 2001 From: Nathan Cannon Date: Fri, 1 Apr 2016 20:27:17 +0100 Subject: [PATCH] Added VLC based audio backend. --- pom.xml | 6 + src/main/java/musicplayer/SwingUIModule.java | 3 +- .../musicplayer/player/GStreamerPlayer.java | 3 +- .../java/musicplayer/player/VLCPlayer.java | 203 ++++++++++++++++++ .../java/musicplayer/swingui/PlayerGUI.java | 5 +- .../player/GStreamerPlayerTest.java | 3 +- .../musicplayer/player/VLCPlayerTest.java | 77 +++++++ 7 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 src/main/java/musicplayer/player/VLCPlayer.java create mode 100644 src/test/java/musicplayer/player/VLCPlayerTest.java diff --git a/pom.xml b/pom.xml index e87628c..67b7411 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,12 @@ 1.5 compile + + uk.co.caprica + vlcj + 3.10.1 + compile + junit junit diff --git a/src/main/java/musicplayer/SwingUIModule.java b/src/main/java/musicplayer/SwingUIModule.java index f9377bc..570461d 100644 --- a/src/main/java/musicplayer/SwingUIModule.java +++ b/src/main/java/musicplayer/SwingUIModule.java @@ -9,6 +9,7 @@ import musicplayer.library.ILibrary; import musicplayer.library.JTreeLibrary; import musicplayer.player.GStreamerPlayer; import musicplayer.player.IPlayer; +import musicplayer.player.VLCPlayer; import musicplayer.playlist.IPlaylist; import musicplayer.playlist.JTablePlaylist; import musicplayer.swingui.PlayerGUI; @@ -17,7 +18,7 @@ class SwingUIModule extends AbstractModule { @Override protected void configure() { - bind(IPlayer.class).to(GStreamerPlayer.class).in(Singleton.class); + bind(IPlayer.class).to(VLCPlayer.class).in(Singleton.class); bind(IDatabase.class).to(HibernateDatabase.class).in(Singleton.class); bind(IPlaylist.class).to(JTablePlaylist.class).in(Singleton.class); bind(ILibrary.class).to(JTreeLibrary.class).in(Singleton.class); diff --git a/src/main/java/musicplayer/player/GStreamerPlayer.java b/src/main/java/musicplayer/player/GStreamerPlayer.java index 13b0f8b..374105e 100644 --- a/src/main/java/musicplayer/player/GStreamerPlayer.java +++ b/src/main/java/musicplayer/player/GStreamerPlayer.java @@ -5,6 +5,7 @@ import musicplayer.StartPlayingException; import musicplayer.callbacks.PlayerCallbackInterface; import musicplayer.model.Song; import musicplayer.playlist.IPlaylist; +import musicplayer.util.ConfigManager; import org.gstreamer.ClockTime; import org.gstreamer.ElementFactory; import org.gstreamer.Gst; @@ -73,7 +74,7 @@ public class GStreamerPlayer implements IPlayer{ }, "seekbar"); seekBarUpdater.start(); } - currentVolume = getVolume(); + setVolume(ConfigManager.getLastVolume()); } private void resetSeek(){ diff --git a/src/main/java/musicplayer/player/VLCPlayer.java b/src/main/java/musicplayer/player/VLCPlayer.java new file mode 100644 index 0000000..eac5e0a --- /dev/null +++ b/src/main/java/musicplayer/player/VLCPlayer.java @@ -0,0 +1,203 @@ +package musicplayer.player; + +import com.google.inject.Inject; +import musicplayer.StartPlayingException; +import musicplayer.callbacks.PlayerCallbackInterface; +import musicplayer.model.Song; +import musicplayer.playlist.IPlaylist; +import musicplayer.util.ConfigManager; +import uk.co.caprica.vlcj.discovery.NativeDiscovery; +import uk.co.caprica.vlcj.player.MediaPlayer; +import uk.co.caprica.vlcj.player.MediaPlayerEventAdapter; +import uk.co.caprica.vlcj.player.MediaPlayerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Optional; + +public class VLCPlayer implements IPlayer { + + private ArrayList libvlcArgs = new ArrayList<>(Collections.singletonList("--vout=dummy")); + private MediaPlayer mediaPlayer; + + private Song currentSong; + private final PlayerCallbackInterface callbackInterface; + private int currentVolume = 100; + private final IPlaylist playlist; + private boolean repeatMode = false; // Repeat the current song? + + /** + * Manages VLC based playback operations. + * @param callbackInterface Interface on which UI updates can be called. + * @param playlist Playlist containing music to play. + */ + @Inject + public VLCPlayer(PlayerCallbackInterface callbackInterface, IPlaylist playlist){ + this(callbackInterface, playlist, false); + } + + /** + * Manages VLC based playback operations. + * @param callbackInterface Interface on which UI updates can be called. + * @param playlist Playlist containing music to play. + */ + VLCPlayer(PlayerCallbackInterface callbackInterface, IPlaylist playlist, boolean testMode){ + new NativeDiscovery().discover(); + if(testMode) + addTestModeFlags(); + this.callbackInterface = callbackInterface; + this.playlist = playlist; + MediaPlayerFactory mediaPlayerFactory = new MediaPlayerFactory(libvlcArgs.toArray(new String[libvlcArgs.size()])); + mediaPlayer = mediaPlayerFactory.newHeadlessMediaPlayer(); + mediaPlayer.addMediaPlayerEventListener(new InternalEventAdapter()); + Thread hookShutdown = new Thread(() -> { + // Cleanly dispose of the media player instance and any associated native resources + mediaPlayer.stop(); + mediaPlayer.release(); + // Cleanly dispose of the media player factory and any associated native resources + mediaPlayerFactory.release(); + }); + Runtime.getRuntime().addShutdownHook(hookShutdown); + setVolume(ConfigManager.getLastVolume()); + } + + private void addTestModeFlags(){ + libvlcArgs.add("--aout=dummy"); + } + + @Override + public void play() throws StartPlayingException { + playSong(playlist.getActive()); + } + + @Override + public void playSong(Optional inputSong) throws StartPlayingException { + if(inputSong.isPresent()) { + playSong(inputSong.get()); + } + } + + private void playSong(Song inputSong) throws StartPlayingException { + resetSeek(); + if(inputSong == currentSong && mediaPlayer.getTime() > 0 && mediaPlayer.getTime() <= mediaPlayer.getLength()) { // Song ~should~ already be loaded, unpause instead? + mediaPlayer.play(); + return; + } + currentSong = inputSong; + playlist.setPlayingSong(currentSong); + File songFile = currentSong.getSongFile(); + if (!songFile.exists()) { + playlist.delete(currentSong); + next(); + return; + } + boolean startedPlaying = mediaPlayer.startMedia(inputSong.getSongFile().getAbsolutePath()); + mediaPlayer.setVolume(currentVolume); + if(!startedPlaying) + throw new StartPlayingException(inputSong); + } + + private void resetSeek(){ + if(callbackInterface != null) + callbackInterface.setSeekBarPosition(0); + } + + @Override + public void stop() { + resetSeek(); + mediaPlayer.stop(); + } + + @Override + public void resume() { + mediaPlayer.setPause(false); + } + + @Override + public void pause() { + mediaPlayer.setPause(true); + if (isPlaying()) { + //noinspection StatementWithEmptyBody + do { // Wait to be paused + } while (isPlaying()); + } + } + + @Override + public void next() throws StartPlayingException { + playSong(playlist.getNext(currentSong)); + } + + @Override + public void previous() throws StartPlayingException { + playSong(playlist.getPrevious(currentSong)); + } + + @Override + public Song getCurrentSong() { + return currentSong; + } + + @Override + public int currentSongPosition() { + return (int)(mediaPlayer.getTime() / 1000); + } + + @Override + public void setVolume(int volume) { + currentVolume = volume; + mediaPlayer.setVolume(volume); + } + + @Override + public int getVolume() { + return (mediaPlayer.getVolume() >= 0) ? mediaPlayer.getVolume() : currentVolume; + } + + @Override + public void seek(int position) { + mediaPlayer.setTime(position * 1000); + } + + @Override + public boolean isPlaying() { + return mediaPlayer.isPlaying(); + } + + @Override + public void setRepeat(boolean repeatMode) { + this.repeatMode = repeatMode; + } + + @Override + public boolean isRepeating() { + return repeatMode; + } + + private class InternalEventAdapter extends MediaPlayerEventAdapter{ + @Override + public void finished(MediaPlayer mediaPlayer) { + try { + if (repeatMode) + playSong(currentSong); + else + playSong(playlist.getNext(currentSong)); + } catch (StartPlayingException ex){ + ex.printStackTrace(); + } + } + + @Override + public void playing(MediaPlayer mediaPlayer) { + if(callbackInterface != null) + callbackInterface.setSeekBarDuration((int) (mediaPlayer.getLength() / 1000)); + } + + @Override + public void timeChanged(MediaPlayer mediaPlayer, long newTime) { + if(callbackInterface != null) + callbackInterface.setSeekBarPosition((int)(newTime / 1000)); + } + } +} diff --git a/src/main/java/musicplayer/swingui/PlayerGUI.java b/src/main/java/musicplayer/swingui/PlayerGUI.java index b68e4bf..405984f 100644 --- a/src/main/java/musicplayer/swingui/PlayerGUI.java +++ b/src/main/java/musicplayer/swingui/PlayerGUI.java @@ -85,7 +85,10 @@ public class PlayerGUI extends JPanel implements PlayerCallbackInterface { * @param seconds New length of seekBar. */ public void setSeekBarDuration(int seconds) { - SwingUtilities.invokeLater(() -> seekBar.setMaximum(seconds)); + SwingUtilities.invokeLater(() -> { + if (!seeking.get()) + seekBar.setMaximum(seconds); + }); } private void showError(StartPlayingException ex) { diff --git a/src/test/java/musicplayer/player/GStreamerPlayerTest.java b/src/test/java/musicplayer/player/GStreamerPlayerTest.java index e61fb5b..5640629 100644 --- a/src/test/java/musicplayer/player/GStreamerPlayerTest.java +++ b/src/test/java/musicplayer/player/GStreamerPlayerTest.java @@ -4,6 +4,7 @@ import musicplayer.library.ILibrary; import musicplayer.model.Song; import musicplayer.playlist.IPlaylist; import musicplayer.playlist.JTablePlaylist; +import musicplayer.util.ConfigManager; import org.junit.Before; import org.junit.Test; @@ -67,7 +68,7 @@ public class GStreamerPlayerTest { @Test public void testSetVolume() throws Exception { - assertEquals(100, player.getVolume()); + assertEquals(ConfigManager.getLastVolume(), player.getVolume()); int newVolume = 25; player.setVolume(25); assertEquals(newVolume, player.getVolume()); diff --git a/src/test/java/musicplayer/player/VLCPlayerTest.java b/src/test/java/musicplayer/player/VLCPlayerTest.java new file mode 100644 index 0000000..db22d3e --- /dev/null +++ b/src/test/java/musicplayer/player/VLCPlayerTest.java @@ -0,0 +1,77 @@ +package musicplayer.player; + +import musicplayer.library.ILibrary; +import musicplayer.model.Song; +import musicplayer.playlist.IPlaylist; +import musicplayer.playlist.JTablePlaylist; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.Optional; + +import static org.junit.Assert.*; + +public class VLCPlayerTest { + + VLCPlayer player; + IPlaylist playlist; + Song sampleSong = new Song(ILibrary.autoParse( + new File(VLCPlayerTest.class.getResource("/sample.mp3").getFile()).toPath()).get()); + + @Before + public void setUp() throws Exception { + playlist = new JTablePlaylist(); + player = new VLCPlayer(null, playlist, true); + } + + @Test + public void testPlayEmptyPlaylist() throws Exception { + player.play(); + assertFalse(player.isPlaying()); + } + + + @Test + public void testPlay() throws Exception { + playlist.addSong(sampleSong); + player.play(); + assertTrue(player.isPlaying()); + } + + @Test + public void testPlaySong() throws Exception { + player.playSong(Optional.of(sampleSong)); + assertTrue(player.isPlaying()); + assertEquals(sampleSong, player.getCurrentSong()); + } + + @Test + public void testStop() throws Exception { + player.playSong(Optional.of(sampleSong)); + assertTrue(player.isPlaying()); + player.stop(); + assertFalse(player.isPlaying()); + } + + @Test + public void testResume() throws Exception { + } + + @Test + public void testPause() throws Exception { + player.playSong(Optional.of(sampleSong)); + assertTrue(player.isPlaying()); + player.pause(); + assertFalse(player.isPlaying()); + } + + @Test + public void testSeek() throws Exception { + player.playSong(Optional.of(sampleSong)); + assertTrue(player.isPlaying()); + player.seek(100); + System.out.println(player.currentSongPosition()); + assertTrue(player.currentSongPosition() >= 100); + } +} \ No newline at end of file