001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.audio;
003
004import java.io.File;
005import java.io.FileNotFoundException;
006import java.io.IOException;
007import java.net.URISyntaxException;
008import java.net.URL;
009import java.util.concurrent.CountDownLatch;
010
011import org.openstreetmap.josm.io.audio.AudioPlayer.Execute;
012import org.openstreetmap.josm.io.audio.AudioPlayer.State;
013import org.openstreetmap.josm.tools.JosmRuntimeException;
014import org.openstreetmap.josm.tools.ListenerList;
015
016import com.sun.javafx.application.PlatformImpl;
017
018import javafx.scene.media.Media;
019import javafx.scene.media.MediaException;
020import javafx.scene.media.MediaPlayer;
021import javafx.scene.media.MediaPlayer.Status;
022import javafx.util.Duration;
023
024/**
025 * Default sound player based on the Java FX Media API.
026 * Used on platforms where Java FX is available. It supports the following audio codecs:<ul>
027 * <li>MP3</li>
028 * <li>AIFF containing uncompressed PCM</li>
029 * <li>WAV containing uncompressed PCM</li>
030 * <li>MPEG-4 multimedia container with Advanced Audio Coding (AAC) audio</li>
031 * </ul>
032 * @since 12328
033 */
034class JavaFxMediaPlayer implements SoundPlayer {
035
036    private final ListenerList<AudioListener> listeners = ListenerList.create();
037
038    private MediaPlayer mediaPlayer;
039
040    JavaFxMediaPlayer() throws JosmRuntimeException {
041        try {
042            initFxPlatform();
043        } catch (InterruptedException e) {
044            throw new JosmRuntimeException(e);
045        }
046    }
047
048    /**
049     * Initializes the JavaFX platform runtime.
050     * @throws InterruptedException if the current thread is interrupted while waiting
051     */
052    public static void initFxPlatform() throws InterruptedException {
053        final CountDownLatch startupLatch = new CountDownLatch(1);
054
055        // Note, this method is called on the FX Application Thread
056        PlatformImpl.startup(startupLatch::countDown);
057
058        // Wait for FX platform to start
059        startupLatch.await();
060    }
061
062    @Override
063    public synchronized void play(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
064        try {
065            final URL url = command.url();
066            if (playingUrl != url) {
067                if (mediaPlayer != null) {
068                    mediaPlayer.stop();
069                }
070                // Fail fast in case of invalid local URI (JavaFX Media locator retries 5 times with a 1 second delay)
071                if ("file".equals(url.getProtocol()) && !new File(url.toURI()).exists()) {
072                    throw new FileNotFoundException(url.toString());
073                }
074                mediaPlayer = new MediaPlayer(new Media(url.toString()));
075                mediaPlayer.setOnPlaying(() ->
076                    listeners.fireEvent(l -> l.playing(url))
077                );
078            }
079            mediaPlayer.setRate(command.speed());
080            if (Status.PLAYING == mediaPlayer.getStatus()) {
081                Duration seekTime = Duration.seconds(command.offset());
082                if (!seekTime.equals(mediaPlayer.getCurrentTime())) {
083                    mediaPlayer.seek(seekTime);
084                }
085            }
086            mediaPlayer.play();
087        } catch (MediaException | URISyntaxException e) {
088            throw new AudioException(e);
089        }
090    }
091
092    @Override
093    public synchronized void pause(Execute command, State stateChange, URL playingUrl) throws AudioException, IOException {
094        if (mediaPlayer != null) {
095            try {
096                mediaPlayer.pause();
097            } catch (MediaException e) {
098                throw new AudioException(e);
099            }
100        }
101    }
102
103    @Override
104    public boolean playing(Execute command) throws AudioException, IOException, InterruptedException {
105        // Not used: JavaFX handles the low-level audio playback
106        return false;
107    }
108
109    @Override
110    public synchronized double position() {
111        return mediaPlayer != null ? mediaPlayer.getCurrentTime().toSeconds() : -1;
112    }
113
114    @Override
115    public synchronized double speed() {
116        return mediaPlayer != null ? mediaPlayer.getCurrentRate() : -1;
117    }
118
119    @Override
120    public void addAudioListener(AudioListener listener) {
121        listeners.addWeakListener(listener);
122    }
123}