#StackBounty: #java #javafx Media player component with JavaFX

Bounty: 50

I am trying to build a media player as a part of a bigger project in JavaFX. It is a simple media player as of now, which requires a Presentation component to output videos to. The dependencies are being injected by Guice.

This is my first time working on a Java project, so I would love some reviews on my code, especially based on design patterns, SOLID/DRY principles, code readability/re-usability/maintainability and how I can improve.

/**
 * Media Player component for Quizatron
 *
 * @author Dedipyaman Das <2d@twodee.me>
 * @version 1.0.18.1
 * @since 1.0.18.1
 */
public class Player {

    private MediaView mediaView;
    private MediaPlayer mediaPlayer;
    private FileChooser fileChooser;
    private Presentation presentation;
    @FXML
    private AnchorPane playerNode;
    @FXML
    private Button playBtn;
    @FXML
    private Label currTimeLbl;
    @FXML
    private Label endTimeLbl;
    @FXML
    private JFXSlider timeSlider;
    @FXML
    private Label mediaInfo;
    @FXML
    private Label sourceFileLbl;

    /**
     * Media player component constructor.
     * Guice or any other injector needs to inject a {@link FileChooser} for getting the media file,
     * {@link MediaView} for visual output
     * and {@link Presentation} to have it presented in a separate view
     *
     * @param fileChooser  FileChooser - single file
     * @param mediaView    MediaView - Parent container for visual output
     * @param presentation Presentation state for separate stage
     */
    @Inject
    public Player(FileChooser fileChooser, MediaView mediaView, Presentation presentation) {

        this.mediaView = mediaView;
        this.fileChooser = fileChooser;
        this.presentation = presentation;
    }

    public enum PlayerState {

        PLAY, PAUSE
    }

    /**
     * Initializer to get the FXML components working.
     */
    @FXML
    public void initialize() {

        timeSlider.setValue(0);
        prepareMediaView();
    }

    /**
     * Prepare the mediaview window with correct dimensions
     * Binds the mediaview's width and height relative to the window size and video ratio
     *
     * @see MediaPresentationView
     * @see Bindings
     */
    private void prepareMediaView() {

        DoubleProperty width = mediaView.fitWidthProperty();
        DoubleProperty height = mediaView.fitHeightProperty();

        width.bind(Bindings.selectDouble(mediaView.sceneProperty(), "width"));
        height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height"));
    }

    /**
     * Open the file chooser and autoplay the media
     *
     * @param event ActionEvent
     * @throws Exception thrown on failure to open file
     */
    @FXML
    private void chooseMediaFromFile(ActionEvent event) throws Exception {

        fileChooser.setTitle("Open Resource File");
        File file = fileChooser.showOpenDialog(playerNode.getScene().getWindow());

        String source = file.toURI().toURL().toExternalForm();
        Media media = new Media(source);
        startPlayer(media);
    }

    /**
     * Loads and plays the media source
     *
     * @param media Media - source media to be played
     */
    private void startPlayer(Media media) {

        loadMedia(media);
        timeSlider.setValue(this.mediaPlayer.getCurrentTime().toSeconds());
        mediaPlayer.setOnReady(this::displayMetaData);
        initTimeSlider();
        initUIControlsBehavior();
        playLoadedMedia();
    }

    /**
     * Loads the media and instantiates a new media player
     *
     * @param media Media - reusable media component, can be used from anywhere
     */
    public void loadMedia(Media media) {

        if (mediaPlayer != null) {

            mediaPlayer.dispose();
        }
        this.setMediaPlayer(new MediaPlayer(media));
    }

    /**
     * Fetches and substitutes the placeholders for media metadata
     */
    private void displayMetaData() {

        timeSlider.setMax(mediaPlayer.getTotalDuration().toSeconds());
        ObservableMap<String, Object> metaData = mediaPlayer.getMedia().getMetadata();

        String artistName = (String) metaData.get("artist");
        String title = (String) metaData.get("title");
        String album = (String) metaData.get("album");
        String mediaSource = mediaPlayer.getMedia().getSource();

        mediaInfo.setText(title + " - " + artistName + " - " + album);
        sourceFileLbl.setText(getFileNameFromPath(mediaSource));

        double duration = mediaPlayer.getTotalDuration().toSeconds();
        endTimeLbl.setText(formatTime(duration));
    }

    /**
     * Get the slider running and enable seeking
     * {@link JFXSlider}
     */
    private void initTimeSlider() {

        timeSlider
                .valueProperty()
                .addListener((observable, oldValue, newValue)
                                     -> sliderSeekBehavior(oldValue, newValue));
        mediaPlayer
                .currentTimeProperty()
                .addListener((observable, oldDuration, newDuration)
                                     -> sliderProgressBehavior(oldDuration, newDuration));
        initIndicatorValueProperty();
    }

    /**
     * The slider behavior on dragging/clicking on the JFXSlider to seek
     *
     * @param oldValue Number - before seeking
     * @param newValue Number - after action on slider
     */
    private void sliderSeekBehavior(Number oldValue, Number newValue) {

        // Is the change significant enough?
        // Drag was buggy, have to run some tests
        // Affects only the drag it seems
        double tolerance = 1;

        if (mediaPlayer.getTotalDuration().toSeconds() <= 100) {

            tolerance = 0.5;
        }
        if (abs(oldValue.doubleValue() - newValue.doubleValue()) >= tolerance) {

            mediaPlayer.seek(Duration.seconds(newValue.doubleValue()));
        }
    }

