diff options
| author | Arjun Roychowdhury <pliablepixels@gmail.com> | 2015-10-28 13:31:08 -0400 |
|---|---|---|
| committer | Arjun Roychowdhury <pliablepixels@gmail.com> | 2015-10-28 13:31:08 -0400 |
| commit | e76b54b8e3f3d7299e5a921dcecc9dc442b278e1 (patch) | |
| tree | cbfa4476dae975ed443361e37acef0ab0a45bfa1 /plugins/org.apache.cordova.media/src/android | |
| parent | 3cb5cda7583566cec66aabf3543b0d876a864369 (diff) | |
media plugin
Diffstat (limited to 'plugins/org.apache.cordova.media/src/android')
3 files changed, 1035 insertions, 0 deletions
diff --git a/plugins/org.apache.cordova.media/src/android/AudioHandler.java b/plugins/org.apache.cordova.media/src/android/AudioHandler.java new file mode 100644 index 00000000..f06b75a4 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/android/AudioHandler.java @@ -0,0 +1,410 @@ +/* + 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 org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaResourceApi; + +import android.content.Context; +import android.media.AudioManager; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; + +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; + +/** + * This class called by CordovaActivity to play and record audio. + * The file can be local or over a network using http. + * + * Audio formats supported (tested): + * .mp3, .wav + * + * 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 AudioHandler extends CordovaPlugin { + + public static String TAG = "AudioHandler"; + HashMap<String, AudioPlayer> players; // Audio player object + ArrayList<AudioPlayer> pausedForPhone; // Audio players that were paused when phone call came in + private int origVolumeStream = -1; + private CallbackContext messageChannel; + + /** + * Constructor. + */ + public AudioHandler() { + this.players = new HashMap<String, AudioPlayer>(); + this.pausedForPhone = new ArrayList<AudioPlayer>(); + } + + /** + * Executes the request and returns PluginResult. + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackContext The callback context used when calling back into JavaScript. + * @return A PluginResult object with a status and message. + */ + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + CordovaResourceApi resourceApi = webView.getResourceApi(); + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + + if (action.equals("startRecordingAudio")) { + String target = args.getString(1); + String fileUriStr; + try { + Uri targetUri = resourceApi.remapUri(Uri.parse(target)); + fileUriStr = targetUri.toString(); + } catch (IllegalArgumentException e) { + fileUriStr = target; + } + this.startRecordingAudio(args.getString(0), FileHelper.stripFileProtocol(fileUriStr)); + } + else if (action.equals("stopRecordingAudio")) { + this.stopRecordingAudio(args.getString(0)); + } + else if (action.equals("startPlayingAudio")) { + String target = args.getString(1); + String fileUriStr; + try { + Uri targetUri = resourceApi.remapUri(Uri.parse(target)); + fileUriStr = targetUri.toString(); + } catch (IllegalArgumentException e) { + fileUriStr = target; + } + this.startPlayingAudio(args.getString(0), FileHelper.stripFileProtocol(fileUriStr)); + } + else if (action.equals("seekToAudio")) { + this.seekToAudio(args.getString(0), args.getInt(1)); + } + else if (action.equals("pausePlayingAudio")) { + this.pausePlayingAudio(args.getString(0)); + } + else if (action.equals("stopPlayingAudio")) { + this.stopPlayingAudio(args.getString(0)); + } else if (action.equals("setVolume")) { + try { + this.setVolume(args.getString(0), Float.parseFloat(args.getString(1))); + } catch (NumberFormatException nfe) { + //no-op + } + } else if (action.equals("getCurrentPositionAudio")) { + float f = this.getCurrentPositionAudio(args.getString(0)); + callbackContext.sendPluginResult(new PluginResult(status, f)); + return true; + } + else if (action.equals("getDurationAudio")) { + float f = this.getDurationAudio(args.getString(0), args.getString(1)); + callbackContext.sendPluginResult(new PluginResult(status, f)); + return true; + } + else if (action.equals("create")) { + String id = args.getString(0); + String src = FileHelper.stripFileProtocol(args.getString(1)); + getOrCreatePlayer(id, src); + } + else if (action.equals("release")) { + boolean b = this.release(args.getString(0)); + callbackContext.sendPluginResult(new PluginResult(status, b)); + return true; + } + else if (action.equals("messageChannel")) { + messageChannel = callbackContext; + return true; + } + else { // Unrecognized action. + return false; + } + + callbackContext.sendPluginResult(new PluginResult(status, result)); + + return true; + } + + /** + * Stop all audio players and recorders. + */ + public void onDestroy() { + if (!players.isEmpty()) { + onLastPlayerReleased(); + } + for (AudioPlayer audio : this.players.values()) { + audio.destroy(); + } + this.players.clear(); + } + + /** + * Stop all audio players and recorders on navigate. + */ + @Override + public void onReset() { + onDestroy(); + } + + /** + * Called when a message is sent to plugin. + * + * @param id The message id + * @param data The message data + * @return Object to stop propagation or null + */ + public Object onMessage(String id, Object data) { + + // If phone message + if (id.equals("telephone")) { + + // If phone ringing, then pause playing + if ("ringing".equals(data) || "offhook".equals(data)) { + + // Get all audio players and pause them + for (AudioPlayer audio : this.players.values()) { + if (audio.getState() == AudioPlayer.STATE.MEDIA_RUNNING.ordinal()) { + this.pausedForPhone.add(audio); + audio.pausePlaying(); + } + } + + } + + // If phone idle, then resume playing those players we paused + else if ("idle".equals(data)) { + for (AudioPlayer audio : this.pausedForPhone) { + audio.startPlaying(null); + } + this.pausedForPhone.clear(); + } + } + return null; + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + private AudioPlayer getOrCreatePlayer(String id, String file) { + AudioPlayer ret = players.get(id); + if (ret == null) { + if (players.isEmpty()) { + onFirstPlayerCreated(); + } + ret = new AudioPlayer(this, id, file); + players.put(id, ret); + } + return ret; + } + + /** + * Release the audio player instance to save memory. + * @param id The id of the audio player + */ + private boolean release(String id) { + AudioPlayer audio = players.remove(id); + if (audio == null) { + return false; + } + if (players.isEmpty()) { + onLastPlayerReleased(); + } + audio.destroy(); + return true; + } + + /** + * Start recording and save the specified file. + * @param id The id of the audio player + * @param file The name of the file + */ + public void startRecordingAudio(String id, String file) { + AudioPlayer audio = getOrCreatePlayer(id, file); + audio.startRecording(file); + } + + /** + * Stop recording and save to the file specified when recording started. + * @param id The id of the audio player + */ + public void stopRecordingAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.stopRecording(); + } + } + + /** + * Start or resume playing audio file. + * @param id The id of the audio player + * @param file The name of the audio file. + */ + public void startPlayingAudio(String id, String file) { + AudioPlayer audio = getOrCreatePlayer(id, file); + audio.startPlaying(file); + } + + /** + * Seek to a location. + * @param id The id of the audio player + * @param milliseconds int: number of milliseconds to skip 1000 = 1 second + */ + public void seekToAudio(String id, int milliseconds) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.seekToPlaying(milliseconds); + } + } + + /** + * Pause playing. + * @param id The id of the audio player + */ + public void pausePlayingAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.pausePlaying(); + } + } + + /** + * Stop playing the audio file. + * @param id The id of the audio player + */ + public void stopPlayingAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.stopPlaying(); + } + } + + /** + * Get current position of playback. + * @param id The id of the audio player + * @return position in msec + */ + public float getCurrentPositionAudio(String id) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + return (audio.getCurrentPosition() / 1000.0f); + } + return -1; + } + + /** + * Get the duration of the audio file. + * @param id The id of the audio player + * @param file The name of the audio file. + * @return The duration in msec. + */ + public float getDurationAudio(String id, String file) { + AudioPlayer audio = getOrCreatePlayer(id, file); + return audio.getDuration(file); + } + + /** + * Set the audio device to be used for playback. + * + * @param output 1=earpiece, 2=speaker + */ + @SuppressWarnings("deprecation") + public void setAudioOutputDevice(int output) { + AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE); + if (output == 2) { + audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_SPEAKER, AudioManager.ROUTE_ALL); + } + else if (output == 1) { + audiMgr.setRouting(AudioManager.MODE_NORMAL, AudioManager.ROUTE_EARPIECE, AudioManager.ROUTE_ALL); + } + else { + System.out.println("AudioHandler.setAudioOutputDevice() Error: Unknown output device."); + } + } + + /** + * Get the audio device to be used for playback. + * + * @return 1=earpiece, 2=speaker + */ + @SuppressWarnings("deprecation") + public int getAudioOutputDevice() { + AudioManager audiMgr = (AudioManager) this.cordova.getActivity().getSystemService(Context.AUDIO_SERVICE); + if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_EARPIECE) { + return 1; + } + else if (audiMgr.getRouting(AudioManager.MODE_NORMAL) == AudioManager.ROUTE_SPEAKER) { + return 2; + } + else { + return -1; + } + } + + /** + * Set the volume for an audio device + * + * @param id The id of the audio player + * @param volume Volume to adjust to 0.0f - 1.0f + */ + public void setVolume(String id, float volume) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.setVolume(volume); + } else { + System.out.println("AudioHandler.setVolume() Error: Unknown Audio Player " + id); + } + } + + private void onFirstPlayerCreated() { + origVolumeStream = cordova.getActivity().getVolumeControlStream(); + cordova.getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC); + } + + private void onLastPlayerReleased() { + if (origVolumeStream != -1) { + cordova.getActivity().setVolumeControlStream(origVolumeStream); + origVolumeStream = -1; + } + } + + void sendEventMessage(String action, JSONObject actionData) { + JSONObject message = new JSONObject(); + try { + message.put("action", action); + if (actionData != null) { + message.put(action, actionData); + } + } catch (JSONException e) { + Log.e(TAG, "Failed to create event message", e); + } + + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, message); + pluginResult.setKeepCallback(true); + if (messageChannel != null) { + messageChannel.sendPluginResult(pluginResult); + } + } +} 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); + } +} diff --git a/plugins/org.apache.cordova.media/src/android/FileHelper.java b/plugins/org.apache.cordova.media/src/android/FileHelper.java new file mode 100644 index 00000000..e20752c6 --- /dev/null +++ b/plugins/org.apache.cordova.media/src/android/FileHelper.java @@ -0,0 +1,38 @@ +/* + 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.net.Uri; + +public class FileHelper { + + /** + * Removes the "file://" prefix from the given URI string, if applicable. + * If the given URI string doesn't have a "file://" prefix, it is returned unchanged. + * + * @param uriString the URI string to operate on + * @return a path without the "file://" prefix + */ + public static String stripFileProtocol(String uriString) { + if (uriString.startsWith("file://")) { + return Uri.parse(uriString).getPath(); + } + return uriString; + } +} |
