Mp3 player with metadata view and control panel
/*
* Copyright (c) 2011, Pro JavaFX Authors
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of JFXtras nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.File;
import java.net.URL;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.LabelBuilder;
import javafx.scene.control.Slider;
import javafx.scene.effect.Reflection;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaPlayer.Status;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import com.sun.javafx.runtime.VersionInfo;
/**
* @author dean
*/
class AudioPlayer3 extends Application {
private final SongModel songModel;
private MetadataView metaDataView;
private PlayerControlsView playerControlsView;
public static void main(String[] args) {
launch(args);
}
public AudioPlayer3() {
songModel = new SongModel();
}
@Override
public void start(Stage primaryStage) {
System.out.println("JavaFX version: "+VersionInfo.getRuntimeVersion());
songModel.setURL("http://traffic.libsyn.com/dickwall/JavaPosse373.mp3");
metaDataView = new MetadataView(songModel);
playerControlsView = new PlayerControlsView(songModel);
final BorderPane root = new BorderPane();
root.setCenter(metaDataView.getViewNode());
root.setBottom(playerControlsView.getViewNode());
final Scene scene = new Scene(root, 800, 400);
initSceneDragAndDrop(scene);
final URL stylesheet = getClass().getResource("media.css");
scene.getStylesheets().add(stylesheet.toString());
primaryStage.setScene(scene);
primaryStage.setTitle("Audio Player 3");
primaryStage.show();
}
private void initSceneDragAndDrop(Scene scene) {
scene.setOnDragOver(new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent event) {
Dragboard db = event.getDragboard();
if (db.hasFiles() || db.hasUrl()) {
event.acceptTransferModes(TransferMode.ANY);
}
event.consume();
}
});
scene.setOnDragDropped(new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent event) {
Dragboard db = event.getDragboard();
String url = null;
if (db.hasFiles()) {
url = db.getFiles().get(0).toURI().toString();
} else if (db.hasUrl()) {
url = db.getUrl();
}
if (url != null) {
songModel.setURL(url);
songModel.getMediaPlayer().play();
}
event.setDropCompleted(url != null);
event.consume();
}
});
}
}
/**
* @author dean
*/
class MetadataView extends AbstractView {
public MetadataView(SongModel songModel) {
super(songModel);
}
@Override
protected Node initView() {
final Label title = createLabel("title");
final Label artist = createLabel("artist");
final Label album = createLabel("album");
final Label year = createLabel("year");
final ImageView albumCover = createAlbumCover();
title.textProperty().bind(songModel.titleProperty());
artist.textProperty().bind(songModel.artistProperty());
album.textProperty().bind(songModel.albumProperty());
year.textProperty().bind(songModel.yearProperty());
albumCover.imageProperty().bind(songModel.albumCoverProperty());
final GridPane gp = new GridPane();
gp.setPadding(new Insets(10));
gp.setHgap(20);
gp.add(albumCover, 0, 0, 1, GridPane.REMAINING);
gp.add(title, 1, 0);
gp.add(artist, 1, 1);
gp.add(album, 1, 2);
gp.add(year, 1, 3);
final ColumnConstraints c0 = new ColumnConstraints();
final ColumnConstraints c1 = new ColumnConstraints();
c1.setHgrow(Priority.ALWAYS);
gp.getColumnConstraints().addAll(c0, c1);
final RowConstraints r0 = new RowConstraints();
r0.setValignment(VPos.TOP);
gp.getRowConstraints().addAll(r0, r0, r0, r0);
return gp;
}
private Label createLabel(String id) {
return LabelBuilder.create().id(id).build();
}
private ImageView createAlbumCover() {
final Reflection reflection = new Reflection();
reflection.setFraction(0.2);
final ImageView albumCover = new ImageView();
albumCover.setFitWidth(240);
albumCover.setPreserveRatio(true);
albumCover.setSmooth(true);
albumCover.setEffect(reflection);
return albumCover;
}
}
/**
* @author dean
*/
class PlayerControlsView extends AbstractView {
private Image pauseImg;
private Image playImg;
private ImageView playPauseIcon;
private StatusListener statusListener;
private CurrentTimeListener currentTimeListener;
private Node controlPanel;
private Label statusLabel;
private Label currentTimeLabel;
private Label totalDurationLabel;
private Slider volumeSlider;
private Slider positionSlider;
public PlayerControlsView(SongModel songModel) {
super(songModel);
songModel.mediaPlayerProperty().addListener(new MediaPlayerListener());
statusListener = new StatusListener();
currentTimeListener = new CurrentTimeListener();
addListenersAndBindings(songModel.getMediaPlayer());
}
@Override
protected Node initView() {
final Button openButton = createOpenButton();
controlPanel = createControlPanel();
volumeSlider = createSlider("volumeSlider");
statusLabel = createLabel("Buffering", "statusDisplay");
positionSlider = createSlider("positionSlider");
totalDurationLabel = createLabel("00:00", "mediaText");
currentTimeLabel = createLabel("00:00", "mediaText");
positionSlider.valueChangingProperty().addListener(new PositionListener());
final ImageView volLow = new ImageView();
volLow.setId("volumeLow");
final ImageView volHigh = new ImageView();
volHigh.setId("volumeHigh");
final GridPane gp = new GridPane();
gp.setHgap(1);
gp.setVgap(1);
gp.setPadding(new Insets(10));
final ColumnConstraints buttonCol = new ColumnConstraints(100);
final ColumnConstraints spacerCol = new ColumnConstraints(40, 80, 80);
final ColumnConstraints middleCol = new ColumnConstraints();
middleCol.setHgrow(Priority.ALWAYS);
gp.getColumnConstraints().addAll(buttonCol, spacerCol, middleCol,
spacerCol, buttonCol);
GridPane.setValignment(openButton, VPos.BOTTOM);
GridPane.setHalignment(volHigh, HPos.RIGHT);
GridPane.setValignment(volumeSlider, VPos.TOP);
GridPane.setHalignment(statusLabel, HPos.RIGHT);
GridPane.setValignment(statusLabel, VPos.TOP);
GridPane.setHalignment(currentTimeLabel, HPos.RIGHT);
gp.add(openButton, 0, 0, 1, 3);
gp.add(volLow, 1, 0);
gp.add(volHigh, 1, 0);
gp.add(volumeSlider, 1, 1);
gp.add(controlPanel, 2, 0, 1, 2);
gp.add(statusLabel, 3, 1);
gp.add(currentTimeLabel, 1, 2);
gp.add(positionSlider, 2, 2);
gp.add(totalDurationLabel, 3, 2);
return gp;
}
private Button createOpenButton() {
final Button openButton = new Button();
openButton.setId("openButton");
openButton.setOnAction(new OpenHandler());
openButton.setPrefWidth(32);
openButton.setPrefHeight(32);
return openButton;
}
private Node createControlPanel() {
final HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER);
hbox.setFillHeight(false);
final Button playPauseButton = createPlayPauseButton();
final Button seekStartButton = new Button();
seekStartButton.setId("seekStartButton");
seekStartButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
seekAndUpdatePosition(Duration.ZERO);
}
});
final Button seekEndButton = new Button();
seekEndButton.setId("seekEndButton");
seekEndButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
final MediaPlayer mediaPlayer = songModel.getMediaPlayer();
final Duration totalDuration = mediaPlayer.getTotalDuration();
final Duration oneSecond = Duration.seconds(1);
seekAndUpdatePosition(totalDuration.subtract(oneSecond));
}
});
hbox.getChildren().addAll(seekStartButton, playPauseButton, seekEndButton);
return hbox;
}
private Button createPlayPauseButton() {
URL url = getClass().getResource("resources/pause.png");
pauseImg = new Image(url.toString());
url = getClass().getResource("resources/play.png");
playImg = new Image(url.toString());
playPauseIcon = new ImageView(playImg);
final Button playPauseButton = new Button(null, playPauseIcon);
playPauseButton.setId("playPauseButton");
playPauseButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent arg0) {
final MediaPlayer mediaPlayer = songModel.getMediaPlayer();
if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING) {
mediaPlayer.pause();
} else {
mediaPlayer.play();
}
}
});
return playPauseButton;
}
private Slider createSlider(String id) {
final Slider slider = new Slider(0.0, 1.0, 0.1);
slider.setId(id);
slider.setValue(0);
return slider;
}
private Label createLabel(String text, String styleClass) {
final Label label = new Label(text);
label.getStyleClass().add(styleClass);
return label;
}
private void addListenersAndBindings(final MediaPlayer mp) {
mp.statusProperty().addListener(statusListener);
mp.currentTimeProperty().addListener(currentTimeListener);
mp.totalDurationProperty().addListener(new TotalDurationListener());
mp.setOnEndOfMedia(new Runnable() {
@Override
public void run() {
songModel.getMediaPlayer().stop();
}
});
volumeSlider.valueProperty().bindBidirectional(mp.volumeProperty());
}
private void removeListenersAndBindings(MediaPlayer mp) {
volumeSlider.valueProperty().unbind();
mp.statusProperty().removeListener(statusListener);
mp.currentTimeProperty().removeListener(currentTimeListener);
}
private void seekAndUpdatePosition(Duration duration) {
final MediaPlayer mediaPlayer = songModel.getMediaPlayer();
if (mediaPlayer.getStatus() == Status.STOPPED) {
mediaPlayer.pause();
}
mediaPlayer.seek(duration);
if (mediaPlayer.getStatus() != Status.PLAYING) {
updatePositionSlider(duration);
}
}
private String formatDuration(Duration duration) {
double millis = duration.toMillis();
int seconds = (int) (millis / 1000) % 60;
int minutes = (int) (millis / (1000 * 60));
return String.format("%02d:%02d", minutes, seconds);
}
private void updateStatus(Status newStatus) {
if (newStatus == Status.UNKNOWN || newStatus == null) {
controlPanel.setDisable(true);
positionSlider.setDisable(true);
statusLabel.setText("Buffering");
} else {
controlPanel.setDisable(false);
positionSlider.setDisable(false);
statusLabel.setText(newStatus.toString());
if (newStatus == Status.PLAYING) {
playPauseIcon.setImage(pauseImg);
} else {
playPauseIcon.setImage(playImg);
}
}
}
private void updatePositionSlider(Duration currentTime) {
if (positionSlider.isValueChanging())
return;
final MediaPlayer mediaPlayer = songModel.getMediaPlayer();
final Duration total = mediaPlayer.getTotalDuration();
if (total == null || currentTime == null) {
positionSlider.setValue(0);
} else {
positionSlider.setValue(currentTime.toMillis() / total.toMillis());
}
}
private class MediaPlayerListener implements ChangeListener<MediaPlayer> {
@Override
public void changed(ObservableValue<? extends MediaPlayer> observable,
MediaPlayer oldValue, MediaPlayer newValue) {
if (oldValue != null) {
removeListenersAndBindings(oldValue);
}
addListenersAndBindings(newValue);
}
}
private class OpenHandler implements EventHandler<ActionEvent> {
@Override
public void handle(ActionEvent event) {
FileChooser fc = new FileChooser();
fc.setTitle("Pick a Sound File");
File song = fc.showOpenDialog(viewNode.getScene().getWindow());
if (song != null) {
songModel.setURL(song.toURI().toString());
songModel.getMediaPlayer().play();
}
}
}
private class StatusListener implements InvalidationListener {
@Override
public void invalidated(Observable observable) {
Platform.runLater(new Runnable() {
@Override
public void run() {
updateStatus(songModel.getMediaPlayer().getStatus());
}
});
}
}
private class CurrentTimeListener implements InvalidationListener {
@Override
public void invalidated(Observable observable) {
Platform.runLater(new Runnable() {
@Override
public void run() {
final MediaPlayer mediaPlayer = songModel.getMediaPlayer();
final Duration currentTime = mediaPlayer.getCurrentTime();
currentTimeLabel.setText(formatDuration(currentTime));
updatePositionSlider(currentTime);
}
});
}
}
private class TotalDurationListener implements InvalidationListener {
@Override
public void invalidated(Observable observable) {
final MediaPlayer mediaPlayer = songModel.getMediaPlayer();
final Duration totalDuration = mediaPlayer.getTotalDuration();
totalDurationLabel.setText(formatDuration(totalDuration));
}
}
private class PositionListener implements ChangeListener<Boolean> {
@Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (oldValue && !newValue) {
double pos = positionSlider.getValue();
final MediaPlayer mediaPlayer = songModel.getMediaPlayer();
final Duration seekTo = mediaPlayer.getTotalDuration().multiply(pos);
seekAndUpdatePosition(seekTo);
}
}
}
}
/**
* @author dean
*/
final class SongModel {
private static final String DEFAULT_IMG_URL =
SongModel.class.getResource("resources/defaultAlbum.png").toString();
private static final Image DEFAULT_ALBUM_COVER =
new Image(DEFAULT_IMG_URL.toString());
private final StringProperty album = new SimpleStringProperty(this, "album");
private final StringProperty artist = new SimpleStringProperty(this,"artist");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty year = new SimpleStringProperty(this, "year");
private final ObjectProperty<Image> albumCover =
new SimpleObjectProperty<Image>(this, "albumCover");
private final ReadOnlyObjectWrapper<MediaPlayer> mediaPlayer =
new ReadOnlyObjectWrapper<MediaPlayer>(this, "mediaPlayer");
public SongModel() {
resetProperties();
}
public void setURL(String url) {
if (mediaPlayer.get() != null) {
mediaPlayer.get().stop();
}
initializeMedia(url);
}
public String getAlbum() { return album.get(); }
public void setAlbum(String value) { album.set(value); }
public StringProperty albumProperty() { return album; }
public String getArtist() { return artist.get(); }
public void setArtist(String value) { artist.set(value); }
public StringProperty artistProperty() { return artist; }
public String getTitle() { return title.get(); }
public void setTitle(String value) { title.set(value); }
public StringProperty titleProperty() { return title; }
public String getYear() { return year.get(); }
public void setYear(String value) { year.set(value); }
public StringProperty yearProperty() { return year; }
public Image getAlbumCover() { return albumCover.get(); }
public void setAlbumCover(Image value) { albumCover.set(value); }
public ObjectProperty<Image> albumCoverProperty() { return albumCover; }
public MediaPlayer getMediaPlayer() { return mediaPlayer.get(); }
public ReadOnlyObjectProperty<MediaPlayer> mediaPlayerProperty() {
return mediaPlayer.getReadOnlyProperty();
}
private void resetProperties() {
setArtist("");
setAlbum("");
setTitle("");
setYear("");
setAlbumCover(DEFAULT_ALBUM_COVER);
}
private void initializeMedia(String url) {
resetProperties();
try {
final Media media = new Media(url);
media.getMetadata().addListener(new MapChangeListener<String, Object>() {
@Override
public void onChanged(Change<? extends String, ? extends Object> ch) {
if (ch.wasAdded()) {
handleMetadata(ch.getKey(), ch.getValueAdded());
}
}
});
mediaPlayer.setValue(new MediaPlayer(media));
mediaPlayer.get().setOnError(new Runnable() {
@Override
public void run() {
String errorMessage = mediaPlayer.get().getError().getMessage();
// Handle errors during playback
System.out.println("MediaPlayer Error: " + errorMessage);
}
});
} catch (RuntimeException re) {
// Handle construction errors
System.out.println("Caught Exception: " + re.getMessage());
}
}
private void handleMetadata(String key, Object value) {
if (key.equals("album")) {
setAlbum(value.toString());
} else if (key.equals("artist")) {
setArtist(value.toString());
} if (key.equals("title")) {
setTitle(value.toString());
} if (key.equals("year")) {
setYear(value.toString());
} if (key.equals("image")) {
setAlbumCover((Image)value);
}
}
}
/**
* @author dean
*/
abstract class AbstractView {
protected final SongModel songModel;
protected final Node viewNode;
public AbstractView(SongModel songModel) {
this.songModel = songModel;
this.viewNode = initView();
}
public Node getViewNode() {
return viewNode;
}
protected abstract Node initView();
}
Related examples in the same category