    /**
     * Behavior of the slider as it progresses
     *
     * @param oldDuration
     * @param newDuration
     */
    private void sliderProgressBehavior(Duration oldDuration, Duration newDuration) {

        // Making sure it doesn't interfere with the manual seeking
        double newElapsedTime = newDuration.toSeconds();
        double oldElapsedTime = oldDuration.toSeconds();
        double totalDuration = mediaPlayer.getTotalDuration().toSeconds();

        if (!timeSlider.isValueChanging()) {

            if (newElapsedTime - oldElapsedTime >= 0.1) {

                timeSlider.setValue(newElapsedTime);
            }
            updateTimeLabel(totalDuration, newElapsedTime);
        }
    }

    /**
     * Setting the time elapsed and time left on the appropriate indicators
     */
    private void updateTimeLabel(double totalDuration, double elapsedTime) {
        // Get rid of the unnecessary decimal points
        double timeLeft = totalDuration - elapsedTime;

        String elapsedTimeFormatted = formatTime(elapsedTime);
        String remainingTimeFormatted = formatTime(timeLeft);

        // Time elapsed/left indicators update
        currTimeLbl.setText(elapsedTimeFormatted);
        endTimeLbl.setText(remainingTimeFormatted);
    }

    /**
     * Display indicator in HH:MM format
     */
    private void initIndicatorValueProperty() {

        // Only the guy on StackOverflow and god knows how this works
        timeSlider.setValueFactory(new Callback<JFXSlider, StringBinding>() {

            @Override
            public StringBinding call(JFXSlider arg0) {

                return Bindings.createStringBinding(new java.util.concurrent.Callable<String>() {

                    @Override
                    public String call() {
                        return formatTime(timeSlider.getValue());
                    }
                }, timeSlider.valueProperty());
            }
        });
    }

    /**
     * Sets the behavior of the player UI components based on the player state
     */
    private void initUIControlsBehavior() {

        mediaPlayer.setOnEndOfMedia(this::stop);
        // Multiline lambdas should be avoided? - Venkat S.
        mediaPlayer.setOnStopped(() -> {
            // Needs to be revisited
            togglePlayPauseBtn(PlayerState.PLAY);
            timeSlider.setValue(0);
        });

        mediaPlayer.setOnPaused(() -> togglePlayPauseBtn(PlayerState.PLAY));
        mediaPlayer.setOnPlaying(() -> togglePlayPauseBtn(PlayerState.PAUSE));
    }

    /**
     * Change the pause button to a startPlayer button and have the appropriate action based on it
     *
     * @param state current state of the player
     *              {@link FontAwesomeIconView}
     */
    private void togglePlayPauseBtn(PlayerState state) {

        FontAwesomeIconView icon;

        if (state.equals(PlayerState.PLAY)) {

            icon = getIcon(FontAwesomeIcon.PLAY);
            playBtn.setOnAction(this::play);
        } else {

            icon = getIcon(FontAwesomeIcon.PAUSE);
            playBtn.setOnAction(this::pause);
        }
        playBtn.setGraphic(icon);
    }

    /**
     * Check if the media player was already there, prepare the presentation and startPlayer the video
     */
    private void playLoadedMedia() {

        if (mediaView.getMediaPlayer() != this.mediaPlayer) {

            preparePresentation();
        }
        mediaPlayer.play();
    }

    /**
     * Prepare the external presentation view
     * Get the view controller and embed visual output
     * {@link Presentation}
     */
    private void preparePresentation() {

        mediaView.setMediaPlayer(this.mediaPlayer);
        try {

            if (!(presentation.getView() instanceof MediaPresentationView)) {

                presentation.changeView("media-view");
            }

            MediaPresentationView mediaViewController = (MediaPresentationView) presentation.getView();
            mediaViewController.embedMediaView(mediaView);
        }
        catch (Exception e) {

            e.printStackTrace();
        }
    }

    /**
     * User action to startPlayer the media
     *
     * @param event ActionEvent
     */
    @FXML
    private void play(ActionEvent event) {

        mediaPlayer.play();
    }

    /**
     * User action event to pause the video
     *
     * @param event
     */
    @FXML
    private void pause(ActionEvent event) {

        mediaPlayer.pause();
    }

    /**
     * User action event to stop the media
     */
    public void stop() {

        mediaPlayer.stop();
    }

    public void openPlaylist() {

    }

    public void back() {

    }

    public void next() {

    }
    // Helper functions

    /**
     * Setter for the class media player
     *
     * @param mediaPlayer MediaPlayer {@link MediaPlayer}
     */
    private void setMediaPlayer(MediaPlayer mediaPlayer) {

        this.mediaPlayer = mediaPlayer;
    }

    /**
     * Extracts the filename + extension from the supplied file path
     *
     * @param filePath full file path
     * @return the filename stripped of slashes and everything before
     */
    private String getFileNameFromPath(String filePath) {

        filePath = filePath.replace("%20", " ");
        return filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length());
    }

    /**
     * Creates a FontAwesomeIconView based on the supplied icon type
     *
     * @param iconType FontAwesome icon to be built
     * @return The final icon with appropriate styles and glyph sizes
     */
    private FontAwesomeIconView getIcon(FontAwesomeIcon iconType) {

        FontAwesomeIconView icon = new FontAwesomeIconView(iconType);
        icon.setGlyphSize(16);
        icon.setStyleClass("trackBtnIcon");
        return icon;
    }

    /**
     * Formats the time to a MM:SS format as a string
     *
     * @param totalSeconds the time specified in seconds
     * @return the formatted time string
     */
    private String formatTime(double totalSeconds) {

        int min = (int) totalSeconds / 60;
        int sec = (int) totalSeconds % 60;

        return String.format("%02d:%02d", min, sec);
    }
}

Any criticism is welcome, thanks for helping me improve my code!

Update: I have made some changes to the original code after having suggestions from people, since there are no answers yet- I would still like to have it reviewed. The purpose of the class remains the same.


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.