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}