diff --git a/framework/src/org/apache/cordova/AudioHandler.java b/framework/src/org/apache/cordova/AudioHandler.java old mode 100755 new mode 100644 index 492043bf..3b92fe5c --- a/framework/src/org/apache/cordova/AudioHandler.java +++ b/framework/src/org/apache/cordova/AudioHandler.java @@ -96,6 +96,12 @@ public class AudioHandler extends Plugin { float f = this.getDurationAudio(args.getString(0), args.getString(1)); return new PluginResult(status, f); } + else if (action.equals("create")) { + String id = args.getString(0); + String src = args.getString(1); + AudioPlayer audio = new AudioPlayer(this, id, src); + this.players.put(id, audio); + } else if (action.equals("release")) { boolean b = this.release(args.getString(0)); return new PluginResult(status, b); @@ -149,7 +155,7 @@ public class AudioHandler extends Plugin { // Get all audio players and pause them for (AudioPlayer audio : this.players.values()) { - if (audio.getState() == AudioPlayer.MEDIA_RUNNING) { + if (audio.getState() == AudioPlayer.STATE.MEDIA_RUNNING.ordinal()) { this.pausedForPhone.add(audio); audio.pausePlaying(); } @@ -192,13 +198,12 @@ public class AudioHandler extends Plugin { * @param file The name of the file */ public void startRecordingAudio(String id, String file) { - // If already recording, then just return; - if (this.players.containsKey(id)) { - return; - } - AudioPlayer audio = new AudioPlayer(this, id); - this.players.put(id, audio); - audio.startRecording(file); + AudioPlayer audio = this.players.get(id); + if ( audio == null) { + audio = new AudioPlayer(this, id, file); + this.players.put(id, audio); + } + audio.startRecording(file); } /** @@ -221,7 +226,7 @@ public class AudioHandler extends Plugin { public void startPlayingAudio(String id, String file) { AudioPlayer audio = this.players.get(id); if (audio == null) { - audio = new AudioPlayer(this, id); + audio = new AudioPlayer(this, id, file); this.players.put(id, audio); } audio.startPlaying(file); @@ -292,7 +297,7 @@ public class AudioHandler extends Plugin { // If not already open, then open the file else { - audio = new AudioPlayer(this, id); + audio = new AudioPlayer(this, id, file); this.players.put(id, audio); return (audio.getDuration(file)); } diff --git a/framework/src/org/apache/cordova/AudioPlayer.java b/framework/src/org/apache/cordova/AudioPlayer.java old mode 100755 new mode 100644 index 5df00f9c..4f72fd0d --- a/framework/src/org/apache/cordova/AudioPlayer.java +++ b/framework/src/org/apache/cordova/AudioPlayer.java @@ -31,6 +31,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import org.apache.cordova.AudioPlayer.MODE; + /** * This class implements the audio playback and recording capabilities used by Cordova. * It is called by the AudioHandler Cordova class. @@ -41,15 +43,20 @@ import java.io.IOException; * sdcard: file name is just sound.mp3 */ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener { - - private static final String LOG_TAG = "AudioPlayer"; - + + // AudioPlayer modes + public enum MODE { NONE, PLAY, RECORD }; + // AudioPlayer states - public static int MEDIA_NONE = 0; - public static int MEDIA_STARTING = 1; - public static int MEDIA_RUNNING = 2; - public static int MEDIA_PAUSED = 3; - public static int MEDIA_STOPPED = 4; + public enum STATE { MEDIA_NONE, + MEDIA_LOADING, + MEDIA_STARTING, + MEDIA_RUNNING, + MEDIA_PAUSED, + MEDIA_STOPPED + }; + + private static final String LOG_TAG = "AudioPlayer"; // AudioPlayer message ids private static int MEDIA_STATE = 1; @@ -64,17 +71,20 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On private static int MEDIA_ERR_DECODE = 3; private static int MEDIA_ERR_NONE_SUPPORTED = 4; - private AudioHandler handler; // The AudioHandler object - private String id; // The id of this player (used to identify Media object in JavaScript) - private int state = MEDIA_NONE; // State of recording or playback - private String audioFile = null; // File name to play or record to - private float duration = -1; // Duration of audio + private AudioHandler handler; // The AudioHandler object + private String id; // The id of this player (used to identify Media object in JavaScript) + private MODE mode = MODE.NONE; // Playback or Recording mode + private STATE state = STATE.MEDIA_NONE; // State of recording or playback - private MediaRecorder recorder = null; // Audio recording object - private String tempFile = null; // Temporary recording file name + private String audioFile = null; // File name to play or record to + private float duration = -1; // Duration of audio + + private MediaRecorder recorder = null; // Audio recording object + private String tempFile = null; // Temporary recording file name - private MediaPlayer mPlayer = null; // Audio player object - private boolean prepareOnly = false; + private MediaPlayer player = null; // Audio player object + private boolean prepareOnly = true; // playback after file prepare flag + private int seekOnPrepared = 0; // seek to this location once media is prepared /** * Constructor. @@ -82,14 +92,17 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * @param handler The audio handler object * @param id The id of this audio player */ - public AudioPlayer(AudioHandler handler, String id) { + public AudioPlayer(AudioHandler handler, String id, String file) { this.handler = handler; this.id = id; + this.audioFile = file; + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { this.tempFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording.mp3"; } else { this.tempFile = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/tmprecording.mp3"; } + } /** @@ -97,13 +110,13 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On */ public void destroy() { // Stop any play or record - if (this.mPlayer != null) { - if ((this.state == MEDIA_RUNNING) || (this.state == MEDIA_PAUSED)) { - this.mPlayer.stop(); - this.setState(MEDIA_STOPPED); + if (this.player != null) { + if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { + this.player.stop(); + this.setState(STATE.MEDIA_STOPPED); } - this.mPlayer.release(); - this.mPlayer = null; + this.player.release(); + this.player = null; } if (this.recorder != null) { this.stopRecording(); @@ -118,31 +131,34 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * @param file The name of the file */ public void startRecording(String file) { - if (this.mPlayer != null) { + switch (this.mode) { + case PLAY: Log.d(LOG_TAG, "AudioPlayer Error: Can't record in play mode."); this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+MEDIA_ERR_ABORTED+"});"); - } - // Make sure we're not already recording - else if (this.recorder == null) { - this.audioFile = file; - this.recorder = new MediaRecorder(); - this.recorder.setAudioSource(MediaRecorder.AudioSource.MIC); - this.recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); // THREE_GPP); - this.recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); //AMR_NB); - this.recorder.setOutputFile(this.tempFile); - try { - this.recorder.prepare(); - this.recorder.start(); - this.setState(MEDIA_RUNNING); - return; - } catch (IllegalStateException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); + break; + case NONE: + // Make sure we're not already recording + if (this.recorder == null) { + this.audioFile = file; + this.recorder = new MediaRecorder(); + this.recorder.setAudioSource(MediaRecorder.AudioSource.MIC); + this.recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); // THREE_GPP); + this.recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); //AMR_NB); + this.recorder.setOutputFile(this.tempFile); + try { + this.recorder.prepare(); + this.recorder.start(); + this.setState(STATE.MEDIA_RUNNING); + return; + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+MEDIA_ERR_ABORTED+"});"); } - this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+MEDIA_ERR_ABORTED+"});"); - } - else { + break; + case RECORD: Log.d(LOG_TAG, "AudioPlayer Error: Already recording."); this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', "+MEDIA_ERROR+", { \"code\":"+MEDIA_ERR_ABORTED+"});"); } @@ -171,9 +187,9 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On public void stopRecording() { if (this.recorder != null) { try{ - if (this.state == MEDIA_RUNNING) { + if (this.state == STATE.MEDIA_RUNNING) { this.recorder.stop(); - this.setState(MEDIA_STOPPED); + this.setState(STATE.MEDIA_STOPPED); } this.moveFile(this.audioFile); } @@ -183,81 +199,22 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On } } + //========================================================================== + // Playback + //========================================================================== + /** * Start or resume playing audio file. * * @param file The name of the audio file. */ public void startPlaying(String file) { - if (this.recorder != null) { - Log.d(LOG_TAG, "AudioPlayer Error: Can't play in record mode."); - this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});"); - } - - // If this is a new request to play audio, or stopped - else if ((this.mPlayer == null) || (this.state == MEDIA_STOPPED)) { - try { - // If stopped, then reset player - if (this.mPlayer != null) { - this.mPlayer.reset(); - } - // Otherwise, create a new one - else { - this.mPlayer = new MediaPlayer(); - } - this.audioFile = file; - - // If streaming file - if (this.isStreaming(file)) { - this.mPlayer.setDataSource(file); - this.mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - this.setState(MEDIA_STARTING); - this.mPlayer.setOnPreparedListener(this); - this.mPlayer.prepareAsync(); - } - - // If local file - else { - if (file.startsWith("/android_asset/")) { - String f = file.substring(15); - android.content.res.AssetFileDescriptor fd = this.handler.cordova.getActivity().getAssets().openFd(f); - this.mPlayer.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength()); - } - else { - File fp = new File(file); - if (fp.exists()) { - FileInputStream fileInputStream = new FileInputStream(file); - this.mPlayer.setDataSource(fileInputStream.getFD()); - } - else { - this.mPlayer.setDataSource("/sdcard/" + file); - } - } - this.setState(MEDIA_STARTING); - this.mPlayer.setOnPreparedListener(this); - this.mPlayer.prepare(); - - // Get duration - this.duration = getDurationInSeconds(); - } - } catch (Exception e) { - e.printStackTrace(); - this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});"); - } - } - - // If we have already have created an audio player - else { - - // If player has been paused, then resume playback - if ((this.state == MEDIA_PAUSED) || (this.state == MEDIA_STARTING)) { - this.mPlayer.start(); - this.setState(MEDIA_RUNNING); - } - else { - Log.d(LOG_TAG, "AudioPlayer Error: startPlaying() called during invalid state: " + this.state); - this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});"); - } + if (this.readyPlayer(file)) { + this.player.start(); + this.setState(STATE.MEDIA_RUNNING); + } else { + // + this.prepareOnly = false; } } @@ -265,11 +222,14 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * Seek or jump to a new time in the track. */ public void seekToPlaying(int milliseconds) { - if (this.mPlayer != null) { - this.mPlayer.seekTo(milliseconds); + if (this.readyPlayer(this.audioFile)) { + this.player.seekTo(milliseconds); Log.d(LOG_TAG, "Send a onStatus update for the new seek"); this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + milliseconds / 1000.0f + ");"); } + else { + this.seekOnPrepared = milliseconds; + } } /** @@ -278,12 +238,12 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On public void pausePlaying() { // If playing, then pause - if (this.state == MEDIA_RUNNING) { - this.mPlayer.pause(); - this.setState(MEDIA_PAUSED); + if (this.state == STATE.MEDIA_RUNNING) { + this.player.pause(); + this.setState(STATE.MEDIA_PAUSED); } else { - Log.d(LOG_TAG, "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state); + Log.d(LOG_TAG, "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state.ordinal()); this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_NONE_ACTIVE + "});"); } } @@ -292,12 +252,12 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * Stop playing the audio file. */ public void stopPlaying() { - if ((this.state == MEDIA_RUNNING) || (this.state == MEDIA_PAUSED)) { - this.mPlayer.stop(); - this.setState(MEDIA_STOPPED); + if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { + this.player.stop(); + this.setState(STATE.MEDIA_STOPPED); } else { - Log.d(LOG_TAG, "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state); + Log.d(LOG_TAG, "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state.ordinal()); this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_NONE_ACTIVE + "});"); } } @@ -305,10 +265,10 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On /** * Callback to be invoked when playback of a media source has completed. * - * @param mPlayer The MediaPlayer that reached the end of the file + * @param player The MediaPlayer that reached the end of the file */ - public void onCompletion(MediaPlayer mPlayer) { - this.setState(MEDIA_STOPPED); + public void onCompletion(MediaPlayer player) { + this.setState(STATE.MEDIA_STOPPED); } /** @@ -317,8 +277,8 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * @return position in msec or -1 if not playing */ public long getCurrentPosition() { - if ((this.state == MEDIA_RUNNING) || (this.state == MEDIA_PAUSED)) { - int curPos = this.mPlayer.getCurrentPosition(); + if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { + int curPos = this.player.getCurrentPosition(); this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + curPos / 1000.0f + ");"); return curPos; } @@ -359,7 +319,7 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On } // If audio file already loaded and started, then return duration - if (this.mPlayer != null) { + if (this.player != null) { return this.duration; } @@ -377,29 +337,28 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On /** * Callback to be invoked when the media source is ready for playback. * - * @param mPlayer The MediaPlayer that is ready for playback + * @param player The MediaPlayer that is ready for playback */ - public void onPrepared(MediaPlayer mPlayer) { + public void onPrepared(MediaPlayer player) { // Listen for playback completion - this.mPlayer.setOnCompletionListener(this); - + this.player.setOnCompletionListener(this); // If start playing after prepared if (!this.prepareOnly) { - - // Start playing - this.mPlayer.start(); - - // Set player init flag - this.setState(MEDIA_RUNNING); + this.player.start(); + this.setState(STATE.MEDIA_RUNNING); + } else { + this.setState(STATE.MEDIA_STARTING); } - // Save off duration this.duration = getDurationInSeconds(); - this.prepareOnly = false; - + // reset prepare only flag + this.prepareOnly = true; + // seek to any location received while not prepared + this.seekToPlaying(this.seekOnPrepared); + // reset seek location + this.seekOnPrepared = 0; // Send status notification to JavaScript this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_DURATION + "," + this.duration + ");"); - } /** @@ -408,23 +367,23 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * @return length of clip in seconds */ private float getDurationInSeconds() { - return (this.mPlayer.getDuration() / 1000.0f); + return (this.player.getDuration() / 1000.0f); } /** * Callback to be invoked when there has been an error during an asynchronous operation * (other errors will throw exceptions at method call time). * - * @param mPlayer the MediaPlayer the error pertains to + * @param player the MediaPlayer the error pertains to * @param arg1 the type of error that has occurred: (MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_SERVER_DIED) * @param arg2 an extra code, specific to the error. */ - public boolean onError(MediaPlayer mPlayer, int arg1, int arg2) { + public boolean onError(MediaPlayer player, int arg1, int arg2) { Log.d(LOG_TAG, "AudioPlayer.onError(" + arg1 + ", " + arg2 + ")"); // TODO: Not sure if this needs to be sent? - this.mPlayer.stop(); - this.mPlayer.release(); + this.player.stop(); + this.player.release(); // Send error notification to JavaScript this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', { \"code\":" + arg1 + "});"); @@ -436,21 +395,33 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * * @param state */ - private void setState(int state) { + private void setState(STATE state) { if (this.state != state) { - this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + state + ");"); + this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + this.state.ordinal() + ");"); } - this.state = state; } + /** + * Set the mode and send it to JavaScript. + * + * @param state + */ + private void setMode(MODE mode) { + if (this.mode != mode) { + //mode is not part of the expected behaviour, so no notification + //this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");"); + } + this.mode = mode; + } + /** * Get the audio state. * * @return int */ public int getState() { - return this.state; + return this.state.ordinal(); } /** @@ -459,6 +430,121 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * @param volume */ public void setVolume(float volume) { - this.mPlayer.setVolume(volume, volume); + this.player.setVolume(volume, volume); + } + + /** + * attempts to put the player in play mode + * @return true if in playmode, false otherwise + */ + private boolean playMode() { + switch(this.mode) { + case NONE: + this.setMode(MODE.PLAY); + break; + case PLAY: + break; + case RECORD: + Log.d(LOG_TAG, "AudioPlayer Error: Can't play in record mode."); + this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});"); + return false; //player is not ready + } + return true; + } + + /** + * attempts to initialize the media player for playback + * @param file the file to play + * @return false if player not ready, reports if in wrong mode or state + */ + private boolean readyPlayer(String file) { + if (playMode()) { + switch (this.state) { + case MEDIA_NONE: + if (this.player == null) { + this.player = new MediaPlayer(); + } + try { + this.loadAudioFile(file); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + case MEDIA_LOADING: + //cordova js is not aware of MEDIA_LOADING, so we send MEDIA_STARTING insntead + Log.d(LOG_TAG, "AudioPlayer Loading: startPlaying() called during media preparation: " + STATE.MEDIA_STARTING.ordinal()); + this.prepareOnly = false; + return false; + case MEDIA_STARTING: + case MEDIA_RUNNING: + case MEDIA_PAUSED: + return true; + case MEDIA_STOPPED: + //if we are readying the same file + if (this.audioFile.compareTo(file) == 0) { + //reset the audio file + player.seekTo(0); + player.pause(); + return true; + } else { + //reset the player + this.player.reset(); + try { + this.loadAudioFile(file); + } catch (Exception e) { + e.printStackTrace(); + this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});"); + } + //if we had to prepare= the file, we won't be in the correct state for playback + return false; + } + default: + Log.d(LOG_TAG, "AudioPlayer Error: startPlaying() called during invalid state: " + this.state); + this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});"); + } + } + return false; + } + + /** + * load audio file + * @throws IOException + * @throws IllegalStateException + * @throws SecurityException + * @throws IllegalArgumentException + */ + private void loadAudioFile(String file) throws IllegalArgumentException, SecurityException, IllegalStateException, IOException { + if (this.isStreaming(file)) { + this.player.setDataSource(file); + this.player.setAudioStreamType(AudioManager.STREAM_MUSIC); + //if it's a streaming file, play mode is implied + this.setMode(MODE.PLAY); + this.setState(STATE.MEDIA_STARTING); + this.player.setOnPreparedListener(this); + this.player.prepareAsync(); + } + else { + if (file.startsWith("/android_asset/")) { + String f = file.substring(15); + android.content.res.AssetFileDescriptor fd = this.handler.ctx.getActivity().getAssets().openFd(f); + this.player.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength()); + } + else { + File fp = new File(file); + if (fp.exists()) { + FileInputStream fileInputStream = new FileInputStream(file); + this.player.setDataSource(fileInputStream.getFD()); + } + else { + this.player.setDataSource("/sdcard/" + file); + } + } + this.setState(STATE.MEDIA_STARTING); + this.player.setOnPreparedListener(this); + this.player.prepare(); + + // Get duration + this.duration = getDurationInSeconds(); + } } }