diff options
Diffstat (limited to 'plugins/org.apache.cordova.media/src/android/AudioPlayer.java')
| -rw-r--r-- | plugins/org.apache.cordova.media/src/android/AudioPlayer.java | 587 |
1 files changed, 587 insertions, 0 deletions
diff --git a/plugins/org.apache.cordova.media/src/android/AudioPlayer.java b/plugins/org.apache.cordova.media/src/android/AudioPlayer.java new file mode 100644 index 00000000..7524c5b6 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/android/AudioPlayer.java @@ -0,0 +1,587 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova.media; + +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.MediaRecorder; +import android.os.Environment; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * This class implements the audio playback and recording capabilities used by Cordova. + * It is called by the AudioHandler Cordova class. + * Only one file can be played or recorded per class instance. + * + * Local audio files must reside in one of two places: + * android_asset: file name must start with /android_asset/sound.mp3 + * sdcard: file name is just sound.mp3 + */ +public class AudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener { + + // AudioPlayer modes + public enum MODE { NONE, PLAY, RECORD }; + + // AudioPlayer states + public enum STATE { MEDIA_NONE, + MEDIA_STARTING, + MEDIA_RUNNING, + MEDIA_PAUSED, + MEDIA_STOPPED, + MEDIA_LOADING + }; + + private static final String LOG_TAG = "AudioPlayer"; + + // AudioPlayer message ids + private static int MEDIA_STATE = 1; + private static int MEDIA_DURATION = 2; + private static int MEDIA_POSITION = 3; + private static int MEDIA_ERROR = 9; + + // Media error codes + private static int MEDIA_ERR_NONE_ACTIVE = 0; + private static int MEDIA_ERR_ABORTED = 1; +// private static int MEDIA_ERR_NETWORK = 2; +// 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 MODE mode = MODE.NONE; // Playback or Recording mode + private STATE state = 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 MediaRecorder recorder = null; // Audio recording object + private String tempFile = null; // Temporary recording file name + + 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. + * + * @param handler The audio handler object + * @param id The id of this audio player + */ + public AudioPlayer(AudioHandler handler, String id, String file) { + this.handler = handler; + this.id = id; + this.audioFile = file; + this.recorder = new MediaRecorder(); + + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + this.tempFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording.3gp"; + } else { + this.tempFile = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/tmprecording.3gp"; + } + + } + + /** + * Destroy player and stop audio playing or recording. + */ + public void destroy() { + // Stop any play or record + if (this.player != null) { + if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { + this.player.stop(); + this.setState(STATE.MEDIA_STOPPED); + } + this.player.release(); + this.player = null; + } + if (this.recorder != null) { + this.stopRecording(); + this.recorder.release(); + this.recorder = null; + } + } + + /** + * Start recording the specified file. + * + * @param file The name of the file + */ + public void startRecording(String file) { + switch (this.mode) { + case PLAY: + Log.d(LOG_TAG, "AudioPlayer Error: Can't record in play mode."); + sendErrorStatus(MEDIA_ERR_ABORTED); + break; + case NONE: + this.audioFile = file; + 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(); + } + + sendErrorStatus(MEDIA_ERR_ABORTED); + break; + case RECORD: + Log.d(LOG_TAG, "AudioPlayer Error: Already recording."); + sendErrorStatus(MEDIA_ERR_ABORTED); + } + } + + /** + * Save temporary recorded file to specified name + * + * @param file + */ + public void moveFile(String file) { + /* this is a hack to save the file as the specified name */ + File f = new File(this.tempFile); + + if (!file.startsWith("/")) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file; + } else { + file = "/data/data/" + handler.cordova.getActivity().getPackageName() + "/cache/" + file; + } + } + + String logMsg = "renaming " + this.tempFile + " to " + file; + Log.d(LOG_TAG, logMsg); + if (!f.renameTo(new File(file))) Log.e(LOG_TAG, "FAILED " + logMsg); + } + + /** + * Stop recording and save to the file specified when recording started. + */ + public void stopRecording() { + if (this.recorder != null) { + try{ + if (this.state == STATE.MEDIA_RUNNING) { + this.recorder.stop(); + this.setState(STATE.MEDIA_STOPPED); + } + this.recorder.reset(); + this.moveFile(this.audioFile); + } + catch (Exception e) { + e.printStackTrace(); + } + } + } + + //========================================================================== + // Playback + //========================================================================== + + /** + * Start or resume playing audio file. + * + * @param file The name of the audio file. + */ + public void startPlaying(String file) { + if (this.readyPlayer(file) && this.player != null) { + this.player.start(); + this.setState(STATE.MEDIA_RUNNING); + this.seekOnPrepared = 0; //insures this is always reset + } else { + this.prepareOnly = false; + } + } + + /** + * Seek or jump to a new time in the track. + */ + public void seekToPlaying(int milliseconds) { + if (this.readyPlayer(this.audioFile)) { + this.player.seekTo(milliseconds); + Log.d(LOG_TAG, "Send a onStatus update for the new seek"); + sendStatusChange(MEDIA_POSITION, null, (milliseconds / 1000.0f)); + } + else { + this.seekOnPrepared = milliseconds; + } + } + + /** + * Pause playing. + */ + public void pausePlaying() { + + // If playing, then pause + if (this.state == STATE.MEDIA_RUNNING && this.player != null) { + this.player.pause(); + this.setState(STATE.MEDIA_PAUSED); + } + else { + Log.d(LOG_TAG, "AudioPlayer Error: pausePlaying() called during invalid state: " + this.state.ordinal()); + sendErrorStatus(MEDIA_ERR_NONE_ACTIVE); + } + } + + /** + * Stop playing the audio file. + */ + public void stopPlaying() { + if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { + this.player.pause(); + this.player.seekTo(0); + Log.d(LOG_TAG, "stopPlaying is calling stopped"); + this.setState(STATE.MEDIA_STOPPED); + } + else { + Log.d(LOG_TAG, "AudioPlayer Error: stopPlaying() called during invalid state: " + this.state.ordinal()); + sendErrorStatus(MEDIA_ERR_NONE_ACTIVE); + } + } + + /** + * Callback to be invoked when playback of a media source has completed. + * + * @param player The MediaPlayer that reached the end of the file + */ + public void onCompletion(MediaPlayer player) { + Log.d(LOG_TAG, "on completion is calling stopped"); + this.setState(STATE.MEDIA_STOPPED); + } + + /** + * Get current position of playback. + * + * @return position in msec or -1 if not playing + */ + public long getCurrentPosition() { + if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) { + int curPos = this.player.getCurrentPosition(); + sendStatusChange(MEDIA_POSITION, null, (curPos / 1000.0f)); + return curPos; + } + else { + return -1; + } + } + + /** + * Determine if playback file is streaming or local. + * It is streaming if file name starts with "http://" + * + * @param file The file name + * @return T=streaming, F=local + */ + public boolean isStreaming(String file) { + if (file.contains("http://") || file.contains("https://")) { + return true; + } + else { + return false; + } + } + + /** + * Get the duration of the audio file. + * + * @param file The name of the audio file. + * @return The duration in msec. + * -1=can't be determined + * -2=not allowed + */ + public float getDuration(String file) { + + // Can't get duration of recording + if (this.recorder != null) { + return (-2); // not allowed + } + + // If audio file already loaded and started, then return duration + if (this.player != null) { + return this.duration; + } + + // If no player yet, then create one + else { + this.prepareOnly = true; + this.startPlaying(file); + + // This will only return value for local, since streaming + // file hasn't been read yet. + return this.duration; + } + } + + /** + * Callback to be invoked when the media source is ready for playback. + * + * @param player The MediaPlayer that is ready for playback + */ + public void onPrepared(MediaPlayer player) { + // Listen for playback completion + this.player.setOnCompletionListener(this); + // seek to any location received while not prepared + this.seekToPlaying(this.seekOnPrepared); + // If start playing after prepared + if (!this.prepareOnly) { + this.player.start(); + this.setState(STATE.MEDIA_RUNNING); + this.seekOnPrepared = 0; //reset only when played + } else { + this.setState(STATE.MEDIA_STARTING); + } + // Save off duration + this.duration = getDurationInSeconds(); + // reset prepare only flag + this.prepareOnly = true; + + // Send status notification to JavaScript + sendStatusChange(MEDIA_DURATION, null, this.duration); + } + + /** + * By default Android returns the length of audio in mills but we want seconds + * + * @return length of clip in seconds + */ + private float getDurationInSeconds() { + 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 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 player, int arg1, int arg2) { + Log.d(LOG_TAG, "AudioPlayer.onError(" + arg1 + ", " + arg2 + ")"); + + // TODO: Not sure if this needs to be sent? + this.player.stop(); + this.player.release(); + + // Send error notification to JavaScript + sendErrorStatus(arg1); + return false; + } + + /** + * Set the state and send it to JavaScript. + * + * @param state + */ + private void setState(STATE state) { + if (this.state != state) { + sendStatusChange(MEDIA_STATE, null, (float)state.ordinal()); + } + this.state = state; + } + + /** + * Set the mode and send it to JavaScript. + * + * @param mode + */ + private void setMode(MODE mode) { + if (this.mode != mode) { + //mode is not part of the expected behavior, so no notification + //this.handler.webView.sendJavascript("cordova.require('org.apache.cordova.media.Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");"); + } + this.mode = mode; + } + + /** + * Get the audio state. + * + * @return int + */ + public int getState() { + return this.state.ordinal(); + } + + /** + * Set the volume for audio player + * + * @param volume + */ + public void setVolume(float 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."); + sendErrorStatus(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) { + sendErrorStatus(MEDIA_ERR_ABORTED); + } + return false; + case MEDIA_LOADING: + //cordova js is not aware of MEDIA_LOADING, so we send MEDIA_STARTING instead + 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) { + sendErrorStatus(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); + sendErrorStatus(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.cordova.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()); + fileInputStream.close(); + } + else { + this.player.setDataSource(Environment.getExternalStorageDirectory().getPath() + "/" + file); + } + } + this.setState(STATE.MEDIA_STARTING); + this.player.setOnPreparedListener(this); + this.player.prepare(); + + // Get duration + this.duration = getDurationInSeconds(); + } + } + + private void sendErrorStatus(int errorCode) { + sendStatusChange(MEDIA_ERROR, errorCode, null); + } + + private void sendStatusChange(int messageType, Integer additionalCode, Float value) { + + if (additionalCode != null && value != null) { + throw new IllegalArgumentException("Only one of additionalCode or value can be specified, not both"); + } + + JSONObject statusDetails = new JSONObject(); + try { + statusDetails.put("id", this.id); + statusDetails.put("msgType", messageType); + if (additionalCode != null) { + JSONObject code = new JSONObject(); + code.put("code", additionalCode.intValue()); + statusDetails.put("value", code); + } + else if (value != null) { + statusDetails.put("value", value.floatValue()); + } + } catch (JSONException e) { + Log.e(LOG_TAG, "Failed to create status details", e); + } + + this.handler.sendEventMessage("status", statusDetails); + } +} |
